mirror of
https://github.com/pheralb/svgl.git
synced 2025-12-29 08:01:36 +08:00
🚀 Create directory page + fix page components + improve header & sidebar items
This commit is contained in:
@@ -8,10 +8,17 @@
|
||||
import Github from "@/components/logos/github.svelte";
|
||||
import Twitter from "@/components/logos/twitter.svelte";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import Badge from "@/components/ui/badge/badge.svelte";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import SendIcon from "@/components/ui/moving-icons/send-icon.svelte";
|
||||
|
||||
interface HeaderProps {
|
||||
githubStars: number;
|
||||
}
|
||||
|
||||
let { githubStars }: HeaderProps = $props();
|
||||
|
||||
const headerItemsClasses = cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"hover:bg-neutral-200 dark:hover:bg-neutral-800",
|
||||
@@ -32,10 +39,8 @@
|
||||
</a>
|
||||
<Badge variant="outline">{globals.currentVersion}</Badge>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="mr-4 flex items-center space-x-0.5 border-r border-neutral-300 pr-3 dark:border-neutral-800"
|
||||
>
|
||||
<div class="flex h-8 items-center">
|
||||
<div class="flex items-center space-x-0.5">
|
||||
<a
|
||||
target="_blank"
|
||||
title="X/Twitter"
|
||||
@@ -44,16 +49,23 @@
|
||||
>
|
||||
<Twitter size={18} />
|
||||
</a>
|
||||
<a
|
||||
target="_blank"
|
||||
title="GitHub Repository"
|
||||
href={globals.githubUrl}
|
||||
class={cn(headerItemsClasses, "h-9 w-9")}
|
||||
>
|
||||
<Github size={20} />
|
||||
</a>
|
||||
<ModeToggle className={cn(headerItemsClasses, "h-9 w-9")} />
|
||||
</div>
|
||||
<Separator orientation="vertical" class="mx-2 h-8" />
|
||||
<a
|
||||
target="_blank"
|
||||
title="GitHub Repository"
|
||||
href={globals.githubUrl}
|
||||
class={cn(headerItemsClasses, "h-9 w-fit")}
|
||||
>
|
||||
<Github size={20} />
|
||||
<span class="text-neutral-600 dark:text-neutral-400">
|
||||
{githubStars >= 1000
|
||||
? `${(githubStars / 1000).toFixed(1)}k`
|
||||
: githubStars.toLocaleString()}
|
||||
</span>
|
||||
</a>
|
||||
<Separator orientation="vertical" class="mr-3 ml-2" />
|
||||
<a
|
||||
target="_blank"
|
||||
href={globals.submitUrl}
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
import { svgs } from "@/data/svgs";
|
||||
import { page } from "$app/state";
|
||||
import { getCategories } from "@/data";
|
||||
|
||||
import { sidebarItemClasses } from "./sidebarItemClasses";
|
||||
import { sidebarBadgeClasses } from "./sidebarBadgeClasses";
|
||||
|
||||
// Get category counts:
|
||||
const categories: tCategory[] = getCategories();
|
||||
@@ -30,10 +32,7 @@
|
||||
>
|
||||
<p class="truncate">{category}</p>
|
||||
<span
|
||||
class={cn(
|
||||
"dark:bg-dark rounded-lg border border-neutral-200 bg-white px-2 py-0.5 font-mono text-xs font-medium text-neutral-600 shadow-sm dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400",
|
||||
page.url.pathname && "border-transparent",
|
||||
)}
|
||||
class={cn(sidebarBadgeClasses, page.url.pathname && "border-transparent")}
|
||||
>
|
||||
{categoryCounts[category]}
|
||||
</span>
|
||||
|
||||
@@ -1,22 +1,74 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "@/utils/cn";
|
||||
import { page } from "$app/state";
|
||||
import favoritesStore from "@/stores/favorites.store";
|
||||
|
||||
import { sidebarLinks } from "./sidebarLinks";
|
||||
import { sidebarItemClasses } from "./sidebarItemClasses";
|
||||
import { sidebarBadgeClasses } from "./sidebarBadgeClasses";
|
||||
|
||||
import Box from "@lucide/svelte/icons/box";
|
||||
import House from "@lucide/svelte/icons/house";
|
||||
import Heart from "@lucide/svelte/icons/heart";
|
||||
import Cloud from "@lucide/svelte/icons/cloud";
|
||||
|
||||
let favorites = $derived($favoritesStore);
|
||||
let favoritesCount = $derived(favoritesStore.getCount(favorites));
|
||||
</script>
|
||||
|
||||
{#each sidebarLinks as sidebarLink}
|
||||
<a
|
||||
href={sidebarLink.href}
|
||||
data-sveltekit-preload-data
|
||||
class={cn(
|
||||
sidebarItemClasses.base,
|
||||
"justify-start space-x-3",
|
||||
page.url.pathname === sidebarLink.href && sidebarItemClasses.active,
|
||||
)}
|
||||
>
|
||||
<sidebarLink.icon size={16} />
|
||||
<p class="truncate">{sidebarLink.title}</p>
|
||||
</a>
|
||||
{/each}
|
||||
<a
|
||||
href="/"
|
||||
data-sveltekit-preload-data
|
||||
class={cn(
|
||||
sidebarItemClasses.base,
|
||||
"justify-start space-x-3",
|
||||
page.url.pathname === "/" && sidebarItemClasses.active,
|
||||
)}
|
||||
>
|
||||
<House size={16} />
|
||||
<p class="truncate">Home</p>
|
||||
</a>
|
||||
<a
|
||||
href="/favorites"
|
||||
data-sveltekit-preload-data
|
||||
class={cn(
|
||||
sidebarItemClasses.base,
|
||||
"justify-between",
|
||||
String(page.url.pathname) === "/favorites" && sidebarItemClasses.active,
|
||||
)}
|
||||
>
|
||||
<div class="flex items-center space-x-3">
|
||||
<Heart size={16} />
|
||||
<p class="truncate">Favorites</p>
|
||||
</div>
|
||||
{#if favoritesCount > 0}
|
||||
<span
|
||||
class={cn(sidebarBadgeClasses, page.url.pathname && "border-transparent")}
|
||||
>
|
||||
{favoritesCount}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
<a
|
||||
href="/api"
|
||||
data-sveltekit-preload-data
|
||||
class={cn(
|
||||
sidebarItemClasses.base,
|
||||
"justify-start space-x-3",
|
||||
String(page.url.pathname) === "/api" && sidebarItemClasses.active,
|
||||
)}
|
||||
>
|
||||
<Cloud size={16} />
|
||||
<p class="truncate">API</p>
|
||||
</a>
|
||||
<a
|
||||
href="/extensions"
|
||||
data-sveltekit-preload-data
|
||||
class={cn(
|
||||
sidebarItemClasses.base,
|
||||
"justify-start space-x-3",
|
||||
String(page.url.pathname) === "/extensions" && sidebarItemClasses.active,
|
||||
)}
|
||||
>
|
||||
<Box size={16} />
|
||||
<p class="truncate">Extensions</p>
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
export const sidebarBadgeClasses = cn(
|
||||
"animate-in zoom-in-20 fade-in",
|
||||
"dark:bg-dark rounded-lg border border-neutral-200 bg-white px-2 py-0.5 font-mono text-xs font-medium text-neutral-600 shadow-sm dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400",
|
||||
);
|
||||
@@ -2,7 +2,7 @@ import { cn } from "@/utils/cn";
|
||||
|
||||
export const sidebarItemClasses = {
|
||||
base: cn(
|
||||
"rounded-md px-2 py-1.5",
|
||||
"rounded-md px-2 py-1.5 h-8",
|
||||
"flex w-full items-center justify-between space-x-3 text-sm",
|
||||
"text-neutral-600 dark:text-neutral-400",
|
||||
"hover:text-black dark:hover:text-white",
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import BoxIcon from "@lucide/svelte/icons/box";
|
||||
import HouseIcon from "@lucide/svelte/icons/house";
|
||||
import CloudIcon from "@lucide/svelte/icons/cloud";
|
||||
import HeartIcon from "@lucide/svelte/icons/heart";
|
||||
|
||||
export const sidebarLinks = [
|
||||
{
|
||||
title: "Home",
|
||||
href: "/",
|
||||
icon: HouseIcon,
|
||||
},
|
||||
{
|
||||
title: "Favorites",
|
||||
href: "/favorites",
|
||||
icon: HeartIcon,
|
||||
},
|
||||
{
|
||||
title: "API",
|
||||
href: "/api",
|
||||
icon: CloudIcon,
|
||||
},
|
||||
{
|
||||
title: "Extensions",
|
||||
href: "/extensions",
|
||||
icon: BoxIcon,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
interface PageCardProps {
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { children }: PageCardProps = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
"mt-2.5 overflow-hidden",
|
||||
"rounded-md border border-neutral-200 dark:border-neutral-800",
|
||||
"bg-white dark:bg-neutral-900/40",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class={cn(
|
||||
"max-h-[calc(100vh-8.6rem)] min-h-[calc(100vh-8.6rem)] overflow-y-auto",
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import { goto } from "$app/navigation";
|
||||
import { SvelteURLSearchParams } from "svelte/reactivity";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import ArrowUpDownIcon from "@lucide/svelte/icons/arrow-up-down";
|
||||
import ArrowDownUpIcon from "@lucide/svelte/icons/arrow-down-up";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
isSorted: boolean;
|
||||
onSortedChange: (isSorted: boolean) => void;
|
||||
}
|
||||
|
||||
let { className, isSorted, onSortedChange }: Props = $props();
|
||||
let sorted = $state<boolean>(isSorted);
|
||||
|
||||
const sort = () => {
|
||||
const newSorted = !sorted;
|
||||
sorted = newSorted;
|
||||
|
||||
const params = new SvelteURLSearchParams(page.url.searchParams);
|
||||
if (newSorted) {
|
||||
params.set("sort", "alphabetical");
|
||||
} else {
|
||||
params.delete("sort");
|
||||
}
|
||||
|
||||
goto(`?${params.toString()}`, {
|
||||
keepFocus: true,
|
||||
noScroll: true,
|
||||
replaceState: true,
|
||||
});
|
||||
|
||||
onSortedChange(sorted);
|
||||
};
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={cn(buttonVariants({ variant: "ghost", class: "px-2.5" }), className)}
|
||||
onclick={() => sort()}
|
||||
>
|
||||
{#if sorted}
|
||||
<ArrowDownUpIcon size={16} strokeWidth={2} />
|
||||
{:else}
|
||||
<ArrowUpDownIcon size={16} strokeWidth={2} />
|
||||
{/if}
|
||||
<span>{sorted ? "Sort by latest" : "Sort A-Z"}</span>
|
||||
</button>
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { LayoutServerLoad } from "./$types";
|
||||
import { globals } from "@/globals";
|
||||
|
||||
export const load: LayoutServerLoad = async ({ fetch, setHeaders }) => {
|
||||
try {
|
||||
const response = await fetch(globals.apiGithubUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 1 day cache:
|
||||
setHeaders({
|
||||
"cache-control": "public, max-age=86400",
|
||||
});
|
||||
|
||||
return {
|
||||
stars: data.stargazers_count,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching GitHub data:", error);
|
||||
return {
|
||||
stars: 0,
|
||||
error: "Failed to fetch repository data",
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutProps } from "./$types";
|
||||
|
||||
// Styles:
|
||||
import "@/styles/globals.css";
|
||||
|
||||
@@ -9,11 +11,12 @@
|
||||
import { ModeWatcher } from "mode-watcher";
|
||||
import Sidebar from "@/components/layout/sidebar.svelte";
|
||||
|
||||
let { children } = $props();
|
||||
// SSR Data:
|
||||
let { data, children }: LayoutProps = $props();
|
||||
</script>
|
||||
|
||||
<ModeWatcher />
|
||||
<Header />
|
||||
<Header githubStars={data?.stars} />
|
||||
<Sidebar>
|
||||
{@render children?.()}
|
||||
</Sidebar>
|
||||
|
||||
+42
-73
@@ -2,21 +2,20 @@
|
||||
import type { iSVG } from "@/types/svg";
|
||||
import type { PageProps } from "./$types";
|
||||
|
||||
import Fuse from "fuse.js";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { svgsData } from "@/data";
|
||||
import { searchWithFuse } from "@/utils/searchWithFuse";
|
||||
|
||||
// Components:
|
||||
import Grid from "@/components/grid.svelte";
|
||||
import Search from "@/components/search.svelte";
|
||||
import SvgCard from "@/components/svgCard.svelte";
|
||||
import SortSvgs from "@/components/sortSvgs.svelte";
|
||||
import Container from "@/components/container.svelte";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
import PageCard from "@/components/pageCard.svelte";
|
||||
import FolderIcon from "@lucide/svelte/icons/folder";
|
||||
import FolderSearchIcon from "@lucide/svelte/icons/folder-search";
|
||||
import ArrowUpDownIcon from "@lucide/svelte/icons/arrow-up-down";
|
||||
import ArrowDownUpIcon from "@lucide/svelte/icons/arrow-down-up";
|
||||
|
||||
// SSR Data:
|
||||
let { data }: PageProps = $props();
|
||||
@@ -31,15 +30,6 @@
|
||||
|
||||
const { latestSorted, alphabeticallySorted } = data;
|
||||
|
||||
// Fuse.js Search (solo para búsquedas del lado cliente):
|
||||
const fuse = new Fuse<iSVG>(svgsData, {
|
||||
keys: ["title"],
|
||||
threshold: 0.35,
|
||||
ignoreLocation: true,
|
||||
isCaseSensitive: false,
|
||||
shouldSort: true,
|
||||
});
|
||||
|
||||
const updateDisplaySvgs = () => {
|
||||
displaySvgs = showAll ? filteredSvgs : filteredSvgs.slice(0, maxDisplay);
|
||||
};
|
||||
@@ -56,17 +46,14 @@
|
||||
svg.title.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
} else {
|
||||
filteredSvgs = fuse.search(searchTerm).map((result) => result.item);
|
||||
filteredSvgs = searchWithFuse(filteredSvgs)
|
||||
.search(searchTerm)
|
||||
.map((result) => result.item);
|
||||
}
|
||||
|
||||
updateDisplaySvgs();
|
||||
};
|
||||
|
||||
const sort = () => {
|
||||
sorted = !sorted;
|
||||
searchSvgs();
|
||||
};
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
searchTerm = value;
|
||||
searchSvgs();
|
||||
@@ -83,65 +70,47 @@
|
||||
placeholder="Search..."
|
||||
/>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
"mt-2.5 overflow-hidden",
|
||||
"rounded-md border border-neutral-200 dark:border-neutral-800",
|
||||
"bg-white dark:bg-neutral-900/40",
|
||||
)}
|
||||
>
|
||||
<PageCard>
|
||||
<div
|
||||
class={cn(
|
||||
"max-h-[calc(100vh-8.6rem)] min-h-[calc(100vh-8.6rem)] overflow-y-auto",
|
||||
"sticky top-0 z-50 flex h-12.5 items-center justify-between py-1.5 pr-2 pl-3",
|
||||
"border-b border-neutral-200 dark:border-neutral-800",
|
||||
"bg-white/80 backdrop-blur-sm dark:bg-neutral-900/40",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class={cn(
|
||||
"sticky top-0 z-50 flex h-12.5 items-center justify-between py-1.5 pr-2 pl-3",
|
||||
"border-b border-neutral-200 dark:border-neutral-800",
|
||||
"bg-white/80 backdrop-blur-sm dark:bg-neutral-900/40",
|
||||
)}
|
||||
class="flex items-center space-x-2 text-neutral-500 dark:text-neutral-400"
|
||||
>
|
||||
<div
|
||||
class="flex items-center space-x-2 text-neutral-500 dark:text-neutral-400"
|
||||
>
|
||||
{#if !searchTerm}
|
||||
<FolderIcon size={18} strokeWidth={1.5} />
|
||||
<p>
|
||||
<span class="font-mono">{svgsData.length}</span>
|
||||
<span>logos</span>
|
||||
</p>
|
||||
{:else}
|
||||
<FolderSearchIcon size={18} strokeWidth={1.5} />
|
||||
<p>
|
||||
<span class="font-mono">{filteredSvgs.length}</span>
|
||||
<span>logos</span>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class={cn(
|
||||
buttonVariants({ variant: "ghost", class: "px-2.5" }),
|
||||
filteredSvgs.length === 0 && "hidden",
|
||||
)}
|
||||
onclick={() => sort()}
|
||||
>
|
||||
{#if sorted}
|
||||
<ArrowDownUpIcon size={16} strokeWidth={2} />
|
||||
{:else}
|
||||
<ArrowUpDownIcon size={16} strokeWidth={2} />
|
||||
{/if}
|
||||
<span>{sorted ? "Sort by latest" : "Sort A-Z"}</span>
|
||||
</button>
|
||||
{#if !searchTerm}
|
||||
<FolderIcon size={18} strokeWidth={1.5} />
|
||||
<p>
|
||||
<span class="font-mono">{svgsData.length}</span>
|
||||
<span>logos</span>
|
||||
</p>
|
||||
{:else}
|
||||
<FolderSearchIcon size={18} strokeWidth={1.5} />
|
||||
<p>
|
||||
<span class="font-mono">{filteredSvgs.length}</span>
|
||||
<span>logos</span>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<Container className="my-6">
|
||||
<Grid
|
||||
className="animate-in fill-mode-backwards fade-in slide-in-from-bottom-4 duration-500"
|
||||
>
|
||||
{#each displaySvgs as svg}
|
||||
<SvgCard svgInfo={svg} />
|
||||
{/each}
|
||||
</Grid>
|
||||
</Container>
|
||||
<SortSvgs
|
||||
className={cn(filteredSvgs.length === 0 && "hidden")}
|
||||
isSorted={sorted}
|
||||
onSortedChange={(value) => {
|
||||
sorted = value;
|
||||
searchSvgs();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Container className="my-6">
|
||||
<Grid
|
||||
className="animate-in fill-mode-backwards fade-in slide-in-from-bottom-4 duration-500"
|
||||
>
|
||||
{#each displaySvgs as svg}
|
||||
<SvgCard svgInfo={svg} />
|
||||
{/each}
|
||||
</Grid>
|
||||
</Container>
|
||||
</PageCard>
|
||||
|
||||
+4
-10
@@ -1,8 +1,8 @@
|
||||
import type { iSVG } from "@/types/svg";
|
||||
import type { Load } from "@sveltejs/kit";
|
||||
|
||||
import Fuse from "fuse.js";
|
||||
import { svgsData } from "@/data";
|
||||
import { searchWithFuse } from "@/utils/searchWithFuse";
|
||||
|
||||
export const load: Load = ({ url }) => {
|
||||
const searchParam = url.searchParams.get("search") || "";
|
||||
@@ -17,21 +17,15 @@ export const load: Load = ({ url }) => {
|
||||
if (!searchParam) {
|
||||
filteredSvgs = sortParam ? alphabeticallySorted : latestSorted;
|
||||
} else {
|
||||
const fuse = new Fuse<iSVG>(svgsData, {
|
||||
keys: ["title"],
|
||||
threshold: 0.35,
|
||||
ignoreLocation: true,
|
||||
isCaseSensitive: false,
|
||||
shouldSort: true,
|
||||
});
|
||||
|
||||
if (searchParam.length < 3) {
|
||||
const baseData = sortParam ? alphabeticallySorted : latestSorted;
|
||||
filteredSvgs = baseData.filter((svg: iSVG) =>
|
||||
svg.title.toLowerCase().includes(searchParam.toLowerCase()),
|
||||
);
|
||||
} else {
|
||||
filteredSvgs = fuse.search(searchParam).map((result) => result.item);
|
||||
filteredSvgs = searchWithFuse(filteredSvgs)
|
||||
.search(searchParam)
|
||||
.map((result) => result.item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import type { iSVG } from "@/types/svg";
|
||||
import type { PageProps } from "./$types";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import { searchWithFuse } from "@/utils/searchWithFuse";
|
||||
|
||||
// Components:
|
||||
import Grid from "@/components/grid.svelte";
|
||||
import Search from "@/components/search.svelte";
|
||||
import SvgCard from "@/components/svgCard.svelte";
|
||||
import Container from "@/components/container.svelte";
|
||||
|
||||
import PageCard from "@/components/pageCard.svelte";
|
||||
import FolderIcon from "@lucide/svelte/icons/folder-open";
|
||||
import ArrowLeftIcon from "@lucide/svelte/icons/arrow-left";
|
||||
|
||||
import { page } from "$app/state";
|
||||
import { goto } from "$app/navigation";
|
||||
import { SvelteURLSearchParams } from "svelte/reactivity";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
// SSR Data:
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
// States:
|
||||
let searchTerm = $state<string>(data.searchTerm || "");
|
||||
let filteredSvgs = $state<iSVG[]>(data.filteredSvgs);
|
||||
|
||||
const searchSvgs = () => {
|
||||
if (!searchTerm) {
|
||||
filteredSvgs = data.svgs;
|
||||
return;
|
||||
}
|
||||
if (searchTerm.length < 3) {
|
||||
filteredSvgs = data.svgs.filter((svg: iSVG) =>
|
||||
svg.title.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
} else {
|
||||
filteredSvgs = searchWithFuse(data.svgs)
|
||||
.search(searchTerm)
|
||||
.map((result) => result.item);
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
filteredSvgs = data.svgs.filter((svg: iSVG) =>
|
||||
svg.title.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Search
|
||||
searchValue={searchTerm}
|
||||
onSearch={handleSearch}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
|
||||
<PageCard>
|
||||
<div
|
||||
class={cn(
|
||||
"sticky top-0 z-50 flex h-12.5 items-center justify-between py-1.5 pr-2 pl-2",
|
||||
"border-b border-neutral-200 dark:border-neutral-800",
|
||||
"bg-white/80 backdrop-blur-sm dark:bg-neutral-900/40",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class="flex items-center space-x-2 font-medium text-neutral-950 dark:text-neutral-50"
|
||||
>
|
||||
<a
|
||||
href="/"
|
||||
class={cn(
|
||||
buttonVariants({ class: "group", variant: "ghost", size: "icon" }),
|
||||
)}
|
||||
>
|
||||
<ArrowLeftIcon
|
||||
size={18}
|
||||
strokeWidth={1.5}
|
||||
class="transition-transform group-hover:translate-x-[-2px]"
|
||||
/>
|
||||
</a>
|
||||
<FolderIcon size={18} strokeWidth={1.5} />
|
||||
<p>
|
||||
{data.category.slice(0, 1).toUpperCase() + data.category.slice(1)}
|
||||
</p>
|
||||
<span>-</span>
|
||||
{#if !searchTerm}
|
||||
<p>
|
||||
<span>{data.svgs.length} SVGs </span>
|
||||
</p>
|
||||
{:else}
|
||||
<p>
|
||||
<span class="font-mono">{filteredSvgs.length}</span>
|
||||
<span>search results</span>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Container className="my-6">
|
||||
<Grid
|
||||
className="animate-in fill-mode-backwards fade-in slide-in-from-bottom-4 duration-500"
|
||||
>
|
||||
{#each filteredSvgs as svg}
|
||||
<SvgCard svgInfo={svg} />
|
||||
{/each}
|
||||
</Grid>
|
||||
</Container>
|
||||
</PageCard>
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import type { iSVG } from "@/types/svg";
|
||||
|
||||
import { svgs } from "@/data/svgs";
|
||||
import { error } from "@sveltejs/kit";
|
||||
import { searchWithFuse } from "@/utils/searchWithFuse";
|
||||
|
||||
export const load: PageLoad = (async ({ params, url }) => {
|
||||
const { category } = params;
|
||||
const searchParam = url.searchParams.get("search") || "";
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
if (svgsByCategory.length === 0) {
|
||||
throw error(404, "Category not found");
|
||||
}
|
||||
|
||||
let filteredSvgs: iSVG[] = [];
|
||||
|
||||
if (!searchParam) {
|
||||
filteredSvgs = svgsByCategory;
|
||||
} else {
|
||||
if (searchParam.length < 3) {
|
||||
filteredSvgs = svgsByCategory.filter((svg: iSVG) =>
|
||||
svg.title.toLowerCase().includes(searchParam.toLowerCase()),
|
||||
);
|
||||
} else {
|
||||
filteredSvgs = searchWithFuse(svgsByCategory)
|
||||
.search(searchParam)
|
||||
.map((result) => result.item);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
category: category,
|
||||
searchTerm: searchParam,
|
||||
svgs: svgsByCategory,
|
||||
filteredSvgs: filteredSvgs,
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
Reference in New Issue
Block a user