🚀 Create directory page + fix page components + improve header & sidebar items

This commit is contained in:
pheralb
2025-08-27 15:32:14 +01:00
parent 78cccd21e9
commit 1a3efeaede
14 changed files with 433 additions and 144 deletions
+29
View File
@@ -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",
};
}
};
+5 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+48
View File
@@ -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;