-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: template and session page for host
- Loading branch information
1 parent
0de54fb
commit ef5c638
Showing
48 changed files
with
1,906 additions
and
1,152 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<script lang="ts"> | ||
import { notifications, type NotificationType } from '$lib/stores/notifications'; | ||
import { fly } from 'svelte/transition'; | ||
import { CheckCircle, XCircle, AlertCircle, Info } from 'lucide-svelte'; | ||
const icons: Record<NotificationType, typeof CheckCircle> = { | ||
success: CheckCircle, | ||
error: XCircle, | ||
warning: AlertCircle, | ||
info: Info | ||
}; | ||
const colors: Record<NotificationType, string> = { | ||
success: 'bg-green-50 text-green-600 border-green-200', | ||
error: 'bg-red-50 text-red-600 border-red-200', | ||
warning: 'bg-yellow-50 text-yellow-600 border-yellow-200', | ||
info: 'bg-blue-50 text-blue-600 border-blue-200' | ||
}; | ||
</script> | ||
|
||
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2"> | ||
{#each $notifications as { id, message, type } (id)} | ||
<div | ||
transition:fly={{ x: 100, duration: 300 }} | ||
class="flex items-center gap-3 rounded-lg border p-4 shadow-lg {colors[type]}" | ||
role="alert" | ||
> | ||
<svelte:component this={icons[type]} class="h-5 w-5" /> | ||
<p>{message}</p> | ||
<button class="ml-auto" on:click={() => notifications.dismiss(id)} aria-label="Dismiss"> | ||
<XCircle class="h-5 w-5" /> | ||
</button> | ||
</div> | ||
{/each} | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,90 @@ | ||
<script lang="ts"> | ||
import { Html5QrcodeScanner } from 'html5-qrcode'; | ||
import { notifications } from '$lib/stores/notifications'; | ||
import { Html5Qrcode } from 'html5-qrcode'; | ||
import { onMount, onDestroy } from 'svelte'; | ||
let { onScan } = $props<{ | ||
onScan: (code: string) => void; | ||
}>(); | ||
let scanner: Html5QrcodeScanner; | ||
let scanner: Html5Qrcode; | ||
let scanning = $state(false); | ||
let fileInput: HTMLInputElement; | ||
onMount(() => { | ||
scanner = new Html5QrcodeScanner('qr-reader', { fps: 10, qrbox: 250 }, true); | ||
scanner.render(success, error); | ||
scanner = new Html5Qrcode('qr-reader'); | ||
}); | ||
onDestroy(() => { | ||
if (scanner) { | ||
if (scanner.isScanning) { | ||
scanner.stop(); | ||
} | ||
scanner.clear(); | ||
} | ||
}); | ||
function success(decodedText: string) { | ||
onScan(decodedText); | ||
async function startCamera() { | ||
try { | ||
scanning = true; | ||
await scanner.start( | ||
{ facingMode: 'environment' }, | ||
{ fps: 10, qrbox: 250 }, | ||
(decodedText) => { | ||
onScan(decodedText); | ||
scanner.stop(); | ||
scanning = false; | ||
}, | ||
(err) => { | ||
notifications.error(err); | ||
} | ||
); | ||
} catch (err) { | ||
notifications.error(`Error starting camera: ${err}`); | ||
scanning = false; | ||
} | ||
} | ||
function error(err: unknown) { | ||
console.error(err); | ||
async function handleFileInput(event: Event) { | ||
const file = (event.target as HTMLInputElement).files?.[0]; | ||
if (!file) return; | ||
try { | ||
const result = await scanner.scanFile(file, true); | ||
onScan(result); | ||
} catch (err) { | ||
notifications.error(`Error scanning file: ${err}`); | ||
} | ||
} | ||
</script> | ||
|
||
<div id="qr-reader"></div> | ||
<div class="space-y-4"> | ||
<div id="qr-reader" class="mx-auto w-full max-w-sm"></div> | ||
|
||
<div class="flex flex-col gap-4"> | ||
<button | ||
type="button" | ||
onclick={scanning ? () => scanner.stop() : startCamera} | ||
class="w-full rounded-lg border px-6 py-2 hover:bg-gray-50" | ||
> | ||
{scanning ? 'Stop Camera' : 'Start Camera'} | ||
</button> | ||
|
||
<div class="relative"> | ||
<input | ||
type="file" | ||
accept="image/*" | ||
bind:this={fileInput} | ||
onchange={handleFileInput} | ||
class="hidden" | ||
id="qr-image-input" | ||
/> | ||
<label | ||
for="qr-image-input" | ||
class="block w-full cursor-pointer rounded-lg border px-6 py-2 text-center hover:bg-gray-50" | ||
> | ||
Upload QR Code Image | ||
</label> | ||
</div> | ||
</div> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
<script lang="ts"> | ||
import { Play } from 'lucide-svelte'; | ||
import type { Session } from '$lib/schema/session'; | ||
import type { Readable } from 'svelte/store'; | ||
import QRCode from '$lib/components/QRCode.svelte'; | ||
import { Button } from 'flowbite-svelte'; | ||
import { notifications } from '$lib/stores/notifications'; | ||
import { page } from '$app/stores'; | ||
let { session }: { session: Readable<Session> } = $props(); | ||
async function handleStartSession() { | ||
const response = await fetch(`/api/session/${$page.params.id}/action/start-individual`, { | ||
method: 'POST' | ||
}); | ||
if (!response.ok) { | ||
const data = await response.json(); | ||
console.error('Failed to start session:', data.error); | ||
} | ||
} | ||
async function handleEndIndividual() { | ||
const response = await fetch(`/api/session/${$page.params.id}/action/end-individual`, { | ||
method: 'POST' | ||
}); | ||
if (!response.ok) { | ||
const data = await response.json(); | ||
console.error('Failed to end individual stage:', data.error); | ||
} | ||
} | ||
async function handleStartGroup() { | ||
const response = await fetch(`/api/session/${$page.params.id}/action/start-group`, { | ||
method: 'POST' | ||
}); | ||
if (!response.ok) { | ||
const data = await response.json(); | ||
console.error('Failed to start group stage:', data.error); | ||
} | ||
} | ||
async function handleEndGroup() { | ||
const response = await fetch(`/api/session/${$page.params.id}/action/end-group`, { | ||
method: 'POST' | ||
}); | ||
if (!response.ok) { | ||
const data = await response.json(); | ||
console.error('Failed to end group stage:', data.error); | ||
} | ||
} | ||
const stageButton = $derived({ | ||
preparing: { | ||
text: 'Start Individual Stage', | ||
action: handleStartSession, | ||
show: true | ||
}, | ||
individual: { | ||
text: 'End Individual Stage', | ||
action: handleEndIndividual, | ||
show: true | ||
}, | ||
'before-group': { | ||
text: 'Start Group Stage', | ||
action: handleStartGroup, | ||
show: true | ||
}, | ||
group: { | ||
text: 'End Group Stage', | ||
action: handleEndGroup, | ||
show: true | ||
}, | ||
ended: { | ||
text: 'Session Ended', | ||
action: () => { | ||
notifications.error('Already Ended', 3000); | ||
}, | ||
show: false | ||
} | ||
}); | ||
</script> | ||
|
||
<main class="mx-auto max-w-4xl px-4 py-16"> | ||
<div class="mb-8 flex items-center justify-between"> | ||
<div> | ||
<h1 class="text-3xl font-bold">{$session?.title}</h1> | ||
</div> | ||
|
||
<div class="flex items-center gap-4"> | ||
{#if $session && stageButton[$session.status].show} | ||
<Button color="primary" on:click={stageButton[$session.status].action}> | ||
<Play class="mr-2 h-4 w-4" /> | ||
{stageButton[$session.status].text} | ||
</Button> | ||
{/if} | ||
</div> | ||
</div> | ||
|
||
<div class="grid gap-8 md:grid-cols-2"> | ||
<!-- Status Section --> | ||
<div class="rounded-lg border p-6"> | ||
<h2 class="mb-4 text-xl font-semibold">Session Status</h2> | ||
<div class="flex items-center gap-2"> | ||
<!-- svelte-ignore element_invalid_self_closing_tag --> | ||
<span | ||
class="inline-block h-3 w-3 rounded-full {$session?.status === 'preparing' | ||
? 'bg-yellow-500' | ||
: $session?.status === 'individual' | ||
? 'bg-blue-500' | ||
: $session?.status === 'before-group' | ||
? 'bg-purple-500' | ||
: $session?.status === 'group' | ||
? 'bg-green-500' | ||
: 'bg-gray-500'}" | ||
/> | ||
<span class="capitalize">{$session?.status}</span> | ||
</div> | ||
|
||
{#if $session?.status === 'preparing'} | ||
<div class="mt-4"> | ||
<h3 class="mb-2 font-medium">Session QR Code</h3> | ||
<QRCode value={`${$page.url.origin}/session/${$page.params.id}`} /> | ||
</div> | ||
{/if} | ||
</div> | ||
|
||
<!-- Task Section --> | ||
<div class="rounded-lg border p-6"> | ||
<h2 class="mb-4 text-xl font-semibold">Main Task</h2> | ||
<p class="text-gray-700">{$session?.task}</p> | ||
|
||
{#if $session?.subtasks.length > 0} | ||
<div class="mt-4"> | ||
<h3 class="mb-2 font-medium">Subtasks:</h3> | ||
<ul class="list-inside list-disc space-y-2"> | ||
{#each $session?.subtasks as subtask} | ||
<li class="text-gray-700">{subtask}</li> | ||
{/each} | ||
</ul> | ||
</div> | ||
{/if} | ||
</div> | ||
|
||
<!-- Resources Section --> | ||
<div class="rounded-lg border p-6 md:col-span-2"> | ||
<h2 class="mb-4 text-xl font-semibold">Resources</h2> | ||
{#if $session?.resources.length === 0} | ||
<p class="text-gray-600">No resources available</p> | ||
{:else} | ||
<div class="space-y-4"> | ||
{#each $session?.resources as resource} | ||
<div class="rounded-lg border p-4"> | ||
<h3 class="font-medium">{resource.name}</h3> | ||
<p class="mt-2 text-gray-700">{resource.content}</p> | ||
</div> | ||
{/each} | ||
</div> | ||
{/if} | ||
</div> | ||
</div> | ||
</main> |
Oops, something went wrong.