You want to build something like …
To be able to test the setup → navigate to → JS-Fiddle
Copy and paste the code to the HTML-AREA and run it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CATALOG-APP</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
body {
font-family: 'Inter', sans-serif;
background-color: #0a0f18;
background-image: radial-gradient(circle at top, #141c2a 0%, #0a0f18 70%);
color: #e2e8f0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.holographic-container {
background: rgba(23, 33, 50, 0.4);
backdrop-filter: blur(10px);
border: 1px solid rgba(45, 55, 72, 0.5);
box-shadow: 0 0 40px rgba(59, 130, 246, 0.1);
transition: box-shadow 0.3s ease;
}
.holographic-container:hover {
box-shadow: 0 0 60px rgba(59, 130, 246, 0.2);
}
.card-highlight-glow {
transition: transform 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease;
position: relative;
overflow: hidden;
}
.card-highlight-glow:hover {
transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);
}
.glow-sci-fi { box-shadow: 0 0 20px rgba(59, 130, 246, 0.6); }
.bg-highlight-sci-fi { background-color: rgba(59, 130, 246, 0.2); }
.glow-fantasy { box-shadow: 0 0 20px rgba(139, 92, 246, 0.6); }
.bg-highlight-fantasy { background-color: rgba(139, 92, 246, 0.2); }
.glow-action { box-shadow: 0 0 20px rgba(239, 68, 68, 0.6); }
.bg-highlight-action { background-color: rgba(239, 68, 68, 0.2); }
.glow-thriller { box-shadow: 0 0 20px rgba(249, 115, 22, 0.6); }
.bg-highlight-thriller { background-color: rgba(249, 115, 22, 0.2); }
.glow-noir { box-shadow: 0 0 20px rgba(75, 85, 99, 0.6); }
.bg-highlight-noir { background-color: rgba(75, 85, 99, 0.2); }
.glow-mystery { box-shadow: 0 0 20px rgba(99, 102, 241, 0.6); }
.bg-highlight-mystery { background-color: rgba(99, 102, 241, 0.2); }
.glow-adventure { box-shadow: 0 0 20px rgba(16, 185, 129, 0.6); }
.bg-highlight-adventure { background-color: rgba(16, 185, 129, 0.2); }
.glow-superhero { box-shadow: 0 0 20px rgba(236, 72, 153, 0.6); }
.bg-highlight-superhero { background-color: rgba(236, 72, 153, 0.2); }
.glow-family { box-shadow: 0 0 20px rgba(251, 191, 36, 0.6); }
.bg-highlight-family { background-color: rgba(251, 191, 36, 0.2); }
.glow-crime { box-shadow: 0 0 20px rgba(30, 41, 59, 0.6); }
.bg-highlight-crime { background-color: rgba(30, 41, 59, 0.2); }
.glow-drama { box-shadow: 0 0 20px rgba(52, 211, 153, 0.6); }
.bg-highlight-drama { background-color: rgba(52, 211, 153, 0.2); }
.glow-comedy { box-shadow: 0 0 20px rgba(124, 58, 237, 0.6); }
.bg-highlight-comedy { background-color: rgba(124, 58, 237, 0.2); }
.glow-animation { box-shadow: 0 0 20px rgba(249, 168, 212, 0.6); }
.bg-highlight-animation { background-color: rgba(249, 168, 212, 0.2); }
.glow-musical { box-shadow: 0 0 20px rgba(129, 140, 248, 0.6); }
.bg-highlight-musical { background-color: rgba(129, 140, 248, 0.2); }
.glow-romance { box-shadow: 0 0 20px rgba(244, 114, 182, 0.6); }
.bg-highlight-romance { background-color: rgba(244, 114, 182, 0.2); }
.glow-biography { box-shadow: 0 0 20px rgba(100, 116, 139, 0.6); }
.bg-highlight-biography { background-color: rgba(100, 116, 139, 0.2); }
.glow-history { box-shadow: 0 0 20px rgba(220, 38, 38, 0.6); }
.bg-highlight-history { background-color: rgba(220, 38, 38, 0.2); }
.glow-war { box-shadow: 0 0 20px rgba(22, 163, 74, 0.6); }
.bg-highlight-war { background-color: rgba(22, 163, 74, 0.2); }
.glow-horror { box-shadow: 0 0 20px rgba(0, 0, 0, 0.6); }
.bg-highlight-horror { background-color: rgba(0, 0, 0, 0.2); }
.glow-western { box-shadow: 0 0 20px rgba(120, 113, 108, 0.6); }
.bg-highlight-western { background-color: rgba(120, 113, 108, 0.2); }
.glow-satire { box-shadow: 0 0 20px rgba(15, 23, 42, 0.6); }
.bg-highlight-satire { background-color: rgba(15, 23, 42, 0.2); }
.glow-supernatural { box-shadow: 0 0 20px rgba(255, 255, 255, 0.6); }
.bg-highlight-supernatural { background-color: rgba(255, 255, 255, 0.1); }
.input-style {
background-color: rgba(30, 41, 59, 0.7);
border: 1px solid rgba(71, 85, 105, 0.5);
color: #e2e8f0;
transition: all 0.2s ease;
}
.input-style:focus {
outline: none;
border-color: #38bdf8;
box-shadow: 0 0 0 2px rgba(14, 165, 233, 0.5);
}
.btn-style {
background: rgba(59, 130, 246, 0.3);
border: 1px solid rgba(59, 130, 246, 0.5);
color: #fff;
transition: all 0.2s ease;
box-shadow: 0 0 10px rgba(59, 130, 246, 0.3);
}
.btn-style:hover {
background: rgba(59, 130, 246, 0.5);
box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
}
.toggle-switch-label {
background: rgba(55, 65, 81, 0.7);
}
.toggle-switch-input:checked + .toggle-switch-label {
background: #2563eb;
}
.json-viewer-container {
background-color: rgba(15, 23, 42, 0.9);
color: #34d399;
border: 1px solid #1e293b;
}
.modal-content {
background-color: rgba(15, 23, 42, 0.9);
border: 1px solid rgba(45, 55, 72, 0.5);
backdrop-filter: blur(5px);
color: #e2e8f0;
}
.modal-content h3 {
color: #38bdf8;
}
.modal-content .category-tag {
background-color: rgba(59, 130, 246, 0.2);
color: #93c5fd;
border: 1px solid rgba(59, 130, 246, 0.5);
}
</style>
</head>
<body class="p-4 md:p-8 flex items-center justify-center">
<div class="container mx-auto max-w-5xl holographic-container rounded-3xl p-6 md:p-10 flex flex-col gap-8">
<h1 class="text-4xl font-extrabold text-cyan-400 text-center font-[Orbitron] tracking-wider">CATALOG-APP</h1>
<div class="flex flex-col md:flex-row gap-4 md:gap-6 items-center">
<div class="relative flex-grow w-full">
<input type="text" id="searchInput" placeholder="Search by title or description..." class="w-full p-4 pl-12 rounded-xl focus:outline-none input-style text-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<div class="flex flex-col md:flex-row gap-4 w-full md:w-auto">
<button id="resetButton" class="font-semibold py-3 px-6 rounded-xl shadow-lg w-full md:w-auto btn-style bg-gray-700 hover:bg-gray-800">
Reset
</button>
<button id="showJsonBtn" class="font-semibold py-3 px-6 rounded-xl shadow-lg w-full md:w-auto btn-style">
Toggle JSON
</button>
</div>
</div>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-center gap-4 text-gray-400">
<span class="text-sm font-medium">Multi-select</span>
<label class="toggle-switch-container">
<input type="checkbox" id="selectionModeSwitch" class="toggle-switch-input">
<span class="toggle-switch-label"></span>
</label>
<span class="text-sm font-medium">Single-select</span>
</div>
<div id="filterButtons" class="flex flex-wrap gap-2 md:gap-3 justify-center">
</div>
</div>
<div id="jsonOutput" class="json-viewer-container hidden p-6 rounded-2xl">
<pre><code id="jsonCode" class="text-sm"></code></pre>
</div>
<div class="relative min-h-[300px]">
<div id="loadingIndicator" class="absolute inset-0 flex items-center justify-center bg-gray-800 bg-opacity-75 z-10 hidden rounded-2xl">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-cyan-500"></div>
</div>
<div id="noResultsMessage" class="absolute inset-0 flex items-center justify-center p-4 z-0 hidden">
<p class="text-2xl text-gray-500 font-medium text-center">No items match your search criteria.</p>
</div>
<div id="catalogContainer" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
</div>
</div>
</div>
<div id="itemModal" class="modal fixed inset-0 z-50 overflow-auto bg-black bg-opacity-80 flex items-center justify-center opacity-0 pointer-events-none transition-opacity duration-300">
<div class="modal-content transform scale-95 transition-transform duration-300 w-11/12 md:w-3/4 lg:w-1/2 p-8 rounded-3xl relative">
<button id="closeModalBtn" class="absolute top-4 right-4 text-gray-500 hover:text-gray-300 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<h3 id="modalTitle" class="text-3xl font-extrabold text-cyan-400 mb-2 font-[Orbitron]"></h3>
<div id="modalCategories" class="flex flex-wrap gap-2 mb-4"></div>
<p id="modalDescription" class="text-md text-gray-300"></p>
</div>
</div>
<script>
const allItemsData = [
{ id: 1, title: 'The Matrix', categories: ['sci-fi', 'action', 'thriller'], description: 'A computer hacker learns about the true nature of his reality.' },
{ id: 2, title: 'Lord of the Rings', categories: ['fantasy', 'adventure'], description: 'A meek Hobbit from the Shire sets out to destroy the One Ring.' },
{ id: 3, title: 'Blade Runner', categories: ['sci-fi', 'noir', 'mystery'], description: 'A bounty hunter must hunt down four replicants.' },
{ id: 4, title: 'Dune', categories: ['sci-fi', 'adventure'], description: 'A noble family becomes embroiled in a war for control of a valuable asset.' },
{ id: 5, title: 'Avengers: Endgame', categories: ['action', 'superhero'], description: 'Adrift in space, Tony Stark sends a message to Pepper Potts.' },
{ id: 6, title: 'The Hobbit', categories: ['fantasy', 'adventure', 'family'], description: 'A young hobbit is swept into an epic quest.' },
{ id: 7, title: 'Star Wars: A New Hope', categories: ['sci-fi', 'action', 'adventure'], description: 'Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, and a wookiee.' },
{ id: 8, title: 'Game of Thrones', categories: ['fantasy', 'drama', 'action'], description: 'Nine noble families fight for control over the lands of Westeros.' },
{ id: 9, title: 'Interstellar', categories: ['sci-fi', 'drama', 'adventure'], description: 'A team of explorers travel through a wormhole in space in an attempt to ensure humanity\'s survival.' },
{ id: 10, title: 'Pulp Fiction', categories: ['crime', 'thriller', 'comedy'], description: 'The lives of two mob hitmen, a boxer, a gangster\'s wife, and a pair of diner bandits intertwine.' },
{ id: 11, title: 'Jurassic Park', categories: ['sci-fi', 'adventure', 'thriller'], description: 'A pragmatic paleontologist tours an island theme park of cloned dinosaurs.' },
{ id: 12, title: 'The Godfather', categories: ['crime', 'drama'], description: 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire.' },
{ id: 13, title: 'Inception', categories: ['sci-fi', 'action', 'thriller'], description: 'A thief who steals corporate secrets through the use of dream-sharing technology.' },
{ id: 14, title: 'The Dark Knight', categories: ['action', 'superhero', 'crime'], description: 'Batman faces a new super-villain, the Joker, who wreaks havoc on Gotham.' },
{ id: 15, title: 'Fight Club', categories: ['drama', 'thriller'], description: 'An insomniac office worker looking for a way to change his life crosses paths with a devil-may-care soap maker.' },
{ id: 16, title: 'Forrest Gump', categories: ['drama', 'romance'], description: 'The presidencies of Kennedy and Johnson, the Vietnam War, and other history told from the perspective of an Alabama man with an IQ of 75.' },
{ id: 17, title: 'The Lion King', categories: ['animation', 'musical', 'family'], description: 'A young lion prince flees his kingdom only to learn about responsibility.' },
{ id: 18, title: 'Schindler\'s List', categories: ['biography', 'drama', 'history'], description: 'A German industrialist saves more than a thousand Jewish refugees from the Holocaust.' },
{ id: 19, title: 'Gladiator', categories: ['action', 'drama', 'history'], description: 'A Roman general is betrayed and his family murdered by an emperor\'s corrupt son.' },
{ id: 20, title: 'The Departed', categories: ['crime', 'thriller'], description: 'An undercover cop and a mole in the police force try to identify each other.' },
{ id: 21, title: 'Toy Story', categories: ['animation', 'comedy', 'family'], description: 'A cowboy doll is profoundly threatened and jealous when a new spaceman toy supplants him.' },
{ id: 22, title: 'Mad Max: Fury Road', categories: ['action', 'sci-fi', 'post-apocalyptic'], description: 'In a post-apocalyptic wasteland, a woman rebels against a tyrannical ruler.' },
{ id: 23, title: 'Coco', categories: ['animation', 'fantasy', 'musical'], description: 'Aspiring musician Miguel, confronted with his family\'s ancestral ban on music, enters the Land of the Dead.' },
{ id: 24, title: 'Get Out', categories: ['horror', 'thriller', 'mystery'], description: 'A young African-American man visits his white girlfriend\'s parents for the first time.' },
{ id: 25, title: 'Back to the Future', categories: ['sci-fi', 'comedy'], description: 'Marty McFly is accidentally sent thirty years into the past in a time-traveling DeLorean.' },
{ id: 26, title: 'The Social Network', categories: ['biography', 'drama'], description: 'Harvard student Mark Zuckerberg creates the social networking site that would become known as Facebook.' },
{ id: 27, title: 'Finding Nemo', categories: ['animation', 'adventure', 'family'], description: 'A clownfish goes on a journey to find his missing son.' },
{ id: 28, title: 'The Silence of the Lambs', categories: ['thriller', 'crime'], description: 'A young F.B.I. cadet must receive the help of an incarcerated cannibal.' },
{ id: 29, title: 'Django Unchained', categories: ['western', 'action'], description: 'A freed slave sets out to rescue his wife from a brutal Mississippi plantation owner.' },
{ id: 30, title: 'Inglourious Basterds', categories: ['war', 'action', 'comedy'], description: 'A group of Jewish-American soldiers is assigned to assassinate Nazi leaders.' },
{ id: 31, title: 'Parasite', categories: ['thriller', 'drama'], description: 'The poor Kim family con their way into becoming the servants of the wealthy Park family.' },
{ id: 32, title: 'Arrival', categories: ['sci-fi', 'drama'], description: 'Linguist Louise Banks is recruited by the military to communicate with alien lifeforms.' },
{ id: 33, title: 'Goodfellas', categories: ['biography', 'crime', 'drama'], description: 'The story of Henry Hill and his life in the mob, covering his relationship with his wife Karen.' },
{ id: 34, title: 'La La Land', categories: ['musical', 'romance', 'drama'], description: 'A jazz pianist and an aspiring actress meet and fall in love in Los Angeles.' },
{ id: 35, title: 'Spirited Away', categories: ['animation', 'fantasy', 'adventure'], description: 'During her family\'s move to the suburbs, a sullen 10-year-old girl wanders into a world ruled by gods.' },
{ id: 36, title: 'E.T. the Extra-Terrestrial', categories: ['sci-fi', 'family', 'adventure'], description: 'A troubled child summons the courage to help a friendly alien escape Earth.' },
{ id: 37, title: 'Joker', categories: ['crime', 'drama', 'thriller'], description: 'A mentally troubled comedian embarks on a downward spiral that leads to a life of crime.' },
{ id: 38, title: 'The Grand Budapest Hotel', categories: ['comedy', 'adventure'], description: 'The adventures of a legendary concierge at a famous European hotel between the first and second World Wars.' },
{ id: 39, title: 'Saving Private Ryan', categories: ['war', 'drama'], description: 'Following the Normandy Landings, a group of U.S. soldiers goes behind enemy lines.' },
{ id: 40, title: '1917', categories: ['war', 'drama', 'action'], description: 'Two young British soldiers are given an impossible mission during World War I.' },
{ id: 41, title: 'Donnie Darko', categories: ['sci-fi', 'mystery', 'coming-of-age'], description: 'A troubled teenager is plagued by visions of a man in a rabbit suit who manipulates him to commit a series of crimes.' },
{ id: 42, title: 'The Shining', categories: ['horror', 'thriller', 'mystery'], description: 'A family heads to an isolated hotel for the winter where a sinister presence influences the father.' },
{ id: 43, title: '2001: A Space Odyssey', categories: ['sci-fi', 'adventure'], description: 'Humanity finds a mysterious, obviously artificial, object buried beneath the lunar surface.' },
{ id: 44, title: 'Eternal Sunshine of the Spotless Mind', categories: ['romance', 'sci-fi', 'drama'], description: 'A couple undergoes a procedure to erase each other from their memories.' },
{ id: 45, title: 'No Country for Old Men', categories: ['crime', 'thriller'], description: 'Violence and mayhem ensue after a hunter stumbles upon a drug deal gone wrong.' },
{ id: 46, title: 'The Shawshank Redemption', categories: ['drama'], description: 'Two imprisoned men bond over a number of years, finding solace and eventual redemption.' },
{ id: 47, title: 'Fight Club', categories: ['drama', 'thriller', 'action'], description: 'An insomniac office worker looking for a way to change his life crosses paths with a devil-may-care soap maker.' },
{ id: 48, title: 'Pulp Fiction', categories: ['crime', 'drama', 'comedy'], description: 'The lives of two mob hitmen, a boxer, a gangster\'s wife, and a pair of diner bandits intertwine.' },
{ id: 49, title: 'The Departed', categories: ['crime', 'drama', 'thriller'], description: 'An undercover cop and a mole in the police force try to identify each other.' },
{ id: 50, title: 'Inception', categories: ['sci-fi', 'action', 'thriller'], description: 'A thief who steals corporate secrets through the use of dream-sharing technology.' },
{ id: 51, title: 'Forrest Gump', categories: ['drama', 'romance'], description: 'The presidencies of Kennedy and Johnson, the Vietnam War, and other history from the perspective of an Alabama man with an IQ of 75.' }
];
const state = {
catalogItems: [],
selectedFilters: new Set(),
searchTerm: '',
isSingleSelect: false,
};
const catalogContainer = document.getElementById('catalogContainer');
const searchInput = document.getElementById('searchInput');
const filterButtonsContainer = document.getElementById('filterButtons');
const loadingIndicator = document.getElementById('loadingIndicator');
const noResultsMessage = document.getElementById('noResultsMessage');
const selectionModeSwitch = document.getElementById('selectionModeSwitch');
const resetButton = document.getElementById('resetButton');
const showJsonBtn = document.getElementById('showJsonBtn');
const jsonOutputDiv = document.getElementById('jsonOutput');
const jsonCodeElement = document.getElementById('jsonCode');
const itemModal = document.getElementById('itemModal');
const closeModalBtn = document.getElementById('closeModalBtn');
const modalTitle = document.getElementById('modalTitle');
const modalDescription = document.getElementById('modalDescription');
const modalCategories = document.getElementById('modalCategories');
const categoryHighlightClasses = {
'sci-fi': { glow: 'glow-sci-fi', bg: 'bg-highlight-sci-fi', tagBg: 'bg-highlight-sci-fi' },
'fantasy': { glow: 'glow-fantasy', bg: 'bg-highlight-fantasy', tagBg: 'bg-highlight-fantasy' },
'action': { glow: 'glow-action', bg: 'bg-highlight-action', tagBg: 'bg-highlight-action' },
'thriller': { glow: 'glow-thriller', bg: 'bg-highlight-thriller', tagBg: 'bg-highlight-thriller' },
'noir': { glow: 'glow-noir', bg: 'bg-highlight-noir', tagBg: 'bg-highlight-noir' },
'mystery': { glow: 'glow-mystery', bg: 'bg-highlight-mystery', tagBg: 'bg-highlight-mystery' },
'adventure': { glow: 'glow-adventure', bg: 'bg-highlight-adventure', tagBg: 'bg-highlight-adventure' },
'superhero': { glow: 'glow-superhero', bg: 'bg-highlight-superhero', tagBg: 'bg-highlight-superhero' },
'family': { glow: 'glow-family', bg: 'bg-highlight-family', tagBg: 'bg-highlight-family' },
'crime': { glow: 'glow-crime', bg: 'bg-highlight-crime', tagBg: 'bg-highlight-crime' },
'drama': { glow: 'glow-drama', bg: 'bg-highlight-drama', tagBg: 'bg-highlight-drama' },
'comedy': { glow: 'glow-comedy', bg: 'bg-highlight-comedy', tagBg: 'bg-highlight-comedy' },
'animation': { glow: 'glow-animation', bg: 'bg-highlight-animation', tagBg: 'bg-highlight-animation' },
'musical': { glow: 'glow-musical', bg: 'bg-highlight-musical', tagBg: 'bg-highlight-musical' },
'romance': { glow: 'glow-romance', bg: 'bg-highlight-romance', tagBg: 'bg-highlight-romance' },
'biography': { glow: 'glow-biography', bg: 'bg-highlight-biography', tagBg: 'bg-highlight-biography' },
'history': { glow: 'glow-history', bg: 'bg-highlight-history', tagBg: 'bg-highlight-history' },
'war': { glow: 'glow-war', bg: 'bg-highlight-war', tagBg: 'bg-highlight-war' },
'horror': { glow: 'glow-horror', bg: 'bg-highlight-horror', tagBg: 'bg-highlight-horror' },
'western': { glow: 'glow-western', bg: 'bg-highlight-western', tagBg: 'bg-highlight-western' },
'satire': { glow: 'glow-satire', bg: 'bg-highlight-satire', tagBg: 'bg-highlight-satire' },
'supernatural': { glow: 'glow-supernatural', bg: 'bg-highlight-supernatural', tagBg: 'bg-highlight-supernatural' },
'post-apocalyptic': { glow: 'glow-sci-fi', bg: 'bg-highlight-sci-fi', tagBg: 'bg-highlight-sci-fi' },
'coming-of-age': { glow: 'glow-drama', bg: 'bg-highlight-drama', tagBg: 'bg-highlight-drama' },
};
function showLoading() {
loadingIndicator.classList.remove('hidden');
catalogContainer.classList.add('hidden');
noResultsMessage.classList.add('hidden');
jsonOutputDiv.classList.add('hidden');
}
function hideLoading() {
loadingIndicator.classList.add('hidden');
catalogContainer.classList.remove('hidden');
}
function showNoResults() {
catalogContainer.classList.add('hidden');
noResultsMessage.classList.remove('hidden');
}
function openModal(item) {
modalTitle.textContent = item.title;
modalDescription.textContent = item.description;
modalCategories.innerHTML = '';
item.categories.forEach(category => {
const span = document.createElement('span');
const catClasses = categoryHighlightClasses[category] ? `bg-opacity-20 ${categoryHighlightClasses[category].tagBg}` : 'bg-gray-600 bg-opacity-20';
span.className = `category-tag text-xs font-semibold px-3 py-1 rounded-full ${catClasses}`;
span.textContent = category;
modalCategories.appendChild(span);
});
itemModal.classList.add('active');
itemModal.classList.remove('opacity-0', 'pointer-events-none');
setTimeout(() => {
itemModal.querySelector('.modal-content').classList.remove('scale-95');
}, 10);
}
function closeModal() {
itemModal.querySelector('.modal-content').classList.add('scale-95');
itemModal.classList.add('opacity-0', 'pointer-events-none');
}
function renderCatalog(items) {
catalogContainer.innerHTML = '';
if (items.length === 0) {
showNoResults();
return;
}
hideLoading();
const hasActiveFilters = state.selectedFilters.size > 0;
items.forEach(item => {
const itemCard = document.createElement('div');
itemCard.dataset.itemId = item.id;
let cardClasses = ['bg-gray-800', 'bg-opacity-40', 'rounded-3xl', 'shadow-xl', 'p-6', 'flex', 'flex-col', 'gap-3', 'card-highlight-glow', 'cursor-pointer', 'border', 'border-transparent'];
if (hasActiveFilters) {
const matchingCategory = item.categories.find(cat => state.selectedFilters.has(cat));
if (matchingCategory && categoryHighlightClasses[matchingCategory]) {
const { glow, bg } = categoryHighlightClasses[matchingCategory];
cardClasses.push(glow, bg, 'border-cyan-500', 'border-opacity-50');
}
}
itemCard.className = cardClasses.join(' ');
itemCard.innerHTML = `
<h3 class="text-xl font-bold text-cyan-400">${item.title}</h3>
<p class="text-sm text-gray-400 line-clamp-3">${item.description}</p>
<div class="flex flex-wrap gap-2 mt-auto">
${item.categories.map(category => {
const catHighlight = categoryHighlightClasses[category];
const tagBgClass = catHighlight ? `${catHighlight.tagBg} bg-opacity-20 text-white border-${catHighlight.tagBg.split('-')[1]}-400` : 'bg-gray-600 bg-opacity-20 text-gray-200 border-gray-500';
return `<span class="category-tag text-xs font-semibold px-3 py-1 rounded-full ${tagBgClass} border">${category}</span>`;
}).join('')}
</div>
`;
catalogContainer.appendChild(itemCard);
});
}
function renderFilterButtons() {
const allCategories = new Set();
allItemsData.forEach(item => {
item.categories.forEach(category => allCategories.add(category));
});
const sortedCategories = Array.from(allCategories).sort();
filterButtonsContainer.innerHTML = `
<button data-filter="all" class="filter-btn btn-style bg-cyan-600 hover:bg-cyan-700 text-white font-semibold py-2 px-6 rounded-full shadow-md transition-all duration-200">All</button>
`;
sortedCategories.forEach(category => {
const button = document.createElement('button');
button.className = 'filter-btn bg-gray-600 text-gray-200 font-medium py-2 px-6 rounded-full shadow-sm hover:bg-gray-700 transition-colors duration-200';
button.dataset.filter = category;
button.textContent = category.charAt(0).toUpperCase() + category.slice(1);
filterButtonsContainer.appendChild(button);
});
updateFilterButtonStyles();
}
function updateFilterButtonStyles() {
document.querySelectorAll('.filter-btn').forEach(btn => {
const filter = btn.dataset.filter;
if (filter === 'all') {
if (state.selectedFilters.size === 0) {
btn.classList.remove('bg-gray-600', 'text-gray-200', 'hover:bg-gray-700');
btn.classList.add('bg-cyan-600', 'text-white', 'hover:bg-cyan-700', 'btn-style');
} else {
btn.classList.remove('bg-cyan-600', 'text-white', 'hover:bg-cyan-700', 'btn-style');
btn.classList.add('bg-gray-600', 'text-gray-200', 'hover:bg-gray-700');
}
} else {
if (state.selectedFilters.has(filter)) {
btn.classList.remove('bg-gray-600', 'text-gray-200', 'hover:bg-gray-700');
btn.classList.add('bg-cyan-600', 'text-white', 'hover:bg-cyan-700', 'btn-style');
} else {
btn.classList.remove('bg-cyan-600', 'text-white', 'hover:bg-gray-700', 'btn-style');
btn.classList.add('bg-gray-600', 'text-gray-200', 'hover:bg-gray-700');
}
}
});
}
async function fetchCatalogItems() {
showLoading();
await new Promise(resolve => setTimeout(resolve, 800));
state.catalogItems = allItemsData;
updateCatalog();
}
function updateCatalog() {
const searchTerm = state.searchTerm.toLowerCase();
const filteredItems = state.catalogItems.filter(item => {
const searchMatch = item.title.toLowerCase().includes(searchTerm) || item.description.toLowerCase().includes(searchTerm);
const categoryMatch = state.selectedFilters.size === 0 || item.categories.some(category => state.selectedFilters.has(category));
return searchMatch && categoryMatch;
});
renderCatalog(filteredItems);
}
searchInput.addEventListener('input', (event) => {
state.searchTerm = event.target.value;
updateCatalog();
});
filterButtonsContainer.addEventListener('click', (event) => {
const button = event.target.closest('.filter-btn');
if (!button) return;
const filter = button.dataset.filter;
if (filter === 'all') {
state.selectedFilters.clear();
} else if (state.isSingleSelect) {
state.selectedFilters.clear();
state.selectedFilters.add(filter);
} else {
if (state.selectedFilters.has(filter)) {
state.selectedFilters.delete(filter);
} else {
state.selectedFilters.add(filter);
}
}
updateFilterButtonStyles();
updateCatalog();
});
selectionModeSwitch.addEventListener('change', (event) => {
state.isSingleSelect = event.target.checked;
if (state.isSingleSelect && state.selectedFilters.size > 1) {
const firstFilter = state.selectedFilters.values().next().value;
state.selectedFilters.clear();
state.selectedFilters.add(firstFilter);
}
updateFilterButtonStyles();
updateCatalog();
});
resetButton.addEventListener('click', () => {
state.searchTerm = '';
state.selectedFilters.clear();
state.isSingleSelect = false;
searchInput.value = '';
selectionModeSwitch.checked = false;
jsonOutputDiv.classList.add('hidden');
updateFilterButtonStyles();
updateCatalog();
});
showJsonBtn.addEventListener('click', () => {
const isHidden = jsonOutputDiv.classList.contains('hidden');
if (isHidden) {
const appState = {
searchTerm: state.searchTerm,
selectedFilters: Array.from(state.selectedFilters),
isSingleSelect: state.isSingleSelect,
};
const jsonString = JSON.stringify(appState, null, 2);
jsonCodeElement.textContent = jsonString;
jsonOutputDiv.classList.remove('hidden');
catalogContainer.classList.add('hidden');
noResultsMessage.classList.add('hidden');
} else {
jsonOutputDiv.classList.add('hidden');
if (state.catalogItems.length > 0) {
catalogContainer.classList.remove('hidden');
} else {
noResultsMessage.classList.remove('hidden');
}
}
});
catalogContainer.addEventListener('click', (event) => {
const card = event.target.closest('div[data-item-id]');
if (!card) return;
const itemId = parseInt(card.dataset.itemId);
const item = allItemsData.find(i => i.id === itemId);
if (item) {
openModal(item);
}
});
closeModalBtn.addEventListener('click', closeModal);
itemModal.addEventListener('click', (event) => {
if (event.target === itemModal) {
closeModal();
}
});
document.addEventListener('DOMContentLoaded', () => {
renderFilterButtons();
fetchCatalogItems();
});
</script>
</body>
</html>