Initial commit with Sveltekit + format files

This commit is contained in:
pheralb
2025-08-21 10:26:07 +01:00
parent ca4f397e0a
commit 459457a7e1
97 changed files with 3892 additions and 9893 deletions
-5
View File
@@ -1,5 +0,0 @@
<script lang="ts">
import { redirect } from '@sveltejs/kit';
redirect(301, '/');
</script>
+8 -103
View File
@@ -1,107 +1,12 @@
<script lang="ts">
import { page } from '$app/stores';
import "../app.css";
import favicon from "$lib/assets/favicon.svg";
// Global styles:
import '@/styles/app.css';
import { cn } from '@/utils/cn';
import { ModeWatcher, mode } from 'mode-watcher';
// Categories:
import type { tCategory } from '@/types/categories';
import { svgs } from '@/data/svgs';
import { getCategories } from '@/data';
// Toaster:
import { Toaster } from 'svelte-sonner';
// Components for all pages:
import Transition from '@/components/transition.svelte';
import Warning from '@/components/warning.svelte';
// Layout:
import Navbar from '@/components/navbar.svelte';
import { sidebarCategoryCountStyles } from '@/ui/styles';
import { sidebarItemStyles } from '@/ui/styles';
// Get category counts:
const categories: tCategory[] = getCategories();
let categoryCounts: Record<string, number> = {};
categories.forEach((category) => {
categoryCounts[category] = svgs.filter((svg) => svg.category.includes(category)).length;
});
// Get main pathname:
$: pathname = $page.url.pathname;
let { children } = $props();
</script>
<ModeWatcher />
<Navbar currentPath={pathname} />
<main>
<aside
class={cn(
'z-50 w-full overflow-y-auto overflow-x-hidden',
'dark:border-neutral-800 md:fixed md:left-0 md:h-[calc(100vh-63px)] md:w-56 md:pb-0',
'bg-white dark:bg-neutral-900',
'opacity-95 backdrop-blur-md',
'border-b border-neutral-200 dark:border-neutral-800 md:border-r'
)}
>
<div class="md:px-3 md:py-6">
<nav
class="flex items-center space-x-1 overflow-y-auto px-6 pb-2 pt-2 md:mb-3 md:flex-col md:space-x-0 md:space-y-1 md:overflow-y-visible md:px-0 md:pt-0"
>
<a
href="/"
class={cn(
sidebarItemStyles,
pathname === '/'
? 'bg-neutral-200 font-medium text-dark dark:bg-neutral-700/30 dark:text-white'
: ''
)}
data-sveltekit-preload-data>All</a
>
<!-- Order alfabetically: -->
{#each categories.sort() as category}
<a
href={`/directory/${category.toLowerCase()}`}
data-sveltekit-preload-data
class={cn(
sidebarItemStyles,
pathname === `/directory/${category.toLowerCase()}`
? 'bg-neutral-200 font-medium text-dark dark:bg-neutral-700/30 dark:text-white'
: ''
)}
>
<span>{category}</span>
<span
class={cn(
sidebarCategoryCountStyles,
pathname === `/directory/${category.toLowerCase()}`
? 'border-neutral-300 dark:border-neutral-700'
: '',
'hidden font-mono text-xs md:inline'
)}>{categoryCounts[category]}</span
>
</a>
{/each}
</nav>
</div>
</aside>
<div class="ml-0 pb-6 md:ml-56">
<Warning />
<Transition {pathname}>
<slot />
</Transition>
<Toaster
position="bottom-right"
theme={$mode}
class="toaster group"
toastOptions={{
classes: {
toast: 'group toast dark:group-[.toaster]:bg-neutral-900 group-[.toaster]:font-sans',
description: 'group-[.toast]:text-xs font-mono'
}
}}
/>
</div>
</main>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{@render children?.()}
+5 -171
View File
@@ -1,171 +1,5 @@
<script lang="ts">
import type { iSVG } from '@/types/svg';
import { cn } from '@/utils/cn';
import { onMount } from 'svelte';
import { queryParam } from 'sveltekit-search-params';
import Fuse from 'fuse.js';
// Get all svgs:
import { svgsData } from '@/data';
const allSvgs = JSON.parse(JSON.stringify(svgsData));
// Cache sorted arrays
const latestSorted = [...allSvgs].sort((a, b) => b.id! - a.id!);
const alphabeticallySorted = [...allSvgs].sort((a, b) => a.title.localeCompare(b.title));
// Fuzzy search setup:
const fuse = new Fuse<iSVG>(allSvgs, {
keys: ['title'],
threshold: 0.35,
ignoreLocation: true,
isCaseSensitive: false,
shouldSort: true
});
// Components:
import Search from '@/components/search.svelte';
import Container from '@/components/container.svelte';
import SvgCard from '@/components/svgCard.svelte';
import Grid from '@/components/grid.svelte';
import NotFound from '@/components/notFound.svelte';
// URL params
const searchParam = queryParam('search');
// Icons:
import { ArrowDown, ArrowDownUpIcon, ArrowUpDownIcon, TrashIcon } from 'lucide-svelte';
import { buttonStyles } from '@/ui/styles';
let sorted: boolean = false;
let showAll: boolean = false;
// Search:
let searchTerm = '';
let filteredSvgs: iSVG[] = [];
let displaySvgs: iSVG[] = [];
let maxDisplay = 24;
const updateDisplaySvgs = () => {
displaySvgs = showAll ? filteredSvgs : filteredSvgs.slice(0, maxDisplay);
};
// Hybrid search strategy:
// - Simple string matching for queries < 3 chars
// - Fuzzy search for longer queries (handle typos and partial matches)
const searchSvgs = () => {
$searchParam = searchTerm || null;
if (!searchTerm) {
filteredSvgs = sorted ? alphabeticallySorted : latestSorted;
updateDisplaySvgs();
return;
}
if (searchTerm.length < 3) {
filteredSvgs = allSvgs.filter((svg: iSVG) =>
svg.title.toLowerCase().includes(searchTerm.toLowerCase())
);
} else {
filteredSvgs = fuse.search(searchTerm).map((result) => result.item);
}
updateDisplaySvgs();
};
// Clear search:
const clearSearch = () => {
searchTerm = '';
// Use current sort state to determine order
filteredSvgs = sorted ? alphabeticallySorted : latestSorted;
updateDisplaySvgs();
};
// Sort:
const sort = () => {
sorted = !sorted;
filteredSvgs = sorted ? alphabeticallySorted : latestSorted;
updateDisplaySvgs();
};
onMount(() => {
if ($searchParam) {
searchTerm = $searchParam;
}
searchSvgs();
});
$: {
if (showAll || filteredSvgs) {
updateDisplaySvgs();
}
}
</script>
<svelte:head>
<title>A beautiful library with SVG logos - Svgl</title>
</svelte:head>
<Search
bind:searchTerm
on:input={searchSvgs}
clearSearch={() => clearSearch()}
placeholder={`Search ${allSvgs.length} logos...`}
/>
<Container>
<div class={cn('mb-4 flex items-center justify-end', searchTerm.length > 0 && 'justify-between')}>
{#if searchTerm.length > 0}
<button
class={cn(
'flex items-center justify-center space-x-1 rounded-md py-1.5 text-sm font-medium opacity-80 transition-opacity hover:opacity-100',
filteredSvgs.length === 0 && 'hidden'
)}
on:click={() => clearSearch()}
>
<TrashIcon size={16} strokeWidth={2} class="mr-1" />
<span>Clear results</span>
</button>
{/if}
<button
class={cn(
'flex items-center justify-center space-x-1 rounded-md py-1.5 text-sm font-medium opacity-80 transition-opacity hover:opacity-100',
filteredSvgs.length === 0 && 'hidden'
)}
on:click={() => sort()}
>
{#if sorted}
<ArrowDownUpIcon size={16} strokeWidth={2} class="mr-1" />
{:else}
<ArrowUpDownIcon size={16} strokeWidth={2} class="mr-1" />
{/if}
<span>{sorted ? 'Sort by latest' : 'Sort A-Z'}</span>
</button>
</div>
<Grid>
{#each displaySvgs as svg}
<SvgCard svgInfo={svg} {searchTerm} />
{/each}
</Grid>
{#if filteredSvgs.length > maxDisplay && !showAll}
<div class="mt-4 flex items-center justify-center">
<button
class={buttonStyles}
on:click={() => {
showAll = true;
updateDisplaySvgs();
}}
>
<div class="relative flex items-center space-x-2">
<ArrowDown size={16} strokeWidth={2} />
<span>Load All SVGs</span>
<span class="opacity-70">
({filteredSvgs.length - maxDisplay} more)
</span>
</div>
</button>
</div>
{/if}
{#if filteredSvgs.length === 0}
<NotFound notFoundTerm={searchTerm} />
{/if}
</Container>
<h1>Welcome to SvelteKit</h1>
<p>
Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the
documentation
</p>
-50
View File
@@ -1,50 +0,0 @@
<script>
import { cn } from '@/utils/cn';
export let data;
</script>
<svelte:head>
<title>{data.meta.title} - SVGL</title>
<meta property="og:type" content="article" />
<meta property="og:title" content={data.meta.title} />
<meta property="og:description" content={data.meta.description} />
</svelte:head>
<section
class="bg-white bg-[url('/images/hero-pattern_light.svg')] dark:bg-neutral-900 dark:bg-[url('/images/hero-pattern_dark.svg')]"
>
<div class="relative z-10 mx-auto max-w-screen-xl px-4 py-8 text-center lg:py-20">
<div class="flex items-center justify-center space-x-4 text-center">
<h1
class="mb-4 text-4xl font-bold leading-none tracking-tight text-neutral-900 dark:text-white md:text-5xl lg:text-6xl"
>
API Reference
</h1>
<span class="relative inline-block overflow-hidden rounded-full p-[1px] shadow-sm">
<span
class="absolute inset-[-1000%] animate-[spin_4s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#f4f4f5_0%,#f4f4f5_50%,#737373_100%)] dark:bg-[conic-gradient(from_90deg_at_50%_50%,#121212_0%,#121212_50%,#737373_100%)]"
/>
<div
class="inline-flex h-full w-full cursor-default items-center justify-center rounded-full border border-neutral-100 bg-neutral-100 px-3 py-1 font-mono text-xs font-medium backdrop-blur-3xl dark:border-neutral-800 dark:bg-neutral-900 dark:text-white"
>
v1
</div>
</span>
</div>
<p class="text-lg font-normal text-gray-500 dark:text-gray-200 sm:px-16 lg:px-48 lg:text-xl">
The API reference is a detailed documentation of all the endpoints available in the API.
</p>
</div>
</section>
<article
class={cn(
'prose dark:prose-invert',
'mx-auto max-w-3xl px-4 py-10',
'prose-h2:font-medium prose-h2:tracking-tight prose-h2:underline prose-h2:decoration-neutral-300 prose-h2:underline-offset-[6px] prose-h2:transition-opacity hover:prose-h2:opacity-70 dark:prose-h2:decoration-neutral-700/65',
'prose-pre:m-0 prose-pre:border prose-pre:border-neutral-200 dark:prose-pre:border dark:prose-pre:border-neutral-800/65',
'prose-inline-code:rounded prose-inline-code:border prose-inline-code:border-neutral-300 prose-inline-code:bg-neutral-200/50 prose-inline-code:p-[2px] prose-inline-code:font-mono prose-inline-code:dark:border-neutral-800 prose-inline-code:dark:bg-neutral-800/50'
)}
>
<svelte:component this={data.content} />
</article>
-14
View File
@@ -1,14 +0,0 @@
import { error } from '@sveltejs/kit';
export async function load() {
try {
const documentTitle = 'api';
const post = await import(`../../docs/${documentTitle}.md`);
return {
content: post.default,
meta: post.metadata
};
} catch (e) {
throw error(404, `Could not find this page`);
}
}
-37
View File
@@ -1,37 +0,0 @@
import type { RequestEvent } from '../$types';
import { transform } from '@svgr/core';
import { json, redirect } from '@sveltejs/kit';
import { ratelimit } from '@/server/redis';
// SVGR Plugins:
import svgrJSX from '@svgr/plugin-jsx';
export const GET = async () => {
return redirect(301, 'https://svgl.app/api');
};
export const POST = async ({ request }: RequestEvent) => {
try {
const body = await request.json();
const svgCode = body.code;
const typescript = body.typescript;
const name = body.name.replace(/[^a-zA-Z0-9]/g, '');
const jsCode = await transform(
svgCode,
{
plugins: [svgrJSX],
icon: true,
typescript: typescript
},
{ componentName: name }
);
return json({ data: jsCode }, { status: 200 });
} catch (error) {
return json({ error: `⚠️ api/svgs/svgr - Error: ${error}` }, { status: 500 });
}
};
@@ -1,16 +0,0 @@
<script>
import Container from '@/components/container.svelte';
import { ArrowLeft } from 'lucide-svelte';
</script>
<Container>
<a href="/">
<div
class="flex items-center space-x-2 duration-100 hover:text-neutral-500 dark:text-neutral-400 dark:hover:text-white group md:mt-2"
>
<ArrowLeft size={20} class="group-hover:-translate-x-[2px] group-hover:duration-200" />
<span>View all</span>
</div>
</a>
</Container>
<slot />
-67
View File
@@ -1,67 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import type { iSVG } from '@/types/svg';
import { queryParam } from 'sveltekit-search-params';
export let data: PageData;
let svgsByCategory = data.svgs || [];
let category = data.category || '';
// Components:
import Container from '@/components/container.svelte';
import Grid from '@/components/grid.svelte';
import Search from '@/components/search.svelte';
import SvgCard from '@/components/svgCard.svelte';
import NotFound from '@/components/notFound.svelte';
// URL params
const searchParam = queryParam('search');
// Search:
let searchTerm = $searchParam || '';
let filteredSvgs: iSVG[] = [];
if (searchTerm.length === 0) {
filteredSvgs = svgsByCategory.sort((a: iSVG, b: iSVG) => {
return a.title.localeCompare(b.title);
});
}
const searchSvgs = () => {
$searchParam = searchTerm || null;
return (filteredSvgs = svgsByCategory.filter((svg: iSVG) => {
let svgTitle = svg.title.toLowerCase();
return svgTitle.includes(searchTerm.toLowerCase());
}));
};
const clearSearch = () => {
searchTerm = '';
searchSvgs();
};
if ($searchParam) {
searchSvgs();
}
</script>
<svelte:head>
<title>{category} logos - Svgl</title>
</svelte:head>
<Container>
<Search
bind:searchTerm
on:input={searchSvgs}
clearSearch={() => clearSearch()}
placeholder={`Search ${filteredSvgs.length} ${category} logos...`}
/>
<Grid>
{#each filteredSvgs as svg}
<SvgCard svgInfo={svg} {searchTerm} />
{/each}
</Grid>
{#if filteredSvgs.length === 0}
<NotFound notFoundTerm={searchTerm} />
{/if}
</Container>
-32
View File
@@ -1,32 +0,0 @@
import type { PageLoad, EntryGenerator } from './$types';
import type { iSVG } from '@/types/svg';
import { error } from '@sveltejs/kit';
import { svgs } from '@/data/svgs';
import { getCategoriesForDirectory } from '@/data';
export const entries: EntryGenerator = () => {
const categories = getCategoriesForDirectory();
return categories;
};
export const load = (async ({ params }) => {
const { slug } = params;
const svgsByCategory = svgs.filter((svg: iSVG) => {
if (Array.isArray(svg.category)) {
return svg.category.some((categoryItem) => categoryItem.toLowerCase() === slug.toLowerCase());
} else {
return svg.category.toLowerCase() === slug.toLowerCase();
}
});
if (svgsByCategory.length === 0) {
throw error(404, 'Category not found');
}
return {
category: slug,
svgs: svgsByCategory
};
}) satisfies PageLoad;