- image-viewer: replace backdrop div with button for a11y
- file-drop-zone: wrap prop check in \$effect to avoid state_referenced_locally
- about: use \$derived for stats array
- magazine: use \$derived for featuredArticle
- play: add role/keyboard support to seek bar slider; fix \$state on SvelteMap in device-mapping-dialog
- admin/videos/[id]: add <track kind="captions"> to video element
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a `photo` field to the users table (and a migration) that serves
as a dedicated profile/card image for models. This is now used in model
cards and on the model single page, while `avatar` is reserved for
comments, article authors, and the user profile page.
- DB: `photo` column on `users` with FK to `files`
- GraphQL: exposed on ModelType, UserType, AdminUserDetailType; photoId arg on adminUpdateUser
- Services: photo field in MODELS_QUERY, MODEL_BY_SLUG_QUERY, ADMIN_GET/UPDATE_USER
- Frontend: model cards and single page use `photo ?? avatar` fallback
- Admin: model photo upload section in user edit page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend resolvers: typed enrichArticle/enrichVideo/enrichModel with DB
and $inferSelect types, SQL<unknown>[] for conditions arrays, proper
enum casts for status/role fields, $inferInsert for .set() updates,
typed raw SQL result rows in gamification, ReplyLike interface for
ctx.reply in auth. Frontend: typed catch blocks with Error/interface
casts, isActiveLink param, adminGetUser response, tags filter callback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unused `or` import in comments resolver
- Remove unused `users` import in recordings resolver
- Add index keys to pagination {#each} loops in videos, models, magazine
- Remove stale svelte-ignore comment in header (a11y warnings no longer fired)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add description field to ARTICLE_BY_SLUG_QUERY and render it in the
author bio card below the name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix login spinner (isLoading never set to true before await)
- Extract PageHero component, replace copy-pasted hero sections on videos/models/tags pages
- Replace inline plasma blobs with SexyBackground on videos/models/tags pages
- Make video/model/tag cards fully clickable (wrap in <a>), remove redundant Watch/View Profile buttons
- Convert inner overlay anchors to divs to avoid nested <a> elements
- Fix home page model avatar preset: mini → thumbnail (correct size for 96px display)
- Reduce home hero height: min-h-screen → min-h-[70vh]
- Remove dead hideName prop from Logo, simplify component
- Add brand name to mobile flyout panel header with gradient styling
- Remove dead _relatedVideos array, isBookmarked state, _handleBookmark from video detail page
- Clean up commented-out code blocks in video detail and models pages
- Note: tag card inner tag links converted to spans to avoid nested anchors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add POST /upload and DELETE /assets/:id routes to backend (session auth via session_token cookie)
- Add avatar arg to updateProfile GraphQL mutation and resolver
- Fix frontend to pass avatarId correctly on save, preserve existing avatar when unchanged
- Ignore 404 on file delete (already gone is fine)
- Remove broken folder lookup (getFolders is a stub, backend has no folder concept)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the drag-to-logout widget in favour of a clean inline layout:
- Desktop: avatar (links to /me), artist name, and a logout icon button
- Mobile flyout: user card with avatar, name, email, and logout button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename brand from SexyArt to Sexy throughout i18n locale
- Apply primary→accent gradient to SVG icon stroke
- Remove brand name text from logo, icon-only display
- Switch logo font to Noto Sans bold (default), drop Dancing Script
- Update favicons, app icons, webmanifest, and add logo.svg
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The edit page loaders were calling adminListVideos/adminListArticles with the
old pre-pagination signatures and filtering by ID client-side, which broke
after pagination limited results to 50. Now fetches the single item by ID
directly via new adminGetVideo and adminGetArticle backend queries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Public pages (videos, magazine, models): URL-driven search, sort, category/duration
filters, and Prev/Next pagination (page size 24)
- Admin tables (videos, articles): search input, toggle filters, and pagination (page size 50)
- Tags page: tag filtering now done server-side via DB arrayContains query instead of
fetching all items and filtering client-side
- Backend resolvers updated for videos, articles, models with paginated { items, total }
responses and filter/sort/tag args
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add "admin" back to User.role union to fix backend TS build
- Replace SVG PeonyIcon with logo.png image in Logo component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace static account card in mobile flyout with swipe-to-logout widget
- Remove redundant logout button from flyout bottom
- Make LogoutButton full-width via class prop and dynamic maxSlide
- Extract clean GraphQL error messages instead of raw JSON in all auth forms
- Add i18n keys for known backend errors (invalid credentials, email taken, invalid token)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add calendar + popover components and a custom DateTimePicker wrapper.
Video forms use date-only; article forms include a time picker.
Also add video player preview to the video edit form.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Initialize isOpen as false and only open in onMount if not yet verified,
instead of opening immediately and closing after localStorage check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Separate admin identity from role: viewer|model + is_admin boolean flag
- DB migration 0001_is_admin: adds column, migrates former admin role users
- Update ACL helpers, auth session, GraphQL types and all resolvers
- Admin layout guard and header links check is_admin instead of role
- Admin users table: show Admin badge next to name, remove admin from role select
- Admin user edit page: is_admin checkbox toggle
- Install shadcn Badge component; use in admin users table
- Fix duplicate photo keys in adminGetUser resolver
- Replace confirm() in /me recordings with Dialog component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: adminGetUser query returns user + photos; adminUpdateUser now
accepts avatarId/bannerId; new adminAddUserPhoto and adminRemoveUserPhoto
mutations; AdminUserDetailType added to GraphQL schema
- Frontend: /admin/users/[id] page for editing name, avatar, banner, and
managing the model photo gallery (upload multiple, delete individually)
- Admin users list: edit button per row linking to the detail page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The full-screen glassmorphism overlay had backdrop-blur-[0.5px] which
triggered GPU compositing on the entire viewport, degrading subpixel
text rendering inconsistently. Also use globalThis.fetch (not SvelteKit
fetch) when forwarding session token in admin SSR calls to avoid header
stripping.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Admin list queries (users, videos, articles) were using getGraphQLClient
without auth credentials, causing silent 403s on server-side loads. Now
extract session_token cookie and pass it to getAuthClient so the backend
sees the admin session on SSR requests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace inline mobile dropdown with sliding flyout panel from right
- Hide burger menu on lg breakpoint, desktop auth buttons use hidden lg:flex
- Add backdrop overlay with opacity transition
- Remove logo from flyout panel header
- Fix backdrop div accessibility with role="presentation"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create packages/types with shared TypeScript domain model interfaces (User, Video, Model, Article, Comment, Recording, etc.)
- Wire both frontend and backend packages to use @sexy.pivoine.art/types via workspace:*
- Update backend Pothos objectRef types to use shared interfaces instead of inline types
- Update frontend $lib/types.ts to re-export from shared package
- Fix all type errors introduced by more accurate nullable types (avatar/banner as string|null UUIDs, author nullable, events/device_info as object[])
- Add artist_name to comment user select in backend resolver
- Widen utility function signatures (getAssetUrl, getUserInitials, calcReadingTime) to accept null/undefined
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Removes Directus 11 and replaces it with a lean, purpose-built backend:
- packages/backend/: Fastify v5 + GraphQL Yoga v5 + Pothos (code-first)
with Drizzle ORM, Redis sessions (session_token cookie), argon2 auth,
Nodemailer, fluent-ffmpeg, and full gamification system ported from bundle
- Frontend: @directus/sdk replaced by graphql-request v7; services.ts fully
rewritten with identical signatures; directus.ts now re-exports from api.ts
- Cookie renamed directus_session_token → session_token
- Dev proxy target updated 8055 → 4000
- compose.yml: Directus service removed, backend service added (port 4000)
- Dockerfile.backend: new multi-stage image with ffmpeg
- Dockerfile: bundle build step and ffmpeg removed from frontend image
- data-migration.ts: one-time script to migrate all Directus/sexy_ tables
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Changed ?transform= to ?key= so Directus storage asset presets
(mini, thumbnail, preview, medium, banner) are actually applied.
Previously all images were served at full resolution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove size="sm" from SharingPopupButton to use default button height (h-9)
instead of small size (h-8), ensuring consistent button heights across all
action buttons on video, model, and article pages.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add GET /sexy/videos/:slug endpoint in bundle that bypasses Directus permissions
- Simplify getVideoBySlug to use the new custom endpoint instead of direct API call
- Fixes "Video not found" error on production for public video pages
- Custom endpoint uses database query like other public endpoints (/sexy/models, etc)
- Ensures videos are accessible to unauthenticated users while respecting upload_date
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>