style: apply prettier formatting to all files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"tailwind": {
|
||||
"css": "src/app.css",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://shadcn-svelte.com/registry"
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"tailwind": {
|
||||
"css": "src/app.css",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://shadcn-svelte.com/registry"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/jsrepo@2.4.9/schemas/project-config.json",
|
||||
"repos": ["@ieedan/shadcn-svelte-extras"],
|
||||
"includeTests": false,
|
||||
"includeDocs": false,
|
||||
"watermark": true,
|
||||
"formatter": "prettier",
|
||||
"configFiles": {},
|
||||
"paths": {
|
||||
"*": "$lib/blocks",
|
||||
"ui": "$lib/components/ui",
|
||||
"actions": "$lib/actions",
|
||||
"hooks": "$lib/hooks",
|
||||
"utils": "$lib/utils"
|
||||
}
|
||||
"$schema": "https://unpkg.com/jsrepo@2.4.9/schemas/project-config.json",
|
||||
"repos": ["@ieedan/shadcn-svelte-extras"],
|
||||
"includeTests": false,
|
||||
"includeDocs": false,
|
||||
"watermark": true,
|
||||
"formatter": "prettier",
|
||||
"configFiles": {},
|
||||
"paths": {
|
||||
"*": "$lib/blocks",
|
||||
"ui": "$lib/components/ui",
|
||||
"actions": "$lib/actions",
|
||||
"hooks": "$lib/hooks",
|
||||
"utils": "$lib/utils"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,82 +8,82 @@
|
||||
@custom-variant hover (&:hover);
|
||||
|
||||
@theme {
|
||||
--animate-vibrate: vibrate 0.3s linear infinite;
|
||||
--animate-fade-in: fadeIn 0.3s ease-out;
|
||||
--animate-slide-up: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--animate-zoom-in: zoomIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--animate-pulse-glow: pulseGlow 2s infinite;
|
||||
--animate-vibrate: vibrate 0.3s linear infinite;
|
||||
--animate-fade-in: fadeIn 0.3s ease-out;
|
||||
--animate-slide-up: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--animate-zoom-in: zoomIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--animate-pulse-glow: pulseGlow 2s infinite;
|
||||
|
||||
@keyframes vibrate {
|
||||
0% {
|
||||
transform: translate(0);
|
||||
}
|
||||
@keyframes vibrate {
|
||||
0% {
|
||||
transform: translate(0);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translate(-2px, 2px);
|
||||
}
|
||||
20% {
|
||||
transform: translate(-2px, 2px);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translate(-2px, -2px);
|
||||
}
|
||||
40% {
|
||||
transform: translate(-2px, -2px);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translate(2px, 2px);
|
||||
}
|
||||
60% {
|
||||
transform: translate(2px, 2px);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translate(2px, -2px);
|
||||
}
|
||||
80% {
|
||||
transform: translate(2px, -2px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
@keyframes zoomIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulseGlow {
|
||||
0%,
|
||||
100% {
|
||||
boxShadow: 0 0 20px rgba(183, 0, 217, 0.3);
|
||||
}
|
||||
@keyframes pulseGlow {
|
||||
0%,
|
||||
100% {
|
||||
boxshadow: 0 0 20px rgba(183, 0, 217, 0.3);
|
||||
}
|
||||
|
||||
50% {
|
||||
boxShadow: 0 0 40px rgba(183, 0, 217, 0.6);
|
||||
}
|
||||
}
|
||||
50% {
|
||||
boxshadow: 0 0 40px rgba(183, 0, 217, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -95,134 +95,134 @@
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
* {
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
outline-color: color-mix(in oklab, var(--ring) 50%, transparent);
|
||||
}
|
||||
}
|
||||
* {
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
outline-color: color-mix(in oklab, var(--ring) 50%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
border-color: var(--border);
|
||||
outline-color: var(--ring);
|
||||
}
|
||||
* {
|
||||
border-color: var(--border);
|
||||
outline-color: var(--ring);
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
@apply text-2xl font-bold mt-8 mb-4 text-foreground;
|
||||
}
|
||||
.prose h2 {
|
||||
@apply text-2xl font-bold mt-8 mb-4 text-foreground;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
@apply text-xl font-semibold mt-6 mb-3 text-foreground;
|
||||
}
|
||||
.prose h3 {
|
||||
@apply text-xl font-semibold mt-6 mb-3 text-foreground;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
@apply mb-4 leading-relaxed;
|
||||
}
|
||||
.prose p {
|
||||
@apply mb-4 leading-relaxed;
|
||||
}
|
||||
|
||||
.prose ul {
|
||||
@apply mb-4 pl-6;
|
||||
}
|
||||
.prose ul {
|
||||
@apply mb-4 pl-6;
|
||||
}
|
||||
|
||||
.prose li {
|
||||
@apply mb-2;
|
||||
}
|
||||
.prose li {
|
||||
@apply mb-2;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--default-font-family: "Noto Sans", sans-serif;
|
||||
--background: oklch(0.98 0.01 320);
|
||||
--foreground: oklch(0.08 0.02 280);
|
||||
--muted: oklch(0.95 0.01 280);
|
||||
--muted-foreground: oklch(0.4 0.02 280);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--card: oklch(0.99 0.005 320);
|
||||
--card-foreground: oklch(0.08 0.02 280);
|
||||
--border: oklch(0.85 0.02 280);
|
||||
--input: oklch(0.922 0 0);
|
||||
--primary: oklch(56.971% 0.27455 319.257);
|
||||
--primary-foreground: oklch(0.98 0.01 320);
|
||||
--secondary: oklch(0.92 0.02 260);
|
||||
--secondary-foreground: oklch(0.15 0.05 260);
|
||||
--accent: oklch(0.45 0.35 280);
|
||||
--accent-foreground: oklch(0.98 0.01 280);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--ring: oklch(0.55 0.3 320);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--default-font-family: "Noto Sans", sans-serif;
|
||||
--background: oklch(0.98 0.01 320);
|
||||
--foreground: oklch(0.08 0.02 280);
|
||||
--muted: oklch(0.95 0.01 280);
|
||||
--muted-foreground: oklch(0.4 0.02 280);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--card: oklch(0.99 0.005 320);
|
||||
--card-foreground: oklch(0.08 0.02 280);
|
||||
--border: oklch(0.85 0.02 280);
|
||||
--input: oklch(0.922 0 0);
|
||||
--primary: oklch(56.971% 0.27455 319.257);
|
||||
--primary-foreground: oklch(0.98 0.01 320);
|
||||
--secondary: oklch(0.92 0.02 260);
|
||||
--secondary-foreground: oklch(0.15 0.05 260);
|
||||
--accent: oklch(0.45 0.35 280);
|
||||
--accent-foreground: oklch(0.98 0.01 280);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--ring: oklch(0.55 0.3 320);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.08 0.02 280);
|
||||
--foreground: oklch(0.98 0.01 280);
|
||||
--muted: oklch(0.12 0.03 280);
|
||||
--muted-foreground: oklch(0.6 0.02 280);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.1 0.02 280);
|
||||
--card-foreground: oklch(0.95 0.01 280);
|
||||
--border: oklch(0.2 0.05 280);
|
||||
--input: oklch(1 0 0 / 0.15);
|
||||
--primary: oklch(0.65 0.25 320);
|
||||
--primary-foreground: oklch(0.98 0.01 320);
|
||||
--secondary: oklch(0.15 0.05 260);
|
||||
--secondary-foreground: oklch(0.9 0.02 260);
|
||||
--accent: oklch(0.55 0.3 280);
|
||||
--accent-foreground: oklch(0.98 0.01 280);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--ring: oklch(0.65 0.25 320);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 0.1);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
--background: oklch(0.08 0.02 280);
|
||||
--foreground: oklch(0.98 0.01 280);
|
||||
--muted: oklch(0.12 0.03 280);
|
||||
--muted-foreground: oklch(0.6 0.02 280);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.1 0.02 280);
|
||||
--card-foreground: oklch(0.95 0.01 280);
|
||||
--border: oklch(0.2 0.05 280);
|
||||
--input: oklch(1 0 0 / 0.15);
|
||||
--primary: oklch(0.65 0.25 320);
|
||||
--primary-foreground: oklch(0.98 0.01 320);
|
||||
--secondary: oklch(0.15 0.05 260);
|
||||
--secondary-foreground: oklch(0.9 0.02 260);
|
||||
--accent: oklch(0.55 0.3 280);
|
||||
--accent-foreground: oklch(0.98 0.01 280);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--ring: oklch(0.65 0.25 320);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 0.1);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
}
|
||||
|
||||
32
packages/frontend/src/app.d.ts
vendored
32
packages/frontend/src/app.d.ts
vendored
@@ -4,22 +4,22 @@ import type { AuthStatus } from "$lib/types";
|
||||
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
authStatus: AuthStatus;
|
||||
requestId: string;
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
interface Window {
|
||||
sidebar: {
|
||||
addPanel: () => void;
|
||||
};
|
||||
opera: object;
|
||||
}
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
authStatus: AuthStatus;
|
||||
requestId: string;
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
interface Window {
|
||||
sidebar: {
|
||||
addPanel: () => void;
|
||||
};
|
||||
opera: object;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Dancing+Script:wght@400..700&family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Dancing+Script:wght@400..700&family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover" class="dark">
|
||||
<body data-sveltekit-preload-data="hover" class="dark">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,88 +6,88 @@ import type { Handle } from "@sveltejs/kit";
|
||||
logger.startup();
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
const { cookies, locals, url, request } = event;
|
||||
const startTime = Date.now();
|
||||
const { cookies, locals, url, request } = event;
|
||||
const startTime = Date.now();
|
||||
|
||||
// Generate unique request ID
|
||||
const requestId = generateRequestId();
|
||||
// Generate unique request ID
|
||||
const requestId = generateRequestId();
|
||||
|
||||
// Add request ID to locals for access in other handlers
|
||||
locals.requestId = requestId;
|
||||
// Add request ID to locals for access in other handlers
|
||||
locals.requestId = requestId;
|
||||
|
||||
// Log incoming request
|
||||
logger.request(request.method, url.pathname, {
|
||||
requestId,
|
||||
context: {
|
||||
userAgent: request.headers.get('user-agent')?.substring(0, 100),
|
||||
referer: request.headers.get('referer'),
|
||||
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
|
||||
},
|
||||
});
|
||||
// Log incoming request
|
||||
logger.request(request.method, url.pathname, {
|
||||
requestId,
|
||||
context: {
|
||||
userAgent: request.headers.get("user-agent")?.substring(0, 100),
|
||||
referer: request.headers.get("referer"),
|
||||
ip: request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip"),
|
||||
},
|
||||
});
|
||||
|
||||
// Handle authentication
|
||||
const token = cookies.get("session_token");
|
||||
// Handle authentication
|
||||
const token = cookies.get("session_token");
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
locals.authStatus = await isAuthenticated(token);
|
||||
if (token) {
|
||||
try {
|
||||
locals.authStatus = await isAuthenticated(token);
|
||||
|
||||
if (locals.authStatus.authenticated) {
|
||||
logger.auth('Token validated', true, {
|
||||
requestId,
|
||||
userId: locals.authStatus.user?.id,
|
||||
context: {
|
||||
email: locals.authStatus.user?.email,
|
||||
role: locals.authStatus.user?.role,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
logger.auth('Token invalid', false, { requestId });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Authentication check failed', {
|
||||
requestId,
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
});
|
||||
locals.authStatus = { authenticated: false };
|
||||
}
|
||||
} else {
|
||||
logger.debug('No session token found', { requestId });
|
||||
locals.authStatus = { authenticated: false };
|
||||
}
|
||||
if (locals.authStatus.authenticated) {
|
||||
logger.auth("Token validated", true, {
|
||||
requestId,
|
||||
userId: locals.authStatus.user?.id,
|
||||
context: {
|
||||
email: locals.authStatus.user?.email,
|
||||
role: locals.authStatus.user?.role,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
logger.auth("Token invalid", false, { requestId });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Authentication check failed", {
|
||||
requestId,
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
});
|
||||
locals.authStatus = { authenticated: false };
|
||||
}
|
||||
} else {
|
||||
logger.debug("No session token found", { requestId });
|
||||
locals.authStatus = { authenticated: false };
|
||||
}
|
||||
|
||||
// Resolve the request
|
||||
let response: Response;
|
||||
try {
|
||||
response = await resolve(event, {
|
||||
filterSerializedResponseHeaders: (key) => {
|
||||
return key.toLowerCase() === "content-type";
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
logger.error('Request handler error', {
|
||||
requestId,
|
||||
method: request.method,
|
||||
path: url.pathname,
|
||||
duration,
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
// Resolve the request
|
||||
let response: Response;
|
||||
try {
|
||||
response = await resolve(event, {
|
||||
filterSerializedResponseHeaders: (key) => {
|
||||
return key.toLowerCase() === "content-type";
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
logger.error("Request handler error", {
|
||||
requestId,
|
||||
method: request.method,
|
||||
path: url.pathname,
|
||||
duration,
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Log response
|
||||
const duration = Date.now() - startTime;
|
||||
logger.response(request.method, url.pathname, response.status, duration, {
|
||||
requestId,
|
||||
userId: locals.authStatus.authenticated ? locals.authStatus.user?.id : undefined,
|
||||
context: {
|
||||
cached: response.headers.get('x-sveltekit-page') === 'true',
|
||||
},
|
||||
});
|
||||
// Log response
|
||||
const duration = Date.now() - startTime;
|
||||
logger.response(request.method, url.pathname, response.status, duration, {
|
||||
requestId,
|
||||
userId: locals.authStatus.authenticated ? locals.authStatus.user?.id : undefined,
|
||||
context: {
|
||||
cached: response.headers.get("x-sveltekit-page") === "true",
|
||||
},
|
||||
});
|
||||
|
||||
// Add request ID to response headers (useful for debugging)
|
||||
response.headers.set('x-request-id', requestId);
|
||||
// Add request ID to response headers (useful for debugging)
|
||||
response.headers.set("x-request-id", requestId);
|
||||
|
||||
return response;
|
||||
return response;
|
||||
};
|
||||
|
||||
@@ -1,77 +1,70 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "$lib/components/ui/dialog";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "$lib/components/ui/dialog";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
const AGE_VERIFICATION_KEY = "age-verified";
|
||||
const AGE_VERIFICATION_KEY = "age-verified";
|
||||
|
||||
let isOpen = true;
|
||||
let isOpen = true;
|
||||
|
||||
function handleAgeConfirmation() {
|
||||
localStorage.setItem(AGE_VERIFICATION_KEY, "true");
|
||||
isOpen = false;
|
||||
}
|
||||
function handleAgeConfirmation() {
|
||||
localStorage.setItem(AGE_VERIFICATION_KEY, "true");
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const storedVerification = localStorage.getItem(AGE_VERIFICATION_KEY);
|
||||
if (storedVerification === "true") {
|
||||
isOpen = false;
|
||||
}
|
||||
});
|
||||
onMount(() => {
|
||||
const storedVerification = localStorage.getItem(AGE_VERIFICATION_KEY);
|
||||
if (storedVerification === "true") {
|
||||
isOpen = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog bind:open={isOpen}>
|
||||
<DialogContent
|
||||
class="sm:max-w-md"
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
showCloseButton={false}
|
||||
>
|
||||
<DialogHeader class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-10 h-10 shrink-0 grow-0 rounded-full bg-gradient-to-br from-primary to-purple-600 flex items-center justify-center"
|
||||
>
|
||||
<span class="text-primary-foreground text-sm"
|
||||
>{$_("age_verification_dialog.age")}</span
|
||||
>
|
||||
</div>
|
||||
<div class="">
|
||||
<DialogTitle class="text-left text-xl font-semibold text-primary-foreground"
|
||||
>{$_("age_verification_dialog.title")}</DialogTitle
|
||||
>
|
||||
<DialogDescription class="text-left text-sm">
|
||||
{$_("age_verification_dialog.description")}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<Separator class="my-4" />
|
||||
|
||||
<!-- Close Button -->
|
||||
<div class="flex justify-end gap-4">
|
||||
<Button variant="destructive" href={$_("age_verification_dialog.exit_url")} size="sm">
|
||||
{$_("age_verification_dialog.exit")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onclick={handleAgeConfirmation}
|
||||
class="cursor-pointer"
|
||||
<DialogContent
|
||||
class="sm:max-w-md"
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
showCloseButton={false}
|
||||
>
|
||||
<DialogHeader class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-10 h-10 shrink-0 grow-0 rounded-full bg-gradient-to-br from-primary to-purple-600 flex items-center justify-center"
|
||||
>
|
||||
<span class="text-primary-foreground text-sm">{$_("age_verification_dialog.age")}</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<DialogTitle class="text-left text-xl font-semibold text-primary-foreground"
|
||||
>{$_("age_verification_dialog.title")}</DialogTitle
|
||||
>
|
||||
<span class="icon-[ri--check-line]"></span>
|
||||
{$_("age_verification_dialog.confirm")}
|
||||
</Button>
|
||||
<DialogDescription class="text-left text-sm">
|
||||
{$_("age_verification_dialog.description")}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<Separator class="my-4" />
|
||||
|
||||
<!-- Close Button -->
|
||||
<div class="flex justify-end gap-4">
|
||||
<Button variant="destructive" href={$_("age_verification_dialog.exit_url")} size="sm">
|
||||
{$_("age_verification_dialog.exit")}
|
||||
</Button>
|
||||
<Button variant="default" size="sm" onclick={handleAgeConfirmation} class="cursor-pointer">
|
||||
<span class="icon-[ri--check-line]"></span>
|
||||
{$_("age_verification_dialog.confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
<!-- Advanced Plasma Background -->
|
||||
<div class="absolute inset-0 pointer-events-none">
|
||||
<!-- Primary gradient layers -->
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-primary/6 via-accent/10 to-primary/4 opacity-60"
|
||||
></div>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-tl from-accent/4 via-primary/8 to-accent/6 opacity-40"
|
||||
></div>
|
||||
<!-- Primary gradient layers -->
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-primary/6 via-accent/10 to-primary/4 opacity-60"
|
||||
></div>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-tl from-accent/4 via-primary/8 to-accent/6 opacity-40"
|
||||
></div>
|
||||
|
||||
<!-- Large floating orbs -->
|
||||
<!-- <div
|
||||
<!-- Large floating orbs -->
|
||||
<!-- <div
|
||||
class="absolute top-20 left-20 w-80 h-80 bg-gradient-to-br from-primary/12 via-accent/18 to-primary/8 rounded-full blur-3xl animate-blob-slow"
|
||||
></div>
|
||||
<div
|
||||
class="absolute bottom-20 right-20 w-96 h-96 bg-gradient-to-tl from-accent/12 via-primary/18 to-accent/8 rounded-full blur-3xl animate-blob-slow animation-delay-6000"
|
||||
></div> -->
|
||||
|
||||
<!-- Medium morphing elements -->
|
||||
<!-- <div
|
||||
<!-- Medium morphing elements -->
|
||||
<!-- <div
|
||||
class="absolute top-1/2 left-1/3 w-64 h-64 bg-gradient-to-r from-primary/10 via-accent/15 to-primary/8 rounded-full blur-2xl animate-blob-reverse animation-delay-3000"
|
||||
></div>
|
||||
<div
|
||||
class="absolute bottom-1/3 right-1/3 w-72 h-72 bg-gradient-to-l from-accent/10 via-primary/15 to-accent/8 rounded-full blur-2xl animate-blob-reverse animation-delay-9000"
|
||||
></div> -->
|
||||
|
||||
<!-- Soft particle effects -->
|
||||
<!-- <div
|
||||
<!-- Soft particle effects -->
|
||||
<!-- <div
|
||||
class="absolute top-1/4 right-1/4 w-48 h-48 bg-gradient-to-br from-primary/15 to-accent/12 rounded-full blur-xl animate-float animation-delay-2000"
|
||||
></div>
|
||||
<div
|
||||
class="absolute bottom-1/4 left-1/4 w-56 h-56 bg-gradient-to-tl from-accent/15 to-primary/12 rounded-full blur-xl animate-float animation-delay-8000"
|
||||
></div> -->
|
||||
|
||||
<!-- Premium glassmorphism overlay -->
|
||||
<!-- <div
|
||||
<!-- Premium glassmorphism overlay -->
|
||||
<!-- <div
|
||||
class="absolute inset-0 bg-gradient-to-br from-primary/2 via-transparent to-accent/3 backdrop-blur-[1px]"
|
||||
></div> -->
|
||||
|
||||
<!-- Animated Plasma Background -->
|
||||
<div
|
||||
class="absolute top-1/3 left-1/3 w-72 h-72 bg-gradient-to-r from-accent/20 via-primary/25 to-accent/15 rounded-full blur-2xl animate-blob"
|
||||
></div>
|
||||
<div
|
||||
class="absolute bottom-1/3 right-1/3 w-88 h-88 bg-gradient-to-r from-primary/20 via-accent/25 to-primary/15 rounded-full blur-3xl animate-blob-reverse animation-delay-3000"
|
||||
></div>
|
||||
<div
|
||||
class="absolute top-1/2 right-1/4 w-64 h-64 bg-gradient-to-r from-accent/15 via-primary/20 to-accent/10 rounded-full blur-2xl animate-float animation-delay-1000"
|
||||
></div>
|
||||
<!-- Animated Plasma Background -->
|
||||
<div
|
||||
class="absolute top-1/3 left-1/3 w-72 h-72 bg-gradient-to-r from-accent/20 via-primary/25 to-accent/15 rounded-full blur-2xl animate-blob"
|
||||
></div>
|
||||
<div
|
||||
class="absolute bottom-1/3 right-1/3 w-88 h-88 bg-gradient-to-r from-primary/20 via-accent/25 to-primary/15 rounded-full blur-3xl animate-blob-reverse animation-delay-3000"
|
||||
></div>
|
||||
<div
|
||||
class="absolute top-1/2 right-1/4 w-64 h-64 bg-gradient-to-r from-accent/15 via-primary/20 to-accent/10 rounded-full blur-2xl animate-float animation-delay-1000"
|
||||
></div>
|
||||
|
||||
<!-- Global Plasma Background -->
|
||||
<!-- <div
|
||||
<!-- Global Plasma Background -->
|
||||
<!-- <div
|
||||
class="absolute top-32 right-32 w-72 h-72 bg-gradient-to-r from-accent/18 via-primary/22 to-accent/12 rounded-full blur-3xl animate-blob"
|
||||
></div>
|
||||
<div
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<script lang="ts">
|
||||
const { isMobileMenuOpen = $bindable(), label, onclick } = $props();
|
||||
const { isMobileMenuOpen = $bindable(), label, onclick } = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="block rounded-full cursor-pointer"
|
||||
onclick={onclick}
|
||||
aria-label={label}
|
||||
>
|
||||
<button class="block rounded-full cursor-pointer" {onclick} aria-label={label}>
|
||||
<div
|
||||
class="relative flex overflow-hidden items-center justify-center rounded-full w-[50px] h-[50px] transform transition-all duration-200 shadow-md opacity-90 translate-x-3"
|
||||
>
|
||||
@@ -14,23 +10,23 @@ const { isMobileMenuOpen = $bindable(), label, onclick } = $props();
|
||||
class="flex flex-col justify-between w-[16px] h-[10px] transform transition-all duration-300 origin-center overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class={`bg-white h-[2px] w-7 transform transition-all duration-300 origin-left ${isMobileMenuOpen ? 'translate-x-10' : ''}`}
|
||||
class={`bg-white h-[2px] w-7 transform transition-all duration-300 origin-left ${isMobileMenuOpen ? "translate-x-10" : ""}`}
|
||||
></div>
|
||||
<div
|
||||
class={`bg-white h-[2px] w-7 rounded transform transition-all duration-300 delay-75 ${isMobileMenuOpen ? 'translate-x-10' : ''}`}
|
||||
class={`bg-white h-[2px] w-7 rounded transform transition-all duration-300 delay-75 ${isMobileMenuOpen ? "translate-x-10" : ""}`}
|
||||
></div>
|
||||
<div
|
||||
class={`bg-white h-[2px] w-7 transform transition-all duration-300 origin-left delay-150 ${isMobileMenuOpen ? 'translate-x-10' : ''}`}
|
||||
class={`bg-white h-[2px] w-7 transform transition-all duration-300 origin-left delay-150 ${isMobileMenuOpen ? "translate-x-10" : ""}`}
|
||||
></div>
|
||||
|
||||
<div
|
||||
class={`absolute items-center justify-between transform transition-all duration-500 top-6.5 -translate-x-10 flex w-0 ${isMobileMenuOpen ? 'translate-x-0 w-12' : ''}`}
|
||||
class={`absolute items-center justify-between transform transition-all duration-500 top-6.5 -translate-x-10 flex w-0 ${isMobileMenuOpen ? "translate-x-0 w-12" : ""}`}
|
||||
>
|
||||
<div
|
||||
class={`absolute bg-white h-[2px] w-4 transform transition-all duration-500 rotate-0 delay-300 ${isMobileMenuOpen ? 'rotate-45' : ''}`}
|
||||
class={`absolute bg-white h-[2px] w-4 transform transition-all duration-500 rotate-0 delay-300 ${isMobileMenuOpen ? "rotate-45" : ""}`}
|
||||
></div>
|
||||
<div
|
||||
class={`absolute bg-white h-[2px] w-4 transform transition-all duration-500 -rotate-0 delay-300 ${isMobileMenuOpen ? '-rotate-45' : ''}`}
|
||||
class={`absolute bg-white h-[2px] w-4 transform transition-all duration-500 -rotate-0 delay-300 ${isMobileMenuOpen ? "-rotate-45" : ""}`}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,99 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import { Slider } from "$lib/components/ui/slider";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Card, CardContent, CardHeader } from "$lib/components/ui/card";
|
||||
import type { BluetoothDevice } from "$lib/types";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { cn } from "$lib/utils";
|
||||
import { Slider } from "$lib/components/ui/slider";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Card, CardContent, CardHeader } from "$lib/components/ui/card";
|
||||
import type { BluetoothDevice } from "$lib/types";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
interface Props {
|
||||
device: BluetoothDevice;
|
||||
onChange: (scalarIndex: number, val: number) => void;
|
||||
onStop: () => void;
|
||||
}
|
||||
interface Props {
|
||||
device: BluetoothDevice;
|
||||
onChange: (scalarIndex: number, val: number) => void;
|
||||
onStop: () => void;
|
||||
}
|
||||
|
||||
let { device, onChange, onStop }: Props = $props();
|
||||
let { device, onChange, onStop }: Props = $props();
|
||||
|
||||
function getBatteryColor(level: number) {
|
||||
if (!device.hasBattery) {
|
||||
return "text-gray-400";
|
||||
}
|
||||
if (level > 60) return "text-green-400";
|
||||
if (level > 30) return "text-yellow-400";
|
||||
return "text-red-400";
|
||||
}
|
||||
function getBatteryColor(level: number) {
|
||||
if (!device.hasBattery) {
|
||||
return "text-gray-400";
|
||||
}
|
||||
if (level > 60) return "text-green-400";
|
||||
if (level > 30) return "text-yellow-400";
|
||||
return "text-red-400";
|
||||
}
|
||||
|
||||
function getBatteryBgColor(level: number) {
|
||||
if (!device.hasBattery) {
|
||||
return "bg-gray-400/20";
|
||||
}
|
||||
if (level > 60) return "bg-green-400/20";
|
||||
if (level > 30) return "bg-yellow-400/20";
|
||||
return "bg-red-400/20";
|
||||
}
|
||||
function getBatteryBgColor(level: number) {
|
||||
if (!device.hasBattery) {
|
||||
return "bg-gray-400/20";
|
||||
}
|
||||
if (level > 60) return "bg-green-400/20";
|
||||
if (level > 30) return "bg-yellow-400/20";
|
||||
return "bg-red-400/20";
|
||||
}
|
||||
|
||||
function getScalarAnimations() {
|
||||
return device.actuators
|
||||
.filter((a) => a.value > 0)
|
||||
.map((a) => `animate-${a.outputType.toLowerCase()}`);
|
||||
}
|
||||
function getScalarAnimations() {
|
||||
return device.actuators
|
||||
.filter((a) => a.value > 0)
|
||||
.map((a) => `animate-${a.outputType.toLowerCase()}`);
|
||||
}
|
||||
|
||||
function isActive() {
|
||||
return device.actuators.some((a) => a.value > 0);
|
||||
}
|
||||
function isActive() {
|
||||
return device.actuators.some((a) => a.value > 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
class="group hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 border-border/50 hover:border-primary/30 bg-card/50 backdrop-blur-sm"
|
||||
class="group hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 border-border/50 hover:border-primary/30 bg-card/50 backdrop-blur-sm"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="p-2 rounded-lg bg-gradient-to-br from-primary/20 to-accent/20 border border-primary/30 flex shrink-0 grow-0"
|
||||
>
|
||||
<span class={cn([...getScalarAnimations(), "icon-[ri--rocket-line] w-5 h-5 text-primary"])}></span>
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
class="font-semibold text-card-foreground group-hover:text-primary transition-colors"
|
||||
>
|
||||
{device.name}
|
||||
</h3>
|
||||
<!-- <p class="text-sm text-muted-foreground">
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="p-2 rounded-lg bg-gradient-to-br from-primary/20 to-accent/20 border border-primary/30 flex shrink-0 grow-0"
|
||||
>
|
||||
<span
|
||||
class={cn([...getScalarAnimations(), "icon-[ri--rocket-line] w-5 h-5 text-primary"])}
|
||||
></span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-card-foreground group-hover:text-primary transition-colors">
|
||||
{device.name}
|
||||
</h3>
|
||||
<!-- <p class="text-sm text-muted-foreground">
|
||||
{device.deviceType}
|
||||
</p> -->
|
||||
</div>
|
||||
</div>
|
||||
<button class={`${isActive() ? "cursor-pointer" : ""} flex items-center gap-2`} onclick={() => isActive() && onStop()}>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full {isActive()
|
||||
? 'bg-green-400'
|
||||
: 'bg-red-400'}"
|
||||
></div>
|
||||
{#if isActive()}
|
||||
<div
|
||||
class="absolute inset-0 w-2 h-2 rounded-full bg-green-400 animate-ping opacity-75"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
<span
|
||||
class="text-xs font-medium {isActive()
|
||||
? 'text-green-400'
|
||||
: 'text-red-400'}"
|
||||
>
|
||||
{isActive()
|
||||
? $_("device_card.active")
|
||||
: $_("device_card.paused")}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</div>
|
||||
<button
|
||||
class={`${isActive() ? "cursor-pointer" : ""} flex items-center gap-2`}
|
||||
onclick={() => isActive() && onStop()}
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="w-2 h-2 rounded-full {isActive() ? 'bg-green-400' : 'bg-red-400'}"></div>
|
||||
{#if isActive()}
|
||||
<div
|
||||
class="absolute inset-0 w-2 h-2 rounded-full bg-green-400 animate-ping opacity-75"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-xs font-medium {isActive() ? 'text-green-400' : 'text-red-400'}">
|
||||
{isActive() ? $_("device_card.active") : $_("device_card.paused")}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="space-y-4">
|
||||
<!-- Current Value -->
|
||||
<!-- <div
|
||||
<CardContent class="space-y-4">
|
||||
<!-- Current Value -->
|
||||
<!-- <div
|
||||
class="flex items-center justify-between p-3 rounded-lg bg-muted/30 border border-border/30"
|
||||
>
|
||||
<span class="text-sm text-muted-foreground"
|
||||
@@ -103,58 +96,54 @@ function isActive() {
|
||||
>
|
||||
</div> -->
|
||||
|
||||
<!-- Battery Level -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="icon-[ri--battery-2-charge-line] w-4 h-4 {getBatteryColor(
|
||||
device.batteryLevel,
|
||||
)}"
|
||||
></span>
|
||||
<span class="text-sm text-muted-foreground">{$_("device_card.battery")}</span>
|
||||
</div>
|
||||
{#if device.hasBattery}
|
||||
<span class="text-sm font-medium {getBatteryColor(device.batteryLevel)}">
|
||||
{device.batteryLevel}%
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-full bg-muted/50 rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-500 {getBatteryBgColor(
|
||||
device.batteryLevel,
|
||||
)} bg-gradient-to-r from-current to-current/80"
|
||||
style="width: {device.batteryLevel}%"
|
||||
></div>
|
||||
</div>
|
||||
<!-- Battery Level -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="icon-[ri--battery-2-charge-line] w-4 h-4 {getBatteryColor(device.batteryLevel)}"
|
||||
></span>
|
||||
<span class="text-sm text-muted-foreground">{$_("device_card.battery")}</span>
|
||||
</div>
|
||||
{#if device.hasBattery}
|
||||
<span class="text-sm font-medium {getBatteryColor(device.batteryLevel)}">
|
||||
{device.batteryLevel}%
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-full bg-muted/50 rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-500 {getBatteryBgColor(
|
||||
device.batteryLevel,
|
||||
)} bg-gradient-to-r from-current to-current/80"
|
||||
style="width: {device.batteryLevel}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last Seen -->
|
||||
<!-- <div
|
||||
<!-- Last Seen -->
|
||||
<!-- <div
|
||||
class="flex items-center justify-between text-xs text-muted-foreground"
|
||||
>
|
||||
<span>{$_("device_card.last_seen")}</span>
|
||||
<span>{device.lastSeen.toLocaleTimeString()}</span>
|
||||
</div> -->
|
||||
|
||||
<!-- Action Button -->
|
||||
{#each device.actuators as actuator, idx (idx)}
|
||||
<div class="space-y-2">
|
||||
<Label for={`device-${device.info.index}-${actuator.featureIndex}-${actuator.outputType}`}
|
||||
>{$_(
|
||||
`device_card.actuator_types.${actuator.outputType.toLowerCase()}`,
|
||||
)}</Label
|
||||
>
|
||||
<Slider
|
||||
id={`device-${device.info.index}-${actuator.featureIndex}-${actuator.outputType}`}
|
||||
type="single"
|
||||
value={actuator.value}
|
||||
onValueChange={(val) => onChange(idx, val)}
|
||||
max={actuator.maxSteps}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</CardContent>
|
||||
<!-- Action Button -->
|
||||
{#each device.actuators as actuator, idx (idx)}
|
||||
<div class="space-y-2">
|
||||
<Label for={`device-${device.info.index}-${actuator.featureIndex}-${actuator.outputType}`}
|
||||
>{$_(`device_card.actuator_types.${actuator.outputType.toLowerCase()}`)}</Label
|
||||
>
|
||||
<Slider
|
||||
id={`device-${device.info.index}-${actuator.featureIndex}-${actuator.outputType}`}
|
||||
type="single"
|
||||
value={actuator.value}
|
||||
onValueChange={(val) => onChange(idx, val)}
|
||||
max={actuator.maxSteps}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,120 +1,120 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import Logo from "../logo/logo.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import Logo from "../logo/logo.svelte";
|
||||
</script>
|
||||
|
||||
<footer
|
||||
class="bg-gradient-to-t from-card/95 to-card/85 backdrop-blur-xl mt-20 shadow-2xl shadow-primary/10"
|
||||
class="bg-gradient-to-t from-card/95 to-card/85 backdrop-blur-xl mt-20 shadow-2xl shadow-primary/10"
|
||||
>
|
||||
<div class="container mx-auto px-4 py-12">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<!-- Brand -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3 text-xl font-bold">
|
||||
<Logo />
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">{$_("brand.description")}</p>
|
||||
<div class="flex gap-3">
|
||||
<a
|
||||
aria-label="Email"
|
||||
href="mailto:{$_('footer.contact.email')}"
|
||||
class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center hover:bg-primary/20 transition-colors"
|
||||
>
|
||||
<span class="icon-[ri--mail-line] w-4 h-4 text-primary"></span>
|
||||
</a>
|
||||
<a
|
||||
aria-label="X"
|
||||
href="https://www.x.com/{$_('footer.contact.x')}"
|
||||
class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center hover:bg-primary/20 transition-colors"
|
||||
>
|
||||
<span class="icon-[ri--twitter-x-line] w-4 h-4 text-primary"></span>
|
||||
</a>
|
||||
<a
|
||||
aria-label="YouTube"
|
||||
href="https://www.youtube.com/@{$_('footer.contact.youtube')}"
|
||||
class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center hover:bg-primary/20 transition-colors"
|
||||
>
|
||||
<span class="icon-[ri--youtube-line] w-4 h-4 text-primary"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="font-semibold text-foreground">
|
||||
{$_("footer.quick_links")}
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<a
|
||||
href="/models"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.models")}</a
|
||||
>
|
||||
<a
|
||||
href="/videos"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.videos")}</a
|
||||
>
|
||||
<a
|
||||
href="/magazine"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.magazine")}</a
|
||||
>
|
||||
<a
|
||||
href="/about"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.about")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Support -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="font-semibold text-foreground">{$_("footer.support")}</h3>
|
||||
<div class="space-y-2">
|
||||
<a
|
||||
href="mailto:{$_('footer.contact_support_email')}"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.contact_support")}</a
|
||||
>
|
||||
<a
|
||||
href="mailto:{$_('footer.model_applications_email')}"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.model_applications")}</a
|
||||
>
|
||||
<a
|
||||
href="/faq"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.faq")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legal -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="font-semibold text-foreground">{$_("footer.legal")}</h3>
|
||||
<div class="space-y-2">
|
||||
<a
|
||||
href="/legal"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.privacy_policy")}</a
|
||||
>
|
||||
<a
|
||||
href="/legal"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.terms_of_service")}</a
|
||||
>
|
||||
<a
|
||||
href="/imprint"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.imprint")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container mx-auto px-4 py-12">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<!-- Brand -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3 text-xl font-bold">
|
||||
<Logo />
|
||||
</div>
|
||||
|
||||
<div class="border-t border-border/50 mt-8 pt-8 text-center">
|
||||
<p class="text-sm text-muted-foreground">{$_("footer.copyright")}</p>
|
||||
<p class="text-sm text-muted-foreground">{$_("brand.description")}</p>
|
||||
<div class="flex gap-3">
|
||||
<a
|
||||
aria-label="Email"
|
||||
href="mailto:{$_('footer.contact.email')}"
|
||||
class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center hover:bg-primary/20 transition-colors"
|
||||
>
|
||||
<span class="icon-[ri--mail-line] w-4 h-4 text-primary"></span>
|
||||
</a>
|
||||
<a
|
||||
aria-label="X"
|
||||
href="https://www.x.com/{$_('footer.contact.x')}"
|
||||
class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center hover:bg-primary/20 transition-colors"
|
||||
>
|
||||
<span class="icon-[ri--twitter-x-line] w-4 h-4 text-primary"></span>
|
||||
</a>
|
||||
<a
|
||||
aria-label="YouTube"
|
||||
href="https://www.youtube.com/@{$_('footer.contact.youtube')}"
|
||||
class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center hover:bg-primary/20 transition-colors"
|
||||
>
|
||||
<span class="icon-[ri--youtube-line] w-4 h-4 text-primary"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="font-semibold text-foreground">
|
||||
{$_("footer.quick_links")}
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<a
|
||||
href="/models"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.models")}</a
|
||||
>
|
||||
<a
|
||||
href="/videos"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.videos")}</a
|
||||
>
|
||||
<a
|
||||
href="/magazine"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.magazine")}</a
|
||||
>
|
||||
<a
|
||||
href="/about"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.about")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Support -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="font-semibold text-foreground">{$_("footer.support")}</h3>
|
||||
<div class="space-y-2">
|
||||
<a
|
||||
href="mailto:{$_('footer.contact_support_email')}"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.contact_support")}</a
|
||||
>
|
||||
<a
|
||||
href="mailto:{$_('footer.model_applications_email')}"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.model_applications")}</a
|
||||
>
|
||||
<a
|
||||
href="/faq"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.faq")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legal -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="font-semibold text-foreground">{$_("footer.legal")}</h3>
|
||||
<div class="space-y-2">
|
||||
<a
|
||||
href="/legal"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.privacy_policy")}</a
|
||||
>
|
||||
<a
|
||||
href="/legal"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.terms_of_service")}</a
|
||||
>
|
||||
<a
|
||||
href="/imprint"
|
||||
class="block text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>{$_("footer.imprint")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-border/50 mt-8 pt-8 text-center">
|
||||
<p class="text-sm text-muted-foreground">{$_("footer.copyright")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
stroke="#ce47eb"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<metadata>
|
||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<metadata> Created by potrace 1.15, written by Peter Selinger 2001-2017 </metadata>
|
||||
<g transform="translate(0.000000,904.000000) scale(0.100000,-0.100000)">
|
||||
<path
|
||||
d="M7930 7043 c-73 -10 -95 -18 -134 -51 -25 -20 -66 -53 -91 -72 -26
|
||||
@@ -117,4 +115,4 @@ m-3487 -790 c-17 -35 -55 -110 -84 -168 -29 -58 -72 -163 -96 -235 -45 -134
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import { page } from "$app/state";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import type { AuthStatus } from "$lib/types";
|
||||
import { logout } from "$lib/services";
|
||||
import { goto } from "$app/navigation";
|
||||
import { getAssetUrl } from "$lib/directus";
|
||||
import LogoutButton from "../logout-button/logout-button.svelte";
|
||||
import Separator from "../ui/separator/separator.svelte";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "$lib/components/ui/avatar";
|
||||
import { getUserInitials } from "$lib/utils";
|
||||
import BurgerMenuButton from "../burger-menu-button/burger-menu-button.svelte";
|
||||
import Girls from "../girls/girls.svelte";
|
||||
import Logo from "../logo/logo.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { page } from "$app/state";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import type { AuthStatus } from "$lib/types";
|
||||
import { logout } from "$lib/services";
|
||||
import { goto } from "$app/navigation";
|
||||
import { getAssetUrl } from "$lib/directus";
|
||||
import LogoutButton from "../logout-button/logout-button.svelte";
|
||||
import Separator from "../ui/separator/separator.svelte";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "$lib/components/ui/avatar";
|
||||
import { getUserInitials } from "$lib/utils";
|
||||
import BurgerMenuButton from "../burger-menu-button/burger-menu-button.svelte";
|
||||
import Girls from "../girls/girls.svelte";
|
||||
import Logo from "../logo/logo.svelte";
|
||||
|
||||
interface Props {
|
||||
authStatus: AuthStatus;
|
||||
}
|
||||
interface Props {
|
||||
authStatus: AuthStatus;
|
||||
}
|
||||
|
||||
let { authStatus }: Props = $props();
|
||||
let { authStatus }: Props = $props();
|
||||
|
||||
let isMobileMenuOpen = $state(false);
|
||||
let isMobileMenuOpen = $state(false);
|
||||
|
||||
const navLinks = [
|
||||
{ name: $_("header.home"), href: "/" },
|
||||
{ name: $_("header.models"), href: "/models" },
|
||||
{ name: $_("header.videos"), href: "/videos" },
|
||||
{ name: $_("header.magazine"), href: "/magazine" },
|
||||
{ name: $_("header.about"), href: "/about" },
|
||||
];
|
||||
const navLinks = [
|
||||
{ name: $_("header.home"), href: "/" },
|
||||
{ name: $_("header.models"), href: "/models" },
|
||||
{ name: $_("header.videos"), href: "/videos" },
|
||||
{ name: $_("header.magazine"), href: "/magazine" },
|
||||
{ name: $_("header.about"), href: "/about" },
|
||||
];
|
||||
|
||||
async function handleLogout() {
|
||||
closeMenu();
|
||||
await logout();
|
||||
goto("/login", { invalidateAll: true });
|
||||
}
|
||||
async function handleLogout() {
|
||||
closeMenu();
|
||||
await logout();
|
||||
goto("/login", { invalidateAll: true });
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
isMobileMenuOpen = false;
|
||||
}
|
||||
function closeMenu() {
|
||||
isMobileMenuOpen = false;
|
||||
}
|
||||
|
||||
function isActiveLink(link: any) {
|
||||
return (
|
||||
(page.url.pathname === "/" && link === navLinks[0]) ||
|
||||
(page.url.pathname.startsWith(link.href) && link !== navLinks[0])
|
||||
);
|
||||
}
|
||||
function isActiveLink(link: any) {
|
||||
return (
|
||||
(page.url.pathname === "/" && link === navLinks[0]) ||
|
||||
(page.url.pathname.startsWith(link.href) && link !== navLinks[0])
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<header
|
||||
@@ -58,7 +58,7 @@ function isActiveLink(link: any) {
|
||||
href="/"
|
||||
class="flex w-full items-center gap-3 hover:scale-105 transition-all duration-300"
|
||||
>
|
||||
<Logo hideName={true} />
|
||||
<Logo hideName={true} />
|
||||
</a>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
@@ -67,12 +67,12 @@ function isActiveLink(link: any) {
|
||||
<a
|
||||
href={link.href}
|
||||
class={`text-sm hover:text-foreground transition-colors duration-200 font-medium relative group ${
|
||||
isActiveLink(link) ? 'text-foreground' : 'text-foreground/85'
|
||||
isActiveLink(link) ? "text-foreground" : "text-foreground/85"
|
||||
}`}
|
||||
>
|
||||
{link.name}
|
||||
<span
|
||||
class={`absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-primary to-accent transition-all duration-300 ${isActiveLink(link) ? 'w-full' : 'group-hover:w-full'}`}
|
||||
class={`absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-primary to-accent transition-all duration-300 ${isActiveLink(link) ? "w-full" : "group-hover:w-full"}`}
|
||||
></span>
|
||||
</a>
|
||||
{/each}
|
||||
@@ -95,29 +95,29 @@ function isActiveLink(link: any) {
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
class={`hidden sm:flex h-9 w-9 rounded-full p-0 relative text-foreground/80 group ${isActiveLink({ href: '/me' }) ? 'text-foreground' : 'hover:text-foreground'}`}
|
||||
class={`hidden sm:flex h-9 w-9 rounded-full p-0 relative text-foreground/80 group ${isActiveLink({ href: "/me" }) ? "text-foreground" : "hover:text-foreground"}`}
|
||||
href="/me"
|
||||
title={$_('header.dashboard')}
|
||||
title={$_("header.dashboard")}
|
||||
>
|
||||
<span class="icon-[ri--dashboard-2-line] h-4 w-4"></span>
|
||||
<span
|
||||
class={`absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-primary to-accent transition-all duration-300 ${isActiveLink({ href: '/me' }) ? 'w-full' : 'group-hover:w-full'}`}
|
||||
class={`absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-primary to-accent transition-all duration-300 ${isActiveLink({ href: "/me" }) ? "w-full" : "group-hover:w-full"}`}
|
||||
></span>
|
||||
<span class="sr-only">{$_('header.dashboard')}</span>
|
||||
<span class="sr-only">{$_("header.dashboard")}</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
class={`hidden sm:flex h-9 w-9 rounded-full p-0 relative text-foreground/80 group ${isActiveLink({ href: '/play' }) ? 'text-foreground' : 'hover:text-foreground'}`}
|
||||
class={`hidden sm:flex h-9 w-9 rounded-full p-0 relative text-foreground/80 group ${isActiveLink({ href: "/play" }) ? "text-foreground" : "hover:text-foreground"}`}
|
||||
href="/play"
|
||||
title={$_('header.play')}
|
||||
title={$_("header.play")}
|
||||
>
|
||||
<span class="icon-[ri--rocket-line] h-4 w-4"></span>
|
||||
<span
|
||||
class={`absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-primary to-accent transition-all duration-300 ${isActiveLink({ href: '/play' }) ? 'w-full' : 'group-hover:w-full'}`}
|
||||
class={`absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-primary to-accent transition-all duration-300 ${isActiveLink({ href: "/play" }) ? "w-full" : "group-hover:w-full"}`}
|
||||
></span>
|
||||
<span class="sr-only">{$_('header.play')}</span>
|
||||
<span class="sr-only">{$_("header.play")}</span>
|
||||
</Button>
|
||||
|
||||
<Separator orientation="vertical" class="hidden md:flex mx-1 h-6 bg-border/50" />
|
||||
@@ -126,9 +126,10 @@ function isActiveLink(link: any) {
|
||||
|
||||
<LogoutButton
|
||||
user={{
|
||||
name: authStatus.user!.artist_name || authStatus.user!.email.split('@')[0] || 'User',
|
||||
avatar: getAssetUrl(authStatus.user!.avatar?.id, 'mini')!,
|
||||
email: authStatus.user!.email
|
||||
name:
|
||||
authStatus.user!.artist_name || authStatus.user!.email.split("@")[0] || "User",
|
||||
avatar: getAssetUrl(authStatus.user!.avatar?.id, "mini")!,
|
||||
email: authStatus.user!.email,
|
||||
}}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
@@ -136,18 +137,16 @@ function isActiveLink(link: any) {
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex w-full items-center justify-end gap-4">
|
||||
<Button variant="outline" class="font-medium" href="/login"
|
||||
>{$_('header.login')}</Button
|
||||
>
|
||||
<Button variant="outline" class="font-medium" href="/login">{$_("header.login")}</Button>
|
||||
<Button
|
||||
href="/signup"
|
||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 font-medium"
|
||||
>{$_('header.signup')}</Button
|
||||
>{$_("header.signup")}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<BurgerMenuButton
|
||||
label={$_('header.navigation')}
|
||||
label={$_("header.navigation")}
|
||||
bind:isMobileMenuOpen
|
||||
onclick={() => (isMobileMenuOpen = !isMobileMenuOpen)}
|
||||
/>
|
||||
@@ -155,26 +154,24 @@ function isActiveLink(link: any) {
|
||||
</div>
|
||||
<!-- Mobile Navigation -->
|
||||
<div
|
||||
class={`border-t border-border/20 bg-background/95 bg-gradient-to-br from-primary to-accent backdrop-blur-xl max-h-[calc(100vh-4rem)] overflow-y-auto shadow-xl/30 transition-all duration-250 ${isMobileMenuOpen ? 'opacity-100' : 'opacity-0'}`}
|
||||
class={`border-t border-border/20 bg-background/95 bg-gradient-to-br from-primary to-accent backdrop-blur-xl max-h-[calc(100vh-4rem)] overflow-y-auto shadow-xl/30 transition-all duration-250 ${isMobileMenuOpen ? "opacity-100" : "opacity-0"}`}
|
||||
>
|
||||
{#if isMobileMenuOpen}
|
||||
<div class="container mx-auto grid grid-cols-1 lg:grid-cols-3">
|
||||
<div class="hidden lg:flex col-span-2">
|
||||
<Girls />
|
||||
</div>
|
||||
<div class="py-6 px-4 space-y-6 lg:col-start-3 border-t border-border/20 bg-background/95 ">
|
||||
<div class="py-6 px-4 space-y-6 lg:col-start-3 border-t border-border/20 bg-background/95">
|
||||
<!-- User Profile Card -->
|
||||
{#if authStatus.authenticated}
|
||||
<div
|
||||
class="relative overflow-hidden rounded-2xl border border-border/50 bg-gradient-to-br from-card to-card/50 p-4 backdrop-blur-sm"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-primary/5 to-accent/5"
|
||||
></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-primary/5 to-accent/5"></div>
|
||||
<div class="relative flex items-center gap-4">
|
||||
<Avatar class="h-14 w-14 ring-2 ring-primary/30">
|
||||
<AvatarImage
|
||||
src={getAssetUrl(authStatus.user!.avatar?.id, 'mini')}
|
||||
src={getAssetUrl(authStatus.user!.avatar?.id, "mini")}
|
||||
alt={authStatus.user!.artist_name}
|
||||
/>
|
||||
<AvatarFallback
|
||||
@@ -212,17 +209,15 @@ function isActiveLink(link: any) {
|
||||
{/if}
|
||||
<!-- Navigation Cards -->
|
||||
<div class="space-y-3">
|
||||
<h3
|
||||
class="px-1 text-xs font-semibold text-muted-foreground uppercase tracking-wider"
|
||||
>
|
||||
{$_('header.navigation')}
|
||||
<h3 class="px-1 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{$_("header.navigation")}
|
||||
</h3>
|
||||
<div class="grid gap-2">
|
||||
{#each navLinks as link (link.href)}
|
||||
<a
|
||||
href={link.href}
|
||||
class="flex items-center justify-between rounded-xl border border-border/50 bg-card/50 p-4 backdrop-blur-sm transition-all hover:bg-card hover:border-primary/20 {isActiveLink(
|
||||
link
|
||||
link,
|
||||
)
|
||||
? 'border-primary/30 bg-primary/5'
|
||||
: ''}"
|
||||
@@ -233,8 +228,7 @@ function isActiveLink(link: any) {
|
||||
<!-- {#if isActiveLink(link)}
|
||||
<div class="h-2 w-2 rounded-full bg-primary"></div>
|
||||
{/if} -->
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-right-line] h-6 w-6 text-muted-foreground"
|
||||
<span class="icon-[ri--arrow-drop-right-line] h-6 w-6 text-muted-foreground"
|
||||
></span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -244,16 +238,14 @@ function isActiveLink(link: any) {
|
||||
|
||||
<!-- Account Actions -->
|
||||
<div class="space-y-3">
|
||||
<h3
|
||||
class="px-1 text-xs font-semibold text-muted-foreground uppercase tracking-wider"
|
||||
>
|
||||
{$_('header.account')}
|
||||
<h3 class="px-1 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{$_("header.account")}
|
||||
</h3>
|
||||
|
||||
<div class="grid gap-2">
|
||||
{#if authStatus.authenticated}
|
||||
<a
|
||||
class={`flex items-center gap-4 rounded-xl border border-border/50 bg-card/50 p-4 text-left backdrop-blur-sm transition-all group hover:bg-card hover:border-primary/20 ${isActiveLink({ href: '/me' }) ? 'border-primary/30 bg-primary/5' : ''}`}
|
||||
class={`flex items-center gap-4 rounded-xl border border-border/50 bg-card/50 p-4 text-left backdrop-blur-sm transition-all group hover:bg-card hover:border-primary/20 ${isActiveLink({ href: "/me" }) ? "border-primary/30 bg-primary/5" : ""}`}
|
||||
href="/me"
|
||||
onclick={closeMenu}
|
||||
>
|
||||
@@ -266,13 +258,9 @@ function isActiveLink(link: any) {
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-foreground"
|
||||
>{$_('header.dashboard')}</span
|
||||
>
|
||||
<span class="font-medium text-foreground">{$_("header.dashboard")}</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground"
|
||||
>{$_('header.dashboard_hint')}</span
|
||||
>
|
||||
<span class="text-sm text-muted-foreground">{$_("header.dashboard_hint")}</span>
|
||||
</div>
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-right-line] h-6 w-6 text-muted-foreground transition-all"
|
||||
@@ -280,7 +268,7 @@ function isActiveLink(link: any) {
|
||||
</a>
|
||||
|
||||
<a
|
||||
class={`flex items-center gap-4 rounded-xl border border-border/50 bg-card/50 p-4 text-left backdrop-blur-sm transition-all group hover:bg-card hover:border-primary/20 ${isActiveLink({ href: '/play' }) ? 'border-primary/30 bg-primary/5' : ''}`}
|
||||
class={`flex items-center gap-4 rounded-xl border border-border/50 bg-card/50 p-4 text-left backdrop-blur-sm transition-all group hover:bg-card hover:border-primary/20 ${isActiveLink({ href: "/play" }) ? "border-primary/30 bg-primary/5" : ""}`}
|
||||
href="/play"
|
||||
onclick={closeMenu}
|
||||
>
|
||||
@@ -293,13 +281,9 @@ function isActiveLink(link: any) {
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-foreground"
|
||||
>{$_('header.play')}</span
|
||||
>
|
||||
<span class="font-medium text-foreground">{$_("header.play")}</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground"
|
||||
>{$_('header.play_hint')}</span
|
||||
>
|
||||
<span class="text-sm text-muted-foreground">{$_("header.play_hint")}</span>
|
||||
</div>
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-right-line] h-6 w-6 text-muted-foreground transition-all"
|
||||
@@ -307,7 +291,7 @@ function isActiveLink(link: any) {
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
class={`flex items-center gap-4 rounded-xl border border-border/50 bg-card/50 p-4 text-left backdrop-blur-sm transition-all group hover:bg-card hover:border-primary/20 ${isActiveLink({ href: '/login' }) ? 'border-primary/30 bg-primary/5' : ''}`}
|
||||
class={`flex items-center gap-4 rounded-xl border border-border/50 bg-card/50 p-4 text-left backdrop-blur-sm transition-all group hover:bg-card hover:border-primary/20 ${isActiveLink({ href: "/login" }) ? "border-primary/30 bg-primary/5" : ""}`}
|
||||
href="/login"
|
||||
onclick={closeMenu}
|
||||
>
|
||||
@@ -320,13 +304,9 @@ function isActiveLink(link: any) {
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-foreground"
|
||||
>{$_('header.login')}</span
|
||||
>
|
||||
<span class="font-medium text-foreground">{$_("header.login")}</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground"
|
||||
>{$_('header.login_hint')}</span
|
||||
>
|
||||
<span class="text-sm text-muted-foreground">{$_("header.login_hint")}</span>
|
||||
</div>
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-right-line] h-6 w-6 text-muted-foreground transition-all"
|
||||
@@ -334,7 +314,7 @@ function isActiveLink(link: any) {
|
||||
</a>
|
||||
|
||||
<a
|
||||
class={`flex items-center gap-4 rounded-xl border border-border/50 bg-card/50 p-4 text-left backdrop-blur-sm transition-all group hover:bg-card hover:border-primary/20 ${isActiveLink({ href: '/signup' }) ? 'border-primary/30 bg-primary/5' : ''}`}
|
||||
class={`flex items-center gap-4 rounded-xl border border-border/50 bg-card/50 p-4 text-left backdrop-blur-sm transition-all group hover:bg-card hover:border-primary/20 ${isActiveLink({ href: "/signup" }) ? "border-primary/30 bg-primary/5" : ""}`}
|
||||
href="/signup"
|
||||
onclick={closeMenu}
|
||||
>
|
||||
@@ -347,13 +327,9 @@ function isActiveLink(link: any) {
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-foreground"
|
||||
>{$_('header.signup')}</span
|
||||
>
|
||||
<span class="font-medium text-foreground">{$_("header.signup")}</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground"
|
||||
>{$_('header.signup_hint')}</span
|
||||
>
|
||||
<span class="text-sm text-muted-foreground">{$_("header.signup_hint")}</span>
|
||||
</div>
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-right-line] h-6 w-6 text-muted-foreground transition-all"
|
||||
@@ -372,17 +348,11 @@ function isActiveLink(link: any) {
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-xl bg-destructive/10 group-hover:bg-destructive/20 transition-all"
|
||||
>
|
||||
<span
|
||||
class="icon-[ri--logout-circle-r-line] h-4 w-4 text-destructive"
|
||||
></span>
|
||||
<span class="icon-[ri--logout-circle-r-line] h-4 w-4 text-destructive"></span>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col gap-1">
|
||||
<span class="font-medium text-foreground"
|
||||
>{$_('header.logout')}</span
|
||||
>
|
||||
<span class="text-sm text-muted-foreground"
|
||||
>{$_('header.logout_hint')}</span
|
||||
>
|
||||
<span class="font-medium text-foreground">{$_("header.logout")}</span>
|
||||
<span class="text-sm text-muted-foreground">{$_("header.logout_hint")}</span>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
class?: string;
|
||||
size?: string | number;
|
||||
}
|
||||
interface Props {
|
||||
class?: string;
|
||||
size?: string | number;
|
||||
}
|
||||
|
||||
let { class: className = "", size = "24" }: Props = $props();
|
||||
let { class: className = "", size = "24" }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 512 512"
|
||||
class={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 512 512"
|
||||
class={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g class="" transform="translate(0,0)" style=""
|
||||
><path
|
||||
d="M418.813 30.625c-21.178 26.27-49.712 50.982-84.125 70.844-36.778 21.225-75.064 33.62-110.313 38.06a310.317 310.317 0 0 0 6.813 18.25c16.01.277 29.366-.434 36.406-1.5l9.47-1.53 8.436-1.28.22 10.186a307.48 307.48 0 0 1-1.095 18.72l56.625 8.843c.86-.095 1.713-.15 2.563-.157 11.188-.114 21.44 7.29 24.468 18.593.657 2.448.922 4.903.845 7.313 5.972-2.075 11.753-4.305 17.28-6.72l9.595-4.188 2.313 10.22a340.211 340.211 0 0 1 7.375 48.062C438.29 247.836 468.438 225.71 493 197.5c-3.22-36.73-16.154-78.04-39.125-117.813a290.509 290.509 0 0 0-2.22-3.78l-27.56 71.374c5.154.762 10.123 3.158 14.092 7.126 9.81 9.807 9.813 25.69 0 35.5-9.812 9.81-25.722 9.807-35.53 0-8.86-8.858-9.69-22.68-2.532-32.5l38.938-100.844a322.02 322.02 0 0 0-20.25-25.937zM51.842 118.72c-8.46 17.373-15.76 36.198-21.187 56.436-14.108 52.617-13.96 103.682-2.812 143.438 13.3-2.605 26.442-3.96 39.312-4.03 1.855-.012 3.688.02 5.53.06 20.857.48 40.98 4.332 59.97 11.5a355.064 355.064 0 0 1-1.656-34.218c0-27.8 3.135-54.377 9-78.937l2.47-10.407 9.655 4.562c29.467 13.98 66.194 23.424 106.28 25.22 5.136-20.05 8.19-39.78 9.408-58.75-35.198 4.83-75.387 2.766-116.407-8.22-38.363-10.272-72.314-26.78-99.562-46.656zm230.594 82.218c-1.535 10.452-3.615 21.03-6.218 31.687a312.754 312.754 0 0 0 46-3.97 24.98 24.98 0 0 1-1.532-21.748l-38.25-5.97zM105 201.375l4.156 18.22-21.594 4.905c8.75 5.174 13.353 15.703 10.594 26-3.32 12.394-16.045 19.758-28.437 16.438-12.394-3.32-19.76-16.075-16.44-28.47a23.235 23.235 0 0 1 3.126-6.874l-21.062 4.78-4.125-18.218 73.78-16.78zm388.594 22.813c-25.53 25.46-55.306 45.445-86.906 60.5.05 2.397.093 4.8.093 7.218 0 9.188-.354 18.232-1.03 27.125 16.635 1.33 32.045-1.7 45.344-9.374 25.925-14.962 40.608-45.694 42.5-85.47zm-338.844 3c-4.03 19.993-6.33 41.31-6.406 63.593l.125-.342c30.568 10.174 62.622 17.572 95.25 21.375l7.5.875.718 7.5 5.687 60.125-18.625 1.75-2.53-26.75a23.117 23.117 0 0 1-14.845.968c-12.393-3.32-19.76-16.042-16.438-28.436.285-1.06.647-2.08 1.063-3.063a496.627 496.627 0 0 1-57.406-14.53c2.69 49.62 16.154 94.04 36.094 126.656 22.366 36.588 52.13 57.78 83.968 57.78 31.838.003 61.602-21.19 83.97-57.78 19.536-31.96 32.846-75.244 35.905-123.656a499.132 499.132 0 0 1-48.25 11.656c1.914 4.57 2.415 9.78 1.033 14.938-3.322 12.394-16.045 19.758-28.438 16.437a23.01 23.01 0 0 1-2.125-.686l-2.5 26.47-18.594-1.752 5.688-60.125.72-7.5 7.498-.875c29.245-3.407 57.995-9.717 85.657-18.312v-1.594c0-21.573-2.27-42.23-6.064-61.75C351.132 242.653 313.092 250 272.312 250c-43.59 0-83.986-8.658-117.562-22.813zm-87.5 105.968c-10.87.102-21.995 1.22-33.375 3.313 12.695 31.62 33.117 53.07 59 60 16.9 4.523 34.896 2.536 52.813-5.25-4.382-13.89-7.874-28.606-10.344-43.97-21.115-9.623-43.934-14.32-68.094-14.094zm137.5 80.22h130.813c-40.082 44.594-92.623 42.844-130.813 0z"
|
||||
fill-opacity="1"
|
||||
style="fill: currentColor; stroke: #ce47eb; stroke-width: 10px;"
|
||||
|
||||
></path></g
|
||||
></svg
|
||||
<g class="" transform="translate(0,0)" style=""
|
||||
><path
|
||||
d="M418.813 30.625c-21.178 26.27-49.712 50.982-84.125 70.844-36.778 21.225-75.064 33.62-110.313 38.06a310.317 310.317 0 0 0 6.813 18.25c16.01.277 29.366-.434 36.406-1.5l9.47-1.53 8.436-1.28.22 10.186a307.48 307.48 0 0 1-1.095 18.72l56.625 8.843c.86-.095 1.713-.15 2.563-.157 11.188-.114 21.44 7.29 24.468 18.593.657 2.448.922 4.903.845 7.313 5.972-2.075 11.753-4.305 17.28-6.72l9.595-4.188 2.313 10.22a340.211 340.211 0 0 1 7.375 48.062C438.29 247.836 468.438 225.71 493 197.5c-3.22-36.73-16.154-78.04-39.125-117.813a290.509 290.509 0 0 0-2.22-3.78l-27.56 71.374c5.154.762 10.123 3.158 14.092 7.126 9.81 9.807 9.813 25.69 0 35.5-9.812 9.81-25.722 9.807-35.53 0-8.86-8.858-9.69-22.68-2.532-32.5l38.938-100.844a322.02 322.02 0 0 0-20.25-25.937zM51.842 118.72c-8.46 17.373-15.76 36.198-21.187 56.436-14.108 52.617-13.96 103.682-2.812 143.438 13.3-2.605 26.442-3.96 39.312-4.03 1.855-.012 3.688.02 5.53.06 20.857.48 40.98 4.332 59.97 11.5a355.064 355.064 0 0 1-1.656-34.218c0-27.8 3.135-54.377 9-78.937l2.47-10.407 9.655 4.562c29.467 13.98 66.194 23.424 106.28 25.22 5.136-20.05 8.19-39.78 9.408-58.75-35.198 4.83-75.387 2.766-116.407-8.22-38.363-10.272-72.314-26.78-99.562-46.656zm230.594 82.218c-1.535 10.452-3.615 21.03-6.218 31.687a312.754 312.754 0 0 0 46-3.97 24.98 24.98 0 0 1-1.532-21.748l-38.25-5.97zM105 201.375l4.156 18.22-21.594 4.905c8.75 5.174 13.353 15.703 10.594 26-3.32 12.394-16.045 19.758-28.437 16.438-12.394-3.32-19.76-16.075-16.44-28.47a23.235 23.235 0 0 1 3.126-6.874l-21.062 4.78-4.125-18.218 73.78-16.78zm388.594 22.813c-25.53 25.46-55.306 45.445-86.906 60.5.05 2.397.093 4.8.093 7.218 0 9.188-.354 18.232-1.03 27.125 16.635 1.33 32.045-1.7 45.344-9.374 25.925-14.962 40.608-45.694 42.5-85.47zm-338.844 3c-4.03 19.993-6.33 41.31-6.406 63.593l.125-.342c30.568 10.174 62.622 17.572 95.25 21.375l7.5.875.718 7.5 5.687 60.125-18.625 1.75-2.53-26.75a23.117 23.117 0 0 1-14.845.968c-12.393-3.32-19.76-16.042-16.438-28.436.285-1.06.647-2.08 1.063-3.063a496.627 496.627 0 0 1-57.406-14.53c2.69 49.62 16.154 94.04 36.094 126.656 22.366 36.588 52.13 57.78 83.968 57.78 31.838.003 61.602-21.19 83.97-57.78 19.536-31.96 32.846-75.244 35.905-123.656a499.132 499.132 0 0 1-48.25 11.656c1.914 4.57 2.415 9.78 1.033 14.938-3.322 12.394-16.045 19.758-28.438 16.437a23.01 23.01 0 0 1-2.125-.686l-2.5 26.47-18.594-1.752 5.688-60.125.72-7.5 7.498-.875c29.245-3.407 57.995-9.717 85.657-18.312v-1.594c0-21.573-2.27-42.23-6.064-61.75C351.132 242.653 313.092 250 272.312 250c-43.59 0-83.986-8.658-117.562-22.813zm-87.5 105.968c-10.87.102-21.995 1.22-33.375 3.313 12.695 31.62 33.117 53.07 59 60 16.9 4.523 34.896 2.536 52.813-5.25-4.382-13.89-7.874-28.606-10.344-43.97-21.115-9.623-43.934-14.32-68.094-14.094zm137.5 80.22h130.813c-40.082 44.594-92.623 42.844-130.813 0z"
|
||||
fill-opacity="1"
|
||||
style="fill: currentColor; stroke: #ce47eb; stroke-width: 10px;"
|
||||
></path></g
|
||||
></svg
|
||||
>
|
||||
|
||||
@@ -1,109 +1,107 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import { browser } from "$app/environment";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import Button from "../ui/button/button.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { browser } from "$app/environment";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import Button from "../ui/button/button.svelte";
|
||||
|
||||
const { images = [] } = $props();
|
||||
const { images = [] } = $props();
|
||||
|
||||
let isViewerOpen = $state(false);
|
||||
let currentImageIndex = $state(0);
|
||||
let imageLoading = $state(false);
|
||||
let isViewerOpen = $state(false);
|
||||
let currentImageIndex = $state(0);
|
||||
let imageLoading = $state(false);
|
||||
|
||||
let currentImage = $derived(images[currentImageIndex]);
|
||||
let canGoPrev = $derived(currentImageIndex > 0);
|
||||
let canGoNext = $derived(currentImageIndex < images.length - 1);
|
||||
let currentImage = $derived(images[currentImageIndex]);
|
||||
let canGoPrev = $derived(currentImageIndex > 0);
|
||||
let canGoNext = $derived(currentImageIndex < images.length - 1);
|
||||
|
||||
function openViewer(index) {
|
||||
currentImageIndex = index;
|
||||
isViewerOpen = true;
|
||||
imageLoading = true;
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
function openViewer(index) {
|
||||
currentImageIndex = index;
|
||||
isViewerOpen = true;
|
||||
imageLoading = true;
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
function closeViewer() {
|
||||
isViewerOpen = false;
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
function closeViewer() {
|
||||
isViewerOpen = false;
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
function navigatePrev() {
|
||||
if (canGoPrev) {
|
||||
currentImageIndex--;
|
||||
imageLoading = true;
|
||||
}
|
||||
}
|
||||
function navigatePrev() {
|
||||
if (canGoPrev) {
|
||||
currentImageIndex--;
|
||||
imageLoading = true;
|
||||
}
|
||||
}
|
||||
|
||||
function navigateNext() {
|
||||
if (canGoNext) {
|
||||
currentImageIndex++;
|
||||
imageLoading = true;
|
||||
}
|
||||
}
|
||||
function navigateNext() {
|
||||
if (canGoNext) {
|
||||
currentImageIndex++;
|
||||
imageLoading = true;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadImage() {
|
||||
const link = document.createElement("a");
|
||||
link.href = currentImage.url;
|
||||
link.download = currentImage.title.replace(/\\s+/g, "_") + ".jpg";
|
||||
link.target = "_blank";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
function downloadImage() {
|
||||
const link = document.createElement("a");
|
||||
link.href = currentImage.url;
|
||||
link.download = currentImage.title.replace(/\\s+/g, "_") + ".jpg";
|
||||
link.target = "_blank";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (!isViewerOpen) return;
|
||||
function handleKeydown(event) {
|
||||
if (!isViewerOpen) return;
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowLeft":
|
||||
event.preventDefault();
|
||||
navigatePrev();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
event.preventDefault();
|
||||
navigateNext();
|
||||
break;
|
||||
case "Escape":
|
||||
event.preventDefault();
|
||||
closeViewer();
|
||||
break;
|
||||
case "d":
|
||||
case "D":
|
||||
event.preventDefault();
|
||||
downloadImage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
switch (event.key) {
|
||||
case "ArrowLeft":
|
||||
event.preventDefault();
|
||||
navigatePrev();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
event.preventDefault();
|
||||
navigateNext();
|
||||
break;
|
||||
case "Escape":
|
||||
event.preventDefault();
|
||||
closeViewer();
|
||||
break;
|
||||
case "d":
|
||||
case "D":
|
||||
event.preventDefault();
|
||||
downloadImage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageLoad() {
|
||||
imageLoading = false;
|
||||
}
|
||||
function handleImageLoad() {
|
||||
imageLoading = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
// Preload images
|
||||
images.forEach((img) => {
|
||||
const preload = new Image();
|
||||
preload.src = img.url;
|
||||
});
|
||||
});
|
||||
onMount(() => {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
// Preload images
|
||||
images.forEach((img) => {
|
||||
const preload = new Image();
|
||||
preload.src = img.url;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
window.removeEventListener("keydown", handleKeydown);
|
||||
document.body.style.overflow = "";
|
||||
});
|
||||
onDestroy(() => {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
window.removeEventListener("keydown", handleKeydown);
|
||||
document.body.style.overflow = "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Gallery Grid -->
|
||||
<div class="w-full mx-auto">
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 animate-fade-in"
|
||||
>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 animate-fade-in">
|
||||
{#each images as image, index (index)}
|
||||
<button
|
||||
onclick={() => openViewer(index)}
|
||||
@@ -145,14 +143,9 @@ onDestroy(() => {
|
||||
|
||||
<!-- Image Viewer Modal -->
|
||||
{#if isViewerOpen}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center animate-fade-in"
|
||||
>
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center animate-fade-in">
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="absolute inset-0 bg-black/95 backdrop-blur-xl"
|
||||
onclick={closeViewer}
|
||||
></div>
|
||||
<div class="absolute inset-0 bg-black/95 backdrop-blur-xl" onclick={closeViewer}></div>
|
||||
|
||||
<!-- Viewer Content -->
|
||||
<div class="relative w-[90vw] h-[90vh] flex flex-col animate-slide-up">
|
||||
@@ -166,9 +159,9 @@ onDestroy(() => {
|
||||
<div class="text-primary font-medium mb-3">
|
||||
{$_("image_viewer.index", {
|
||||
values: {
|
||||
index: currentImageIndex + 1,
|
||||
size: images.length
|
||||
}
|
||||
index: currentImageIndex + 1,
|
||||
size: images.length,
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
<p class="text-zinc-400 max-w-2xl">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import PeonyIcon from "../icon/peony-icon.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import PeonyIcon from "../icon/peony-icon.svelte";
|
||||
|
||||
const { hideName = false } = $props();
|
||||
const { hideName = false } = $props();
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
@@ -11,11 +11,11 @@ const { hideName = false } = $props();
|
||||
<span
|
||||
class={`logo text-3xl text-foreground opacity-90 tracking-wide font-extrabold drop-shadow-x ${hideName ? "hidden sm:inline-block" : ""}`}
|
||||
>
|
||||
{$_('brand.name')}
|
||||
{$_("brand.name")}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.logo {
|
||||
font-family: 'Dancing Script', cursive;
|
||||
font-family: "Dancing Script", cursive;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,148 +1,184 @@
|
||||
<script lang="ts">
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "$lib/components/ui/avatar";
|
||||
import { getUserInitials } from "$lib/utils";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "$lib/components/ui/avatar";
|
||||
import { getUserInitials } from "$lib/utils";
|
||||
|
||||
interface User {
|
||||
name?: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
}
|
||||
interface User {
|
||||
name?: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
onLogout: () => void;
|
||||
}
|
||||
interface Props {
|
||||
user: User;
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
let { user, onLogout }: Props = $props();
|
||||
let { user, onLogout }: Props = $props();
|
||||
|
||||
let isDragging = $state(false);
|
||||
let slidePosition = $state(0);
|
||||
let startX = 0;
|
||||
let currentX = 0;
|
||||
let maxSlide = 117; // Maximum slide distance
|
||||
let threshold = 0.75; // 70% threshold to trigger logout
|
||||
let isDragging = $state(false);
|
||||
let slidePosition = $state(0);
|
||||
let startX = 0;
|
||||
let currentX = 0;
|
||||
let maxSlide = 117; // Maximum slide distance
|
||||
let threshold = 0.75; // 70% threshold to trigger logout
|
||||
|
||||
// Calculate slide progress (0 to 1)
|
||||
const slideProgress = $derived(Math.min(slidePosition / maxSlide, 1));
|
||||
const isNearThreshold = $derived(slideProgress > threshold);
|
||||
// Calculate slide progress (0 to 1)
|
||||
const slideProgress = $derived(Math.min(slidePosition / maxSlide, 1));
|
||||
const isNearThreshold = $derived(slideProgress > threshold);
|
||||
|
||||
const handleStart = (clientX: number) => {
|
||||
isDragging = true;
|
||||
startX = clientX;
|
||||
currentX = clientX;
|
||||
};
|
||||
const handleStart = (clientX: number) => {
|
||||
isDragging = true;
|
||||
startX = clientX;
|
||||
currentX = clientX;
|
||||
};
|
||||
|
||||
const handleMove = (clientX: number) => {
|
||||
if (!isDragging) return;
|
||||
const handleMove = (clientX: number) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
currentX = clientX;
|
||||
const deltaX = currentX - startX;
|
||||
slidePosition = Math.max(0, Math.min(deltaX, maxSlide));
|
||||
};
|
||||
currentX = clientX;
|
||||
const deltaX = currentX - startX;
|
||||
slidePosition = Math.max(0, Math.min(deltaX, maxSlide));
|
||||
};
|
||||
|
||||
const handleEnd = () => {
|
||||
if (!isDragging) return;
|
||||
const handleEnd = () => {
|
||||
if (!isDragging) return;
|
||||
|
||||
isDragging = false;
|
||||
isDragging = false;
|
||||
|
||||
if (slideProgress >= threshold) {
|
||||
// Trigger logout
|
||||
slidePosition = maxSlide;
|
||||
onLogout();
|
||||
} else {
|
||||
// Snap back
|
||||
slidePosition = 0;
|
||||
}
|
||||
};
|
||||
if (slideProgress >= threshold) {
|
||||
// Trigger logout
|
||||
slidePosition = maxSlide;
|
||||
onLogout();
|
||||
} else {
|
||||
// Snap back
|
||||
slidePosition = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Mouse events
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
handleStart(e.clientX);
|
||||
};
|
||||
// Mouse events
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
handleStart(e.clientX);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
handleMove(e.clientX);
|
||||
};
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
handleMove(e.clientX);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
handleEnd();
|
||||
};
|
||||
const handleMouseUp = () => {
|
||||
handleEnd();
|
||||
};
|
||||
|
||||
// Touch events
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
handleStart(e.touches[0].clientX);
|
||||
};
|
||||
// Touch events
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
handleStart(e.touches[0].clientX);
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: TouchEvent) => {
|
||||
e.preventDefault();
|
||||
handleMove(e.touches[0].clientX);
|
||||
};
|
||||
const handleTouchMove = (e: TouchEvent) => {
|
||||
e.preventDefault();
|
||||
handleMove(e.touches[0].clientX);
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
handleEnd();
|
||||
};
|
||||
const handleTouchEnd = () => {
|
||||
handleEnd();
|
||||
};
|
||||
|
||||
// Add global event listeners when dragging
|
||||
$effect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
document.addEventListener("touchmove", handleTouchMove, { passive: false });
|
||||
document.addEventListener("touchend", handleTouchEnd);
|
||||
// Add global event listeners when dragging
|
||||
$effect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
document.addEventListener("touchmove", handleTouchMove, { passive: false });
|
||||
document.addEventListener("touchend", handleTouchEnd);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
document.removeEventListener("touchmove", handleTouchMove);
|
||||
document.removeEventListener("touchend", handleTouchEnd);
|
||||
};
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
document.removeEventListener("touchmove", handleTouchMove);
|
||||
document.removeEventListener("touchend", handleTouchEnd);
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative h-10 w-40 rounded-full bg-muted/30 overflow-hidden select-none transition-all duration-300 bg-muted/40 shadow-lg shadow-accent/10 {isDragging ? 'cursor-grabbing' : ''}"
|
||||
style="background: linear-gradient(90deg,
|
||||
class="relative h-10 w-40 rounded-full bg-muted/30 overflow-hidden select-none transition-all duration-300 bg-muted/40 shadow-lg shadow-accent/10 {isDragging
|
||||
? 'cursor-grabbing'
|
||||
: ''}"
|
||||
style="background: linear-gradient(90deg,
|
||||
oklch(var(--primary) / 0.3) 0%,
|
||||
oklch(var(--primary) / 0.3) {(1 - slideProgress) * 100}%,
|
||||
oklch(var(--accent) / {0.1 + slideProgress * 0.2}) {(1 - slideProgress) * 100}%,
|
||||
oklch(var(--accent) / {0.2 + slideProgress * 0.3}) 100%
|
||||
)"
|
||||
>
|
||||
<!-- Background slide indicator -->
|
||||
<div
|
||||
class="absolute inset-0 rounded-full transition-all duration-200"
|
||||
style="background: linear-gradient(90deg,
|
||||
<!-- Background slide indicator -->
|
||||
<div
|
||||
class="absolute inset-0 rounded-full transition-all duration-200"
|
||||
style="background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
transparent {Math.max(0, slideProgress * 100 - 20)}%,
|
||||
oklch(var(--accent) / {slideProgress * 0.1}) {slideProgress * 100}%,
|
||||
oklch(var(--accent) / {slideProgress * 0.2}) 100%
|
||||
)"
|
||||
></div>
|
||||
></div>
|
||||
|
||||
<!-- Sliding user info -->
|
||||
<button class="cursor-grab absolute left-0 top-0 h-full flex items-center gap-3 px-2 transition-all duration-200 ease-out rounded-full bg-background/80 backdrop-blur-sm border border-border/50 bg-background/90 border-primary/20 {isDragging ? '' : 'transition-all duration-300 ease-out'}" style="transform: translateX({slidePosition}px); width: calc(100% - {slidePosition}px);" onmousedown={handleMouseDown} ontouchstart={handleTouchStart}>
|
||||
<Avatar class="h-7 w-7 ring-2 ring-accent/20 transition-all duration-200 {isNearThreshold ? 'ring-destructive/40' : ''}" style="opacity: {Math.max(0.1, 1 - slideProgress * 1.8)}">
|
||||
<AvatarImage src={user.avatar} alt={user.name || user.email} />
|
||||
<AvatarFallback class="bg-gradient-to-br from-primary to-accent text-primary-foreground text-xs font-semibold transition-all duration-200 {isNearThreshold ? 'from-destructive to-destructive/80' : ''}">
|
||||
{getUserInitials(user.name || user.email)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="text-left flex flex-col min-w-0 flex-1">
|
||||
<span class="text-sm font-medium text-foreground leading-none truncate transition-all duration-200 {isNearThreshold ? 'text-destructive' : ''}" style="opacity: {Math.max(0.15, 1 - slideProgress * 1.5)}">{user?.name ? user.name.split(" ")[0] : "User"}</span>
|
||||
<span class="text-xs text-muted-foreground leading-none transition-all duration-200 {isNearThreshold ? 'text-destructive/70' : ''}" style="opacity: {Math.max(0.1, 1 - slideProgress * 1.8)}">
|
||||
{slideProgress > 0.3 ? "Logout" : "Online"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<!-- Sliding user info -->
|
||||
<button
|
||||
class="cursor-grab absolute left-0 top-0 h-full flex items-center gap-3 px-2 transition-all duration-200 ease-out rounded-full bg-background/80 backdrop-blur-sm border border-border/50 bg-background/90 border-primary/20 {isDragging
|
||||
? ''
|
||||
: 'transition-all duration-300 ease-out'}"
|
||||
style="transform: translateX({slidePosition}px); width: calc(100% - {slidePosition}px);"
|
||||
onmousedown={handleMouseDown}
|
||||
ontouchstart={handleTouchStart}
|
||||
>
|
||||
<Avatar
|
||||
class="h-7 w-7 ring-2 ring-accent/20 transition-all duration-200 {isNearThreshold
|
||||
? 'ring-destructive/40'
|
||||
: ''}"
|
||||
style="opacity: {Math.max(0.1, 1 - slideProgress * 1.8)}"
|
||||
>
|
||||
<AvatarImage src={user.avatar} alt={user.name || user.email} />
|
||||
<AvatarFallback
|
||||
class="bg-gradient-to-br from-primary to-accent text-primary-foreground text-xs font-semibold transition-all duration-200 {isNearThreshold
|
||||
? 'from-destructive to-destructive/80'
|
||||
: ''}"
|
||||
>
|
||||
{getUserInitials(user.name || user.email)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="text-left flex flex-col min-w-0 flex-1">
|
||||
<span
|
||||
class="text-sm font-medium text-foreground leading-none truncate transition-all duration-200 {isNearThreshold
|
||||
? 'text-destructive'
|
||||
: ''}"
|
||||
style="opacity: {Math.max(0.15, 1 - slideProgress * 1.5)}"
|
||||
>{user?.name ? user.name.split(" ")[0] : "User"}</span
|
||||
>
|
||||
<span
|
||||
class="text-xs text-muted-foreground leading-none transition-all duration-200 {isNearThreshold
|
||||
? 'text-destructive/70'
|
||||
: ''}"
|
||||
style="opacity: {Math.max(0.1, 1 - slideProgress * 1.8)}"
|
||||
>
|
||||
{slideProgress > 0.3 ? "Logout" : "Online"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Logout icon area -->
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center w-8 h-8 rounded-full transition-all duration-200 {isNearThreshold ? 'bg-destructive text-destructive-foreground scale-110' : 'bg-transparent text-foreground'}">
|
||||
<span class="icon-[ri--logout-circle-r-line] h-4 w-4 transition-transform duration-200 {isNearThreshold ? 'scale-110' : ''}" ></span>
|
||||
</div>
|
||||
<!-- Logout icon area -->
|
||||
<div
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center w-8 h-8 rounded-full transition-all duration-200 {isNearThreshold
|
||||
? 'bg-destructive text-destructive-foreground scale-110'
|
||||
: 'bg-transparent text-foreground'}"
|
||||
>
|
||||
<span
|
||||
class="icon-[ri--logout-circle-r-line] h-4 w-4 transition-transform duration-200 {isNearThreshold
|
||||
? 'scale-110'
|
||||
: ''}"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Progress indicator -->
|
||||
<!-- <div class="absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-primary to-accent transition-all duration-200 rounded-full" style="width: {slideProgress * 100}%"></div> -->
|
||||
<!-- Progress indicator -->
|
||||
<!-- <div class="absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-primary to-accent transition-all duration-200 rounded-full" style="width: {slideProgress * 100}%"></div> -->
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { env } from "$env/dynamic/public";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
image?: string;
|
||||
}
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
title,
|
||||
description,
|
||||
image = `${env.PUBLIC_URL || "http://localhost:3000"}/img/kamasutra.jpg`,
|
||||
}: Props = $props();
|
||||
let {
|
||||
title,
|
||||
description,
|
||||
image = `${env.PUBLIC_URL || "http://localhost:3000"}/img/kamasutra.jpg`,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_("head.title", { values: { title } })}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta
|
||||
property="og:title"
|
||||
content={$_("head.title", { values: { title } })}
|
||||
/>
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={image} />
|
||||
<title>{$_("head.title", { values: { title } })}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:title" content={$_("head.title", { values: { title } })} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={image} />
|
||||
</svelte:head>
|
||||
|
||||
@@ -1,180 +1,163 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import { Card, CardContent, CardHeader } from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import type { Recording } from "$lib/types";
|
||||
import { cn } from "$lib/utils";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { Card, CardContent, CardHeader } from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import type { Recording } from "$lib/types";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
interface Props {
|
||||
recording: Recording;
|
||||
onPlay?: (id: string) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
}
|
||||
interface Props {
|
||||
recording: Recording;
|
||||
onPlay?: (id: string) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
}
|
||||
|
||||
let { recording, onPlay, onDelete }: Props = $props();
|
||||
let { recording, onPlay, onDelete }: Props = $props();
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||
}
|
||||
function formatDuration(ms: number): string {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function getStatusColor(status: string): string {
|
||||
switch (status) {
|
||||
case "published":
|
||||
return "text-green-400 bg-green-400/20";
|
||||
case "draft":
|
||||
return "text-yellow-400 bg-yellow-400/20";
|
||||
case "archived":
|
||||
return "text-red-400 bg-red-400/20";
|
||||
default:
|
||||
return "text-gray-400 bg-gray-400/20";
|
||||
}
|
||||
}
|
||||
function getStatusColor(status: string): string {
|
||||
switch (status) {
|
||||
case "published":
|
||||
return "text-green-400 bg-green-400/20";
|
||||
case "draft":
|
||||
return "text-yellow-400 bg-yellow-400/20";
|
||||
case "archived":
|
||||
return "text-red-400 bg-red-400/20";
|
||||
default:
|
||||
return "text-gray-400 bg-gray-400/20";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
class="group hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 border-border/50 hover:border-primary/30 bg-card/50 backdrop-blur-sm"
|
||||
class="group hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 border-border/50 hover:border-primary/30 bg-card/50 backdrop-blur-sm"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3
|
||||
class="font-semibold text-card-foreground group-hover:text-primary transition-colors"
|
||||
>
|
||||
{recording.title}
|
||||
</h3>
|
||||
<span
|
||||
class={cn(
|
||||
"text-xs px-2 py-0.5 rounded-full",
|
||||
getStatusColor(recording.status),
|
||||
)}
|
||||
>
|
||||
{$_(`recording_card.status_${recording.status}`)}
|
||||
</span>
|
||||
</div>
|
||||
{#if recording.description}
|
||||
<p class="text-sm text-muted-foreground line-clamp-2">
|
||||
{recording.description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="font-semibold text-card-foreground group-hover:text-primary transition-colors">
|
||||
{recording.title}
|
||||
</h3>
|
||||
<span class={cn("text-xs px-2 py-0.5 rounded-full", getStatusColor(recording.status))}>
|
||||
{$_(`recording_card.status_${recording.status}`)}
|
||||
</span>
|
||||
</div>
|
||||
{#if recording.description}
|
||||
<p class="text-sm text-muted-foreground line-clamp-2">
|
||||
{recording.description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="space-y-4">
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div
|
||||
class="flex flex-col items-center p-3 rounded-lg bg-muted/30 border border-border/30"
|
||||
>
|
||||
<span class="icon-[ri--time-line] w-4 h-4 text-primary mb-1"></span>
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>{$_("recording_card.duration")}</span
|
||||
>
|
||||
<span class="font-medium text-sm">{formatDuration(recording.duration)}</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center p-3 rounded-lg bg-muted/30 border border-border/30"
|
||||
>
|
||||
<span class="icon-[ri--pulse-line] w-4 h-4 text-accent mb-1"></span>
|
||||
<span class="text-xs text-muted-foreground">{$_("recording_card.events")}</span>
|
||||
<span class="font-medium text-sm">{recording.events.length}</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center p-3 rounded-lg bg-muted/30 border border-border/30"
|
||||
>
|
||||
<span class="icon-[ri--gamepad-line] w-4 h-4 text-primary mb-1"></span>
|
||||
<span class="text-xs text-muted-foreground">{$_("recording_card.devices")}</span>
|
||||
<span class="font-medium text-sm">{recording.device_info.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="flex flex-col items-center p-3 rounded-lg bg-muted/30 border border-border/30">
|
||||
<span class="icon-[ri--time-line] w-4 h-4 text-primary mb-1"></span>
|
||||
<span class="text-xs text-muted-foreground">{$_("recording_card.duration")}</span>
|
||||
<span class="font-medium text-sm">{formatDuration(recording.duration)}</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center p-3 rounded-lg bg-muted/30 border border-border/30">
|
||||
<span class="icon-[ri--pulse-line] w-4 h-4 text-accent mb-1"></span>
|
||||
<span class="text-xs text-muted-foreground">{$_("recording_card.events")}</span>
|
||||
<span class="font-medium text-sm">{recording.events.length}</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center p-3 rounded-lg bg-muted/30 border border-border/30">
|
||||
<span class="icon-[ri--gamepad-line] w-4 h-4 text-primary mb-1"></span>
|
||||
<span class="text-xs text-muted-foreground">{$_("recording_card.devices")}</span>
|
||||
<span class="font-medium text-sm">{recording.device_info.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Info -->
|
||||
<div class="space-y-1">
|
||||
{#each recording.device_info.slice(0, 2) as device (device.name)}
|
||||
<div
|
||||
class="flex items-center gap-2 text-xs text-muted-foreground bg-muted/20 rounded px-2 py-1"
|
||||
>
|
||||
<span class="icon-[ri--rocket-line] w-3 h-3"></span>
|
||||
<span>{device.name}</span>
|
||||
<span class="text-xs opacity-60">• {device.capabilities.join(", ")}</span>
|
||||
</div>
|
||||
{/each}
|
||||
{#if recording.device_info.length > 2}
|
||||
<div class="text-xs text-muted-foreground/60 px-2">
|
||||
+{recording.device_info.length - 2} more device{recording.device_info.length -
|
||||
2 >
|
||||
1
|
||||
? "s"
|
||||
: ""}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Device Info -->
|
||||
<div class="space-y-1">
|
||||
{#each recording.device_info.slice(0, 2) as device (device.name)}
|
||||
<div
|
||||
class="flex items-center gap-2 text-xs text-muted-foreground bg-muted/20 rounded px-2 py-1"
|
||||
>
|
||||
<span class="icon-[ri--rocket-line] w-3 h-3"></span>
|
||||
<span>{device.name}</span>
|
||||
<span class="text-xs opacity-60">• {device.capabilities.join(", ")}</span>
|
||||
</div>
|
||||
{/each}
|
||||
{#if recording.device_info.length > 2}
|
||||
<div class="text-xs text-muted-foreground/60 px-2">
|
||||
+{recording.device_info.length - 2} more device{recording.device_info.length - 2 > 1
|
||||
? "s"
|
||||
: ""}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
{#if recording.tags && recording.tags.length > 0}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each recording.tags as tag (tag)}
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Tags -->
|
||||
{#if recording.tags && recording.tags.length > 0}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each recording.tags as tag (tag)}
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Metadata -->
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground pt-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<span>
|
||||
{new Date(recording.date_created).toLocaleDateString()}
|
||||
</span>
|
||||
{#if recording.public}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="icon-[ri--global-line] w-3 h-3"></span>
|
||||
{$_("recording_card.public")}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="icon-[ri--lock-line] w-3 h-3"></span>
|
||||
{$_("recording_card.private")}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if recording.linked_video}
|
||||
<span class="flex items-center gap-1 text-accent">
|
||||
<span class="icon-[ri--video-line] w-3 h-3"></span>
|
||||
{$_("recording_card.linked_video")}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Metadata -->
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground pt-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<span>
|
||||
{new Date(recording.date_created).toLocaleDateString()}
|
||||
</span>
|
||||
{#if recording.public}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="icon-[ri--global-line] w-3 h-3"></span>
|
||||
{$_("recording_card.public")}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="icon-[ri--lock-line] w-3 h-3"></span>
|
||||
{$_("recording_card.private")}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if recording.linked_video}
|
||||
<span class="flex items-center gap-1 text-accent">
|
||||
<span class="icon-[ri--video-line] w-3 h-3"></span>
|
||||
{$_("recording_card.linked_video")}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2 pt-2">
|
||||
{#if onPlay}
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={() => onPlay?.(recording.id)}
|
||||
class="flex-1 cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
>
|
||||
<span class="icon-[ri--play-fill] w-4 h-4 mr-1"></span>
|
||||
{$_("recording_card.play")}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if onDelete}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => onDelete?.(recording.id)}
|
||||
class="cursor-pointer border-destructive/20 hover:bg-destructive/10 hover:text-destructive"
|
||||
>
|
||||
<span class="icon-[ri--delete-bin-line] w-4 h-4"></span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</CardContent>
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2 pt-2">
|
||||
{#if onPlay}
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={() => onPlay?.(recording.id)}
|
||||
class="flex-1 cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
>
|
||||
<span class="icon-[ri--play-fill] w-4 h-4 mr-1"></span>
|
||||
{$_("recording_card.play")}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if onDelete}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => onDelete?.(recording.id)}
|
||||
class="cursor-pointer border-destructive/20 hover:bg-destructive/10 hover:text-destructive"
|
||||
>
|
||||
<span class="icon-[ri--delete-bin-line] w-4 h-4"></span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
onclick: () => void;
|
||||
icon: string;
|
||||
label: string;
|
||||
}
|
||||
interface Props {
|
||||
onclick: () => void;
|
||||
icon: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
let { onclick, icon, label }: Props = $props();
|
||||
let { onclick, icon, label }: Props = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
{onclick}
|
||||
aria-label={label}
|
||||
class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center hover:bg-primary/20 transition-colors cursor-pointer"
|
||||
{onclick}
|
||||
aria-label={label}
|
||||
class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center hover:bg-primary/20 transition-colors cursor-pointer"
|
||||
>
|
||||
<span class={icon + " w-4 h-4 text-primary"}></span>
|
||||
<span class={icon + " w-4 h-4 text-primary"}></span>
|
||||
</button>
|
||||
|
||||
@@ -1,110 +1,110 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import ShareButton from "./share-button.svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import type { ShareContent } from "$lib/types";
|
||||
import { _ } from "svelte-i18n";
|
||||
import ShareButton from "./share-button.svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import type { ShareContent } from "$lib/types";
|
||||
|
||||
interface Props {
|
||||
content: ShareContent;
|
||||
}
|
||||
interface Props {
|
||||
content: ShareContent;
|
||||
}
|
||||
|
||||
let { content }: Props = $props();
|
||||
let { content }: Props = $props();
|
||||
|
||||
// Share handlers
|
||||
const shareToX = () => {
|
||||
const text = `${content.title} - ${content.description}`;
|
||||
const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(content.url)}`;
|
||||
window.open(url, "_blank", "width=600,height=400");
|
||||
toast.success($_("sharing_popup.success.x"));
|
||||
};
|
||||
// Share handlers
|
||||
const shareToX = () => {
|
||||
const text = `${content.title} - ${content.description}`;
|
||||
const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(content.url)}`;
|
||||
window.open(url, "_blank", "width=600,height=400");
|
||||
toast.success($_("sharing_popup.success.x"));
|
||||
};
|
||||
|
||||
const shareToFacebook = () => {
|
||||
const url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(content.url)}"e=${encodeURIComponent(content.title)}`;
|
||||
window.open(url, "_blank", "width=600,height=400");
|
||||
toast.success($_("sharing_popup.success.facebook"));
|
||||
};
|
||||
const shareToFacebook = () => {
|
||||
const url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(content.url)}"e=${encodeURIComponent(content.title)}`;
|
||||
window.open(url, "_blank", "width=600,height=400");
|
||||
toast.success($_("sharing_popup.success.facebook"));
|
||||
};
|
||||
|
||||
const shareViaEmail = () => {
|
||||
const subject = encodeURIComponent(content.title);
|
||||
const body = encodeURIComponent(`${content.description}\n\n${content.url}`);
|
||||
const url = `mailto:?subject=${subject}&body=${body}`;
|
||||
window.location.href = url;
|
||||
toast.success($_("sharing_popup.success.email"));
|
||||
};
|
||||
const shareViaEmail = () => {
|
||||
const subject = encodeURIComponent(content.title);
|
||||
const body = encodeURIComponent(`${content.description}\n\n${content.url}`);
|
||||
const url = `mailto:?subject=${subject}&body=${body}`;
|
||||
window.location.href = url;
|
||||
toast.success($_("sharing_popup.success.email"));
|
||||
};
|
||||
|
||||
const shareToWhatsApp = () => {
|
||||
const text = `${content.title}\n\n${content.description}\n\n${content.url}`;
|
||||
const url = `https://wa.me/?text=${encodeURIComponent(text)}`;
|
||||
window.open(url, "_blank");
|
||||
toast.success($_("sharing_popup.success.whatsapp"));
|
||||
};
|
||||
const shareToWhatsApp = () => {
|
||||
const text = `${content.title}\n\n${content.description}\n\n${content.url}`;
|
||||
const url = `https://wa.me/?text=${encodeURIComponent(text)}`;
|
||||
window.open(url, "_blank");
|
||||
toast.success($_("sharing_popup.success.whatsapp"));
|
||||
};
|
||||
|
||||
const shareToTelegram = () => {
|
||||
const text = `${content.title}\n\n${content.description}`;
|
||||
const url = `https://t.me/share/url?url=${encodeURIComponent(content.url)}&text=${encodeURIComponent(text)}`;
|
||||
window.open(url, "_blank");
|
||||
toast.success($_("sharing_popup.success.telegram"));
|
||||
};
|
||||
const shareToTelegram = () => {
|
||||
const text = `${content.title}\n\n${content.description}`;
|
||||
const url = `https://t.me/share/url?url=${encodeURIComponent(content.url)}&text=${encodeURIComponent(text)}`;
|
||||
window.open(url, "_blank");
|
||||
toast.success($_("sharing_popup.success.telegram"));
|
||||
};
|
||||
|
||||
const copyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content.url);
|
||||
toast.success($_("sharing_popup.success.copy"));
|
||||
} catch {
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = content.url;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textArea);
|
||||
toast.success($_("sharing_popup.success.copy"));
|
||||
}
|
||||
};
|
||||
const copyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content.url);
|
||||
toast.success($_("sharing_popup.success.copy"));
|
||||
} catch {
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = content.url;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textArea);
|
||||
toast.success($_("sharing_popup.success.copy"));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="text-center space-y-4">
|
||||
<h4 class="text-sm font-medium text-muted-foreground">
|
||||
{$_("sharing_popup.subtitle")}
|
||||
</h4>
|
||||
<div class="text-center space-y-4">
|
||||
<h4 class="text-sm font-medium text-muted-foreground">
|
||||
{$_("sharing_popup.subtitle")}
|
||||
</h4>
|
||||
|
||||
<div class="flex justify-center gap-3 flex-wrap">
|
||||
<ShareButton
|
||||
onclick={shareToX}
|
||||
icon="icon-[ri--twitter-x-line]"
|
||||
label={$_("sharing_popup.share.x")}
|
||||
/>
|
||||
<div class="flex justify-center gap-3 flex-wrap">
|
||||
<ShareButton
|
||||
onclick={shareToX}
|
||||
icon="icon-[ri--twitter-x-line]"
|
||||
label={$_("sharing_popup.share.x")}
|
||||
/>
|
||||
|
||||
<ShareButton
|
||||
onclick={shareToFacebook}
|
||||
icon="icon-[ri--facebook-line]"
|
||||
label={$_("sharing_popup.share.facebook")}
|
||||
/>
|
||||
<ShareButton
|
||||
onclick={shareToFacebook}
|
||||
icon="icon-[ri--facebook-line]"
|
||||
label={$_("sharing_popup.share.facebook")}
|
||||
/>
|
||||
|
||||
<ShareButton
|
||||
onclick={shareViaEmail}
|
||||
icon="icon-[ri--mail-line]"
|
||||
label={$_("sharing_popup.share.email")}
|
||||
/>
|
||||
<ShareButton
|
||||
onclick={shareViaEmail}
|
||||
icon="icon-[ri--mail-line]"
|
||||
label={$_("sharing_popup.share.email")}
|
||||
/>
|
||||
|
||||
<ShareButton
|
||||
onclick={shareToWhatsApp}
|
||||
icon="icon-[ri--whatsapp-line]"
|
||||
label={$_("sharing_popup.share.whatsapp")}
|
||||
/>
|
||||
<ShareButton
|
||||
onclick={shareToWhatsApp}
|
||||
icon="icon-[ri--whatsapp-line]"
|
||||
label={$_("sharing_popup.share.whatsapp")}
|
||||
/>
|
||||
|
||||
<ShareButton
|
||||
onclick={shareToTelegram}
|
||||
icon="icon-[ri--telegram-2-line]"
|
||||
label={$_("sharing_popup.share.telegram")}
|
||||
/>
|
||||
<ShareButton
|
||||
onclick={shareToTelegram}
|
||||
icon="icon-[ri--telegram-2-line]"
|
||||
label={$_("sharing_popup.share.telegram")}
|
||||
/>
|
||||
|
||||
<ShareButton
|
||||
onclick={copyLink}
|
||||
icon="icon-[ri--file-copy-line]"
|
||||
label={$_("sharing_popup.share.copy")}
|
||||
/>
|
||||
</div>
|
||||
<ShareButton
|
||||
onclick={copyLink}
|
||||
icon="icon-[ri--file-copy-line]"
|
||||
label={$_("sharing_popup.share.copy")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import { _ } from "svelte-i18n";
|
||||
import SharingPopup from "./sharing-popup.svelte";
|
||||
import Button from "../ui/button/button.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import SharingPopup from "./sharing-popup.svelte";
|
||||
import Button from "../ui/button/button.svelte";
|
||||
|
||||
const { content } = $props();
|
||||
let isPopupOpen = $state(false);
|
||||
const { content } = $props();
|
||||
let isPopupOpen = $state(false);
|
||||
</script>
|
||||
|
||||
<Button
|
||||
@@ -13,6 +13,6 @@ let isPopupOpen = $state(false);
|
||||
class="flex items-center gap-2 border-primary/20 hover:bg-primary/10 cursor-pointer"
|
||||
>
|
||||
<span class="icon-[ri--share-2-line] w-4 h-4"></span>
|
||||
{$_('sharing_popup_button.share')}
|
||||
{$_("sharing_popup_button.share")}
|
||||
</Button>
|
||||
<SharingPopup bind:open={isPopupOpen} {content} />
|
||||
|
||||
@@ -1,89 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "$lib/components/ui/dialog";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import ShareServices from "./share-services.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "$lib/components/ui/dialog";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import ShareServices from "./share-services.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface ShareContent {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
type: "video" | "model" | "article" | "link";
|
||||
}
|
||||
interface ShareContent {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
type: "video" | "model" | "article" | "link";
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
content: ShareContent;
|
||||
children?: Snippet;
|
||||
}
|
||||
interface Props {
|
||||
open: boolean;
|
||||
content: ShareContent;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { open = $bindable(), content }: Props = $props();
|
||||
let { open = $bindable(), content }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Dialog bind:open>
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-10 h-10 rounded-full bg-gradient-to-br from-primary to-purple-600 flex items-center justify-center shrink-0 grow-0"
|
||||
>
|
||||
<span class="icon-[ri--share-2-line] text-primary-foreground"></span>
|
||||
</div>
|
||||
<div class="">
|
||||
<DialogTitle class="text-left text-xl font-semibold text-primary-foreground"
|
||||
>{$_("sharing_popup.title")}</DialogTitle
|
||||
>
|
||||
<DialogDescription class="text-left text-sm">
|
||||
{$_("sharing_popup.description", {
|
||||
values: { type: content.type },
|
||||
})}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Preview -->
|
||||
<div class="text-left bg-muted/60 rounded-lg p-4 space-y-2">
|
||||
<h4 class="font-medium text-sm text-primary-foreground">
|
||||
{content.title}
|
||||
</h4>
|
||||
<p class="text-xs text-muted-foreground">{content.description}</p>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="px-2 py-1 bg-primary/10 text-primary rounded-full capitalize">
|
||||
{content.type}
|
||||
</span>
|
||||
<span class="text-muted-foreground text-clip">{content.url}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<Separator class="my-4" />
|
||||
|
||||
<!-- Share Services -->
|
||||
<ShareServices {content} />
|
||||
|
||||
<Separator class="my-4" />
|
||||
|
||||
<!-- Close Button -->
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => (open = false)}
|
||||
class="text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-10 h-10 rounded-full bg-gradient-to-br from-primary to-purple-600 flex items-center justify-center shrink-0 grow-0"
|
||||
>
|
||||
<span class="icon-[ri--share-2-line] text-primary-foreground"></span>
|
||||
</div>
|
||||
<div class="">
|
||||
<DialogTitle class="text-left text-xl font-semibold text-primary-foreground"
|
||||
>{$_("sharing_popup.title")}</DialogTitle
|
||||
>
|
||||
<span class="icon-[ri--close-large-line]"></span>
|
||||
{$_("sharing_popup.close")}
|
||||
</Button>
|
||||
<DialogDescription class="text-left text-sm">
|
||||
{$_("sharing_popup.description", {
|
||||
values: { type: content.type },
|
||||
})}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</div>
|
||||
|
||||
<!-- Content Preview -->
|
||||
<div class="text-left bg-muted/60 rounded-lg p-4 space-y-2">
|
||||
<h4 class="font-medium text-sm text-primary-foreground">
|
||||
{content.title}
|
||||
</h4>
|
||||
<p class="text-xs text-muted-foreground">{content.description}</p>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="px-2 py-1 bg-primary/10 text-primary rounded-full capitalize">
|
||||
{content.type}
|
||||
</span>
|
||||
<span class="text-muted-foreground text-clip">{content.url}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<Separator class="my-4" />
|
||||
|
||||
<!-- Share Services -->
|
||||
<ShareServices {content} />
|
||||
|
||||
<Separator class="my-4" />
|
||||
|
||||
<!-- Close Button -->
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => (open = false)}
|
||||
class="text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
>
|
||||
<span class="icon-[ri--close-large-line]"></span>
|
||||
{$_("sharing_popup.close")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-description"
|
||||
class={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="alert-description"
|
||||
class={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-title"
|
||||
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="alert-title"
|
||||
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const alertVariants = tv({
|
||||
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
export const alertVariants = tv({
|
||||
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
|
||||
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
variant = "default",
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
variant?: AlertVariant;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
variant = "default",
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
variant?: AlertVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert"
|
||||
class={cn(alertVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
role="alert"
|
||||
bind:this={ref}
|
||||
data-slot="alert"
|
||||
class={cn(alertVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
role="alert"
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,11 @@ import Title from "./alert-title.svelte";
|
||||
export { alertVariants, type AlertVariant } from "./alert.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Description,
|
||||
Title,
|
||||
//
|
||||
Root as Alert,
|
||||
Description as AlertDescription,
|
||||
Title as AlertTitle,
|
||||
Root,
|
||||
Description,
|
||||
Title,
|
||||
//
|
||||
Root as Alert,
|
||||
Description as AlertDescription,
|
||||
Title as AlertTitle,
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.FallbackProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.FallbackProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Fallback
|
||||
bind:ref
|
||||
data-slot="avatar-fallback"
|
||||
class={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="avatar-fallback"
|
||||
class={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.ImageProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.ImageProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Image
|
||||
bind:ref
|
||||
data-slot="avatar-image"
|
||||
class={cn("aspect-square size-full", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="avatar-image"
|
||||
class={cn("aspect-square size-full", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
loadingStatus = $bindable("loading"),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.RootProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
loadingStatus = $bindable("loading"),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Root
|
||||
bind:ref
|
||||
bind:loadingStatus
|
||||
data-slot="avatar"
|
||||
class={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
bind:loadingStatus
|
||||
data-slot="avatar"
|
||||
class={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -3,11 +3,11 @@ 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,
|
||||
Root,
|
||||
Image,
|
||||
Fallback,
|
||||
//
|
||||
Root as Avatar,
|
||||
Image as AvatarImage,
|
||||
Fallback as AvatarFallback,
|
||||
};
|
||||
|
||||
@@ -1,86 +1,80 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type {
|
||||
HTMLAnchorAttributes,
|
||||
HTMLButtonAttributes,
|
||||
} from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
|
||||
outline:
|
||||
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
|
||||
outline:
|
||||
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? "link" : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? "link" : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
} from "./button.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
};
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-action"
|
||||
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card-action"
|
||||
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||
</script>
|
||||
|
||||
<p
|
||||
bind:this={ref}
|
||||
data-slot="card-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</p>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-footer"
|
||||
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card-footer"
|
||||
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-header"
|
||||
class={cn(
|
||||
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card-header"
|
||||
class={cn(
|
||||
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-title"
|
||||
class={cn("font-semibold leading-none", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card-title"
|
||||
class={cn("font-semibold leading-none", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card"
|
||||
class={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card"
|
||||
class={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -7,19 +7,19 @@ import Title from "./card-title.svelte";
|
||||
import Action from "./card-action.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
};
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { Checkbox as CheckboxPrimitive } from "bits-ui";
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import MinusIcon from "@lucide/svelte/icons/minus";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import { Checkbox as CheckboxPrimitive } from "bits-ui";
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import MinusIcon from "@lucide/svelte/icons/minus";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
|
||||
</script>
|
||||
|
||||
<CheckboxPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="checkbox"
|
||||
class={cn(
|
||||
"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="checkbox"
|
||||
class={cn(
|
||||
"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<div data-slot="checkbox-indicator" class="text-current transition-none">
|
||||
{#if checked}
|
||||
<CheckIcon class="size-3.5" />
|
||||
{:else if indeterminate}
|
||||
<MinusIcon class="size-3.5" />
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<div data-slot="checkbox-indicator" class="text-current transition-none">
|
||||
{#if checked}
|
||||
<CheckIcon class="size-3.5" />
|
||||
{:else if indeterminate}
|
||||
<MinusIcon class="size-3.5" />
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</CheckboxPrimitive.Root>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Root from "./checkbox.svelte";
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Checkbox,
|
||||
Root,
|
||||
//
|
||||
Root as Checkbox,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps =
|
||||
$props();
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Dialog from "./index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Dialog from "./index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
children: Snippet;
|
||||
showCloseButton?: boolean;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
children: Snippet;
|
||||
showCloseButton?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Portal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close
|
||||
class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute end-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</DialogPrimitive.Content>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close
|
||||
class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute end-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</DialogPrimitive.Content>
|
||||
</Dialog.Portal>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="dialog-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="dialog-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-footer"
|
||||
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="dialog-footer"
|
||||
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-header"
|
||||
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="dialog-header"
|
||||
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn("text-lg font-semibold leading-none", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn("text-lg font-semibold leading-none", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps =
|
||||
$props();
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
|
||||
|
||||
@@ -13,25 +13,25 @@ const Root = DialogPrimitive.Root;
|
||||
const Portal = DialogPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose,
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose,
|
||||
};
|
||||
|
||||
@@ -3,183 +3,174 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/utils";
|
||||
import UploadIcon from "@lucide/svelte/icons/upload";
|
||||
import { displaySize } from ".";
|
||||
import { useId } from "bits-ui";
|
||||
import type { FileDropZoneProps, FileRejectedReason } from "./types";
|
||||
import { cn } from "$lib/utils/utils";
|
||||
import UploadIcon from "@lucide/svelte/icons/upload";
|
||||
import { displaySize } from ".";
|
||||
import { useId } from "bits-ui";
|
||||
import type { FileDropZoneProps, FileRejectedReason } from "./types";
|
||||
|
||||
let {
|
||||
id = useId(),
|
||||
children,
|
||||
maxFiles,
|
||||
maxFileSize,
|
||||
fileCount,
|
||||
disabled = false,
|
||||
onUpload,
|
||||
onFileRejected,
|
||||
accept,
|
||||
class: className,
|
||||
...rest
|
||||
}: FileDropZoneProps = $props();
|
||||
let {
|
||||
id = useId(),
|
||||
children,
|
||||
maxFiles,
|
||||
maxFileSize,
|
||||
fileCount,
|
||||
disabled = false,
|
||||
onUpload,
|
||||
onFileRejected,
|
||||
accept,
|
||||
class: className,
|
||||
...rest
|
||||
}: FileDropZoneProps = $props();
|
||||
|
||||
if (maxFiles !== undefined && fileCount === undefined) {
|
||||
console.warn(
|
||||
"Make sure to provide FileDropZone with `fileCount` when using the `maxFiles` prompt",
|
||||
);
|
||||
}
|
||||
if (maxFiles !== undefined && fileCount === undefined) {
|
||||
console.warn(
|
||||
"Make sure to provide FileDropZone with `fileCount` when using the `maxFiles` prompt",
|
||||
);
|
||||
}
|
||||
|
||||
let uploading = $state(false);
|
||||
let uploading = $state(false);
|
||||
|
||||
const drop = async (
|
||||
e: DragEvent & {
|
||||
currentTarget: EventTarget & HTMLLabelElement;
|
||||
},
|
||||
) => {
|
||||
if (disabled || !canUploadFiles) return;
|
||||
const drop = async (
|
||||
e: DragEvent & {
|
||||
currentTarget: EventTarget & HTMLLabelElement;
|
||||
},
|
||||
) => {
|
||||
if (disabled || !canUploadFiles) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.preventDefault();
|
||||
|
||||
const droppedFiles = Array.from(e.dataTransfer?.files ?? []);
|
||||
const droppedFiles = Array.from(e.dataTransfer?.files ?? []);
|
||||
|
||||
await upload(droppedFiles);
|
||||
};
|
||||
await upload(droppedFiles);
|
||||
};
|
||||
|
||||
const change = async (
|
||||
e: Event & {
|
||||
currentTarget: EventTarget & HTMLInputElement;
|
||||
},
|
||||
) => {
|
||||
if (disabled) return;
|
||||
const change = async (
|
||||
e: Event & {
|
||||
currentTarget: EventTarget & HTMLInputElement;
|
||||
},
|
||||
) => {
|
||||
if (disabled) return;
|
||||
|
||||
const selectedFiles = e.currentTarget.files;
|
||||
const selectedFiles = e.currentTarget.files;
|
||||
|
||||
if (!selectedFiles) return;
|
||||
if (!selectedFiles) return;
|
||||
|
||||
await upload(Array.from(selectedFiles));
|
||||
await upload(Array.from(selectedFiles));
|
||||
|
||||
// this if a file fails and we upload the same file again we still get feedback
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
};
|
||||
// this if a file fails and we upload the same file again we still get feedback
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
};
|
||||
|
||||
const shouldAcceptFile = (
|
||||
file: File,
|
||||
fileNumber: number,
|
||||
): FileRejectedReason | undefined => {
|
||||
if (maxFileSize !== undefined && file.size > maxFileSize)
|
||||
return "Maximum file size exceeded";
|
||||
const shouldAcceptFile = (file: File, fileNumber: number): FileRejectedReason | undefined => {
|
||||
if (maxFileSize !== undefined && file.size > maxFileSize) return "Maximum file size exceeded";
|
||||
|
||||
if (maxFiles !== undefined && fileNumber > maxFiles)
|
||||
return "Maximum files uploaded";
|
||||
if (maxFiles !== undefined && fileNumber > maxFiles) return "Maximum files uploaded";
|
||||
|
||||
if (!accept) return undefined;
|
||||
if (!accept) return undefined;
|
||||
|
||||
const acceptedTypes = accept.split(",").map((a) => a.trim().toLowerCase());
|
||||
const fileType = file.type.toLowerCase();
|
||||
const fileName = file.name.toLowerCase();
|
||||
const acceptedTypes = accept.split(",").map((a) => a.trim().toLowerCase());
|
||||
const fileType = file.type.toLowerCase();
|
||||
const fileName = file.name.toLowerCase();
|
||||
|
||||
const isAcceptable = acceptedTypes.some((pattern) => {
|
||||
// check extension like .mp4
|
||||
if (fileType.startsWith(".")) {
|
||||
return fileName.endsWith(pattern);
|
||||
}
|
||||
const isAcceptable = acceptedTypes.some((pattern) => {
|
||||
// check extension like .mp4
|
||||
if (fileType.startsWith(".")) {
|
||||
return fileName.endsWith(pattern);
|
||||
}
|
||||
|
||||
// if pattern has wild card like video/*
|
||||
if (pattern.endsWith("/*")) {
|
||||
const baseType = pattern.slice(0, pattern.indexOf("/*"));
|
||||
return fileType.startsWith(baseType + "/");
|
||||
}
|
||||
// if pattern has wild card like video/*
|
||||
if (pattern.endsWith("/*")) {
|
||||
const baseType = pattern.slice(0, pattern.indexOf("/*"));
|
||||
return fileType.startsWith(baseType + "/");
|
||||
}
|
||||
|
||||
// otherwise it must be a specific type like video/mp4
|
||||
return fileType === pattern;
|
||||
});
|
||||
// otherwise it must be a specific type like video/mp4
|
||||
return fileType === pattern;
|
||||
});
|
||||
|
||||
if (!isAcceptable) return "File type not allowed";
|
||||
if (!isAcceptable) return "File type not allowed";
|
||||
|
||||
return undefined;
|
||||
};
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const upload = async (uploadFiles: File[]) => {
|
||||
uploading = true;
|
||||
const upload = async (uploadFiles: File[]) => {
|
||||
uploading = true;
|
||||
|
||||
const validFiles: File[] = [];
|
||||
const validFiles: File[] = [];
|
||||
|
||||
for (let i = 0; i < uploadFiles.length; i++) {
|
||||
const file = uploadFiles[i];
|
||||
for (let i = 0; i < uploadFiles.length; i++) {
|
||||
const file = uploadFiles[i];
|
||||
|
||||
const rejectedReason = shouldAcceptFile(file, (fileCount ?? 0) + i + 1);
|
||||
const rejectedReason = shouldAcceptFile(file, (fileCount ?? 0) + i + 1);
|
||||
|
||||
if (rejectedReason) {
|
||||
onFileRejected?.({ file, reason: rejectedReason });
|
||||
continue;
|
||||
}
|
||||
if (rejectedReason) {
|
||||
onFileRejected?.({ file, reason: rejectedReason });
|
||||
continue;
|
||||
}
|
||||
|
||||
validFiles.push(file);
|
||||
}
|
||||
validFiles.push(file);
|
||||
}
|
||||
|
||||
await onUpload(validFiles);
|
||||
await onUpload(validFiles);
|
||||
|
||||
uploading = false;
|
||||
};
|
||||
uploading = false;
|
||||
};
|
||||
|
||||
const canUploadFiles = $derived(
|
||||
!disabled &&
|
||||
!uploading &&
|
||||
!(
|
||||
maxFiles !== undefined &&
|
||||
fileCount !== undefined &&
|
||||
fileCount >= maxFiles
|
||||
),
|
||||
);
|
||||
const canUploadFiles = $derived(
|
||||
!disabled &&
|
||||
!uploading &&
|
||||
!(maxFiles !== undefined && fileCount !== undefined && fileCount >= maxFiles),
|
||||
);
|
||||
</script>
|
||||
|
||||
<label
|
||||
ondragover={(e) => e.preventDefault()}
|
||||
ondrop={drop}
|
||||
for={id}
|
||||
aria-disabled={!canUploadFiles}
|
||||
class={cn(
|
||||
"border-border hover:bg-accent/25 flex h-48 w-full place-items-center justify-center rounded-lg border-2 border-dashed p-6 transition-all hover:cursor-pointer aria-disabled:opacity-50 aria-disabled:hover:cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
ondragover={(e) => e.preventDefault()}
|
||||
ondrop={drop}
|
||||
for={id}
|
||||
aria-disabled={!canUploadFiles}
|
||||
class={cn(
|
||||
"border-border hover:bg-accent/25 flex h-48 w-full place-items-center justify-center rounded-lg border-2 border-dashed p-6 transition-all hover:cursor-pointer aria-disabled:opacity-50 aria-disabled:hover:cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<div class="flex flex-col place-items-center justify-center gap-2">
|
||||
<div
|
||||
class="border-border text-muted-foreground flex size-14 place-items-center justify-center rounded-full border border-dashed"
|
||||
>
|
||||
<UploadIcon class="size-7" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 text-center">
|
||||
<span class="text-muted-foreground font-medium">
|
||||
Drag 'n' drop files here, or click to select files
|
||||
</span>
|
||||
{#if maxFiles || maxFileSize}
|
||||
<span class="text-muted-foreground/75 text-sm">
|
||||
{#if maxFiles}
|
||||
<span>You can upload {maxFiles} files</span>
|
||||
{/if}
|
||||
{#if maxFiles && maxFileSize}
|
||||
<span>(up to {displaySize(maxFileSize)} each)</span>
|
||||
{/if}
|
||||
{#if maxFileSize && !maxFiles}
|
||||
<span>Maximum size {displaySize(maxFileSize)}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<input
|
||||
{...rest}
|
||||
disabled={!canUploadFiles}
|
||||
{id}
|
||||
{accept}
|
||||
multiple={maxFiles === undefined || maxFiles - (fileCount ?? 0) > 1}
|
||||
type="file"
|
||||
onchange={change}
|
||||
class="hidden"
|
||||
/>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<div class="flex flex-col place-items-center justify-center gap-2">
|
||||
<div
|
||||
class="border-border text-muted-foreground flex size-14 place-items-center justify-center rounded-full border border-dashed"
|
||||
>
|
||||
<UploadIcon class="size-7" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 text-center">
|
||||
<span class="text-muted-foreground font-medium">
|
||||
Drag 'n' drop files here, or click to select files
|
||||
</span>
|
||||
{#if maxFiles || maxFileSize}
|
||||
<span class="text-muted-foreground/75 text-sm">
|
||||
{#if maxFiles}
|
||||
<span>You can upload {maxFiles} files</span>
|
||||
{/if}
|
||||
{#if maxFiles && maxFileSize}
|
||||
<span>(up to {displaySize(maxFileSize)} each)</span>
|
||||
{/if}
|
||||
{#if maxFileSize && !maxFiles}
|
||||
<span>Maximum size {displaySize(maxFileSize)}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<input
|
||||
{...rest}
|
||||
disabled={!canUploadFiles}
|
||||
{id}
|
||||
{accept}
|
||||
multiple={maxFiles === undefined || maxFiles - (fileCount ?? 0) > 1}
|
||||
type="file"
|
||||
onchange={change}
|
||||
class="hidden"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -6,13 +6,13 @@ import FileDropZone from "./file-drop-zone.svelte";
|
||||
import { type FileRejectedReason, type FileDropZoneProps } from "./types";
|
||||
|
||||
export const displaySize = (bytes: number): string => {
|
||||
if (bytes < KILOBYTE) return `${bytes.toFixed(0)} B`;
|
||||
if (bytes < KILOBYTE) return `${bytes.toFixed(0)} B`;
|
||||
|
||||
if (bytes < MEGABYTE) return `${(bytes / KILOBYTE).toFixed(0)} KB`;
|
||||
if (bytes < MEGABYTE) return `${(bytes / KILOBYTE).toFixed(0)} KB`;
|
||||
|
||||
if (bytes < GIGABYTE) return `${(bytes / MEGABYTE).toFixed(0)} MB`;
|
||||
if (bytes < GIGABYTE) return `${(bytes / MEGABYTE).toFixed(0)} MB`;
|
||||
|
||||
return `${(bytes / GIGABYTE).toFixed(0)} GB`;
|
||||
return `${(bytes / GIGABYTE).toFixed(0)} GB`;
|
||||
};
|
||||
|
||||
// Utilities for working with file sizes
|
||||
|
||||
@@ -6,46 +6,46 @@ import type { WithChildren } from "bits-ui";
|
||||
import type { HTMLInputAttributes } from "svelte/elements";
|
||||
|
||||
export type FileRejectedReason =
|
||||
| "Maximum file size exceeded"
|
||||
| "File type not allowed"
|
||||
| "Maximum files uploaded";
|
||||
| "Maximum file size exceeded"
|
||||
| "File type not allowed"
|
||||
| "Maximum files uploaded";
|
||||
|
||||
export type FileDropZonePropsWithoutHTML = WithChildren<{
|
||||
ref?: HTMLInputElement | null;
|
||||
/** Called with the uploaded files when the user drops or clicks and selects their files.
|
||||
*
|
||||
* @param files
|
||||
*/
|
||||
onUpload: (files: File[]) => Promise<void>;
|
||||
/** The maximum amount files allowed to be uploaded */
|
||||
maxFiles?: number;
|
||||
fileCount?: number;
|
||||
/** The maximum size of a file in bytes */
|
||||
maxFileSize?: number;
|
||||
/** Called when a file does not meet the upload criteria (size, or type) */
|
||||
onFileRejected?: (opts: { reason: FileRejectedReason; file: File }) => void;
|
||||
ref?: HTMLInputElement | null;
|
||||
/** Called with the uploaded files when the user drops or clicks and selects their files.
|
||||
*
|
||||
* @param files
|
||||
*/
|
||||
onUpload: (files: File[]) => Promise<void>;
|
||||
/** The maximum amount files allowed to be uploaded */
|
||||
maxFiles?: number;
|
||||
fileCount?: number;
|
||||
/** The maximum size of a file in bytes */
|
||||
maxFileSize?: number;
|
||||
/** Called when a file does not meet the upload criteria (size, or type) */
|
||||
onFileRejected?: (opts: { reason: FileRejectedReason; file: File }) => void;
|
||||
|
||||
// just for extra documentation
|
||||
/** Takes a comma separated list of one or more file types.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept)
|
||||
*
|
||||
* ### Usage
|
||||
* ```svelte
|
||||
* <FileDropZone
|
||||
* accept=".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* ### Common Values
|
||||
* ```svelte
|
||||
* <FileDropZone accept="audio/*"/>
|
||||
* <FileDropZone accept="image/*"/>
|
||||
* <FileDropZone accept="video/*"/>
|
||||
* ```
|
||||
*/
|
||||
accept?: string;
|
||||
// just for extra documentation
|
||||
/** Takes a comma separated list of one or more file types.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept)
|
||||
*
|
||||
* ### Usage
|
||||
* ```svelte
|
||||
* <FileDropZone
|
||||
* accept=".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* ### Common Values
|
||||
* ```svelte
|
||||
* <FileDropZone accept="audio/*"/>
|
||||
* <FileDropZone accept="image/*"/>
|
||||
* <FileDropZone accept="video/*"/>
|
||||
* ```
|
||||
*/
|
||||
accept?: string;
|
||||
}>;
|
||||
|
||||
export type FileDropZoneProps = FileDropZonePropsWithoutHTML &
|
||||
Omit<HTMLInputAttributes, "multiple" | "files">;
|
||||
Omit<HTMLInputAttributes, "multiple" | "files">;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
|
||||
@@ -1,57 +1,51 @@
|
||||
<script lang="ts">
|
||||
import type {
|
||||
HTMLInputAttributes,
|
||||
HTMLInputTypeAttribute,
|
||||
} from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type"> &
|
||||
(
|
||||
| { type: "file"; files?: FileList }
|
||||
| { type?: InputType; files?: undefined }
|
||||
)
|
||||
>;
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type"> &
|
||||
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type === "file"}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
class={cn(
|
||||
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
class={cn(
|
||||
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
class={cn(
|
||||
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
class={cn(
|
||||
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Label as LabelPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: LabelPrimitive.RootProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: LabelPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="label"
|
||||
class={cn(
|
||||
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="label"
|
||||
class={cn(
|
||||
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -13,25 +13,25 @@ import GroupHeading from "./select-group-heading.svelte";
|
||||
const Root = SelectPrimitive.Root;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Label,
|
||||
Item,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
ScrollDownButton,
|
||||
ScrollUpButton,
|
||||
GroupHeading,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Label as SelectLabel,
|
||||
Item as SelectItem,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
ScrollDownButton as SelectScrollDownButton,
|
||||
ScrollUpButton as SelectScrollUpButton,
|
||||
GroupHeading as SelectGroupHeading,
|
||||
Root,
|
||||
Group,
|
||||
Label,
|
||||
Item,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
ScrollDownButton,
|
||||
ScrollUpButton,
|
||||
GroupHeading,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Label as SelectLabel,
|
||||
Item as SelectItem,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
ScrollDownButton as SelectScrollDownButton,
|
||||
ScrollUpButton as SelectScrollUpButton,
|
||||
GroupHeading as SelectGroupHeading,
|
||||
};
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||
portalProps?: SelectPrimitive.PortalProps;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||
portalProps?: SelectPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Portal {...portalProps}>
|
||||
<SelectPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
data-slot="select-content"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--bits-select-content-available-height) origin-(--bits-select-content-transform-origin) relative z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
<SelectPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
data-slot="select-content"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--bits-select-content-available-height) origin-(--bits-select-content-transform-origin) relative z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
class={cn(
|
||||
"h-(--bits-select-anchor-height) min-w-(--bits-select-anchor-width) w-full scroll-my-1 p-1",
|
||||
)}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
class={cn(
|
||||
"h-(--bits-select-anchor-height) min-w-(--bits-select-anchor-width) w-full scroll-my-1 p-1",
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="select-group-heading"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="select-group-heading"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.GroupHeading>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
// eslint-disable-next-line no-useless-assignment
|
||||
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps =
|
||||
$props();
|
||||
// eslint-disable-next-line no-useless-assignment
|
||||
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Group data-slot="select-group" {...restProps} />
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<script lang="ts">
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
label,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
label,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
bind:ref
|
||||
{value}
|
||||
data-slot="select-item"
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
{value}
|
||||
data-slot="select-item"
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ selected, highlighted })}
|
||||
<span class="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
{#if selected}
|
||||
<CheckIcon class="size-4" />
|
||||
{/if}
|
||||
</span>
|
||||
{#if childrenProp}
|
||||
{@render childrenProp({ selected, highlighted })}
|
||||
{:else}
|
||||
{label || value}
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet children({ selected, highlighted })}
|
||||
<span class="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
{#if selected}
|
||||
<CheckIcon class="size-4" />
|
||||
{/if}
|
||||
</span>
|
||||
{#if childrenProp}
|
||||
{@render childrenProp({ selected, highlighted })}
|
||||
{:else}
|
||||
{label || value}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SelectPrimitive.Item>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="select-label"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="select-label"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-down-button"
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="select-scroll-down-button"
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronDownIcon class="size-4" />
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-up-button"
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="select-scroll-up-button"
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronUpIcon class="size-4" />
|
||||
<ChevronUpIcon class="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-slot="select-separator"
|
||||
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="select-separator"
|
||||
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = "default",
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||
size?: "sm" | "default";
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = "default",
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||
size?: "sm" | "default";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 shadow-xs flex w-fit select-none items-center justify-between gap-2 whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 shadow-xs flex w-fit select-none items-center justify-between gap-2 whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronDownIcon class="size-4 opacity-50" />
|
||||
{@render children?.()}
|
||||
<ChevronDownIcon class="size-4 opacity-50" />
|
||||
</SelectPrimitive.Trigger>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="separator"
|
||||
class={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="separator"
|
||||
class={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./slider.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Slider,
|
||||
Root,
|
||||
//
|
||||
Root as Slider,
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Slider as SliderPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import { Slider as SliderPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
orientation = "horizontal",
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SliderPrimitive.RootProps> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
orientation = "horizontal",
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SliderPrimitive.RootProps> = $props();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
@@ -16,37 +16,37 @@ Discriminated Unions + Destructing (required for bindable) do not
|
||||
get along, so we shut typescript up by casting `value` to `never`.
|
||||
-->
|
||||
<SliderPrimitive.Root
|
||||
bind:ref
|
||||
bind:value={value as never}
|
||||
data-slot="slider"
|
||||
{orientation}
|
||||
class={cn(
|
||||
"relative flex w-full touch-none select-none items-center data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
bind:value={value as never}
|
||||
data-slot="slider"
|
||||
{orientation}
|
||||
class={cn(
|
||||
"relative flex w-full touch-none select-none items-center data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ thumbs })}
|
||||
<span
|
||||
data-orientation={orientation}
|
||||
data-slot="slider-track"
|
||||
class={cn(
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-1.5",
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
class={cn(
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
{#each thumbs as thumb (thumb)}
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
index={thumb}
|
||||
class="border-primary bg-background ring-ring/50 focus-visible:outline-hidden block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
{/each}
|
||||
{/snippet}
|
||||
{#snippet children({ thumbs })}
|
||||
<span
|
||||
data-orientation={orientation}
|
||||
data-slot="slider-track"
|
||||
class={cn(
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-1.5",
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
class={cn(
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
{#each thumbs as thumb (thumb)}
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
index={thumb}
|
||||
class="border-primary bg-background ring-ring/50 focus-visible:outline-hidden block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
{/each}
|
||||
{/snippet}
|
||||
</SliderPrimitive.Root>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Toaster as Sonner,
|
||||
type ToasterProps as SonnerProps,
|
||||
} from "svelte-sonner";
|
||||
import { mode } from "mode-watcher";
|
||||
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
let { ...restProps }: SonnerProps = $props();
|
||||
let { ...restProps }: SonnerProps = $props();
|
||||
</script>
|
||||
|
||||
<Sonner
|
||||
theme={mode.current}
|
||||
class="toaster group"
|
||||
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
|
||||
{...restProps}
|
||||
theme={mode.current}
|
||||
class="toaster group"
|
||||
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -4,13 +4,13 @@ import List from "./tabs-list.svelte";
|
||||
import Trigger from "./tabs-trigger.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
List,
|
||||
Trigger,
|
||||
//
|
||||
Root as Tabs,
|
||||
Content as TabsContent,
|
||||
List as TabsList,
|
||||
Trigger as TabsTrigger,
|
||||
Root,
|
||||
Content,
|
||||
List,
|
||||
Trigger,
|
||||
//
|
||||
Root as Tabs,
|
||||
Content as TabsContent,
|
||||
List as TabsList,
|
||||
Trigger as TabsTrigger,
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.ContentProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.ContentProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="tabs-content"
|
||||
class={cn("flex-1 outline-none", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="tabs-content"
|
||||
class={cn("flex-1 outline-none", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.ListProps = $props();
|
||||
let { ref = $bindable(null), class: className, ...restProps }: TabsPrimitive.ListProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.List
|
||||
bind:ref
|
||||
data-slot="tabs-list"
|
||||
class={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="tabs-list"
|
||||
class={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.TriggerProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="tabs-trigger"
|
||||
class={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1 text-sm font-medium transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="tabs-trigger"
|
||||
class={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1 text-sm font-medium transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.RootProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="tabs"
|
||||
class={cn("flex flex-col gap-2", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="tabs"
|
||||
class={cn("flex flex-col gap-2", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -3,26 +3,26 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
disabled: boolean | null;
|
||||
active: boolean;
|
||||
onDelete: (value: string) => void;
|
||||
};
|
||||
type Props = {
|
||||
value: string;
|
||||
disabled: boolean | null;
|
||||
active: boolean;
|
||||
onDelete: (value: string) => void;
|
||||
};
|
||||
|
||||
let { value, disabled, onDelete, active }: Props = $props();
|
||||
let { value, disabled, onDelete, active }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="bg-secondary ring-offset-background hover:bg-secondary/90 aria-selected:bg-secondary/90 aria-selected:ring-ring flex place-items-center gap-2 rounded-md px-2 py-0.5 transition-all hover:cursor-default aria-selected:ring-2 aria-selected:ring-offset-2"
|
||||
aria-selected={active}
|
||||
class="bg-secondary ring-offset-background hover:bg-secondary/90 aria-selected:bg-secondary/90 aria-selected:ring-ring flex place-items-center gap-2 rounded-md px-2 py-0.5 transition-all hover:cursor-default aria-selected:ring-2 aria-selected:ring-offset-2"
|
||||
aria-selected={active}
|
||||
>
|
||||
<span>
|
||||
{value}
|
||||
</span>
|
||||
<button type="button" {disabled} onclick={() => onDelete(value)}>
|
||||
<XIcon class="size-4" />
|
||||
</button>
|
||||
<span>
|
||||
{value}
|
||||
</span>
|
||||
<button type="button" {disabled} onclick={() => onDelete(value)}>
|
||||
<XIcon class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -3,209 +3,208 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/utils";
|
||||
import type { TagsInputProps } from "./types";
|
||||
import TagsInputTag from "./tags-input-tag.svelte";
|
||||
import { untrack } from "svelte";
|
||||
import { cn } from "$lib/utils/utils";
|
||||
import type { TagsInputProps } from "./types";
|
||||
import TagsInputTag from "./tags-input-tag.svelte";
|
||||
import { untrack } from "svelte";
|
||||
|
||||
const defaultValidate: TagsInputProps["validate"] = (val, tags) => {
|
||||
const transformed = val.trim();
|
||||
const defaultValidate: TagsInputProps["validate"] = (val, tags) => {
|
||||
const transformed = val.trim();
|
||||
|
||||
// disallow empties
|
||||
if (transformed.length === 0) return undefined;
|
||||
// disallow empties
|
||||
if (transformed.length === 0) return undefined;
|
||||
|
||||
// disallow duplicates
|
||||
if (tags.find((t) => transformed === t)) return undefined;
|
||||
// disallow duplicates
|
||||
if (tags.find((t) => transformed === t)) return undefined;
|
||||
|
||||
return transformed;
|
||||
};
|
||||
return transformed;
|
||||
};
|
||||
|
||||
let {
|
||||
value = $bindable([]),
|
||||
placeholder,
|
||||
class: className,
|
||||
disabled = false,
|
||||
validate = defaultValidate,
|
||||
...rest
|
||||
}: TagsInputProps = $props();
|
||||
let {
|
||||
value = $bindable([]),
|
||||
placeholder,
|
||||
class: className,
|
||||
disabled = false,
|
||||
validate = defaultValidate,
|
||||
...rest
|
||||
}: TagsInputProps = $props();
|
||||
|
||||
let inputValue = $state("");
|
||||
let tagIndex = $state<number>();
|
||||
let invalid = $state(false);
|
||||
let isComposing = $state(false);
|
||||
let inputValue = $state("");
|
||||
let tagIndex = $state<number>();
|
||||
let invalid = $state(false);
|
||||
let isComposing = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
// whenever input value changes reset invalid
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
inputValue;
|
||||
$effect(() => {
|
||||
// whenever input value changes reset invalid
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
inputValue;
|
||||
|
||||
untrack(() => {
|
||||
invalid = false;
|
||||
});
|
||||
});
|
||||
untrack(() => {
|
||||
invalid = false;
|
||||
});
|
||||
});
|
||||
|
||||
const enter = () => {
|
||||
if (isComposing) return;
|
||||
const enter = () => {
|
||||
if (isComposing) return;
|
||||
|
||||
const validated = validate(inputValue, value);
|
||||
const validated = validate(inputValue, value);
|
||||
|
||||
if (!validated) {
|
||||
invalid = true;
|
||||
return;
|
||||
}
|
||||
if (!validated) {
|
||||
invalid = true;
|
||||
return;
|
||||
}
|
||||
|
||||
value = [...value, validated];
|
||||
inputValue = "";
|
||||
};
|
||||
value = [...value, validated];
|
||||
inputValue = "";
|
||||
};
|
||||
|
||||
const compositionStart = () => {
|
||||
isComposing = true;
|
||||
};
|
||||
const compositionStart = () => {
|
||||
isComposing = true;
|
||||
};
|
||||
|
||||
const compositionEnd = () => {
|
||||
isComposing = false;
|
||||
};
|
||||
const compositionEnd = () => {
|
||||
isComposing = false;
|
||||
};
|
||||
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
if (e.key === "Enter") {
|
||||
// prevent form submit
|
||||
e.preventDefault();
|
||||
if (e.key === "Enter") {
|
||||
// prevent form submit
|
||||
e.preventDefault();
|
||||
|
||||
if (isComposing) return;
|
||||
if (isComposing) return;
|
||||
|
||||
enter();
|
||||
return;
|
||||
}
|
||||
enter();
|
||||
return;
|
||||
}
|
||||
|
||||
const isAtBeginning =
|
||||
target.selectionStart === 0 && target.selectionEnd === 0;
|
||||
const isAtBeginning = target.selectionStart === 0 && target.selectionEnd === 0;
|
||||
|
||||
let shouldResetIndex = true;
|
||||
let shouldResetIndex = true;
|
||||
|
||||
if (e.key === "Backspace") {
|
||||
if (isAtBeginning) {
|
||||
e.preventDefault();
|
||||
if (e.key === "Backspace") {
|
||||
if (isAtBeginning) {
|
||||
e.preventDefault();
|
||||
|
||||
if (tagIndex !== undefined) {
|
||||
deleteIndex(tagIndex);
|
||||
if (tagIndex !== undefined) {
|
||||
deleteIndex(tagIndex);
|
||||
|
||||
// focus previous
|
||||
const prev = tagIndex - 1;
|
||||
// focus previous
|
||||
const prev = tagIndex - 1;
|
||||
|
||||
if (prev < 0) {
|
||||
tagIndex = undefined;
|
||||
} else {
|
||||
tagIndex = prev;
|
||||
}
|
||||
} else {
|
||||
tagIndex = value.length - 1;
|
||||
}
|
||||
if (prev < 0) {
|
||||
tagIndex = undefined;
|
||||
} else {
|
||||
tagIndex = prev;
|
||||
}
|
||||
} else {
|
||||
tagIndex = value.length - 1;
|
||||
}
|
||||
|
||||
shouldResetIndex = false;
|
||||
}
|
||||
}
|
||||
shouldResetIndex = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "Delete") {
|
||||
if (isAtBeginning) {
|
||||
if (inputValue.length === 0) {
|
||||
if (tagIndex !== undefined) {
|
||||
e.preventDefault();
|
||||
if (e.key === "Delete") {
|
||||
if (isAtBeginning) {
|
||||
if (inputValue.length === 0) {
|
||||
if (tagIndex !== undefined) {
|
||||
e.preventDefault();
|
||||
|
||||
deleteIndex(tagIndex);
|
||||
deleteIndex(tagIndex);
|
||||
|
||||
// stay focused on the same index unless value.length === 0
|
||||
if (value.length === 0) tagIndex = undefined;
|
||||
// stay focused on the same index unless value.length === 0
|
||||
if (value.length === 0) tagIndex = undefined;
|
||||
|
||||
shouldResetIndex = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shouldResetIndex = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// controls for tag selection
|
||||
if (isAtBeginning) {
|
||||
// left
|
||||
if (e.key === "ArrowLeft") {
|
||||
if (tagIndex !== undefined) {
|
||||
const prev = tagIndex - 1;
|
||||
// controls for tag selection
|
||||
if (isAtBeginning) {
|
||||
// left
|
||||
if (e.key === "ArrowLeft") {
|
||||
if (tagIndex !== undefined) {
|
||||
const prev = tagIndex - 1;
|
||||
|
||||
if (prev < 0) {
|
||||
tagIndex = 0;
|
||||
} else {
|
||||
tagIndex = prev;
|
||||
}
|
||||
} else {
|
||||
// set initial index
|
||||
tagIndex = value.length - 1;
|
||||
}
|
||||
if (prev < 0) {
|
||||
tagIndex = 0;
|
||||
} else {
|
||||
tagIndex = prev;
|
||||
}
|
||||
} else {
|
||||
// set initial index
|
||||
tagIndex = value.length - 1;
|
||||
}
|
||||
|
||||
shouldResetIndex = false;
|
||||
}
|
||||
shouldResetIndex = false;
|
||||
}
|
||||
|
||||
// right
|
||||
// we can only move right if the value is empty
|
||||
if (inputValue.length === 0) {
|
||||
if (e.key === "ArrowRight") {
|
||||
if (tagIndex !== undefined) {
|
||||
const next = tagIndex + 1;
|
||||
// right
|
||||
// we can only move right if the value is empty
|
||||
if (inputValue.length === 0) {
|
||||
if (e.key === "ArrowRight") {
|
||||
if (tagIndex !== undefined) {
|
||||
const next = tagIndex + 1;
|
||||
|
||||
if (next > value.length - 1) {
|
||||
tagIndex = undefined;
|
||||
} else {
|
||||
tagIndex = next;
|
||||
}
|
||||
if (next > value.length - 1) {
|
||||
tagIndex = undefined;
|
||||
} else {
|
||||
tagIndex = next;
|
||||
}
|
||||
|
||||
shouldResetIndex = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shouldResetIndex = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reset the tag index to undefined
|
||||
if (shouldResetIndex) {
|
||||
tagIndex = undefined;
|
||||
}
|
||||
};
|
||||
// reset the tag index to undefined
|
||||
if (shouldResetIndex) {
|
||||
tagIndex = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteValue = (val: string) => {
|
||||
const index = value.findIndex((v) => val === v);
|
||||
const deleteValue = (val: string) => {
|
||||
const index = value.findIndex((v) => val === v);
|
||||
|
||||
if (index === -1) return;
|
||||
if (index === -1) return;
|
||||
|
||||
deleteIndex(index);
|
||||
};
|
||||
deleteIndex(index);
|
||||
};
|
||||
|
||||
const deleteIndex = (index: number) => {
|
||||
value = [...value.slice(0, index), ...value.slice(index + 1)];
|
||||
};
|
||||
const deleteIndex = (index: number) => {
|
||||
value = [...value.slice(0, index), ...value.slice(index + 1)];
|
||||
};
|
||||
|
||||
const blur = () => {
|
||||
tagIndex = undefined;
|
||||
};
|
||||
const blur = () => {
|
||||
tagIndex = undefined;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
"border-input bg-background selection:bg-primary dark:bg-input/30 flex min-h-[36px] w-full flex-wrap place-items-center gap-1 rounded-md border py-0.5 pr-1 pl-1 disabled:opacity-50 aria-disabled:cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
aria-disabled={disabled}
|
||||
class={cn(
|
||||
"border-input bg-background selection:bg-primary dark:bg-input/30 flex min-h-[36px] w-full flex-wrap place-items-center gap-1 rounded-md border py-0.5 pr-1 pl-1 disabled:opacity-50 aria-disabled:cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
aria-disabled={disabled}
|
||||
>
|
||||
{#each value as tag, i (tag)}
|
||||
<TagsInputTag value={tag} {disabled} onDelete={deleteValue} active={i === tagIndex} />
|
||||
{/each}
|
||||
<input
|
||||
{...rest}
|
||||
bind:value={inputValue}
|
||||
onblur={blur}
|
||||
oncompositionstart={compositionStart}
|
||||
oncompositionend={compositionEnd}
|
||||
{disabled}
|
||||
{placeholder}
|
||||
data-invalid={invalid}
|
||||
onkeydown={keydown}
|
||||
class="placeholder:text-muted-foreground min-w-16 shrink grow basis-0 border-none bg-transparent px-2 outline-hidden focus:outline-hidden disabled:cursor-not-allowed data-[invalid=true]:text-red-500 md:text-sm"
|
||||
/>
|
||||
{#each value as tag, i (tag)}
|
||||
<TagsInputTag value={tag} {disabled} onDelete={deleteValue} active={i === tagIndex} />
|
||||
{/each}
|
||||
<input
|
||||
{...rest}
|
||||
bind:value={inputValue}
|
||||
onblur={blur}
|
||||
oncompositionstart={compositionStart}
|
||||
oncompositionend={compositionEnd}
|
||||
{disabled}
|
||||
{placeholder}
|
||||
data-invalid={invalid}
|
||||
onkeydown={keydown}
|
||||
class="placeholder:text-muted-foreground min-w-16 shrink grow basis-0 border-none bg-transparent px-2 outline-hidden focus:outline-hidden disabled:cursor-not-allowed data-[invalid=true]:text-red-500 md:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
import type { HTMLInputAttributes } from "svelte/elements";
|
||||
|
||||
export type TagsInputPropsWithoutHTML = {
|
||||
value?: string[];
|
||||
validate?: (val: string, tags: string[]) => string | undefined;
|
||||
value?: string[];
|
||||
validate?: (val: string, tags: string[]) => string | undefined;
|
||||
};
|
||||
|
||||
export type TagsInputProps = TagsInputPropsWithoutHTML &
|
||||
Omit<HTMLInputAttributes, "value">;
|
||||
export type TagsInputProps = TagsInputPropsWithoutHTML & Omit<HTMLInputAttributes, "value">;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./textarea.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Textarea,
|
||||
Root,
|
||||
//
|
||||
Root as Textarea,
|
||||
};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||
import type { HTMLTextareaAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||
import type { HTMLTextareaAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
bind:this={ref}
|
||||
data-slot="textarea"
|
||||
class={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 field-sizing-content shadow-xs flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
bind:value
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="textarea"
|
||||
class={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 field-sizing-content shadow-xs flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
bind:value
|
||||
{...restProps}
|
||||
></textarea>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// Re-export from api.ts for backwards compatibility
|
||||
// All components that import from $lib/directus continue to work
|
||||
export { apiUrl as directusApiUrl, getAssetUrl, isModel, getGraphQLClient as getDirectusInstance } from "./api.js";
|
||||
export {
|
||||
apiUrl as directusApiUrl,
|
||||
getAssetUrl,
|
||||
isModel,
|
||||
getGraphQLClient as getDirectusInstance,
|
||||
} from "./api.js";
|
||||
|
||||
@@ -6,6 +6,6 @@ const defaultLocale = "en";
|
||||
addMessages("en", en);
|
||||
|
||||
init({
|
||||
fallbackLocale: defaultLocale,
|
||||
initialLocale: defaultLocale,
|
||||
fallbackLocale: defaultLocale,
|
||||
initialLocale: defaultLocale,
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,140 +3,137 @@
|
||||
* Provides structured logging with context and request tracing
|
||||
*/
|
||||
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
interface LogContext {
|
||||
timestamp: string;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
context?: Record<string, unknown>;
|
||||
requestId?: string;
|
||||
userId?: string;
|
||||
path?: string;
|
||||
method?: string;
|
||||
duration?: number;
|
||||
error?: Error;
|
||||
timestamp: string;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
context?: Record<string, unknown>;
|
||||
requestId?: string;
|
||||
userId?: string;
|
||||
path?: string;
|
||||
method?: string;
|
||||
duration?: number;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
class Logger {
|
||||
private isDev = process.env.NODE_ENV === 'development';
|
||||
private serviceName = 'sexy.pivoine.art';
|
||||
private isDev = process.env.NODE_ENV === "development";
|
||||
private serviceName = "sexy.pivoine.art";
|
||||
|
||||
private formatLog(ctx: LogContext): string {
|
||||
const { timestamp, level, message, context, requestId, userId, path, method, duration, error } = ctx;
|
||||
private formatLog(ctx: LogContext): string {
|
||||
const { timestamp, level, message, context, requestId, userId, path, method, duration, error } =
|
||||
ctx;
|
||||
|
||||
const parts = [
|
||||
`[${timestamp}]`,
|
||||
`[${level.toUpperCase()}]`,
|
||||
requestId ? `[${requestId}]` : null,
|
||||
method && path ? `${method} ${path}` : null,
|
||||
message,
|
||||
userId ? `user=${userId}` : null,
|
||||
duration !== undefined ? `${duration}ms` : null,
|
||||
].filter(Boolean);
|
||||
const parts = [
|
||||
`[${timestamp}]`,
|
||||
`[${level.toUpperCase()}]`,
|
||||
requestId ? `[${requestId}]` : null,
|
||||
method && path ? `${method} ${path}` : null,
|
||||
message,
|
||||
userId ? `user=${userId}` : null,
|
||||
duration !== undefined ? `${duration}ms` : null,
|
||||
].filter(Boolean);
|
||||
|
||||
let logString = parts.join(' ');
|
||||
let logString = parts.join(" ");
|
||||
|
||||
if (context && Object.keys(context).length > 0) {
|
||||
logString += ' ' + JSON.stringify(context);
|
||||
}
|
||||
if (context && Object.keys(context).length > 0) {
|
||||
logString += " " + JSON.stringify(context);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
logString += `\n Error: ${error.message}\n Stack: ${error.stack}`;
|
||||
}
|
||||
if (error) {
|
||||
logString += `\n Error: ${error.message}\n Stack: ${error.stack}`;
|
||||
}
|
||||
|
||||
return logString;
|
||||
}
|
||||
return logString;
|
||||
}
|
||||
|
||||
private log(level: LogLevel, message: string, meta: Partial<LogContext> = {}) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logContext: LogContext = {
|
||||
timestamp,
|
||||
level,
|
||||
message,
|
||||
...meta,
|
||||
};
|
||||
private log(level: LogLevel, message: string, meta: Partial<LogContext> = {}) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logContext: LogContext = {
|
||||
timestamp,
|
||||
level,
|
||||
message,
|
||||
...meta,
|
||||
};
|
||||
|
||||
const formattedLog = this.formatLog(logContext);
|
||||
const formattedLog = this.formatLog(logContext);
|
||||
|
||||
switch (level) {
|
||||
case 'debug':
|
||||
if (this.isDev) console.debug(formattedLog);
|
||||
break;
|
||||
case 'info':
|
||||
console.info(formattedLog);
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(formattedLog);
|
||||
break;
|
||||
case 'error':
|
||||
console.error(formattedLog);
|
||||
break;
|
||||
}
|
||||
}
|
||||
switch (level) {
|
||||
case "debug":
|
||||
if (this.isDev) console.debug(formattedLog);
|
||||
break;
|
||||
case "info":
|
||||
console.info(formattedLog);
|
||||
break;
|
||||
case "warn":
|
||||
console.warn(formattedLog);
|
||||
break;
|
||||
case "error":
|
||||
console.error(formattedLog);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, meta?: Partial<LogContext>) {
|
||||
this.log('debug', message, meta);
|
||||
}
|
||||
debug(message: string, meta?: Partial<LogContext>) {
|
||||
this.log("debug", message, meta);
|
||||
}
|
||||
|
||||
info(message: string, meta?: Partial<LogContext>) {
|
||||
this.log('info', message, meta);
|
||||
}
|
||||
info(message: string, meta?: Partial<LogContext>) {
|
||||
this.log("info", message, meta);
|
||||
}
|
||||
|
||||
warn(message: string, meta?: Partial<LogContext>) {
|
||||
this.log('warn', message, meta);
|
||||
}
|
||||
warn(message: string, meta?: Partial<LogContext>) {
|
||||
this.log("warn", message, meta);
|
||||
}
|
||||
|
||||
error(message: string, meta?: Partial<LogContext>) {
|
||||
this.log('error', message, meta);
|
||||
}
|
||||
error(message: string, meta?: Partial<LogContext>) {
|
||||
this.log("error", message, meta);
|
||||
}
|
||||
|
||||
// Request logging helper
|
||||
request(
|
||||
method: string,
|
||||
path: string,
|
||||
meta: Partial<LogContext> = {}
|
||||
) {
|
||||
this.info('→ Request received', { method, path, ...meta });
|
||||
}
|
||||
// Request logging helper
|
||||
request(method: string, path: string, meta: Partial<LogContext> = {}) {
|
||||
this.info("→ Request received", { method, path, ...meta });
|
||||
}
|
||||
|
||||
response(
|
||||
method: string,
|
||||
path: string,
|
||||
status: number,
|
||||
duration: number,
|
||||
meta: Partial<LogContext> = {}
|
||||
) {
|
||||
const level = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';
|
||||
this.log(level, `← Response ${status}`, { method, path, duration, ...meta });
|
||||
}
|
||||
response(
|
||||
method: string,
|
||||
path: string,
|
||||
status: number,
|
||||
duration: number,
|
||||
meta: Partial<LogContext> = {},
|
||||
) {
|
||||
const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
|
||||
this.log(level, `← Response ${status}`, { method, path, duration, ...meta });
|
||||
}
|
||||
|
||||
// Authentication logging
|
||||
auth(action: string, success: boolean, meta: Partial<LogContext> = {}) {
|
||||
this.info(`🔐 Auth: ${action} ${success ? 'success' : 'failed'}`, meta);
|
||||
}
|
||||
// Authentication logging
|
||||
auth(action: string, success: boolean, meta: Partial<LogContext> = {}) {
|
||||
this.info(`🔐 Auth: ${action} ${success ? "success" : "failed"}`, meta);
|
||||
}
|
||||
|
||||
// Startup logging
|
||||
startup() {
|
||||
const env = {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
PUBLIC_API_URL: process.env.PUBLIC_API_URL,
|
||||
PUBLIC_URL: process.env.PUBLIC_URL,
|
||||
PUBLIC_UMAMI_ID: process.env.PUBLIC_UMAMI_ID ? '***set***' : 'not set',
|
||||
PUBLIC_UMAMI_SCRIPT: process.env.PUBLIC_UMAMI_SCRIPT || 'not set',
|
||||
PORT: process.env.PORT || '3000',
|
||||
HOST: process.env.HOST || '0.0.0.0',
|
||||
};
|
||||
// Startup logging
|
||||
startup() {
|
||||
const env = {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
PUBLIC_API_URL: process.env.PUBLIC_API_URL,
|
||||
PUBLIC_URL: process.env.PUBLIC_URL,
|
||||
PUBLIC_UMAMI_ID: process.env.PUBLIC_UMAMI_ID ? "***set***" : "not set",
|
||||
PUBLIC_UMAMI_SCRIPT: process.env.PUBLIC_UMAMI_SCRIPT || "not set",
|
||||
PORT: process.env.PORT || "3000",
|
||||
HOST: process.env.HOST || "0.0.0.0",
|
||||
};
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('🍑 sexy.pivoine.art - Server Starting 💜');
|
||||
console.log('='.repeat(60));
|
||||
console.log('\n📋 Environment Configuration:');
|
||||
Object.entries(env).forEach(([key, value]) => {
|
||||
console.log(` ${key}: ${value}`);
|
||||
});
|
||||
console.log('\n' + '='.repeat(60) + '\n');
|
||||
}
|
||||
console.log("\n" + "=".repeat(60));
|
||||
console.log("🍑 sexy.pivoine.art - Server Starting 💜");
|
||||
console.log("=".repeat(60));
|
||||
console.log("\n📋 Environment Configuration:");
|
||||
Object.entries(env).forEach(([key, value]) => {
|
||||
console.log(` ${key}: ${value}`);
|
||||
});
|
||||
console.log("\n" + "=".repeat(60) + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
@@ -144,5 +141,5 @@ export const logger = new Logger();
|
||||
|
||||
// Generate request ID
|
||||
export function generateRequestId(): string {
|
||||
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,205 +1,205 @@
|
||||
import { type ButtplugClientDevice } from "@sexy.pivoine.art/buttplug";
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
artist_name: string;
|
||||
slug: string;
|
||||
email: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
avatar: string | File;
|
||||
password: string;
|
||||
directus_users_id?: User;
|
||||
id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
artist_name: string;
|
||||
slug: string;
|
||||
email: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
avatar: string | File;
|
||||
password: string;
|
||||
directus_users_id?: User;
|
||||
}
|
||||
|
||||
export interface CurrentUser extends User {
|
||||
avatar: File;
|
||||
role: "model" | "viewer" | "admin";
|
||||
policies: string[];
|
||||
avatar: File;
|
||||
role: "model" | "viewer" | "admin";
|
||||
policies: string[];
|
||||
}
|
||||
|
||||
export interface AuthStatus {
|
||||
authenticated: boolean;
|
||||
user?: CurrentUser;
|
||||
data?: {
|
||||
refresh_token: string | null;
|
||||
};
|
||||
authenticated: boolean;
|
||||
user?: CurrentUser;
|
||||
data?: {
|
||||
refresh_token: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface File {
|
||||
id: string;
|
||||
filesize: number;
|
||||
title: string;
|
||||
description: string;
|
||||
duration: number;
|
||||
directus_files_id?: File;
|
||||
id: string;
|
||||
filesize: number;
|
||||
title: string;
|
||||
description: string;
|
||||
duration: number;
|
||||
directus_files_id?: File;
|
||||
}
|
||||
|
||||
export interface Article {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
content: string;
|
||||
image: string;
|
||||
tags: string[];
|
||||
publish_date: Date;
|
||||
author: {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
avatar: string;
|
||||
description?: string;
|
||||
website?: string;
|
||||
};
|
||||
category: string;
|
||||
featured?: boolean;
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
content: string;
|
||||
image: string;
|
||||
tags: string[];
|
||||
publish_date: Date;
|
||||
author: {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
avatar: string;
|
||||
description?: string;
|
||||
website?: string;
|
||||
};
|
||||
category: string;
|
||||
featured?: boolean;
|
||||
}
|
||||
|
||||
export interface Model {
|
||||
id: string;
|
||||
slug: string;
|
||||
artist_name: string;
|
||||
description: string;
|
||||
avatar: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
join_date: Date;
|
||||
featured?: boolean;
|
||||
photos: File[];
|
||||
banner?: File;
|
||||
id: string;
|
||||
slug: string;
|
||||
artist_name: string;
|
||||
description: string;
|
||||
avatar: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
join_date: Date;
|
||||
featured?: boolean;
|
||||
photos: File[];
|
||||
banner?: File;
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
movie: File;
|
||||
models: User[];
|
||||
tags: string[];
|
||||
upload_date: Date;
|
||||
premium?: boolean;
|
||||
featured?: boolean;
|
||||
likes_count?: number;
|
||||
plays_count?: number;
|
||||
views_count?: number;
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
movie: File;
|
||||
models: User[];
|
||||
tags: string[];
|
||||
upload_date: Date;
|
||||
premium?: boolean;
|
||||
featured?: boolean;
|
||||
likes_count?: number;
|
||||
plays_count?: number;
|
||||
views_count?: number;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
id: string;
|
||||
comment: string;
|
||||
item: string;
|
||||
user_created: User;
|
||||
date_created: Date;
|
||||
id: string;
|
||||
comment: string;
|
||||
item: string;
|
||||
user_created: User;
|
||||
date_created: Date;
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
videos_count: number;
|
||||
models_count: number;
|
||||
viewers_count: number;
|
||||
videos_count: number;
|
||||
models_count: number;
|
||||
viewers_count: number;
|
||||
}
|
||||
|
||||
export interface DeviceActuator {
|
||||
featureIndex: number;
|
||||
outputType: string;
|
||||
maxSteps: number;
|
||||
descriptor: string;
|
||||
value: number;
|
||||
featureIndex: number;
|
||||
outputType: string;
|
||||
maxSteps: number;
|
||||
descriptor: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface BluetoothDevice {
|
||||
id: string;
|
||||
name: string;
|
||||
actuators: DeviceActuator[];
|
||||
batteryLevel: number;
|
||||
hasBattery: boolean;
|
||||
isConnected: boolean;
|
||||
lastSeen: Date;
|
||||
info: ButtplugClientDevice;
|
||||
id: string;
|
||||
name: string;
|
||||
actuators: DeviceActuator[];
|
||||
batteryLevel: number;
|
||||
hasBattery: boolean;
|
||||
isConnected: boolean;
|
||||
lastSeen: Date;
|
||||
info: ButtplugClientDevice;
|
||||
}
|
||||
|
||||
export interface ShareContent {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
type: "video" | "model" | "article" | "link";
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
type: "video" | "model" | "article" | "link";
|
||||
}
|
||||
|
||||
export interface RecordedEvent {
|
||||
timestamp: number;
|
||||
deviceIndex: number;
|
||||
deviceName: string;
|
||||
actuatorIndex: number;
|
||||
actuatorType: string;
|
||||
value: number;
|
||||
timestamp: number;
|
||||
deviceIndex: number;
|
||||
deviceName: string;
|
||||
actuatorIndex: number;
|
||||
actuatorType: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface DeviceInfo {
|
||||
name: string;
|
||||
index: number;
|
||||
capabilities: string[];
|
||||
name: string;
|
||||
index: number;
|
||||
capabilities: string[];
|
||||
}
|
||||
|
||||
export interface Recording {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
slug: string;
|
||||
duration: number;
|
||||
events: RecordedEvent[];
|
||||
device_info: DeviceInfo[];
|
||||
user_created: string | User;
|
||||
date_created: Date;
|
||||
date_updated?: Date;
|
||||
status: "draft" | "published" | "archived";
|
||||
tags?: string[];
|
||||
linked_video?: string | Video;
|
||||
featured?: boolean;
|
||||
public?: boolean;
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
slug: string;
|
||||
duration: number;
|
||||
events: RecordedEvent[];
|
||||
device_info: DeviceInfo[];
|
||||
user_created: string | User;
|
||||
date_created: Date;
|
||||
date_updated?: Date;
|
||||
status: "draft" | "published" | "archived";
|
||||
tags?: string[];
|
||||
linked_video?: string | Video;
|
||||
featured?: boolean;
|
||||
public?: boolean;
|
||||
}
|
||||
|
||||
export interface VideoLikeStatus {
|
||||
liked: boolean;
|
||||
liked: boolean;
|
||||
}
|
||||
|
||||
export interface VideoPlayRecord {
|
||||
id: string;
|
||||
video_id: string;
|
||||
duration_watched?: number;
|
||||
completed: boolean;
|
||||
id: string;
|
||||
video_id: string;
|
||||
duration_watched?: number;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export interface VideoLikeResponse {
|
||||
liked: boolean;
|
||||
likes_count: number;
|
||||
liked: boolean;
|
||||
likes_count: number;
|
||||
}
|
||||
|
||||
export interface VideoPlayResponse {
|
||||
success: boolean;
|
||||
play_id: string;
|
||||
plays_count: number;
|
||||
success: boolean;
|
||||
play_id: string;
|
||||
plays_count: number;
|
||||
}
|
||||
|
||||
export interface VideoAnalytics {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
upload_date: Date;
|
||||
likes: number;
|
||||
plays: number;
|
||||
completed_plays: number;
|
||||
completion_rate: number;
|
||||
avg_watch_time: number;
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
upload_date: Date;
|
||||
likes: number;
|
||||
plays: number;
|
||||
completed_plays: number;
|
||||
completion_rate: number;
|
||||
avg_watch_time: number;
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
total_videos: number;
|
||||
total_likes: number;
|
||||
total_plays: number;
|
||||
plays_by_date: Record<string, number>;
|
||||
likes_by_date: Record<string, number>;
|
||||
videos: VideoAnalytics[];
|
||||
total_videos: number;
|
||||
total_likes: number;
|
||||
total_plays: number;
|
||||
plays_by_date: Record<string, number>;
|
||||
likes_by_date: Record<string, number>;
|
||||
videos: VideoAnalytics[];
|
||||
}
|
||||
|
||||
@@ -2,43 +2,41 @@ import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any }
|
||||
? Omit<T, "children">
|
||||
: T;
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & {
|
||||
ref?: U | null;
|
||||
ref?: U | null;
|
||||
};
|
||||
|
||||
export const calcReadingTime = (text: string) => {
|
||||
const wordsPerMinute = 200; // Average case.
|
||||
const textLength = text.split(" ").length; // Split by words
|
||||
if (textLength > 0) {
|
||||
return Math.ceil(textLength / wordsPerMinute);
|
||||
}
|
||||
return 0;
|
||||
const wordsPerMinute = 200; // Average case.
|
||||
const textLength = text.split(" ").length; // Split by words
|
||||
if (textLength > 0) {
|
||||
return Math.ceil(textLength / wordsPerMinute);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const getUserInitials = (name: string) => {
|
||||
if (!name) return "??";
|
||||
return name
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0))
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
if (!name) return "??";
|
||||
return name
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0))
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
|
||||
export const formatVideoDuration = (duration: number) => {
|
||||
const hours = Math.floor(duration / 3600);
|
||||
const minutes = Math.floor((duration - hours * 3600) / 60);
|
||||
const seconds = duration - hours * 3600 - minutes * 60;
|
||||
const hours = Math.floor(duration / 3600);
|
||||
const minutes = Math.floor((duration - hours * 3600) / 60);
|
||||
const seconds = duration - hours * 3600 - minutes * 60;
|
||||
|
||||
return `${hours < 10 ? "0" + hours : hours}:${minutes < 10 ? "0" + minutes : minutes}:${seconds < 10 ? "0" + seconds : seconds}`;
|
||||
return `${hours < 10 ? "0" + hours : hours}:${minutes < 10 ? "0" + minutes : minutes}:${seconds < 10 ? "0" + seconds : seconds}`;
|
||||
};
|
||||
|
||||
@@ -6,16 +6,14 @@ import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any }
|
||||
? Omit<T, "children">
|
||||
: T;
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & {
|
||||
ref?: U | null;
|
||||
ref?: U | null;
|
||||
};
|
||||
|
||||
@@ -1,123 +1,119 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import { page } from "$app/state";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Card, CardContent } from "$lib/components/ui/card";
|
||||
import PeonyIcon from "$lib/components/icon/peony-icon.svelte";
|
||||
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
||||
import Meta from "$lib/components/meta/meta.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { page } from "$app/state";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Card, CardContent } from "$lib/components/ui/card";
|
||||
import PeonyIcon from "$lib/components/icon/peony-icon.svelte";
|
||||
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
||||
import Meta from "$lib/components/meta/meta.svelte";
|
||||
</script>
|
||||
|
||||
<Meta
|
||||
title={page.status === 404 ? $_("error.not_found") : $_("error.common")}
|
||||
description={$_("error.description")}
|
||||
title={page.status === 404 ? $_("error.not_found") : $_("error.common")}
|
||||
description={$_("error.description")}
|
||||
/>
|
||||
|
||||
<div
|
||||
class="relative min-h-screen flex items-center justify-center bg-gradient-to-br from-background via-primary/5 to-accent/5 overflow-hidden"
|
||||
class="relative min-h-screen flex items-center justify-center bg-gradient-to-br from-background via-primary/5 to-accent/5 overflow-hidden"
|
||||
>
|
||||
<PeonyBackground />
|
||||
<PeonyBackground />
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative z-10 container mx-auto px-4 text-center">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<!-- Premium Glassmorphism Card -->
|
||||
<Card
|
||||
class="bg-gradient-to-br from-card/25 via-card/30 to-card/20 backdrop-blur-2xl shadow-2xl shadow-primary/20"
|
||||
<!-- Content -->
|
||||
<div class="relative z-10 container mx-auto px-4 text-center">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<!-- Premium Glassmorphism Card -->
|
||||
<Card
|
||||
class="bg-gradient-to-br from-card/25 via-card/30 to-card/20 backdrop-blur-2xl shadow-2xl shadow-primary/20"
|
||||
>
|
||||
<CardContent class="p-12">
|
||||
<!-- 404 Animation -->
|
||||
<div class="mb-8">
|
||||
<div
|
||||
class="text-8xl md:text-9xl font-bold bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent animate-pulse"
|
||||
>
|
||||
<CardContent class="p-12">
|
||||
<!-- 404 Animation -->
|
||||
<div class="mb-8">
|
||||
<div
|
||||
class="text-8xl md:text-9xl font-bold bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent animate-pulse"
|
||||
>
|
||||
{page.status}
|
||||
</div>
|
||||
<div class="flex justify-center mt-4">
|
||||
<PeonyIcon class="w-16 h-16 text-primary/60 animate-bounce" />
|
||||
</div>
|
||||
</div>
|
||||
{page.status}
|
||||
</div>
|
||||
<div class="flex justify-center mt-4">
|
||||
<PeonyIcon class="w-16 h-16 text-primary/60 animate-bounce" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div class="space-y-6 mb-10">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-foreground">
|
||||
{page.status === 404 ? $_("error.not_found") : $_("error.common")}
|
||||
</h1>
|
||||
<p class="text-xl text-muted-foreground leading-relaxed max-w-2xl mx-auto">
|
||||
{$_("error.description")}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Error Message -->
|
||||
<div class="space-y-6 mb-10">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-foreground">
|
||||
{page.status === 404 ? $_("error.not_found") : $_("error.common")}
|
||||
</h1>
|
||||
<p class="text-xl text-muted-foreground leading-relaxed max-w-2xl mx-auto">
|
||||
{$_("error.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<Button
|
||||
size="lg"
|
||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 text-lg px-8 py-6"
|
||||
href="/"
|
||||
>
|
||||
<span class="icon-[ri--home-2-line]"></span>
|
||||
{$_("error.go_home")}
|
||||
</Button>
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<Button
|
||||
size="lg"
|
||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 text-lg px-8 py-6"
|
||||
href="/"
|
||||
>
|
||||
<span class="icon-[ri--home-2-line]"></span>
|
||||
{$_("error.go_home")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="text-lg px-8 py-6 border-primary/50 hover:bg-primary/10"
|
||||
href="/videos"
|
||||
>
|
||||
<span class="icon-[ri--search-line]"></span>
|
||||
{$_("error.explore_videos")}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="text-lg px-8 py-6 border-primary/50 hover:bg-primary/10"
|
||||
href="/videos"
|
||||
>
|
||||
<span class="icon-[ri--search-line]"></span>
|
||||
{$_("error.explore_videos")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div class="mt-8 pt-8 border-t border-border/50">
|
||||
<p class="text-sm text-muted-foreground mb-4">
|
||||
{$_("error.quick_links")}
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-3">
|
||||
<button
|
||||
class="text-sm text-primary hover:text-accent transition-colors hover:underline"
|
||||
><a href="/models">{$_("error.featured_models")}</a></button
|
||||
>
|
||||
<span class="text-muted-foreground">•</span>
|
||||
<button
|
||||
class="text-sm text-primary hover:text-accent transition-colors hover:underline"
|
||||
><a href="/magazine">{$_("error.magazine")}</a></button
|
||||
>
|
||||
<span class="text-muted-foreground">•</span>
|
||||
<button
|
||||
class="text-sm text-primary hover:text-accent transition-colors hover:underline"
|
||||
><a href="/about">{$_("error.about_us")}</a></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<!-- Quick Links -->
|
||||
<div class="mt-8 pt-8 border-t border-border/50">
|
||||
<p class="text-sm text-muted-foreground mb-4">
|
||||
{$_("error.quick_links")}
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-3">
|
||||
<button
|
||||
class="text-sm text-primary hover:text-accent transition-colors hover:underline"
|
||||
><a href="/models">{$_("error.featured_models")}</a></button
|
||||
>
|
||||
<span class="text-muted-foreground">•</span>
|
||||
<button
|
||||
class="text-sm text-primary hover:text-accent transition-colors hover:underline"
|
||||
><a href="/magazine">{$_("error.magazine")}</a></button
|
||||
>
|
||||
<span class="text-muted-foreground">•</span>
|
||||
<button
|
||||
class="text-sm text-primary hover:text-accent transition-colors hover:underline"
|
||||
><a href="/about">{$_("error.about_us")}</a></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Hearts Animation -->
|
||||
<div class="absolute inset-0 pointer-events-none">
|
||||
<div class="absolute top-1/4 left-1/4 opacity-20">
|
||||
<span
|
||||
class="icon-[ri--heart-3-line] w-6 h-6 text-primary animate-float animation-delay-1000"
|
||||
></span>
|
||||
</div>
|
||||
<div class="absolute top-1/3 right-1/3 opacity-15">
|
||||
<span
|
||||
class="icon-[ri--heart-3-line] w-4 h-4 text-accent animate-float animation-delay-3000"
|
||||
></span>
|
||||
</div>
|
||||
<div class="absolute bottom-1/4 left-1/3 opacity-25">
|
||||
<span
|
||||
class="icon-[ri--heart-3-line] w-5 h-5 text-primary animate-float animation-delay-5000"
|
||||
></span>
|
||||
</div>
|
||||
<div class="absolute bottom-1/3 right-1/4 opacity-20">
|
||||
<span
|
||||
class="icon-[ri--heart-3-line] w-3 h-3 text-accent animate-float animation-delay-7000"
|
||||
></span>
|
||||
</div>
|
||||
<!-- Floating Hearts Animation -->
|
||||
<div class="absolute inset-0 pointer-events-none">
|
||||
<div class="absolute top-1/4 left-1/4 opacity-20">
|
||||
<span class="icon-[ri--heart-3-line] w-6 h-6 text-primary animate-float animation-delay-1000"
|
||||
></span>
|
||||
</div>
|
||||
<div class="absolute top-1/3 right-1/3 opacity-15">
|
||||
<span class="icon-[ri--heart-3-line] w-4 h-4 text-accent animate-float animation-delay-3000"
|
||||
></span>
|
||||
</div>
|
||||
<div class="absolute bottom-1/4 left-1/3 opacity-25">
|
||||
<span class="icon-[ri--heart-3-line] w-5 h-5 text-primary animate-float animation-delay-5000"
|
||||
></span>
|
||||
</div>
|
||||
<div class="absolute bottom-1/3 right-1/4 opacity-20">
|
||||
<span class="icon-[ri--heart-3-line] w-3 h-3 text-accent animate-float animation-delay-7000"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export async function load({ locals }) {
|
||||
return {
|
||||
authStatus: locals.authStatus,
|
||||
};
|
||||
return {
|
||||
authStatus: locals.authStatus,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
<script lang="ts">
|
||||
import "../app.css";
|
||||
import { onMount } from "svelte";
|
||||
import { waitLocale } from "svelte-i18n";
|
||||
import "$lib/i18n";
|
||||
import Footer from "$lib/components/footer/footer.svelte";
|
||||
import { Toaster } from "$lib/components/ui/sonner";
|
||||
import Header from "$lib/components/header/header.svelte";
|
||||
import AgeVerificationDialog from "$lib/components/age-verification-dialog/age-verification-dialog.svelte";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import "../app.css";
|
||||
import { onMount } from "svelte";
|
||||
import { waitLocale } from "svelte-i18n";
|
||||
import "$lib/i18n";
|
||||
import Footer from "$lib/components/footer/footer.svelte";
|
||||
import { Toaster } from "$lib/components/ui/sonner";
|
||||
import Header from "$lib/components/header/header.svelte";
|
||||
import AgeVerificationDialog from "$lib/components/age-verification-dialog/age-verification-dialog.svelte";
|
||||
import { env } from "$env/dynamic/public";
|
||||
|
||||
onMount(async () => {
|
||||
await waitLocale();
|
||||
});
|
||||
onMount(async () => {
|
||||
await waitLocale();
|
||||
});
|
||||
|
||||
let { children, data } = $props();
|
||||
let { children, data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if import.meta.env.PROD && env.PUBLIC_UMAMI_ID && env.PUBLIC_UMAMI_SCRIPT}
|
||||
<script
|
||||
defer
|
||||
src={env.PUBLIC_UMAMI_SCRIPT}
|
||||
data-website-id={env.PUBLIC_UMAMI_ID}
|
||||
></script>
|
||||
{/if}
|
||||
{#if import.meta.env.PROD && env.PUBLIC_UMAMI_ID && env.PUBLIC_UMAMI_SCRIPT}
|
||||
<script defer src={env.PUBLIC_UMAMI_SCRIPT} data-website-id={env.PUBLIC_UMAMI_ID}></script>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<AgeVerificationDialog />
|
||||
@@ -31,48 +27,48 @@ let { children, data } = $props();
|
||||
<Toaster />
|
||||
|
||||
<div class="bg-background text-foreground min-h-screen">
|
||||
<!-- Advanced Global Plasma Background -->
|
||||
<div class="fixed inset-0 pointer-events-none overflow-hidden">
|
||||
<!-- Large primary blobs -->
|
||||
<div
|
||||
class="absolute -top-40 -left-40 w-80 h-80 bg-gradient-to-r from-primary/12 via-accent/18 to-primary/8 rounded-full blur-3xl animate-blob-ultra-slow"
|
||||
></div>
|
||||
<div
|
||||
class="absolute -bottom-40 -right-40 w-96 h-96 bg-gradient-to-r from-accent/12 via-primary/18 to-accent/8 rounded-full blur-3xl animate-blob-ultra-slow animation-delay-5000"
|
||||
></div>
|
||||
<!-- Advanced Global Plasma Background -->
|
||||
<div class="fixed inset-0 pointer-events-none overflow-hidden">
|
||||
<!-- Large primary blobs -->
|
||||
<div
|
||||
class="absolute -top-40 -left-40 w-80 h-80 bg-gradient-to-r from-primary/12 via-accent/18 to-primary/8 rounded-full blur-3xl animate-blob-ultra-slow"
|
||||
></div>
|
||||
<div
|
||||
class="absolute -bottom-40 -right-40 w-96 h-96 bg-gradient-to-r from-accent/12 via-primary/18 to-accent/8 rounded-full blur-3xl animate-blob-ultra-slow animation-delay-5000"
|
||||
></div>
|
||||
|
||||
<!-- Medium floating elements -->
|
||||
<div
|
||||
class="absolute top-1/2 -left-20 w-64 h-64 bg-gradient-to-r from-primary/10 via-accent/15 to-primary/6 rounded-full blur-2xl animate-blob-ultra-slow animation-delay-8000"
|
||||
></div>
|
||||
<div
|
||||
class="absolute top-1/4 -right-20 w-72 h-72 bg-gradient-to-r from-accent/10 via-primary/15 to-accent/6 rounded-full blur-2xl animate-blob-ultra-slow animation-delay-10000"
|
||||
></div>
|
||||
<!-- Medium floating elements -->
|
||||
<div
|
||||
class="absolute top-1/2 -left-20 w-64 h-64 bg-gradient-to-r from-primary/10 via-accent/15 to-primary/6 rounded-full blur-2xl animate-blob-ultra-slow animation-delay-8000"
|
||||
></div>
|
||||
<div
|
||||
class="absolute top-1/4 -right-20 w-72 h-72 bg-gradient-to-r from-accent/10 via-primary/15 to-accent/6 rounded-full blur-2xl animate-blob-ultra-slow animation-delay-10000"
|
||||
></div>
|
||||
|
||||
<!-- Small particle-like elements -->
|
||||
<div
|
||||
class="absolute top-1/3 left-1/4 w-32 h-32 bg-gradient-to-r from-primary/15 to-accent/12 rounded-full blur-xl animate-float animation-delay-2000"
|
||||
></div>
|
||||
<div
|
||||
class="absolute bottom-1/3 right-1/3 w-40 h-40 bg-gradient-to-r from-accent/15 to-primary/12 rounded-full blur-xl animate-float animation-delay-6000"
|
||||
></div>
|
||||
<div
|
||||
class="absolute top-2/3 left-1/2 w-24 h-24 bg-gradient-to-r from-primary/20 to-accent/15 rounded-full blur-lg animate-pulse-slow animation-delay-4000"
|
||||
></div>
|
||||
<!-- Small particle-like elements -->
|
||||
<div
|
||||
class="absolute top-1/3 left-1/4 w-32 h-32 bg-gradient-to-r from-primary/15 to-accent/12 rounded-full blur-xl animate-float animation-delay-2000"
|
||||
></div>
|
||||
<div
|
||||
class="absolute bottom-1/3 right-1/3 w-40 h-40 bg-gradient-to-r from-accent/15 to-primary/12 rounded-full blur-xl animate-float animation-delay-6000"
|
||||
></div>
|
||||
<div
|
||||
class="absolute top-2/3 left-1/2 w-24 h-24 bg-gradient-to-r from-primary/20 to-accent/15 rounded-full blur-lg animate-pulse-slow animation-delay-4000"
|
||||
></div>
|
||||
|
||||
<!-- Glassmorphism overlay -->
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-primary/2 via-transparent to-accent/2 backdrop-blur-[0.5px]"
|
||||
></div>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<Header authStatus={data.authStatus} />
|
||||
<!-- Glassmorphism overlay -->
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-primary/2 via-transparent to-accent/2 backdrop-blur-[0.5px]"
|
||||
></div>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<Header authStatus={data.authStatus} />
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="min-h-screen">
|
||||
{@render children()}
|
||||
</main>
|
||||
<!-- Main Content -->
|
||||
<main class="min-h-screen">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<Footer />
|
||||
<!-- Footer -->
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getFeaturedModels, getFeaturedVideos } from "$lib/services";
|
||||
export async function load({ fetch }) {
|
||||
return {
|
||||
models: await getFeaturedModels(3, fetch),
|
||||
videos: await getFeaturedVideos(3, fetch),
|
||||
};
|
||||
return {
|
||||
models: await getFeaturedModels(3, fetch),
|
||||
videos: await getFeaturedVideos(3, fetch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Card, CardContent } from "$lib/components/ui/card";
|
||||
import { getAssetUrl } from "$lib/directus";
|
||||
import Meta from "$lib/components/meta/meta.svelte";
|
||||
import { formatVideoDuration } from "$lib/utils.js";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Card, CardContent } from "$lib/components/ui/card";
|
||||
import { getAssetUrl } from "$lib/directus";
|
||||
import Meta from "$lib/components/meta/meta.svelte";
|
||||
import { formatVideoDuration } from "$lib/utils.js";
|
||||
|
||||
const { data } = $props();
|
||||
const { data } = $props();
|
||||
</script>
|
||||
|
||||
<Meta title={$_('home.hero.title')} description={$_('home.hero.description')} />
|
||||
<Meta title={$_("home.hero.title")} description={$_("home.hero.description")} />
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section
|
||||
class="relative min-h-screen flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<section class="relative min-h-screen flex items-center justify-center overflow-hidden">
|
||||
<!-- Background Gradient -->
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-primary/20 via-accent/10 to-background"
|
||||
></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-primary/20 via-accent/10 to-background"></div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative z-10 container mx-auto px-4 text-center">
|
||||
@@ -26,13 +22,11 @@ const { data } = $props();
|
||||
<h1
|
||||
class="text-6xl md:text-8xl font-bold bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent leading-tight mb-8"
|
||||
>
|
||||
{$_('home.hero.title')}
|
||||
{$_("home.hero.title")}
|
||||
</h1>
|
||||
|
||||
<p
|
||||
class="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto leading-relaxed"
|
||||
>
|
||||
{$_('home.hero.description')}
|
||||
<p class="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
|
||||
{$_("home.hero.description")}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
@@ -42,13 +36,13 @@ const { data } = $props();
|
||||
href="/videos"
|
||||
>
|
||||
<span class="icon-[ri--play-large-fill]"></span>
|
||||
{$_('home.hero.cta_videos')}
|
||||
{$_("home.hero.cta_videos")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="text-lg px-8 py-6 border-primary/50 hover:bg-primary/10"
|
||||
href="/models">{$_('home.hero.cta_models')}</Button
|
||||
href="/models">{$_("home.hero.cta_models")}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,10 +62,10 @@ const { data } = $props();
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-4">
|
||||
{$_('home.featured_models.title')}
|
||||
{$_("home.featured_models.title")}
|
||||
</h2>
|
||||
<p class="text-muted-foreground text-lg">
|
||||
{$_('home.featured_models.description')}
|
||||
{$_("home.featured_models.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +77,7 @@ const { data } = $props();
|
||||
<CardContent class="p-6 text-center">
|
||||
<div class="relative mb-4">
|
||||
<img
|
||||
src={getAssetUrl(model.avatar, 'mini')}
|
||||
src={getAssetUrl(model.avatar, "mini")}
|
||||
alt={model.artist_name}
|
||||
class="w-24 h-24 rounded-full mx-auto object-cover ring-4 ring-primary/20 group-hover:ring-primary/40 transition-all"
|
||||
/>
|
||||
@@ -107,8 +101,7 @@ const { data } = $props();
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="mt-4 w-full group-hover:bg-primary/10"
|
||||
href="/models/{model.slug}"
|
||||
>{$_('home.featured_models.view_profile')}</Button
|
||||
href="/models/{model.slug}">{$_("home.featured_models.view_profile")}</Button
|
||||
>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -122,7 +115,7 @@ const { data } = $props();
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-4">
|
||||
{$_('home.trending.title')}
|
||||
{$_("home.trending.title")}
|
||||
</h2>
|
||||
<!-- <p class="text-muted-foreground text-lg">Most watched romantic content</p> -->
|
||||
</div>
|
||||
@@ -134,16 +127,14 @@ const { data } = $props();
|
||||
>
|
||||
<div class="relative">
|
||||
<img
|
||||
src={getAssetUrl(video.image, 'preview')}
|
||||
src={getAssetUrl(video.image, "preview")}
|
||||
alt={video.title}
|
||||
class="w-full h-48 object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent group-hover:scale-105 transition-transform duration-300"
|
||||
></div>
|
||||
<div
|
||||
class="absolute bottom-2 left-2 text-white text-sm font-medium"
|
||||
>
|
||||
<div class="absolute bottom-2 left-2 text-white text-sm font-medium">
|
||||
{#if video.movie_file?.duration}{formatVideoDuration(video.movie_file.duration)}{/if}
|
||||
</div>
|
||||
<!-- <div
|
||||
@@ -160,21 +151,18 @@ const { data } = $props();
|
||||
href="/videos/{video.slug}"
|
||||
aria-label={video.title}
|
||||
>
|
||||
<span class="icon-[ri--play-large-fill] w-8 h-8 text-white"
|
||||
></span>
|
||||
<span class="icon-[ri--play-large-fill] w-8 h-8 text-white"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent class="px-4 pb-4 pt-0">
|
||||
<h3
|
||||
class="font-semibold mb-2 group-hover:text-primary transition-colors"
|
||||
>
|
||||
<h3 class="font-semibold mb-2 group-hover:text-primary transition-colors">
|
||||
{video.title}
|
||||
</h3>
|
||||
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span class="icon-[ri--fire-line] w-4 h-4"></span>
|
||||
{$_('home.trending.trending')}
|
||||
{$_("home.trending.trending")}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -184,29 +172,27 @@ const { data } = $props();
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section
|
||||
class="py-20 bg-gradient-to-r from-primary/10 via-accent/10 to-primary/10"
|
||||
>
|
||||
<section class="py-20 bg-gradient-to-r from-primary/10 via-accent/10 to-primary/10">
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
<div class="max-w-3xl mx-auto space-y-8">
|
||||
<h2 class="text-3xl md:text-4xl font-bold">
|
||||
{$_('home.featured_models.join_community')}
|
||||
{$_("home.featured_models.join_community")}
|
||||
</h2>
|
||||
<p class="text-lg text-muted-foreground">
|
||||
{$_('home.featured_models.join_community_description')}
|
||||
{$_("home.featured_models.join_community_description")}
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
href="/signup"
|
||||
size="lg"
|
||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 text-lg px-8 py-6"
|
||||
>{$_('home.community.cta_join')}</Button
|
||||
>{$_("home.community.cta_join")}</Button
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="text-lg px-8 py-6 border-primary/50 hover:bg-primary/10"
|
||||
href="/magazine">{$_('home.community.cta_magazine')}</Button
|
||||
href="/magazine">{$_("home.community.cta_magazine")}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getStats } from "$lib/services";
|
||||
export async function load({ fetch }) {
|
||||
return {
|
||||
stats: await getStats(fetch),
|
||||
};
|
||||
return {
|
||||
stats: await getStats(fetch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,312 +1,310 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Card, CardContent } from "$lib/components/ui/card";
|
||||
import Meta from "$lib/components/meta/meta.svelte";
|
||||
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Card, CardContent } from "$lib/components/ui/card";
|
||||
import Meta from "$lib/components/meta/meta.svelte";
|
||||
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
||||
|
||||
const { data } = $props();
|
||||
const { data } = $props();
|
||||
|
||||
const stats = [
|
||||
{
|
||||
icon: "icon-[ri--user-heart-line]",
|
||||
value: data.stats.viewers_count,
|
||||
label: $_("about.stats.members"),
|
||||
},
|
||||
{
|
||||
icon: "icon-[ri--video-on-line]",
|
||||
value: data.stats.videos_count,
|
||||
label: $_("about.stats.videos"),
|
||||
},
|
||||
{
|
||||
icon: "icon-[ri--star-line]",
|
||||
value: data.stats.models_count,
|
||||
label: $_("about.stats.models"),
|
||||
},
|
||||
{
|
||||
icon: "icon-[ri--award-line]",
|
||||
value: $_("about.stats.yearsFormatted", { values: { years: 5 } }),
|
||||
label: $_("about.stats.experience"),
|
||||
},
|
||||
];
|
||||
const stats = [
|
||||
{
|
||||
icon: "icon-[ri--user-heart-line]",
|
||||
value: data.stats.viewers_count,
|
||||
label: $_("about.stats.members"),
|
||||
},
|
||||
{
|
||||
icon: "icon-[ri--video-on-line]",
|
||||
value: data.stats.videos_count,
|
||||
label: $_("about.stats.videos"),
|
||||
},
|
||||
{
|
||||
icon: "icon-[ri--star-line]",
|
||||
value: data.stats.models_count,
|
||||
label: $_("about.stats.models"),
|
||||
},
|
||||
{
|
||||
icon: "icon-[ri--award-line]",
|
||||
value: $_("about.stats.yearsFormatted", { values: { years: 5 } }),
|
||||
label: $_("about.stats.experience"),
|
||||
},
|
||||
];
|
||||
|
||||
const team = [
|
||||
{
|
||||
name: $_("about.team.sebastian.name"),
|
||||
role: $_("about.team.sebastian.role"),
|
||||
image: $_("about.team.sebastian.image"),
|
||||
bio: $_("about.team.sebastian.bio"),
|
||||
},
|
||||
{
|
||||
name: $_("about.team.valknar.name"),
|
||||
role: $_("about.team.valknar.role"),
|
||||
image: $_("about.team.valknar.image"),
|
||||
bio: $_("about.team.valknar.bio"),
|
||||
},
|
||||
];
|
||||
const team = [
|
||||
{
|
||||
name: $_("about.team.sebastian.name"),
|
||||
role: $_("about.team.sebastian.role"),
|
||||
image: $_("about.team.sebastian.image"),
|
||||
bio: $_("about.team.sebastian.bio"),
|
||||
},
|
||||
{
|
||||
name: $_("about.team.valknar.name"),
|
||||
role: $_("about.team.valknar.role"),
|
||||
image: $_("about.team.valknar.image"),
|
||||
bio: $_("about.team.valknar.bio"),
|
||||
},
|
||||
];
|
||||
|
||||
const values = [
|
||||
{
|
||||
icon: "icon-[ri--heart-line]",
|
||||
title: $_("about.values.authentic_expression.title"),
|
||||
description: $_("about.values.authentic_expression.description"),
|
||||
},
|
||||
{
|
||||
icon: "icon-[ri--shield-line]",
|
||||
title: $_("about.values.safety_respect.title"),
|
||||
description: $_("about.values.safety_respect.description"),
|
||||
},
|
||||
{
|
||||
icon: "icon-[ri--star-line]",
|
||||
title: $_("about.values.artistic_excellence.title"),
|
||||
description: $_("about.values.artistic_excellence.description"),
|
||||
},
|
||||
{
|
||||
icon: "icon-[ri--user-heart-line]",
|
||||
title: $_("about.values.community_first.title"),
|
||||
description: $_("about.values.community_first.description"),
|
||||
},
|
||||
];
|
||||
const values = [
|
||||
{
|
||||
icon: "icon-[ri--heart-line]",
|
||||
title: $_("about.values.authentic_expression.title"),
|
||||
description: $_("about.values.authentic_expression.description"),
|
||||
},
|
||||
{
|
||||
icon: "icon-[ri--shield-line]",
|
||||
title: $_("about.values.safety_respect.title"),
|
||||
description: $_("about.values.safety_respect.description"),
|
||||
},
|
||||
{
|
||||
icon: "icon-[ri--star-line]",
|
||||
title: $_("about.values.artistic_excellence.title"),
|
||||
description: $_("about.values.artistic_excellence.description"),
|
||||
},
|
||||
{
|
||||
icon: "icon-[ri--user-heart-line]",
|
||||
title: $_("about.values.community_first.title"),
|
||||
description: $_("about.values.community_first.description"),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Meta title={$_("about.title")} description={$_("about.subtitle")} />
|
||||
|
||||
<div
|
||||
class="relative min-h-screen bg-gradient-to-br from-background via-primary/5 to-accent/5 overflow-hidden"
|
||||
class="relative min-h-screen bg-gradient-to-br from-background via-primary/5 to-accent/5 overflow-hidden"
|
||||
>
|
||||
<PeonyBackground />
|
||||
<PeonyBackground />
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="relative py-20 overflow-hidden">
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-primary/10 via-accent/5 to-background"
|
||||
></div>
|
||||
<div class="relative container mx-auto px-4 text-center">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<h1
|
||||
class="text-5xl md:text-7xl font-bold mb-8 bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent"
|
||||
<!-- Hero Section -->
|
||||
<section class="relative py-20 overflow-hidden">
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-primary/10 via-accent/5 to-background"
|
||||
></div>
|
||||
<div class="relative container mx-auto px-4 text-center">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<h1
|
||||
class="text-5xl md:text-7xl font-bold mb-8 bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent"
|
||||
>
|
||||
{$_("about.title")}
|
||||
</h1>
|
||||
<p
|
||||
class="text-xl md:text-2xl text-muted-foreground mb-10 leading-relaxed max-w-4xl mx-auto"
|
||||
>
|
||||
{$_("about.subtitle")}
|
||||
</p>
|
||||
<div class="flex justify-center">
|
||||
<Button
|
||||
size="lg"
|
||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
href="/signup">{$_("about.join_community")}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<section class="py-16 bg-card/30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||
{#each stats as stat (stat.icon)}
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-16 h-16 bg-gradient-to-br from-primary/20 to-accent/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<span class={stat.icon + " w-8 h-8 text-primary"}></span>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-primary mb-2">{stat.value}</div>
|
||||
<div class="text-muted-foreground">{stat.label}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Our Story Section -->
|
||||
<section class="py-20">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-4">
|
||||
{$_("about.story.title")}
|
||||
</h2>
|
||||
<p class="text-lg text-muted-foreground">
|
||||
{$_("about.story.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div class="space-y-6">
|
||||
<p class="text-muted-foreground leading-relaxed text-lg">
|
||||
{$_("about.story.description_part1")}
|
||||
</p>
|
||||
<p class="text-muted-foreground leading-relaxed text-lg">
|
||||
{$_("about.story.description_part2")}
|
||||
</p>
|
||||
<p class="text-muted-foreground leading-relaxed text-lg">
|
||||
{$_("about.story.description_part3")}
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<img
|
||||
src="/img/babes.jpg"
|
||||
alt="Our story"
|
||||
class="w-full object-cover rounded-2xl shadow-2xl"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-primary/20 to-transparent rounded-2xl"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Values Section -->
|
||||
<section class="py-20 bg-card/30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-4">
|
||||
{$_("about.values.title")}
|
||||
</h2>
|
||||
<p class="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{$_("about.values.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
{#each values as value (value.title)}
|
||||
<Card
|
||||
class="bg-gradient-to-br from-card to-card/50 border-primary/20 hover:shadow-xl hover:shadow-primary/10 transition-all duration-300"
|
||||
>
|
||||
<CardContent class="p-6">
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="w-12 h-12 bg-gradient-to-br from-primary/20 to-accent/20 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
{$_("about.title")}
|
||||
</h1>
|
||||
<p
|
||||
class="text-xl md:text-2xl text-muted-foreground mb-10 leading-relaxed max-w-4xl mx-auto"
|
||||
>
|
||||
{$_("about.subtitle")}
|
||||
</p>
|
||||
<div class="flex justify-center">
|
||||
<Button
|
||||
size="lg"
|
||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
href="/signup">{$_("about.join_community")}</Button
|
||||
>
|
||||
<span class={value.icon + " w-6 h-6 text-primary"}></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<section class="py-16 bg-card/30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||
{#each stats as stat (stat.icon)}
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-16 h-16 bg-gradient-to-br from-primary/20 to-accent/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<span class={stat.icon + " w-8 h-8 text-primary"}></span>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-primary mb-2">{stat.value}</div>
|
||||
<div class="text-muted-foreground">{stat.label}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Our Story Section -->
|
||||
<section class="py-20">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-4">
|
||||
{$_("about.story.title")}
|
||||
</h2>
|
||||
<p class="text-lg text-muted-foreground">
|
||||
{$_("about.story.subtitle")}
|
||||
</p>
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg mb-2">{value.title}</h3>
|
||||
<p class="text-muted-foreground leading-relaxed">
|
||||
{value.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div class="space-y-6">
|
||||
<p class="text-muted-foreground leading-relaxed text-lg">
|
||||
{$_("about.story.description_part1")}
|
||||
</p>
|
||||
<p class="text-muted-foreground leading-relaxed text-lg">
|
||||
{$_("about.story.description_part2")}
|
||||
</p>
|
||||
<p class="text-muted-foreground leading-relaxed text-lg">
|
||||
{$_("about.story.description_part3")}
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<img
|
||||
src="/img/babes.jpg"
|
||||
alt="Our story"
|
||||
class="w-full object-cover rounded-2xl shadow-2xl"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-primary/20 to-transparent rounded-2xl"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Team Section -->
|
||||
<section class="py-20">
|
||||
<div class="container mx-auto px-4 max-w-xl">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-4">
|
||||
{$_("about.team.title")}
|
||||
</h2>
|
||||
<p class="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{$_("about.team.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{#each team as member (member.name)}
|
||||
<Card
|
||||
class="bg-gradient-to-br from-card to-card/50 border-primary/20 hover:shadow-xl hover:shadow-primary/10 transition-all duration-300 hover:-translate-y-2"
|
||||
>
|
||||
<CardContent class="p-6 text-center">
|
||||
<img
|
||||
src={member.image}
|
||||
alt={member.name}
|
||||
class="w-24 h-24 rounded-full mx-auto mb-4 object-cover ring-4 ring-primary/20"
|
||||
/>
|
||||
<h3 class="font-semibold text-lg mb-1">{member.name}</h3>
|
||||
<p class="text-primary text-sm mb-3">{member.role}</p>
|
||||
<p class="text-muted-foreground text-sm leading-relaxed">
|
||||
{member.bio}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Mission Section -->
|
||||
<section class="py-20 bg-gradient-to-r from-primary/10 via-accent/10 to-primary/10">
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-6">
|
||||
{$_("about.mission.title")}
|
||||
</h2>
|
||||
<p class="text-xl text-muted-foreground mb-8 leading-relaxed">
|
||||
{$_("about.mission.description")}
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
size="lg"
|
||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
href="/signup">{$_("about.mission.cta_creator")}</Button
|
||||
>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="border-primary/50 hover:bg-primary/10"
|
||||
href="/signup">{$_("about.mission.cta_community")}</Button
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Values Section -->
|
||||
<section class="py-20 bg-card/30">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-4">
|
||||
{$_("about.values.title")}
|
||||
</h2>
|
||||
<p class="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{$_("about.values.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
{#each values as value (value.title)}
|
||||
<Card
|
||||
class="bg-gradient-to-br from-card to-card/50 border-primary/20 hover:shadow-xl hover:shadow-primary/10 transition-all duration-300"
|
||||
>
|
||||
<CardContent class="p-6">
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="w-12 h-12 bg-gradient-to-br from-primary/20 to-accent/20 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<span class={value.icon + " w-6 h-6 text-primary"}></span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg mb-2">{value.title}</h3>
|
||||
<p class="text-muted-foreground leading-relaxed">
|
||||
{value.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- Contact Section -->
|
||||
<section class="py-20">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-2xl mx-auto text-center">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-6">
|
||||
{$_("about.contact.title")}
|
||||
</h2>
|
||||
<p class="text-lg text-muted-foreground mb-8">
|
||||
{$_("about.contact.description")}
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card class="bg-gradient-to-br from-card to-card/50 border-primary/20">
|
||||
<CardContent class="p-6 text-center">
|
||||
<h3 class="font-semibold mb-2">
|
||||
{$_("about.contact.general.title")}
|
||||
</h3>
|
||||
<p class="text-muted-foreground text-sm mb-4">
|
||||
{$_("about.contact.general.description")}
|
||||
</p>
|
||||
<a
|
||||
href="mailto:{$_('about.contact.general.mailto')}"
|
||||
class="text-primary hover:underline">{$_("about.contact.general.mailto")}</a
|
||||
>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="bg-gradient-to-br from-card to-card/50 border-primary/20">
|
||||
<CardContent class="p-6 text-center">
|
||||
<h3 class="font-semibold mb-2">
|
||||
{$_("about.contact.creators.title")}
|
||||
</h3>
|
||||
<p class="text-muted-foreground text-sm mb-4">
|
||||
{$_("about.contact.creators.description")}
|
||||
</p>
|
||||
<a
|
||||
href="mailto:{$_('about.contact.creators.mailto')}"
|
||||
class="text-primary hover:underline">{$_("about.contact.creators.mailto")}</a
|
||||
>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Team Section -->
|
||||
<section class="py-20">
|
||||
<div class="container mx-auto px-4 max-w-xl">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-4">
|
||||
{$_("about.team.title")}
|
||||
</h2>
|
||||
<p class="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{$_("about.team.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{#each team as member (member.name)}
|
||||
<Card
|
||||
class="bg-gradient-to-br from-card to-card/50 border-primary/20 hover:shadow-xl hover:shadow-primary/10 transition-all duration-300 hover:-translate-y-2"
|
||||
>
|
||||
<CardContent class="p-6 text-center">
|
||||
<img
|
||||
src={member.image}
|
||||
alt={member.name}
|
||||
class="w-24 h-24 rounded-full mx-auto mb-4 object-cover ring-4 ring-primary/20"
|
||||
/>
|
||||
<h3 class="font-semibold text-lg mb-1">{member.name}</h3>
|
||||
<p class="text-primary text-sm mb-3">{member.role}</p>
|
||||
<p class="text-muted-foreground text-sm leading-relaxed">
|
||||
{member.bio}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Mission Section -->
|
||||
<section class="py-20 bg-gradient-to-r from-primary/10 via-accent/10 to-primary/10">
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-6">
|
||||
{$_("about.mission.title")}
|
||||
</h2>
|
||||
<p class="text-xl text-muted-foreground mb-8 leading-relaxed">
|
||||
{$_("about.mission.description")}
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
size="lg"
|
||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
href="/signup">{$_("about.mission.cta_creator")}</Button
|
||||
>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="border-primary/50 hover:bg-primary/10"
|
||||
href="/signup">{$_("about.mission.cta_community")}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Contact Section -->
|
||||
<section class="py-20">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-2xl mx-auto text-center">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-6">
|
||||
{$_("about.contact.title")}
|
||||
</h2>
|
||||
<p class="text-lg text-muted-foreground mb-8">
|
||||
{$_("about.contact.description")}
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card class="bg-gradient-to-br from-card to-card/50 border-primary/20">
|
||||
<CardContent class="p-6 text-center">
|
||||
<h3 class="font-semibold mb-2">
|
||||
{$_("about.contact.general.title")}
|
||||
</h3>
|
||||
<p class="text-muted-foreground text-sm mb-4">
|
||||
{$_("about.contact.general.description")}
|
||||
</p>
|
||||
<a
|
||||
href="mailto:{$_('about.contact.general.mailto')}"
|
||||
class="text-primary hover:underline"
|
||||
>{$_("about.contact.general.mailto")}</a
|
||||
>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="bg-gradient-to-br from-card to-card/50 border-primary/20">
|
||||
<CardContent class="p-6 text-center">
|
||||
<h3 class="font-semibold mb-2">
|
||||
{$_("about.contact.creators.title")}
|
||||
</h3>
|
||||
<p class="text-muted-foreground text-sm mb-4">
|
||||
{$_("about.contact.creators.description")}
|
||||
</p>
|
||||
<a
|
||||
href="mailto:{$_('about.contact.creators.mailto')}"
|
||||
class="text-primary hover:underline"
|
||||
>{$_("about.contact.creators.mailto")}</a
|
||||
>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,358 +1,346 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import { SvelteSet } from "svelte/reactivity";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "$lib/components/ui/card";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
||||
import Meta from "$lib/components/meta/meta.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { SvelteSet } from "svelte/reactivity";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "$lib/components/ui/card";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
||||
import Meta from "$lib/components/meta/meta.svelte";
|
||||
|
||||
let searchQuery = $state("");
|
||||
let expandedItems = new SvelteSet<number>();
|
||||
let searchQuery = $state("");
|
||||
let expandedItems = new SvelteSet<number>();
|
||||
|
||||
const faqCategories = [
|
||||
{
|
||||
id: 1,
|
||||
title: $_("faq.getting_started.title"),
|
||||
icon: "icon-[ri--home-heart-line]",
|
||||
questions: [
|
||||
{
|
||||
id: 1,
|
||||
question: $_("faq.getting_started.questions.0.question"),
|
||||
answer: $_("faq.getting_started.questions.0.answer"),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
question: $_("faq.getting_started.questions.1.question"),
|
||||
answer: $_("faq.getting_started.questions.1.answer"),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
question: $_("faq.getting_started.questions.2.question"),
|
||||
answer: $_("faq.getting_started.questions.2.answer"),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
question: $_("faq.getting_started.questions.3.question"),
|
||||
answer: $_("faq.getting_started.questions.3.answer"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: $_("faq.creators.title"),
|
||||
icon: "icon-[ri--user-heart-line]",
|
||||
questions: [
|
||||
{
|
||||
id: 5,
|
||||
question: $_("faq.creators.questions.0.question"),
|
||||
answer: $_("faq.creators.questions.0.answer"),
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
question: $_("faq.creators.questions.1.question"),
|
||||
answer: $_("faq.creators.questions.1.answer"),
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
question: $_("faq.creators.questions.2.question"),
|
||||
answer: $_("faq.creators.questions.2.answer"),
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
question: $_("faq.creators.questions.3.question"),
|
||||
answer: $_("faq.creators.questions.3.answer"),
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// id: 3,
|
||||
// title: $_("faq.categories.payments"),
|
||||
// icon: CreditCardIcon,
|
||||
// questions: [
|
||||
// {
|
||||
// id: 9,
|
||||
// question: "What payment methods do you accept?",
|
||||
// answer:
|
||||
// "We accept all major credit cards (Visa, MasterCard, American Express), PayPal, and various digital payment methods. All transactions are processed securely through encrypted payment gateways.",
|
||||
// },
|
||||
// {
|
||||
// id: 10,
|
||||
// question: "How does billing work?",
|
||||
// answer:
|
||||
// "Subscriptions are billed monthly or annually depending on your chosen plan. You'll be charged on the same date each billing cycle. We send email notifications before each billing date, and you can cancel anytime from your account settings.",
|
||||
// },
|
||||
// {
|
||||
// id: 11,
|
||||
// question: "Can I get a refund?",
|
||||
// answer:
|
||||
// "We offer refunds within 7 days of purchase if you haven't accessed premium content. For technical issues or billing errors, contact our support team. Refunds are processed within 5-10 business days to your original payment method.",
|
||||
// },
|
||||
// {
|
||||
// id: 12,
|
||||
// question: "How do creator payouts work?",
|
||||
// answer:
|
||||
// "Creators are paid weekly via direct deposit, PayPal, or wire transfer. Minimum payout is $50. Earnings are calculated after a 7-day processing period to account for potential chargebacks or refunds.",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
id: 4,
|
||||
title: $_("faq.privacy.title"),
|
||||
icon: "icon-[ri--git-repository-private-line]",
|
||||
questions: [
|
||||
{
|
||||
id: 13,
|
||||
question: $_("faq.privacy.questions.0.question"),
|
||||
answer: $_("faq.privacy.questions.0.answer"),
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
question: $_("faq.privacy.questions.1.question"),
|
||||
answer: $_("faq.privacy.questions.1.answer"),
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
question: $_("faq.privacy.questions.2.question"),
|
||||
answer: $_("faq.privacy.questions.2.answer"),
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
question: $_("faq.privacy.questions.3.question"),
|
||||
answer: $_("faq.privacy.questions.3.answer"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: $_("faq.technical.title"),
|
||||
icon: "icon-[ri--settings-3-line]",
|
||||
questions: [
|
||||
{
|
||||
id: 17,
|
||||
question: $_("faq.technical.questions.0.question"),
|
||||
answer: $_("faq.technical.questions.0.answer"),
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
question: $_("faq.technical.questions.1.question"),
|
||||
answer: $_("faq.technical.questions.1.answer"),
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
question: $_("faq.technical.questions.2.question"),
|
||||
answer: $_("faq.technical.questions.2.answer"),
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
question: $_("faq.technical.questions.3.question"),
|
||||
answer: $_("faq.technical.questions.3.answer"),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const faqCategories = [
|
||||
{
|
||||
id: 1,
|
||||
title: $_("faq.getting_started.title"),
|
||||
icon: "icon-[ri--home-heart-line]",
|
||||
questions: [
|
||||
{
|
||||
id: 1,
|
||||
question: $_("faq.getting_started.questions.0.question"),
|
||||
answer: $_("faq.getting_started.questions.0.answer"),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
question: $_("faq.getting_started.questions.1.question"),
|
||||
answer: $_("faq.getting_started.questions.1.answer"),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
question: $_("faq.getting_started.questions.2.question"),
|
||||
answer: $_("faq.getting_started.questions.2.answer"),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
question: $_("faq.getting_started.questions.3.question"),
|
||||
answer: $_("faq.getting_started.questions.3.answer"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: $_("faq.creators.title"),
|
||||
icon: "icon-[ri--user-heart-line]",
|
||||
questions: [
|
||||
{
|
||||
id: 5,
|
||||
question: $_("faq.creators.questions.0.question"),
|
||||
answer: $_("faq.creators.questions.0.answer"),
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
question: $_("faq.creators.questions.1.question"),
|
||||
answer: $_("faq.creators.questions.1.answer"),
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
question: $_("faq.creators.questions.2.question"),
|
||||
answer: $_("faq.creators.questions.2.answer"),
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
question: $_("faq.creators.questions.3.question"),
|
||||
answer: $_("faq.creators.questions.3.answer"),
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// id: 3,
|
||||
// title: $_("faq.categories.payments"),
|
||||
// icon: CreditCardIcon,
|
||||
// questions: [
|
||||
// {
|
||||
// id: 9,
|
||||
// question: "What payment methods do you accept?",
|
||||
// answer:
|
||||
// "We accept all major credit cards (Visa, MasterCard, American Express), PayPal, and various digital payment methods. All transactions are processed securely through encrypted payment gateways.",
|
||||
// },
|
||||
// {
|
||||
// id: 10,
|
||||
// question: "How does billing work?",
|
||||
// answer:
|
||||
// "Subscriptions are billed monthly or annually depending on your chosen plan. You'll be charged on the same date each billing cycle. We send email notifications before each billing date, and you can cancel anytime from your account settings.",
|
||||
// },
|
||||
// {
|
||||
// id: 11,
|
||||
// question: "Can I get a refund?",
|
||||
// answer:
|
||||
// "We offer refunds within 7 days of purchase if you haven't accessed premium content. For technical issues or billing errors, contact our support team. Refunds are processed within 5-10 business days to your original payment method.",
|
||||
// },
|
||||
// {
|
||||
// id: 12,
|
||||
// question: "How do creator payouts work?",
|
||||
// answer:
|
||||
// "Creators are paid weekly via direct deposit, PayPal, or wire transfer. Minimum payout is $50. Earnings are calculated after a 7-day processing period to account for potential chargebacks or refunds.",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
id: 4,
|
||||
title: $_("faq.privacy.title"),
|
||||
icon: "icon-[ri--git-repository-private-line]",
|
||||
questions: [
|
||||
{
|
||||
id: 13,
|
||||
question: $_("faq.privacy.questions.0.question"),
|
||||
answer: $_("faq.privacy.questions.0.answer"),
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
question: $_("faq.privacy.questions.1.question"),
|
||||
answer: $_("faq.privacy.questions.1.answer"),
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
question: $_("faq.privacy.questions.2.question"),
|
||||
answer: $_("faq.privacy.questions.2.answer"),
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
question: $_("faq.privacy.questions.3.question"),
|
||||
answer: $_("faq.privacy.questions.3.answer"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: $_("faq.technical.title"),
|
||||
icon: "icon-[ri--settings-3-line]",
|
||||
questions: [
|
||||
{
|
||||
id: 17,
|
||||
question: $_("faq.technical.questions.0.question"),
|
||||
answer: $_("faq.technical.questions.0.answer"),
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
question: $_("faq.technical.questions.1.question"),
|
||||
answer: $_("faq.technical.questions.1.answer"),
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
question: $_("faq.technical.questions.2.question"),
|
||||
answer: $_("faq.technical.questions.2.answer"),
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
question: $_("faq.technical.questions.3.question"),
|
||||
answer: $_("faq.technical.questions.3.answer"),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const allQuestions = faqCategories.flatMap((category) =>
|
||||
category.questions.map((q) => ({ ...q, categoryTitle: category.title })),
|
||||
);
|
||||
const allQuestions = faqCategories.flatMap((category) =>
|
||||
category.questions.map((q) => ({ ...q, categoryTitle: category.title })),
|
||||
);
|
||||
|
||||
const filteredQuestions = $derived(() => {
|
||||
if (!searchQuery.trim()) return allQuestions;
|
||||
return allQuestions.filter(
|
||||
(q) =>
|
||||
q.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
q.answer.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
q.categoryTitle.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
});
|
||||
const filteredQuestions = $derived(() => {
|
||||
if (!searchQuery.trim()) return allQuestions;
|
||||
return allQuestions.filter(
|
||||
(q) =>
|
||||
q.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
q.answer.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
q.categoryTitle.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
});
|
||||
|
||||
function toggleExpanded(id: number) {
|
||||
const newExpanded = new SvelteSet(expandedItems);
|
||||
if (newExpanded.has(id)) {
|
||||
newExpanded.delete(id);
|
||||
} else {
|
||||
newExpanded.add(id);
|
||||
}
|
||||
expandedItems = newExpanded;
|
||||
}
|
||||
function toggleExpanded(id: number) {
|
||||
const newExpanded = new SvelteSet(expandedItems);
|
||||
if (newExpanded.has(id)) {
|
||||
newExpanded.delete(id);
|
||||
} else {
|
||||
newExpanded.add(id);
|
||||
}
|
||||
expandedItems = newExpanded;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Meta title={$_("faq.title")} description={$_("faq.description")} />
|
||||
|
||||
<div
|
||||
class="relative min-h-screen bg-gradient-to-br from-background via-primary/5 to-accent/5 overflow-hidden"
|
||||
class="relative min-h-screen bg-gradient-to-br from-background via-primary/5 to-accent/5 overflow-hidden"
|
||||
>
|
||||
<PeonyBackground />
|
||||
<PeonyBackground />
|
||||
|
||||
<div class="container mx-auto py-20 relative px-4">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-16">
|
||||
<h1
|
||||
class="text-5xl md:text-6xl font-bold mb-8 bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent"
|
||||
<div class="container mx-auto py-20 relative px-4">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-16">
|
||||
<h1
|
||||
class="text-5xl md:text-6xl font-bold mb-8 bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent"
|
||||
>
|
||||
{$_("faq.title")}
|
||||
</h1>
|
||||
<p class="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
|
||||
{$_("faq.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="max-w-2xl mx-auto mb-12">
|
||||
<div class="relative">
|
||||
<span class="icon-[ri--search-line] absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5"
|
||||
></span>
|
||||
<Input
|
||||
placeholder={$_("faq.search_placeholder")}
|
||||
bind:value={searchQuery}
|
||||
class="pl-12 h-14 text-lg bg-card/50 border-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if searchQuery.trim()}
|
||||
<!-- Search Results -->
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-2xl font-bold mb-6">
|
||||
{$_("faq.search_results", {
|
||||
values: { count: filteredQuestions().length },
|
||||
})}
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
{#each filteredQuestions() as question (question.id)}
|
||||
<Card
|
||||
class="bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10"
|
||||
>
|
||||
{$_("faq.title")}
|
||||
</h1>
|
||||
<p class="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
|
||||
{$_("faq.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="max-w-2xl mx-auto mb-12">
|
||||
<div class="relative">
|
||||
<span
|
||||
class="icon-[ri--search-line] absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5"
|
||||
></span>
|
||||
<Input
|
||||
placeholder={$_("faq.search_placeholder")}
|
||||
bind:value={searchQuery}
|
||||
class="pl-12 h-14 text-lg bg-card/50 border-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if searchQuery.trim()}
|
||||
<!-- Search Results -->
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-2xl font-bold mb-6">
|
||||
{$_("faq.search_results", {
|
||||
values: { count: filteredQuestions().length },
|
||||
})}
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
{#each filteredQuestions() as question (question.id)}
|
||||
<Card
|
||||
class="bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10"
|
||||
>
|
||||
<CardContent class="p-0">
|
||||
<button
|
||||
onclick={() => toggleExpanded(question.id)}
|
||||
class="w-full p-6 text-left hover:bg-primary/5 transition-colors flex items-center justify-between"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm text-primary font-medium mb-1">
|
||||
{question.categoryTitle}
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg">{question.question}</h3>
|
||||
</div>
|
||||
{#if expandedItems.has(question.id)}
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-up-line] w-7 h-7 text-muted-foreground flex-shrink-0 ml-4"
|
||||
></span>
|
||||
{:else}
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-down-line] w-7 h-7 text-muted-foreground flex-shrink-0 ml-4"
|
||||
></span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if expandedItems.has(question.id)}
|
||||
<div class="p-6">
|
||||
<p class="text-muted-foreground leading-relaxed">
|
||||
{question.answer}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
{#if filteredQuestions.length === 0}
|
||||
<div class="text-center py-12">
|
||||
<p class="text-muted-foreground text-lg">{$_("faq.no_results")}</p>
|
||||
<Button variant="outline" onclick={() => (searchQuery = "")} class="mt-4"
|
||||
>{$_("faq.clear_search")}</Button
|
||||
>
|
||||
<CardContent class="p-0">
|
||||
<button
|
||||
onclick={() => toggleExpanded(question.id)}
|
||||
class="w-full p-6 text-left hover:bg-primary/5 transition-colors flex items-center justify-between"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm text-primary font-medium mb-1">
|
||||
{question.categoryTitle}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Category View -->
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{#each faqCategories as category (category.id)}
|
||||
<Card
|
||||
class="bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10"
|
||||
>
|
||||
<CardHeader class="pb-4">
|
||||
<CardTitle class="flex items-center gap-3 text-xl">
|
||||
<div
|
||||
class="w-10 h-10 bg-gradient-to-br from-primary/20 to-accent/20 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<span class={category.icon + " w-5 h-5 text-primary"}
|
||||
></span>
|
||||
</div>
|
||||
{category.title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="pt-0">
|
||||
<div class="space-y-3">
|
||||
{#each category.questions as question (question.id)}
|
||||
<div
|
||||
class="border border-border/50 rounded-lg overflow-hidden"
|
||||
>
|
||||
<button
|
||||
onclick={() => toggleExpanded(question.id)}
|
||||
class="w-full p-4 text-left hover:bg-primary/5 transition-colors flex items-center justify-between"
|
||||
>
|
||||
<h4 class="font-medium text-sm pr-4">
|
||||
{question.question}
|
||||
</h4>
|
||||
{#if expandedItems.has(question.id)}
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-up-line] w-6 h-6 text-muted-foreground flex-shrink-0"
|
||||
></span>
|
||||
{:else}
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-down-line] w-6 h-6 text-muted-foreground flex-shrink-0"
|
||||
></span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if expandedItems.has(question.id)}
|
||||
<div class="p-4 border-t border-border/50">
|
||||
<p
|
||||
class="text-muted-foreground text-sm leading-relaxed"
|
||||
>
|
||||
{question.answer}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Contact Support -->
|
||||
<div class="max-w-2xl mx-auto mt-16">
|
||||
<Card class="bg-gradient-to-br from-primary/10 to-accent/10 border-primary/20">
|
||||
<CardContent class="p-8 text-center">
|
||||
<h3 class="text-2xl font-bold mb-4">{$_("faq.support.title")}</h3>
|
||||
<p class="text-muted-foreground mb-6 leading-relaxed">
|
||||
{$_("faq.support.description")}
|
||||
<h3 class="font-semibold text-lg">{question.question}</h3>
|
||||
</div>
|
||||
{#if expandedItems.has(question.id)}
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-up-line] w-7 h-7 text-muted-foreground flex-shrink-0 ml-4"
|
||||
></span>
|
||||
{:else}
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-down-line] w-7 h-7 text-muted-foreground flex-shrink-0 ml-4"
|
||||
></span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if expandedItems.has(question.id)}
|
||||
<div class="p-6">
|
||||
<p class="text-muted-foreground leading-relaxed">
|
||||
{question.answer}
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
href="mailto:{$_('faq.support.contact_email')}"
|
||||
>{$_("faq.support.contact")}</Button
|
||||
>
|
||||
<!-- <Button
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
{#if filteredQuestions.length === 0}
|
||||
<div class="text-center py-12">
|
||||
<p class="text-muted-foreground text-lg">{$_("faq.no_results")}</p>
|
||||
<Button variant="outline" onclick={() => (searchQuery = "")} class="mt-4"
|
||||
>{$_("faq.clear_search")}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Category View -->
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{#each faqCategories as category (category.id)}
|
||||
<Card
|
||||
class="bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10"
|
||||
>
|
||||
<CardHeader class="pb-4">
|
||||
<CardTitle class="flex items-center gap-3 text-xl">
|
||||
<div
|
||||
class="w-10 h-10 bg-gradient-to-br from-primary/20 to-accent/20 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<span class={category.icon + " w-5 h-5 text-primary"}></span>
|
||||
</div>
|
||||
{category.title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="pt-0">
|
||||
<div class="space-y-3">
|
||||
{#each category.questions as question (question.id)}
|
||||
<div class="border border-border/50 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onclick={() => toggleExpanded(question.id)}
|
||||
class="w-full p-4 text-left hover:bg-primary/5 transition-colors flex items-center justify-between"
|
||||
>
|
||||
<h4 class="font-medium text-sm pr-4">
|
||||
{question.question}
|
||||
</h4>
|
||||
{#if expandedItems.has(question.id)}
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-up-line] w-6 h-6 text-muted-foreground flex-shrink-0"
|
||||
></span>
|
||||
{:else}
|
||||
<span
|
||||
class="icon-[ri--arrow-drop-down-line] w-6 h-6 text-muted-foreground flex-shrink-0"
|
||||
></span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if expandedItems.has(question.id)}
|
||||
<div class="p-4 border-t border-border/50">
|
||||
<p class="text-muted-foreground text-sm leading-relaxed">
|
||||
{question.answer}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Contact Support -->
|
||||
<div class="max-w-2xl mx-auto mt-16">
|
||||
<Card class="bg-gradient-to-br from-primary/10 to-accent/10 border-primary/20">
|
||||
<CardContent class="p-8 text-center">
|
||||
<h3 class="text-2xl font-bold mb-4">{$_("faq.support.title")}</h3>
|
||||
<p class="text-muted-foreground mb-6 leading-relaxed">
|
||||
{$_("faq.support.description")}
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
href="mailto:{$_('faq.support.contact_email')}">{$_("faq.support.contact")}</Button
|
||||
>
|
||||
<!-- <Button
|
||||
variant="outline"
|
||||
class="border-primary/50 hover:bg-primary/10"
|
||||
>{$_("faq.support.live_chat")}</Button
|
||||
> -->
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user