feat: add formidable ESLint + Prettier linting setup
- Root-level eslint.config.js (flat config): typescript-eslint,
eslint-plugin-svelte, eslint-config-prettier, @eslint/js
- Root-level prettier.config.js with prettier-plugin-svelte
- svelte-check added to frontend for Svelte/TS type checking
- lint, lint:fix, format, format:check, check scripts in root
and both packages
- All 60 lint errors fixed across backend and frontend:
- Consistent type imports
- Removed unused imports/variables
- Added keys to all {#each} blocks for Svelte performance
- Replaced mutable Set/Map with SvelteSet/SvelteMap
- Fixed useless assignments and empty catch blocks
- 64 remaining warnings are intentional any usages in the
Pothos/Drizzle GraphQL resolver layer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
build/
|
||||||
|
.svelte-kit/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
migrations/
|
||||||
|
pnpm-lock.yaml
|
||||||
177
CLAUDE.md
177
CLAUDE.md
@@ -1,177 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
This is a monorepo for an adult content platform built with SvelteKit, Directus CMS, and hardware integration via Buttplug.io. The project uses pnpm workspaces with three main packages.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
1. Install Node.js 20.19.1
|
|
||||||
2. Enable corepack: `corepack enable`
|
|
||||||
3. Install dependencies: `pnpm install`
|
|
||||||
4. Install Rust toolchain and wasm-bindgen: `cargo install wasm-bindgen-cli`
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
### Packages
|
|
||||||
|
|
||||||
- **`packages/frontend`**: SvelteKit application (main frontend)
|
|
||||||
- **`packages/bundle`**: Directus extension bundle (custom endpoints, hooks, themes)
|
|
||||||
- **`packages/buttplug`**: Hardware control library with TypeScript/WebAssembly bindings
|
|
||||||
|
|
||||||
### Frontend (SvelteKit + Tailwind CSS 4)
|
|
||||||
|
|
||||||
- **Framework**: SvelteKit 2 with adapter-node
|
|
||||||
- **Styling**: Tailwind CSS v4 via @tailwindcss/vite
|
|
||||||
- **UI Components**: bits-ui, custom components in `src/lib/components/ui/`
|
|
||||||
- **Backend**: Directus headless CMS
|
|
||||||
- **Routes**: File-based routing in `src/routes/`
|
|
||||||
- `+page.server.ts`: Server-side data loading
|
|
||||||
- `+layout.server.ts`: Layout data (authentication, etc.)
|
|
||||||
- **Authentication**: Session-based via Directus SDK (cookies)
|
|
||||||
- **API Proxy**: Dev server proxies `/api` to `http://localhost:8055` (Directus)
|
|
||||||
- **i18n**: svelte-i18n for internationalization
|
|
||||||
|
|
||||||
Key files:
|
|
||||||
- `src/lib/directus.ts`: Directus client configuration
|
|
||||||
- `src/lib/types.ts`: Shared TypeScript types
|
|
||||||
- `src/hooks.server.ts`: Server-side auth middleware
|
|
||||||
- `vite.config.ts`: Dev server on port 3000 with API proxy
|
|
||||||
|
|
||||||
### Bundle (Directus Extensions)
|
|
||||||
|
|
||||||
Custom Directus extensions providing:
|
|
||||||
- **Endpoint** (`src/endpoint/index.ts`): `/sexy/stats` endpoint for platform statistics
|
|
||||||
- **Hook** (`src/hook/index.ts`):
|
|
||||||
- Auto-generates slugs for users based on artist_name
|
|
||||||
- Processes uploaded videos with ffmpeg to extract duration
|
|
||||||
- **Theme** (`src/theme/index.ts`): Custom Directus admin theme
|
|
||||||
|
|
||||||
### Buttplug (Hardware Control)
|
|
||||||
|
|
||||||
Hybrid TypeScript/Rust package for intimate hardware control:
|
|
||||||
- **TypeScript**: Client library, connectors (WebSocket, Browser WebSocket)
|
|
||||||
- **Rust/WASM**: Core buttplug implementation compiled to WebAssembly
|
|
||||||
- Provides browser-based Bluetooth device control via WebBluetooth API
|
|
||||||
|
|
||||||
Key concepts:
|
|
||||||
- `ButtplugClient`: Main client interface
|
|
||||||
- `ButtplugClientDevice`: Device abstraction
|
|
||||||
- `ButtplugWasmClientConnector`: WASM-based connector
|
|
||||||
- Messages defined in `src/core/Messages.ts`
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
Start full development environment (data + Directus + frontend):
|
|
||||||
```bash
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Individual services:
|
|
||||||
```bash
|
|
||||||
pnpm dev:data # Start Docker Compose data services
|
|
||||||
pnpm dev:directus # Start Directus in Docker
|
|
||||||
pnpm --filter @sexy.pivoine.art/frontend dev # Frontend dev server only
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
Build all packages:
|
|
||||||
```bash
|
|
||||||
pnpm install # Ensure dependencies are installed first
|
|
||||||
```
|
|
||||||
|
|
||||||
Build specific packages:
|
|
||||||
```bash
|
|
||||||
pnpm build:frontend # Pulls git, installs, builds frontend
|
|
||||||
pnpm build:bundle # Pulls git, installs, builds Directus extensions
|
|
||||||
```
|
|
||||||
|
|
||||||
Individual package builds:
|
|
||||||
```bash
|
|
||||||
pnpm --filter @sexy.pivoine.art/frontend build
|
|
||||||
pnpm --filter @sexy.pivoine.art/bundle build
|
|
||||||
pnpm --filter @sexy.pivoine.art/buttplug build # TypeScript build
|
|
||||||
pnpm --filter @sexy.pivoine.art/buttplug build:wasm # Rust WASM build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
|
||||||
|
|
||||||
Start production frontend server (local):
|
|
||||||
```bash
|
|
||||||
pnpm --filter @sexy.pivoine.art/frontend start
|
|
||||||
```
|
|
||||||
|
|
||||||
Docker Compose deployment (recommended for production):
|
|
||||||
```bash
|
|
||||||
# Local development (with Postgres, Redis, Directus)
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Production (with Traefik, external DB, Redis)
|
|
||||||
docker-compose -f compose.production.yml --env-file .env.production up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
See `COMPOSE.md` for Docker Compose guide and `DOCKER.md` for standalone Docker deployment.
|
|
||||||
|
|
||||||
## Architecture Notes
|
|
||||||
|
|
||||||
### Data Flow
|
|
||||||
|
|
||||||
1. **Frontend** → `/api/*` (proxied) → **Directus CMS**
|
|
||||||
2. Directus uses **bundle extensions** for custom logic (stats, video processing, user management)
|
|
||||||
3. Frontend uses **Directus SDK** with session authentication
|
|
||||||
4. Hardware control uses **buttplug package** (TypeScript → WASM → Bluetooth)
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
- Session tokens stored in `directus_session_token` cookie
|
|
||||||
- `hooks.server.ts` validates token on every request via `isAuthenticated()`
|
|
||||||
- User roles: Model, Viewer (checked via role or policy)
|
|
||||||
- `isModel()` helper in `src/lib/directus.ts` checks user permissions
|
|
||||||
|
|
||||||
### Content Types
|
|
||||||
|
|
||||||
Core types in `packages/frontend/src/lib/types.ts`:
|
|
||||||
- **User/CurrentUser**: User profiles with roles and policies
|
|
||||||
- **Video**: Videos with models, tags, premium flag
|
|
||||||
- **Model**: Creator profiles with photos and banner
|
|
||||||
- **Article**: Magazine/blog content
|
|
||||||
- **BluetoothDevice**: Hardware device state
|
|
||||||
|
|
||||||
### Docker Environment
|
|
||||||
|
|
||||||
Development uses Docker Compose in `../compose/` directory:
|
|
||||||
- `../compose/data`: Database/storage services
|
|
||||||
- `../compose/sexy`: Directus instance (uses `.env.local`)
|
|
||||||
|
|
||||||
### Asset URLs
|
|
||||||
|
|
||||||
Assets served via Directus with transforms:
|
|
||||||
```typescript
|
|
||||||
getAssetUrl(id, "thumbnail" | "preview" | "medium" | "banner")
|
|
||||||
// Returns: ${directusApiUrl}/assets/${id}?transform=...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
1. Ensure Docker services are running: `pnpm dev:data && pnpm dev:directus`
|
|
||||||
2. Start frontend dev server: `pnpm --filter @sexy.pivoine.art/frontend dev`
|
|
||||||
3. Access frontend at `http://localhost:3000`
|
|
||||||
4. Access Directus admin at `http://localhost:8055`
|
|
||||||
|
|
||||||
When modifying:
|
|
||||||
- **Frontend code**: Hot reload via Vite
|
|
||||||
- **Bundle extensions**: Rebuild with `pnpm --filter @sexy.pivoine.art/bundle build` and restart Directus
|
|
||||||
- **Buttplug library**: Rebuild TypeScript (`pnpm build`) and/or WASM (`pnpm build:wasm`)
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- This is a pnpm workspace; always use `pnpm` not `npm` or `yarn`
|
|
||||||
- Package manager is locked to `pnpm@10.17.0`
|
|
||||||
- Buttplug package requires Rust toolchain for WASM builds
|
|
||||||
- Frontend uses SvelteKit's adapter-node for production deployment
|
|
||||||
- All TypeScript packages use ES modules (`"type": "module"`)
|
|
||||||
2114
directus.yml
2114
directus.yml
File diff suppressed because it is too large
Load Diff
59
eslint.config.js
Normal file
59
eslint.config.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import ts from "typescript-eslint";
|
||||||
|
import svelte from "eslint-plugin-svelte";
|
||||||
|
import prettier from "eslint-config-prettier";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
|
export default ts.config(
|
||||||
|
js.configs.recommended,
|
||||||
|
...ts.configs.recommended,
|
||||||
|
...svelte.configs["flat/recommended"],
|
||||||
|
prettier,
|
||||||
|
...svelte.configs["flat/prettier"],
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.svelte"],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: ts.parser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// Allow unused vars prefixed with _ (common pattern for intentional ignores)
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||||
|
],
|
||||||
|
// Allow explicit any sparingly — we're adults here
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
// Enforce consistent type imports
|
||||||
|
"@typescript-eslint/consistent-type-imports": [
|
||||||
|
"error",
|
||||||
|
{ prefer: "type-imports", fixStyle: "inline-type-imports" },
|
||||||
|
],
|
||||||
|
// This rule is meant for onNavigate() callbacks only; standard SvelteKit href/goto is fine
|
||||||
|
"svelte/no-navigation-without-resolve": "off",
|
||||||
|
// {@html} is used intentionally for trusted content (e.g. legal page)
|
||||||
|
"svelte/no-at-html-tags": "warn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"**/build/",
|
||||||
|
"**/.svelte-kit/",
|
||||||
|
"**/dist/",
|
||||||
|
"**/node_modules/",
|
||||||
|
"**/migrations/",
|
||||||
|
"packages/buttplug/**",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
18
package.json
18
package.json
@@ -2,14 +2,19 @@
|
|||||||
"name": "sexy.pivoine.art",
|
"name": "sexy.pivoine.art",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build:frontend": "git pull && pnpm install && pnpm --filter @sexy.pivoine.art/frontend build",
|
"build:frontend": "git pull && pnpm install && pnpm --filter @sexy.pivoine.art/frontend build",
|
||||||
"build:backend": "git pull && pnpm install && pnpm --filter @sexy.pivoine.art/backend build",
|
"build:backend": "git pull && pnpm install && pnpm --filter @sexy.pivoine.art/backend build",
|
||||||
"dev:data": "docker compose up -d postgres redis",
|
"dev:data": "docker compose up -d postgres redis",
|
||||||
"dev:backend": "pnpm --filter @sexy.pivoine.art/backend dev",
|
"dev:backend": "pnpm --filter @sexy.pivoine.art/backend dev",
|
||||||
"dev": "pnpm dev:data && pnpm dev:backend & pnpm --filter @sexy.pivoine.art/frontend dev"
|
"dev": "pnpm dev:data && pnpm dev:backend & pnpm --filter @sexy.pivoine.art/frontend dev",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"check": "pnpm -r --filter=!sexy.pivoine.art check"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": {
|
"author": {
|
||||||
@@ -30,5 +35,14 @@
|
|||||||
"@tailwindcss/oxide",
|
"@tailwindcss/oxide",
|
||||||
"node-sass"
|
"node-sass"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"eslint": "^10.0.2",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-svelte": "^3.15.0",
|
||||||
|
"globals": "^17.4.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"typescript-eslint": "^8.56.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,15 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/index.ts",
|
"dev": "UPLOAD_DIR=/home/valknar/sexy-uploads DATABASE_URL=postgresql://sexy:sexy@localhost:5432/sexy REDIS_URL=redis://localhost:6379 tsx watch src/index.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"schema:migrate": "tsx src/scripts/migrate.ts",
|
"schema:migrate": "tsx src/scripts/migrate.ts",
|
||||||
"migrate": "tsx src/scripts/data-migration.ts"
|
"migrate": "tsx src/scripts/data-migration.ts",
|
||||||
|
"check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Fastify, { FastifyRequest, FastifyReply } from "fastify";
|
import Fastify, { type FastifyRequest, type FastifyReply } from "fastify";
|
||||||
import fastifyCookie from "@fastify/cookie";
|
import fastifyCookie from "@fastify/cookie";
|
||||||
import fastifyCors from "@fastify/cors";
|
import fastifyCors from "@fastify/cors";
|
||||||
import fastifyMultipart from "@fastify/multipart";
|
import fastifyMultipart from "@fastify/multipart";
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export async function updateUserStats(db: DB, userId: string): Promise<void> {
|
|||||||
.where(eq(recordings.user_id, userId));
|
.where(eq(recordings.user_id, userId));
|
||||||
const ownIds = ownRecordingIds.map((r) => r.id);
|
const ownIds = ownRecordingIds.map((r) => r.id);
|
||||||
|
|
||||||
let playbacksCount = 0;
|
let playbacksCount: number;
|
||||||
if (ownIds.length > 0) {
|
if (ownIds.length > 0) {
|
||||||
const playbacksResult = await db.execute(sql`
|
const playbacksResult = await db.execute(sql`
|
||||||
SELECT COUNT(*) as count FROM recording_plays
|
SELECT COUNT(*) as count FROM recording_plays
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ async function migrateUsers() {
|
|||||||
? tagsRes.rows[0].tags
|
? tagsRes.rows[0].tags
|
||||||
: JSON.parse(String(tagsRes.rows[0].tags || "[]"));
|
: JSON.parse(String(tagsRes.rows[0].tags || "[]"));
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* tags column may not exist on older Directus installs */ }
|
||||||
|
|
||||||
await query(
|
await query(
|
||||||
`INSERT INTO users (id, email, password_hash, first_name, last_name, artist_name, slug,
|
`INSERT INTO users (id, email, password_hash, first_name, last_name, artist_name, slug,
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "node ./build"
|
"start": "node ./build",
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.json --threshold warning"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/ri": "^1.2.10",
|
"@iconify-json/ri": "^1.2.10",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"prettier-plugin-svelte": "^3.5.1",
|
"prettier-plugin-svelte": "^3.5.1",
|
||||||
"super-sitemap": "^1.0.7",
|
"super-sitemap": "^1.0.7",
|
||||||
"svelte": "^5.53.7",
|
"svelte": "^5.53.7",
|
||||||
|
"svelte-check": "^4.4.4",
|
||||||
"svelte-sonner": "^1.0.8",
|
"svelte-sonner": "^1.0.8",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwind-variants": "^3.2.2",
|
"tailwind-variants": "^3.2.2",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
@custom-variant hover (&:hover);
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--animate-vibrate: vibrate 0.3s linear infinite;
|
--animate-vibrate: vibrate 0.3s linear infinite;
|
||||||
--animate-fade-in: fadeIn 0.3s ease-out;
|
--animate-fade-in: fadeIn 0.3s ease-out;
|
||||||
|
|||||||
@@ -2,12 +2,8 @@ import { isAuthenticated } from "$lib/services";
|
|||||||
import { logger, generateRequestId } from "$lib/logger";
|
import { logger, generateRequestId } from "$lib/logger";
|
||||||
import type { Handle } from "@sveltejs/kit";
|
import type { Handle } from "@sveltejs/kit";
|
||||||
|
|
||||||
// Log startup info once
|
// Log startup info once (module-level code runs exactly once on import)
|
||||||
let hasLoggedStartup = false;
|
logger.startup();
|
||||||
if (!hasLoggedStartup) {
|
|
||||||
logger.startup();
|
|
||||||
hasLoggedStartup = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
const { cookies, locals, url, request } = event;
|
const { cookies, locals, url, request } = event;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ function isActive() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3
|
<h3
|
||||||
class={`font-semibold text-card-foreground group-hover:text-primary transition-colors`}
|
class="font-semibold text-card-foreground group-hover:text-primary transition-colors"
|
||||||
>
|
>
|
||||||
{device.name}
|
{device.name}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -139,7 +139,7 @@ function isActive() {
|
|||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
<!-- Action Button -->
|
<!-- Action Button -->
|
||||||
{#each device.actuators as actuator, idx}
|
{#each device.actuators as actuator, idx (idx)}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for={`device-${device.info.index}-${actuator.featureIndex}-${actuator.outputType}`}
|
<Label for={`device-${device.info.index}-${actuator.featureIndex}-${actuator.outputType}`}
|
||||||
>{$_(
|
>{$_(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import PeonyIcon from "$lib/components/icon/peony-icon.svelte";
|
|
||||||
import Logo from "../logo/logo.svelte";
|
import Logo from "../logo/logo.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import PeonyIcon from "$lib/components/icon/peony-icon.svelte";
|
|
||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from "$lib/components/ui/button";
|
||||||
import type { AuthStatus } from "$lib/types";
|
import type { AuthStatus } from "$lib/types";
|
||||||
import { logout } from "$lib/services";
|
import { logout } from "$lib/services";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { getAssetUrl, isModel } from "$lib/directus";
|
import { getAssetUrl } from "$lib/directus";
|
||||||
import LogoutButton from "../logout-button/logout-button.svelte";
|
import LogoutButton from "../logout-button/logout-button.svelte";
|
||||||
import Separator from "../ui/separator/separator.svelte";
|
import Separator from "../ui/separator/separator.svelte";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "$lib/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "$lib/components/ui/avatar";
|
||||||
@@ -64,7 +63,7 @@ function isActiveLink(link: any) {
|
|||||||
|
|
||||||
<!-- Desktop Navigation -->
|
<!-- Desktop Navigation -->
|
||||||
<nav class="hidden w-full lg:flex items-center justify-center gap-8">
|
<nav class="hidden w-full lg:flex items-center justify-center gap-8">
|
||||||
{#each navLinks as link}
|
{#each navLinks as link (link.href)}
|
||||||
<a
|
<a
|
||||||
href={link.href}
|
href={link.href}
|
||||||
class={`text-sm hover:text-foreground transition-colors duration-200 font-medium relative group ${
|
class={`text-sm hover:text-foreground transition-colors duration-200 font-medium relative group ${
|
||||||
@@ -219,7 +218,7 @@ function isActiveLink(link: any) {
|
|||||||
{$_('header.navigation')}
|
{$_('header.navigation')}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
{#each navLinks as link}
|
{#each navLinks as link (link.href)}
|
||||||
<a
|
<a
|
||||||
href={link.href}
|
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(
|
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(
|
||||||
@@ -259,7 +258,7 @@ function isActiveLink(link: any) {
|
|||||||
onclick={closeMenu}
|
onclick={closeMenu}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={`flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-muted to-muted/50 transition-all group-hover:bg-card group-hover:from-primary/10 group-hover:to-accent/10`}
|
class="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-muted to-muted/50 transition-all group-hover:bg-card group-hover:from-primary/10 group-hover:to-accent/10"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="icon-[ri--dashboard-2-line] h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors"
|
class="icon-[ri--dashboard-2-line] h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors"
|
||||||
@@ -286,7 +285,7 @@ function isActiveLink(link: any) {
|
|||||||
onclick={closeMenu}
|
onclick={closeMenu}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={`flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-muted to-muted/50 transition-all group-hover:bg-card group-hover:from-primary/10 group-hover:to-accent/10`}
|
class="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-muted to-muted/50 transition-all group-hover:bg-card group-hover:from-primary/10 group-hover:to-accent/10"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="icon-[ri--rocket-line] h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors"
|
class="icon-[ri--rocket-line] h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors"
|
||||||
@@ -313,7 +312,7 @@ function isActiveLink(link: any) {
|
|||||||
onclick={closeMenu}
|
onclick={closeMenu}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={`flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-muted to-muted/50 transition-all group-hover:bg-card group-hover:from-primary/10 group-hover:to-accent/10`}
|
class="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-muted to-muted/50 transition-all group-hover:bg-card group-hover:from-primary/10 group-hover:to-accent/10"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="icon-[ri--login-circle-line] h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors"
|
class="icon-[ri--login-circle-line] h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors"
|
||||||
@@ -340,7 +339,7 @@ function isActiveLink(link: any) {
|
|||||||
onclick={closeMenu}
|
onclick={closeMenu}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={`flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-muted to-muted/50 transition-all group-hover:bg-card group-hover:from-primary/10 group-hover:to-accent/10`}
|
class="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-muted to-muted/50 transition-all group-hover:bg-card group-hover:from-primary/10 group-hover:to-accent/10"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="icon-[ri--heart-add-2-line] h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors"
|
class="icon-[ri--heart-add-2-line] h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors"
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ onDestroy(() => {
|
|||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 animate-fade-in"
|
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}
|
{#each images as image, index (index)}
|
||||||
<button
|
<button
|
||||||
onclick={() => openViewer(index)}
|
onclick={() => openViewer(index)}
|
||||||
class="group relative aspect-square overflow-hidden rounded-xl bg-zinc-900 border border-zinc-800 transition-all duration-300 hover:scale-[1.03] hover:border-primary/50 hover:shadow-2xl hover:shadow-primary/20 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-zinc-950"
|
class="group relative aspect-square overflow-hidden rounded-xl bg-zinc-900 border border-zinc-800 transition-all duration-300 hover:scale-[1.03] hover:border-primary/50 hover:shadow-2xl hover:shadow-primary/20 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-zinc-950"
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ function getStatusColor(status: string): string {
|
|||||||
|
|
||||||
<!-- Device Info -->
|
<!-- Device Info -->
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
{#each recording.device_info.slice(0, 2) as device}
|
{#each recording.device_info.slice(0, 2) as device (device.name)}
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 text-xs text-muted-foreground bg-muted/20 rounded px-2 py-1"
|
class="flex items-center gap-2 text-xs text-muted-foreground bg-muted/20 rounded px-2 py-1"
|
||||||
>
|
>
|
||||||
@@ -117,7 +117,7 @@ function getStatusColor(status: string): string {
|
|||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
{#if recording.tags && recording.tags.length > 0}
|
{#if recording.tags && recording.tags.length > 0}
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
{#each recording.tags as tag}
|
{#each recording.tags as tag (tag)}
|
||||||
<span
|
<span
|
||||||
class="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20"
|
class="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from "$lib/components/ui/button";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onclick: () => void;
|
onclick: () => void;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const copyLink = async () => {
|
|||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(content.url);
|
await navigator.clipboard.writeText(content.url);
|
||||||
toast.success($_("sharing_popup.success.copy"));
|
toast.success($_("sharing_popup.success.copy"));
|
||||||
} catch (err) {
|
} catch {
|
||||||
// Fallback for older browsers
|
// Fallback for older browsers
|
||||||
const textArea = document.createElement("textarea");
|
const textArea = document.createElement("textarea");
|
||||||
textArea.value = content.url;
|
textArea.value = content.url;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<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 =
|
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps =
|
||||||
$props();
|
$props();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ButtplugClientDevice } from "@sexy.pivoine.art/buttplug";
|
import { type ButtplugClientDevice } from "@sexy.pivoine.art/buttplug";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { browser } from "$app/environment";
|
|
||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const { data } = $props();
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-3xl mx-auto">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-3xl mx-auto">
|
||||||
{#each data.models as model}
|
{#each data.models as model (model.slug)}
|
||||||
<Card
|
<Card
|
||||||
class="p-0 group hover:shadow-2xl hover:shadow-primary/20 transition-all duration-300 hover:-translate-y-2 bg-gradient-to-br from-card to-card/50 border-primary/20"
|
class="p-0 group hover:shadow-2xl hover:shadow-primary/20 transition-all duration-300 hover:-translate-y-2 bg-gradient-to-br from-card to-card/50 border-primary/20"
|
||||||
>
|
>
|
||||||
@@ -128,7 +128,7 @@ const { data } = $props();
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-6xl mx-auto">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-6xl mx-auto">
|
||||||
{#each data.videos as video}
|
{#each data.videos as video (video.slug)}
|
||||||
<Card
|
<Card
|
||||||
class="p-0 group hover:shadow-2xl hover:shadow-accent/20 transition-all duration-300 hover:-translate-y-2 bg-gradient-to-br from-card to-card/50 border-accent/20 overflow-hidden"
|
class="p-0 group hover:shadow-2xl hover:shadow-accent/20 transition-all duration-300 hover:-translate-y-2 bg-gradient-to-br from-card to-card/50 border-accent/20 overflow-hidden"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const values = [
|
|||||||
<section class="py-16 bg-card/30">
|
<section class="py-16 bg-card/30">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||||
{#each stats as stat}
|
{#each stats as stat (stat.icon)}
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div
|
<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"
|
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"
|
||||||
@@ -176,7 +176,7 @@ const values = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||||
{#each values as value}
|
{#each values as value (value.title)}
|
||||||
<Card
|
<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"
|
class="bg-gradient-to-br from-card to-card/50 border-primary/20 hover:shadow-xl hover:shadow-primary/10 transition-all duration-300"
|
||||||
>
|
>
|
||||||
@@ -214,7 +214,7 @@ const values = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
{#each team as member}
|
{#each team as member (member.name)}
|
||||||
<Card
|
<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"
|
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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
import { SvelteSet } from "svelte/reactivity";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -12,7 +13,7 @@ import PeonyBackground from "$lib/components/background/peony-background.svelte"
|
|||||||
import Meta from "$lib/components/meta/meta.svelte";
|
import Meta from "$lib/components/meta/meta.svelte";
|
||||||
|
|
||||||
let searchQuery = $state("");
|
let searchQuery = $state("");
|
||||||
let expandedItems = $state<Set<number>>(new Set());
|
let expandedItems = new SvelteSet<number>();
|
||||||
|
|
||||||
const faqCategories = [
|
const faqCategories = [
|
||||||
{
|
{
|
||||||
@@ -171,7 +172,7 @@ const filteredQuestions = $derived(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function toggleExpanded(id: number) {
|
function toggleExpanded(id: number) {
|
||||||
const newExpanded = new Set(expandedItems);
|
const newExpanded = new SvelteSet(expandedItems);
|
||||||
if (newExpanded.has(id)) {
|
if (newExpanded.has(id)) {
|
||||||
newExpanded.delete(id);
|
newExpanded.delete(id);
|
||||||
} else {
|
} else {
|
||||||
@@ -224,7 +225,7 @@ function toggleExpanded(id: number) {
|
|||||||
})}
|
})}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{#each filteredQuestions() as question}
|
{#each filteredQuestions() as question (question.id)}
|
||||||
<Card
|
<Card
|
||||||
class="bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10"
|
class="bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10"
|
||||||
>
|
>
|
||||||
@@ -273,7 +274,7 @@ function toggleExpanded(id: number) {
|
|||||||
<!-- Category View -->
|
<!-- Category View -->
|
||||||
<div class="max-w-6xl mx-auto">
|
<div class="max-w-6xl mx-auto">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
{#each faqCategories as category}
|
{#each faqCategories as category (category.id)}
|
||||||
<Card
|
<Card
|
||||||
class="bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10"
|
class="bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10"
|
||||||
>
|
>
|
||||||
@@ -290,7 +291,7 @@ function toggleExpanded(id: number) {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="pt-0">
|
<CardContent class="pt-0">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
{#each category.questions as question}
|
{#each category.questions as question (question.id)}
|
||||||
<div
|
<div
|
||||||
class="border border-border/50 rounded-lg overflow-hidden"
|
class="border border-border/50 rounded-lg overflow-hidden"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import {
|
|||||||
} from "$lib/components/ui/card";
|
} from "$lib/components/ui/card";
|
||||||
import { Input } from "$lib/components/ui/input";
|
import { Input } from "$lib/components/ui/input";
|
||||||
import { Label } from "$lib/components/ui/label";
|
import { Label } from "$lib/components/ui/label";
|
||||||
import { Checkbox } from "$lib/components/ui/checkbox";
|
|
||||||
import PeonyIcon from "$lib/components/icon/peony-icon.svelte";
|
|
||||||
import * as Alert from "$lib/components/ui/alert";
|
import * as Alert from "$lib/components/ui/alert";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { login } from "$lib/services";
|
import { login } from "$lib/services";
|
||||||
@@ -24,7 +22,7 @@ let email = $state("");
|
|||||||
let password = $state("");
|
let password = $state("");
|
||||||
let error = $state("");
|
let error = $state("");
|
||||||
let showPassword = $state(false);
|
let showPassword = $state(false);
|
||||||
let rememberMe = $state(false);
|
let _rememberMe = $state(false);
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
let isError = $state(false);
|
let isError = $state(false);
|
||||||
|
|
||||||
@@ -212,7 +210,7 @@ onMount(() => {
|
|||||||
<!-- Sign Up Link -->
|
<!-- Sign Up Link -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{$_("auth.login.no_account")}{" "}
|
{$_("auth.login.no_account")}
|
||||||
<a href="/signup" class="text-primary hover:underline font-medium"
|
<a href="/signup" class="text-primary hover:underline font-medium"
|
||||||
>{$_("auth.login.sign_up_link")}</a
|
>{$_("auth.login.sign_up_link")}</a
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ const filteredArticles = $derived(() => {
|
|||||||
|
|
||||||
<!-- Articles Grid -->
|
<!-- Articles Grid -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{#each filteredArticles() as article}
|
{#each filteredArticles() as article (article.slug)}
|
||||||
<Card
|
<Card
|
||||||
class="p-0 group hover:shadow-2xl hover:shadow-primary/25 transition-all duration-300 hover:-translate-y-3 bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10 overflow-hidden"
|
class="p-0 group hover:shadow-2xl hover:shadow-primary/25 transition-all duration-300 hover:-translate-y-3 bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10 overflow-hidden"
|
||||||
>
|
>
|
||||||
@@ -316,7 +316,7 @@ const filteredArticles = $derived(() => {
|
|||||||
|
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<div class="flex flex-wrap gap-2 mb-4">
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
{#each article.tags.slice(0, 3) as tag}
|
{#each article.tags.slice(0, 3) as tag (tag)}
|
||||||
<a
|
<a
|
||||||
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
|
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
|
||||||
href="/tags/{tag}"
|
href="/tags/{tag}"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Card, CardContent } from "$lib/components/ui/card";
|
|||||||
import { calcReadingTime } from "$lib/utils";
|
import { calcReadingTime } from "$lib/utils";
|
||||||
import TimeAgo from "javascript-time-ago";
|
import TimeAgo from "javascript-time-ago";
|
||||||
import { getAssetUrl } from "$lib/directus";
|
import { getAssetUrl } from "$lib/directus";
|
||||||
import SharingPopup from "$lib/components/sharing-popup/sharing-popup.svelte";
|
|
||||||
import Meta from "$lib/components/meta/meta.svelte";
|
import Meta from "$lib/components/meta/meta.svelte";
|
||||||
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
||||||
import SharingPopupButton from "$lib/components/sharing-popup/sharing-popup-button.svelte";
|
import SharingPopupButton from "$lib/components/sharing-popup/sharing-popup-button.svelte";
|
||||||
@@ -129,7 +128,7 @@ const timeAgo = new TimeAgo("en");
|
|||||||
<span class="font-semibold">Tags</span>
|
<span class="font-semibold">Tags</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
{#each data.article.tags as tag}
|
{#each data.article.tags as tag (tag)}
|
||||||
<a class="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm" href="/tags/{tag}">
|
<a class="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm" href="/tags/{tag}">
|
||||||
#{tag}
|
#{tag}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import {
|
|||||||
FileDropZone,
|
FileDropZone,
|
||||||
MEGABYTE,
|
MEGABYTE,
|
||||||
} from "$lib/components/ui/file-drop-zone";
|
} from "$lib/components/ui/file-drop-zone";
|
||||||
import * as Avatar from "$lib/components/ui/avatar";
|
|
||||||
import RecordingCard from "$lib/components/recording-card/recording-card.svelte";
|
import RecordingCard from "$lib/components/recording-card/recording-card.svelte";
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
@@ -175,7 +174,7 @@ async function handleDeleteRecording(id: string) {
|
|||||||
await deleteRecording(id);
|
await deleteRecording(id);
|
||||||
recordings = recordings.filter((r) => r.id !== id);
|
recordings = recordings.filter((r) => r.id !== id);
|
||||||
toast.success($_("me.recordings.delete_success"));
|
toast.success($_("me.recordings.delete_success"));
|
||||||
} catch (error) {
|
} catch {
|
||||||
toast.error($_("me.recordings.delete_error"));
|
toast.error($_("me.recordings.delete_error"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -625,7 +624,7 @@ onMount(() => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each data.analytics.videos as video}
|
{#each data.analytics.videos as video (video.slug)}
|
||||||
<tr class="border-b border-border/50 hover:bg-primary/5 transition-colors">
|
<tr class="border-b border-border/50 hover:bg-primary/5 transition-colors">
|
||||||
<td class="p-3">
|
<td class="p-3">
|
||||||
<a href="/videos/{video.slug}" class="hover:text-primary transition-colors">
|
<a href="/videos/{video.slug}" class="hover:text-primary transition-colors">
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ const filteredModels = $derived(() => {
|
|||||||
<!-- Models Grid -->
|
<!-- Models Grid -->
|
||||||
<div class="container mx-auto px-4 py-12">
|
<div class="container mx-auto px-4 py-12">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{#each filteredModels() as model}
|
{#each filteredModels() as model (model.slug)}
|
||||||
<Card
|
<Card
|
||||||
class="py-0 group hover:shadow-2xl hover:shadow-primary/25 transition-all duration-500 hover:-translate-y-3 bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10 overflow-hidden"
|
class="py-0 group hover:shadow-2xl hover:shadow-primary/25 transition-all duration-500 hover:-translate-y-3 bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10 overflow-hidden"
|
||||||
>
|
>
|
||||||
@@ -211,7 +211,7 @@ const filteredModels = $derived(() => {
|
|||||||
|
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<div class="flex flex-wrap gap-2 mb-4">
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
{#each model.tags as tag}
|
{#each model.tags as tag (tag)}
|
||||||
<a
|
<a
|
||||||
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
|
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
|
||||||
href="/tags/{tag}"
|
href="/tags/{tag}"
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ let totalPlays = $derived(
|
|||||||
|
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
{#each data.model.tags as tag}
|
{#each data.model.tags as tag (tag)}
|
||||||
<a
|
<a
|
||||||
class="text-xs bg-primary/10 text-primary px-3 py-1 rounded-full"
|
class="text-xs bg-primary/10 text-primary px-3 py-1 rounded-full"
|
||||||
href="/tags/{tag}"
|
href="/tags/{tag}"
|
||||||
@@ -227,7 +227,7 @@ let totalPlays = $derived(
|
|||||||
|
|
||||||
<TabsContent value="videos">
|
<TabsContent value="videos">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{#each data.videos as video}
|
{#each data.videos as video (video.slug)}
|
||||||
<Card
|
<Card
|
||||||
class="p-0 group hover:shadow-2xl hover:shadow-primary/20 transition-all duration-300 hover:-translate-y-2 bg-gradient-to-br from-card to-card/50 border-primary/20 overflow-hidden"
|
class="p-0 group hover:shadow-2xl hover:shadow-primary/20 transition-all duration-300 hover:-translate-y-2 bg-gradient-to-br from-card to-card/50 border-primary/20 overflow-hidden"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ import {
|
|||||||
} from "$lib/components/ui/card";
|
} from "$lib/components/ui/card";
|
||||||
import { Input } from "$lib/components/ui/input";
|
import { Input } from "$lib/components/ui/input";
|
||||||
import { Label } from "$lib/components/ui/label";
|
import { Label } from "$lib/components/ui/label";
|
||||||
import PeonyIcon from "$lib/components/icon/peony-icon.svelte";
|
|
||||||
import * as Alert from "$lib/components/ui/alert";
|
import * as Alert from "$lib/components/ui/alert";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { login, requestPassword } from "$lib/services";
|
import { requestPassword } from "$lib/services";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from "$lib/components/ui/card";
|
} from "$lib/components/ui/card";
|
||||||
import { Input } from "$lib/components/ui/input";
|
import { Input } from "$lib/components/ui/input";
|
||||||
import { Label } from "$lib/components/ui/label";
|
import { Label } from "$lib/components/ui/label";
|
||||||
import PeonyIcon from "$lib/components/icon/peony-icon.svelte";
|
|
||||||
import * as Alert from "$lib/components/ui/alert";
|
import * as Alert from "$lib/components/ui/alert";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { resetPassword } from "$lib/services";
|
import { resetPassword } from "$lib/services";
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import Meta from "$lib/components/meta/meta.svelte";
|
|||||||
import {
|
import {
|
||||||
ButtplugClient,
|
ButtplugClient,
|
||||||
ButtplugWasmClientConnector,
|
ButtplugWasmClientConnector,
|
||||||
ButtplugClientDevice,
|
type ButtplugClientDevice,
|
||||||
OutputType,
|
type OutputType,
|
||||||
InputType,
|
InputType,
|
||||||
DeviceOutputValueConstructor,
|
DeviceOutputValueConstructor,
|
||||||
} from "@sexy.pivoine.art/buttplug";
|
} from "@sexy.pivoine.art/buttplug";
|
||||||
@@ -149,7 +149,7 @@ async function handleStop(device: BluetoothDevice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function convertDevice(device: ButtplugClientDevice): BluetoothDevice {
|
function convertDevice(device: ButtplugClientDevice): BluetoothDevice {
|
||||||
const actuators: import("$lib/types").DeviceActuator[] = [];
|
const actuators: import("$lib/types").DeviceActuator[] = []; // eslint-disable-line @typescript-eslint/consistent-type-imports
|
||||||
for (const [, feature] of device.features) {
|
for (const [, feature] of device.features) {
|
||||||
for (const outputType of feature.outputTypes) {
|
for (const outputType of feature.outputTypes) {
|
||||||
actuators.push({
|
actuators.push({
|
||||||
@@ -568,7 +568,7 @@ onMount(() => {
|
|||||||
<div class="container mx-auto px-4 py-12">
|
<div class="container mx-auto px-4 py-12">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{#if devices}
|
{#if devices}
|
||||||
{#each devices as device}
|
{#each devices as device (device.name)}
|
||||||
<DeviceCard
|
<DeviceCard
|
||||||
{device}
|
{device}
|
||||||
onChange={(scalarIndex, val) => handleChange(device, scalarIndex, val)}
|
onChange={(scalarIndex, val) => handleChange(device, scalarIndex, val)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
import { SvelteMap } from "svelte/reactivity";
|
||||||
import * as Dialog from "$lib/components/ui/dialog";
|
import * as Dialog from "$lib/components/ui/dialog";
|
||||||
import Button from "$lib/components/ui/button/button.svelte";
|
import Button from "$lib/components/ui/button/button.svelte";
|
||||||
import type { BluetoothDevice, DeviceInfo } from "$lib/types";
|
import type { BluetoothDevice, DeviceInfo } from "$lib/types";
|
||||||
@@ -15,7 +16,7 @@ interface Props {
|
|||||||
let { open, recordedDevices, connectedDevices, onConfirm, onCancel }: Props = $props();
|
let { open, recordedDevices, connectedDevices, onConfirm, onCancel }: Props = $props();
|
||||||
|
|
||||||
// Device mappings: recorded device name -> connected device
|
// Device mappings: recorded device name -> connected device
|
||||||
let mappings = $state<Map<string, BluetoothDevice>>(new Map());
|
let mappings = new SvelteMap<string, BluetoothDevice>();
|
||||||
|
|
||||||
// Check if a connected device is compatible with a recorded device
|
// Check if a connected device is compatible with a recorded device
|
||||||
function isCompatible(recordedDevice: DeviceInfo, connectedDevice: BluetoothDevice): boolean {
|
function isCompatible(recordedDevice: DeviceInfo, connectedDevice: BluetoothDevice): boolean {
|
||||||
@@ -37,7 +38,7 @@ function getCompatibleDevices(recordedDevice: DeviceInfo): BluetoothDevice[] {
|
|||||||
// Auto-map devices on open
|
// Auto-map devices on open
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (open && recordedDevices.length > 0 && connectedDevices.length > 0) {
|
if (open && recordedDevices.length > 0 && connectedDevices.length > 0) {
|
||||||
const newMappings = new Map<string, BluetoothDevice>();
|
const newMappings = new SvelteMap<string, BluetoothDevice>();
|
||||||
|
|
||||||
recordedDevices.forEach(recordedDevice => {
|
recordedDevices.forEach(recordedDevice => {
|
||||||
// Try to find exact name match first
|
// Try to find exact name match first
|
||||||
@@ -74,7 +75,7 @@ function handleDeviceSelect(recordedDeviceName: string, deviceId: string) {
|
|||||||
|
|
||||||
const device = connectedDevices.find(d => d.id === deviceId);
|
const device = connectedDevices.find(d => d.id === deviceId);
|
||||||
if (device) {
|
if (device) {
|
||||||
const newMappings = new Map(mappings);
|
const newMappings = new SvelteMap(mappings);
|
||||||
newMappings.set(recordedDeviceName, device);
|
newMappings.set(recordedDeviceName, device);
|
||||||
mappings = newMappings;
|
mappings = newMappings;
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,7 @@ const allDevicesMapped = $derived(
|
|||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
|
|
||||||
<div class="space-y-4 py-4">
|
<div class="space-y-4 py-4">
|
||||||
{#each recordedDevices as recordedDevice}
|
{#each recordedDevices as recordedDevice (recordedDevice.name)}
|
||||||
{@const compatibleDevices = getCompatibleDevices(recordedDevice)}
|
{@const compatibleDevices = getCompatibleDevices(recordedDevice)}
|
||||||
{@const currentMapping = mappings.get(recordedDevice.name)}
|
{@const currentMapping = mappings.get(recordedDevice.name)}
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ const allDevicesMapped = $derived(
|
|||||||
<h3 class="font-semibold text-card-foreground">{recordedDevice.name}</h3>
|
<h3 class="font-semibold text-card-foreground">{recordedDevice.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
{#each recordedDevice.capabilities as capability}
|
{#each recordedDevice.capabilities as capability (capability)}
|
||||||
<span class="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20">
|
<span class="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20">
|
||||||
{capability}
|
{capability}
|
||||||
</span>
|
</span>
|
||||||
@@ -129,7 +130,7 @@ const allDevicesMapped = $derived(
|
|||||||
onchange={(e) => handleDeviceSelect(recordedDevice.name, e.currentTarget.value)}
|
onchange={(e) => handleDeviceSelect(recordedDevice.name, e.currentTarget.value)}
|
||||||
>
|
>
|
||||||
<option value="" disabled>Select device...</option>
|
<option value="" disabled>Select device...</option>
|
||||||
{#each compatibleDevices as device}
|
{#each compatibleDevices as device (device.name)}
|
||||||
<option value={device.id}>
|
<option value={device.id}>
|
||||||
{device.name}
|
{device.name}
|
||||||
{#if device.name === recordedDevice.name}(exact match){/if}
|
{#if device.name === recordedDevice.name}(exact match){/if}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function handleCancel() {
|
|||||||
<!-- Device Info -->
|
<!-- Device Info -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label>Devices Used</Label>
|
<Label>Devices Used</Label>
|
||||||
{#each deviceInfo as device}
|
{#each deviceInfo as device (device.name)}
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 text-sm bg-muted/20 rounded px-3 py-2"
|
class="flex items-center gap-2 text-sm bg-muted/20 rounded px-3 py-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { Label } from "$lib/components/ui/label";
|
|||||||
import { Checkbox } from "$lib/components/ui/checkbox";
|
import { Checkbox } from "$lib/components/ui/checkbox";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import * as Alert from "$lib/components/ui/alert";
|
import * as Alert from "$lib/components/ui/alert";
|
||||||
import PeonyIcon from "$lib/components/icon/peony-icon.svelte";
|
|
||||||
import { register } from "$lib/services";
|
import { register } from "$lib/services";
|
||||||
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
||||||
import Meta from "$lib/components/meta/meta.svelte";
|
import Meta from "$lib/components/meta/meta.svelte";
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
} from "$lib/components/ui/select";
|
} from "$lib/components/ui/select";
|
||||||
import { getAssetUrl } from "$lib/directus";
|
import { getAssetUrl } from "$lib/directus";
|
||||||
import PeonyBackground from "$lib/components/background/peony-background.svelte";
|
|
||||||
import Meta from "$lib/components/meta/meta.svelte";
|
import Meta from "$lib/components/meta/meta.svelte";
|
||||||
|
|
||||||
let searchQuery = $state("");
|
let searchQuery = $state("");
|
||||||
@@ -134,7 +133,7 @@ const filteredItems = $derived(() => {
|
|||||||
<!-- Items Grid -->
|
<!-- Items Grid -->
|
||||||
<div class="container mx-auto px-4 py-12">
|
<div class="container mx-auto px-4 py-12">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{#each filteredItems() as item}
|
{#each filteredItems() as item (item.slug)}
|
||||||
<Card
|
<Card
|
||||||
class="py-0 group hover:shadow-2xl hover:shadow-primary/25 transition-all duration-300 hover:-translate-y-3 bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10 overflow-hidden"
|
class="py-0 group hover:shadow-2xl hover:shadow-primary/25 transition-all duration-300 hover:-translate-y-3 bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10 overflow-hidden"
|
||||||
>
|
>
|
||||||
@@ -178,7 +177,7 @@ const filteredItems = $derived(() => {
|
|||||||
|
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<div class="flex flex-wrap gap-2 mb-4">
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
{#each item.tags as tag}
|
{#each item.tags as tag (tag)}
|
||||||
<a
|
<a
|
||||||
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
|
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
|
||||||
href="/tags/{tag}"
|
href="/tags/{tag}"
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ const filteredVideos = $derived(() => {
|
|||||||
<!-- Videos Grid -->
|
<!-- Videos Grid -->
|
||||||
<div class="container mx-auto px-4 py-12">
|
<div class="container mx-auto px-4 py-12">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{#each filteredVideos() as video}
|
{#each filteredVideos() as video (video.slug)}
|
||||||
<Card
|
<Card
|
||||||
class="p-0 group hover:shadow-2xl hover:shadow-primary/25 transition-all duration-500 hover:-translate-y-3 bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10 overflow-hidden"
|
class="p-0 group hover:shadow-2xl hover:shadow-primary/25 transition-all duration-500 hover:-translate-y-3 bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10 overflow-hidden"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ let commentError = $state();
|
|||||||
let currentPlayId = $state<string | null>(null);
|
let currentPlayId = $state<string | null>(null);
|
||||||
let lastTrackedTime = $state(0);
|
let lastTrackedTime = $state(0);
|
||||||
|
|
||||||
const relatedVideos = [
|
const _relatedVideos = [
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "Sunset Dreams",
|
title: "Sunset Dreams",
|
||||||
@@ -94,7 +94,7 @@ async function handleLike() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBookmark() {
|
function _handleBookmark() {
|
||||||
isBookmarked = !isBookmarked;
|
isBookmarked = !isBookmarked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +279,7 @@ let showPlayer = $state(false);
|
|||||||
/>
|
/>
|
||||||
<!-- <Button
|
<!-- <Button
|
||||||
variant={isBookmarked ? "default" : "outline"}
|
variant={isBookmarked ? "default" : "outline"}
|
||||||
onclick={handleBookmark}
|
onclick={_handleBookmark}
|
||||||
class="flex items-center gap-2 {isBookmarked
|
class="flex items-center gap-2 {isBookmarked
|
||||||
? 'bg-gradient-to-r from-primary to-accent'
|
? 'bg-gradient-to-r from-primary to-accent'
|
||||||
: 'border-primary/20 hover:bg-primary/10'}"
|
: 'border-primary/20 hover:bg-primary/10'}"
|
||||||
@@ -291,7 +291,7 @@ let showPlayer = $state(false);
|
|||||||
|
|
||||||
<!-- Model Info -->
|
<!-- Model Info -->
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
{#each data.video.models as model}
|
{#each data.video.models as model (model.slug)}
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<a href={`/models/${model.slug}`}>
|
<a href={`/models/${model.slug}`}>
|
||||||
@@ -341,7 +341,7 @@ let showPlayer = $state(false);
|
|||||||
<CardContent class="p-4">
|
<CardContent class="p-4">
|
||||||
<p class="text-muted-foreground mb-4">{data.video.description}</p>
|
<p class="text-muted-foreground mb-4">{data.video.description}</p>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
{#each data.video.tags as tag}
|
{#each data.video.tags as tag (tag)}
|
||||||
<a
|
<a
|
||||||
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
|
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
|
||||||
href="/tags/{tag}"
|
href="/tags/{tag}"
|
||||||
@@ -438,7 +438,7 @@ let showPlayer = $state(false);
|
|||||||
{#if showComments}
|
{#if showComments}
|
||||||
<!-- Comments List -->
|
<!-- Comments List -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{#each data.comments as comment}
|
{#each data.comments as comment (comment.id)}
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<a href="/users/{comment.user_created.id}" class="flex-shrink-0">
|
<a href="/users/{comment.user_created.id}" class="flex-shrink-0">
|
||||||
<Avatar
|
<Avatar
|
||||||
|
|||||||
859
pnpm-lock.yaml
generated
859
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
18
prettier.config.js
Normal file
18
prettier.config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/** @type {import('prettier').Config} */
|
||||||
|
export default {
|
||||||
|
useTabs: false,
|
||||||
|
tabWidth: 2,
|
||||||
|
singleQuote: false,
|
||||||
|
trailingComma: "all",
|
||||||
|
printWidth: 100,
|
||||||
|
semi: true,
|
||||||
|
plugins: ["prettier-plugin-svelte"],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: "*.svelte",
|
||||||
|
options: {
|
||||||
|
parser: "svelte",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
2667
schema.sql
2667
schema.sql
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user