refactor: huge ui/ux refactor
This commit is contained in:
parent
238c02b328
commit
acb176aee3
150 changed files with 4169 additions and 3144 deletions
14
components.json
Normal file
14
components.json
Normal 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
|
||||||
|
}
|
51
package.json
51
package.json
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "peer-at-code",
|
"name": "peer-at-code",
|
||||||
"version": "0.1.0",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -16,37 +15,45 @@
|
||||||
"test:unit": "vitest"
|
"test:unit": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bits-ui": "^0.20.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"lucide-svelte": "^0.330.0",
|
"formsnap": "^0.5.1",
|
||||||
"marked": "^12.0.0",
|
"lucide-svelte": "^0.363.0",
|
||||||
"svelte-boring-avatars": "^1.2.5",
|
"mode-watcher": "^0.3.0",
|
||||||
"svelte-sonner": "^0.3.18",
|
"svelte-boring-avatars": "^1.2.6",
|
||||||
"tailwind-merge": "^2.2.1"
|
"svelte-sonner": "^0.3.19",
|
||||||
|
"tailwind-merge": "^2.2.2",
|
||||||
|
"tailwind-variants": "^0.2.1",
|
||||||
|
"vaul-svelte": "^0.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.41.2",
|
"@playwright/test": "^1.42.1",
|
||||||
"@sveltejs/adapter-node": "^4.0.1",
|
"@sveltejs/adapter-node": "^5.0.1",
|
||||||
"@sveltejs/kit": "^2.5.0",
|
"@sveltejs/kit": "^2.5.4",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@typescript-eslint/parser": "^7.0.1",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"autoprefixer": "^10.4.17",
|
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||||
"eslint": "^8.56.0",
|
"@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-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.35.1",
|
"eslint-plugin-svelte": "^2.35.1",
|
||||||
"postcss": "^8.4.35",
|
"mdsvex": "^0.11.0",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-svelte": "^3.1.2",
|
"prettier-plugin-svelte": "^3.2.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
"prettier-plugin-tailwindcss": "^0.5.12",
|
||||||
"svelte": "^4.2.10",
|
"svelte": "^4.2.12",
|
||||||
"svelte-check": "^3.6.4",
|
"svelte-check": "^3.6.8",
|
||||||
"sveltekit-superforms": "^2.1.0",
|
"sveltekit-superforms": "^2.11.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.4.3",
|
||||||
"vite": "^5.1.1",
|
"vite": "^5.2.6",
|
||||||
"vitest": "^1.2.2",
|
"vitest": "^1.4.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1716
pnpm-lock.yaml
generated
1716
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
156
src/app.css
156
src/app.css
|
@ -1,107 +1,105 @@
|
||||||
@import 'tailwindcss/base';
|
@tailwind base;
|
||||||
@import 'tailwindcss/components';
|
@tailwind components;
|
||||||
@import 'tailwindcss/utilities';
|
@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 {
|
@layer base {
|
||||||
:root {
|
@font-face {
|
||||||
--background: 0 0% 100%;
|
font-family: 'Karrik';
|
||||||
--foreground: 222.2 84% 4.9%;
|
src: url('/fonts/Karrik.woff2');
|
||||||
|
|
||||||
--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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
@font-face {
|
||||||
--background: 222.2 84% 4.9%;
|
font-family: 'Fira Code';
|
||||||
--foreground: 210 40% 98%;
|
src: url('/fonts/FiraCode.woff2');
|
||||||
|
}
|
||||||
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
:root {
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--gradient: linear-gradient(to top left, hsl(258 85% 67%), hsl(258 60% 40%));
|
||||||
|
|
||||||
--popover: 222.2 84% 4.9%;
|
--background: 255 14.81% 10.59%;
|
||||||
--popover-foreground: 210 40% 98%;
|
--foreground: 284 4.7% 97.05%;
|
||||||
|
|
||||||
--card: 222.2 84% 4.9%;
|
--muted: 258 15% 17%;
|
||||||
--card-foreground: 210 40% 98%;
|
--muted-foreground: 284 4.7% 54.1%;
|
||||||
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
--popover: 256.36 14.29% 15.1%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--popover-foreground: 284 4.7% 97.05%;
|
||||||
|
|
||||||
--primary: 210 40% 98%;
|
--card: 256.36 14.29% 15.1%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--card-foreground: 284 4.7% 97.05%;
|
||||||
|
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--border: 258 15% 17%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--input: 258 15% 17%;
|
||||||
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
--primary: 258 85% 67%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--primary-foreground: 284 4.7% 97.05%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--secondary: 258 60% 40%;
|
||||||
--destructive-foreground: 0 85.7% 97.3%;
|
--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 {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
@apply border-border;
|
||||||
scroll-behavior: smooth;
|
|
||||||
@apply border-border text-white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
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 {
|
@layer components {
|
||||||
.console {
|
.bg-gradient {
|
||||||
@apply relative top-0.5 inline-block;
|
background: var(--gradient);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
src/app.html
18
src/app.html
|
@ -1,9 +1,15 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr" style="color-scheme: dark;">
|
||||||
<head>
|
|
||||||
|
<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%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover" class="relative min-h-screen">
|
|
||||||
<main style="display: contents">%sveltekit.body%</main>
|
<body data-sveltekit-preload-data="hover" class="min-h-screen antialiased font-sans text-base">
|
||||||
</body>
|
<div style="display: contents" class="relative flex min-h-screen flex-col">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -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}
|
|
|
@ -4,13 +4,13 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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 w-full items-center justify-between">
|
||||||
<div class="flex-col">
|
<div class="">
|
||||||
<h2 class="text-xl font-semibold">{data}</h2>
|
<h2 class="text-xl font-semibold">{data}</h2>
|
||||||
<p class="text-muted">{title}</p>
|
<p class="text-muted-foreground">{title}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
<script lang="ts">
|
<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 type { Chapter } from '$lib/types';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils';
|
||||||
|
|
||||||
import ChevronRight from './Icons/ChevronRight.svelte';
|
|
||||||
|
|
||||||
export let chapter: Chapter;
|
export let chapter: Chapter;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
class={cn(
|
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
|
'opacity-50': !chapter.show
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
@ -21,14 +20,14 @@
|
||||||
{#if chapter.show}
|
{#if chapter.show}
|
||||||
<a
|
<a
|
||||||
class="flex h-full w-full items-center justify-between gap-4 p-4"
|
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">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-base font-semibold">
|
<span class="font-semibold">
|
||||||
{chapter.name}
|
{chapter.name}
|
||||||
</span>
|
</span>
|
||||||
{#if chapter.id === 1}
|
{#if chapter.id === 1}
|
||||||
<Trophy class="stroke-highlight-secondary" />
|
<Swords class="stroke-muted-foreground" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<span class="translate-x-0 transform-gpu duration-300 group-hover:translate-x-2">
|
<span class="translate-x-0 transform-gpu duration-300 group-hover:translate-x-2">
|
||||||
|
@ -38,7 +37,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<span class="flex h-full w-full items-center gap-4 p-4">
|
<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">
|
<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}
|
{chapter.name}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -2,48 +2,49 @@
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
import { siteConfig } from '$lib/config/site';
|
import { siteConfig } from '$lib/config/site';
|
||||||
|
|
||||||
|
export let title = siteConfig.name;
|
||||||
|
|
||||||
|
$: title = $page.data?.title ? `${$page.data.title} | ${siteConfig.name}` : siteConfig.name;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
{#key $page.url.pathname}
|
<title>{title}</title>
|
||||||
<title>{siteConfig.title}</title>
|
<meta name="title" content={title} />
|
||||||
<meta name="title" content={siteConfig.title} />
|
|
||||||
<meta name="description" content={siteConfig.description} />
|
<meta name="description" content={siteConfig.description} />
|
||||||
<meta name="keywords" content={siteConfig.keywords.join(',')} />
|
<meta name="keywords" content={siteConfig.keywords.join(',')} />
|
||||||
<meta name="author" content={siteConfig.author} />
|
<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="name" content={title} />
|
||||||
<meta itemprop="description" content={siteConfig.description} />
|
<meta itemprop="description" content={siteConfig.description} />
|
||||||
<meta itemprop="image" content={siteConfig.imageUrl} />
|
<meta itemprop="image" content={siteConfig.imageUrl} />
|
||||||
|
|
||||||
<meta property="og:site_name" content={siteConfig.name} />
|
<meta property="og:site_name" content={siteConfig.name} />
|
||||||
<meta property="og:title" content={siteConfig.title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:description" content={siteConfig.description} />
|
<meta property="og:description" content={siteConfig.description} />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content={siteConfig.url + $page.url.pathname} />
|
<meta property="og:url" content={siteConfig.url + $page.url.pathname} />
|
||||||
<meta property="og:image" content={siteConfig.imageUrl} />
|
<meta property="og:image" content={siteConfig.imageUrl} />
|
||||||
<meta property="og:image:alt" content={siteConfig.title} />
|
<meta property="og:image:alt" content={title} />
|
||||||
<meta property="og:locale" content={siteConfig.locale} />
|
<meta property="og:locale" content="fr" />
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:site" content={siteConfig.url} />
|
<meta name="twitter:site" content={siteConfig.url} />
|
||||||
<meta name="twitter:title" content={siteConfig.title} />
|
<meta name="twitter:title" content={title} />
|
||||||
<meta name="twitter:description" content={siteConfig.description} />
|
<meta name="twitter:description" content={siteConfig.description} />
|
||||||
<meta name="twitter:image" content={siteConfig.imageUrl} />
|
<meta name="twitter:image" content={siteConfig.imageUrl} />
|
||||||
<meta name="twitter:image:alt" content={siteConfig.title} />
|
<meta name="twitter:image:alt" content={title} />
|
||||||
<meta name="twitter:creator" content={siteConfig.twitter.creator} />
|
<meta name="twitter:creator" content={`@${siteConfig.author}`} />
|
||||||
|
|
||||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
|
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<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="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="32x32" href="/assets/icons/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
||||||
{/key}
|
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
|
@ -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>
|
|
|
@ -1,25 +1,24 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
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 type { Puzzle } from '$lib/types';
|
||||||
import ChevronRight from './Icons/ChevronRight.svelte';
|
import { cn } from '$lib/utils';
|
||||||
|
|
||||||
export let puzzle: Puzzle;
|
export let puzzle: Puzzle;
|
||||||
|
|
||||||
const chapterId = $page.params.chapterId;
|
const chapterId = $page.params.chapterId;
|
||||||
|
|
||||||
$: tags = puzzle.tags?.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
class={cn(
|
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-green-500/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'easy'),
|
||||||
'border-yellow-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'medium'),
|
'border-yellow-500/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'medium'),
|
||||||
'border-red-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'hard'),
|
'border-red-500/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'hard'),
|
||||||
'border-highlight-primary': !puzzle.tags?.length,
|
'hover:bg-card/80': puzzle.show,
|
||||||
'hover:bg-primary-600': puzzle.show,
|
|
||||||
'opacity-50': !puzzle.show
|
'opacity-50': !puzzle.show
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
@ -27,12 +26,12 @@
|
||||||
{#if puzzle.show}
|
{#if puzzle.show}
|
||||||
<a
|
<a
|
||||||
class="flex h-full w-full items-center gap-4 p-4"
|
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">
|
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
|
||||||
<h2 class="text-base font-semibold">
|
<h2 class="text-base font-semibold">
|
||||||
{puzzle.name}
|
{puzzle.name}
|
||||||
<span class="text-sm text-highlight-secondary">
|
<span class="text-highlight-secondary text-sm">
|
||||||
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
|
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -40,9 +39,7 @@
|
||||||
<div class="flex items-center gap-x-6">
|
<div class="flex items-center gap-x-6">
|
||||||
<div class="flex gap-x-2 text-sm">
|
<div class="flex gap-x-2 text-sm">
|
||||||
{#each puzzle.tags as tag}
|
{#each puzzle.tags as tag}
|
||||||
<span
|
<span class="inline-block rounded bg-muted px-2 py-1 text-muted-foreground">
|
||||||
class="inline-block rounded-md bg-primary-800 px-2 py-1 text-highlight-secondary"
|
|
||||||
>
|
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -59,7 +56,7 @@
|
||||||
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
|
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
|
||||||
<h2 class="text-base font-semibold">
|
<h2 class="text-base font-semibold">
|
||||||
{puzzle.name}
|
{puzzle.name}
|
||||||
<span class="text-sm text-highlight-secondary">
|
<span class="text-highlight-secondary text-sm">
|
||||||
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
|
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -67,9 +64,7 @@
|
||||||
{#if puzzle.tags?.length}
|
{#if puzzle.tags?.length}
|
||||||
<div class="flex gap-x-2 text-sm">
|
<div class="flex gap-x-2 text-sm">
|
||||||
{#each puzzle.tags as tag}
|
{#each puzzle.tags as tag}
|
||||||
<span
|
<span class="inline-block rounded bg-muted px-2 py-1 text-muted-foreground">
|
||||||
class="inline-block rounded-md bg-primary-800 px-2 py-1 text-highlight-secondary"
|
|
||||||
>
|
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
|
28
src/lib/components/breadcrumb.svelte
Normal file
28
src/lib/components/breadcrumb.svelte
Normal 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>
|
31
src/lib/components/copy-code-button.svelte
Normal file
31
src/lib/components/copy-code-button.svelte
Normal 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>
|
47
src/lib/components/copy-code-injector.svelte
Normal file
47
src/lib/components/copy-code-injector.svelte
Normal 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 />
|
4
src/lib/components/index.ts
Normal file
4
src/lib/components/index.ts
Normal 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';
|
3
src/lib/components/layout/index.ts
Normal file
3
src/lib/components/layout/index.ts
Normal 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';
|
42
src/lib/components/layout/loader/loader.svelte
Normal file
42
src/lib/components/layout/loader/loader.svelte
Normal 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>
|
63
src/lib/components/layout/mobile-nav/mobile-nav-item.svelte
Normal file
63
src/lib/components/layout/mobile-nav/mobile-nav-item.svelte
Normal 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}
|
32
src/lib/components/layout/mobile-nav/mobile-nav-link.svelte
Normal file
32
src/lib/components/layout/mobile-nav/mobile-nav-link.svelte
Normal 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>
|
32
src/lib/components/layout/mobile-nav/mobile-nav.svelte
Normal file
32
src/lib/components/layout/mobile-nav/mobile-nav.svelte
Normal 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>
|
65
src/lib/components/layout/navbar/navbar-user.svelte
Normal file
65
src/lib/components/layout/navbar/navbar-user.svelte
Normal 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>
|
17
src/lib/components/layout/navbar/navbar.svelte
Normal file
17
src/lib/components/layout/navbar/navbar.svelte
Normal 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>
|
63
src/lib/components/layout/sidenav/sidenav-item.svelte
Normal file
63
src/lib/components/layout/sidenav/sidenav-item.svelte
Normal 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}
|
28
src/lib/components/layout/sidenav/sidenav-link.svelte
Normal file
28
src/lib/components/layout/sidenav/sidenav-link.svelte
Normal 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>
|
27
src/lib/components/layout/sidenav/sidenav.svelte
Normal file
27
src/lib/components/layout/sidenav/sidenav.svelte
Normal 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>
|
|
@ -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>
|
|
|
@ -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}
|
|
||||||
/>
|
|
16
src/lib/components/ui/avatar/avatar-fallback.svelte
Normal file
16
src/lib/components/ui/avatar/avatar-fallback.svelte
Normal 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>
|
18
src/lib/components/ui/avatar/avatar-image.svelte
Normal file
18
src/lib/components/ui/avatar/avatar-image.svelte
Normal 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}
|
||||||
|
/>
|
18
src/lib/components/ui/avatar/avatar.svelte
Normal file
18
src/lib/components/ui/avatar/avatar.svelte
Normal 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>
|
13
src/lib/components/ui/avatar/index.ts
Normal file
13
src/lib/components/ui/avatar/index.ts
Normal 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,
|
||||||
|
};
|
24
src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte
Normal file
24
src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte
Normal 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>
|
16
src/lib/components/ui/breadcrumb/breadcrumb-item.svelte
Normal file
16
src/lib/components/ui/breadcrumb/breadcrumb-item.svelte
Normal 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>
|
31
src/lib/components/ui/breadcrumb/breadcrumb-link.svelte
Normal file
31
src/lib/components/ui/breadcrumb/breadcrumb-link.svelte
Normal 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}
|
23
src/lib/components/ui/breadcrumb/breadcrumb-list.svelte
Normal file
23
src/lib/components/ui/breadcrumb/breadcrumb-list.svelte
Normal 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>
|
23
src/lib/components/ui/breadcrumb/breadcrumb-page.svelte
Normal file
23
src/lib/components/ui/breadcrumb/breadcrumb-page.svelte
Normal 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>
|
25
src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte
Normal file
25
src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte
Normal 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>
|
15
src/lib/components/ui/breadcrumb/breadcrumb.svelte
Normal file
15
src/lib/components/ui/breadcrumb/breadcrumb.svelte
Normal 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>
|
25
src/lib/components/ui/breadcrumb/index.ts
Normal file
25
src/lib/components/ui/breadcrumb/index.ts
Normal 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,
|
||||||
|
};
|
25
src/lib/components/ui/button/button.svelte
Normal file
25
src/lib/components/ui/button/button.svelte
Normal 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>
|
50
src/lib/components/ui/button/index.ts
Normal file
50
src/lib/components/ui/button/index.ts
Normal 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,
|
||||||
|
};
|
24
src/lib/components/ui/drawer/drawer-content.svelte
Normal file
24
src/lib/components/ui/drawer/drawer-content.svelte
Normal 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>
|
18
src/lib/components/ui/drawer/drawer-description.svelte
Normal file
18
src/lib/components/ui/drawer/drawer-description.svelte
Normal 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>
|
16
src/lib/components/ui/drawer/drawer-footer.svelte
Normal file
16
src/lib/components/ui/drawer/drawer-footer.svelte
Normal 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>
|
19
src/lib/components/ui/drawer/drawer-header.svelte
Normal file
19
src/lib/components/ui/drawer/drawer-header.svelte
Normal 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>
|
12
src/lib/components/ui/drawer/drawer-nested.svelte
Normal file
12
src/lib/components/ui/drawer/drawer-nested.svelte
Normal 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>
|
18
src/lib/components/ui/drawer/drawer-overlay.svelte
Normal file
18
src/lib/components/ui/drawer/drawer-overlay.svelte
Normal 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>
|
18
src/lib/components/ui/drawer/drawer-title.svelte
Normal file
18
src/lib/components/ui/drawer/drawer-title.svelte
Normal 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>
|
12
src/lib/components/ui/drawer/drawer.svelte
Normal file
12
src/lib/components/ui/drawer/drawer.svelte
Normal 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>
|
41
src/lib/components/ui/drawer/index.ts
Normal file
41
src/lib/components/ui/drawer/index.ts
Normal 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,
|
||||||
|
};
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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}
|
||||||
|
/>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
48
src/lib/components/ui/dropdown-menu/index.ts
Normal file
48
src/lib/components/ui/dropdown-menu/index.ts
Normal 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,
|
||||||
|
};
|
10
src/lib/components/ui/form/form-button.svelte
Normal file
10
src/lib/components/ui/form/form-button.svelte
Normal 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>
|
17
src/lib/components/ui/form/form-description.svelte
Normal file
17
src/lib/components/ui/form/form-description.svelte
Normal 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>
|
26
src/lib/components/ui/form/form-element-field.svelte
Normal file
26
src/lib/components/ui/form/form-element-field.svelte
Normal 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>
|
26
src/lib/components/ui/form/form-field-errors.svelte
Normal file
26
src/lib/components/ui/form/form-field-errors.svelte
Normal 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>
|
26
src/lib/components/ui/form/form-field.svelte
Normal file
26
src/lib/components/ui/form/form-field.svelte
Normal 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>
|
31
src/lib/components/ui/form/form-fieldset.svelte
Normal file
31
src/lib/components/ui/form/form-fieldset.svelte
Normal 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>
|
17
src/lib/components/ui/form/form-label.svelte
Normal file
17
src/lib/components/ui/form/form-label.svelte
Normal 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>
|
17
src/lib/components/ui/form/form-legend.svelte
Normal file
17
src/lib/components/ui/form/form-legend.svelte
Normal 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>
|
33
src/lib/components/ui/form/index.ts
Normal file
33
src/lib/components/ui/form/index.ts
Normal 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,
|
||||||
|
};
|
27
src/lib/components/ui/input/index.ts
Normal file
27
src/lib/components/ui/input/index.ts
Normal 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,
|
||||||
|
};
|
35
src/lib/components/ui/input/input.svelte
Normal file
35
src/lib/components/ui/input/input.svelte
Normal 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}
|
||||||
|
/>
|
7
src/lib/components/ui/label/index.ts
Normal file
7
src/lib/components/ui/label/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import Root from "./label.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Label,
|
||||||
|
};
|
21
src/lib/components/ui/label/label.svelte
Normal file
21
src/lib/components/ui/label/label.svelte
Normal 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>
|
1
src/lib/components/ui/sonner/index.ts
Normal file
1
src/lib/components/ui/sonner/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default as Toaster } from "./sonner.svelte";
|
20
src/lib/components/ui/sonner/sonner.svelte
Normal file
20
src/lib/components/ui/sonner/sonner.svelte
Normal 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
2
src/lib/config/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './site';
|
||||||
|
export * from './navigation';
|
57
src/lib/config/navigation.ts
Normal file
57
src/lib/config/navigation.ts
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
|
@ -1,14 +1,13 @@
|
||||||
export const siteConfig = {
|
export const siteConfig = {
|
||||||
name: 'Peer-at Code',
|
name: 'Peer-at Code',
|
||||||
url: 'https://app.peerat.dev',
|
url: 'https://app.peerat.dev',
|
||||||
title: 'Peer-at Code',
|
|
||||||
description: 'Apprendre la programmation et la cybersécurité en s\'amusant.',
|
description: 'Apprendre la programmation et la cybersécurité en s\'amusant.',
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
keywords: ['peerat', 'code', 'cybersecurite', 'programmation', 'apprendre en s\'amusant'],
|
keywords: ['peerat', 'code', 'cybersecurite', 'programmation', "apprendre en s'amusant"],
|
||||||
author: 'peerat',
|
author: 'peerat',
|
||||||
locale: 'fr',
|
links: {
|
||||||
twitter: {
|
github: "https://git.peerat.dev",
|
||||||
creator: '@peerat'
|
discord: "https://discord.gg/72vuHcwUkE",
|
||||||
},
|
},
|
||||||
themeColor: '#110F15'
|
themeColor: '#110F15'
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,62 @@
|
||||||
import { clsx, type ClassValue } from 'clsx';
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { cubicOut } from "svelte/easing";
|
||||||
|
import type { TransitionConfig } from "svelte/transition";
|
||||||
|
|
||||||
export const cn = (...inputs: ClassValue[]) => {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
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
|
||||||
|
};
|
||||||
|
};
|
84
src/lib/validations/auth.ts
Normal file
84
src/lib/validations/auth.ts
Normal 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' }),
|
||||||
|
});
|
8
src/lib/validations/puzzle.ts
Normal file
8
src/lib/validations/puzzle.ts
Normal 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')
|
||||||
|
});
|
6
src/routes/(app)/+layout.server.ts
Normal file
6
src/routes/(app)/+layout.server.ts
Normal 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');
|
||||||
|
};
|
23
src/routes/(app)/+layout.svelte
Normal file
23
src/routes/(app)/+layout.svelte
Normal 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>
|
|
@ -1,11 +1,13 @@
|
||||||
import { API_URL } from '$env/static/private';
|
import { API_URL } from '$env/static/private';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
import type { Chapter } from '$lib/types';
|
import type { Chapter } from '$lib/types';
|
||||||
|
|
||||||
export const load = (async ({ parent, fetch, cookies }) => {
|
export const load = (async ({ fetch, cookies, locals: { user } }) => {
|
||||||
await parent();
|
|
||||||
|
if (!user) redirect(302, '/login');
|
||||||
|
|
||||||
const session = cookies.get('session');
|
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();
|
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();
|
const lastPuzzle = chapter.puzzles.filter((puzzle) => puzzle.show).pop();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
title: 'Dashboard',
|
||||||
daily: {
|
daily: {
|
||||||
chapter: lastChapter,
|
chapter: lastChapter,
|
||||||
puzzle: lastPuzzle
|
puzzle: lastPuzzle
|
|
@ -1,8 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
import Card from '$lib/components/Card.svelte';
|
import Card from '$lib/components/card.svelte';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
@ -12,7 +11,7 @@
|
||||||
<section class="flex w-full flex-col gap-4">
|
<section class="flex w-full flex-col gap-4">
|
||||||
<header>
|
<header>
|
||||||
<h1 class="text-xl font-semibold">Tableau de bord</h1>
|
<h1 class="text-xl font-semibold">Tableau de bord</h1>
|
||||||
<p class="text-highlight-secondary">Ceci est la page d'accueil du dashboard</p>
|
<p class="text-muted-foreground">Ceci est la page d'accueil du dashboard</p>
|
||||||
</header>
|
</header>
|
||||||
<main class="flex flex-col gap-4">
|
<main class="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
|
@ -25,28 +24,25 @@
|
||||||
{#if data.daily && data.daily.puzzle}
|
{#if data.daily && data.daily.puzzle}
|
||||||
<header>
|
<header>
|
||||||
<h1 class="text-lg font-semibold">Puzzle du jour</h1>
|
<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
|
Essayer de résoudre le puzzle du jour pour gagner des points supplémentaires
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<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">
|
<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-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})
|
{data.daily.puzzle.name} ({data.daily.puzzle.score ?? '?'}/{data.daily.puzzle.scoreMax})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Button
|
<a href="/chapters/{data.daily.chapter.id}/puzzle/{data.daily.puzzle.id}">
|
||||||
variant="brand"
|
|
||||||
href="/dashboard/chapters/{data.daily.chapter.id}/puzzle/{data.daily.puzzle.id}"
|
|
||||||
>
|
|
||||||
<span class="text-lg font-semibold">
|
<span class="text-lg font-semibold">
|
||||||
{data.daily.puzzle.score ? 'Voir' : 'Jouer'}
|
{data.daily.puzzle.score ? 'Voir' : 'Jouer'}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -54,15 +50,15 @@
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<header>
|
<header>
|
||||||
<h2 class="text-lg font-semibold">Derniers puzzles</h2>
|
<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
|
Voici les derniers puzzles que vous avez résolus ou essayer de résoudres
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<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">
|
<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}
|
{#each user.completionsList as completion, key}
|
||||||
<li class="flex justify-between space-x-2">
|
<li class="flex justify-between space-x-2">
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
|
@ -75,13 +71,13 @@
|
||||||
<span class="text-sm font-semibold">
|
<span class="text-sm font-semibold">
|
||||||
Essai{completion.tries > 1 ? 's' : ''}
|
Essai{completion.tries > 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-right text-lg text-highlight-secondary">
|
<span class="text-right text-lg text-muted-foreground">
|
||||||
{completion.tries}
|
{completion.tries}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-sm font-semibold">Score</span>
|
<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}
|
{completion.score}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -90,7 +86,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<li class="m-auto flex items-center justify-center">
|
<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>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue