feat: add brand filtering functionality and integrate with localStorage

This commit is contained in:
sHa
2025-05-29 13:00:27 +03:00
parent 436d6847b2
commit 52a4292079
3 changed files with 421 additions and 71 deletions

View File

@@ -21,6 +21,7 @@
let mq; let mq;
let allTags = []; let allTags = [];
let selectedTags = []; let selectedTags = [];
let selectedBrands = [];
let tagDropdownOpen = false; let tagDropdownOpen = false;
let showModal = false; let showModal = false;
let selectedLogo = null; let selectedLogo = null;
@@ -44,7 +45,10 @@
logo.tags.some((tag) => logo.tags.some((tag) =>
selectedTags.includes(typeof tag === "string" ? tag : tag.text), selectedTags.includes(typeof tag === "string" ? tag : tag.text),
)); ));
return matchesSearch && matchesTags; const matchesBrands =
!selectedBrands.length ||
(logo.brand && selectedBrands.includes(logo.brand));
return matchesSearch && matchesTags && matchesBrands;
}); });
window.appData.displayLogos = window.appData.displayLogos =
@@ -95,6 +99,7 @@
searchQuery, searchQuery,
allTags: [], allTags: [],
selectedTags: [], selectedTags: [],
selectedBrands: [],
tagDropdownOpen, tagDropdownOpen,
compactMode, compactMode,
setSearchQuery, setSearchQuery,
@@ -111,6 +116,8 @@
setCompactMode, setCompactMode,
onCopy: copyUrl, onCopy: copyUrl,
onDownload: downloadLogo, onDownload: downloadLogo,
addBrand,
removeBrand,
}; };
} }
@@ -149,6 +156,7 @@
searchQuery, searchQuery,
allTags, allTags,
selectedTags, selectedTags,
selectedBrands,
tagDropdownOpen, tagDropdownOpen,
compactMode, compactMode,
setSearchQuery, setSearchQuery,
@@ -165,6 +173,8 @@
setCompactMode, setCompactMode,
onCopy: copyUrl, onCopy: copyUrl,
onDownload: downloadLogo, onDownload: downloadLogo,
addBrand,
removeBrand,
}; };
console.log( console.log(
"App: Updated window.appData after loading with", "App: Updated window.appData after loading with",
@@ -223,6 +233,21 @@
localStorage.removeItem("selectedTags"); localStorage.removeItem("selectedTags");
} }
} }
// Restore selected brands from localStorage
const savedBrands = localStorage.getItem("selectedBrands");
if (savedBrands) {
try {
const parsedBrands = JSON.parse(savedBrands);
if (Array.isArray(parsedBrands)) {
selectedBrands = parsedBrands;
console.log("App: Restored selectedBrands from localStorage:", selectedBrands);
}
} catch (error) {
console.error("App: Error parsing saved brands:", error);
localStorage.removeItem("selectedBrands");
}
}
}); });
// Make sure to apply theme whenever it changes // Make sure to apply theme whenever it changes
@@ -243,6 +268,7 @@
).values(), ).values(),
).sort((a, b) => a.text.localeCompare(b.text)); ).sort((a, b) => a.text.localeCompare(b.text));
// Update the filtering logic to include brand filtering in the reactive statement
$: filteredLogos = logos.filter((logo) => { $: filteredLogos = logos.filter((logo) => {
const matchesSearch = const matchesSearch =
logo.name.toLowerCase().includes(searchQuery.toLowerCase()) || logo.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -254,7 +280,10 @@
logo.tags.some((tag) => logo.tags.some((tag) =>
selectedTags.includes(typeof tag === "string" ? tag : tag.text), selectedTags.includes(typeof tag === "string" ? tag : tag.text),
)); ));
return matchesSearch && matchesTags; const matchesBrands =
!selectedBrands.length ||
(logo.brand && selectedBrands.includes(logo.brand));
return matchesSearch && matchesTags && matchesBrands;
}); });
$: displayLogos = $: displayLogos =
@@ -287,6 +316,7 @@
searchQuery, searchQuery,
allTags, allTags,
selectedTags, selectedTags,
selectedBrands,
tagDropdownOpen, tagDropdownOpen,
compactMode, compactMode,
setSearchQuery, setSearchQuery,
@@ -301,6 +331,8 @@
addTag, addTag,
removeTag, removeTag,
toggleTag, toggleTag,
addBrand,
removeBrand,
getTagObj, getTagObj,
closeDropdown, closeDropdown,
setCompactMode, setCompactMode,
@@ -490,6 +522,48 @@
} }
} }
function addBrand(brand) {
console.log("App: Adding brand:", brand);
if (!selectedBrands.includes(brand)) {
selectedBrands = [...selectedBrands, brand];
localStorage.setItem("selectedBrands", JSON.stringify(selectedBrands));
console.log("App: Updated selectedBrands:", selectedBrands);
// Update window.appData immediately
if (typeof window !== "undefined" && window.appData) {
window.appData.selectedBrands = [...selectedBrands];
console.log("App: Updated selectedBrands in window.appData");
// Update filtered logos immediately
updateFilteredLogosImmediate();
}
}
// Close dropdown after adding brand
tagDropdownOpen = false;
// Also update the dropdown state in window.appData
if (typeof window !== "undefined" && window.appData) {
window.appData.tagDropdownOpen = false;
}
}
function removeBrand(brand) {
console.log("App: Removing brand:", brand);
selectedBrands = selectedBrands.filter((b) => b !== brand);
localStorage.setItem("selectedBrands", JSON.stringify(selectedBrands));
console.log("App: Updated selectedBrands:", selectedBrands);
// Update window.appData immediately
if (typeof window !== "undefined" && window.appData) {
window.appData.selectedBrands = [...selectedBrands];
console.log("App: Updated selectedBrands in window.appData");
// Update filtered logos immediately
updateFilteredLogosImmediate();
}
}
// Helper function to immediately update filtered/display logos in window.appData // Helper function to immediately update filtered/display logos in window.appData
function updateFilteredLogosImmediate() { function updateFilteredLogosImmediate() {
if (typeof window !== "undefined" && window.appData) { if (typeof window !== "undefined" && window.appData) {
@@ -504,7 +578,10 @@
logo.tags.some((tag) => logo.tags.some((tag) =>
selectedTags.includes(typeof tag === "string" ? tag : tag.text), selectedTags.includes(typeof tag === "string" ? tag : tag.text),
)); ));
return matchesSearch && matchesTags; const matchesBrands =
!selectedBrands.length ||
(logo.brand && selectedBrands.includes(logo.brand));
return matchesSearch && matchesTags && matchesBrands;
}); });
window.appData.displayLogos = window.appData.displayLogos =

