UI Refactor and group support #13
170 changed files with 5436 additions and 3190 deletions
|
@ -1,12 +1,37 @@
|
|||
.git
|
||||
# build folder
|
||||
dist/
|
||||
build/
|
||||
|
||||
# logs
|
||||
logs/
|
||||
.cache
|
||||
.DS_Store
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# process data
|
||||
.pid
|
||||
pids
|
||||
.pid-lock
|
||||
|
||||
# Coverage & Test
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Dependency folder
|
||||
node_modules
|
||||
.eslint*
|
||||
.prettier*
|
||||
.git*
|
||||
bower_components
|
||||
jspm_packages
|
||||
|
||||
# Environment files
|
||||
.dot
|
||||
|
||||
# Editor and other files
|
||||
.idea
|
||||
typing
|
||||
internal
|
||||
.vscode
|
||||
README.md
|
||||
Dockerfile*
|
||||
docker-compose.yml
|
||||
public
|
||||
*.tgz
|
||||
.eslintcache
|
||||
.svelte-kit
|
||||
build
|
32
Dockerfile
32
Dockerfile
|
@ -1,35 +1,37 @@
|
|||
# Base Stage
|
||||
FROM node:21-slim AS base
|
||||
|
||||
FROM node:18-alpine AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
RUN npm i -g pnpm
|
||||
|
||||
FROM base AS dependencies
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
# Dependencies Stage for Production
|
||||
FROM base AS deps
|
||||
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
|
||||
# Build Stage
|
||||
FROM base AS build
|
||||
|
||||
WORKDIR /app
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
COPY --from=dependencies /app/node_modules ./node_modules
|
||||
|
||||
RUN pnpm build
|
||||
|
||||
RUN pnpm prune --prod
|
||||
RUN pnpm run build
|
||||
|
||||
# Deploy Stage
|
||||
FROM base AS deploy
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules /app/node_modules
|
||||
COPY --from=build /app/build /app/build
|
||||
|
||||
COPY --from=build /app/build ./build
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
COPY --from=build /app/package.json ./package.json
|
||||
# Run as non-root user
|
||||
# USER node
|
||||
|
||||
ARG PORT=3000
|
||||
|
||||
|
|
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
|
||||
}
|
63
package.json
63
package.json
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"name": "peer-at-code",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -16,39 +15,45 @@
|
|||
"test:unit": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"bits-ui": "^0.20.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"lucide-svelte": "^0.279.0",
|
||||
"marked": "^7.0.5",
|
||||
"svelte-boring-avatars": "^1.2.4",
|
||||
"tailwind-merge": "^1.14.0"
|
||||
"clsx": "^2.1.0",
|
||||
"formsnap": "^0.5.1",
|
||||
"lucide-svelte": "^0.363.0",
|
||||
"mode-watcher": "^0.3.0",
|
||||
"svelte-boring-avatars": "^1.2.6",
|
||||
"svelte-sonner": "^0.3.19",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"vaul-svelte": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@melt-ui/pp": "^0.1.4",
|
||||
"@melt-ui/svelte": "^0.50.1",
|
||||
"@playwright/test": "^1.40.0",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"@sveltejs/kit": "^1.27.6",
|
||||
"@types/marked": "^5.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@sveltejs/adapter-node": "^5.0.1",
|
||||
"@sveltejs/kit": "^2.5.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.3.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"boring-avatars": "^1.10.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.1",
|
||||
"svelte-sequential-preprocessor": "^2.0.1",
|
||||
"sveltekit-superforms": "^1.10.2",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"mdsvex": "^0.11.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.2",
|
||||
"prettier-plugin-tailwindcss": "^0.5.12",
|
||||
"svelte": "^4.2.12",
|
||||
"svelte-check": "^3.6.8",
|
||||
"sveltekit-superforms": "^2.11.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^4.5.0",
|
||||
"vitest": "^0.32.4",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.6",
|
||||
"vitest": "^1.4.0",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
|
|
2212
pnpm-lock.yaml
generated
2212
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
154
src/app.css
154
src/app.css
|
@ -1,103 +1,105 @@
|
|||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: 'Karrik';
|
||||
src: url('/fonts/Karrik.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fira Code';
|
||||
src: url('/fonts/FiraCode.woff2');
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 215 20.2% 65.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
@font-face {
|
||||
font-family: 'Karrik';
|
||||
src: url('/fonts/Karrik.woff2');
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
@font-face {
|
||||
font-family: 'Fira Code';
|
||||
src: url('/fonts/FiraCode.woff2');
|
||||
}
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
:root {
|
||||
--gradient: linear-gradient(to top left, hsl(258 85% 67%), hsl(258 60% 40%));
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--background: 255 14.81% 10.59%;
|
||||
--foreground: 284 4.7% 97.05%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--muted: 258 15% 17%;
|
||||
--muted-foreground: 284 4.7% 54.1%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--popover: 256.36 14.29% 15.1%;
|
||||
--popover-foreground: 284 4.7% 97.05%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--card: 256.36 14.29% 15.1%;
|
||||
--card-foreground: 284 4.7% 97.05%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--border: 258 15% 17%;
|
||||
--input: 258 15% 17%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--primary: 258 85% 67%;
|
||||
--primary-foreground: 284 4.7% 97.05%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 85.7% 97.3%;
|
||||
--secondary: 258 60% 40%;
|
||||
--secondary-foreground: 284 4.7% 97.05%;
|
||||
|
||||
--ring: 217.2 32.6% 17.5%;
|
||||
--accent: 258 60% 40%;
|
||||
--accent-foreground: 284 4.7% 97.05%;
|
||||
|
||||
--destructive: 7 100% 67%;
|
||||
--destructive-foreground: 284 4.7% 97.05%;
|
||||
|
||||
--ring: 258 85% 67%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border text-white;
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply text-foreground;
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-2;
|
||||
@apply h-2;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply !bg-muted;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply rounded-sm !bg-muted-foreground/30;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
|
||||
html {
|
||||
scrollbar-color: hsl(215.4 16.3% 46.9% / 0.3);
|
||||
}
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.antialised {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.console {
|
||||
@apply relative top-0.5 inline-block;
|
||||
}
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
textarea:-webkit-autofill,
|
||||
textarea:-webkit-autofill:hover,
|
||||
textarea:-webkit-autofill:focus {
|
||||
-webkit-box-shadow: 0 0 0px 1000px hsl(258deg 15% 17%) inset;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
.bg-gradient {
|
||||
background: var(--gradient);
|
||||
}
|
||||
}
|
||||
|
|
4
src/app.d.ts
vendored
4
src/app.d.ts
vendored
|
@ -7,10 +7,10 @@ declare global {
|
|||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
user?: User;
|
||||
user: User | null;
|
||||
}
|
||||
interface PageData {
|
||||
user?: User;
|
||||
user: User | null;
|
||||
}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
|
38
src/app.html
38
src/app.html
|
@ -1,31 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr" class="scroll-smooth bg-gradient-to-b from-primary-800 to-primary-900">
|
||||
<head>
|
||||
<html lang="fr" style="color-scheme: dark;">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="%sveltekit.assets%/favicon.ico" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="%sveltekit.assets%/assets/icons/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="%sveltekit.assets%/assets/icons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="%sveltekit.assets%/assets/icons/favicon-16x16.png"
|
||||
/>
|
||||
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
||||
<script data-domain="app.peerat.dev" src="https://plosibl.peerat.dev/js/script.js" defer></script>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" class="relative min-h-screen">
|
||||
<main style="display: contents">%sveltekit.body%</main>
|
||||
</body>
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover" class="min-h-screen antialiased font-sans text-base">
|
||||
<div style="display: contents" class="relative flex min-h-screen flex-col">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -4,24 +4,29 @@ import { API_URL } from '$env/static/private';
|
|||
|
||||
import type { User } from '$lib/types';
|
||||
|
||||
export const handle = (async ({ event, resolve }) => {
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
|
||||
const session = event.cookies.get('session');
|
||||
|
||||
if (session) {
|
||||
if (!session) {
|
||||
event.locals.user = null;
|
||||
return resolve(event);
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_URL}/player/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session}`
|
||||
}
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const user = (await res.json()) as User;
|
||||
event.locals.user = user;
|
||||
} else {
|
||||
event.locals.user = undefined;
|
||||
event.cookies.delete('session');
|
||||
}
|
||||
if (!res.ok) {
|
||||
event.locals.user = null;
|
||||
event.cookies.delete('session', { path: '/' });
|
||||
return resolve(event);
|
||||
}
|
||||
|
||||
return await resolve(event);
|
||||
}) satisfies Handle;
|
||||
const user: User = await res.json();
|
||||
event.locals.user = user;
|
||||
|
||||
return resolve(event);
|
||||
};
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
|
@ -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}
|
|
@ -1,16 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let title: string;
|
||||
export let data: any;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex w-full items-center space-x-4 rounded-lg border-2 border-highlight-primary bg-primary-700 p-4 shadow-md"
|
||||
>
|
||||
<!-- <Icon class="text-muted" size={30} /> -->
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<div class="flex-col">
|
||||
<h2 class="text-xl font-semibold">{data}</h2>
|
||||
<p class="text-muted">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,47 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/Utils';
|
||||
import type { Chapter } from '$lib/types';
|
||||
import { Trophy } from 'lucide-svelte';
|
||||
|
||||
import ChevronRight from './Icons/ChevronRight.svelte';
|
||||
import Button from './ui/Button.svelte';
|
||||
|
||||
export let chapter: Chapter;
|
||||
</script>
|
||||
|
||||
<li
|
||||
class={cn(
|
||||
'group relative flex h-full w-full flex-col rounded-md bg-primary-700 transition-colors duration-150',
|
||||
{
|
||||
'hover:bg-primary-600': chapter.show,
|
||||
'opacity-50': !chapter.show
|
||||
}
|
||||
)}
|
||||
>
|
||||
{#if chapter.show}
|
||||
<a
|
||||
class="flex h-full w-full items-center justify-between gap-4 p-4"
|
||||
href={chapter.show ? `/dashboard/chapters/${chapter.id}` : '#'}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-base font-semibold">
|
||||
{chapter.name}
|
||||
</span>
|
||||
{#if chapter.id === 1}
|
||||
<Trophy class="stroke-highlight-secondary" />
|
||||
{/if}
|
||||
</div>
|
||||
<span class="translate-x-0 transform-gpu duration-300 group-hover:translate-x-2">
|
||||
<ChevronRight />
|
||||
</span>
|
||||
</a>
|
||||
{:else}
|
||||
<span class="flex h-full w-full items-center gap-4 p-4">
|
||||
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
|
||||
<h2 class="text-base font-semibold">
|
||||
{chapter.name}
|
||||
</h2>
|
||||
</div>
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
|
@ -1,26 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib';
|
||||
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
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
|
||||
>
|
|
@ -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,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>
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/Utils';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
export let name: string;
|
||||
export let src: string;
|
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>
|
16
src/lib/components/card.svelte
Normal file
16
src/lib/components/card.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
export let title: string;
|
||||
export let data: any;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex w-full items-center space-x-4 border rounded border-border bg-card p-4 shadow-md"
|
||||
>
|
||||
<!-- <Icon class="text-muted-foreground" size={30} /> -->
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<div class="">
|
||||
<h2 class="text-xl font-semibold">{data}</h2>
|
||||
<p class="text-muted-foreground">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
46
src/lib/components/chapter.svelte
Normal file
46
src/lib/components/chapter.svelte
Normal file
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import Swords from 'lucide-svelte/icons/swords';
|
||||
import ChevronRight from 'lucide-svelte/icons/chevron-right';
|
||||
|
||||
import type { Chapter } from '$lib/types';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
export let chapter: Chapter;
|
||||
</script>
|
||||
|
||||
<li
|
||||
class={cn(
|
||||
'group relative flex h-full w-full flex-col rounded border border-border bg-card transition-colors duration-150',
|
||||
{
|
||||
'hover:bg-card/80': chapter.show || (chapter.start && chapter.end),
|
||||
'opacity-50': !chapter.show && !(chapter.start && chapter.end)
|
||||
}
|
||||
)}
|
||||
>
|
||||
{#if chapter.show || (chapter.start && chapter.end)}
|
||||
<a
|
||||
class="flex h-full w-full items-center justify-between gap-4 p-4"
|
||||
href={chapter.show || (chapter.start && chapter.end) ? `/chapters/${chapter.id}` : null}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">
|
||||
{chapter.name}
|
||||
</span>
|
||||
{#if chapter.start && chapter.end}
|
||||
<Swords class="stroke-muted-foreground" />
|
||||
{/if}
|
||||
</div>
|
||||
<span class="translate-x-0 transform-gpu duration-300 group-hover:translate-x-2">
|
||||
<ChevronRight />
|
||||
</span>
|
||||
</a>
|
||||
{:else}
|
||||
<span class="flex h-full w-full items-center gap-4 p-4">
|
||||
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
|
||||
<h2 class="font-semibold">
|
||||
{chapter.name}
|
||||
</h2>
|
||||
</div>
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
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>
|
91
src/lib/components/layout/navbar/navbar-user.svelte
Normal file
91
src/lib/components/layout/navbar/navbar-user.svelte
Normal file
|
@ -0,0 +1,91 @@
|
|||
<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 Shield from 'lucide-svelte/icons/shield';
|
||||
import ScrollText from 'lucide-svelte/icons/scroll-text';
|
||||
import Code from 'lucide-svelte/icons/code';
|
||||
|
||||
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
|
||||
>
|
||||
{#if $page.data.user?.email.endsWith('@peerat.dev')}
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>
|
||||
<Shield class="mr-2 h-4 w-4" />
|
||||
<span>Administration</span>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.Item href="/admin/logs">
|
||||
<ScrollText class="mr-2 h-4 w-4" />
|
||||
<span>Logs</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item href="/admin/chapters">
|
||||
<Code class="mr-2 h-4 w-4" />
|
||||
<span>Chapitres</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item href="/admin/puzzles">
|
||||
<Code class="mr-2 h-4 w-4" />
|
||||
<span>Puzzles</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
{/if}
|
||||
<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="/groups">
|
||||
<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>
|
50
src/lib/components/metadata.svelte
Normal file
50
src/lib/components/metadata.svelte
Normal file
|
@ -0,0 +1,50 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { siteConfig } from '$lib/config/site';
|
||||
|
||||
export let title = siteConfig.name;
|
||||
|
||||
$: title = $page.data?.title ? `${$page.data.title} | ${siteConfig.name}` : siteConfig.name;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={siteConfig.description} />
|
||||
<meta name="keywords" content={siteConfig.keywords.join(',')} />
|
||||
<meta name="author" content={siteConfig.author} />
|
||||
|
||||
<meta name="theme-color" content={siteConfig.themeColor} />
|
||||
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
|
||||
<meta itemprop="name" content={title} />
|
||||
<meta itemprop="description" content={siteConfig.description} />
|
||||
<meta itemprop="image" content={siteConfig.imageUrl} />
|
||||
|
||||
<meta property="og:site_name" content={siteConfig.name} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={siteConfig.description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={siteConfig.url + $page.url.pathname} />
|
||||
<meta property="og:image" content={siteConfig.imageUrl} />
|
||||
<meta property="og:image:alt" content={title} />
|
||||
<meta property="og:locale" content="fr" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content={siteConfig.url} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={siteConfig.description} />
|
||||
<meta name="twitter:image" content={siteConfig.imageUrl} />
|
||||
<meta name="twitter:image:alt" content={title} />
|
||||
<meta name="twitter:creator" content={`@${siteConfig.author}`} />
|
||||
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
||||
</svelte:head>
|
|
@ -1,25 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { cn } from '$lib/Utils';
|
||||
|
||||
import ChevronRight from 'lucide-svelte/icons/chevron-right';
|
||||
|
||||
import type { Puzzle } from '$lib/types';
|
||||
import ChevronRight from './Icons/ChevronRight.svelte';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
export let puzzle: Puzzle;
|
||||
|
||||
const chapterId = $page.params.chapterId;
|
||||
|
||||
$: tags = puzzle.tags?.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name));
|
||||
</script>
|
||||
|
||||
<li
|
||||
class={cn(
|
||||
'group relative flex h-full w-full rounded-md border-2 bg-primary-700 transition-colors duration-150',
|
||||
'group relative flex h-full w-full rounded border border-border bg-card transition-colors duration-150',
|
||||
{
|
||||
'border-green-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'easy'),
|
||||
'border-yellow-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'medium'),
|
||||
'border-red-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'hard'),
|
||||
'border-highlight-primary': !puzzle.tags?.length,
|
||||
'hover:bg-primary-600': puzzle.show,
|
||||
'border-green-500/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'easy'),
|
||||
'border-yellow-500/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'medium'),
|
||||
'border-red-500/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'hard'),
|
||||
'hover:bg-card/80': puzzle.show,
|
||||
'opacity-50': !puzzle.show
|
||||
}
|
||||
)}
|
||||
|
@ -27,12 +26,12 @@
|
|||
{#if puzzle.show}
|
||||
<a
|
||||
class="flex h-full w-full items-center gap-4 p-4"
|
||||
href="/dashboard/chapters/{chapterId}/puzzle/{puzzle.id}"
|
||||
href="/chapters/{chapterId}/puzzle/{puzzle.id}"
|
||||
>
|
||||
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
|
||||
<h2 class="text-base font-semibold">
|
||||
{puzzle.name}
|
||||
<span class="text-sm text-highlight-secondary">
|
||||
<span class="text-highlight-secondary text-sm">
|
||||
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
|
||||
</span>
|
||||
</h2>
|
||||
|
@ -40,9 +39,7 @@
|
|||
<div class="flex items-center gap-x-6">
|
||||
<div class="flex gap-x-2 text-sm">
|
||||
{#each puzzle.tags as tag}
|
||||
<span
|
||||
class="inline-block rounded-md bg-primary-800 px-2 py-1 text-highlight-secondary"
|
||||
>
|
||||
<span class="inline-block rounded bg-muted px-2 py-1 text-muted-foreground">
|
||||
{tag.name}
|
||||
</span>
|
||||
{/each}
|
||||
|
@ -59,7 +56,7 @@
|
|||
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
|
||||
<h2 class="text-base font-semibold">
|
||||
{puzzle.name}
|
||||
<span class="text-sm text-highlight-secondary">
|
||||
<span class="text-highlight-secondary text-sm">
|
||||
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
|
||||
</span>
|
||||
</h2>
|
||||
|
@ -67,9 +64,7 @@
|
|||
{#if puzzle.tags?.length}
|
||||
<div class="flex gap-x-2 text-sm">
|
||||
{#each puzzle.tags as tag}
|
||||
<span
|
||||
class="inline-block rounded-md bg-primary-800 px-2 py-1 text-highlight-secondary"
|
||||
>
|
||||
<span class="inline-block rounded bg-muted px-2 py-1 text-muted-foreground">
|
||||
{tag.name}
|
||||
</span>
|
||||
{/each}
|
|
@ -1,78 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '$lib';
|
||||
|
||||
// TODO: Remove this
|
||||
|
||||
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';
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue