Add extension page, create extension component for displaying items & add search functionality

This commit is contained in:
pheralb
2025-09-04 10:45:56 +01:00
parent 74e42b00dc
commit 26f23f7e5b
3 changed files with 216 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
<script lang="ts">
import type { Extension } from "@/types/extensions";
import { cn } from "@/utils/cn";
import { buttonVariants } from "@/components/ui/button";
import ArrowUpRightIcon from "@lucide/svelte/icons/arrow-up-right";
interface ExtensionProps {
data: Extension;
}
let { data }: ExtensionProps = $props();
</script>
<div
class={cn(
"relative h-48 max-h-48",
"rounded-md transition-shadow hover:shadow-sm",
"bg-white dark:bg-neutral-900",
"border border-neutral-200 dark:border-neutral-800",
"flex flex-col",
)}
>
<div class="p-4">
<div class="mb-4 flex items-center justify-between">
<img
src={data.image}
alt={data.name}
loading="lazy"
class="h-8 w-8 rounded-md object-contain"
/>
<a
href={data.url}
target="_blank"
rel="noopener noreferrer"
class={buttonVariants({ variant: "outline" })}
>
<span>Install</span>
<ArrowUpRightIcon
size={12}
strokeWidth={2}
class="text-neutral-600 dark:text-neutral-400"
/>
</a>
</div>
<p class="mb-1 font-medium">{data.name}</p>
<p
title={data.description}
class="truncate text-sm text-pretty text-neutral-700 dark:text-neutral-300"
>
{data.description}
</p>
</div>
<div
class="absolute bottom-0 flex w-full items-center justify-end gap-1 border-t border-neutral-200 bg-neutral-100/60 px-4 py-2 text-sm dark:border-neutral-800 dark:bg-neutral-800/40"
>
<span>Created by</span>
<a
href={data.created_by.socialUrl}
target="_blank"
rel="noopener noreferrer"
class="font-medium text-pretty decoration-neutral-400 underline-offset-2 transition-colors hover:text-neutral-600 hover:underline dark:decoration-neutral-600 dark:hover:text-neutral-400"
>
{data.created_by.name}
</a>
</div>
</div>
+127
View File
@@ -0,0 +1,127 @@
<script lang="ts">
import type { PageProps } from "./$types";
import type { Extension } from "@/types/extensions";
// Utils:
import { cn } from "@/utils/cn";
import { searchExtensionsWithFuse } from "@/utils/searchWithFuse";
// UI Components:
import { buttonVariants } from "@/components/ui/button";
// Page components:
import PageCard from "@/components/pageCard.svelte";
import PageHeader from "@/components/pageHeader.svelte";
import Grid from "@/components/grid.svelte";
import Container from "@/components/container.svelte";
import ExtensionItem from "@/components/extension.svelte";
// Svgs:
import RocketIcon from "@lucide/svelte/icons/rocket";
import PackageOpen from "@lucide/svelte/icons/package-open";
import PlusIcon from "@lucide/svelte/icons/plus";
import Search from "@/components/search.svelte";
// SSR Data:
let { data }: PageProps = $props();
// States:
let searchTerm = $state<string>(data.searchTerm || "");
let filteredExtensions = $state<Extension[]>(data.initialExtensions);
const searchExtensions = () => {
if (!searchTerm) {
filteredExtensions = data.allExtensions;
return;
}
filteredExtensions = searchExtensionsWithFuse(data.allExtensions)
.search(searchTerm)
.map((result) => result.item);
};
const handleSearch = (value: string) => {
searchTerm = value;
searchExtensions();
};
</script>
<svelte:head>
<title>Extensions - Svgl</title>
<meta
name="description"
content="Integrate SVGL with your favorite tools and apps to streamline your workflow. Created by the community."
/>
</svelte:head>
<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"
>
<PackageOpen size={18} strokeWidth={1.5} />
<p>Extensions</p>
</div>
</PageHeader>
<section class="flex flex-col px-4 py-12">
<h2
class={cn(
"mb-4 text-center",
"font-onest text-4xl font-semibold",
"animate-in delay-100 duration-500 fill-mode-backwards fade-in slide-in-from-bottom-4",
)}
>
Extensions
</h2>
<div
class={cn(
"flex w-full flex-col items-center justify-center space-y-4",
"animate-in delay-300 duration-500 fill-mode-backwards fade-in slide-in-from-bottom-4",
)}
>
<p>
Integrate SVGL with your favorite tools and apps to streamline your
workflow. Created by the community.
</p>
<div class="flex items-center space-x-2">
<a href="/docs/api" class={cn(buttonVariants({ variant: "radial" }))}>
<RocketIcon size={16} strokeWidth={2} />
<span>Start Building</span>
</a>
<a
target="_blank"
href="https://github.com/pheralb/svgl/issues/new/choose"
class={cn(buttonVariants({ variant: "outline" }))}
>
<PlusIcon size={16} strokeWidth={2} />
<span>Add your extension</span>
</a>
</div>
</div>
</section>
<Container
className={cn(
"max-w-3xl",
"animate-in delay-500 duration-500 fill-mode-backwards fade-in slide-in-from-bottom-4",
)}
>
<div
class="sticky top-12 z-10 bg-white/80 pt-4 backdrop-blur-sm dark:bg-transparent"
>
<Search
iconSize={18}
searchValue={searchTerm}
onSearch={handleSearch}
placeholder="Search..."
inputClass="text-md"
/>
</div>
<Grid columns="2" className="mt-5 mb-7">
{#each filteredExtensions as extension (extension.id)}
<ExtensionItem data={extension} />
{/each}
</Grid>
</Container>
</PageCard>
+22
View File
@@ -0,0 +1,22 @@
import type { Load } from "@sveltejs/kit";
import { extensionsData } from "@/data";
import { searchExtensionsWithFuse } from "@/utils/searchWithFuse";
export const load: Load = ({ url }) => {
const searchParam = url.searchParams.get("search") || "";
let filteredExtensions = [...extensionsData];
if (searchParam) {
const fuseSearch = searchExtensionsWithFuse(extensionsData);
filteredExtensions = fuseSearch
.search(searchParam)
.map((result) => result.item);
}
return {
searchTerm: searchParam,
initialExtensions: filteredExtensions,
allExtensions: extensionsData,
};
};