View File

@@ -13,16 +13,20 @@
export let setSearchQuery; export let setSearchQuery;
export let allTags = []; export let allTags = [];
export let selectedTags = []; export let selectedTags = [];
export let selectedBrands = [];
export let tagDropdownOpen = false; export let tagDropdownOpen = false;
export let toggleDropdown = () => console.log("toggleDropdown not provided"); export let toggleDropdown = () => console.log("toggleDropdown not provided");
export let addTag = () => console.log("addTag not provided"); export let addTag = () => console.log("addTag not provided");
export let removeTag = () => console.log("removeTag not provided"); export let removeTag = () => console.log("removeTag not provided");
export let addBrand = () => console.log("addBrand not provided");
export let removeBrand = () => console.log("removeBrand not provided");
export let getTagObj = (tag) => ({ text: tag }); export let getTagObj = (tag) => ({ text: tag });
export let compactMode = false; export let compactMode = false;
export let setCompactMode = () => {}; export let setCompactMode = () => {};
let searchInput; // Reference to the search input element let searchInput; // Reference to the search input element
let tagSearchQuery = ""; // Search query for filtering tags let tagSearchQuery = ""; // Search query for filtering tags
let activeTab = "categories"; // "categories" or "brands"
// Filter available tags based on search query // Filter available tags based on search query
$: filteredAvailableTags = allTags.filter( $: filteredAvailableTags = allTags.filter(
@@ -36,6 +40,20 @@
t.text.toLowerCase().includes(tagSearchQuery.toLowerCase()), t.text.toLowerCase().includes(tagSearchQuery.toLowerCase()),
); );
// Compute all unique brands
$: allBrands = Array.from(
new Set(
logos
.map((logo) => logo.brand)
.filter((brand) => brand && brand.trim() !== "")
)
).sort();
// Filter brands based on search query
$: filteredAllBrands = allBrands.filter((brand) =>
brand.toLowerCase().includes(tagSearchQuery.toLowerCase())
);
onMount(() => { onMount(() => {
// Add global keydown listener for the / hotkey // Add global keydown listener for the / hotkey
function handleKeydown(event) { function handleKeydown(event) {
@@ -74,6 +92,43 @@
console.log("Header: Search query set to:", searchQuery); console.log("Header: Search query set to:", searchQuery);
} }
function toggleBrand(brand) {
if (selectedBrands.includes(brand)) {
selectedBrands = selectedBrands.filter(b => b !== brand);
} else {
selectedBrands = [...selectedBrands, brand];
}
// Save to localStorage
localStorage.setItem("selectedBrands", JSON.stringify(selectedBrands));
// Update URL parameters
updateFilterParams();
}
function updateFilterParams() {
const params = new URLSearchParams(window.location.search);
// Update tags
if (selectedTags.length > 0) {
params.set("tags", selectedTags.join(","));
} else {
params.delete("tags");
}
// Update brands
if (selectedBrands.length > 0) {
params.set("brands", selectedBrands.join(","));
} else {
params.delete("brands");
}
const newUrl = window.location.pathname + (params.toString() ? "?" + params.toString() : "");
history.replaceState(null, "", newUrl);
}
</script> </script>
<header class="main-header"> <header class="main-header">
@@ -229,9 +284,9 @@
fill="currentColor" fill="currentColor"
/> />
</svg> </svg>
{#if selectedTags.length + (compactMode ? 1 : 0) > 0} {#if selectedTags.length + selectedBrands.length + (compactMode ? 1 : 0) > 0}
<span class="filter-count" <span class="filter-count"
>{selectedTags.length + (compactMode ? 1 : 0)}</span >{selectedTags.length + selectedBrands.length + (compactMode ? 1 : 0)}</span
> >
{/if} {/if}
</button> </button>
@@ -265,15 +320,30 @@
</button> </button>
</div> </div>
{#if filteredAvailableTags.length > 0 || tagSearchQuery} {#if filteredAvailableTags.length > 0 || tagSearchQuery || allBrands.length > 0}
<div class="filter-separator"></div> <div class="filter-separator"></div>
<div class="filter-tags-section"> <div class="filter-tabs-section">
<div class="filter-section-title">Categories</div> <div class="filter-tabs">
<button
class="filter-tab"
class:active={activeTab === "categories"}
on:click={() => activeTab = "categories"}
>
Categories
</button>
<button
class="filter-tab"
class:active={activeTab === "brands"}
on:click={() => activeTab = "brands"}
>
Brands
</button>
</div>
<div class="tags-search-bar"> <div class="tags-search-bar">
<input <input
type="text" type="text"
placeholder="Search categories..." placeholder={activeTab === "categories" ? "Search categories..." : "Search brands..."}
bind:value={tagSearchQuery} bind:value={tagSearchQuery}
class="tags-search-input" class="tags-search-input"
/> />
@@ -281,7 +351,7 @@
<button <button
class="tags-search-clear" class="tags-search-clear"
on:click|stopPropagation={() => (tagSearchQuery = "")} on:click|stopPropagation={() => (tagSearchQuery = "")}
aria-label="Clear tag search" aria-label="Clear search"
> >
<svg <svg
width="14" width="14"
@@ -301,6 +371,7 @@
{/if} {/if}
</div> </div>
{#if activeTab === "categories"}
{#if filteredAllTags.length > 0} {#if filteredAllTags.length > 0}
<div class="filter-tags-list"> <div class="filter-tags-list">
{#each filteredAllTags as tagObj} {#each filteredAllTags as tagObj}
@@ -341,20 +412,63 @@
{:else} {:else}
<div class="no-tags"> <div class="no-tags">
{tagSearchQuery {tagSearchQuery
? "No tags match your search" ? "No categories match your search"
: "No available tags"} : "No available categories"}
</div> </div>
{/if} {/if}
{:else}
{#if filteredAllBrands.length > 0}
<div class="filter-tags-list">
{#each filteredAllBrands as brand}
{@const isSelected = selectedBrands.includes(brand)}
<button
class="filter-tag-item filter-brand-item"
class:selected={isSelected}
on:click={() =>
isSelected
? removeBrand(brand)
: addBrand(brand)}
aria-label={isSelected
? `Remove brand: ${brand}`
: `Add brand: ${brand}`}
>
<span class="tag-icon">
<span class="permanent-icon">
{#if isSelected}
✔︎
{/if}
</span>
<span class="hover-icon">
{#if isSelected}
{:else}
✔︎
{/if}
</span>
</span>
<span class="brand-text">{brand}</span>
</button>
{/each}
</div>
{:else}
<div class="no-tags">
{tagSearchQuery
? "No brands match your search"
: "No available brands"}
</div>
{/if}
{/if}
</div> </div>
{/if} {/if}
{#if selectedTags.length > 0 || compactMode} {#if selectedTags.length > 0 || selectedBrands.length > 0 || compactMode}
<div class="filter-separator"></div> <div class="filter-separator"></div>
<div class="clear-all-section"> <div class="clear-all-section">
<button <button
class="clear-all-button" class="clear-all-button"
on:click={() => { on:click={() => {
selectedTags.forEach(tag => removeTag(tag)); selectedTags.forEach(tag => removeTag(tag));
selectedBrands.forEach(brand => removeBrand(brand));
if (compactMode) setCompactMode(false); if (compactMode) setCompactMode(false);
}} }}
aria-label="Clear all filters" aria-label="Clear all filters"
@@ -379,6 +493,17 @@
</button> </button>
{/each} {/each}
{#each selectedBrands as brand}
<button
class="selected-brand"
aria-label={`Remove brand: ${brand}`}
on:click={() => removeBrand(brand)}
>
{brand}
<span class="close">&times;</span>
</button>
{/each}
{#if compactMode} {#if compactMode}
<button <button
class="compact-indicator" class="compact-indicator"
@@ -500,10 +625,7 @@
/><rect /><rect
x="4" x="4"
y="13" y="13"
width="12" width="12" height="2" fill="currentColor" /></svg
height="2"
fill="currentColor"
/></svg
> >
</button> </button>
</div> </div>
@@ -773,17 +895,32 @@
margin: 0.5rem 0; margin: 0.5rem 0;
} }
.filter-tags-section { .filter-tabs {
display: flex; display: flex;
flex-direction: column; margin-bottom: 0.5rem;
gap: 0.5rem;
} }
.filter-section-title { .filter-tab {
flex: 1;
background: none;
border: none;
padding: 0.4rem 0.8rem;
font-size: 0.85em; font-size: 0.85em;
font-weight: 600; font-weight: 500;
cursor: pointer;
transition: all 0.2s;
color: var(--color-text); color: var(--color-text);
opacity: 0.8; opacity: 0.7;
border-radius: 0;
}
.filter-tab.active {
background: var(--color-card);
opacity: 1;
}
.filter-tab:hover:not(.active) {
opacity: 0.9;
} }
.tags-search-bar { .tags-search-bar {
@@ -860,6 +997,15 @@
opacity: 1; opacity: 1;
} }
.filter-brand-item.selected {
background: none;
color: var(--color-text);
}
.filter-brand-item.selected:hover {
background: var(--color-border);
}
.tag-icon { .tag-icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -898,6 +1044,11 @@
font-weight: 500; font-weight: 500;
} }
.brand-text {
font-size: 0.85em;
font-weight: 500;
}
.no-tags { .no-tags {
color: #888; color: #888;
font-size: 0.85em; font-size: 0.85em;
@@ -999,4 +1150,71 @@
font-size: 0.85em; font-size: 0.85em;
font-weight: 500; font-weight: 500;
} }
.filter-tabs {
display: flex;
border-bottom: 1px solid var(--color-border);
margin-bottom: 0.75rem;
}
.filter-tab {
background: none;
border: none;
color: var(--color-text);
cursor: pointer;
padding: 0.5rem 0.75rem;
border-bottom: 2px solid transparent;
transition: color 0.2s, border-color 0.2s;
font-size: 0.9rem;
opacity: 0.7;
border-radius: 0;
}
.filter-tab:hover {
opacity: 1;
}
.filter-tab.active {
color: var(--color-accent);
border-bottom-color: var(--color-accent);
opacity: 1;
font-weight: 500;
box-shadow: none;
}
.selected-brand {
background: #2196F3;
color: #fff;
border: none;
border-radius: 8px;
padding: 0.2em 0.8em 0.2em 0.8em;
font-size: 0.85em;
font-weight: 500;
letter-spacing: 0.02em;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.3em;
opacity: 1;
transition:
background 0.2s,
color 0.2s;
}
.selected-brand:hover {
background: #1976D2;
}
.selected-brand .close {
margin-left: 0.4em;
font-size: 1.1em;
font-weight: bold;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.selected-brand .close:hover {
opacity: 1;
}
</style> </style>

View File

@@ -14,8 +14,43 @@
let tagDropdownOpen = false; let tagDropdownOpen = false;
let selectedTags = []; let selectedTags = [];
let allTags = []; let allTags = [];
let selectedBrands = [];
let addBrand = () => {};
let removeBrand = () => {};
let setTheme = () => {}; let setTheme = () => {};
$: ({
logos = [],
filteredLogos = [],
displayLogos = [],
theme = "system",
effectiveTheme = "light",
viewMode = "grid",
searchQuery = "",
allTags = [],
selectedTags = [],
selectedBrands = [],
tagDropdownOpen = false,
compactMode = false,
setSearchQuery = () => {},
setGridView = () => {},
setListView = () => {},
setCompactView = () => {},
setTheme = () => {},
toggleDropdown = () => {},
addTag = () => {},
removeTag = () => {},
addBrand = () => {},
removeBrand = () => {},
toggleTag = () => {},
getTagObj = () => ({ text: "" }),
closeDropdown = () => {},
setCompactMode = () => {},
onCopy = () => {},
onDownload = () => {},
} = appData || {});
onMount(() => { onMount(() => {
if (typeof window !== 'undefined' && window.appData) { if (typeof window !== 'undefined' && window.appData) {
logos = window.appData.displayLogos || []; logos = window.appData.displayLogos || [];
@@ -27,6 +62,7 @@
tagDropdownOpen = window.appData.tagDropdownOpen || false; tagDropdownOpen = window.appData.tagDropdownOpen || false;
selectedTags = window.appData.selectedTags || []; selectedTags = window.appData.selectedTags || [];
allTags = window.appData.allTags || []; allTags = window.appData.allTags || [];
selectedBrands = window.appData.selectedBrands || [];
if (window.appData.setTheme && typeof window.appData.setTheme === 'function') { if (window.appData.setTheme && typeof window.appData.setTheme === 'function') {
console.log("Home: Found window.appData.setTheme function"); console.log("Home: Found window.appData.setTheme function");
@@ -35,6 +71,18 @@
console.warn("Home: window.appData.setTheme not found or not a function"); console.warn("Home: window.appData.setTheme not found or not a function");
} }
if (window.appData.addBrand && typeof window.appData.addBrand === 'function') {
addBrand = window.appData.addBrand;
}
if (window.appData.removeBrand && typeof window.appData.removeBrand === 'function') {
removeBrand = window.appData.removeBrand;
}
if (window.appData.selectedBrands) {
selectedBrands = window.appData.selectedBrands;
}
// Set up reactivity with window.appData // Set up reactivity with window.appData
const interval = setInterval(() => { const interval = setInterval(() => {
if (window.appData) { if (window.appData) {
@@ -56,6 +104,10 @@
if (window.appData.allTags) { if (window.appData.allTags) {
allTags = [...window.appData.allTags]; allTags = [...window.appData.allTags];
} }
if (window.appData.selectedBrands) {
selectedBrands = [...window.appData.selectedBrands];
}
} }
}, 100); }, 100);
@@ -134,25 +186,28 @@
<div class="container"> <div class="container">
<Header <Header
{searchQuery} {logos}
{setSearchQuery} displayLogos={logos}
{viewMode}
{theme} {theme}
{setTheme} {setTheme}
{viewMode}
{setGridView} {setGridView}
{setListView} {setListView}
{setCompactView} {setCompactView}
logos={allLogos} {searchQuery}
displayLogos={logos} {setSearchQuery}
allTags={allTags}
selectedTags={selectedTags}
selectedBrands={selectedBrands}
{tagDropdownOpen}
{toggleDropdown} {toggleDropdown}
{addTag} {addTag}
{removeTag} {removeTag}
allTags={allTags} addBrand={addBrand}
selectedTags={selectedTags} removeBrand={removeBrand}
tagDropdownOpen={tagDropdownOpen} getTagObj={(tag) => (window.appData?.getTagObj ? window.appData.getTagObj(tag) : {text: tag})}
{compactMode} {compactMode}
{setCompactMode} {setCompactMode}
getTagObj={(tag) => (window.appData?.getTagObj ? window.appData.getTagObj(tag) : {text: tag})}
/> />
<main> <main>