From 18116072c92097f020832ea222fdc7fa4e2041ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Wed, 4 Mar 2026 22:24:55 +0100 Subject: [PATCH] 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 --- .prettierignore | 6 + CLAUDE.md | 177 -- directus.yml | 2114 ------------- eslint.config.js | 59 + package.json | 18 +- packages/backend/package.json | 5 +- packages/backend/src/index.ts | 2 +- packages/backend/src/lib/gamification.ts | 2 +- .../backend/src/scripts/data-migration.ts | 2 +- packages/frontend/package.json | 4 +- packages/frontend/src/app.css | 2 + packages/frontend/src/hooks.server.ts | 8 +- .../components/device-card/device-card.svelte | 4 +- .../src/lib/components/footer/footer.svelte | 1 - .../src/lib/components/header/header.svelte | 15 +- .../image-viewer/image-viewer.svelte | 2 +- .../recording-card/recording-card.svelte | 4 +- .../sharing-popup/share-button.svelte | 2 - .../sharing-popup/share-services.svelte | 2 +- .../components/ui/select/select-group.svelte | 1 + packages/frontend/src/lib/types.ts | 2 +- packages/frontend/src/lib/utils.ts | 1 - packages/frontend/src/routes/+page.svelte | 4 +- .../frontend/src/routes/about/+page.svelte | 6 +- packages/frontend/src/routes/faq/+page.svelte | 11 +- .../frontend/src/routes/login/+page.svelte | 6 +- .../frontend/src/routes/magazine/+page.svelte | 4 +- .../src/routes/magazine/[slug]/+page.svelte | 3 +- packages/frontend/src/routes/me/+page.svelte | 5 +- .../frontend/src/routes/models/+page.svelte | 4 +- .../src/routes/models/[slug]/+page.svelte | 4 +- .../frontend/src/routes/password/+page.svelte | 3 +- .../src/routes/password/reset/+page.svelte | 1 - .../frontend/src/routes/play/+page.svelte | 8 +- .../components/device-mapping-dialog.svelte | 13 +- .../components/recording-save-dialog.svelte | 2 +- .../frontend/src/routes/signup/+page.svelte | 1 - .../src/routes/tags/[tag]/+page.svelte | 5 +- .../frontend/src/routes/videos/+page.svelte | 2 +- .../src/routes/videos/[slug]/+page.svelte | 12 +- pnpm-lock.yaml | 859 +++++- prettier.config.js | 18 + schema.sql | 2667 ----------------- 43 files changed, 1023 insertions(+), 5048 deletions(-) create mode 100644 .prettierignore delete mode 100644 CLAUDE.md delete mode 100644 directus.yml create mode 100644 eslint.config.js create mode 100644 prettier.config.js delete mode 100644 schema.sql diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..22f2e3a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +build/ +.svelte-kit/ +dist/ +node_modules/ +migrations/ +pnpm-lock.yaml diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 484b37e..0000000 --- a/CLAUDE.md +++ /dev/null @@ -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"`) diff --git a/directus.yml b/directus.yml deleted file mode 100644 index 0a6afbe..0000000 --- a/directus.yml +++ /dev/null @@ -1,2114 +0,0 @@ -version: 1 -directus: 11.12.0 -vendor: postgres -collections: - - collection: junction_directus_users_files - meta: - accountability: all - archive_app_filter: true - archive_field: null - archive_value: null - collapse: open - collection: junction_directus_users_files - color: null - display_template: null - group: null - hidden: true - icon: import_export - item_duplication_fields: null - note: null - preview_url: null - singleton: false - sort: null - sort_field: null - translations: null - unarchive_value: null - versioning: false - schema: - name: junction_directus_users_files - - collection: sexy_articles - meta: - accountability: all - archive_app_filter: true - archive_field: status - archive_value: archived - collapse: open - collection: sexy_articles - color: null - display_template: null - group: null - hidden: false - icon: newsmode - item_duplication_fields: null - note: null - preview_url: null - singleton: false - sort: null - sort_field: null - translations: - - language: en-US - plural: Articles - singular: Article - translation: Sexy Articles - unarchive_value: draft - versioning: true - schema: - name: sexy_articles - - collection: sexy_videos - meta: - accountability: all - archive_app_filter: true - archive_field: status - archive_value: archived - collapse: open - collection: sexy_videos - color: null - display_template: null - group: null - hidden: false - icon: videocam - item_duplication_fields: null - note: null - preview_url: null - singleton: false - sort: null - sort_field: null - translations: null - unarchive_value: draft - versioning: false - schema: - name: sexy_videos - - collection: sexy_videos_directus_users - meta: - accountability: all - archive_app_filter: true - archive_field: null - archive_value: null - collapse: open - collection: sexy_videos_directus_users - color: null - display_template: null - group: null - hidden: true - icon: import_export - item_duplication_fields: null - note: null - preview_url: null - singleton: false - sort: null - sort_field: null - translations: null - unarchive_value: null - versioning: false - schema: - name: sexy_videos_directus_users -fields: - - collection: directus_users - field: website - type: string - meta: - collection: directus_users - conditions: null - display: null - display_options: null - field: website - group: null - hidden: false - interface: input - note: null - options: null - readonly: false - required: false - sort: 2 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: website - table: directus_users - data_type: character varying - default_value: null - max_length: 255 - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: directus_users - field: slug - type: string - meta: - collection: directus_users - conditions: - - name: Enable for role "Administrator" - readonly: false - rule: - _and: - - role: - _eq: ea3a9127-2b65-462c-85a8-dbafe9b4fe24 - display: null - display_options: null - field: slug - group: null - hidden: false - interface: input - note: null - options: null - readonly: false - required: true - sort: 3 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: slug - table: directus_users - data_type: character varying - default_value: null - max_length: 255 - numeric_precision: null - numeric_scale: null - is_nullable: false - is_unique: true - is_indexed: true - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: directus_users - field: join_date - type: dateTime - meta: - collection: directus_users - conditions: null - display: null - display_options: null - field: join_date - group: null - hidden: false - interface: datetime - note: null - options: - format: short - readonly: false - required: true - sort: 4 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: join_date - table: directus_users - data_type: timestamp without time zone - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: false - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: directus_users - field: featured - type: boolean - meta: - collection: directus_users - conditions: null - display: null - display_options: null - field: featured - group: null - hidden: false - interface: boolean - note: null - options: - label: Featured - readonly: false - required: false - sort: 5 - special: - - cast-boolean - translations: null - validation: null - validation_message: null - width: full - schema: - name: featured - table: directus_users - data_type: boolean - default_value: false - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: directus_users - field: artist_name - type: string - meta: - collection: directus_users - conditions: null - display: null - display_options: null - field: artist_name - group: null - hidden: false - interface: input - note: null - options: null - readonly: false - required: true - sort: 1 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: artist_name - table: directus_users - data_type: character varying - default_value: null - max_length: 255 - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: directus_users - field: photos - type: alias - meta: - collection: directus_users - conditions: null - display: null - display_options: null - field: photos - group: null - hidden: false - interface: files - note: null - options: - filter: - _and: - - type: - _starts_with: image - folder: 4cb93083-f3f7-4a61-a80f-d56fd9e6ee62 - readonly: false - required: false - sort: 6 - special: - - files - translations: null - validation: null - validation_message: null - width: full - - collection: directus_users - field: banner - type: uuid - meta: - collection: directus_users - conditions: null - display: null - display_options: null - field: banner - group: null - hidden: false - interface: file-image - note: null - options: - folder: 9fd092ff-9e7b-48f0-b26c-bcead509ba9e - readonly: false - required: false - sort: 7 - special: - - file - translations: null - validation: null - validation_message: null - width: full - schema: - name: banner - table: directus_users - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: directus_files - foreign_key_column: id - - collection: junction_directus_users_files - field: id - type: integer - meta: - collection: junction_directus_users_files - conditions: null - display: null - display_options: null - field: id - group: null - hidden: true - interface: null - note: null - options: null - readonly: false - required: false - sort: 1 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: id - table: junction_directus_users_files - data_type: integer - default_value: nextval('junction_directus_users_files_id_seq'::regclass) - max_length: null - numeric_precision: 32 - numeric_scale: 0 - is_nullable: false - is_unique: true - is_indexed: false - is_primary_key: true - is_generated: false - generation_expression: null - has_auto_increment: true - foreign_key_table: null - foreign_key_column: null - - collection: junction_directus_users_files - field: directus_users_id - type: uuid - meta: - collection: junction_directus_users_files - conditions: null - display: null - display_options: null - field: directus_users_id - group: null - hidden: true - interface: null - note: null - options: null - readonly: false - required: false - sort: 2 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: directus_users_id - table: junction_directus_users_files - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: directus_users - foreign_key_column: id - - collection: junction_directus_users_files - field: directus_files_id - type: uuid - meta: - collection: junction_directus_users_files - conditions: null - display: null - display_options: null - field: directus_files_id - group: null - hidden: true - interface: null - note: null - options: null - readonly: false - required: false - sort: 3 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: directus_files_id - table: junction_directus_users_files - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: directus_files - foreign_key_column: id - - collection: sexy_articles - field: id - type: uuid - meta: - collection: sexy_articles - conditions: null - display: null - display_options: null - field: id - group: null - hidden: true - interface: input - note: null - options: null - readonly: true - required: false - sort: 1 - special: - - uuid - translations: null - validation: null - validation_message: null - width: full - schema: - name: id - table: sexy_articles - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: false - is_unique: true - is_indexed: false - is_primary_key: true - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_articles - field: status - type: string - meta: - collection: sexy_articles - conditions: null - display: labels - display_options: - choices: - - background: var(--theme--primary-background) - color: var(--theme--primary) - foreground: var(--theme--primary) - text: $t:published - value: published - - background: var(--theme--background-normal) - color: var(--theme--foreground) - foreground: var(--theme--foreground) - text: $t:draft - value: draft - - background: var(--theme--warning-background) - color: var(--theme--warning) - foreground: var(--theme--warning) - text: $t:archived - value: archived - showAsDot: true - field: status - group: null - hidden: false - interface: select-dropdown - note: null - options: - choices: - - color: var(--theme--primary) - text: $t:published - value: published - - color: var(--theme--foreground) - text: $t:draft - value: draft - - color: var(--theme--warning) - text: $t:archived - value: archived - readonly: false - required: false - sort: 2 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: status - table: sexy_articles - data_type: character varying - default_value: draft - max_length: 255 - numeric_precision: null - numeric_scale: null - is_nullable: false - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_articles - field: user_created - type: uuid - meta: - collection: sexy_articles - conditions: null - display: user - display_options: null - field: user_created - group: null - hidden: true - interface: select-dropdown-m2o - note: null - options: - template: '{{avatar}} {{first_name}} {{last_name}}' - readonly: true - required: false - sort: 3 - special: - - user-created - translations: null - validation: null - validation_message: null - width: half - schema: - name: user_created - table: sexy_articles - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: directus_users - foreign_key_column: id - - collection: sexy_articles - field: date_created - type: timestamp - meta: - collection: sexy_articles - conditions: null - display: datetime - display_options: - relative: true - field: date_created - group: null - hidden: true - interface: datetime - note: null - options: null - readonly: true - required: false - sort: 4 - special: - - date-created - translations: null - validation: null - validation_message: null - width: half - schema: - name: date_created - table: sexy_articles - data_type: timestamp with time zone - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_articles - field: date_updated - type: timestamp - meta: - collection: sexy_articles - conditions: null - display: datetime - display_options: - relative: true - field: date_updated - group: null - hidden: true - interface: datetime - note: null - options: null - readonly: true - required: false - sort: 5 - special: - - date-updated - translations: null - validation: null - validation_message: null - width: half - schema: - name: date_updated - table: sexy_articles - data_type: timestamp with time zone - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_articles - field: slug - type: string - meta: - collection: sexy_articles - conditions: null - display: null - display_options: null - field: slug - group: null - hidden: false - interface: input - note: null - options: - slug: true - readonly: false - required: true - sort: 6 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: slug - table: sexy_articles - data_type: character varying - default_value: null - max_length: 255 - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: true - is_indexed: true - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_articles - field: title - type: string - meta: - collection: sexy_articles - conditions: null - display: null - display_options: null - field: title - group: null - hidden: false - interface: input - note: null - options: null - readonly: false - required: true - sort: 7 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: title - table: sexy_articles - data_type: character varying - default_value: null - max_length: 255 - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_articles - field: excerpt - type: text - meta: - collection: sexy_articles - conditions: null - display: null - display_options: null - field: excerpt - group: null - hidden: false - interface: input-multiline - note: null - options: - trim: true - readonly: false - required: true - sort: 9 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: excerpt - table: sexy_articles - data_type: text - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_articles - field: content - type: text - meta: - collection: sexy_articles - conditions: null - display: null - display_options: null - field: content - group: null - hidden: false - interface: input-rich-text-html - note: null - options: - folder: c214c905-885b-4d66-a6a1-6527b0606200 - toolbar: - - bold - - italic - - underline - - h2 - - h3 - - numlist - - bullist - - removeformat - - blockquote - - customLink - - hr - - fullscreen - - code - readonly: false - required: true - sort: 10 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: content - table: sexy_articles - data_type: text - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_articles - field: image - type: uuid - meta: - collection: sexy_articles - conditions: null - display: null - display_options: null - field: image - group: null - hidden: false - interface: file-image - note: null - options: - folder: 452680cc-8e19-4352-a943-21520d3f3621 - readonly: false - required: true - sort: 11 - special: - - file - translations: null - validation: null - validation_message: null - width: full - schema: - name: image - table: sexy_articles - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: false - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: directus_files - foreign_key_column: id - - collection: sexy_articles - field: tags - type: json - meta: - collection: sexy_articles - conditions: null - display: null - display_options: null - field: tags - group: null - hidden: false - interface: tags - note: null - options: - capitalization: auto-format - whitespace: _ - readonly: false - required: false - sort: 12 - special: - - cast-json - translations: null - validation: null - validation_message: null - width: full - schema: - name: tags - table: sexy_articles - data_type: json - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_articles - field: publish_date - type: dateTime - meta: - collection: sexy_articles - conditions: null - display: null - display_options: null - field: publish_date - group: null - hidden: false - interface: datetime - note: null - options: null - readonly: false - required: true - sort: 13 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: publish_date - table: sexy_articles - data_type: timestamp without time zone - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_articles - field: category - type: string - meta: - collection: sexy_articles - conditions: null - display: null - display_options: null - field: category - group: null - hidden: false - interface: input - note: null - options: null - readonly: false - required: true - sort: 14 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: category - table: sexy_articles - data_type: character varying - default_value: null - max_length: 255 - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_articles - field: featured - type: boolean - meta: - collection: sexy_articles - conditions: null - display: null - display_options: null - field: featured - group: null - hidden: false - interface: boolean - note: null - options: - label: Featured - readonly: false - required: false - sort: 15 - special: - - cast-boolean - translations: null - validation: null - validation_message: null - width: full - schema: - name: featured - table: sexy_articles - data_type: boolean - default_value: false - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_articles - field: author - type: uuid - meta: - collection: sexy_articles - conditions: null - display: null - display_options: null - field: author - group: null - hidden: false - interface: select-dropdown-m2o - note: null - options: - enableLink: true - filter: - _and: - - policies: - policy: - name: - _eq: Editor - readonly: false - required: true - sort: 8 - special: - - m2o - translations: null - validation: null - validation_message: null - width: full - schema: - name: author - table: sexy_articles - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: directus_users - foreign_key_column: id - - collection: sexy_videos - field: id - type: uuid - meta: - collection: sexy_videos - conditions: null - display: null - display_options: null - field: id - group: null - hidden: true - interface: input - note: null - options: null - readonly: true - required: false - sort: 1 - special: - - uuid - translations: null - validation: null - validation_message: null - width: full - schema: - name: id - table: sexy_videos - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: false - is_unique: true - is_indexed: false - is_primary_key: true - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_videos - field: status - type: string - meta: - collection: sexy_videos - conditions: null - display: labels - display_options: - choices: - - background: var(--theme--primary-background) - color: var(--theme--primary) - foreground: var(--theme--primary) - text: $t:published - value: published - - background: var(--theme--background-normal) - color: var(--theme--foreground) - foreground: var(--theme--foreground) - text: $t:draft - value: draft - - background: var(--theme--warning-background) - color: var(--theme--warning) - foreground: var(--theme--warning) - text: $t:archived - value: archived - showAsDot: true - field: status - group: null - hidden: false - interface: select-dropdown - note: null - options: - choices: - - color: var(--theme--primary) - text: $t:published - value: published - - color: var(--theme--foreground) - text: $t:draft - value: draft - - color: var(--theme--warning) - text: $t:archived - value: archived - readonly: false - required: false - sort: 2 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: status - table: sexy_videos - data_type: character varying - default_value: draft - max_length: 255 - numeric_precision: null - numeric_scale: null - is_nullable: false - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_videos - field: user_created - type: uuid - meta: - collection: sexy_videos - conditions: null - display: user - display_options: null - field: user_created - group: null - hidden: true - interface: select-dropdown-m2o - note: null - options: - template: '{{avatar}} {{first_name}} {{last_name}}' - readonly: true - required: false - sort: 3 - special: - - user-created - translations: null - validation: null - validation_message: null - width: half - schema: - name: user_created - table: sexy_videos - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: directus_users - foreign_key_column: id - - collection: sexy_videos - field: date_created - type: timestamp - meta: - collection: sexy_videos - conditions: null - display: datetime - display_options: - relative: true - field: date_created - group: null - hidden: true - interface: datetime - note: null - options: null - readonly: true - required: false - sort: 4 - special: - - date-created - translations: null - validation: null - validation_message: null - width: half - schema: - name: date_created - table: sexy_videos - data_type: timestamp with time zone - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_videos - field: date_updated - type: timestamp - meta: - collection: sexy_videos - conditions: null - display: datetime - display_options: - relative: true - field: date_updated - group: null - hidden: true - interface: datetime - note: null - options: null - readonly: true - required: false - sort: 5 - special: - - date-updated - translations: null - validation: null - validation_message: null - width: half - schema: - name: date_updated - table: sexy_videos - data_type: timestamp with time zone - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_videos - field: slug - type: string - meta: - collection: sexy_videos - conditions: null - display: null - display_options: null - field: slug - group: null - hidden: false - interface: input - note: null - options: - slug: true - trim: true - readonly: false - required: true - sort: 6 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: slug - table: sexy_videos - data_type: character varying - default_value: null - max_length: 255 - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_videos - field: title - type: string - meta: - collection: sexy_videos - conditions: null - display: null - display_options: null - field: title - group: null - hidden: false - interface: input - note: null - options: null - readonly: false - required: true - sort: 7 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: title - table: sexy_videos - data_type: character varying - default_value: null - max_length: 255 - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_videos - field: image - type: uuid - meta: - collection: sexy_videos - conditions: null - display: null - display_options: null - field: image - group: null - hidden: false - interface: file-image - note: null - options: - folder: 26657630-d9cd-47a3-9e45-9831f3674f97 - readonly: false - required: true - sort: 9 - special: - - file - translations: null - validation: null - validation_message: null - width: full - schema: - name: image - table: sexy_videos - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: directus_files - foreign_key_column: id - - collection: sexy_videos - field: upload_date - type: dateTime - meta: - collection: sexy_videos - conditions: null - display: null - display_options: null - field: upload_date - group: null - hidden: false - interface: datetime - note: null - options: null - readonly: false - required: true - sort: 12 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: upload_date - table: sexy_videos - data_type: timestamp without time zone - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_videos - field: premium - type: boolean - meta: - collection: sexy_videos - conditions: null - display: null - display_options: null - field: premium - group: null - hidden: false - interface: boolean - note: null - options: - label: Premium - readonly: false - required: false - sort: 13 - special: - - cast-boolean - translations: null - validation: null - validation_message: null - width: full - schema: - name: premium - table: sexy_videos - data_type: boolean - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_videos - field: featured - type: boolean - meta: - collection: sexy_videos - conditions: null - display: null - display_options: null - field: featured - group: null - hidden: false - interface: boolean - note: null - options: - label: Featured - readonly: false - required: false - sort: 14 - special: - - cast-boolean - translations: null - validation: null - validation_message: null - width: full - schema: - name: featured - table: sexy_videos - data_type: boolean - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_videos - field: tags - type: json - meta: - collection: sexy_videos - conditions: null - display: null - display_options: null - field: tags - group: null - hidden: false - interface: tags - note: null - options: null - readonly: false - required: false - sort: 15 - special: - - cast-json - translations: null - validation: null - validation_message: null - width: full - schema: - name: tags - table: sexy_videos - data_type: json - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_videos - field: models - type: alias - meta: - collection: sexy_videos - conditions: null - display: null - display_options: null - field: models - group: null - hidden: false - interface: list-m2m - note: null - options: null - readonly: false - required: true - sort: 11 - special: - - m2m - translations: null - validation: null - validation_message: null - width: full - - collection: sexy_videos - field: movie - type: uuid - meta: - collection: sexy_videos - conditions: null - display: null - display_options: null - field: movie - group: null - hidden: false - interface: file - note: null - options: - filter: - _and: - - type: - _eq: video/mp4 - folder: 3f83c727-9c90-4e0d-871f-ab81c295043a - readonly: false - required: true - sort: 10 - special: - - file - translations: null - validation: null - validation_message: null - width: full - schema: - name: movie - table: sexy_videos - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: directus_files - foreign_key_column: id - - collection: sexy_videos - field: description - type: text - meta: - collection: sexy_videos - conditions: null - display: null - display_options: null - field: description - group: null - hidden: false - interface: input-multiline - note: null - options: - trim: true - readonly: false - required: true - sort: 8 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: description - table: sexy_videos - data_type: text - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: null - foreign_key_column: null - - collection: sexy_videos_directus_users - field: id - type: integer - meta: - collection: sexy_videos_directus_users - conditions: null - display: null - display_options: null - field: id - group: null - hidden: true - interface: null - note: null - options: null - readonly: false - required: false - sort: 1 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: id - table: sexy_videos_directus_users - data_type: integer - default_value: nextval('sexy_videos_directus_users_id_seq'::regclass) - max_length: null - numeric_precision: 32 - numeric_scale: 0 - is_nullable: false - is_unique: true - is_indexed: false - is_primary_key: true - is_generated: false - generation_expression: null - has_auto_increment: true - foreign_key_table: null - foreign_key_column: null - - collection: sexy_videos_directus_users - field: sexy_videos_id - type: uuid - meta: - collection: sexy_videos_directus_users - conditions: null - display: null - display_options: null - field: sexy_videos_id - group: null - hidden: true - interface: null - note: null - options: null - readonly: false - required: false - sort: 2 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: sexy_videos_id - table: sexy_videos_directus_users - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: sexy_videos - foreign_key_column: id - - collection: sexy_videos_directus_users - field: directus_users_id - type: uuid - meta: - collection: sexy_videos_directus_users - conditions: null - display: null - display_options: null - field: directus_users_id - group: null - hidden: true - interface: null - note: null - options: null - readonly: false - required: false - sort: 3 - special: null - translations: null - validation: null - validation_message: null - width: full - schema: - name: directus_users_id - table: sexy_videos_directus_users - data_type: uuid - default_value: null - max_length: null - numeric_precision: null - numeric_scale: null - is_nullable: true - is_unique: false - is_indexed: false - is_primary_key: false - is_generated: false - generation_expression: null - has_auto_increment: false - foreign_key_table: directus_users - foreign_key_column: id -relations: - - collection: directus_users - field: banner - related_collection: directus_files - meta: - junction_field: null - many_collection: directus_users - many_field: banner - one_allowed_collections: null - one_collection: directus_files - one_collection_field: null - one_deselect_action: nullify - one_field: null - sort_field: null - schema: - table: directus_users - column: banner - foreign_key_table: directus_files - foreign_key_column: id - constraint_name: directus_users_banner_foreign - on_update: NO ACTION - on_delete: SET NULL - - collection: junction_directus_users_files - field: directus_files_id - related_collection: directus_files - meta: - junction_field: directus_users_id - many_collection: junction_directus_users_files - many_field: directus_files_id - one_allowed_collections: null - one_collection: directus_files - one_collection_field: null - one_deselect_action: nullify - one_field: null - sort_field: null - schema: - table: junction_directus_users_files - column: directus_files_id - foreign_key_table: directus_files - foreign_key_column: id - constraint_name: junction_directus_users_files_directus_files_id_foreign - on_update: NO ACTION - on_delete: SET NULL - - collection: junction_directus_users_files - field: directus_users_id - related_collection: directus_users - meta: - junction_field: directus_files_id - many_collection: junction_directus_users_files - many_field: directus_users_id - one_allowed_collections: null - one_collection: directus_users - one_collection_field: null - one_deselect_action: nullify - one_field: photos - sort_field: null - schema: - table: junction_directus_users_files - column: directus_users_id - foreign_key_table: directus_users - foreign_key_column: id - constraint_name: junction_directus_users_files_directus_users_id_foreign - on_update: NO ACTION - on_delete: SET NULL - - collection: sexy_articles - field: user_created - related_collection: directus_users - meta: - junction_field: null - many_collection: sexy_articles - many_field: user_created - one_allowed_collections: null - one_collection: directus_users - one_collection_field: null - one_deselect_action: nullify - one_field: null - sort_field: null - schema: - table: sexy_articles - column: user_created - foreign_key_table: directus_users - foreign_key_column: id - constraint_name: sexy_articles_user_created_foreign - on_update: NO ACTION - on_delete: NO ACTION - - collection: sexy_articles - field: image - related_collection: directus_files - meta: - junction_field: null - many_collection: sexy_articles - many_field: image - one_allowed_collections: null - one_collection: directus_files - one_collection_field: null - one_deselect_action: nullify - one_field: null - sort_field: null - schema: - table: sexy_articles - column: image - foreign_key_table: directus_files - foreign_key_column: id - constraint_name: sexy_articles_image_foreign - on_update: NO ACTION - on_delete: NO ACTION - - collection: sexy_articles - field: author - related_collection: directus_users - meta: - junction_field: null - many_collection: sexy_articles - many_field: author - one_allowed_collections: null - one_collection: directus_users - one_collection_field: null - one_deselect_action: nullify - one_field: null - sort_field: null - schema: - table: sexy_articles - column: author - foreign_key_table: directus_users - foreign_key_column: id - constraint_name: sexy_articles_author_foreign - on_update: NO ACTION - on_delete: SET NULL - - collection: sexy_videos - field: user_created - related_collection: directus_users - meta: - junction_field: null - many_collection: sexy_videos - many_field: user_created - one_allowed_collections: null - one_collection: directus_users - one_collection_field: null - one_deselect_action: nullify - one_field: null - sort_field: null - schema: - table: sexy_videos - column: user_created - foreign_key_table: directus_users - foreign_key_column: id - constraint_name: sexy_videos_user_created_foreign - on_update: NO ACTION - on_delete: NO ACTION - - collection: sexy_videos - field: image - related_collection: directus_files - meta: - junction_field: null - many_collection: sexy_videos - many_field: image - one_allowed_collections: null - one_collection: directus_files - one_collection_field: null - one_deselect_action: nullify - one_field: null - sort_field: null - schema: - table: sexy_videos - column: image - foreign_key_table: directus_files - foreign_key_column: id - constraint_name: sexy_videos_image_foreign - on_update: NO ACTION - on_delete: SET NULL - - collection: sexy_videos - field: movie - related_collection: directus_files - meta: - junction_field: null - many_collection: sexy_videos - many_field: movie - one_allowed_collections: null - one_collection: directus_files - one_collection_field: null - one_deselect_action: nullify - one_field: null - sort_field: null - schema: - table: sexy_videos - column: movie - foreign_key_table: directus_files - foreign_key_column: id - constraint_name: sexy_videos_movie_foreign - on_update: NO ACTION - on_delete: SET NULL - - collection: sexy_videos_directus_users - field: directus_users_id - related_collection: directus_users - meta: - junction_field: sexy_videos_id - many_collection: sexy_videos_directus_users - many_field: directus_users_id - one_allowed_collections: null - one_collection: directus_users - one_collection_field: null - one_deselect_action: nullify - one_field: null - sort_field: null - schema: - table: sexy_videos_directus_users - column: directus_users_id - foreign_key_table: directus_users - foreign_key_column: id - constraint_name: sexy_videos_directus_users_directus_users_id_foreign - on_update: NO ACTION - on_delete: SET NULL - - collection: sexy_videos_directus_users - field: sexy_videos_id - related_collection: sexy_videos - meta: - junction_field: directus_users_id - many_collection: sexy_videos_directus_users - many_field: sexy_videos_id - one_allowed_collections: null - one_collection: sexy_videos - one_collection_field: null - one_deselect_action: nullify - one_field: models - sort_field: null - schema: - table: sexy_videos_directus_users - column: sexy_videos_id - foreign_key_table: sexy_videos - foreign_key_column: id - constraint_name: sexy_videos_directus_users_sexy_videos_id_foreign - on_update: NO ACTION - on_delete: SET NULL diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..fa8e45b --- /dev/null +++ b/eslint.config.js @@ -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/**", + ], + }, +); diff --git a/package.json b/package.json index aedf8a8..1e28bf1 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,19 @@ "name": "sexy.pivoine.art", "version": "1.0.0", "description": "", - "main": "index.js", + "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "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", "dev:data": "docker compose up -d postgres redis", "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": [], "author": { @@ -30,5 +35,14 @@ "@tailwindcss/oxide", "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" } } diff --git a/packages/backend/package.json b/packages/backend/package.json index 54d94fc..0a77e2b 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -3,14 +3,15 @@ "version": "1.0.0", "private": true, "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", "start": "node dist/index.js", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:studio": "drizzle-kit studio", "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": { "@fastify/cookie": "^11.0.2", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 3770f1d..c7a87be 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,4 +1,4 @@ -import Fastify, { FastifyRequest, FastifyReply } from "fastify"; +import Fastify, { type FastifyRequest, type FastifyReply } from "fastify"; import fastifyCookie from "@fastify/cookie"; import fastifyCors from "@fastify/cors"; import fastifyMultipart from "@fastify/multipart"; diff --git a/packages/backend/src/lib/gamification.ts b/packages/backend/src/lib/gamification.ts index d5f3ed9..3e9148f 100644 --- a/packages/backend/src/lib/gamification.ts +++ b/packages/backend/src/lib/gamification.ts @@ -74,7 +74,7 @@ export async function updateUserStats(db: DB, userId: string): Promise { .where(eq(recordings.user_id, userId)); const ownIds = ownRecordingIds.map((r) => r.id); - let playbacksCount = 0; + let playbacksCount: number; if (ownIds.length > 0) { const playbacksResult = await db.execute(sql` SELECT COUNT(*) as count FROM recording_plays diff --git a/packages/backend/src/scripts/data-migration.ts b/packages/backend/src/scripts/data-migration.ts index 9aee813..c9dda15 100644 --- a/packages/backend/src/scripts/data-migration.ts +++ b/packages/backend/src/scripts/data-migration.ts @@ -128,7 +128,7 @@ async function migrateUsers() { ? tagsRes.rows[0].tags : JSON.parse(String(tagsRes.rows[0].tags || "[]")); } - } catch {} + } catch { /* tags column may not exist on older Directus installs */ } await query( `INSERT INTO users (id, email, password_hash, first_name, last_name, artist_name, slug, diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 00e3398..ed6de42 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -8,7 +8,8 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "start": "node ./build" + "start": "node ./build", + "check": "svelte-check --tsconfig ./tsconfig.json --threshold warning" }, "devDependencies": { "@iconify-json/ri": "^1.2.10", @@ -30,6 +31,7 @@ "prettier-plugin-svelte": "^3.5.1", "super-sitemap": "^1.0.7", "svelte": "^5.53.7", + "svelte-check": "^4.4.4", "svelte-sonner": "^1.0.8", "tailwind-merge": "^3.5.0", "tailwind-variants": "^3.2.2", diff --git a/packages/frontend/src/app.css b/packages/frontend/src/app.css index 888eeed..dd85862 100644 --- a/packages/frontend/src/app.css +++ b/packages/frontend/src/app.css @@ -5,6 +5,8 @@ @custom-variant dark (&:where(.dark, .dark *)); +@custom-variant hover (&:hover); + @theme { --animate-vibrate: vibrate 0.3s linear infinite; --animate-fade-in: fadeIn 0.3s ease-out; diff --git a/packages/frontend/src/hooks.server.ts b/packages/frontend/src/hooks.server.ts index b0b3456..f2bb3f0 100644 --- a/packages/frontend/src/hooks.server.ts +++ b/packages/frontend/src/hooks.server.ts @@ -2,12 +2,8 @@ import { isAuthenticated } from "$lib/services"; import { logger, generateRequestId } from "$lib/logger"; import type { Handle } from "@sveltejs/kit"; -// Log startup info once -let hasLoggedStartup = false; -if (!hasLoggedStartup) { - logger.startup(); - hasLoggedStartup = true; -} +// Log startup info once (module-level code runs exactly once on import) +logger.startup(); export const handle: Handle = async ({ event, resolve }) => { const { cookies, locals, url, request } = event; diff --git a/packages/frontend/src/lib/components/device-card/device-card.svelte b/packages/frontend/src/lib/components/device-card/device-card.svelte index 7c448dd..989810b 100644 --- a/packages/frontend/src/lib/components/device-card/device-card.svelte +++ b/packages/frontend/src/lib/components/device-card/device-card.svelte @@ -56,7 +56,7 @@ function isActive() {

{device.name}

@@ -139,7 +139,7 @@ function isActive() {
--> - {#each device.actuators as actuator, idx} + {#each device.actuators as actuator, idx (idx)}
- {#each data.models as model} + {#each data.models as model (model.slug)} @@ -128,7 +128,7 @@ const { data } = $props();
- {#each data.videos as video} + {#each data.videos as video (video.slug)} diff --git a/packages/frontend/src/routes/about/+page.svelte b/packages/frontend/src/routes/about/+page.svelte index 323f23d..97765a4 100644 --- a/packages/frontend/src/routes/about/+page.svelte +++ b/packages/frontend/src/routes/about/+page.svelte @@ -108,7 +108,7 @@ const values = [
- {#each stats as stat} + {#each stats as stat (stat.icon)}
- {#each values as value} + {#each values as value (value.title)} @@ -214,7 +214,7 @@ const values = [
- {#each team as member} + {#each team as member (member.name)} diff --git a/packages/frontend/src/routes/faq/+page.svelte b/packages/frontend/src/routes/faq/+page.svelte index 35b5f92..dfd08c6 100644 --- a/packages/frontend/src/routes/faq/+page.svelte +++ b/packages/frontend/src/routes/faq/+page.svelte @@ -1,5 +1,6 @@