Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sse impl #152

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ vite.config.ts.timestamp-*
/.vs
/package-lock.json
.vscode
server-config.json
server-config.json
.git
3 changes: 1 addition & 2 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<script lang="ts">
import '../app.css';
import { ModeWatcher } from 'mode-watcher';
import { Toaster } from '$lib/components/ui/sonner';
import '../app.css';

import { afterNavigate, beforeNavigate } from '$app/navigation';
import NProgress from 'nprogress';
import { setContext } from 'svelte';
Expand Down
73 changes: 73 additions & 0 deletions src/routes/api/sse/[stream]/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { produce } from 'sveltekit-sse';
import type { RequestHandler } from './$types';

const validStreams: string[] = [];

async function fetchValidStreams(backendUrl: string, apiKey: string) {
const response = await fetch(`${backendUrl}/api/v1/stream/event_types`, {
headers: {
'x-api-key': apiKey
}
});
if (!response.ok) {
throw new Error('Failed to fetch valid stream types');
}
let data = await response.json()
return data.message;
}
export const POST: RequestHandler = async ({ fetch, params, locals }) => {
const { stream } = params;
const backendUrl = locals.backendUrl;
const apiKey = locals.apiKey;

if (!backendUrl || !apiKey) {
throw new Error('Backend URL or API key not configured');
}

if (validStreams.length === 0) {
try {
validStreams.push(...await fetchValidStreams(backendUrl, apiKey));
} catch (err) {
console.error('Error fetching valid streams:', err);
throw new Error('Failed to fetch valid stream types');
}
}

const streamUrl = `${backendUrl}/api/v1/stream/${stream}`;

try {
const streamedResponse = await fetch(streamUrl, {
method: 'GET',
headers: {
'x-api-key': apiKey
}
});

if (!streamedResponse.body) {
throw new Error('No response body');
}

const reader = streamedResponse.body.getReader();
const textDecoder = new TextDecoder();

return produce(async function start({ emit }) {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value) {
emit('message', textDecoder.decode(value));
}
}
} catch (error) {
console.error('Error reading stream:', error);
emit('error', 'Error reading stream');
} finally {
reader.releaseLock();
}
});
} catch (error) {
console.error('Error fetching stream:', error);
return new Response('Error fetching stream', { status: 500 });
}
};
286 changes: 140 additions & 146 deletions src/routes/browse/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -82,152 +82,146 @@

<Header />

