Skip to content

Commit

Permalink
feat: template and session page for host
Browse files Browse the repository at this point in the history
  • Loading branch information
JacobLinCool committed Nov 27, 2024
1 parent 0de54fb commit ef5c638
Show file tree
Hide file tree
Showing 48 changed files with 1,906 additions and 1,152 deletions.
13 changes: 13 additions & 0 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,18 @@ export const handle: Handle = async ({ event, resolve }) => {
}
}

if (
event.url.pathname.startsWith('/api') &&
!event.url.pathname.startsWith('/api/auth/signin') &&
!event.locals.user
) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: {
'Content-Type': 'application/json'
}
});
}

return resolve(event);
};
35 changes: 35 additions & 0 deletions src/lib/components/Notifications.svelte
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>
77 changes: 68 additions & 9 deletions src/lib/components/QrScanner.svelte
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>
165 changes: 165 additions & 0 deletions src/lib/components/session/HostView.svelte
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>
Loading

0 comments on commit ef5c638

Please sign in to comment.