refactor: huge ui/ux refactor

This commit is contained in:
glazk0 2024-03-24 23:15:58 +01:00
parent 238c02b328
commit acb176aee3
No known key found for this signature in database
GPG key ID: E45BF177782B9FEB
150 changed files with 4169 additions and 3144 deletions

14
components.json Normal file
View file

@ -0,0 +1,14 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
},
"typescript": true
}

View file

@ -1,6 +1,5 @@
{
"name": "peer-at-code",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
@ -16,37 +15,45 @@
"test:unit": "vitest"
},
"dependencies": {
"bits-ui": "^0.20.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-svelte": "^0.330.0",
"marked": "^12.0.0",
"svelte-boring-avatars": "^1.2.5",
"svelte-sonner": "^0.3.18",
"tailwind-merge": "^2.2.1"
"formsnap": "^0.5.1",
"lucide-svelte": "^0.363.0",
"mode-watcher": "^0.3.0",
"svelte-boring-avatars": "^1.2.6",
"svelte-sonner": "^0.3.19",
"tailwind-merge": "^2.2.2",
"tailwind-variants": "^0.2.1",
"vaul-svelte": "^0.3.0"
},
"devDependencies": {
"@playwright/test": "^1.41.2",
"@sveltejs/adapter-node": "^4.0.1",
"@sveltejs/kit": "^2.5.0",
"@playwright/test": "^1.42.1",
"@sveltejs/adapter-node": "^5.0.1",
"@sveltejs/kit": "^2.5.4",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"autoprefixer": "^10.4.19",
"boring-avatars": "^1.10.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"postcss": "^8.4.35",
"mdsvex": "^0.11.0",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-tailwindcss": "^0.5.11",
"svelte": "^4.2.10",
"svelte-check": "^3.6.4",
"sveltekit-superforms": "^2.1.0",
"prettier-plugin-svelte": "^3.2.2",
"prettier-plugin-tailwindcss": "^0.5.12",
"svelte": "^4.2.12",
"svelte-check": "^3.6.8",
"sveltekit-superforms": "^2.11.0",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vite": "^5.1.1",
"vitest": "^1.2.2",
"typescript": "^5.4.3",
"vite": "^5.2.6",
"vitest": "^1.4.0",
"zod": "^3.22.4"
}
}