<div class="min-h-screen bg-background text-foreground">
<main class="container mx-auto mt-16 h-screen w-screen px-4 py-8">
<form
method="POST"
use:enhance
on:submit|preventDefault={handleFilterChange}
class="mb-6 flex flex-wrap items-center gap-4"
>
<Field {form} name="state">
<Control let:attrs>
<Label>State</Label>
<Select.Root
selected={selectedState}
onSelectedChange={(s) => {
if (s) {
$formData.state = s.value;
handleFilterChange();
}
}}
>
<Select.Input name={attrs.name} />
<Select.Trigger {...attrs} class="w-[180px]">
<Select.Value placeholder="Filter by state" />
</Select.Trigger>
<Select.Content>
{#each Object.entries(states) as [value, label]}
<Select.Item {value} {label} />
{/each}
</Select.Content>
</Select.Root>
</Control>
<FieldErrors />
</Field>

<Field {form} name="type">
<Control let:attrs>
<Label>Type</Label>
<Select.Root
selected={selectedType}
onSelectedChange={(s) => {
if (s) {
$formData.type = s.value;
handleFilterChange();
}
}}
>
<Select.Input name={attrs.name} />
<Select.Trigger {...attrs} class="w-[180px]">
<Select.Value placeholder="Filter by type" />
</Select.Trigger>
<Select.Content>
{#each Object.entries(types) as [value, label]}
<Select.Item {value} {label} />
{/each}
</Select.Content>
</Select.Root>
</Control>
<FieldErrors />
</Field>

<Field {form} name="sort">
<Control let:attrs>
<Label>Sort</Label>
<Select.Root
selected={selectedSort}
onSelectedChange={(s) => {
if (s) {
$formData.sort = s.value;
handleFilterChange();
}
}}
>
<Select.Input name={attrs.name} />
<Select.Trigger {...attrs} class="w-[180px]">
<Select.Value placeholder="Sort by" />
</Select.Trigger>
<Select.Content>
{#each Object.entries(sortOptions) as [value, label]}
<Select.Item {value} {label} />
{/each}
</Select.Content>
</Select.Root>
</Control>
<FieldErrors />
</Field>

<Button variant="outline" size="icon" on:click={toggleSortOrder}>
{#if $formData.sort.includes('asc')}
<SortAsc class="h-4 w-4" />
{:else}
<SortDesc class="h-4 w-4" />
{/if}
</Button>
</form>

<div class="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
{#each items as item (item.id)}
<div
class="relative aspect-[2/3] overflow-hidden rounded-lg shadow-lg transition-transform hover:scale-105"
<div class="mt-32 h-full p-8 md:px-24 lg:px-32">
<form
method="POST"
use:enhance
on:submit|preventDefault={handleFilterChange}
class="mb-6 flex flex-wrap items-center gap-4"
>
<Field {form} name="state">
<Control let:attrs>
<Label>State</Label>
<Select.Root
selected={selectedState}
onSelectedChange={(s) => {
if (s) {
$formData.state = s.value;
handleFilterChange();
}
}}
>
<Select.Input name={attrs.name} />
<Select.Trigger {...attrs} class="w-[180px]">
<Select.Value placeholder="Filter by state" />
</Select.Trigger>
<Select.Content>
{#each Object.entries(states) as [value, label]}
<Select.Item {value} {label} />
{/each}
</Select.Content>
</Select.Root>
</Control>
<FieldErrors />
</Field>

<Field {form} name="type">
<Control let:attrs>
<Label>Type</Label>
<Select.Root
selected={selectedType}
onSelectedChange={(s) => {
if (s) {
$formData.type = s.value;
handleFilterChange();
}
}}
>
<MediaItem data={item} />
</div>
{/each}
</div>

<div class="mt-8">
<Pagination.Root
count={$totalDataItems}
perPage={limit}
let:pages
let:currentPage
onPageChange={(page) => {
pageNumber = page;
fetchItems();
}}
<Select.Input name={attrs.name} />
<Select.Trigger {...attrs} class="w-[180px]">
<Select.Value placeholder="Filter by type" />
</Select.Trigger>
<Select.Content>
{#each Object.entries(types) as [value, label]}
<Select.Item {value} {label} />
{/each}
</Select.Content>
</Select.Root>
</Control>
<FieldErrors />
</Field>

<Field {form} name="sort">
<Control let:attrs>
<Label>Sort</Label>
<Select.Root
selected={selectedSort}
onSelectedChange={(s) => {
if (s) {
$formData.sort = s.value;
handleFilterChange();
}
}}
>
<Select.Input name={attrs.name} />
<Select.Trigger {...attrs} class="w-[180px]">
<Select.Value placeholder="Sort by" />
</Select.Trigger>
<Select.Content>
{#each Object.entries(sortOptions) as [value, label]}
<Select.Item {value} {label} />
{/each}
</Select.Content>
</Select.Root>
</Control>
<FieldErrors />
</Field>

<Button variant="outline" size="icon" on:click={toggleSortOrder}>
{#if $formData.sort.includes('asc')}
<SortAsc class="h-4 w-4" />
{:else}
<SortDesc class="h-4 w-4" />
{/if}
</Button>
</form>

<div class="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
{#each items as item (item.id)}
<div
class="relative aspect-[2/3] overflow-hidden rounded-lg shadow-lg transition-transform hover:scale-105"
>
<Pagination.Content class="flex items-center justify-center space-x-2">
<Pagination.Item>
<Pagination.PrevButton
class="rounded-md bg-primary px-4 py-2 text-primary-foreground"
/>
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<Pagination.Item>
<Pagination.Ellipsis class="px-4 py-2" />
</Pagination.Item>
{:else}
<Pagination.Item>
<Pagination.Link
{page}
isActive={currentPage === page.value}
class="rounded-md px-4 py-2"
>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton
class="rounded-md bg-primary px-4 py-2 text-primary-foreground"
/>
</Pagination.Item>
</Pagination.Content>
</Pagination.Root>
</div>
</main>
<MediaItem data={item} />
</div>
{/each}
</div>

<div class="mt-8">
<Pagination.Root
count={$totalDataItems}
perPage={limit}
let:pages
let:currentPage
onPageChange={(page) => {
pageNumber = page;
fetchItems();
}}
>
<Pagination.Content class="flex items-center justify-center space-x-2">
<Pagination.Item>
<Pagination.PrevButton class="rounded-md bg-primary px-4 py-2 text-primary-foreground" />
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<Pagination.Item>
<Pagination.Ellipsis class="px-4 py-2" />
</Pagination.Item>
{:else}
<Pagination.Item>
<Pagination.Link
{page}
isActive={currentPage === page.value}
class="rounded-md px-4 py-2"
>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton class="rounded-md bg-primary px-4 py-2 text-primary-foreground" />
</Pagination.Item>
</Pagination.Content>
</Pagination.Root>
</div>
</div>
Loading
Loading