7 Commits

Author SHA1 Message Date
pheralb 803e13001a 🛠️ Refactor header component; streamline button classes and improve layout consistency
📦 Build / 🛠️ Build app (push) Has been cancelled
🧑‍🚀 Check / ⚙️ Linting (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
2025-09-01 17:44:16 +01:00
pheralb 77356d3215 🛠️ Refactor SVG filtering logic in load function; ensure base data is used for `searchWithFuse` 2025-09-01 11:49:09 +01:00
pheralb 1591ea3146 🛠️ Refactor svgCard and index files; streamline image handling and improve type definitions 2025-09-01 11:48:51 +01:00
pheralb 2a38b834c3 🛠️ Refactor search handling in search.svelte and +page.svelte; implement custom addParams and deleteParam utility 2025-09-01 11:34:24 +01:00
pheralb e6d441e9f2 🛠️ Refactor load function in +page.ts to use getSvgsByCategory for improved category filtering and sorting logic 2025-09-01 11:27:53 +01:00
pheralb bc34bdc904 🛠️ Update PageCard component to include container and content card classes for improved styling 2025-09-01 11:27:33 +01:00
pheralb 2692c7d34d 🛠️ Rename type tCategory to Category for consistency in type definitions 2025-09-01 11:27:22 +01:00
12 changed files with 200 additions and 159 deletions
+18 -12
View File
@@ -18,11 +18,6 @@
}
let { githubStars }: HeaderProps = $props();
const headerItemsClasses = cn(
buttonVariants({ variant: "ghost" }),
"hover:bg-neutral-200 dark:hover:bg-neutral-800",
);
</script>
<header
@@ -39,24 +34,35 @@
</a>
<SvglVersion />
</div>
<div class="flex h-8 items-center">
<div class="flex items-center space-x-0.5">
<div class="flex h-5 items-center space-x-2.5">
<div class="flex items-center space-x-1.5">
<a
target="_blank"
title="X/Twitter"
href={globals.twitterUrl}
class={cn(headerItemsClasses, "h-9 w-9")}
class={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"hover:bg-neutral-200 dark:hover:bg-neutral-800",
)}
>
<Twitter size={18} />
</a>
<ModeToggle className={cn(headerItemsClasses, "h-9 w-9")} />
<ModeToggle
className={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"hover:bg-neutral-200 dark:hover:bg-neutral-800",
)}
/>
</div>
<Separator orientation="vertical" class="mx-2 h-8" />
<Separator orientation="vertical" />
<a
target="_blank"
title="GitHub Repository"
href={globals.githubUrl}
class={cn(headerItemsClasses, "h-9 w-fit")}
class={cn(
buttonVariants({ variant: "ghost" }),
"w-fit hover:bg-neutral-200 dark:hover:bg-neutral-800",
)}
>
<Github size={20} />
<span class="text-neutral-600 dark:text-neutral-400">
@@ -65,7 +71,7 @@
: githubStars.toLocaleString()}
</span>
</a>
<Separator orientation="vertical" class="mr-3 ml-2" />
<Separator orientation="vertical" />
<a
target="_blank"
href={globals.submitUrl}
+5 -15
View File
@@ -2,12 +2,9 @@
import { cn } from "@/utils/cn";
import { onMount } from "svelte";
import { page } from "$app/state";
import { goto } from "$app/navigation";
import { addParams } from "@/utils/searchParams";
import SearchIcon from "@lucide/svelte/icons/search";
import CommandIcon from "@lucide/svelte/icons/command";
import { SvelteURLSearchParams } from "svelte/reactivity";
interface Props {
searchValue: string;
@@ -19,19 +16,12 @@
let inputElement: HTMLInputElement;
const onInput = (event: Event) => {
const param = "search";
const value = (event.target as HTMLInputElement).value;
onSearch(value);
const params = new SvelteURLSearchParams(page.url.searchParams);
if (value) {
params.set(param, value);
} else {
params.delete(param);
}
goto(`?${params.toString()}`, {
keepFocus: true,
noScroll: true,
replaceState: true,
addParams({
params: {
search: value,
},
});
};
+21 -57
View File
@@ -1,7 +1,9 @@
<script lang="ts">
import type { iSVG } from "@/types/svg";
import { cn } from "@/utils/cn";
import { mode } from "mode-watcher";
import { getSvgImgUrl } from "@/data";
// Icons:
import XIcon from "@lucide/svelte/icons/x";
@@ -32,7 +34,6 @@
// States:
let wordmarkSvg = $state<boolean>(false);
let moreTagsOptions = $state<boolean>(false);
let changeThemeMode = $state<boolean>(false);
// Icon Stroke & Size:
let iconStroke = 1.8;
@@ -69,47 +70,17 @@
<AddToFavorite svg={svgInfo} />
</div>
<!-- Image -->
{#if wordmarkSvg == true && svgInfo.wordmark !== undefined}
{#if changeThemeMode}
<img
class={cn("block", globalImageStyles)}
src={typeof svgInfo.wordmark !== "string"
? mode.current === "dark"
? svgInfo.wordmark?.light || ""
: svgInfo.wordmark?.dark || ""
: svgInfo.wordmark || ""}
alt={svgInfo.title}
title={svgInfo.title}
loading="lazy"
/>
{:else}
<img
class={cn("hidden dark:block", globalImageStyles)}
src={typeof svgInfo.wordmark !== "string"
? svgInfo.wordmark?.dark || ""
: svgInfo.wordmark || ""}
alt={svgInfo.title}
title={svgInfo.title}
loading="lazy"
/>
<img
class={cn("block dark:hidden", globalImageStyles)}
src={typeof svgInfo.wordmark !== "string"
? svgInfo.wordmark?.light || ""
: svgInfo.wordmark || ""}
alt={svgInfo.title}
title={svgInfo.title}
loading="lazy"
/>
{/if}
{:else if changeThemeMode}
{#if wordmarkSvg && svgInfo.wordmark !== undefined}
<img
class={cn("block", globalImageStyles)}
src={typeof svgInfo.route !== "string"
? mode.current === "dark"
? svgInfo.route.light
: svgInfo.route.dark
: svgInfo.route}
class={cn("hidden dark:block", globalImageStyles)}
src={getSvgImgUrl({ url: svgInfo.wordmark, isDark: true })}
alt={svgInfo.title}
title={svgInfo.title}
loading="lazy"
/>
<img
class={cn("block dark:hidden", globalImageStyles)}
src={getSvgImgUrl({ url: svgInfo.wordmark, isDark: false })}
alt={svgInfo.title}
title={svgInfo.title}
loading="lazy"
@@ -117,18 +88,14 @@
{:else}
<img
class={cn("hidden dark:block", globalImageStyles)}
src={typeof svgInfo.route !== "string"
? svgInfo.route.dark
: svgInfo.route}
src={getSvgImgUrl({ url: svgInfo.route, isDark: true })}
alt={svgInfo.title}
title={svgInfo.title}
loading="lazy"
/>
<img
class={cn("block dark:hidden", globalImageStyles)}
src={typeof svgInfo.route !== "string"
? svgInfo.route.light
: svgInfo.route}
src={getSvgImgUrl({ url: svgInfo.route, isDark: false })}
alt={svgInfo.title}
title={svgInfo.title}
loading="lazy"
@@ -148,7 +115,8 @@
href={`/directory/${c.toLowerCase()}`}
class={badgeVariants({
variant: "outline",
class: "cursor-pointer font-mono",
class:
"cursor-pointer font-mono hover:border-neutral-400 dark:hover:border-neutral-600",
})}
title={`This icon is part of the ${svgInfo.category} category`}
>
@@ -164,7 +132,8 @@
<Popover.Trigger
class={badgeVariants({
variant: "outline",
class: "cursor-pointer font-mono",
class:
"cursor-pointer font-mono hover:border-neutral-400 dark:hover:border-neutral-600",
})}
title="More Tags"
>
@@ -193,7 +162,8 @@
href={`/directory/${svgInfo.category.toLowerCase()}`}
class={badgeVariants({
variant: "outline",
class: "cursor-pointer font-mono",
class:
"cursor-pointer font-mono hover:border-neutral-400 dark:hover:border-neutral-600",
})}
>
{svgInfo.category}
@@ -221,13 +191,7 @@
/>
{/if}
<DownloadSvg
{svgInfo}
isDarkTheme={() => {
const dark = document.documentElement.classList.contains("dark");
return dark;
}}
/>
<DownloadSvg {svgInfo} isDarkTheme={() => mode.current === "dark"} />
<a
href={svgInfo.url}
+23 -13
View File
@@ -1,11 +1,12 @@
import type { iSVG } from "@/types/svg";
import { svgs } from "./svgs";
import type { iSVG, ThemeOptions } from "@/types/svg";
import type { Category } from "@/types/categories";
import { svgs } from "@/data/svgs";
export const svgsData = svgs.map((svg: iSVG, index: number) => {
return { id: index, ...svg };
}) as iSVG[];
export const getCategories = () => {
export const getCategories = (): Category[] => {
const categories = svgs
.flatMap((svg) =>
Array.isArray(svg.category) ? svg.category : [svg.category],
@@ -14,14 +15,23 @@ export const getCategories = () => {
return categories;
};
export const getCategoriesForDirectory = () => {
const categories = svgs
.flatMap((svg) =>
Array.isArray(svg.category) ? svg.category : [svg.category],
)
.filter((category, index, array) => array.indexOf(category) === index)
.map((category) => ({
slug: category.toLowerCase(),
}));
return categories;
export const getSvgsByCategory = (category: string): iSVG[] =>
svgsData.filter((svg: iSVG) => {
if (Array.isArray(svg.category)) {
return svg.category.some(
(categoryItem) => categoryItem.toLowerCase() === category.toLowerCase(),
);
} else {
return svg.category.toLowerCase() === category.toLowerCase();
}
});
interface GetSvgImgUrl {
url: string | ThemeOptions;
isDark: boolean;
}
export const getSvgImgUrl = ({ url, isDark }: GetSvgImgUrl) => {
if (typeof url === "string") return url;
return isDark ? url.dark : url.light;
};
+19 -3
View File
@@ -3,6 +3,7 @@
import type { PageProps } from "./$types";
import { cn } from "@/utils/cn";
import { deleteParam } from "@/utils/searchParams";
import { svgsData } from "@/data";
import { searchWithFuse } from "@/utils/searchWithFuse";
@@ -12,11 +13,12 @@
import SvgCard from "@/components/svgs/svgCard.svelte";
import SortSvgs from "@/components/svgs/sortSvgs.svelte";
import Container from "@/components/container.svelte";
import SearchXIcon from "@lucide/svelte/icons/search-x";
import PageCard from "@/components/pageCard.svelte";
import PageHeader from "@/components/pageHeader.svelte";
import FolderIcon from "@lucide/svelte/icons/folder";
import FolderSearchIcon from "@lucide/svelte/icons/folder-search";
import PageHeader from "@/components/pageHeader.svelte";
import Button from "@/components/ui/button/button.svelte";
// SSR Data:
let { data }: PageProps = $props();
@@ -60,6 +62,13 @@
searchSvgs();
};
const handleClearSearch = () => {
searchTerm = "";
filteredSvgs = sorted ? alphabeticallySorted : latestSorted;
deleteParam("search");
updateDisplaySvgs();
};
$effect(() => {
updateDisplaySvgs();
});
@@ -87,7 +96,14 @@
<span>logos</span>
</p>
{:else}
<FolderSearchIcon size={18} strokeWidth={1.5} />
<Button
title="Clear Search"
onclick={handleClearSearch}
variant="ghost"
size="icon"
>
<SearchXIcon size={18} strokeWidth={1.5} />
</Button>
<p>
<span class="font-mono">{filteredSvgs.length}</span>
<span>logos</span>
+2 -1
View File
@@ -23,7 +23,8 @@ export const load: Load = ({ url }) => {
svg.title.toLowerCase().includes(searchParam.toLowerCase()),
);
} else {
filteredSvgs = searchWithFuse(filteredSvgs)
const baseData = sortParam ? alphabeticallySorted : latestSorted;
filteredSvgs = searchWithFuse(baseData)
.search(searchParam)
.map((result) => result.item);
}
+51 -37
View File
@@ -3,7 +3,6 @@
import type { PageProps } from "./$types";
import { page } from "$app/state";
import { goto } from "$app/navigation";
import { SvelteURLSearchParams } from "svelte/reactivity";
import { cn } from "@/utils/cn";
@@ -14,68 +13,70 @@
import Search from "@/components/search.svelte";
import SvgCard from "@/components/svgs/svgCard.svelte";
import Container from "@/components/container.svelte";
import SearchXIcon from "@lucide/svelte/icons/search-x";
import PageCard from "@/components/pageCard.svelte";
import PageHeader from "@/components/pageHeader.svelte";
import FolderIcon from "@lucide/svelte/icons/folder-open";
import ArrowLeftIcon from "@lucide/svelte/icons/arrow-left";
import { buttonVariants } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";
import SortSvgs from "@/components/svgs/sortSvgs.svelte";
import { deleteParam } from "@/utils/searchParams";
// SSR Data:
let { data }: PageProps = $props();
const directoryData = $derived(data);
// States:
let maxDisplay = 30;
let searchTerm = $state<string>(data.searchTerm || "");
let filteredSvgs = $derived<iSVG[]>(data.filteredSvgs);
let filteredSvgs = $derived<iSVG[]>(data.initialSvgs);
let sorted = $state<boolean>(data.sorted);
let displaySvgs = $state<iSVG[]>([]);
let showAll = $state<boolean>(false);
const updateDisplaySvgs = () => {
displaySvgs = showAll ? filteredSvgs : filteredSvgs.slice(0, maxDisplay);
};
const searchSvgs = () => {
if (!searchTerm) {
filteredSvgs = data.svgs;
filteredSvgs = sorted ? data.alphabeticallySorted : data.latestSorted;
updateDisplaySvgs();
return;
}
if (searchTerm.length < 3) {
filteredSvgs = data.svgs.filter((svg: iSVG) =>
filteredSvgs = (
sorted ? data.alphabeticallySorted : data.latestSorted
).filter((svg: iSVG) =>
svg.title.toLowerCase().includes(searchTerm.toLowerCase()),
);
} else {
filteredSvgs = searchWithFuse(data.svgs)
filteredSvgs = searchWithFuse(filteredSvgs)
.search(searchTerm)
.map((result) => result.item);
}
updateDisplaySvgs();
};
const handleSearch = (value: string) => {
searchTerm = value;
const params = new SvelteURLSearchParams(page.url.searchParams);
if (value) {
params.set("search", value);
} else {
params.delete("search");
}
goto(`?${params.toString()}`, {
keepFocus: true,
noScroll: true,
replaceState: true,
});
searchSvgs();
};
const formatCategory = (category: string) =>
category.charAt(0).toUpperCase() + category.slice(1);
const handleClearSearch = () => {
searchTerm = "";
deleteParam("search");
updateDisplaySvgs();
};
$effect(() => {
filteredSvgs = data.svgs.filter((svg: iSVG) =>
svg.title.toLowerCase().includes(searchTerm.toLowerCase()),
);
updateDisplaySvgs();
});
</script>
<svelte:head>
<title>{formatCategory(directoryData.category)} SVG logos - Svgl</title>
<title>{directoryData.category} SVG logos - Svgl</title>
</svelte:head>
<Search
@@ -91,24 +92,29 @@
>
<a
href="/"
class={cn(
buttonVariants({ class: "group", variant: "ghost", size: "icon" }),
)}
class={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
>
<ArrowLeftIcon
size={18}
strokeWidth={1.5}
class="transition-transform group-hover:translate-x-[-2px]"
/>
<ArrowLeftIcon size={18} strokeWidth={1.5} />
</a>
<FolderIcon size={18} strokeWidth={1.5} />
{#if searchTerm}
<Button
title="Clear Search"
onclick={handleClearSearch}
variant="ghost"
size="icon"
>
<SearchXIcon size={18} strokeWidth={1.5} />
</Button>
{:else}
<FolderIcon class="ml-1" size={18} strokeWidth={1.5} />
{/if}
<p>
{formatCategory(directoryData.category)}
{directoryData.category}
</p>
<span>-</span>
{#if !searchTerm}
<p>
<span>{data.svgs.length} SVGs </span>
<span>{data.initialSvgs.length} SVGs </span>
</p>
{:else}
<p>
@@ -117,6 +123,14 @@
</p>
{/if}
</div>
<SortSvgs
className={cn(filteredSvgs.length === 0 && "hidden")}
isSorted={sorted}
onSortedChange={(value) => {
sorted = value;
searchSvgs();
}}
/>
</PageHeader>
<Container className="my-6">
<Grid>
+19 -17
View File
@@ -1,48 +1,50 @@
import type { PageLoad } from "./$types";
import type { iSVG } from "@/types/svg";
import { svgs } from "@/data/svgs";
import { error } from "@sveltejs/kit";
import { getSvgsByCategory } from "@/data";
import { searchWithFuse } from "@/utils/searchWithFuse";
export const load: PageLoad = (async ({ params, url }) => {
const { category } = params;
const searchParam = url.searchParams.get("search") || "";
const sortParam = url.searchParams.get("sort") === "alphabetical";
const svgsByCategory = svgs.filter((svg: iSVG) => {
if (Array.isArray(svg.category)) {
return svg.category.some(
(categoryItem) => categoryItem.toLowerCase() === category.toLowerCase(),
);
} else {
return svg.category.toLowerCase() === category.toLowerCase();
}
});
const svgsByCategory = getSvgsByCategory(category);
if (svgsByCategory.length === 0) {
if (!svgsByCategory.length) {
throw error(404, "Category not found");
}
let filteredSvgs: iSVG[] = [];
const latestSorted = [...svgsByCategory].sort((a, b) => b.id! - a.id!);
const alphabeticallySorted = [...svgsByCategory].sort((a, b) =>
a.title.localeCompare(b.title),
);
const formatCategory = category.charAt(0).toUpperCase() + category.slice(1);
if (!searchParam) {
filteredSvgs = svgsByCategory;
filteredSvgs = sortParam ? alphabeticallySorted : latestSorted;
} else {
if (searchParam.length < 3) {
filteredSvgs = svgsByCategory.filter((svg: iSVG) =>
const baseData = sortParam ? alphabeticallySorted : latestSorted;
filteredSvgs = baseData.filter((svg: iSVG) =>
svg.title.toLowerCase().includes(searchParam.toLowerCase()),
);
} else {
filteredSvgs = searchWithFuse(svgsByCategory)
const baseData = sortParam ? alphabeticallySorted : latestSorted;
filteredSvgs = searchWithFuse(baseData)
.search(searchParam)
.map((result) => result.item);
}
}
return {
category: category,
category: formatCategory,
searchTerm: searchParam,
svgs: svgsByCategory,
filteredSvgs: filteredSvgs,
sorted: sortParam,
initialSvgs: filteredSvgs,
latestSorted,
alphabeticallySorted,
};
}) satisfies PageLoad;
+4 -1
View File
@@ -16,7 +16,10 @@
<meta name="description" content={data.document.description} />
</svelte:head>
<PageCard>
<PageCard
containerClass="mt-0"
contentCardClass="max-h-[calc(100vh-5.2rem)] min-h-[calc(100vh-5.2rem)]"
>
<PageHeader>
<div
class="flex items-center space-x-2 font-medium text-neutral-950 dark:text-neutral-50"
+1 -1
View File
@@ -1,4 +1,4 @@
export type tCategory =
export type Category =
| "All"
| "AI"
| "Software"
+2 -2
View File
@@ -1,4 +1,4 @@
import type { tCategory } from "./categories";
import type { Category } from "./categories";
export type ThemeOptions = {
dark: string;
@@ -8,7 +8,7 @@ export type ThemeOptions = {
export interface iSVG {
id?: number;
title: string;
category: tCategory | tCategory[];
category: Category | Category[];
route: string | ThemeOptions;
wordmark?: string | ThemeOptions;
brandUrl?: string;
+35
View File
@@ -0,0 +1,35 @@
import { page } from "$app/state";
import { goto } from "$app/navigation";
import { SvelteURLSearchParams } from "svelte/reactivity";
interface SearchParams {
params: Record<string, string | null>;
}
const addParams = ({ params }: SearchParams) => {
const searchParams = new SvelteURLSearchParams(page.url.searchParams);
Object.entries(params).forEach(([key, value]) => {
if (value) {
searchParams.set(key, value);
} else {
searchParams.delete(key);
}
});
goto(`?${searchParams.toString()}`, {
keepFocus: true,
noScroll: true,
replaceState: true,
});
};
const deleteParam = (key: string) => {
const params = new SvelteURLSearchParams(page.url.searchParams);
params.delete(key);
goto(`?${params.toString()}`, {
keepFocus: true,
noScroll: true,
replaceState: true,
});
};
export { addParams, deleteParam };