1716
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,107 +1,105 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'Karrik';
src: url('/fonts/Karrik.woff2');
}
@font-face {
font-family: 'Fira Code';
src: url('/fonts/FiraCode.woff2');
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem;
@font-face {
font-family: 'Karrik';
src: url('/fonts/Karrik.woff2');
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
@font-face {
font-family: 'Fira Code';
src: url('/fonts/FiraCode.woff2');
}
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
:root {
--gradient: linear-gradient(to top left, hsl(258 85% 67%), hsl(258 60% 40%));
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--background: 255 14.81% 10.59%;
--foreground: 284 4.7% 97.05%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--muted: 258 15% 17%;
--muted-foreground: 284 4.7% 54.1%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--popover: 256.36 14.29% 15.1%;
--popover-foreground: 284 4.7% 97.05%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--card: 256.36 14.29% 15.1%;
--card-foreground: 284 4.7% 97.05%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--border: 258 15% 17%;
--input: 258 15% 17%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--primary: 258 85% 67%;
--primary-foreground: 284 4.7% 97.05%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--secondary: 258 60% 40%;
--secondary-foreground: 284 4.7% 97.05%;
--ring: 217.2 32.6% 17.5%;
--accent: 258 60% 40%;
--accent-foreground: 284 4.7% 97.05%;
--destructive: 7 100% 67%;
--destructive-foreground: 284 4.7% 97.05%;
--ring: 258 85% 67%;
--radius: 0.5rem;
}
}
@layer base {
* {
box-sizing: border-box;
scroll-behavior: smooth;
@apply border-border text-white;
@apply border-border;
}
body {
@apply text-foreground bg-gradient-to-b from-primary-800 to-primary-900;
@apply bg-background text-foreground;
}
::selection {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-muted;
}
::-webkit-scrollbar-thumb {
@apply rounded-sm !bg-muted-foreground/30;
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: hsl(215.4 16.3% 46.9% / 0.3);
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.antialised {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer components {
.console {
@apply relative top-0.5 inline-block;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0px 1000px hsl(258deg 15% 17%) inset;
transition: background-color 5000s ease-in-out 0s;
.bg-gradient {
background: var(--gradient);
}
}

View file

@ -1,9 +1,15 @@
<!DOCTYPE html>
<html lang="fr">
<head>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="relative min-h-screen">
<main style="display: contents">%sveltekit.body%</main>
</body>
<html lang="fr" style="color-scheme: dark;">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<script data-domain="app.peerat.dev" src="https://plosibl.peerat.dev/js/script.js" defer></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="min-h-screen antialiased font-sans text-base">
<div style="display: contents" class="relative flex min-h-screen flex-col">%sveltekit.body%</div>
</body>
</html>

View file

@ -1,16 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import Avatar from 'svelte-boring-avatars';
$: user = $page.data.user;
</script>
{#if user?.avatar}
<img
src="data:image;base64,${user.avatar}"
alt="Avatar de {user.pseudo}"
class="h-9 w-9 rounded-full object-cover"
/>
{:else}
<Avatar name={user?.pseudo} size={35} variant="beam" />
{/if}

View file

@ -4,13 +4,13 @@
</script>
<div
class="flex w-full items-center space-x-4 rounded-lg border-2 border-highlight-primary bg-primary-700 p-4 shadow-md"
class="flex w-full items-center space-x-4 border rounded border-border bg-card p-4 shadow-md"
>
<!-- <Icon class="text-muted" size={30} /> -->
<!-- <Icon class="text-muted-foreground" size={30} /> -->
<div class="flex w-full items-center justify-between">
<div class="flex-col">
<div class="">
<h2 class="text-xl font-semibold">{data}</h2>
<p class="text-muted">{title}</p>
<p class="text-muted-foreground">{title}</p>
</div>
</div>
</div>

View file

@ -1,19 +1,18 @@
<script lang="ts">
import { Trophy } from 'lucide-svelte';
import Swords from 'lucide-svelte/icons/swords';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import type { Chapter } from '$lib/types';
import { cn } from '$lib/utils';
import ChevronRight from './Icons/ChevronRight.svelte';
export let chapter: Chapter;
</script>
<li
class={cn(
'group relative flex h-full w-full flex-col rounded-md bg-primary-700 transition-colors duration-150',
'group relative flex h-full w-full flex-col rounded border border-border bg-card transition-colors duration-150',
{
'hover:bg-primary-600': chapter.show,
'hover:bg-card/80': chapter.show,
'opacity-50': !chapter.show
}
)}
@ -21,14 +20,14 @@
{#if chapter.show}
<a
class="flex h-full w-full items-center justify-between gap-4 p-4"
href={chapter.show ? `/dashboard/chapters/${chapter.id}` : '#'}
href={chapter.show ? `/chapters/${chapter.id}` : '#'}
>
<div class="flex items-center gap-2">
<span class="text-base font-semibold">
<span class="font-semibold">
{chapter.name}
</span>
{#if chapter.id === 1}
<Trophy class="stroke-highlight-secondary" />
<Swords class="stroke-muted-foreground" />
{/if}
</div>
<span class="translate-x-0 transform-gpu duration-300 group-hover:translate-x-2">
@ -38,7 +37,7 @@
{:else}
<span class="flex h-full w-full items-center gap-4 p-4">
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
<h2 class="text-base font-semibold">
<h2 class="font-semibold">
{chapter.name}
</h2>
</div>

View file

@ -1,26 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: string | undefined | null = undefined;
export { className as class };
</script>
<svg
class={cn(className)}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><line x1="21" x2="3" y1="6" y2="6" /><line x1="15" x2="3" y1="12" y2="12" /><line
x1="17"
x2="3"
y1="18"
y2="18"
/></svg
>

View file

@ -1,21 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: string | undefined | null = undefined;
export { className as class };
</script>
<svg
class={cn(className)}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><circle cx="12" cy="8" r="6" /><path d="M15.477 12.89 17 22l-5-3-5 3 1.523-9.11" /></svg
>

View file

@ -1,20 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: string | undefined | null = undefined;
export { className as class };
</script>
<svg
class={cn(className)}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg
>

View file

@ -1,21 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: string | undefined | null = undefined;
export { className as class };
</script>
<svg
class={cn(className)}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><polyline points="16 18 22 12 16 6" /><polyline points="8 6 2 12 8 18" /></svg
>

View file

@ -1,33 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: string | undefined | null = undefined;
export { className as class };
</script>
<svg
class={cn(className)}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><rect width="7" height="9" x="3" y="3" rx="1" /><rect
width="7"
height="5"
x="14"
y="3"
rx="1"
/><rect width="7" height="9" x="14" y="12" rx="1" /><rect
width="7"
height="5"
x="3"
y="16"
rx="1"
/></svg
>

View file

@ -1,19 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: string | undefined | null = undefined;
export { className as class };
</script>
<svg
class={cn(className)}
xmlns="http://www.w3.org/2000/svg"
height="24"
width="24"
viewBox="0 0 640 512"
fill="currentColor"
><path
d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"
/></svg
>

View file

@ -1,23 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: string | undefined | null = undefined;
export { className as class };
</script>
<svg
class={cn(className)}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
/><path d="M9 18c-4.51 2-5-2-7-2" /></svg
>

View file

@ -1,23 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: string | undefined | null = undefined;
export { className as class };
</script>
<svg
class={cn(className)}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><path
d="M12 17h.01"
/></svg
>

View file

@ -1,25 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: string | undefined | null = undefined;
export { className as class };
</script>
<svg
class={cn(className)}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><line x1="18" x2="18" y1="20" y2="10" /><line x1="12" x2="12" y1="20" y2="4" /><line
x1="6"
x2="6"
y1="20"
y2="14"
/></svg
>

View file

@ -1,25 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: string | undefined | null = undefined;
export { className as class };
</script>
<svg
class={cn(className)}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M22 10.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h12.5" /><path
d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"
/><path d="M18 15.28c.2-.4.5-.8.9-1a2.1 2.1 0 0 1 2.6.4c.3.4.5.8.5 1.3 0 1.3-2 2-2 2" /><path
d="M20 22v.01"
/></svg
>

View file

@ -1,36 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: string | undefined | null = undefined;
export { className as class };
</script>
<svg
class={cn(className)}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><line x1="21" x2="14" y1="4" y2="4" /><line x1="10" x2="3" y1="4" y2="4" /><line
x1="21"
x2="12"
y1="12"
y2="12"
/><line x1="8" x2="3" y1="12" y2="12" /><line x1="21" x2="16" y1="20" y2="20" /><line
x1="12"
x2="3"
y1="20"
y2="20"
/><line x1="14" x2="14" y1="2" y2="6" /><line x1="8" x2="8" y1="10" y2="14" /><line
x1="16"
x2="16"
y1="18"
y2="22"
/></svg
>

View file

@ -1,20 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: string | undefined | null = undefined;
export { className as class };
</script>
<svg
class={cn(className)}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg
>

View file

@ -2,48 +2,49 @@
import { page } from '$app/stores';
import { siteConfig } from '$lib/config/site';
export let title = siteConfig.name;
$: title = $page.data?.title ? `${$page.data.title} | ${siteConfig.name}` : siteConfig.name;
</script>
<svelte:head>
{#key $page.url.pathname}
<title>{siteConfig.title}</title>
<meta name="title" content={siteConfig.title} />
<meta name="description" content={siteConfig.description} />
<meta name="keywords" content={siteConfig.keywords.join(',')} />
<meta name="author" content={siteConfig.author} />
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={siteConfig.description} />
<meta name="keywords" content={siteConfig.keywords.join(',')} />
<meta name="author" content={siteConfig.author} />
<meta name="theme-color" content={siteConfig.themeColor} />
<meta name="theme-color" content={siteConfig.themeColor} />
<meta name="robots" content="index,follow" />
<meta name="robots" content="noindex,nofollow" />
<meta itemprop="name" content={siteConfig.title} />
<meta itemprop="description" content={siteConfig.description} />
<meta itemprop="image" content={siteConfig.imageUrl} />
<meta itemprop="name" content={title} />
<meta itemprop="description" content={siteConfig.description} />
<meta itemprop="image" content={siteConfig.imageUrl} />
<meta property="og:site_name" content={siteConfig.name} />
<meta property="og:title" content={siteConfig.title} />
<meta property="og:description" content={siteConfig.description} />
<meta property="og:type" content="website" />
<meta property="og:url" content={siteConfig.url + $page.url.pathname} />
<meta property="og:image" content={siteConfig.imageUrl} />
<meta property="og:image:alt" content={siteConfig.title} />
<meta property="og:locale" content={siteConfig.locale} />
<meta property="og:site_name" content={siteConfig.name} />
<meta property="og:title" content={title} />
<meta property="og:description" content={siteConfig.description} />
<meta property="og:type" content="website" />
<meta property="og:url" content={siteConfig.url + $page.url.pathname} />
<meta property="og:image" content={siteConfig.imageUrl} />
<meta property="og:image:alt" content={title} />
<meta property="og:locale" content="fr" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={siteConfig.url} />
<meta name="twitter:title" content={siteConfig.title} />
<meta name="twitter:description" content={siteConfig.description} />
<meta name="twitter:image" content={siteConfig.imageUrl} />
<meta name="twitter:image:alt" content={siteConfig.title} />
<meta name="twitter:creator" content={siteConfig.twitter.creator} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={siteConfig.url} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={siteConfig.description} />
<meta name="twitter:image" content={siteConfig.imageUrl} />
<meta name="twitter:image:alt" content={title} />
<meta name="twitter:creator" content={`@${siteConfig.author}`} />
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
{/key}
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
</svelte:head>

View file

@ -1,95 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import Avatar from './Avatar.svelte';
import AlignLeft from './Icons/AlignLeft.svelte';
import X from './Icons/X.svelte';
export let isOpen: boolean;
$: user = $page.data.user;
$: segments = $page.url.pathname.slice(1).split('/');
$: breadcrumb = segments.map((segment, index) => {
return { name: segment, href: '/' + segments.slice(0, index + 1).join('/') };
}) as { name: string; href: string }[];
function handleToggle() {
isOpen = !isOpen;
}
</script>
<div
class="z-50 flex w-full flex-row items-center justify-between border-b border-solid border-highlight-primary px-4 py-4 sm:px-8"
>
<div class="flex flex-row items-center space-x-2 sm:space-x-0">
<div class="flex items-center">
<button on:click={handleToggle} class="block sm:hidden">
{#if isOpen}
<X class="h-5 w-5 text-muted" />
{:else}
<AlignLeft class="h-5 w-5 text-muted" />
{/if}
</button>
</div>
{#if !isOpen && segments.length}
<div class="hidden items-center justify-center capitalize text-highlight-secondary sm:flex">
{#each breadcrumb as segment}
{@const last = segment === breadcrumb[breadcrumb.length - 1]}
<a class="hover:text-primary hover:underline" href={segment.href}>
{segment.name}
</a>
{#if !last}
<span class="mx-1 text-highlight-secondary">/</span>
{/if}
{/each}
</div>
{/if}
<!-- {#if !isOpen && segment}
<div class="flex items-center justify-center capitalize text-highlight-secondary">
{segment}
</div>
{/if} -->
</div>
<div class="flex flex-row items-center gap-2">
{#if !isOpen}
<a href="/logout">
<button
class="flex items-center gap-1 rounded-md p-2 text-destructive hover:bg-destructive/10"
>
Se déconnecter
</button>
</a>
{/if}
<div class="flex flex-row items-center gap-2">
<Avatar />
{user?.pseudo}
</div>
<!-- {!isLoading && me ? (
<Popover
open={isMenuOpen}
onOpenChange={setIsMenuOpen}
trigger={
<button class="mx-auto flex items-center gap-2">
<AvatarComponent name={me.pseudo} src={me.avatar} class="h-9 w-9" />
<span>{me?.pseudo}</span>
</button>
}
>
<nav class="flex w-32 flex-col gap-2">
<button
class="flex items-center gap-1 p-2 text-destructive hover:bg-destructive/10"
onClick={() => router.push('/logout')}
>
Se déconnecter
</button>
</nav>
</Popover>
) : (
<div class="animate-pulse">
<div class="flex items-center gap-2">
<div class="h-9 w-9 rounded-full bg-highlight-primary" />
<div class="h-4 w-14 rounded-full bg-highlight-primary" />
</div>
</div>
)} -->
</div>
</div>

View file

@ -1,25 +1,24 @@
<script lang="ts">
import { page } from '$app/stores';
import { cn } from '$lib/utils';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import type { Puzzle } from '$lib/types';
import ChevronRight from './Icons/ChevronRight.svelte';
import { cn } from '$lib/utils';
export let puzzle: Puzzle;
const chapterId = $page.params.chapterId;
$: tags = puzzle.tags?.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name));
</script>
<li
class={cn(
'group relative flex h-full w-full rounded-md border-2 bg-primary-700 transition-colors duration-150',
'group relative flex h-full w-full rounded border border-border bg-card transition-colors duration-150',
{
'border-green-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'easy'),
'border-yellow-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'medium'),
'border-red-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'hard'),
'border-highlight-primary': !puzzle.tags?.length,
'hover:bg-primary-600': puzzle.show,
'border-green-500/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'easy'),
'border-yellow-500/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'medium'),
'border-red-500/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'hard'),
'hover:bg-card/80': puzzle.show,
'opacity-50': !puzzle.show
}
)}
@ -27,12 +26,12 @@
{#if puzzle.show}
<a
class="flex h-full w-full items-center gap-4 p-4"
href="/dashboard/chapters/{chapterId}/puzzle/{puzzle.id}"
href="/chapters/{chapterId}/puzzle/{puzzle.id}"
>
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
<h2 class="text-base font-semibold">
{puzzle.name}
<span class="text-sm text-highlight-secondary">
<span class="text-highlight-secondary text-sm">
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
</span>
</h2>
@ -40,9 +39,7 @@
<div class="flex items-center gap-x-6">
<div class="flex gap-x-2 text-sm">
{#each puzzle.tags as tag}
<span
class="inline-block rounded-md bg-primary-800 px-2 py-1 text-highlight-secondary"
>
<span class="inline-block rounded bg-muted px-2 py-1 text-muted-foreground">
{tag.name}
</span>
{/each}
@ -59,7 +56,7 @@
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
<h2 class="text-base font-semibold">
{puzzle.name}
<span class="text-sm text-highlight-secondary">
<span class="text-highlight-secondary text-sm">
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
</span>
</h2>
@ -67,9 +64,7 @@
{#if puzzle.tags?.length}
<div class="flex gap-x-2 text-sm">
{#each puzzle.tags as tag}
<span
class="inline-block rounded-md bg-primary-800 px-2 py-1 text-highlight-secondary"
>
<span class="inline-block rounded bg-muted px-2 py-1 text-muted-foreground">
{tag.name}
</span>
{/each}

View file

@ -1,211 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { cn } from '$lib/utils';
import Badge from '$lib/components/Icons/Badge.svelte';
import Code from '$lib/components/Icons/Code.svelte';
import Dashboard from '$lib/components/Icons/Dashboard.svelte';
import Leaderboard from '$lib/components/Icons/Leaderboard.svelte';
import Settings from '$lib/components/Icons/Settings.svelte';
import Discord from './Icons/Discord.svelte';
import Git from './Icons/Git.svelte';
import Help from './Icons/Help.svelte';
import Mail from './Icons/Mail.svelte';
$: path = $page.url.pathname;
$: isActive = (slug: string) => path === slug;
export let isOpen: boolean;
const navItems = [
{
name: 'Dashboard',
slug: '/dashboard',
icon: Dashboard
},
{
name: 'Classement',
slug: '/dashboard/leaderboard',
icon: Leaderboard
},
{
name: 'Chapitres',
slug: '/dashboard/chapters',
icon: Code
},
{
name: 'Badges',
slug: '/dashboard/badges',
icon: Badge
},
{
name: 'Paramètres',
slug: '/dashboard/settings',
icon: Settings
}
];
</script>
<aside
class={cn(
'absolute z-10 h-screen w-28 border-r border-highlight-primary bg-gradient-to-b from-primary-800 to-primary-900 shadow-md transition-all duration-300 ease-in-out sm:relative sm:flex sm:flex-col lg:w-60',
{
'bottom-0 -translate-x-full sm:translate-x-0': !isOpen,
'bottom-0 w-full sm:w-28': isOpen
}
)}
>
<div class="flex h-full flex-col">
<div class="flex w-full justify-center p-[8.5px]">
<img
src="/assets/brand/peerat.png"
alt="Logo"
width="50"
height="50"
loading="eager"
draggable="false"
/>
</div>
<div class="px-4">
<hr class="border-highlight-primary" />
</div>
<div class="px-4 pt-4">
<ul class="flex flex-col gap-4">
{#each navItems as item}
<li>
<a
on:click={() => {
isOpen = false;
}}
href={item.slug}
class={cn(
'flex items-center justify-center gap-2 rounded-md px-3 py-3 text-sm transition-colors duration-150 lg:justify-start',
{
'bg-primary-700': isActive(item.slug),
'group hover:bg-primary-700': !isActive(item.slug)
}
)}
>
<svelte:component
this={item.icon}
class={cn({
'stroke-highlight-secondary transition-colors duration-150 group-hover:stroke-primary-0':
!isActive(item.slug)
})}
/>
<span
class={cn('hidden lg:block', {
'block sm:hidden': isOpen,
hidden: !isOpen,
'text-highlight-secondary transition-colors duration-150 group-hover:text-primary':
!isActive(item.slug)
})}
>
{item.name}
</span>
</a>
</li>
{/each}
</ul>
</div>
<div class="px-4 pt-4">
<hr class="border-highlight-primary" />
</div>
<div class="px-4 pt-4">
<ul class="flex flex-col gap-4">
<li>
<span
class="group pointer-events-none flex items-center justify-center gap-2 rounded-md px-3 py-3 text-sm opacity-50 transition-colors duration-150 lg:justify-start"
>
<Help class="stroke-highlight-secondary transition-colors duration-150" />
<span
class={cn('hidden text-highlight-secondary transition-colors duration-150 lg:block', {
'block sm:hidden': isOpen,
hidden: !isOpen
})}
>
Aide
</span>
</span>
</li>
<li>
<a
on:click={() => {
isOpen = false;
}}
href="mailto:cyberbottle@peerat.dev"
class="group flex items-center justify-center gap-2 rounded-md px-3 py-3 text-sm transition-colors duration-150 hover:bg-primary-700 lg:justify-start"
>
<Mail
class="stroke-highlight-secondary transition-colors duration-150 group-hover:stroke-primary-0"
/>
<span
class={cn(
'hidden text-highlight-secondary transition-colors duration-150 group-hover:text-primary lg:block',
{
'block sm:hidden': isOpen,
hidden: !isOpen
}
)}
>
Mail
</span>
</a>
</li>
<li>
<a
on:click={() => {
isOpen = false;
}}
href="//discord.gg/72vuHcwUkE"
target="_blank"
rel="noopener"
class="group flex items-center justify-center gap-2 rounded-md px-3 py-3 text-sm transition-colors duration-150 hover:bg-primary-700 lg:justify-start"
>
<Discord
class="fill-highlight-secondary transition-colors duration-150 group-hover:fill-primary-0"
/>
<span
class={cn(
'hidden text-highlight-secondary transition-colors duration-150 group-hover:text-primary lg:block',
{
'block sm:hidden': isOpen,
hidden: !isOpen
}
)}
>
Discord
</span>
</a>
</li>
<li>
<a
on:click={() => {
isOpen = false;
}}
href="//git.peerat.dev"
target="_blank"
rel="noopener"
class="group flex items-center justify-center gap-2 rounded-md px-3 py-3 text-sm transition-colors duration-150 hover:bg-primary-700 lg:justify-start"
>
<Git
class="stroke-highlight-secondary transition-colors duration-150 group-hover:stroke-primary-0"
/>
<span
class={cn(
'hidden text-highlight-secondary transition-colors duration-150 group-hover:text-primary lg:block',
{
'block sm:hidden': isOpen,
hidden: !isOpen
}
)}
>
Git
</span>
</a>
</li>
</ul>
</div>
</div>
</aside>

View file

@ -1,55 +0,0 @@
<script lang="ts" context="module">
export type ToastData = {
title: string;
description: string;
};
const {
elements: { content, title, description, close },
helpers,
states: { toasts },
actions: { portal }
} = createToaster<ToastData>();
export const addToast = helpers.addToast;
</script>
<script lang="ts">
import { createToaster, melt } from '@melt-ui/svelte';
import { flip } from 'svelte/animate';
import { fly } from 'svelte/transition';
import { X } from 'lucide-svelte';
</script>
<div class="fixed right-0 top-0 z-50 m-4 flex flex-col items-end gap-2 sm:top-20" use:portal>
{#each $toasts as { id, data } (id)}
<div
use:melt={$content(id)}
animate:flip={{ duration: 500 }}
in:fly={{ duration: 150, x: '100%' }}
out:fly={{ duration: 150, x: '100%' }}
class="rounded-lg border border-primary-600 bg-highlight-primary text-white shadow-md"
>
<div
class="relative flex w-[24rem] max-w-[calc(100vw-2rem)] items-center justify-between gap-4 p-5"
>
<div>
<h3 use:melt={$title(id)} class="flex items-center gap-2 font-semibold">
{data.title}
<span class="square-1.5 rounded-full" />
</h3>
<div use:melt={$description(id)}>
{data.description}
</div>
</div>
<button
use:melt={$close(id)}
class="text-magnum-500 square-6 hover:bg-magnum-900/50 absolute right-4 top-4 grid place-items-center
rounded-full"
>
<X class="square-4" />
</button>
</div>
</div>
{/each}
</div>

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { page } from '$app/stores';
import * as Breadcrumb from '$lib/components/ui/breadcrumb';
$: segments = $page.url.pathname.slice(1).split('/');
$: breadcrumb = segments.map((segment, index) => {
return {
name: segment.charAt(0).toUpperCase() + segment.slice(1),
href: '/' + segments.slice(0, index + 1).join('/')
};
}) as { name: string; href: string }[];
</script>
<Breadcrumb.Root>
<Breadcrumb.List>
{#each breadcrumb as { name, href }, index}
<Breadcrumb.Item>
<Breadcrumb.Link {href}>
{name}
</Breadcrumb.Link>
</Breadcrumb.Item>
{#if index < breadcrumb.length - 1}
<Breadcrumb.Separator />
{/if}
{/each}
</Breadcrumb.List>
</Breadcrumb.Root>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import { Check, Copy } from 'lucide-svelte';
let element: HTMLButtonElement;
let copying = false;
const copy = () => {
const sibling = element.nextElementSibling as HTMLElement;
const code = sibling.innerText;
navigator.clipboard.writeText(code);
copying = true;
setTimeout(() => (copying = false), 1000);
};
</script>
<button
class="absolute right-2 top-2 rounded-md p-2 text-white"
bind:this={element}
on:click={copy}
{...$$restProps}
>
{#if copying}
<Check color="green" />
{:else}
<Copy />
{/if}
</button>

View file

@ -0,0 +1,47 @@
<script lang="ts">
import { onMount } from 'svelte';
import CopyCodeButton from './copy-code-button.svelte';
onMount(() => {
const preTags: HTMLCollectionOf<HTMLPreElement> = document.getElementsByTagName('pre');
for (let preTag of preTags) {
const classList = Array.from(preTag.classList);
const isCodeBlock = classList.some((className) => className.startsWith('language-'));
if (isCodeBlock) {
const preTagParent = preTag.parentNode;
const newCodeBlockWrapper = document.createElement('div');
newCodeBlockWrapper.className = 'relative';
new CopyCodeButton({
target: newCodeBlockWrapper
});
if (preTagParent) {
preTagParent.replaceChild(newCodeBlockWrapper, preTag);
newCodeBlockWrapper.appendChild(preTag);
}
}
}
return () => {
for (let preTag of preTags) {
const preTagParent = preTag.parentNode;
if (preTagParent) {
const newCodeBlockWrapper = preTagParent.parentNode;
if (newCodeBlockWrapper) {
preTagParent.replaceChild(preTag, newCodeBlockWrapper);
}
}
}
};
});
</script>
<slot />

View file

@ -0,0 +1,4 @@
// TODO: Add more components here
export { default as CopyCodeInjector } from './copy-code-injector.svelte';
export { default as Metadata } from './metadata.svelte';

View file

@ -0,0 +1,3 @@
export { default as Loader } from './loader/loader.svelte';
export { default as Navbar } from './navbar/navbar.svelte';
export { default as Sidenav } from './sidenav/sidenav.svelte';

View file

@ -0,0 +1,42 @@
<div class="loading-bar">
<div class="progress-bar">
<div class="progress-bar-value" />
</div>
</div>
<style>
.loading-bar {
width: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 100000;
}
.progress-bar {
@apply bg-muted;
height: 2px;
width: 100%;
overflow: hidden;
}
.progress-bar-value {
@apply bg-primary;
width: 100%;
height: 100%;
animation: loadingBarAnimation 2s infinite linear;
transform-origin: 0% 50%;
}
@keyframes loadingBarAnimation {
0% {
transform: translateX(0) scaleX(0);
}
40% {
transform: translateX(0) scaleX(0.4);
}
100% {
transform: translateX(100%) scaleX(0.5);
}
}
</style>

View file

@ -0,0 +1,63 @@
<script lang="ts">
import { page } from '$app/stores';
import type { NavItemWithChildren } from '$lib/config';
import { cn } from '$lib/utils';
import MobileNavLink from './mobile-nav-link.svelte';
export let navItem: NavItemWithChildren;
</script>
{#if navItem.children?.length}
<h5 class="mb-2 font-medium">{navItem.name}</h5>
<ul class="space-y-2">
{#each navItem.children as item}
{@const isActive = $page.url.pathname === item.href}
<li>
<MobileNavLink
class={cn(
'group flex items-center gap-2 rounded border border-border border-opacity-0 p-2 font-semibold text-muted-foreground lg:leading-6',
{
'border border-opacity-100 bg-card text-foreground': isActive,
'hover:border-opacity-100 hover:bg-card hover:text-foreground': !isActive
}
)}
href={item.href}
external={item.external}
>
<svelte:component
this={item.icon}
class={cn('stroke-muted-foreground', {
'stroke-foreground': $page.url.pathname === navItem.href,
'group-hover:stroke-foreground': $page.url.pathname !== navItem.href
})}
/>
<span>{item.name}</span>
</MobileNavLink>
</li>
{/each}
</ul>
{:else}
<MobileNavLink
class={cn(
'group flex items-center gap-2 rounded border border-border border-opacity-0 p-2 font-semibold text-muted-foreground lg:leading-6',
{
'border border-opacity-100 bg-card text-foreground': $page.url.pathname === navItem.href,
'hover:border-opacity-100 hover:bg-card hover:text-foreground':
$page.url.pathname !== navItem.href
}
)}
href={navItem.href}
external={navItem.external}
>
<svelte:component
this={navItem.icon}
class={cn('stroke-muted-foreground', {
'stroke-foreground': $page.url.pathname === navItem.href,
'group-hover:stroke-foreground': $page.url.pathname !== navItem.href
})}
/>
<span>{navItem.name}</span>
</MobileNavLink>
{/if}

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { ArrowUpRight } from 'lucide-svelte';
import type { HTMLAnchorAttributes } from 'svelte/elements';
import { Drawer } from 'vaul-svelte';
import { cn } from '$lib/utils';
type $$Props = HTMLAnchorAttributes & {
external?: boolean;
};
let className: string | undefined | null = undefined;
export { className as class };
export let href: $$Props['href'] = '';
export let external = false;
</script>
<Drawer.Close asChild let:builder>
<a
use:builder.action
{href}
target={external ? '_blank' : undefined}
class={cn(external && 'flex items-center gap-0.5', className)}
{...$$restProps}
>
<slot />
{#if external}
<ArrowUpRight class="h-3 w-3" />
{/if}
</a>
</Drawer.Close>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { Menu } from 'lucide-svelte';
import { navigation } from '$lib/config';
import MobileNavItem from './mobile-nav-item.svelte';
import * as Drawer from '$lib/components/ui/drawer';
import Button from '$lib/components/ui/button/button.svelte';
</script>
<Drawer.Root>
<Drawer.Trigger asChild let:builder aria-label="open mobile menu">
<Button class="sm:hidden" builders={[builder]} variant="outline" size="icon"
><Menu class="h-4 w-4" /></Button
>
</Drawer.Trigger>
<Drawer.Content>
<div class="container mx-auto">
<Drawer.Title class="sr-only mb-4 font-medium">Navigation</Drawer.Title>
<nav class="py-4">
<ul class="flex w-full flex-col justify-center gap-4">
{#each navigation as navItem}
<li>
<MobileNavItem {navItem} />
</li>
{/each}
</ul>
</nav>
</div>
</Drawer.Content>
</Drawer.Root>

View file

@ -0,0 +1,65 @@
<script lang="ts">
import { page } from '$app/stores';
import Boring from 'svelte-boring-avatars';
import Award from 'lucide-svelte/icons/award';
import Github from 'lucide-svelte/icons/github';
import LifeBuoy from 'lucide-svelte/icons/life-buoy';
import LogOut from 'lucide-svelte/icons/log-out';
import Settings from 'lucide-svelte/icons/settings';
import Users from 'lucide-svelte/icons/users';
import * as Avatar from '$lib/components/ui/avatar';
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button builders={[builder]} variant="plain" size="icon">
<Avatar.Root>
<Avatar.Image
src="data:image;base64,{$page.data.user?.avatar}"
alt={$page.data.user?.pseudo}
/>
<Avatar.Fallback>
<Boring name={$page.data.user?.pseudo} variant="beam" />
</Avatar.Fallback>
</Avatar.Root>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-56">
<DropdownMenu.Label
>Salutation, <span class="text-primary">{$page.data.user?.pseudo}</span></DropdownMenu.Label
>
<DropdownMenu.Separator />
<DropdownMenu.Item href="/settings">
<Settings class="mr-2 h-4 w-4" />
<span>Paramètres</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item href="/badges">
<Award class="mr-2 h-4 w-4" />
<span>Mes badges</span>
</DropdownMenu.Item>
<DropdownMenu.Item href="/teams">
<Users class="mr-2 h-4 w-4" />
<span>Mes équipes</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item href="/git" target="_blank">
<Github class="mr-2 h-4 w-4" />
<span>GitHub</span>
</DropdownMenu.Item>
<DropdownMenu.Item href="/discord" target="_blank">
<LifeBuoy class="mr-2 h-4 w-4" />
<span>Discord</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item href="/logout" class="text-destructive data-[highlighted]:bg-destructive">
<LogOut class="mr-2 h-4 w-4" />
Se déconnecter
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import Breadcrumb from '$lib/components/breadcrumb.svelte';
import MobileNav from '../mobile-nav/mobile-nav.svelte';
import NavbarUser from './navbar-user.svelte';
</script>
<nav class="w-full border-b border-muted p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<MobileNav />
<Breadcrumb />
</div>
<div class="flex items-center">
<NavbarUser />
</div>
</div>
</nav>

View file

@ -0,0 +1,63 @@
<script lang="ts">
import { page } from '$app/stores';
import type { NavItemWithChildren } from '$lib/config';
import { cn } from '$lib/utils';
import SidenavLink from './sidenav-link.svelte';
export let navItem: NavItemWithChildren;
</script>
{#if navItem.children?.length}
<h5 class="mb-2 font-medium">{navItem.name}</h5>
<ul class="space-y-2">
{#each navItem.children as item}
{@const isActive = $page.url.pathname === item.href}
<li>
<SidenavLink
class={cn(
'group flex items-center gap-2 rounded border border-border border-opacity-0 p-2 font-semibold text-muted-foreground lg:leading-6',
{
'border border-opacity-100 bg-card text-foreground': isActive,
'hover:border-opacity-100 hover:bg-card hover:text-foreground': !isActive
}
)}
href={item.href}
external={item.external}
>
<svelte:component
this={item.icon}
class={cn('stroke-muted-foreground', {
'stroke-foreground': $page.url.pathname === navItem.href,
'group-hover:stroke-foreground': $page.url.pathname !== navItem.href
})}
/>
<span>{item.name}</span>
</SidenavLink>
</li>
{/each}
</ul>
{:else}
<SidenavLink
class={cn(
'group flex items-center gap-2 rounded border border-border border-opacity-0 p-2 font-semibold text-muted-foreground lg:leading-6',
{
'border border-opacity-100 bg-card text-foreground': $page.url.pathname === navItem.href,
'hover:border-opacity-100 hover:bg-card hover:text-foreground':
$page.url.pathname !== navItem.href
}
)}
href={navItem.href}
external={navItem.external}
>
<svelte:component
this={navItem.icon}
class={cn('stroke-muted-foreground', {
'stroke-foreground': $page.url.pathname === navItem.href,
'group-hover:stroke-foreground': $page.url.pathname !== navItem.href
})}
/>
<span>{navItem.name}</span>
</SidenavLink>
{/if}

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { ArrowUpRight } from 'lucide-svelte';
import type { HTMLAnchorAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
type $$Props = HTMLAnchorAttributes & {
external?: boolean;
};
let className: string | undefined | null = undefined;
export { className as class };
export let href: $$Props['href'] = '';
export let external = false;
</script>
<a
{href}
target={external ? '_blank' : undefined}
class={cn(external && 'flex items-center gap-0.5', className)}
{...$$restProps}
>
<slot />
{#if external}
<ArrowUpRight class="h-3 w-3" />
{/if}
</a>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { navigation } from '$lib/config';
import SidenavItem from './sidenav-item.svelte';
</script>
<aside
class="hidden min-w-60 overflow-hidden border-r border-border transition-all duration-300 ease-in-out sm:flex sm:flex-col"
>
<div class="flex flex-col p-4">
<img
src="/assets/brand/peerat.webp"
alt="Logo"
width="50"
height="50"
loading="eager"
draggable="false"
/>
<ul class="flex flex-col gap-2 pt-4">
{#each navigation as navItem}
<li>
<SidenavItem {navItem} />
</li>
{/each}
</ul>
</div>
</aside>

View file

@ -1,76 +0,0 @@
<script lang="ts">
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { type VariantProps, cva } from 'class-variance-authority';
import { cn } from '$lib/utils';
export const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'underline-offset-4 hover:underline text-primary',
brand: 'bg-gradient-to-tl from-brand to-brand-accent transition-opacity hover:opacity-90'
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-9 px-3 rounded-md',
lg: 'h-11 px-8 rounded-md'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
);
let className: string | undefined | null = undefined;
export { className as class };
export let href: HTMLAnchorAttributes['href'] = undefined;
export let type: HTMLButtonAttributes['type'] = undefined;
export let variant: VariantProps<typeof buttonVariants>['variant'] = 'default';
export let size: VariantProps<typeof buttonVariants>['size'] = 'default';
type Props = {
class?: string | null;
variant?: VariantProps<typeof buttonVariants>['variant'];
size?: VariantProps<typeof buttonVariants>['size'];
};
interface AnchorElement extends Props, HTMLAnchorAttributes {
href?: HTMLAnchorAttributes['href'];
type?: never;
}
interface ButtonElement extends Props, HTMLButtonAttributes {
type?: HTMLButtonAttributes['type'];
href?: never;
}
type $$Props = AnchorElement | ButtonElement;
</script>
<svelte:element
this={href ? 'a' : 'button'}
type={href ? undefined : type}
{href}
class={cn(buttonVariants({ variant, size, className }))}
{...$$restProps}
on:click
on:change
on:keydown
on:keyup
on:mouseenter
on:mouseleave
role="button"
tabindex="0"
>
<slot />
</svelte:element>

View file

@ -1,52 +0,0 @@
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
};
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props['class'] = undefined;
export let value: $$Props['value'] = undefined;
export { className as class };
</script>
<input
class={cn(
'flex h-10 w-full rounded-md border border-primary-600 bg-highlight-primary px-3 py-2 text-sm ring-offset-highlight-primary file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted focus:bg-primary-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
bind:value
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
/>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = AvatarPrimitive.FallbackProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Fallback
class={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
{...$$restProps}
>
<slot />
</AvatarPrimitive.Fallback>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = AvatarPrimitive.ImageProps;
let className: $$Props["class"] = undefined;
export let src: $$Props["src"] = undefined;
export let alt: $$Props["alt"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Image
{src}
{alt}
class={cn("aspect-square h-full w-full", className)}
{...$$restProps}
/>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = AvatarPrimitive.Props;
let className: $$Props["class"] = undefined;
export let delayMs: $$Props["delayMs"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Root
{delayMs}
class={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...$$restProps}
>
<slot />
</AvatarPrimitive.Root>

View file

@ -0,0 +1,13 @@
import Root from "./avatar.svelte";
import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
export {
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
};

View file

@ -0,0 +1,24 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import Ellipsis from "lucide-svelte/icons/ellipsis";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="presentation"
aria-hidden="true"
class={cn("flex h-9 w-9 items-center justify-center", className)}
{...$$restProps}
>
<Ellipsis class="h-4 w-4" />
<span class="sr-only">More</span>
</span>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<li bind:this={el} class={cn("inline-flex items-center gap-1.5", className)}>
<slot />
</li>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAnchorAttributes & {
el?: HTMLAnchorElement;
asChild?: boolean;
};
export let href: $$Props["href"] = undefined;
export let el: $$Props["el"] = undefined;
export let asChild: $$Props["asChild"] = false;
let className: $$Props["class"] = undefined;
export { className as class };
let attrs: Record<string, unknown>;
$: attrs = {
class: cn("transition-colors hover:text-foreground", className),
href,
...$$restProps,
};
</script>
{#if asChild}
<slot {attrs} />
{:else}
<a bind:this={el} {...attrs} {href}>
<slot {attrs} />
</a>
{/if}

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLOlAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLOlAttributes & {
el?: HTMLOListElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<ol
bind:this={el}
class={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...$$restProps}
>
<slot />
</ol>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props["el"] = undefined;
export let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="link"
aria-disabled="true"
aria-current="page"
class={cn("font-normal text-foreground", className)}
{...$$restProps}
>
<slot />
</span>

View file

@ -0,0 +1,25 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<li
role="presentation"
aria-hidden="true"
class={cn("[&>svg]:size-3.5", className)}
bind:this={el}
{...$$restProps}
>
<slot>
<ChevronRight />
</slot>
</li>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLElement> & {
el?: HTMLElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<nav class={className} bind:this={el} aria-label="breadcrumb" {...$$restProps}>
<slot />
</nav>

View file

@ -0,0 +1,25 @@
import Root from "./breadcrumb.svelte";
import Ellipsis from "./breadcrumb-ellipsis.svelte";
import Item from "./breadcrumb-item.svelte";
import Separator from "./breadcrumb-separator.svelte";
import Link from "./breadcrumb-link.svelte";
import List from "./breadcrumb-list.svelte";
import Page from "./breadcrumb-page.svelte";
export {
Root,
Ellipsis,
Item,
Separator,
Link,
List,
Page,
//
Root as Breadcrumb,
Ellipsis as BreadcrumbEllipsis,
Item as BreadcrumbItem,
Separator as BreadcrumbSeparator,
Link as BreadcrumbLink,
List as BreadcrumbList,
Page as BreadcrumbPage,
};

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { buttonVariants, type Props, type Events } from "./index.js";
type $$Props = Props;
type $$Events = Events;
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export let size: $$Props["size"] = "default";
export let builders: $$Props["builders"] = [];
export { className as class };
</script>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>

View file

@ -0,0 +1,50 @@
import Root from "./button.svelte";
import { tv, type VariantProps } from "tailwind-variants";
import type { Button as ButtonPrimitive } from "bits-ui";
const buttonVariants = tv({
base: "inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
plain: "hover:bg-transparent hover:text-primary",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
type Variant = VariantProps<typeof buttonVariants>["variant"];
type Size = VariantProps<typeof buttonVariants>["size"];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
export {
Root,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants,
};

View file

@ -0,0 +1,24 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import DrawerOverlay from "./drawer-overlay.svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Portal>
<DrawerOverlay />
<DrawerPrimitive.Content
class={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...$$restProps}
>
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
<slot />
</DrawerPrimitive.Content>
</DrawerPrimitive.Portal>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.DescriptionProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Description
bind:el
class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Description>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLDivElement> & {
el?: HTMLDivElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div bind:this={el} class={cn("mt-auto flex flex-col gap-2 p-4", className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLDivElement> & {
el?: HTMLDivElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
bind:this={el}
class={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...$$restProps}
>
<slot />
</div>

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
type $$Props = DrawerPrimitive.Props;
export let shouldScaleBackground: $$Props["shouldScaleBackground"] = true;
export let open: $$Props["open"] = false;
export let activeSnapPoint: $$Props["activeSnapPoint"] = undefined;
</script>
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...$$restProps}>
<slot />
</DrawerPrimitive.NestedRoot>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.OverlayProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Overlay
bind:el
class={cn("fixed inset-0 z-50 bg-black/80", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Overlay>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.TitleProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Title
bind:el
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Title>

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
type $$Props = DrawerPrimitive.Props;
export let shouldScaleBackground: $$Props["shouldScaleBackground"] = true;
export let open: $$Props["open"] = false;
export let activeSnapPoint: $$Props["activeSnapPoint"] = undefined;
</script>
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...$$restProps}>
<slot />
</DrawerPrimitive.Root>

View file

@ -0,0 +1,41 @@
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import Root from "./drawer.svelte";
import Content from "./drawer-content.svelte";
import Description from "./drawer-description.svelte";
import Overlay from "./drawer-overlay.svelte";
import Footer from "./drawer-footer.svelte";
import Header from "./drawer-header.svelte";
import Title from "./drawer-title.svelte";
import NestedRoot from "./drawer-nested.svelte";
const Trigger = DrawerPrimitive.Trigger;
const Portal = DrawerPrimitive.Portal;
const Close = DrawerPrimitive.Close;
export {
Root,
NestedRoot,
Content,
Description,
Overlay,
Footer,
Header,
Title,
Trigger,
Portal,
Close,
//
Root as Drawer,
NestedRoot as DrawerNestedRoot,
Content as DrawerContent,
Description as DrawerDescription,
Overlay as DrawerOverlay,
Footer as DrawerFooter,
Header as DrawerHeader,
Title as DrawerTitle,
Trigger as DrawerTrigger,
Portal as DrawerPortal,
Close as DrawerClose,
};

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:checked
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" />
</DropdownMenuPrimitive.CheckboxIndicator>
</span>
<slot />
</DropdownMenuPrimitive.CheckboxItem>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.ContentProps;
type $$Events = DropdownMenuPrimitive.ContentEvents;
let className: $$Props["class"] = undefined;
export let sideOffset: $$Props["sideOffset"] = 4;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Content
{transition}
{transitionConfig}
{sideOffset}
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none",
className
)}
{...$$restProps}
on:keydown
>
<slot />
</DropdownMenuPrimitive.Content>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Item
class={cn(
"relative flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<slot />
</DropdownMenuPrimitive.Item>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.LabelProps & {
inset?: boolean;
};
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Label
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...$$restProps}
>
<slot />
</DropdownMenuPrimitive.Label>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
export let value: $$Props["value"] = undefined;
</script>
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
<slot />
</DropdownMenuPrimitive.RadioGroup>

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<DropdownMenuPrimitive.RadioItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.RadioIndicator>
<Circle class="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.RadioIndicator>
</span>
<slot />
</DropdownMenuPrimitive.RadioItem>

View file

@ -0,0 +1,14 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SeparatorProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", className)}
{...$$restProps}
/>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span class={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...$$restProps}>
<slot />
</span>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SubContentProps;
type $$Events = DropdownMenuPrimitive.SubContentEvents;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
x: -10,
y: 0,
};
export { className as class };
</script>
<DropdownMenuPrimitive.SubContent
{transition}
{transitionConfig}
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none",
className
)}
{...$$restProps}
on:keydown
on:focusout
on:pointermove
>
<slot />
</DropdownMenuPrimitive.SubContent>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.SubTrigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>

View file

@ -0,0 +1,48 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import Content from "./dropdown-menu-content.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
const Trigger = DropdownMenuPrimitive.Trigger;
const Group = DropdownMenuPrimitive.Group;
export {
Sub,
Root,
Item,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Root as DropdownMenu,
Sub as DropdownMenuSub,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
Group as DropdownMenuGroup,
Content as DropdownMenuContent,
Trigger as DropdownMenuTrigger,
Shortcut as DropdownMenuShortcut,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
RadioGroup as DropdownMenuRadioGroup,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
CheckboxItem as DropdownMenuCheckboxItem,
};

View file

@ -0,0 +1,10 @@
<script lang="ts">
import * as Button from "$lib/components/ui/button/index.js";
type $$Props = Button.Props;
type $$Events = Button.Events;
</script>
<Button.Root type="submit" on:click on:keydown {...$$restProps}>
<slot />
</Button.Root>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<FormPrimitive.Description
class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
let:descriptionAttrs
>
<slot {descriptionAttrs} />
</FormPrimitive.Description>

View file

@ -0,0 +1,26 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.ElementField {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.ElementField>

View file

@ -0,0 +1,26 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;
};
let className: $$Props["class"] = undefined;
export { className as class };
export let errorClasses: $$Props["class"] = undefined;
</script>
<FormPrimitive.FieldErrors
class={cn("text-sm font-medium text-destructive", className)}
{...$$restProps}
let:errors
let:fieldErrorsAttrs
let:errorAttrs
>
<slot {errors} {fieldErrorsAttrs} {errorAttrs}>
{#each errors as error}
<div {...errorAttrs} class={cn(errorClasses)}>{error}</div>
{/each}
</slot>
</FormPrimitive.FieldErrors>

View file

@ -0,0 +1,26 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Field {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.Field>

View file

@ -0,0 +1,31 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldsetProps<T, U>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Fieldset
{form}
{name}
let:constraints
let:errors
let:tainted
let:value
class={cn("space-y-2", className)}
>
<slot {constraints} {errors} {tainted} {value} />
</FormPrimitive.Fieldset>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import type { Label as LabelPrimitive } from 'bits-ui';
import { getFormControl } from 'formsnap';
import { cn } from '$lib/utils.js';
import { Label } from '$lib/components/ui/label/index.js';
type $$Props = LabelPrimitive.Props;
let className: $$Props['class'] = undefined;
export { className as class };
const { labelAttrs } = getFormControl();
</script>
<Label {...$labelAttrs} class={cn('data-[fs-error]:text-destructive', className)} {...$$restProps}>
<slot {labelAttrs} />
</Label>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.LegendProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Legend
{...$$restProps}
class={cn("text-sm font-medium leading-none data-[fs-error]:text-destructive", className)}
let:legendAttrs
>
<slot {legendAttrs} />
</FormPrimitive.Legend>

View file

@ -0,0 +1,33 @@
import * as FormPrimitive from "formsnap";
import Description from "./form-description.svelte";
import Label from "./form-label.svelte";
import FieldErrors from "./form-field-errors.svelte";
import Field from "./form-field.svelte";
import Fieldset from "./form-fieldset.svelte";
import Legend from "./form-legend.svelte";
import ElementField from "./form-element-field.svelte";
import Button from "./form-button.svelte";
const Control = FormPrimitive.Control;
export {
Field,
Control,
Label,
Button,
FieldErrors,
Description,
Fieldset,
Legend,
ElementField,
//
Field as FormField,
Control as FormControl,
Description as FormDescription,
Label as FormLabel,
FieldErrors as FormFieldErrors,
Fieldset as FormFieldset,
Legend as FormLegend,
ElementField as FormElementField,
Button as FormButton,
};

View file

@ -0,0 +1,27 @@
import Root from "./input.svelte";
export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
};
export {
Root,
//
Root as Input,
};

View file

@ -0,0 +1,35 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
import type { InputEvents } from "./index.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
</script>
<input
class={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
/>

View file

@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<LabelPrimitive.Root
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}
on:mousedown
>
<slot />
</LabelPrimitive.Root>

View file

@ -0,0 +1 @@
export { default as Toaster } from "./sonner.svelte";

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import { mode } from "mode-watcher";
type $$Props = SonnerProps;
</script>
<Sonner
theme={$mode}
class="toaster group"
toastOptions={{
classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...$$restProps}
/>

2
src/lib/config/index.ts Normal file
View file

@ -0,0 +1,2 @@
export * from './site';
export * from './navigation';

View file

@ -0,0 +1,57 @@
import type { ComponentType } from "svelte";
import type { Icon } from "lucide-svelte";
import BarChart2 from "lucide-svelte/icons/bar-chart-2";
import Code from "lucide-svelte/icons/code";
import Github from "lucide-svelte/icons/github";
import LayoutDashboard from "lucide-svelte/icons/layout-dashboard";
import LifeBuoy from "lucide-svelte/icons/life-buoy";
export type NavItem = {
name: string;
icon?: ComponentType<Icon>;
href?: string;
disabled?: boolean;
external?: boolean;
}
export type NavItemWithChildren = NavItem & {
children?: NavItemWithChildren[];
}
export type Navigation = NavItem[];
export const navigation: NavItemWithChildren[] = [
{
name: "Dashboard",
href: "/",
icon: LayoutDashboard
},
{
name: "Classement",
href: "/leaderboard",
icon: BarChart2
},
{
name: "Challenges",
href: "/chapters",
icon: Code
},
{
name: "Documentation",
children: [
{
name: "Git",
href: "/git",
external: true,
icon: Github
},
{
name: "Discord",
href: "/discord",
external: true,
icon: LifeBuoy
}
]
}
]

View file

@ -1,14 +1,13 @@
export const siteConfig = {
name: 'Peer-at Code',
url: 'https://app.peerat.dev',
title: 'Peer-at Code',
description: 'Apprendre la programmation et la cybersécurité en s\'amusant.',
imageUrl: '',
keywords: ['peerat', 'code', 'cybersecurite', 'programmation', 'apprendre en s\'amusant'],
keywords: ['peerat', 'code', 'cybersecurite', 'programmation', "apprendre en s'amusant"],
author: 'peerat',
locale: 'fr',
twitter: {
creator: '@peerat'
links: {
github: "https://git.peerat.dev",
discord: "https://discord.gg/72vuHcwUkE",
},
themeColor: '#110F15'
};

View file

@ -1,6 +1,62 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
export const cn = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs));
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, "");
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};

View file

@ -0,0 +1,84 @@
import { z } from 'zod';
export const loginSchema = z.object({
pseudo: z.string({ required_error: "Nom d'utilisateur requis", })
.trim()
.min(1, { message: "Nom d'utilisateur requis" }),
passwd: z.string({ required_error: 'Mot de passe requis' })
.trim()
.min(1, { message: 'Mot de passe requis' }),
});
export const registerSchema = z.object({
email: z
.string({
required_error: 'Email requis'
})
.trim()
.max(64, {
message: 'Email trop long (max 64 caractères)'
})
.email({
message: 'Email invalide'
}),
firstname: z.string()
.trim(),
lastname: z.string()
.trim(),
pseudo: z.string({
required_error: 'Nom d\'utilisateur requis'
}).trim(),
});
export const registerConfirmationSchema = z.object({
email: z
.string({
required_error: 'Email requis'
})
.trim()
.max(64, {
message: 'Email trop long (max 64 caractères)'
})
.email({
message: 'Email invalide'
}),
firstname: z.string()
.trim(),
lastname: z.string()
.trim(),
pseudo: z.string({
required_error: 'Nom d\'utilisateur requis'
}).trim(),
passwd: z.string({
required_error: 'Mot de passe requis'
})
.trim()
.min(1, { message: 'Mot de passe requis' }),
code: z.string({
required_error: 'Code manquant'
})
.regex(/^[0-9]{4}$/, { message: 'Code invalide, il doit contenir 4 chiffres' })
.trim(),
});
export const requestPasswordResetSchema = z.object({
email: z.string({ required_error: 'Email requis' })
.trim()
.email({ message: 'Email invalide' })
.min(1, { message: 'Email requis' })
});
export const resetPasswordSchema = z.object({
email: z.string({ required_error: 'Email requis' })
.trim()
.email({ message: 'Email invalide' })
.min(1, { message: 'Email requis' }),
password: z.string({ required_error: 'Mot de passe requis' })
.trim()
.min(1, { message: 'Mot de passe requis' }),
code: z.string({
required_error: 'Code manquant'
})
.regex(/^[0-9]{4}$/, { message: 'Code invalide, il doit contenir 4 chiffres' }),
});

View file

@ -0,0 +1,8 @@
import { z } from 'zod';
export const puzzleSchema = z.object({
answer: z.string({
required_error: 'Une réponse est requise',
})
.min(1, 'Une réponse est requise')
});

View file

@ -0,0 +1,6 @@
import { redirect, type ServerLoad } from '@sveltejs/kit';
export const load: ServerLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) redirect(302, '/login');
};

View file

@ -0,0 +1,23 @@
<script class="ts">
import { navigating } from '$app/stores';
import { Loader, Navbar, Sidenav } from '$lib/components/layout';
</script>
{#if $navigating}
<Loader />
{/if}
<div class="flex h-screen w-full flex-col">
<div class="flex flex-1 overflow-hidden">
<Sidenav />
<div class="flex flex-1 flex-col">
<Navbar />
<div
class="flex w-full flex-1 transform flex-col overflow-y-auto p-4 duration-300 ease-in-out"
>
<slot />
</div>
</div>
</div>
</div>

View file

@ -1,11 +1,13 @@
import { API_URL } from '$env/static/private';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import type { Chapter } from '$lib/types';
export const load = (async ({ parent, fetch, cookies }) => {
await parent();
export const load = (async ({ fetch, cookies, locals: { user } }) => {
if (!user) redirect(302, '/login');
const session = cookies.get('session');
@ -21,7 +23,7 @@ export const load = (async ({ parent, fetch, cookies }) => {
};
}
const chapters = (await res.json()) as Chapter[];
const chapters: Chapter[] = await res.json();
const lastChapter = chapters.filter((chapter) => chapter.show).pop();
@ -51,6 +53,7 @@ export const load = (async ({ parent, fetch, cookies }) => {
const lastPuzzle = chapter.puzzles.filter((puzzle) => puzzle.show).pop();
return {
title: 'Dashboard',
daily: {
chapter: lastChapter,
puzzle: lastPuzzle

View file

@ -1,8 +1,7 @@
<script lang="ts">
import type { PageData } from './$types';
import Card from '$lib/components/Card.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Card from '$lib/components/card.svelte';
export let data: PageData;
@ -12,7 +11,7 @@
<section class="flex w-full flex-col gap-4">
<header>
<h1 class="text-xl font-semibold">Tableau de bord</h1>
<p class="text-highlight-secondary">Ceci est la page d&apos;accueil du dashboard</p>
<p class="text-muted-foreground">Ceci est la page d&apos;accueil du dashboard</p>
</header>
<main class="flex flex-col gap-4">
<div
@ -25,28 +24,25 @@
{#if data.daily && data.daily.puzzle}
<header>
<h1 class="text-lg font-semibold">Puzzle du jour</h1>
<p class="text-highlight-secondary">
<p class="text-muted-foreground">
Essayer de résoudre le puzzle du jour pour gagner des points supplémentaires
</p>
</header>
<div
class="flex items-center justify-between gap-4 rounded-lg border-2 border-brand/40 bg-primary-700 px-4 py-2"
class="flex items-center justify-between gap-4 rounded border border-border bg-card px-4 py-2"
>
<div class="flex w-full flex-col justify-between md:flex-row md:items-center md:gap-4">
<span class="text-lg font-semibold">{data.daily.chapter.name}</span>
<span class="text-highlight-secondary">
<span class="text-muted-foreground">
{data.daily.puzzle.name} ({data.daily.puzzle.score ?? '?'}/{data.daily.puzzle.scoreMax})
</span>
</div>
<div class="flex items-center gap-4">
<Button
variant="brand"
href="/dashboard/chapters/{data.daily.chapter.id}/puzzle/{data.daily.puzzle.id}"
>
<a href="/chapters/{data.daily.chapter.id}/puzzle/{data.daily.puzzle.id}">
<span class="text-lg font-semibold">
{data.daily.puzzle.score ? 'Voir' : 'Jouer'}
</span>
</Button>
</a>
</div>
</div>
{/if}
@ -54,15 +50,15 @@
<div class="flex flex-col gap-4">
<header>
<h2 class="text-lg font-semibold">Derniers puzzles</h2>
<p class="text-highlight-secondary">
<p class="text-muted-foreground">
Voici les derniers puzzles que vous avez résolus ou essayer de résoudres
</p>
</header>
<div
class="h-full max-h-96 overflow-y-scroll rounded-lg border-2 border-highlight-primary bg-primary-700 p-4 shadow-md"
class="border bg-card h-full max-h-96 overflow-y-scroll rounded border-border p-4 shadow-md"
>
<ul class="flex flex-col space-y-2">
{#if user?.completionsList && user.completionsList.length > 0}
{#if user?.completionsList?.length}
{#each user.completionsList as completion, key}
<li class="flex justify-between space-x-2">
<div class="flex items-center space-x-4">
@ -75,13 +71,13 @@
<span class="text-sm font-semibold">
Essai{completion.tries > 1 ? 's' : ''}
</span>
<span class="text-right text-lg text-highlight-secondary">
<span class="text-right text-lg text-muted-foreground">
{completion.tries}
</span>
</div>
<div class="flex flex-col">
<span class="text-sm font-semibold">Score</span>
<span class="text-right text-lg text-highlight-secondary">
<span class="text-right text-lg text-muted-foreground">
{completion.score}
</span>
</div>
@ -90,7 +86,7 @@
{/each}
{:else}
<li class="m-auto flex items-center justify-center">
<span class="text-lg text-highlight-secondary"> Aucun puzzles </span>
<span class="text-lg text-muted-foreground"> Aucun puzzles </span>
</li>
{/if}
</ul>

Some files were not shown because too many files have changed in this diff Show more