diff --git a/.gitea/workflows/docker-build-backend.yml b/.gitea/workflows/docker-build-backend.yml index 221f5e1..830195a 100644 --- a/.gitea/workflows/docker-build-backend.yml +++ b/.gitea/workflows/docker-build-backend.yml @@ -6,7 +6,7 @@ on: - main - develop tags: - - 'v*.*.*' + - "v*.*.*" pull_request: branches: - main diff --git a/.gitea/workflows/docker-build-frontend.yml b/.gitea/workflows/docker-build-frontend.yml index 5540013..fb0ff94 100644 --- a/.gitea/workflows/docker-build-frontend.yml +++ b/.gitea/workflows/docker-build-frontend.yml @@ -6,7 +6,7 @@ on: - main - develop tags: - - 'v*.*.*' + - "v*.*.*" pull_request: branches: - main diff --git a/README.md b/README.md index 4245a86..a839121 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![sexy lips tongue mouth american apparel moist lip gloss ](https://i.gifer.com/1pYe.gif) -*"Lust und Liebe gehören zusammen - wer das eine verteufelt, zerstört auch das andere."* +_"Lust und Liebe gehören zusammen - wer das eine verteufelt, zerstört auch das andere."_ — **Beate Uhse**, Pionierin der sexuellen Befreiung ✈️ --- @@ -104,10 +104,10 @@ docker compose up -d **Prerequisites:** -1. Node.js 20.19.1 — *the foundation* -2. `corepack enable` — *unlock the tools* -3. `pnpm install` — *gather your ingredients* -4. PostgreSQL 16 + Redis — *the data lovers* +1. Node.js 20.19.1 — _the foundation_ +2. `corepack enable` — _unlock the tools_ +3. `pnpm install` — _gather your ingredients_ +4. PostgreSQL 16 + Redis — _the data lovers_ **Start your pleasure journey:** @@ -198,13 +198,13 @@ Every request: Assets are transformed on first request and cached as WebP: -| Preset | Size | Fit | Use | -|--------|------|-----|-----| -| `mini` | 80×80 | cover | Avatars in lists | -| `thumbnail` | 300×300 | cover | Profile photos | -| `preview` | 800px wide | inside | Video teasers | -| `medium` | 1400px wide | inside | Full-size images | -| `banner` | 1600×480 | cover | Profile banners | +| Preset | Size | Fit | Use | +| ----------- | ----------- | ------ | ---------------- | +| `mini` | 80×80 | cover | Avatars in lists | +| `thumbnail` | 300×300 | cover | Profile photos | +| `preview` | 800px wide | inside | Video teasers | +| `medium` | 1400px wide | inside | Full-size images | +| `banner` | 1600×480 | cover | Profile banners | --- @@ -276,33 +276,33 @@ graph LR ### Backend (required) -| Variable | Description | -|----------|-------------| -| `DATABASE_URL` | PostgreSQL connection string | -| `REDIS_URL` | Redis connection string | +| Variable | Description | +| --------------- | ----------------------------- | +| `DATABASE_URL` | PostgreSQL connection string | +| `REDIS_URL` | Redis connection string | | `COOKIE_SECRET` | Session cookie signing secret | -| `CORS_ORIGIN` | Allowed frontend origin | -| `UPLOAD_DIR` | Path for uploaded files | +| `CORS_ORIGIN` | Allowed frontend origin | +| `UPLOAD_DIR` | Path for uploaded files | ### Backend (optional) -| Variable | Default | Description | -|----------|---------|-------------| -| `PORT` | `4000` | Backend listen port | -| `LOG_LEVEL` | `info` | Fastify log level | -| `SMTP_HOST` | — | Email server for auth flows | -| `SMTP_PORT` | `587` | Email server port | -| `EMAIL_FROM` | — | Sender address | -| `PUBLIC_URL` | — | Frontend URL (for email links) | +| Variable | Default | Description | +| ------------ | ------- | ------------------------------ | +| `PORT` | `4000` | Backend listen port | +| `LOG_LEVEL` | `info` | Fastify log level | +| `SMTP_HOST` | — | Email server for auth flows | +| `SMTP_PORT` | `587` | Email server port | +| `EMAIL_FROM` | — | Sender address | +| `PUBLIC_URL` | — | Frontend URL (for email links) | ### Frontend -| Variable | Description | -|----------|-------------| -| `PUBLIC_API_URL` | Backend URL (e.g. `http://sexy_backend:4000`) | -| `PUBLIC_URL` | Frontend public URL | -| `PUBLIC_UMAMI_ID` | Umami analytics site ID (optional) | -| `PUBLIC_UMAMI_SCRIPT` | Umami script URL (optional) | +| Variable | Description | +| --------------------- | --------------------------------------------- | +| `PUBLIC_API_URL` | Backend URL (e.g. `http://sexy_backend:4000`) | +| `PUBLIC_URL` | Frontend public URL | +| `PUBLIC_UMAMI_ID` | Umami analytics site ID (optional) | +| `PUBLIC_UMAMI_SCRIPT` | Umami script URL (optional) | --- @@ -314,23 +314,23 @@ graph LR **[Palina](https://sexy.pivoine.art) & [Valknar](https://sexy.pivoine.art)** -*Für die Mäuse...* 🐭💕 +_Für die Mäuse..._ 🐭💕 --- ### 🙏 Built With -| Technology | Purpose | -|------------|---------| -| [SvelteKit](https://kit.svelte.dev/) | Frontend framework | -| [Fastify](https://fastify.dev/) | HTTP server | -| [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) | GraphQL server | -| [Pothos](https://pothos-graphql.dev/) | Code-first schema | -| [Drizzle ORM](https://orm.drizzle.team/) | Database | -| [Sharp](https://sharp.pixelplumbing.com/) | Image transforms | -| [Buttplug.io](https://buttplug.io/) | Hardware | -| [bits-ui](https://www.bits-ui.com/) | UI components | -| [Gitea](https://dev.pivoine.art) | Self-hosted VCS & CI | +| Technology | Purpose | +| --------------------------------------------------------- | -------------------- | +| [SvelteKit](https://kit.svelte.dev/) | Frontend framework | +| [Fastify](https://fastify.dev/) | HTTP server | +| [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) | GraphQL server | +| [Pothos](https://pothos-graphql.dev/) | Code-first schema | +| [Drizzle ORM](https://orm.drizzle.team/) | Database | +| [Sharp](https://sharp.pixelplumbing.com/) | Image transforms | +| [Buttplug.io](https://buttplug.io/) | Hardware | +| [bits-ui](https://www.bits-ui.com/) | UI components | +| [Gitea](https://dev.pivoine.art) | Self-hosted VCS & CI | --- @@ -339,7 +339,7 @@ graph LR Pioneer of sexual liberation (1919-2001) Pilot, Entrepreneur, Freedom Fighter -*"Eine Frau, die ihre Sexualität selbstbestimmt lebt, ist eine freie Frau."* +_"Eine Frau, die ihre Sexualität selbstbestimmt lebt, ist eine freie Frau."_ ![Beate Uhse Quote](https://img.shields.io/badge/Beate_Uhse-Sexual_Liberation_Pioneer-FF1493?style=for-the-badge&logo=heart&logoColor=white&labelColor=8B008B) @@ -381,7 +381,7 @@ Pilot, Entrepreneur, Freedom Fighter ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ -*Pleasure is a human right. Technology is freedom. Together, they are power.* +_Pleasure is a human right. Technology is freedom. Together, they are power._ **[sexy.pivoine.art](https://sexy.pivoine.art)** | © 2025 Palina & Valknar diff --git a/package.json b/package.json index 1e28bf1..081052e 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,49 @@ { - "name": "sexy.pivoine.art", - "version": "1.0.0", - "description": "", - "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", - "lint": "eslint .", - "lint:fix": "eslint . --fix", - "format": "prettier --write .", - "format:check": "prettier --check .", - "check": "pnpm -r --filter=!sexy.pivoine.art check" - }, - "keywords": [], - "author": { - "name": "Valknar", - "email": "valknar@pivoine.art" - }, - "license": "MIT", - "packageManager": "pnpm@10.19.0", - "pnpm": { - "onlyBuiltDependencies": [ - "argon2", - "es5-ext", - "esbuild", - "svelte-preprocess", - "wasm-pack" - ], - "ignoredBuiltDependencies": [ - "@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" - } + "name": "sexy.pivoine.art", + "version": "1.0.0", + "description": "", + "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", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "check": "pnpm -r --filter=!sexy.pivoine.art check" + }, + "keywords": [], + "author": { + "name": "Valknar", + "email": "valknar@pivoine.art" + }, + "license": "MIT", + "packageManager": "pnpm@10.19.0", + "pnpm": { + "onlyBuiltDependencies": [ + "argon2", + "es5-ext", + "esbuild", + "svelte-preprocess", + "wasm-pack" + ], + "ignoredBuiltDependencies": [ + "@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", + "prettier-plugin-svelte": "^3.5.1", + "typescript-eslint": "^8.56.1" + } } diff --git a/packages/backend/src/db/schema/articles.ts b/packages/backend/src/db/schema/articles.ts index c79b742..34dd731 100644 --- a/packages/backend/src/db/schema/articles.ts +++ b/packages/backend/src/db/schema/articles.ts @@ -1,18 +1,13 @@ -import { - pgTable, - text, - timestamp, - boolean, - index, - uniqueIndex, -} from "drizzle-orm/pg-core"; +import { pgTable, text, timestamp, boolean, index, uniqueIndex } from "drizzle-orm/pg-core"; import { users } from "./users"; import { files } from "./files"; export const articles = pgTable( "articles", { - id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), slug: text("slug").notNull(), title: text("title").notNull(), excerpt: text("excerpt"), diff --git a/packages/backend/src/db/schema/comments.ts b/packages/backend/src/db/schema/comments.ts index 0097449..57da478 100644 --- a/packages/backend/src/db/schema/comments.ts +++ b/packages/backend/src/db/schema/comments.ts @@ -1,10 +1,4 @@ -import { - pgTable, - text, - timestamp, - index, - integer, -} from "drizzle-orm/pg-core"; +import { pgTable, text, timestamp, index, integer } from "drizzle-orm/pg-core"; import { users } from "./users"; export const comments = pgTable( diff --git a/packages/backend/src/db/schema/files.ts b/packages/backend/src/db/schema/files.ts index 7c2c2a9..5066523 100644 --- a/packages/backend/src/db/schema/files.ts +++ b/packages/backend/src/db/schema/files.ts @@ -1,16 +1,11 @@ -import { - pgTable, - text, - timestamp, - bigint, - integer, - index, -} from "drizzle-orm/pg-core"; +import { pgTable, text, timestamp, bigint, integer, index } from "drizzle-orm/pg-core"; export const files = pgTable( "files", { - id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), title: text("title"), description: text("description"), filename: text("filename").notNull(), diff --git a/packages/backend/src/db/schema/gamification.ts b/packages/backend/src/db/schema/gamification.ts index 35252df..80cacaf 100644 --- a/packages/backend/src/db/schema/gamification.ts +++ b/packages/backend/src/db/schema/gamification.ts @@ -11,15 +11,14 @@ import { import { users } from "./users"; import { recordings } from "./recordings"; -export const achievementStatusEnum = pgEnum("achievement_status", [ - "draft", - "published", -]); +export const achievementStatusEnum = pgEnum("achievement_status", ["draft", "published"]); export const achievements = pgTable( "achievements", { - id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), code: text("code").notNull(), name: text("name").notNull(), description: text("description"), diff --git a/packages/backend/src/db/schema/recordings.ts b/packages/backend/src/db/schema/recordings.ts index 411c637..e5d7139 100644 --- a/packages/backend/src/db/schema/recordings.ts +++ b/packages/backend/src/db/schema/recordings.ts @@ -12,16 +12,14 @@ import { import { users } from "./users"; import { videos } from "./videos"; -export const recordingStatusEnum = pgEnum("recording_status", [ - "draft", - "published", - "archived", -]); +export const recordingStatusEnum = pgEnum("recording_status", ["draft", "published", "archived"]); export const recordings = pgTable( "recordings", { - id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), title: text("title").notNull(), description: text("description"), slug: text("slug").notNull(), @@ -53,7 +51,9 @@ export const recordings = pgTable( export const recording_plays = pgTable( "recording_plays", { - id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), recording_id: text("recording_id") .notNull() .references(() => recordings.id, { onDelete: "cascade" }), diff --git a/packages/backend/src/db/schema/users.ts b/packages/backend/src/db/schema/users.ts index ff89598..2efe8d2 100644 --- a/packages/backend/src/db/schema/users.ts +++ b/packages/backend/src/db/schema/users.ts @@ -15,7 +15,9 @@ export const roleEnum = pgEnum("user_role", ["model", "viewer", "admin"]); export const users = pgTable( "users", { - id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), email: text("email").notNull(), password_hash: text("password_hash").notNull(), first_name: text("first_name"), diff --git a/packages/backend/src/db/schema/videos.ts b/packages/backend/src/db/schema/videos.ts index 966d76b..3f1a355 100644 --- a/packages/backend/src/db/schema/videos.ts +++ b/packages/backend/src/db/schema/videos.ts @@ -14,7 +14,9 @@ import { files } from "./files"; export const videos = pgTable( "videos", { - id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), slug: text("slug").notNull(), title: text("title").notNull(), description: text("description"), @@ -50,7 +52,9 @@ export const video_models = pgTable( export const video_likes = pgTable( "video_likes", { - id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), video_id: text("video_id") .notNull() .references(() => videos.id, { onDelete: "cascade" }), @@ -68,7 +72,9 @@ export const video_likes = pgTable( export const video_plays = pgTable( "video_plays", { - id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), video_id: text("video_id") .notNull() .references(() => videos.id, { onDelete: "cascade" }), diff --git a/packages/backend/src/graphql/resolvers/comments.ts b/packages/backend/src/graphql/resolvers/comments.ts index c58f1de..c918665 100644 --- a/packages/backend/src/graphql/resolvers/comments.ts +++ b/packages/backend/src/graphql/resolvers/comments.ts @@ -21,7 +21,12 @@ builder.queryField("commentsForVideo", (t) => return Promise.all( commentList.map(async (c: any) => { const user = await ctx.db - .select({ id: users.id, first_name: users.first_name, last_name: users.last_name, avatar: users.avatar }) + .select({ + id: users.id, + first_name: users.first_name, + last_name: users.last_name, + avatar: users.avatar, + }) .from(users) .where(eq(users.id, c.user_id)) .limit(1); @@ -57,7 +62,12 @@ builder.mutationField("createCommentForVideo", (t) => await checkAchievements(ctx.db, ctx.currentUser.id, "social"); const user = await ctx.db - .select({ id: users.id, first_name: users.first_name, last_name: users.last_name, avatar: users.avatar }) + .select({ + id: users.id, + first_name: users.first_name, + last_name: users.last_name, + avatar: users.avatar, + }) .from(users) .where(eq(users.id, ctx.currentUser.id)) .limit(1); diff --git a/packages/backend/src/graphql/resolvers/gamification.ts b/packages/backend/src/graphql/resolvers/gamification.ts index 4cd6543..810f078 100644 --- a/packages/backend/src/graphql/resolvers/gamification.ts +++ b/packages/backend/src/graphql/resolvers/gamification.ts @@ -1,6 +1,12 @@ import { builder } from "../builder"; import { LeaderboardEntryType, UserGamificationType, AchievementType } from "../types/index"; -import { user_stats, users, user_achievements, achievements, user_points } from "../../db/schema/index"; +import { + user_stats, + users, + user_achievements, + achievements, + user_points, +} from "../../db/schema/index"; import { eq, desc, gt, count, isNotNull, and } from "drizzle-orm"; builder.queryField("leaderboard", (t) => @@ -73,7 +79,12 @@ builder.queryField("userGamification", (t) => }) .from(user_achievements) .leftJoin(achievements, eq(user_achievements.achievement_id, achievements.id)) - .where(and(eq(user_achievements.user_id, args.userId), isNotNull(user_achievements.date_unlocked))) + .where( + and( + eq(user_achievements.user_id, args.userId), + isNotNull(user_achievements.date_unlocked), + ), + ) .orderBy(desc(user_achievements.date_unlocked)); const recentPoints = await ctx.db diff --git a/packages/backend/src/graphql/resolvers/recordings.ts b/packages/backend/src/graphql/resolvers/recordings.ts index e6e2507..9364b49 100644 --- a/packages/backend/src/graphql/resolvers/recordings.ts +++ b/packages/backend/src/graphql/resolvers/recordings.ts @@ -162,11 +162,13 @@ builder.mutationField("updateRecording", (t) => updates.title = args.title; updates.slug = slugify(args.title); } - if (args.description !== null && args.description !== undefined) updates.description = args.description; + if (args.description !== null && args.description !== undefined) + updates.description = args.description; if (args.tags !== null && args.tags !== undefined) updates.tags = args.tags; if (args.status !== null && args.status !== undefined) updates.status = args.status; if (args.public !== null && args.public !== undefined) updates.public = args.public; - if (args.linkedVideoId !== null && args.linkedVideoId !== undefined) updates.linked_video = args.linkedVideoId; + if (args.linkedVideoId !== null && args.linkedVideoId !== undefined) + updates.linked_video = args.linkedVideoId; const updated = await ctx.db .update(recordings) @@ -319,11 +321,20 @@ builder.mutationField("updateRecordingPlay", (t) => await ctx.db .update(recording_plays) - .set({ duration_played: args.durationPlayed, completed: args.completed, date_updated: new Date() }) + .set({ + duration_played: args.durationPlayed, + completed: args.completed, + date_updated: new Date(), + }) .where(eq(recording_plays.id, args.playId)); if (args.completed && !wasCompleted && ctx.currentUser) { - await awardPoints(ctx.db, ctx.currentUser.id, "RECORDING_COMPLETE", existing[0].recording_id); + await awardPoints( + ctx.db, + ctx.currentUser.id, + "RECORDING_COMPLETE", + existing[0].recording_id, + ); await checkAchievements(ctx.db, ctx.currentUser.id, "playback"); } diff --git a/packages/backend/src/graphql/resolvers/stats.ts b/packages/backend/src/graphql/resolvers/stats.ts index 632243b..37586a1 100644 --- a/packages/backend/src/graphql/resolvers/stats.ts +++ b/packages/backend/src/graphql/resolvers/stats.ts @@ -15,9 +15,7 @@ builder.queryField("stats", (t) => .select({ count: count() }) .from(users) .where(eq(users.role, "viewer")); - const videosCount = await ctx.db - .select({ count: count() }) - .from(videos); + const videosCount = await ctx.db.select({ count: count() }).from(videos); return { models_count: modelsCount[0]?.count || 0, diff --git a/packages/backend/src/graphql/resolvers/users.ts b/packages/backend/src/graphql/resolvers/users.ts index 31feb75..343f460 100644 --- a/packages/backend/src/graphql/resolvers/users.ts +++ b/packages/backend/src/graphql/resolvers/users.ts @@ -28,11 +28,7 @@ builder.queryField("userProfile", (t) => id: t.arg.string({ required: true }), }, resolve: async (_root, args, ctx) => { - const user = await ctx.db - .select() - .from(users) - .where(eq(users.id, args.id)) - .limit(1); + const user = await ctx.db.select().from(users).where(eq(users.id, args.id)).limit(1); return user[0] || null; }, }), @@ -53,13 +49,19 @@ builder.mutationField("updateProfile", (t) => if (!ctx.currentUser) throw new GraphQLError("Unauthorized"); const updates: Record = { date_updated: new Date() }; - if (args.firstName !== undefined && args.firstName !== null) updates.first_name = args.firstName; + if (args.firstName !== undefined && args.firstName !== null) + updates.first_name = args.firstName; if (args.lastName !== undefined && args.lastName !== null) updates.last_name = args.lastName; - if (args.artistName !== undefined && args.artistName !== null) updates.artist_name = args.artistName; - if (args.description !== undefined && args.description !== null) updates.description = args.description; + if (args.artistName !== undefined && args.artistName !== null) + updates.artist_name = args.artistName; + if (args.description !== undefined && args.description !== null) + updates.description = args.description; if (args.tags !== undefined && args.tags !== null) updates.tags = args.tags; - await ctx.db.update(users).set(updates as any).where(eq(users.id, ctx.currentUser.id)); + await ctx.db + .update(users) + .set(updates as any) + .where(eq(users.id, ctx.currentUser.id)); const updated = await ctx.db .select() diff --git a/packages/backend/src/graphql/resolvers/videos.ts b/packages/backend/src/graphql/resolvers/videos.ts index 9b89f1e..f61ba98 100644 --- a/packages/backend/src/graphql/resolvers/videos.ts +++ b/packages/backend/src/graphql/resolvers/videos.ts @@ -1,7 +1,19 @@ import { GraphQLError } from "graphql"; import { builder } from "../builder"; -import { VideoType, VideoLikeResponseType, VideoPlayResponseType, VideoLikeStatusType } from "../types/index"; -import { videos, video_models, video_likes, video_plays, users, files } from "../../db/schema/index"; +import { + VideoType, + VideoLikeResponseType, + VideoPlayResponseType, + VideoLikeStatusType, +} from "../types/index"; +import { + videos, + video_models, + video_likes, + video_plays, + users, + files, +} from "../../db/schema/index"; import { eq, and, lte, desc, inArray, count } from "drizzle-orm"; async function enrichVideo(db: any, video: any) { @@ -25,8 +37,14 @@ async function enrichVideo(db: any, video: any) { } // Count likes - const likesCount = await db.select({ count: count() }).from(video_likes).where(eq(video_likes.video_id, video.id)); - const playsCount = await db.select({ count: count() }).from(video_plays).where(eq(video_plays.video_id, video.id)); + const likesCount = await db + .select({ count: count() }) + .from(video_likes) + .where(eq(video_likes.video_id, video.id)); + const playsCount = await db + .select({ count: count() }) + .from(video_plays) + .where(eq(video_plays.video_id, video.id)); return { ...video, @@ -63,10 +81,15 @@ builder.queryField("videos", (t) => query = ctx.db .select({ v: videos }) .from(videos) - .where(and( - lte(videos.upload_date, new Date()), - inArray(videos.id, videoIds.map((v: any) => v.video_id)), - )) + .where( + and( + lte(videos.upload_date, new Date()), + inArray( + videos.id, + videoIds.map((v: any) => v.video_id), + ), + ), + ) .orderBy(desc(videos.upload_date)); } @@ -74,10 +97,7 @@ builder.queryField("videos", (t) => query = ctx.db .select({ v: videos }) .from(videos) - .where(and( - lte(videos.upload_date, new Date()), - eq(videos.featured, args.featured), - )) + .where(and(lte(videos.upload_date, new Date()), eq(videos.featured, args.featured))) .orderBy(desc(videos.upload_date)); } @@ -123,7 +143,9 @@ builder.queryField("videoLikeStatus", (t) => const existing = await ctx.db .select() .from(video_likes) - .where(and(eq(video_likes.video_id, args.videoId), eq(video_likes.user_id, ctx.currentUser.id))) + .where( + and(eq(video_likes.video_id, args.videoId), eq(video_likes.user_id, ctx.currentUser.id)), + ) .limit(1); return { liked: existing.length > 0 }; }, @@ -142,7 +164,9 @@ builder.mutationField("likeVideo", (t) => const existing = await ctx.db .select() .from(video_likes) - .where(and(eq(video_likes.video_id, args.videoId), eq(video_likes.user_id, ctx.currentUser.id))) + .where( + and(eq(video_likes.video_id, args.videoId), eq(video_likes.user_id, ctx.currentUser.id)), + ) .limit(1); if (existing.length > 0) throw new GraphQLError("Already liked"); @@ -154,10 +178,22 @@ builder.mutationField("likeVideo", (t) => await ctx.db .update(videos) - .set({ likes_count: (await ctx.db.select({ c: videos.likes_count }).from(videos).where(eq(videos.id, args.videoId)).limit(1))[0]?.c as number + 1 || 1 }) + .set({ + likes_count: + (( + await ctx.db + .select({ c: videos.likes_count }) + .from(videos) + .where(eq(videos.id, args.videoId)) + .limit(1) + )[0]?.c as number) + 1 || 1, + }) .where(eq(videos.id, args.videoId)); - const likesCount = await ctx.db.select({ count: count() }).from(video_likes).where(eq(video_likes.video_id, args.videoId)); + const likesCount = await ctx.db + .select({ count: count() }) + .from(video_likes) + .where(eq(video_likes.video_id, args.videoId)); return { liked: true, likes_count: likesCount[0]?.count || 1 }; }, }), @@ -175,21 +211,39 @@ builder.mutationField("unlikeVideo", (t) => const existing = await ctx.db .select() .from(video_likes) - .where(and(eq(video_likes.video_id, args.videoId), eq(video_likes.user_id, ctx.currentUser.id))) + .where( + and(eq(video_likes.video_id, args.videoId), eq(video_likes.user_id, ctx.currentUser.id)), + ) .limit(1); if (existing.length === 0) throw new GraphQLError("Not liked"); await ctx.db .delete(video_likes) - .where(and(eq(video_likes.video_id, args.videoId), eq(video_likes.user_id, ctx.currentUser.id))); + .where( + and(eq(video_likes.video_id, args.videoId), eq(video_likes.user_id, ctx.currentUser.id)), + ); await ctx.db .update(videos) - .set({ likes_count: Math.max(((await ctx.db.select({ c: videos.likes_count }).from(videos).where(eq(videos.id, args.videoId)).limit(1))[0]?.c as number || 1) - 1, 0) }) + .set({ + likes_count: Math.max( + ((( + await ctx.db + .select({ c: videos.likes_count }) + .from(videos) + .where(eq(videos.id, args.videoId)) + .limit(1) + )[0]?.c as number) || 1) - 1, + 0, + ), + }) .where(eq(videos.id, args.videoId)); - const likesCount = await ctx.db.select({ count: count() }).from(video_likes).where(eq(video_likes.video_id, args.videoId)); + const likesCount = await ctx.db + .select({ count: count() }) + .from(video_likes) + .where(eq(video_likes.video_id, args.videoId)); return { liked: false, likes_count: likesCount[0]?.count || 0 }; }, }), @@ -203,13 +257,19 @@ builder.mutationField("recordVideoPlay", (t) => sessionId: t.arg.string(), }, resolve: async (_root, args, ctx) => { - const play = await ctx.db.insert(video_plays).values({ - video_id: args.videoId, - user_id: ctx.currentUser?.id || null, - session_id: args.sessionId || null, - }).returning({ id: video_plays.id }); + const play = await ctx.db + .insert(video_plays) + .values({ + video_id: args.videoId, + user_id: ctx.currentUser?.id || null, + session_id: args.sessionId || null, + }) + .returning({ id: video_plays.id }); - const playsCount = await ctx.db.select({ count: count() }).from(video_plays).where(eq(video_plays.video_id, args.videoId)); + const playsCount = await ctx.db + .select({ count: count() }) + .from(video_plays) + .where(eq(video_plays.video_id, args.videoId)); await ctx.db .update(videos) @@ -237,7 +297,11 @@ builder.mutationField("updateVideoPlay", (t) => resolve: async (_root, args, ctx) => { await ctx.db .update(video_plays) - .set({ duration_watched: args.durationWatched, completed: args.completed, date_updated: new Date() }) + .set({ + duration_watched: args.durationWatched, + completed: args.completed, + date_updated: new Date(), + }) .where(eq(video_plays.id, args.playId)); return true; }, @@ -262,13 +326,26 @@ builder.queryField("analytics", (t) => .where(eq(video_models.user_id, userId)); if (modelVideoIds.length === 0) { - return { total_videos: 0, total_likes: 0, total_plays: 0, plays_by_date: {}, likes_by_date: {}, videos: [] }; + return { + total_videos: 0, + total_likes: 0, + total_plays: 0, + plays_by_date: {}, + likes_by_date: {}, + videos: [], + }; } const videoIds = modelVideoIds.map((v: any) => v.video_id); const videoList = await ctx.db.select().from(videos).where(inArray(videos.id, videoIds)); - const plays = await ctx.db.select().from(video_plays).where(inArray(video_plays.video_id, videoIds)); - const likes = await ctx.db.select().from(video_likes).where(inArray(video_likes.video_id, videoIds)); + const plays = await ctx.db + .select() + .from(video_plays) + .where(inArray(video_plays.video_id, videoIds)); + const likes = await ctx.db + .select() + .from(video_likes) + .where(inArray(video_likes.video_id, videoIds)); const totalLikes = videoList.reduce((sum, v) => sum + (v.likes_count || 0), 0); const totalPlays = videoList.reduce((sum, v) => sum + (v.plays_count || 0), 0); @@ -290,9 +367,10 @@ builder.queryField("analytics", (t) => const videoAnalytics = videoList.map((video) => { const vPlays = plays.filter((p) => p.video_id === video.id); const completedPlays = vPlays.filter((p) => p.completed).length; - const avgWatchTime = vPlays.length > 0 - ? vPlays.reduce((sum, p) => sum + (p.duration_watched || 0), 0) / vPlays.length - : 0; + const avgWatchTime = + vPlays.length > 0 + ? vPlays.reduce((sum, p) => sum + (p.duration_watched || 0), 0) / vPlays.length + : 0; return { id: video.id, diff --git a/packages/backend/src/graphql/types/index.ts b/packages/backend/src/graphql/types/index.ts index a61c920..1b6ab61 100644 --- a/packages/backend/src/graphql/types/index.ts +++ b/packages/backend/src/graphql/types/index.ts @@ -1,371 +1,461 @@ import { builder } from "../builder"; // File type -export const FileType = builder.objectRef<{ - id: string; - title: string | null; - description: string | null; - filename: string; - mime_type: string | null; - filesize: number | null; - duration: number | null; - uploaded_by: string | null; - date_created: Date; -}>("File").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - title: t.exposeString("title", { nullable: true }), - description: t.exposeString("description", { nullable: true }), - filename: t.exposeString("filename"), - mime_type: t.exposeString("mime_type", { nullable: true }), - filesize: t.exposeFloat("filesize", { nullable: true }), - duration: t.exposeInt("duration", { nullable: true }), - uploaded_by: t.exposeString("uploaded_by", { nullable: true }), - date_created: t.expose("date_created", { type: "DateTime" }), - }), -}); +export const FileType = builder + .objectRef<{ + id: string; + title: string | null; + description: string | null; + filename: string; + mime_type: string | null; + filesize: number | null; + duration: number | null; + uploaded_by: string | null; + date_created: Date; + }>("File") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + title: t.exposeString("title", { nullable: true }), + description: t.exposeString("description", { nullable: true }), + filename: t.exposeString("filename"), + mime_type: t.exposeString("mime_type", { nullable: true }), + filesize: t.exposeFloat("filesize", { nullable: true }), + duration: t.exposeInt("duration", { nullable: true }), + uploaded_by: t.exposeString("uploaded_by", { nullable: true }), + date_created: t.expose("date_created", { type: "DateTime" }), + }), + }); // User type -export const UserType = builder.objectRef<{ - id: string; - email: string; - first_name: string | null; - last_name: string | null; - artist_name: string | null; - slug: string | null; - description: string | null; - tags: string[] | null; - role: "model" | "viewer" | "admin"; - avatar: string | null; - banner: string | null; - email_verified: boolean; - date_created: Date; -}>("User").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - email: t.exposeString("email"), - first_name: t.exposeString("first_name", { nullable: true }), - last_name: t.exposeString("last_name", { nullable: true }), - artist_name: t.exposeString("artist_name", { nullable: true }), - slug: t.exposeString("slug", { nullable: true }), - description: t.exposeString("description", { nullable: true }), - tags: t.exposeStringList("tags", { nullable: true }), - role: t.exposeString("role"), - avatar: t.exposeString("avatar", { nullable: true }), - banner: t.exposeString("banner", { nullable: true }), - email_verified: t.exposeBoolean("email_verified"), - date_created: t.expose("date_created", { type: "DateTime" }), - }), -}); +export const UserType = builder + .objectRef<{ + id: string; + email: string; + first_name: string | null; + last_name: string | null; + artist_name: string | null; + slug: string | null; + description: string | null; + tags: string[] | null; + role: "model" | "viewer" | "admin"; + avatar: string | null; + banner: string | null; + email_verified: boolean; + date_created: Date; + }>("User") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + email: t.exposeString("email"), + first_name: t.exposeString("first_name", { nullable: true }), + last_name: t.exposeString("last_name", { nullable: true }), + artist_name: t.exposeString("artist_name", { nullable: true }), + slug: t.exposeString("slug", { nullable: true }), + description: t.exposeString("description", { nullable: true }), + tags: t.exposeStringList("tags", { nullable: true }), + role: t.exposeString("role"), + avatar: t.exposeString("avatar", { nullable: true }), + banner: t.exposeString("banner", { nullable: true }), + email_verified: t.exposeBoolean("email_verified"), + date_created: t.expose("date_created", { type: "DateTime" }), + }), + }); // CurrentUser type (same shape, used for auth context) -export const CurrentUserType = builder.objectRef<{ - id: string; - email: string; - first_name: string | null; - last_name: string | null; - artist_name: string | null; - slug: string | null; - description: string | null; - tags: string[] | null; - role: "model" | "viewer" | "admin"; - avatar: string | null; - banner: string | null; - email_verified: boolean; - date_created: Date; -}>("CurrentUser").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - email: t.exposeString("email"), - first_name: t.exposeString("first_name", { nullable: true }), - last_name: t.exposeString("last_name", { nullable: true }), - artist_name: t.exposeString("artist_name", { nullable: true }), - slug: t.exposeString("slug", { nullable: true }), - description: t.exposeString("description", { nullable: true }), - tags: t.exposeStringList("tags", { nullable: true }), - role: t.exposeString("role"), - avatar: t.exposeString("avatar", { nullable: true }), - banner: t.exposeString("banner", { nullable: true }), - email_verified: t.exposeBoolean("email_verified"), - date_created: t.expose("date_created", { type: "DateTime" }), - }), -}); +export const CurrentUserType = builder + .objectRef<{ + id: string; + email: string; + first_name: string | null; + last_name: string | null; + artist_name: string | null; + slug: string | null; + description: string | null; + tags: string[] | null; + role: "model" | "viewer" | "admin"; + avatar: string | null; + banner: string | null; + email_verified: boolean; + date_created: Date; + }>("CurrentUser") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + email: t.exposeString("email"), + first_name: t.exposeString("first_name", { nullable: true }), + last_name: t.exposeString("last_name", { nullable: true }), + artist_name: t.exposeString("artist_name", { nullable: true }), + slug: t.exposeString("slug", { nullable: true }), + description: t.exposeString("description", { nullable: true }), + tags: t.exposeStringList("tags", { nullable: true }), + role: t.exposeString("role"), + avatar: t.exposeString("avatar", { nullable: true }), + banner: t.exposeString("banner", { nullable: true }), + email_verified: t.exposeBoolean("email_verified"), + date_created: t.expose("date_created", { type: "DateTime" }), + }), + }); // Video type -export const VideoType = builder.objectRef<{ - id: string; - slug: string; - title: string; - description: string | null; - image: string | null; - movie: string | null; - tags: string[] | null; - upload_date: Date; - premium: boolean | null; - featured: boolean | null; - likes_count: number | null; - plays_count: number | null; - models?: { id: string; artist_name: string | null; slug: string | null; avatar: string | null }[]; - movie_file?: { id: string; filename: string; mime_type: string | null; duration: number | null } | null; -}>("Video").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - slug: t.exposeString("slug"), - title: t.exposeString("title"), - description: t.exposeString("description", { nullable: true }), - image: t.exposeString("image", { nullable: true }), - movie: t.exposeString("movie", { nullable: true }), - tags: t.exposeStringList("tags", { nullable: true }), - upload_date: t.expose("upload_date", { type: "DateTime" }), - premium: t.exposeBoolean("premium", { nullable: true }), - featured: t.exposeBoolean("featured", { nullable: true }), - likes_count: t.exposeInt("likes_count", { nullable: true }), - plays_count: t.exposeInt("plays_count", { nullable: true }), - models: t.expose("models", { type: [VideoModelType], nullable: true }), - movie_file: t.expose("movie_file", { type: VideoFileType, nullable: true }), - }), -}); +export const VideoType = builder + .objectRef<{ + id: string; + slug: string; + title: string; + description: string | null; + image: string | null; + movie: string | null; + tags: string[] | null; + upload_date: Date; + premium: boolean | null; + featured: boolean | null; + likes_count: number | null; + plays_count: number | null; + models?: { + id: string; + artist_name: string | null; + slug: string | null; + avatar: string | null; + }[]; + movie_file?: { + id: string; + filename: string; + mime_type: string | null; + duration: number | null; + } | null; + }>("Video") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + slug: t.exposeString("slug"), + title: t.exposeString("title"), + description: t.exposeString("description", { nullable: true }), + image: t.exposeString("image", { nullable: true }), + movie: t.exposeString("movie", { nullable: true }), + tags: t.exposeStringList("tags", { nullable: true }), + upload_date: t.expose("upload_date", { type: "DateTime" }), + premium: t.exposeBoolean("premium", { nullable: true }), + featured: t.exposeBoolean("featured", { nullable: true }), + likes_count: t.exposeInt("likes_count", { nullable: true }), + plays_count: t.exposeInt("plays_count", { nullable: true }), + models: t.expose("models", { type: [VideoModelType], nullable: true }), + movie_file: t.expose("movie_file", { type: VideoFileType, nullable: true }), + }), + }); -export const VideoModelType = builder.objectRef<{ - id: string; - artist_name: string | null; - slug: string | null; - avatar: string | null; -}>("VideoModel").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - artist_name: t.exposeString("artist_name", { nullable: true }), - slug: t.exposeString("slug", { nullable: true }), - avatar: t.exposeString("avatar", { nullable: true }), - }), -}); +export const VideoModelType = builder + .objectRef<{ + id: string; + artist_name: string | null; + slug: string | null; + avatar: string | null; + }>("VideoModel") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + artist_name: t.exposeString("artist_name", { nullable: true }), + slug: t.exposeString("slug", { nullable: true }), + avatar: t.exposeString("avatar", { nullable: true }), + }), + }); -export const VideoFileType = builder.objectRef<{ - id: string; - filename: string; - mime_type: string | null; - duration: number | null; -}>("VideoFile").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - filename: t.exposeString("filename"), - mime_type: t.exposeString("mime_type", { nullable: true }), - duration: t.exposeInt("duration", { nullable: true }), - }), -}); +export const VideoFileType = builder + .objectRef<{ + id: string; + filename: string; + mime_type: string | null; + duration: number | null; + }>("VideoFile") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + filename: t.exposeString("filename"), + mime_type: t.exposeString("mime_type", { nullable: true }), + duration: t.exposeInt("duration", { nullable: true }), + }), + }); // Model type (model profile, enriched user) -export const ModelType = builder.objectRef<{ - id: string; - slug: string | null; - artist_name: string | null; - description: string | null; - avatar: string | null; - banner: string | null; - tags: string[] | null; - date_created: Date; - photos?: { id: string; filename: string }[]; -}>("Model").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - slug: t.exposeString("slug", { nullable: true }), - artist_name: t.exposeString("artist_name", { nullable: true }), - description: t.exposeString("description", { nullable: true }), - avatar: t.exposeString("avatar", { nullable: true }), - banner: t.exposeString("banner", { nullable: true }), - tags: t.exposeStringList("tags", { nullable: true }), - date_created: t.expose("date_created", { type: "DateTime" }), - photos: t.expose("photos", { type: [ModelPhotoType], nullable: true }), - }), -}); +export const ModelType = builder + .objectRef<{ + id: string; + slug: string | null; + artist_name: string | null; + description: string | null; + avatar: string | null; + banner: string | null; + tags: string[] | null; + date_created: Date; + photos?: { id: string; filename: string }[]; + }>("Model") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + slug: t.exposeString("slug", { nullable: true }), + artist_name: t.exposeString("artist_name", { nullable: true }), + description: t.exposeString("description", { nullable: true }), + avatar: t.exposeString("avatar", { nullable: true }), + banner: t.exposeString("banner", { nullable: true }), + tags: t.exposeStringList("tags", { nullable: true }), + date_created: t.expose("date_created", { type: "DateTime" }), + photos: t.expose("photos", { type: [ModelPhotoType], nullable: true }), + }), + }); -export const ModelPhotoType = builder.objectRef<{ - id: string; - filename: string; -}>("ModelPhoto").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - filename: t.exposeString("filename"), - }), -}); +export const ModelPhotoType = builder + .objectRef<{ + id: string; + filename: string; + }>("ModelPhoto") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + filename: t.exposeString("filename"), + }), + }); // Article type -export const ArticleType = builder.objectRef<{ - id: string; - slug: string; - title: string; - excerpt: string | null; - content: string | null; - image: string | null; - tags: string[] | null; - publish_date: Date; - category: string | null; - featured: boolean | null; - author?: { first_name: string | null; last_name: string | null; avatar: string | null; description: string | null } | null; -}>("Article").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - slug: t.exposeString("slug"), - title: t.exposeString("title"), - excerpt: t.exposeString("excerpt", { nullable: true }), - content: t.exposeString("content", { nullable: true }), - image: t.exposeString("image", { nullable: true }), - tags: t.exposeStringList("tags", { nullable: true }), - publish_date: t.expose("publish_date", { type: "DateTime" }), - category: t.exposeString("category", { nullable: true }), - featured: t.exposeBoolean("featured", { nullable: true }), - author: t.expose("author", { type: ArticleAuthorType, nullable: true }), - }), -}); +export const ArticleType = builder + .objectRef<{ + id: string; + slug: string; + title: string; + excerpt: string | null; + content: string | null; + image: string | null; + tags: string[] | null; + publish_date: Date; + category: string | null; + featured: boolean | null; + author?: { + first_name: string | null; + last_name: string | null; + avatar: string | null; + description: string | null; + } | null; + }>("Article") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + slug: t.exposeString("slug"), + title: t.exposeString("title"), + excerpt: t.exposeString("excerpt", { nullable: true }), + content: t.exposeString("content", { nullable: true }), + image: t.exposeString("image", { nullable: true }), + tags: t.exposeStringList("tags", { nullable: true }), + publish_date: t.expose("publish_date", { type: "DateTime" }), + category: t.exposeString("category", { nullable: true }), + featured: t.exposeBoolean("featured", { nullable: true }), + author: t.expose("author", { type: ArticleAuthorType, nullable: true }), + }), + }); -export const ArticleAuthorType = builder.objectRef<{ - first_name: string | null; - last_name: string | null; - avatar: string | null; - description: string | null; -}>("ArticleAuthor").implement({ - fields: (t) => ({ - first_name: t.exposeString("first_name", { nullable: true }), - last_name: t.exposeString("last_name", { nullable: true }), - avatar: t.exposeString("avatar", { nullable: true }), - description: t.exposeString("description", { nullable: true }), - }), -}); +export const ArticleAuthorType = builder + .objectRef<{ + first_name: string | null; + last_name: string | null; + avatar: string | null; + description: string | null; + }>("ArticleAuthor") + .implement({ + fields: (t) => ({ + first_name: t.exposeString("first_name", { nullable: true }), + last_name: t.exposeString("last_name", { nullable: true }), + avatar: t.exposeString("avatar", { nullable: true }), + description: t.exposeString("description", { nullable: true }), + }), + }); // Recording type -export const RecordingType = builder.objectRef<{ - id: string; - title: string; - description: string | null; - slug: string; - duration: number; - events: object[] | null; - device_info: object[] | null; - user_id: string; - status: string; - tags: string[] | null; - linked_video: string | null; - featured: boolean | null; - public: boolean | null; - date_created: Date; - date_updated: Date | null; -}>("Recording").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - title: t.exposeString("title"), - description: t.exposeString("description", { nullable: true }), - slug: t.exposeString("slug"), - duration: t.exposeInt("duration"), - events: t.expose("events", { type: "JSON", nullable: true }), - device_info: t.expose("device_info", { type: "JSON", nullable: true }), - user_id: t.exposeString("user_id"), - status: t.exposeString("status"), - tags: t.exposeStringList("tags", { nullable: true }), - linked_video: t.exposeString("linked_video", { nullable: true }), - featured: t.exposeBoolean("featured", { nullable: true }), - public: t.exposeBoolean("public", { nullable: true }), - date_created: t.expose("date_created", { type: "DateTime" }), - date_updated: t.expose("date_updated", { type: "DateTime", nullable: true }), - }), -}); +export const RecordingType = builder + .objectRef<{ + id: string; + title: string; + description: string | null; + slug: string; + duration: number; + events: object[] | null; + device_info: object[] | null; + user_id: string; + status: string; + tags: string[] | null; + linked_video: string | null; + featured: boolean | null; + public: boolean | null; + date_created: Date; + date_updated: Date | null; + }>("Recording") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + title: t.exposeString("title"), + description: t.exposeString("description", { nullable: true }), + slug: t.exposeString("slug"), + duration: t.exposeInt("duration"), + events: t.expose("events", { type: "JSON", nullable: true }), + device_info: t.expose("device_info", { type: "JSON", nullable: true }), + user_id: t.exposeString("user_id"), + status: t.exposeString("status"), + tags: t.exposeStringList("tags", { nullable: true }), + linked_video: t.exposeString("linked_video", { nullable: true }), + featured: t.exposeBoolean("featured", { nullable: true }), + public: t.exposeBoolean("public", { nullable: true }), + date_created: t.expose("date_created", { type: "DateTime" }), + date_updated: t.expose("date_updated", { type: "DateTime", nullable: true }), + }), + }); // Comment type -export const CommentType = builder.objectRef<{ - id: number; - collection: string; - item_id: string; - comment: string; - user_id: string; - date_created: Date; - user?: { id: string; first_name: string | null; last_name: string | null; avatar: string | null } | null; -}>("Comment").implement({ - fields: (t) => ({ - id: t.exposeInt("id"), - collection: t.exposeString("collection"), - item_id: t.exposeString("item_id"), - comment: t.exposeString("comment"), - user_id: t.exposeString("user_id"), - date_created: t.expose("date_created", { type: "DateTime" }), - user: t.expose("user", { type: CommentUserType, nullable: true }), - }), -}); +export const CommentType = builder + .objectRef<{ + id: number; + collection: string; + item_id: string; + comment: string; + user_id: string; + date_created: Date; + user?: { + id: string; + first_name: string | null; + last_name: string | null; + avatar: string | null; + } | null; + }>("Comment") + .implement({ + fields: (t) => ({ + id: t.exposeInt("id"), + collection: t.exposeString("collection"), + item_id: t.exposeString("item_id"), + comment: t.exposeString("comment"), + user_id: t.exposeString("user_id"), + date_created: t.expose("date_created", { type: "DateTime" }), + user: t.expose("user", { type: CommentUserType, nullable: true }), + }), + }); -export const CommentUserType = builder.objectRef<{ - id: string; - first_name: string | null; - last_name: string | null; - avatar: string | null; -}>("CommentUser").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - first_name: t.exposeString("first_name", { nullable: true }), - last_name: t.exposeString("last_name", { nullable: true }), - avatar: t.exposeString("avatar", { nullable: true }), - }), -}); +export const CommentUserType = builder + .objectRef<{ + id: string; + first_name: string | null; + last_name: string | null; + avatar: string | null; + }>("CommentUser") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + first_name: t.exposeString("first_name", { nullable: true }), + last_name: t.exposeString("last_name", { nullable: true }), + avatar: t.exposeString("avatar", { nullable: true }), + }), + }); // Stats type -export const StatsType = builder.objectRef<{ - videos_count: number; - models_count: number; - viewers_count: number; -}>("Stats").implement({ - fields: (t) => ({ - videos_count: t.exposeInt("videos_count"), - models_count: t.exposeInt("models_count"), - viewers_count: t.exposeInt("viewers_count"), - }), -}); +export const StatsType = builder + .objectRef<{ + videos_count: number; + models_count: number; + viewers_count: number; + }>("Stats") + .implement({ + fields: (t) => ({ + videos_count: t.exposeInt("videos_count"), + models_count: t.exposeInt("models_count"), + viewers_count: t.exposeInt("viewers_count"), + }), + }); // Gamification types -export const LeaderboardEntryType = builder.objectRef<{ - user_id: string; - display_name: string | null; - avatar: string | null; - total_weighted_points: number | null; - total_raw_points: number | null; - recordings_count: number | null; - playbacks_count: number | null; - achievements_count: number | null; - rank: number; -}>("LeaderboardEntry").implement({ - fields: (t) => ({ - user_id: t.exposeString("user_id"), - display_name: t.exposeString("display_name", { nullable: true }), - avatar: t.exposeString("avatar", { nullable: true }), - total_weighted_points: t.exposeFloat("total_weighted_points", { nullable: true }), - total_raw_points: t.exposeInt("total_raw_points", { nullable: true }), - recordings_count: t.exposeInt("recordings_count", { nullable: true }), - playbacks_count: t.exposeInt("playbacks_count", { nullable: true }), - achievements_count: t.exposeInt("achievements_count", { nullable: true }), - rank: t.exposeInt("rank"), - }), -}); +export const LeaderboardEntryType = builder + .objectRef<{ + user_id: string; + display_name: string | null; + avatar: string | null; + total_weighted_points: number | null; + total_raw_points: number | null; + recordings_count: number | null; + playbacks_count: number | null; + achievements_count: number | null; + rank: number; + }>("LeaderboardEntry") + .implement({ + fields: (t) => ({ + user_id: t.exposeString("user_id"), + display_name: t.exposeString("display_name", { nullable: true }), + avatar: t.exposeString("avatar", { nullable: true }), + total_weighted_points: t.exposeFloat("total_weighted_points", { nullable: true }), + total_raw_points: t.exposeInt("total_raw_points", { nullable: true }), + recordings_count: t.exposeInt("recordings_count", { nullable: true }), + playbacks_count: t.exposeInt("playbacks_count", { nullable: true }), + achievements_count: t.exposeInt("achievements_count", { nullable: true }), + rank: t.exposeInt("rank"), + }), + }); -export const AchievementType = builder.objectRef<{ - id: string; - code: string; - name: string; - description: string | null; - icon: string | null; - category: string | null; - required_count: number; - points_reward: number; -}>("Achievement").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - code: t.exposeString("code"), - name: t.exposeString("name"), - description: t.exposeString("description", { nullable: true }), - icon: t.exposeString("icon", { nullable: true }), - category: t.exposeString("category", { nullable: true }), - required_count: t.exposeInt("required_count"), - points_reward: t.exposeInt("points_reward"), - }), -}); +export const AchievementType = builder + .objectRef<{ + id: string; + code: string; + name: string; + description: string | null; + icon: string | null; + category: string | null; + required_count: number; + points_reward: number; + }>("Achievement") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + code: t.exposeString("code"), + name: t.exposeString("name"), + description: t.exposeString("description", { nullable: true }), + icon: t.exposeString("icon", { nullable: true }), + category: t.exposeString("category", { nullable: true }), + required_count: t.exposeInt("required_count"), + points_reward: t.exposeInt("points_reward"), + }), + }); -export const UserGamificationType = builder.objectRef<{ - stats: { +export const UserGamificationType = builder + .objectRef<{ + stats: { + user_id: string; + total_raw_points: number | null; + total_weighted_points: number | null; + recordings_count: number | null; + playbacks_count: number | null; + comments_count: number | null; + achievements_count: number | null; + rank: number; + } | null; + achievements: { + id: string; + code: string; + name: string; + description: string | null; + icon: string | null; + category: string | null; + date_unlocked: Date; + progress: number | null; + required_count: number; + }[]; + recent_points: { + action: string; + points: number; + date_created: Date; + recording_id: string | null; + }[]; + }>("UserGamification") + .implement({ + fields: (t) => ({ + stats: t.expose("stats", { type: UserStatsType, nullable: true }), + achievements: t.expose("achievements", { type: [UserAchievementType] }), + recent_points: t.expose("recent_points", { type: [RecentPointType] }), + }), + }); + +export const UserStatsType = builder + .objectRef<{ user_id: string; total_raw_points: number | null; total_weighted_points: number | null; @@ -374,8 +464,22 @@ export const UserGamificationType = builder.objectRef<{ comments_count: number | null; achievements_count: number | null; rank: number; - } | null; - achievements: { + }>("UserStats") + .implement({ + fields: (t) => ({ + user_id: t.exposeString("user_id"), + total_raw_points: t.exposeInt("total_raw_points", { nullable: true }), + total_weighted_points: t.exposeFloat("total_weighted_points", { nullable: true }), + recordings_count: t.exposeInt("recordings_count", { nullable: true }), + playbacks_count: t.exposeInt("playbacks_count", { nullable: true }), + comments_count: t.exposeInt("comments_count", { nullable: true }), + achievements_count: t.exposeInt("achievements_count", { nullable: true }), + rank: t.exposeInt("rank"), + }), + }); + +export const UserAchievementType = builder + .objectRef<{ id: string; code: string; name: string; @@ -385,89 +489,70 @@ export const UserGamificationType = builder.objectRef<{ date_unlocked: Date; progress: number | null; required_count: number; - }[]; - recent_points: { + }>("UserAchievement") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + code: t.exposeString("code"), + name: t.exposeString("name"), + description: t.exposeString("description", { nullable: true }), + icon: t.exposeString("icon", { nullable: true }), + category: t.exposeString("category", { nullable: true }), + date_unlocked: t.expose("date_unlocked", { type: "DateTime" }), + progress: t.exposeInt("progress", { nullable: true }), + required_count: t.exposeInt("required_count"), + }), + }); + +export const RecentPointType = builder + .objectRef<{ action: string; points: number; date_created: Date; recording_id: string | null; - }[]; -}>("UserGamification").implement({ - fields: (t) => ({ - stats: t.expose("stats", { type: UserStatsType, nullable: true }), - achievements: t.expose("achievements", { type: [UserAchievementType] }), - recent_points: t.expose("recent_points", { type: [RecentPointType] }), - }), -}); - -export const UserStatsType = builder.objectRef<{ - user_id: string; - total_raw_points: number | null; - total_weighted_points: number | null; - recordings_count: number | null; - playbacks_count: number | null; - comments_count: number | null; - achievements_count: number | null; - rank: number; -}>("UserStats").implement({ - fields: (t) => ({ - user_id: t.exposeString("user_id"), - total_raw_points: t.exposeInt("total_raw_points", { nullable: true }), - total_weighted_points: t.exposeFloat("total_weighted_points", { nullable: true }), - recordings_count: t.exposeInt("recordings_count", { nullable: true }), - playbacks_count: t.exposeInt("playbacks_count", { nullable: true }), - comments_count: t.exposeInt("comments_count", { nullable: true }), - achievements_count: t.exposeInt("achievements_count", { nullable: true }), - rank: t.exposeInt("rank"), - }), -}); - -export const UserAchievementType = builder.objectRef<{ - id: string; - code: string; - name: string; - description: string | null; - icon: string | null; - category: string | null; - date_unlocked: Date; - progress: number | null; - required_count: number; -}>("UserAchievement").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - code: t.exposeString("code"), - name: t.exposeString("name"), - description: t.exposeString("description", { nullable: true }), - icon: t.exposeString("icon", { nullable: true }), - category: t.exposeString("category", { nullable: true }), - date_unlocked: t.expose("date_unlocked", { type: "DateTime" }), - progress: t.exposeInt("progress", { nullable: true }), - required_count: t.exposeInt("required_count"), - }), -}); - -export const RecentPointType = builder.objectRef<{ - action: string; - points: number; - date_created: Date; - recording_id: string | null; -}>("RecentPoint").implement({ - fields: (t) => ({ - action: t.exposeString("action"), - points: t.exposeInt("points"), - date_created: t.expose("date_created", { type: "DateTime" }), - recording_id: t.exposeString("recording_id", { nullable: true }), - }), -}); + }>("RecentPoint") + .implement({ + fields: (t) => ({ + action: t.exposeString("action"), + points: t.exposeInt("points"), + date_created: t.expose("date_created", { type: "DateTime" }), + recording_id: t.exposeString("recording_id", { nullable: true }), + }), + }); // Analytics types -export const AnalyticsType = builder.objectRef<{ - total_videos: number; - total_likes: number; - total_plays: number; - plays_by_date: Record; - likes_by_date: Record; - videos: { +export const AnalyticsType = builder + .objectRef<{ + total_videos: number; + total_likes: number; + total_plays: number; + plays_by_date: Record; + likes_by_date: Record; + videos: { + id: string; + title: string; + slug: string; + upload_date: Date; + likes: number; + plays: number; + completed_plays: number; + completion_rate: number; + avg_watch_time: number; + }[]; + }>("Analytics") + .implement({ + fields: (t) => ({ + total_videos: t.exposeInt("total_videos"), + total_likes: t.exposeInt("total_likes"), + total_plays: t.exposeInt("total_plays"), + plays_by_date: t.expose("plays_by_date", { type: "JSON" }), + likes_by_date: t.expose("likes_by_date", { type: "JSON" }), + videos: t.expose("videos", { type: [VideoAnalyticsType] }), + }), + }); + +export const VideoAnalyticsType = builder + .objectRef<{ id: string; title: string; slug: string; @@ -477,69 +562,54 @@ export const AnalyticsType = builder.objectRef<{ completed_plays: number; completion_rate: number; avg_watch_time: number; - }[]; -}>("Analytics").implement({ - fields: (t) => ({ - total_videos: t.exposeInt("total_videos"), - total_likes: t.exposeInt("total_likes"), - total_plays: t.exposeInt("total_plays"), - plays_by_date: t.expose("plays_by_date", { type: "JSON" }), - likes_by_date: t.expose("likes_by_date", { type: "JSON" }), - videos: t.expose("videos", { type: [VideoAnalyticsType] }), - }), -}); - -export const VideoAnalyticsType = builder.objectRef<{ - id: string; - title: string; - slug: string; - upload_date: Date; - likes: number; - plays: number; - completed_plays: number; - completion_rate: number; - avg_watch_time: number; -}>("VideoAnalytics").implement({ - fields: (t) => ({ - id: t.exposeString("id"), - title: t.exposeString("title"), - slug: t.exposeString("slug"), - upload_date: t.expose("upload_date", { type: "DateTime" }), - likes: t.exposeInt("likes"), - plays: t.exposeInt("plays"), - completed_plays: t.exposeInt("completed_plays"), - completion_rate: t.exposeFloat("completion_rate"), - avg_watch_time: t.exposeInt("avg_watch_time"), - }), -}); + }>("VideoAnalytics") + .implement({ + fields: (t) => ({ + id: t.exposeString("id"), + title: t.exposeString("title"), + slug: t.exposeString("slug"), + upload_date: t.expose("upload_date", { type: "DateTime" }), + likes: t.exposeInt("likes"), + plays: t.exposeInt("plays"), + completed_plays: t.exposeInt("completed_plays"), + completion_rate: t.exposeFloat("completion_rate"), + avg_watch_time: t.exposeInt("avg_watch_time"), + }), + }); // Response types -export const VideoLikeResponseType = builder.objectRef<{ - liked: boolean; - likes_count: number; -}>("VideoLikeResponse").implement({ - fields: (t) => ({ - liked: t.exposeBoolean("liked"), - likes_count: t.exposeInt("likes_count"), - }), -}); +export const VideoLikeResponseType = builder + .objectRef<{ + liked: boolean; + likes_count: number; + }>("VideoLikeResponse") + .implement({ + fields: (t) => ({ + liked: t.exposeBoolean("liked"), + likes_count: t.exposeInt("likes_count"), + }), + }); -export const VideoPlayResponseType = builder.objectRef<{ - success: boolean; - play_id: string; - plays_count: number; -}>("VideoPlayResponse").implement({ - fields: (t) => ({ - success: t.exposeBoolean("success"), - play_id: t.exposeString("play_id"), - plays_count: t.exposeInt("plays_count"), - }), -}); +export const VideoPlayResponseType = builder + .objectRef<{ + success: boolean; + play_id: string; + plays_count: number; + }>("VideoPlayResponse") + .implement({ + fields: (t) => ({ + success: t.exposeBoolean("success"), + play_id: t.exposeString("play_id"), + plays_count: t.exposeInt("plays_count"), + }), + }); -export const VideoLikeStatusType = builder.objectRef<{ - liked: boolean; -}>("VideoLikeStatus").implement({ - fields: (t) => ({ - liked: t.exposeBoolean("liked"), - }), -}); +export const VideoLikeStatusType = builder + .objectRef<{ + liked: boolean; + }>("VideoLikeStatus") + .implement({ + fields: (t) => ({ + liked: t.exposeBoolean("liked"), + }), + }); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index c7a87be..40967ba 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -49,7 +49,12 @@ async function main() { decorateReply: true, }); - const yoga = createYoga<{ req: FastifyRequest; reply: FastifyReply; db: typeof db; redis: typeof redis }>({ + const yoga = createYoga<{ + req: FastifyRequest; + reply: FastifyReply; + db: typeof db; + redis: typeof redis; + }>({ schema, context: buildContext, graphqlEndpoint: "/graphql", @@ -101,7 +106,12 @@ async function main() { if (!existsSync(cacheFile)) { const originalPath = path.join(UPLOAD_DIR, id, filename); await sharp(originalPath) - .resize({ width: preset.width, height: preset.height, fit: preset.fit ?? "inside", withoutEnlargement: true }) + .resize({ + width: preset.width, + height: preset.height, + fit: preset.fit ?? "inside", + withoutEnlargement: true, + }) .webp({ quality: 92 }) .toFile(cacheFile); } diff --git a/packages/backend/src/lib/email.ts b/packages/backend/src/lib/email.ts index 30e6df2..6360ac2 100644 --- a/packages/backend/src/lib/email.ts +++ b/packages/backend/src/lib/email.ts @@ -4,10 +4,12 @@ const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST || "localhost", port: parseInt(process.env.SMTP_PORT || "587"), secure: process.env.SMTP_SECURE === "true", - auth: process.env.SMTP_USER ? { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS, - } : undefined, + auth: process.env.SMTP_USER + ? { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS, + } + : undefined, }); const FROM = process.env.EMAIL_FROM || "noreply@sexy.pivoine.art"; diff --git a/packages/backend/src/lib/gamification.ts b/packages/backend/src/lib/gamification.ts index 3e9148f..50649d2 100644 --- a/packages/backend/src/lib/gamification.ts +++ b/packages/backend/src/lib/gamification.ts @@ -79,7 +79,10 @@ export async function updateUserStats(db: DB, userId: string): Promise { const playbacksResult = await db.execute(sql` SELECT COUNT(*) as count FROM recording_plays WHERE user_id = ${userId} - AND recording_id NOT IN (${sql.join(ownIds.map(id => sql`${id}`), sql`, `)}) + AND recording_id NOT IN (${sql.join( + ownIds.map((id) => sql`${id}`), + sql`, `, + )}) `); playbacksCount = parseInt((playbacksResult.rows[0] as any)?.count || "0"); } else { @@ -135,11 +138,7 @@ export async function updateUserStats(db: DB, userId: string): Promise { } } -export async function checkAchievements( - db: DB, - userId: string, - category?: string, -): Promise { +export async function checkAchievements(db: DB, userId: string, category?: string): Promise { let achievementsQuery = db .select() .from(achievements) @@ -176,7 +175,7 @@ export async function checkAchievements( .update(user_achievements) .set({ progress, - date_unlocked: isUnlocked ? (existing[0].date_unlocked || new Date()) : null, + date_unlocked: isUnlocked ? existing[0].date_unlocked || new Date() : null, }) .where( and( diff --git a/packages/backend/src/scripts/data-migration.ts b/packages/backend/src/scripts/data-migration.ts index c9dda15..37f4368 100644 --- a/packages/backend/src/scripts/data-migration.ts +++ b/packages/backend/src/scripts/data-migration.ts @@ -128,7 +128,9 @@ async function migrateUsers() { ? tagsRes.rows[0].tags : JSON.parse(String(tagsRes.rows[0].tags || "[]")); } - } catch { /* tags column may not exist on older Directus installs */ } + } 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, @@ -279,9 +281,7 @@ async function migrateVideoModels() { async function migrateVideoLikes() { console.log("❤️ Migrating video likes..."); - const { rows } = await query( - `SELECT id, video_id, user_id, date_created FROM sexy_video_likes`, - ); + const { rows } = await query(`SELECT id, video_id, user_id, date_created FROM sexy_video_likes`); let migrated = 0; for (const row of rows) { diff --git a/packages/buttplug/package.json b/packages/buttplug/package.json index bdc58f7..acb630b 100644 --- a/packages/buttplug/package.json +++ b/packages/buttplug/package.json @@ -1,25 +1,25 @@ { - "name": "@sexy.pivoine.art/buttplug", - "version": "1.0.0", - "type": "module", - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist" - ], - "scripts": { - "build": "vite build", - "build:wasm": "wasm-pack build --out-dir wasm --out-name index --target bundler --release" - }, - "dependencies": { - "eventemitter3": "^5.0.4", - "typescript": "^5.9.3", - "vite": "^7.3.1", - "vite-plugin-wasm": "3.5.0", - "ws": "^8.19.0" - }, - "devDependencies": { - "wasm-pack": "^0.14.0" - } + "name": "@sexy.pivoine.art/buttplug", + "version": "1.0.0", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "vite build", + "build:wasm": "wasm-pack build --out-dir wasm --out-name index --target bundler --release" + }, + "dependencies": { + "eventemitter3": "^5.0.4", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vite-plugin-wasm": "3.5.0", + "ws": "^8.19.0" + }, + "devDependencies": { + "wasm-pack": "^0.14.0" + } } diff --git a/packages/buttplug/src/client/ButtplugBrowserWebsocketClientConnector.ts b/packages/buttplug/src/client/ButtplugBrowserWebsocketClientConnector.ts index e44f3e6..750ec1e 100644 --- a/packages/buttplug/src/client/ButtplugBrowserWebsocketClientConnector.ts +++ b/packages/buttplug/src/client/ButtplugBrowserWebsocketClientConnector.ts @@ -6,11 +6,11 @@ * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. */ -'use strict'; +"use strict"; -import { IButtplugClientConnector } from './IButtplugClientConnector'; -import { ButtplugMessage } from '../core/Messages'; -import { ButtplugBrowserWebsocketConnector } from '../utils/ButtplugBrowserWebsocketConnector'; +import { IButtplugClientConnector } from "./IButtplugClientConnector"; +import { ButtplugMessage } from "../core/Messages"; +import { ButtplugBrowserWebsocketConnector } from "../utils/ButtplugBrowserWebsocketConnector"; export class ButtplugBrowserWebsocketClientConnector extends ButtplugBrowserWebsocketConnector @@ -18,7 +18,7 @@ export class ButtplugBrowserWebsocketClientConnector { public send = (msg: ButtplugMessage): void => { if (!this.Connected) { - throw new Error('ButtplugClient not connected'); + throw new Error("ButtplugClient not connected"); } this.sendMessage(msg); }; diff --git a/packages/buttplug/src/client/ButtplugClient.ts b/packages/buttplug/src/client/ButtplugClient.ts index 187cc2a..c06b0e8 100644 --- a/packages/buttplug/src/client/ButtplugClient.ts +++ b/packages/buttplug/src/client/ButtplugClient.ts @@ -6,20 +6,16 @@ * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. */ -'use strict'; +"use strict"; -import { ButtplugLogger } from '../core/Logging'; -import { EventEmitter } from 'eventemitter3'; -import { ButtplugClientDevice } from './ButtplugClientDevice'; -import { IButtplugClientConnector } from './IButtplugClientConnector'; -import { ButtplugMessageSorter } from '../utils/ButtplugMessageSorter'; -import * as Messages from '../core/Messages'; -import { - ButtplugError, - ButtplugInitError, - ButtplugMessageError, -} from '../core/Exceptions'; -import { ButtplugClientConnectorException } from './ButtplugClientConnectorException'; +import { ButtplugLogger } from "../core/Logging"; +import { EventEmitter } from "eventemitter3"; +import { ButtplugClientDevice } from "./ButtplugClientDevice"; +import { IButtplugClientConnector } from "./IButtplugClientConnector"; +import { ButtplugMessageSorter } from "../utils/ButtplugMessageSorter"; +import * as Messages from "../core/Messages"; +import { ButtplugError, ButtplugInitError, ButtplugMessageError } from "../core/Exceptions"; +import { ButtplugClientConnectorException } from "./ButtplugClientConnectorException"; export class ButtplugClient extends EventEmitter { protected _pingTimer: NodeJS.Timeout | null = null; @@ -30,7 +26,7 @@ export class ButtplugClient extends EventEmitter { protected _isScanning = false; private _sorter: ButtplugMessageSorter = new ButtplugMessageSorter(true); - constructor(clientName = 'Generic Buttplug Client') { + constructor(clientName = "Generic Buttplug Client") { super(); this._clientName = clientName; this._logger.Debug(`ButtplugClient: Client ${clientName} created.`); @@ -52,18 +48,16 @@ export class ButtplugClient extends EventEmitter { } public connect = async (connector: IButtplugClientConnector) => { - this._logger.Info( - `ButtplugClient: Connecting using ${connector.constructor.name}` - ); + this._logger.Info(`ButtplugClient: Connecting using ${connector.constructor.name}`); await connector.connect(); this._connector = connector; - this._connector.addListener('message', this.parseMessages); - this._connector.addListener('disconnect', this.disconnectHandler); + this._connector.addListener("message", this.parseMessages); + this._connector.addListener("disconnect", this.disconnectHandler); await this.initializeConnection(); }; public disconnect = async () => { - this._logger.Debug('ButtplugClient: Disconnect called'); + this._logger.Debug("ButtplugClient: Disconnect called"); this._devices.clear(); this.checkConnector(); await this.shutdownConnection(); @@ -71,25 +65,33 @@ export class ButtplugClient extends EventEmitter { }; public startScanning = async () => { - this._logger.Debug('ButtplugClient: StartScanning called'); + this._logger.Debug("ButtplugClient: StartScanning called"); this._isScanning = true; await this.sendMsgExpectOk({ StartScanning: { Id: 1 } }); }; public stopScanning = async () => { - this._logger.Debug('ButtplugClient: StopScanning called'); + this._logger.Debug("ButtplugClient: StopScanning called"); this._isScanning = false; await this.sendMsgExpectOk({ StopScanning: { Id: 1 } }); }; public stopAllDevices = async () => { - this._logger.Debug('ButtplugClient: StopAllDevices'); - await this.sendMsgExpectOk({ StopCmd: { Id: 1, DeviceIndex: undefined, FeatureIndex: undefined, Inputs: true, Outputs: true } }); + this._logger.Debug("ButtplugClient: StopAllDevices"); + await this.sendMsgExpectOk({ + StopCmd: { + Id: 1, + DeviceIndex: undefined, + FeatureIndex: undefined, + Inputs: true, + Outputs: true, + }, + }); }; protected disconnectHandler = () => { - this._logger.Info('ButtplugClient: Disconnect event receieved.'); - this.emit('disconnect'); + this._logger.Info("ButtplugClient: Disconnect event receieved."); + this.emit("disconnect"); }; protected parseMessages = (msgs: Messages.ButtplugMessage[]) => { @@ -100,10 +102,10 @@ export class ButtplugClient extends EventEmitter { break; } else if (x.ScanningFinished !== undefined) { this._isScanning = false; - this.emit('scanningfinished', x); + this.emit("scanningfinished", x); } else if (x.InputReading !== undefined) { // TODO this should be emitted from the device or feature, not the client - this.emit('inputreading', x); + this.emit("inputreading", x); } else { console.log(`Unhandled message: ${x}`); } @@ -112,21 +114,17 @@ export class ButtplugClient extends EventEmitter { protected initializeConnection = async (): Promise => { this.checkConnector(); - const msg = await this.sendMessage( - { - RequestServerInfo: { - ClientName: this._clientName, - Id: 1, - ProtocolVersionMajor: Messages.MESSAGE_SPEC_VERSION_MAJOR, - ProtocolVersionMinor: Messages.MESSAGE_SPEC_VERSION_MINOR - } - } - ); + const msg = await this.sendMessage({ + RequestServerInfo: { + ClientName: this._clientName, + Id: 1, + ProtocolVersionMajor: Messages.MESSAGE_SPEC_VERSION_MAJOR, + ProtocolVersionMinor: Messages.MESSAGE_SPEC_VERSION_MINOR, + }, + }); if (msg.ServerInfo !== undefined) { const serverinfo = msg as Messages.ServerInfo; - this._logger.Info( - `ButtplugClient: Connected to Server ${serverinfo.ServerName}` - ); + this._logger.Info(`ButtplugClient: Connected to Server ${serverinfo.ServerName}`); // TODO: maybe store server name, do something with message template version? const ping = serverinfo.MaxPingTime; // If the server version is lower than the client version, the server will disconnect here. @@ -153,22 +151,19 @@ export class ButtplugClient extends EventEmitter { throw ButtplugError.LogAndError( ButtplugInitError, this._logger, - `Cannot connect to server. ${err.ErrorMessage}` + `Cannot connect to server. ${err.ErrorMessage}`, ); } return false; - } + }; private parseDeviceList = (list: Messages.DeviceList) => { for (let [_, d] of Object.entries(list.Devices)) { if (!this._devices.has(d.DeviceIndex)) { - const device = ButtplugClientDevice.fromMsg( - d, - this.sendMessageClosure - ); + const device = ButtplugClientDevice.fromMsg(d, this.sendMessageClosure); this._logger.Debug(`ButtplugClient: Adding Device: ${device}`); this._devices.set(d.DeviceIndex, device); - this.emit('deviceadded', device); + this.emit("deviceadded", device); } else { this._logger.Debug(`ButtplugClient: Device already added: ${d}`); } @@ -176,19 +171,17 @@ export class ButtplugClient extends EventEmitter { for (let [index, device] of this._devices.entries()) { if (!list.Devices.hasOwnProperty(index.toString())) { this._devices.delete(index); - this.emit('deviceremoved', device); + this.emit("deviceremoved", device); } } - } + }; protected requestDeviceList = async () => { this.checkConnector(); - this._logger.Debug('ButtplugClient: ReceiveDeviceList called'); - const response = (await this.sendMessage( - { - RequestDeviceList: { Id: 1 } - } - )); + this._logger.Debug("ButtplugClient: ReceiveDeviceList called"); + const response = await this.sendMessage({ + RequestDeviceList: { Id: 1 }, + }); this.parseDeviceList(response.DeviceList!); }; @@ -200,9 +193,7 @@ export class ButtplugClient extends EventEmitter { } }; - protected async sendMessage( - msg: Messages.ButtplugMessage - ): Promise { + protected async sendMessage(msg: Messages.ButtplugMessage): Promise { this.checkConnector(); const p = this._sorter.PrepareOutgoingMessage(msg); await this._connector!.send(msg); @@ -211,15 +202,11 @@ export class ButtplugClient extends EventEmitter { protected checkConnector() { if (!this.connected) { - throw new ButtplugClientConnectorException( - 'ButtplugClient not connected' - ); + throw new ButtplugClientConnectorException("ButtplugClient not connected"); } } - protected sendMsgExpectOk = async ( - msg: Messages.ButtplugMessage - ): Promise => { + protected sendMsgExpectOk = async (msg: Messages.ButtplugMessage): Promise => { const response = await this.sendMessage(msg); if (response.Ok !== undefined) { return; @@ -229,13 +216,13 @@ export class ButtplugClient extends EventEmitter { throw ButtplugError.LogAndError( ButtplugMessageError, this._logger, - `Message ${response} not handled by SendMsgExpectOk` + `Message ${response} not handled by SendMsgExpectOk`, ); } }; protected sendMessageClosure = async ( - msg: Messages.ButtplugMessage + msg: Messages.ButtplugMessage, ): Promise => { return await this.sendMessage(msg); }; diff --git a/packages/buttplug/src/client/ButtplugClientConnectorException.ts b/packages/buttplug/src/client/ButtplugClientConnectorException.ts index b037b92..711b2aa 100644 --- a/packages/buttplug/src/client/ButtplugClientConnectorException.ts +++ b/packages/buttplug/src/client/ButtplugClientConnectorException.ts @@ -6,8 +6,8 @@ * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. */ -import { ButtplugError } from '../core/Exceptions'; -import * as Messages from '../core/Messages'; +import { ButtplugError } from "../core/Exceptions"; +import * as Messages from "../core/Messages"; export class ButtplugClientConnectorException extends ButtplugError { public constructor(message: string) { diff --git a/packages/buttplug/src/client/ButtplugClientDevice.ts b/packages/buttplug/src/client/ButtplugClientDevice.ts index a84fd6c..41c740b 100644 --- a/packages/buttplug/src/client/ButtplugClientDevice.ts +++ b/packages/buttplug/src/client/ButtplugClientDevice.ts @@ -6,22 +6,17 @@ * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. */ -'use strict'; -import * as Messages from '../core/Messages'; -import { - ButtplugDeviceError, - ButtplugError, - ButtplugMessageError, -} from '../core/Exceptions'; -import { EventEmitter } from 'eventemitter3'; -import { ButtplugClientDeviceFeature } from './ButtplugClientDeviceFeature'; -import { DeviceOutputCommand } from './ButtplugClientDeviceCommand'; +"use strict"; +import * as Messages from "../core/Messages"; +import { ButtplugDeviceError, ButtplugError, ButtplugMessageError } from "../core/Exceptions"; +import { EventEmitter } from "eventemitter3"; +import { ButtplugClientDeviceFeature } from "./ButtplugClientDeviceFeature"; +import { DeviceOutputCommand } from "./ButtplugClientDeviceCommand"; /** * Represents an abstract device, capable of taking certain kinds of messages. */ export class ButtplugClientDevice extends EventEmitter { - private _features: Map; /** @@ -58,9 +53,7 @@ export class ButtplugClientDevice extends EventEmitter { public static fromMsg( msg: Messages.DeviceInfo, - sendClosure: ( - msg: Messages.ButtplugMessage - ) => Promise + sendClosure: (msg: Messages.ButtplugMessage) => Promise, ): ButtplugClientDevice { return new ButtplugClientDevice(msg, sendClosure); } @@ -72,25 +65,29 @@ export class ButtplugClientDevice extends EventEmitter { */ private constructor( private _deviceInfo: Messages.DeviceInfo, - private _sendClosure: ( - msg: Messages.ButtplugMessage - ) => Promise + private _sendClosure: (msg: Messages.ButtplugMessage) => Promise, ) { super(); - this._features = new Map(Object.entries(_deviceInfo.DeviceFeatures).map(([index, v]) => [parseInt(index), new ButtplugClientDeviceFeature(_deviceInfo.DeviceIndex, _deviceInfo.DeviceName, v, _sendClosure)])); + this._features = new Map( + Object.entries(_deviceInfo.DeviceFeatures).map(([index, v]) => [ + parseInt(index), + new ButtplugClientDeviceFeature( + _deviceInfo.DeviceIndex, + _deviceInfo.DeviceName, + v, + _sendClosure, + ), + ]), + ); } - public async send( - msg: Messages.ButtplugMessage - ): Promise { + public async send(msg: Messages.ButtplugMessage): Promise { // Assume we're getting the closure from ButtplugClient, which does all of // the index/existence/connection/message checks for us. return await this._sendClosure(msg); } - protected sendMsgExpectOk = async ( - msg: Messages.ButtplugMessage - ): Promise => { + protected sendMsgExpectOk = async (msg: Messages.ButtplugMessage): Promise => { const response = await this.send(msg); if (response.Ok !== undefined) { return; @@ -109,19 +106,36 @@ export class ButtplugClientDevice extends EventEmitter { protected isOutputValid(featureIndex: number, type: Messages.OutputType) { if (!this._deviceInfo.DeviceFeatures.hasOwnProperty(featureIndex.toString())) { - throw new ButtplugDeviceError(`Feature index ${featureIndex} does not exist for device ${this.name}`); + throw new ButtplugDeviceError( + `Feature index ${featureIndex} does not exist for device ${this.name}`, + ); } - if (this._deviceInfo.DeviceFeatures[featureIndex.toString()].Outputs !== undefined && !this._deviceInfo.DeviceFeatures[featureIndex.toString()].Outputs.hasOwnProperty(type)) { - throw new ButtplugDeviceError(`Feature index ${featureIndex} does not support type ${type} for device ${this.name}`); + if ( + this._deviceInfo.DeviceFeatures[featureIndex.toString()].Outputs !== undefined && + !this._deviceInfo.DeviceFeatures[featureIndex.toString()].Outputs.hasOwnProperty(type) + ) { + throw new ButtplugDeviceError( + `Feature index ${featureIndex} does not support type ${type} for device ${this.name}`, + ); } } public hasOutput(type: Messages.OutputType): boolean { - return this._features.values().filter((f) => f.hasOutput(type)).toArray().length > 0; + return ( + this._features + .values() + .filter((f) => f.hasOutput(type)) + .toArray().length > 0 + ); } public hasInput(type: Messages.InputType): boolean { - return this._features.values().filter((f) => f.hasInput(type)).toArray().length > 0; + return ( + this._features + .values() + .filter((f) => f.hasInput(type)) + .toArray().length > 0 + ); } public async runOutput(cmd: DeviceOutputCommand): Promise { @@ -138,7 +152,15 @@ export class ButtplugClientDevice extends EventEmitter { } public async stop(): Promise { - await this.sendMsgExpectOk({StopCmd: { Id: 1, DeviceIndex: this.index, FeatureIndex: undefined, Inputs: true, Outputs: true}}); + await this.sendMsgExpectOk({ + StopCmd: { + Id: 1, + DeviceIndex: this.index, + FeatureIndex: undefined, + Inputs: true, + Outputs: true, + }, + }); } public async battery(): Promise { @@ -160,6 +182,6 @@ export class ButtplugClientDevice extends EventEmitter { } public emitDisconnected() { - this.emit('deviceremoved'); + this.emit("deviceremoved"); } } diff --git a/packages/buttplug/src/client/ButtplugClientDeviceCommand.ts b/packages/buttplug/src/client/ButtplugClientDeviceCommand.ts index d2858ad..53bb1a4 100644 --- a/packages/buttplug/src/client/ButtplugClientDeviceCommand.ts +++ b/packages/buttplug/src/client/ButtplugClientDeviceCommand.ts @@ -14,7 +14,7 @@ class PercentOrSteps { } public static createSteps(s: number): PercentOrSteps { - let v = new PercentOrSteps; + let v = new PercentOrSteps(); v._steps = s; return v; } @@ -24,7 +24,7 @@ class PercentOrSteps { throw new ButtplugDeviceError(`Percent value ${p} is not in the range 0.0 <= x <= 1.0`); } - let v = new PercentOrSteps; + let v = new PercentOrSteps(); v._percent = p; return v; } @@ -35,8 +35,7 @@ export class DeviceOutputCommand { private _outputType: OutputType, private _value: PercentOrSteps, private _duration?: number, - ) - {} + ) {} public get outputType() { return this._outputType; @@ -52,26 +51,36 @@ export class DeviceOutputCommand { } export class DeviceOutputValueConstructor { - public constructor( - private _outputType: OutputType) - {} + public constructor(private _outputType: OutputType) {} public steps(steps: number): DeviceOutputCommand { return new DeviceOutputCommand(this._outputType, PercentOrSteps.createSteps(steps), undefined); } public percent(percent: number): DeviceOutputCommand { - return new DeviceOutputCommand(this._outputType, PercentOrSteps.createPercent(percent), undefined); + return new DeviceOutputCommand( + this._outputType, + PercentOrSteps.createPercent(percent), + undefined, + ); } } export class DeviceOutputPositionWithDurationConstructor { public steps(steps: number, duration: number): DeviceOutputCommand { - return new DeviceOutputCommand(OutputType.Position, PercentOrSteps.createSteps(steps), duration); + return new DeviceOutputCommand( + OutputType.Position, + PercentOrSteps.createSteps(steps), + duration, + ); } public percent(percent: number, duration: number): DeviceOutputCommand { - return new DeviceOutputCommand(OutputType.HwPositionWithDuration, PercentOrSteps.createPercent(percent), duration); + return new DeviceOutputCommand( + OutputType.HwPositionWithDuration, + PercentOrSteps.createPercent(percent), + duration, + ); } } diff --git a/packages/buttplug/src/client/ButtplugClientDeviceFeature.ts b/packages/buttplug/src/client/ButtplugClientDeviceFeature.ts index 911b46b..2135e75 100644 --- a/packages/buttplug/src/client/ButtplugClientDeviceFeature.ts +++ b/packages/buttplug/src/client/ButtplugClientDeviceFeature.ts @@ -3,23 +3,18 @@ import * as Messages from "../core/Messages"; import { DeviceOutputCommand } from "./ButtplugClientDeviceCommand"; export class ButtplugClientDeviceFeature { - constructor( private _deviceIndex: number, private _deviceName: string, private _feature: Messages.DeviceFeature, - private _sendClosure: ( - msg: Messages.ButtplugMessage - ) => Promise) { - } + private _sendClosure: (msg: Messages.ButtplugMessage) => Promise, + ) {} protected send = async (msg: Messages.ButtplugMessage): Promise => { return await this._sendClosure(msg); - } + }; - protected sendMsgExpectOk = async ( - msg: Messages.ButtplugMessage - ): Promise => { + protected sendMsgExpectOk = async (msg: Messages.ButtplugMessage): Promise => { const response = await this.send(msg); if (response.Ok !== undefined) { return; @@ -32,13 +27,17 @@ export class ButtplugClientDeviceFeature { protected isOutputValid(type: Messages.OutputType) { if (this._feature.Output !== undefined && !this._feature.Output.hasOwnProperty(type)) { - throw new ButtplugDeviceError(`Feature index ${this._feature.FeatureIndex} does not support type ${type} for device ${this._deviceName}`); + throw new ButtplugDeviceError( + `Feature index ${this._feature.FeatureIndex} does not support type ${type} for device ${this._deviceName}`, + ); } } protected isInputValid(type: Messages.InputType) { if (this._feature.Input !== undefined && !this._feature.Input.hasOwnProperty(type)) { - throw new ButtplugDeviceError(`Feature index ${this._feature.FeatureIndex} does not support type ${type} for device ${this._deviceName}`); + throw new ButtplugDeviceError( + `Feature index ${this._feature.FeatureIndex} does not support type ${type} for device ${this._deviceName}`, + ); } } @@ -74,8 +73,8 @@ export class ButtplugClientDeviceFeature { Id: 1, DeviceIndex: this._deviceIndex, FeatureIndex: this._feature.FeatureIndex, - Command: outCommand - } + Command: outCommand, + }, }; await this.sendMsgExpectOk(cmd); } @@ -124,20 +123,29 @@ export class ButtplugClientDeviceFeature { return false; } - public async runOutput(cmd: DeviceOutputCommand): Promise { - if (this._feature.Output !== undefined && this._feature.Output.hasOwnProperty(cmd.outputType.toString())) { + if ( + this._feature.Output !== undefined && + this._feature.Output.hasOwnProperty(cmd.outputType.toString()) + ) { return this.sendOutputCmd(cmd); } throw new ButtplugDeviceError(`Output type ${cmd.outputType} not supported by feature.`); } - public async runInput(inputType: Messages.InputType, inputCommand: Messages.InputCommandType): Promise { + public async runInput( + inputType: Messages.InputType, + inputCommand: Messages.InputCommandType, + ): Promise { // Make sure the requested feature is valid this.isInputValid(inputType); let inputAttributes = this._feature.Input[inputType]; console.log(this._feature.Input); - if ((inputCommand === Messages.InputCommandType.Unsubscribe && !inputAttributes.Command.includes(Messages.InputCommandType.Subscribe)) && !inputAttributes.Command.includes(inputCommand)) { + if ( + inputCommand === Messages.InputCommandType.Unsubscribe && + !inputAttributes.Command.includes(Messages.InputCommandType.Subscribe) && + !inputAttributes.Command.includes(inputCommand) + ) { throw new ButtplugDeviceError(`${inputType} does not support command ${inputCommand}`); } @@ -148,7 +156,7 @@ export class ButtplugClientDeviceFeature { FeatureIndex: this._feature.FeatureIndex, Type: inputType, Command: inputCommand, - } + }, }; if (inputCommand == Messages.InputCommandType.Read) { const response = await this.send(cmd); diff --git a/packages/buttplug/src/client/ButtplugNodeWebsocketClientConnector.ts b/packages/buttplug/src/client/ButtplugNodeWebsocketClientConnector.ts index 5eeb535..53e921c 100644 --- a/packages/buttplug/src/client/ButtplugNodeWebsocketClientConnector.ts +++ b/packages/buttplug/src/client/ButtplugNodeWebsocketClientConnector.ts @@ -6,12 +6,11 @@ * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. */ -'use strict'; +"use strict"; -import { ButtplugBrowserWebsocketClientConnector } from './ButtplugBrowserWebsocketClientConnector'; -import { WebSocket as NodeWebSocket } from 'ws'; +import { ButtplugBrowserWebsocketClientConnector } from "./ButtplugBrowserWebsocketClientConnector"; +import { WebSocket as NodeWebSocket } from "ws"; export class ButtplugNodeWebsocketClientConnector extends ButtplugBrowserWebsocketClientConnector { - protected _websocketConstructor = - NodeWebSocket as unknown as typeof WebSocket; + protected _websocketConstructor = NodeWebSocket as unknown as typeof WebSocket; } diff --git a/packages/buttplug/src/client/IButtplugClientConnector.ts b/packages/buttplug/src/client/IButtplugClientConnector.ts index 110b191..5f53586 100644 --- a/packages/buttplug/src/client/IButtplugClientConnector.ts +++ b/packages/buttplug/src/client/IButtplugClientConnector.ts @@ -6,8 +6,8 @@ * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. */ -import { ButtplugMessage } from '../core/Messages'; -import { EventEmitter } from 'eventemitter3'; +import { ButtplugMessage } from "../core/Messages"; +import { EventEmitter } from "eventemitter3"; export interface IButtplugClientConnector extends EventEmitter { connect: () => Promise; diff --git a/packages/buttplug/src/core/Exceptions.ts b/packages/buttplug/src/core/Exceptions.ts index 04d047c..4f7470e 100644 --- a/packages/buttplug/src/core/Exceptions.ts +++ b/packages/buttplug/src/core/Exceptions.ts @@ -6,8 +6,8 @@ * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. */ -import * as Messages from './Messages'; -import { ButtplugLogger } from './Logging'; +import * as Messages from "./Messages"; +import { ButtplugLogger } from "./Logging"; export class ButtplugError extends Error { public get ErrorClass(): Messages.ErrorClass { @@ -27,16 +27,16 @@ export class ButtplugError extends Error { Error: { Id: this.Id, ErrorCode: this.ErrorClass, - ErrorMessage: this.message - } - } + ErrorMessage: this.message, + }, + }; } public static LogAndError( constructor: new (str: string, num: number) => T, logger: ButtplugLogger, message: string, - id: number = Messages.SYSTEM_MESSAGE_ID + id: number = Messages.SYSTEM_MESSAGE_ID, ): T { logger.Error(message); return new constructor(message, id); @@ -67,7 +67,7 @@ export class ButtplugError extends Error { message: string, errorClass: Messages.ErrorClass, id: number = Messages.SYSTEM_MESSAGE_ID, - inner?: Error + inner?: Error, ) { super(message); this.errorClass = errorClass; diff --git a/packages/buttplug/src/core/Logging.ts b/packages/buttplug/src/core/Logging.ts index 8fab968..ed05211 100644 --- a/packages/buttplug/src/core/Logging.ts +++ b/packages/buttplug/src/core/Logging.ts @@ -6,7 +6,7 @@ * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. */ -import { EventEmitter } from 'eventemitter3'; +import { EventEmitter } from "eventemitter3"; export enum ButtplugLogLevel { Off, @@ -69,9 +69,7 @@ export class LogMessage { * Returns a formatted string with timestamp, level, and message. */ public get FormattedMessage() { - return `${ButtplugLogLevel[this.logLevel]} : ${this.timestamp} : ${ - this.logMessage - }`; + return `${ButtplugLogLevel[this.logLevel]} : ${this.timestamp} : ${this.logMessage}`; } } @@ -176,10 +174,7 @@ export class ButtplugLogger extends EventEmitter { */ protected AddLogMessage(msg: string, level: ButtplugLogLevel) { // If nothing wants the log message we have, ignore it. - if ( - level > this.maximumEventLogLevel && - level > this.maximumConsoleLogLevel - ) { + if (level > this.maximumEventLogLevel && level > this.maximumConsoleLogLevel) { return; } const logMsg = new LogMessage(msg, level); @@ -191,7 +186,7 @@ export class ButtplugLogger extends EventEmitter { console.log(logMsg.FormattedMessage); } if (level <= this.maximumEventLogLevel) { - this.emit('log', logMsg); + this.emit("log", logMsg); } } } diff --git a/packages/buttplug/src/core/Messages.ts b/packages/buttplug/src/core/Messages.ts index 6562f47..52a26ec 100644 --- a/packages/buttplug/src/core/Messages.ts +++ b/packages/buttplug/src/core/Messages.ts @@ -7,9 +7,9 @@ */ // tslint:disable:max-classes-per-file -'use strict'; +"use strict"; -import { ButtplugMessageError } from './Exceptions'; +import { ButtplugMessageError } from "./Exceptions"; export const SYSTEM_MESSAGE_ID = 0; export const DEFAULT_MESSAGE_ID = 1; @@ -132,34 +132,34 @@ export interface DeviceList { } export enum OutputType { - Unknown = 'Unknown', - Vibrate = 'Vibrate', - Rotate = 'Rotate', - Oscillate = 'Oscillate', - Constrict = 'Constrict', - Inflate = 'Inflate', - Position = 'Position', - HwPositionWithDuration = 'HwPositionWithDuration', - Temperature = 'Temperature', - Spray = 'Spray', - Led = 'Led', + Unknown = "Unknown", + Vibrate = "Vibrate", + Rotate = "Rotate", + Oscillate = "Oscillate", + Constrict = "Constrict", + Inflate = "Inflate", + Position = "Position", + HwPositionWithDuration = "HwPositionWithDuration", + Temperature = "Temperature", + Spray = "Spray", + Led = "Led", } export enum InputType { - Unknown = 'Unknown', - Battery = 'Battery', - RSSI = 'RSSI', - Button = 'Button', - Pressure = 'Pressure', + Unknown = "Unknown", + Battery = "Battery", + RSSI = "RSSI", + Button = "Button", + Pressure = "Pressure", // Temperature, // Accelerometer, // Gyro, } export enum InputCommandType { - Read = 'Read', - Subscribe = 'Subscribe', - Unsubscribe = 'Unsubscribe', + Read = "Read", + Subscribe = "Subscribe", + Unsubscribe = "Unsubscribe", } export interface DeviceFeatureInput { diff --git a/packages/buttplug/src/core/index.d.ts b/packages/buttplug/src/core/index.d.ts index a970ab3..df8135e 100644 --- a/packages/buttplug/src/core/index.d.ts +++ b/packages/buttplug/src/core/index.d.ts @@ -1,4 +1,4 @@ declare module "*.json" { - const content: string; - export default content; + const content: string; + export default content; } diff --git a/packages/buttplug/src/index.ts b/packages/buttplug/src/index.ts index 619369a..835bd4c 100644 --- a/packages/buttplug/src/index.ts +++ b/packages/buttplug/src/index.ts @@ -6,27 +6,24 @@ * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. */ -import { ButtplugMessage } from './core/Messages'; -import { IButtplugClientConnector } from './client/IButtplugClientConnector'; -import { EventEmitter } from 'eventemitter3'; +import { ButtplugMessage } from "./core/Messages"; +import { IButtplugClientConnector } from "./client/IButtplugClientConnector"; +import { EventEmitter } from "eventemitter3"; -export * from './client/ButtplugClient'; -export * from './client/ButtplugClientDevice'; -export * from './client/ButtplugBrowserWebsocketClientConnector'; -export * from './client/ButtplugNodeWebsocketClientConnector'; -export * from './client/ButtplugClientConnectorException'; -export * from './utils/ButtplugMessageSorter'; -export * from './client/ButtplugClientDeviceCommand'; -export * from './client/ButtplugClientDeviceFeature'; -export * from './client/IButtplugClientConnector'; -export * from './core/Messages'; -export * from './core/Logging'; -export * from './core/Exceptions'; +export * from "./client/ButtplugClient"; +export * from "./client/ButtplugClientDevice"; +export * from "./client/ButtplugBrowserWebsocketClientConnector"; +export * from "./client/ButtplugNodeWebsocketClientConnector"; +export * from "./client/ButtplugClientConnectorException"; +export * from "./utils/ButtplugMessageSorter"; +export * from "./client/ButtplugClientDeviceCommand"; +export * from "./client/ButtplugClientDeviceFeature"; +export * from "./client/IButtplugClientConnector"; +export * from "./core/Messages"; +export * from "./core/Logging"; +export * from "./core/Exceptions"; -export class ButtplugWasmClientConnector - extends EventEmitter - implements IButtplugClientConnector -{ +export class ButtplugWasmClientConnector extends EventEmitter implements IButtplugClientConnector { private static _loggingActivated = false; private static wasmInstance; private _connected: boolean = false; @@ -43,35 +40,30 @@ export class ButtplugWasmClientConnector private static maybeLoadWasm = async () => { if (ButtplugWasmClientConnector.wasmInstance == undefined) { - ButtplugWasmClientConnector.wasmInstance = await import( - '../wasm/index.js' - ); + ButtplugWasmClientConnector.wasmInstance = await import("../wasm/index.js"); } }; - public static activateLogging = async (logLevel: string = 'debug') => { + public static activateLogging = async (logLevel: string = "debug") => { await ButtplugWasmClientConnector.maybeLoadWasm(); if (this._loggingActivated) { - console.log('Logging already activated, ignoring.'); + console.log("Logging already activated, ignoring."); return; } - console.log('Turning on logging.'); - ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger( - logLevel, - ); + console.log("Turning on logging."); + ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger(logLevel); }; public initialize = async (): Promise => {}; public connect = async (): Promise => { await ButtplugWasmClientConnector.maybeLoadWasm(); - this.client = - ButtplugWasmClientConnector.wasmInstance.buttplug_create_embedded_wasm_server( - (msgs) => { - this.emitMessage(msgs); - }, - this.serverPtr, - ); + this.client = ButtplugWasmClientConnector.wasmInstance.buttplug_create_embedded_wasm_server( + (msgs) => { + this.emitMessage(msgs); + }, + this.serverPtr, + ); this._connected = true; }; @@ -80,7 +72,7 @@ export class ButtplugWasmClientConnector public send = (msg: ButtplugMessage): void => { ButtplugWasmClientConnector.wasmInstance.buttplug_client_send_json_message( this.client, - new TextEncoder().encode('[' + JSON.stringify(msg) + ']'), + new TextEncoder().encode("[" + JSON.stringify(msg) + "]"), (output) => { this.emitMessage(output); }, @@ -90,6 +82,6 @@ export class ButtplugWasmClientConnector private emitMessage = (msg: Uint8Array) => { const str = new TextDecoder().decode(msg); const msgs: ButtplugMessage[] = JSON.parse(str); - this.emit('message', msgs); + this.emit("message", msgs); }; } diff --git a/packages/buttplug/src/utils/ButtplugBrowserWebsocketConnector.ts b/packages/buttplug/src/utils/ButtplugBrowserWebsocketConnector.ts index 76a87ef..9381abf 100644 --- a/packages/buttplug/src/utils/ButtplugBrowserWebsocketConnector.ts +++ b/packages/buttplug/src/utils/ButtplugBrowserWebsocketConnector.ts @@ -6,10 +6,10 @@ * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. */ -'use strict'; +"use strict"; -import { EventEmitter } from 'eventemitter3'; -import { ButtplugMessage } from '../core/Messages'; +import { EventEmitter } from "eventemitter3"; +import { ButtplugMessage } from "../core/Messages"; export class ButtplugBrowserWebsocketConnector extends EventEmitter { protected _ws: WebSocket | undefined; @@ -26,18 +26,20 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter { public connect = async (): Promise => { return new Promise((resolve, reject) => { const ws = new (this._websocketConstructor ?? WebSocket)(this._url); - const onErrorCallback = (event: Event) => {reject(event)} - const onCloseCallback = (event: CloseEvent) => reject(event.reason) - ws.addEventListener('open', async () => { + const onErrorCallback = (event: Event) => { + reject(event); + }; + const onCloseCallback = (event: CloseEvent) => reject(event.reason); + ws.addEventListener("open", async () => { this._ws = ws; try { await this.initialize(); - this._ws.addEventListener('message', (msg) => { + this._ws.addEventListener("message", (msg) => { this.parseIncomingMessage(msg); }); - this._ws.removeEventListener('close', onCloseCallback); - this._ws.removeEventListener('error', onErrorCallback); - this._ws.addEventListener('close', this.disconnect); + this._ws.removeEventListener("close", onCloseCallback); + this._ws.removeEventListener("error", onErrorCallback); + this._ws.addEventListener("close", this.disconnect); resolve(); } catch (e) { reject(e); @@ -47,8 +49,8 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter { // browsers usually only throw Error Code 1006. It's up to those using this // library to state what the problem might be. - ws.addEventListener('error', onErrorCallback) - ws.addEventListener('close', onCloseCallback); + ws.addEventListener("error", onErrorCallback); + ws.addEventListener("close", onCloseCallback); }); }; @@ -58,14 +60,14 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter { } this._ws!.close(); this._ws = undefined; - this.emit('disconnect'); + this.emit("disconnect"); }; public sendMessage(msg: ButtplugMessage) { if (!this.Connected) { - throw new Error('ButtplugBrowserWebsocketConnector not connected'); + throw new Error("ButtplugBrowserWebsocketConnector not connected"); } - this._ws!.send('[' + JSON.stringify(msg) + ']'); + this._ws!.send("[" + JSON.stringify(msg) + "]"); } public initialize = async (): Promise => { @@ -73,9 +75,9 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter { }; protected parseIncomingMessage(event: MessageEvent) { - if (typeof event.data === 'string') { + if (typeof event.data === "string") { const msgs: ButtplugMessage[] = JSON.parse(event.data); - this.emit('message', msgs); + this.emit("message", msgs); } else if (event.data instanceof Blob) { // No-op, we only use text message types. } @@ -83,6 +85,6 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter { protected onReaderLoad(event: Event) { const msgs: ButtplugMessage[] = JSON.parse((event.target as FileReader).result as string); - this.emit('message', msgs); + this.emit("message", msgs); } } diff --git a/packages/buttplug/src/utils/ButtplugMessageSorter.ts b/packages/buttplug/src/utils/ButtplugMessageSorter.ts index d9deb42..9598c09 100644 --- a/packages/buttplug/src/utils/ButtplugMessageSorter.ts +++ b/packages/buttplug/src/utils/ButtplugMessageSorter.ts @@ -6,8 +6,8 @@ * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. */ -import * as Messages from '../core/Messages'; -import { ButtplugError } from '../core/Exceptions'; +import * as Messages from "../core/Messages"; +import { ButtplugError } from "../core/Exceptions"; export class ButtplugMessageSorter { protected _counter = 1; @@ -21,9 +21,7 @@ export class ButtplugMessageSorter { // One of the places we should actually return a promise, as we need to store // them while waiting for them to return across the line. // tslint:disable:promise-function-async - public PrepareOutgoingMessage( - msg: Messages.ButtplugMessage - ): Promise { + public PrepareOutgoingMessage(msg: Messages.ButtplugMessage): Promise { if (this._useCounter) { Messages.setMsgId(msg, this._counter); // Always increment last, otherwise we might lose sync @@ -31,19 +29,15 @@ export class ButtplugMessageSorter { } let res; let rej; - const msgPromise = new Promise( - (resolve, reject) => { - res = resolve; - rej = reject; - } - ); + const msgPromise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); this._waitingMsgs.set(Messages.msgId(msg), [res, rej]); return msgPromise; } - public ParseIncomingMessages( - msgs: Messages.ButtplugMessage[] - ): Messages.ButtplugMessage[] { + public ParseIncomingMessages(msgs: Messages.ButtplugMessage[]): Messages.ButtplugMessage[] { const noMatch: Messages.ButtplugMessage[] = []; for (const x of msgs) { let id = Messages.msgId(x); diff --git a/packages/buttplug/src/utils/Utils.ts b/packages/buttplug/src/utils/Utils.ts index 7d460e0..50afea2 100644 --- a/packages/buttplug/src/utils/Utils.ts +++ b/packages/buttplug/src/utils/Utils.ts @@ -1,3 +1,3 @@ export function getRandomInt(max: number) { - return Math.floor(Math.random() * Math.floor(max)); + return Math.floor(Math.random() * Math.floor(max)); } diff --git a/packages/buttplug/tsconfig.json b/packages/buttplug/tsconfig.json index c6ec8e4..1c80fe8 100644 --- a/packages/buttplug/tsconfig.json +++ b/packages/buttplug/tsconfig.json @@ -1,11 +1,11 @@ { - "compilerOptions": { - "target": "esnext", - "module": "esnext", - "outDir": "dist", - "moduleResolution": "bundler", - "esModuleInterop": true, - "skipLibCheck": true - }, - "include": ["src"] + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "outDir": "dist", + "moduleResolution": "bundler", + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src"] } diff --git a/packages/buttplug/vite.config.ts b/packages/buttplug/vite.config.ts index 839447e..fa827e1 100644 --- a/packages/buttplug/vite.config.ts +++ b/packages/buttplug/vite.config.ts @@ -3,19 +3,19 @@ import path from "path"; import wasm from "vite-plugin-wasm"; export default defineConfig({ - plugins: [wasm()], // include wasm plugin - build: { - lib: { - entry: path.resolve(__dirname, "src/index.ts"), - name: "buttplug", - fileName: "index", - formats: ["es"], // this is important - }, - minify: false, // for demo purposes - target: "esnext", // this is important as well - outDir: "dist", - rollupOptions: { - external: [/\.\/wasm\//, /\.\.\/wasm\//], - }, - }, + plugins: [wasm()], // include wasm plugin + build: { + lib: { + entry: path.resolve(__dirname, "src/index.ts"), + name: "buttplug", + fileName: "index", + formats: ["es"], // this is important + }, + minify: false, // for demo purposes + target: "esnext", // this is important as well + outDir: "dist", + rollupOptions: { + external: [/\.\/wasm\//, /\.\.\/wasm\//], + }, + }, }); diff --git a/packages/frontend/components.json b/packages/frontend/components.json index c5d91b4..298e869 100644 --- a/packages/frontend/components.json +++ b/packages/frontend/components.json @@ -1,16 +1,16 @@ { - "$schema": "https://shadcn-svelte.com/schema.json", - "tailwind": { - "css": "src/app.css", - "baseColor": "slate" - }, - "aliases": { - "components": "$lib/components", - "utils": "$lib/utils", - "ui": "$lib/components/ui", - "hooks": "$lib/hooks", - "lib": "$lib" - }, - "typescript": true, - "registry": "https://shadcn-svelte.com/registry" + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/app.css", + "baseColor": "slate" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" } diff --git a/packages/frontend/jsrepo.json b/packages/frontend/jsrepo.json index b2af8c9..fcfa7dd 100644 --- a/packages/frontend/jsrepo.json +++ b/packages/frontend/jsrepo.json @@ -1,16 +1,16 @@ { - "$schema": "https://unpkg.com/jsrepo@2.4.9/schemas/project-config.json", - "repos": ["@ieedan/shadcn-svelte-extras"], - "includeTests": false, - "includeDocs": false, - "watermark": true, - "formatter": "prettier", - "configFiles": {}, - "paths": { - "*": "$lib/blocks", - "ui": "$lib/components/ui", - "actions": "$lib/actions", - "hooks": "$lib/hooks", - "utils": "$lib/utils" - } + "$schema": "https://unpkg.com/jsrepo@2.4.9/schemas/project-config.json", + "repos": ["@ieedan/shadcn-svelte-extras"], + "includeTests": false, + "includeDocs": false, + "watermark": true, + "formatter": "prettier", + "configFiles": {}, + "paths": { + "*": "$lib/blocks", + "ui": "$lib/components/ui", + "actions": "$lib/actions", + "hooks": "$lib/hooks", + "utils": "$lib/utils" + } } diff --git a/packages/frontend/src/app.css b/packages/frontend/src/app.css index dd85862..435208f 100644 --- a/packages/frontend/src/app.css +++ b/packages/frontend/src/app.css @@ -8,82 +8,82 @@ @custom-variant hover (&:hover); @theme { - --animate-vibrate: vibrate 0.3s linear infinite; - --animate-fade-in: fadeIn 0.3s ease-out; - --animate-slide-up: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1); - --animate-zoom-in: zoomIn 0.4s cubic-bezier(0.4, 0, 0.2, 1); - --animate-pulse-glow: pulseGlow 2s infinite; + --animate-vibrate: vibrate 0.3s linear infinite; + --animate-fade-in: fadeIn 0.3s ease-out; + --animate-slide-up: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1); + --animate-zoom-in: zoomIn 0.4s cubic-bezier(0.4, 0, 0.2, 1); + --animate-pulse-glow: pulseGlow 2s infinite; - @keyframes vibrate { - 0% { - transform: translate(0); - } + @keyframes vibrate { + 0% { + transform: translate(0); + } - 20% { - transform: translate(-2px, 2px); - } + 20% { + transform: translate(-2px, 2px); + } - 40% { - transform: translate(-2px, -2px); - } + 40% { + transform: translate(-2px, -2px); + } - 60% { - transform: translate(2px, 2px); - } + 60% { + transform: translate(2px, 2px); + } - 80% { - transform: translate(2px, -2px); - } + 80% { + transform: translate(2px, -2px); + } - 100% { - transform: translate(0); - } - } + 100% { + transform: translate(0); + } + } - @keyframes fadeIn { - 0% { - opacity: 0; - } + @keyframes fadeIn { + 0% { + opacity: 0; + } - 100% { - opacity: 1; - } - } + 100% { + opacity: 1; + } + } - @keyframes slideUp { - 0% { - opacity: 0; - transform: translateY(30px) scale(0.95); - } + @keyframes slideUp { + 0% { + opacity: 0; + transform: translateY(30px) scale(0.95); + } - 100% { - opacity: 1; - transform: translateY(0) scale(1); - } - } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } + } - @keyframes zoomIn { - 0% { - opacity: 0; - transform: scale(0.9); - } + @keyframes zoomIn { + 0% { + opacity: 0; + transform: scale(0.9); + } - 100% { - opacity: 1; - transform: scale(1); - } - } + 100% { + opacity: 1; + transform: scale(1); + } + } - @keyframes pulseGlow { - 0%, - 100% { - boxShadow: 0 0 20px rgba(183, 0, 217, 0.3); - } + @keyframes pulseGlow { + 0%, + 100% { + boxshadow: 0 0 20px rgba(183, 0, 217, 0.3); + } - 50% { - boxShadow: 0 0 40px rgba(183, 0, 217, 0.6); - } - } + 50% { + boxshadow: 0 0 40px rgba(183, 0, 217, 0.6); + } + } } /* @@ -95,134 +95,134 @@ color utility to any element that depends on these defaults. */ @layer base { - * { - @supports (color: color-mix(in lab, red, red)) { - outline-color: color-mix(in oklab, var(--ring) 50%, transparent); - } - } + * { + @supports (color: color-mix(in lab, red, red)) { + outline-color: color-mix(in oklab, var(--ring) 50%, transparent); + } + } - * { - border-color: var(--border); - outline-color: var(--ring); - } + * { + border-color: var(--border); + outline-color: var(--ring); + } - .prose h2 { - @apply text-2xl font-bold mt-8 mb-4 text-foreground; - } + .prose h2 { + @apply text-2xl font-bold mt-8 mb-4 text-foreground; + } - .prose h3 { - @apply text-xl font-semibold mt-6 mb-3 text-foreground; - } + .prose h3 { + @apply text-xl font-semibold mt-6 mb-3 text-foreground; + } - .prose p { - @apply mb-4 leading-relaxed; - } + .prose p { + @apply mb-4 leading-relaxed; + } - .prose ul { - @apply mb-4 pl-6; - } + .prose ul { + @apply mb-4 pl-6; + } - .prose li { - @apply mb-2; - } + .prose li { + @apply mb-2; + } } :root { - --default-font-family: "Noto Sans", sans-serif; - --background: oklch(0.98 0.01 320); - --foreground: oklch(0.08 0.02 280); - --muted: oklch(0.95 0.01 280); - --muted-foreground: oklch(0.4 0.02 280); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --card: oklch(0.99 0.005 320); - --card-foreground: oklch(0.08 0.02 280); - --border: oklch(0.85 0.02 280); - --input: oklch(0.922 0 0); - --primary: oklch(56.971% 0.27455 319.257); - --primary-foreground: oklch(0.98 0.01 320); - --secondary: oklch(0.92 0.02 260); - --secondary-foreground: oklch(0.15 0.05 260); - --accent: oklch(0.45 0.35 280); - --accent-foreground: oklch(0.98 0.01 280); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.985 0 0); - --ring: oklch(0.55 0.3 320); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --default-font-family: "Noto Sans", sans-serif; + --background: oklch(0.98 0.01 320); + --foreground: oklch(0.08 0.02 280); + --muted: oklch(0.95 0.01 280); + --muted-foreground: oklch(0.4 0.02 280); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --card: oklch(0.99 0.005 320); + --card-foreground: oklch(0.08 0.02 280); + --border: oklch(0.85 0.02 280); + --input: oklch(0.922 0 0); + --primary: oklch(56.971% 0.27455 319.257); + --primary-foreground: oklch(0.98 0.01 320); + --secondary: oklch(0.92 0.02 260); + --secondary-foreground: oklch(0.15 0.05 260); + --accent: oklch(0.45 0.35 280); + --accent-foreground: oklch(0.98 0.01 280); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.985 0 0); + --ring: oklch(0.55 0.3 320); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } .dark { - --background: oklch(0.08 0.02 280); - --foreground: oklch(0.98 0.01 280); - --muted: oklch(0.12 0.03 280); - --muted-foreground: oklch(0.6 0.02 280); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --card: oklch(0.1 0.02 280); - --card-foreground: oklch(0.95 0.01 280); - --border: oklch(0.2 0.05 280); - --input: oklch(1 0 0 / 0.15); - --primary: oklch(0.65 0.25 320); - --primary-foreground: oklch(0.98 0.01 320); - --secondary: oklch(0.15 0.05 260); - --secondary-foreground: oklch(0.9 0.02 260); - --accent: oklch(0.55 0.3 280); - --accent-foreground: oklch(0.98 0.01 280); - --destructive: oklch(0.704 0.191 22.216); - --destructive-foreground: oklch(0.985 0 0); - --ring: oklch(0.65 0.25 320); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 0.1); - --sidebar-ring: oklch(0.556 0 0); + --background: oklch(0.08 0.02 280); + --foreground: oklch(0.98 0.01 280); + --muted: oklch(0.12 0.03 280); + --muted-foreground: oklch(0.6 0.02 280); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --card: oklch(0.1 0.02 280); + --card-foreground: oklch(0.95 0.01 280); + --border: oklch(0.2 0.05 280); + --input: oklch(1 0 0 / 0.15); + --primary: oklch(0.65 0.25 320); + --primary-foreground: oklch(0.98 0.01 320); + --secondary: oklch(0.15 0.05 260); + --secondary-foreground: oklch(0.9 0.02 260); + --accent: oklch(0.55 0.3 280); + --accent-foreground: oklch(0.98 0.01 280); + --destructive: oklch(0.704 0.191 22.216); + --destructive-foreground: oklch(0.985 0 0); + --ring: oklch(0.65 0.25 320); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 0.1); + --sidebar-ring: oklch(0.556 0 0); } @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); - --font-sans: var(--font-sans); - --font-mono: var(--font-mono); - --font-serif: var(--font-serif); + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-serif: var(--font-serif); } diff --git a/packages/frontend/src/app.d.ts b/packages/frontend/src/app.d.ts index 6cffbe9..8e46147 100644 --- a/packages/frontend/src/app.d.ts +++ b/packages/frontend/src/app.d.ts @@ -4,22 +4,22 @@ import type { AuthStatus } from "$lib/types"; // for information about these interfaces declare global { - namespace App { - // interface Error {} - interface Locals { - authStatus: AuthStatus; - requestId: string; - } - // interface PageData {} - // interface PageState {} - // interface Platform {} - } - interface Window { - sidebar: { - addPanel: () => void; - }; - opera: object; - } + namespace App { + // interface Error {} + interface Locals { + authStatus: AuthStatus; + requestId: string; + } + // interface PageData {} + // interface PageState {} + // interface Platform {} + } + interface Window { + sidebar: { + addPanel: () => void; + }; + opera: object; + } } export {}; diff --git a/packages/frontend/src/app.html b/packages/frontend/src/app.html index 8e033bb..e2623be 100644 --- a/packages/frontend/src/app.html +++ b/packages/frontend/src/app.html @@ -1,24 +1,23 @@ - - + - - - - - + + + %sveltekit.head% - + - +
%sveltekit.body%
- - - \ No newline at end of file + + diff --git a/packages/frontend/src/hooks.server.ts b/packages/frontend/src/hooks.server.ts index f2bb3f0..a1cfe09 100644 --- a/packages/frontend/src/hooks.server.ts +++ b/packages/frontend/src/hooks.server.ts @@ -6,88 +6,88 @@ import type { Handle } from "@sveltejs/kit"; logger.startup(); export const handle: Handle = async ({ event, resolve }) => { - const { cookies, locals, url, request } = event; - const startTime = Date.now(); + const { cookies, locals, url, request } = event; + const startTime = Date.now(); - // Generate unique request ID - const requestId = generateRequestId(); + // Generate unique request ID + const requestId = generateRequestId(); - // Add request ID to locals for access in other handlers - locals.requestId = requestId; + // Add request ID to locals for access in other handlers + locals.requestId = requestId; - // Log incoming request - logger.request(request.method, url.pathname, { - requestId, - context: { - userAgent: request.headers.get('user-agent')?.substring(0, 100), - referer: request.headers.get('referer'), - ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'), - }, - }); + // Log incoming request + logger.request(request.method, url.pathname, { + requestId, + context: { + userAgent: request.headers.get("user-agent")?.substring(0, 100), + referer: request.headers.get("referer"), + ip: request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip"), + }, + }); - // Handle authentication - const token = cookies.get("session_token"); + // Handle authentication + const token = cookies.get("session_token"); - if (token) { - try { - locals.authStatus = await isAuthenticated(token); + if (token) { + try { + locals.authStatus = await isAuthenticated(token); - if (locals.authStatus.authenticated) { - logger.auth('Token validated', true, { - requestId, - userId: locals.authStatus.user?.id, - context: { - email: locals.authStatus.user?.email, - role: locals.authStatus.user?.role, - }, - }); - } else { - logger.auth('Token invalid', false, { requestId }); - } - } catch (error) { - logger.error('Authentication check failed', { - requestId, - error: error instanceof Error ? error : new Error(String(error)), - }); - locals.authStatus = { authenticated: false }; - } - } else { - logger.debug('No session token found', { requestId }); - locals.authStatus = { authenticated: false }; - } + if (locals.authStatus.authenticated) { + logger.auth("Token validated", true, { + requestId, + userId: locals.authStatus.user?.id, + context: { + email: locals.authStatus.user?.email, + role: locals.authStatus.user?.role, + }, + }); + } else { + logger.auth("Token invalid", false, { requestId }); + } + } catch (error) { + logger.error("Authentication check failed", { + requestId, + error: error instanceof Error ? error : new Error(String(error)), + }); + locals.authStatus = { authenticated: false }; + } + } else { + logger.debug("No session token found", { requestId }); + locals.authStatus = { authenticated: false }; + } - // Resolve the request - let response: Response; - try { - response = await resolve(event, { - filterSerializedResponseHeaders: (key) => { - return key.toLowerCase() === "content-type"; - }, - }); - } catch (error) { - const duration = Date.now() - startTime; - logger.error('Request handler error', { - requestId, - method: request.method, - path: url.pathname, - duration, - error: error instanceof Error ? error : new Error(String(error)), - }); - throw error; - } + // Resolve the request + let response: Response; + try { + response = await resolve(event, { + filterSerializedResponseHeaders: (key) => { + return key.toLowerCase() === "content-type"; + }, + }); + } catch (error) { + const duration = Date.now() - startTime; + logger.error("Request handler error", { + requestId, + method: request.method, + path: url.pathname, + duration, + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } - // Log response - const duration = Date.now() - startTime; - logger.response(request.method, url.pathname, response.status, duration, { - requestId, - userId: locals.authStatus.authenticated ? locals.authStatus.user?.id : undefined, - context: { - cached: response.headers.get('x-sveltekit-page') === 'true', - }, - }); + // Log response + const duration = Date.now() - startTime; + logger.response(request.method, url.pathname, response.status, duration, { + requestId, + userId: locals.authStatus.authenticated ? locals.authStatus.user?.id : undefined, + context: { + cached: response.headers.get("x-sveltekit-page") === "true", + }, + }); - // Add request ID to response headers (useful for debugging) - response.headers.set('x-request-id', requestId); + // Add request ID to response headers (useful for debugging) + response.headers.set("x-request-id", requestId); - return response; + return response; }; diff --git a/packages/frontend/src/lib/components/age-verification-dialog/age-verification-dialog.svelte b/packages/frontend/src/lib/components/age-verification-dialog/age-verification-dialog.svelte index eba0923..0f9344c 100644 --- a/packages/frontend/src/lib/components/age-verification-dialog/age-verification-dialog.svelte +++ b/packages/frontend/src/lib/components/age-verification-dialog/age-verification-dialog.svelte @@ -1,77 +1,70 @@ - e.preventDefault()} - showCloseButton={false} - > - -
-
-
- {$_("age_verification_dialog.age")} -
-
- {$_("age_verification_dialog.title")} - - {$_("age_verification_dialog.description")} - -
-
-
-
- - - - -
- - + + {$_("age_verification_dialog.description")} + +
-
+ + + + + + +
+ + +
+
diff --git a/packages/frontend/src/lib/components/background/peony-background.svelte b/packages/frontend/src/lib/components/background/peony-background.svelte index fba4f00..8abaa22 100644 --- a/packages/frontend/src/lib/components/background/peony-background.svelte +++ b/packages/frontend/src/lib/components/background/peony-background.svelte @@ -1,55 +1,55 @@
- -
-
+ +
+
- - + - - + - - + - - + - -
-
-
+ +
+
+
- - + -
- - - + + + + - - - + - -
-
-
- - {$_("device_card.battery")} -
- {#if device.hasBattery} - - {device.batteryLevel}% - - {/if} -
-
-
-
+ +
+
+
+ + {$_("device_card.battery")}
+ {#if device.hasBattery} + + {device.batteryLevel}% + + {/if} +
+
+
+
+
- - + - - {#each device.actuators as actuator, idx (idx)} -
- - onChange(idx, val)} - max={actuator.maxSteps} - step={1} - /> -
- {/each} - + + {#each device.actuators as actuator, idx (idx)} +
+ + onChange(idx, val)} + max={actuator.maxSteps} + step={1} + /> +
+ {/each} + diff --git a/packages/frontend/src/lib/components/footer/footer.svelte b/packages/frontend/src/lib/components/footer/footer.svelte index 04e3c85..63695a2 100644 --- a/packages/frontend/src/lib/components/footer/footer.svelte +++ b/packages/frontend/src/lib/components/footer/footer.svelte @@ -1,120 +1,120 @@ diff --git a/packages/frontend/src/lib/components/girls/girls.svelte b/packages/frontend/src/lib/components/girls/girls.svelte index 1dd55b2..3a23019 100644 --- a/packages/frontend/src/lib/components/girls/girls.svelte +++ b/packages/frontend/src/lib/components/girls/girls.svelte @@ -7,9 +7,7 @@ stroke="#ce47eb" preserveAspectRatio="xMidYMid meet" > - - Created by potrace 1.15, written by Peter Selinger 2001-2017 - + Created by potrace 1.15, written by Peter Selinger 2001-2017 -import { _ } from "svelte-i18n"; -import { page } from "$app/state"; -import { Button } from "$lib/components/ui/button"; -import type { AuthStatus } from "$lib/types"; -import { logout } from "$lib/services"; -import { goto } from "$app/navigation"; -import { getAssetUrl } from "$lib/directus"; -import LogoutButton from "../logout-button/logout-button.svelte"; -import Separator from "../ui/separator/separator.svelte"; -import { Avatar, AvatarFallback, AvatarImage } from "$lib/components/ui/avatar"; -import { getUserInitials } from "$lib/utils"; -import BurgerMenuButton from "../burger-menu-button/burger-menu-button.svelte"; -import Girls from "../girls/girls.svelte"; -import Logo from "../logo/logo.svelte"; + import { _ } from "svelte-i18n"; + import { page } from "$app/state"; + import { Button } from "$lib/components/ui/button"; + import type { AuthStatus } from "$lib/types"; + import { logout } from "$lib/services"; + import { goto } from "$app/navigation"; + import { getAssetUrl } from "$lib/directus"; + import LogoutButton from "../logout-button/logout-button.svelte"; + import Separator from "../ui/separator/separator.svelte"; + import { Avatar, AvatarFallback, AvatarImage } from "$lib/components/ui/avatar"; + import { getUserInitials } from "$lib/utils"; + import BurgerMenuButton from "../burger-menu-button/burger-menu-button.svelte"; + import Girls from "../girls/girls.svelte"; + import Logo from "../logo/logo.svelte"; -interface Props { - authStatus: AuthStatus; -} + interface Props { + authStatus: AuthStatus; + } -let { authStatus }: Props = $props(); + let { authStatus }: Props = $props(); -let isMobileMenuOpen = $state(false); + let isMobileMenuOpen = $state(false); -const navLinks = [ - { name: $_("header.home"), href: "/" }, - { name: $_("header.models"), href: "/models" }, - { name: $_("header.videos"), href: "/videos" }, - { name: $_("header.magazine"), href: "/magazine" }, - { name: $_("header.about"), href: "/about" }, -]; + const navLinks = [ + { name: $_("header.home"), href: "/" }, + { name: $_("header.models"), href: "/models" }, + { name: $_("header.videos"), href: "/videos" }, + { name: $_("header.magazine"), href: "/magazine" }, + { name: $_("header.about"), href: "/about" }, + ]; -async function handleLogout() { - closeMenu(); - await logout(); - goto("/login", { invalidateAll: true }); -} + async function handleLogout() { + closeMenu(); + await logout(); + goto("/login", { invalidateAll: true }); + } -function closeMenu() { - isMobileMenuOpen = false; -} + function closeMenu() { + isMobileMenuOpen = false; + } -function isActiveLink(link: any) { - return ( - (page.url.pathname === "/" && link === navLinks[0]) || - (page.url.pathname.startsWith(link.href) && link !== navLinks[0]) - ); -} + function isActiveLink(link: any) { + return ( + (page.url.pathname === "/" && link === navLinks[0]) || + (page.url.pathname.startsWith(link.href) && link !== navLinks[0]) + ); + }
- + @@ -67,12 +67,12 @@ function isActiveLink(link: any) { {link.name} {/each} @@ -95,29 +95,29 @@ function isActiveLink(link: any) {
{:else}
- + {$_("header.signup")}
{/if} (isMobileMenuOpen = !isMobileMenuOpen)} /> @@ -155,26 +154,24 @@ function isActiveLink(link: any) {
{#if isMobileMenuOpen}
-
+
- {$_('header.logout')} - {$_('header.logout_hint')} + {$_("header.logout")} + {$_("header.logout_hint")}
{/if} diff --git a/packages/frontend/src/lib/components/icon/peony-icon.svelte b/packages/frontend/src/lib/components/icon/peony-icon.svelte index fc45d72..d5d2999 100644 --- a/packages/frontend/src/lib/components/icon/peony-icon.svelte +++ b/packages/frontend/src/lib/components/icon/peony-icon.svelte @@ -1,25 +1,24 @@ - diff --git a/packages/frontend/src/lib/components/image-viewer/image-viewer.svelte b/packages/frontend/src/lib/components/image-viewer/image-viewer.svelte index ed0ffaa..03dbcb5 100644 --- a/packages/frontend/src/lib/components/image-viewer/image-viewer.svelte +++ b/packages/frontend/src/lib/components/image-viewer/image-viewer.svelte @@ -1,109 +1,107 @@
-
+
{#each images as image, index (index)} + + - -
- -
+ +
+ +
- - - + +
diff --git a/packages/frontend/src/lib/components/meta/meta.svelte b/packages/frontend/src/lib/components/meta/meta.svelte index 7131908..fa9f88b 100644 --- a/packages/frontend/src/lib/components/meta/meta.svelte +++ b/packages/frontend/src/lib/components/meta/meta.svelte @@ -1,27 +1,24 @@ - {$_("head.title", { values: { title } })} - - - - + {$_("head.title", { values: { title } })} + + + + diff --git a/packages/frontend/src/lib/components/recording-card/recording-card.svelte b/packages/frontend/src/lib/components/recording-card/recording-card.svelte index 3f3c26e..2e25320 100644 --- a/packages/frontend/src/lib/components/recording-card/recording-card.svelte +++ b/packages/frontend/src/lib/components/recording-card/recording-card.svelte @@ -1,180 +1,163 @@ - -
-
-
-

- {recording.title} -

- - {$_(`recording_card.status_${recording.status}`)} - -
- {#if recording.description} -

- {recording.description} -

- {/if} -
-
-
+ +
+
+
+

+ {recording.title} +

+ + {$_(`recording_card.status_${recording.status}`)} + +
+ {#if recording.description} +

+ {recording.description} +

+ {/if} +
+
+
- - -
-
- - {$_("recording_card.duration")} - {formatDuration(recording.duration)} -
-
- - {$_("recording_card.events")} - {recording.events.length} -
-
- - {$_("recording_card.devices")} - {recording.device_info.length} -
-
+ + +
+
+ + {$_("recording_card.duration")} + {formatDuration(recording.duration)} +
+
+ + {$_("recording_card.events")} + {recording.events.length} +
+
+ + {$_("recording_card.devices")} + {recording.device_info.length} +
+
- -
- {#each recording.device_info.slice(0, 2) as device (device.name)} -
- - {device.name} - • {device.capabilities.join(", ")} -
- {/each} - {#if recording.device_info.length > 2} -
- +{recording.device_info.length - 2} more device{recording.device_info.length - - 2 > - 1 - ? "s" - : ""} -
- {/if} -
+ +
+ {#each recording.device_info.slice(0, 2) as device (device.name)} +
+ + {device.name} + • {device.capabilities.join(", ")} +
+ {/each} + {#if recording.device_info.length > 2} +
+ +{recording.device_info.length - 2} more device{recording.device_info.length - 2 > 1 + ? "s" + : ""} +
+ {/if} +
- - {#if recording.tags && recording.tags.length > 0} -
- {#each recording.tags as tag (tag)} - - {tag} - - {/each} -
- {/if} + + {#if recording.tags && recording.tags.length > 0} +
+ {#each recording.tags as tag (tag)} + + {tag} + + {/each} +
+ {/if} - -
-
- - {new Date(recording.date_created).toLocaleDateString()} - - {#if recording.public} - - - {$_("recording_card.public")} - - {:else} - - - {$_("recording_card.private")} - - {/if} -
- {#if recording.linked_video} - - - {$_("recording_card.linked_video")} - - {/if} -
+ +
+
+ + {new Date(recording.date_created).toLocaleDateString()} + + {#if recording.public} + + + {$_("recording_card.public")} + + {:else} + + + {$_("recording_card.private")} + + {/if} +
+ {#if recording.linked_video} + + + {$_("recording_card.linked_video")} + + {/if} +
- -
- {#if onPlay} - - {/if} - {#if onDelete} - - {/if} -
-
+ +
+ {#if onPlay} + + {/if} + {#if onDelete} + + {/if} +
+
diff --git a/packages/frontend/src/lib/components/sharing-popup/share-button.svelte b/packages/frontend/src/lib/components/sharing-popup/share-button.svelte index fcc108b..0a1b4e4 100644 --- a/packages/frontend/src/lib/components/sharing-popup/share-button.svelte +++ b/packages/frontend/src/lib/components/sharing-popup/share-button.svelte @@ -1,17 +1,17 @@ diff --git a/packages/frontend/src/lib/components/sharing-popup/share-services.svelte b/packages/frontend/src/lib/components/sharing-popup/share-services.svelte index a0b83ec..f356e69 100644 --- a/packages/frontend/src/lib/components/sharing-popup/share-services.svelte +++ b/packages/frontend/src/lib/components/sharing-popup/share-services.svelte @@ -1,110 +1,110 @@
-
-

- {$_("sharing_popup.subtitle")} -

+
+

+ {$_("sharing_popup.subtitle")} +

-
- +
+ - + - + - + - + - -
+
+
diff --git a/packages/frontend/src/lib/components/sharing-popup/sharing-popup-button.svelte b/packages/frontend/src/lib/components/sharing-popup/sharing-popup-button.svelte index 391a444..83a8bff 100644 --- a/packages/frontend/src/lib/components/sharing-popup/sharing-popup-button.svelte +++ b/packages/frontend/src/lib/components/sharing-popup/sharing-popup-button.svelte @@ -1,10 +1,10 @@ diff --git a/packages/frontend/src/lib/components/sharing-popup/sharing-popup.svelte b/packages/frontend/src/lib/components/sharing-popup/sharing-popup.svelte index f283998..7f612d2 100644 --- a/packages/frontend/src/lib/components/sharing-popup/sharing-popup.svelte +++ b/packages/frontend/src/lib/components/sharing-popup/sharing-popup.svelte @@ -1,89 +1,89 @@ - - -
-
-
- -
-
- {$_("sharing_popup.title")} - - {$_("sharing_popup.description", { - values: { type: content.type }, - })} - -
-
-
- - -
-

- {content.title} -

-

{content.description}

-
- - {content.type} - - {content.url} -
-
-
- - - - - - - - - -
- + + {$_("sharing_popup.description", { + values: { type: content.type }, + })} + +
- +
+ + +
+

+ {content.title} +

+

{content.description}

+
+ + {content.type} + + {content.url} +
+
+ + + + + + + + + + +
+ +
+ diff --git a/packages/frontend/src/lib/components/ui/alert/alert-description.svelte b/packages/frontend/src/lib/components/ui/alert/alert-description.svelte index 0d89ee8..939ba5b 100644 --- a/packages/frontend/src/lib/components/ui/alert/alert-description.svelte +++ b/packages/frontend/src/lib/components/ui/alert/alert-description.svelte @@ -1,23 +1,23 @@
- {@render children?.()} + {@render children?.()}
diff --git a/packages/frontend/src/lib/components/ui/alert/alert-title.svelte b/packages/frontend/src/lib/components/ui/alert/alert-title.svelte index 730c97f..25feecc 100644 --- a/packages/frontend/src/lib/components/ui/alert/alert-title.svelte +++ b/packages/frontend/src/lib/components/ui/alert/alert-title.svelte @@ -1,20 +1,20 @@
- {@render children?.()} + {@render children?.()}
diff --git a/packages/frontend/src/lib/components/ui/alert/alert.svelte b/packages/frontend/src/lib/components/ui/alert/alert.svelte index 21e5fc6..1dbadbb 100644 --- a/packages/frontend/src/lib/components/ui/alert/alert.svelte +++ b/packages/frontend/src/lib/components/ui/alert/alert.svelte @@ -1,44 +1,44 @@ diff --git a/packages/frontend/src/lib/components/ui/alert/index.ts b/packages/frontend/src/lib/components/ui/alert/index.ts index 97e21b4..e47ba7d 100644 --- a/packages/frontend/src/lib/components/ui/alert/index.ts +++ b/packages/frontend/src/lib/components/ui/alert/index.ts @@ -4,11 +4,11 @@ import Title from "./alert-title.svelte"; export { alertVariants, type AlertVariant } from "./alert.svelte"; export { - Root, - Description, - Title, - // - Root as Alert, - Description as AlertDescription, - Title as AlertTitle, + Root, + Description, + Title, + // + Root as Alert, + Description as AlertDescription, + Title as AlertTitle, }; diff --git a/packages/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte b/packages/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte index 0b72f59..fee7425 100644 --- a/packages/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte +++ b/packages/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -1,17 +1,17 @@ diff --git a/packages/frontend/src/lib/components/ui/avatar/avatar-image.svelte b/packages/frontend/src/lib/components/ui/avatar/avatar-image.svelte index 2478fc1..8f9ceea 100644 --- a/packages/frontend/src/lib/components/ui/avatar/avatar-image.svelte +++ b/packages/frontend/src/lib/components/ui/avatar/avatar-image.svelte @@ -1,17 +1,17 @@ diff --git a/packages/frontend/src/lib/components/ui/avatar/avatar.svelte b/packages/frontend/src/lib/components/ui/avatar/avatar.svelte index efac1d7..0832b53 100644 --- a/packages/frontend/src/lib/components/ui/avatar/avatar.svelte +++ b/packages/frontend/src/lib/components/ui/avatar/avatar.svelte @@ -1,19 +1,19 @@ diff --git a/packages/frontend/src/lib/components/ui/avatar/index.ts b/packages/frontend/src/lib/components/ui/avatar/index.ts index d06457b..71f5b20 100644 --- a/packages/frontend/src/lib/components/ui/avatar/index.ts +++ b/packages/frontend/src/lib/components/ui/avatar/index.ts @@ -3,11 +3,11 @@ import Image from "./avatar-image.svelte"; import Fallback from "./avatar-fallback.svelte"; export { - Root, - Image, - Fallback, - // - Root as Avatar, - Image as AvatarImage, - Fallback as AvatarFallback, + Root, + Image, + Fallback, + // + Root as Avatar, + Image as AvatarImage, + Fallback as AvatarFallback, }; diff --git a/packages/frontend/src/lib/components/ui/button/button.svelte b/packages/frontend/src/lib/components/ui/button/button.svelte index 7ffd487..c29915b 100644 --- a/packages/frontend/src/lib/components/ui/button/button.svelte +++ b/packages/frontend/src/lib/components/ui/button/button.svelte @@ -1,86 +1,80 @@ {#if href} -
- {@render children?.()} - + + {@render children?.()} + {:else} - + {/if} diff --git a/packages/frontend/src/lib/components/ui/button/index.ts b/packages/frontend/src/lib/components/ui/button/index.ts index fb585d7..872d97c 100644 --- a/packages/frontend/src/lib/components/ui/button/index.ts +++ b/packages/frontend/src/lib/components/ui/button/index.ts @@ -1,17 +1,17 @@ import Root, { - type ButtonProps, - type ButtonSize, - type ButtonVariant, - buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants, } from "./button.svelte"; export { - Root, - type ButtonProps as Props, - // - Root as Button, - buttonVariants, - type ButtonProps, - type ButtonSize, - type ButtonVariant, + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant, }; diff --git a/packages/frontend/src/lib/components/ui/card/card-action.svelte b/packages/frontend/src/lib/components/ui/card/card-action.svelte index 1679859..aea2eb9 100644 --- a/packages/frontend/src/lib/components/ui/card/card-action.svelte +++ b/packages/frontend/src/lib/components/ui/card/card-action.svelte @@ -1,20 +1,20 @@
- {@render children?.()} + {@render children?.()}
diff --git a/packages/frontend/src/lib/components/ui/card/card-content.svelte b/packages/frontend/src/lib/components/ui/card/card-content.svelte index bfa440b..c55a8d4 100644 --- a/packages/frontend/src/lib/components/ui/card/card-content.svelte +++ b/packages/frontend/src/lib/components/ui/card/card-content.svelte @@ -1,15 +1,15 @@
- {@render children?.()} + {@render children?.()}
diff --git a/packages/frontend/src/lib/components/ui/card/card-description.svelte b/packages/frontend/src/lib/components/ui/card/card-description.svelte index 8d36345..4ad41e5 100644 --- a/packages/frontend/src/lib/components/ui/card/card-description.svelte +++ b/packages/frontend/src/lib/components/ui/card/card-description.svelte @@ -1,20 +1,20 @@

- {@render children?.()} + {@render children?.()}

diff --git a/packages/frontend/src/lib/components/ui/card/card-footer.svelte b/packages/frontend/src/lib/components/ui/card/card-footer.svelte index bcfd3c2..6ff6f57 100644 --- a/packages/frontend/src/lib/components/ui/card/card-footer.svelte +++ b/packages/frontend/src/lib/components/ui/card/card-footer.svelte @@ -1,20 +1,20 @@
- {@render children?.()} + {@render children?.()}
diff --git a/packages/frontend/src/lib/components/ui/card/card-header.svelte b/packages/frontend/src/lib/components/ui/card/card-header.svelte index bc92139..30a4bed 100644 --- a/packages/frontend/src/lib/components/ui/card/card-header.svelte +++ b/packages/frontend/src/lib/components/ui/card/card-header.svelte @@ -1,23 +1,23 @@
- {@render children?.()} + {@render children?.()}
diff --git a/packages/frontend/src/lib/components/ui/card/card-title.svelte b/packages/frontend/src/lib/components/ui/card/card-title.svelte index 9e12de9..594ff33 100644 --- a/packages/frontend/src/lib/components/ui/card/card-title.svelte +++ b/packages/frontend/src/lib/components/ui/card/card-title.svelte @@ -1,20 +1,20 @@
- {@render children?.()} + {@render children?.()}
diff --git a/packages/frontend/src/lib/components/ui/card/card.svelte b/packages/frontend/src/lib/components/ui/card/card.svelte index 70c61a8..4ec39e9 100644 --- a/packages/frontend/src/lib/components/ui/card/card.svelte +++ b/packages/frontend/src/lib/components/ui/card/card.svelte @@ -1,23 +1,23 @@
- {@render children?.()} + {@render children?.()}
diff --git a/packages/frontend/src/lib/components/ui/card/index.ts b/packages/frontend/src/lib/components/ui/card/index.ts index 4d3fce4..406a5ce 100644 --- a/packages/frontend/src/lib/components/ui/card/index.ts +++ b/packages/frontend/src/lib/components/ui/card/index.ts @@ -7,19 +7,19 @@ import Title from "./card-title.svelte"; import Action from "./card-action.svelte"; export { - Root, - Content, - Description, - Footer, - Header, - Title, - Action, - // - Root as Card, - Content as CardContent, - Description as CardDescription, - Footer as CardFooter, - Header as CardHeader, - Title as CardTitle, - Action as CardAction, + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction, }; diff --git a/packages/frontend/src/lib/components/ui/checkbox/checkbox.svelte b/packages/frontend/src/lib/components/ui/checkbox/checkbox.svelte index fbda558..1e4a615 100644 --- a/packages/frontend/src/lib/components/ui/checkbox/checkbox.svelte +++ b/packages/frontend/src/lib/components/ui/checkbox/checkbox.svelte @@ -1,36 +1,36 @@ - {#snippet children({ checked, indeterminate })} -
- {#if checked} - - {:else if indeterminate} - - {/if} -
- {/snippet} + {#snippet children({ checked, indeterminate })} +
+ {#if checked} + + {:else if indeterminate} + + {/if} +
+ {/snippet}
diff --git a/packages/frontend/src/lib/components/ui/checkbox/index.ts b/packages/frontend/src/lib/components/ui/checkbox/index.ts index 6d92d94..d1b2485 100644 --- a/packages/frontend/src/lib/components/ui/checkbox/index.ts +++ b/packages/frontend/src/lib/components/ui/checkbox/index.ts @@ -1,6 +1,6 @@ import Root from "./checkbox.svelte"; export { - Root, - // - Root as Checkbox, + Root, + // + Root as Checkbox, }; diff --git a/packages/frontend/src/lib/components/ui/dialog/dialog-close.svelte b/packages/frontend/src/lib/components/ui/dialog/dialog-close.svelte index 091092a..7bcb438 100644 --- a/packages/frontend/src/lib/components/ui/dialog/dialog-close.svelte +++ b/packages/frontend/src/lib/components/ui/dialog/dialog-close.svelte @@ -1,8 +1,7 @@ diff --git a/packages/frontend/src/lib/components/ui/dialog/dialog-content.svelte b/packages/frontend/src/lib/components/ui/dialog/dialog-content.svelte index 50329af..408dfb0 100644 --- a/packages/frontend/src/lib/components/ui/dialog/dialog-content.svelte +++ b/packages/frontend/src/lib/components/ui/dialog/dialog-content.svelte @@ -1,43 +1,43 @@ - - - {@render children?.()} - {#if showCloseButton} - - - Close - - {/if} - + + + {@render children?.()} + {#if showCloseButton} + + + Close + + {/if} + diff --git a/packages/frontend/src/lib/components/ui/dialog/dialog-description.svelte b/packages/frontend/src/lib/components/ui/dialog/dialog-description.svelte index aa6a0c6..5162cd3 100644 --- a/packages/frontend/src/lib/components/ui/dialog/dialog-description.svelte +++ b/packages/frontend/src/lib/components/ui/dialog/dialog-description.svelte @@ -1,17 +1,17 @@ diff --git a/packages/frontend/src/lib/components/ui/dialog/dialog-footer.svelte b/packages/frontend/src/lib/components/ui/dialog/dialog-footer.svelte index cddd47a..fdb2ebf 100644 --- a/packages/frontend/src/lib/components/ui/dialog/dialog-footer.svelte +++ b/packages/frontend/src/lib/components/ui/dialog/dialog-footer.svelte @@ -1,20 +1,20 @@
- {@render children?.()} + {@render children?.()}
diff --git a/packages/frontend/src/lib/components/ui/dialog/dialog-header.svelte b/packages/frontend/src/lib/components/ui/dialog/dialog-header.svelte index e74280e..be7438c 100644 --- a/packages/frontend/src/lib/components/ui/dialog/dialog-header.svelte +++ b/packages/frontend/src/lib/components/ui/dialog/dialog-header.svelte @@ -1,20 +1,20 @@
- {@render children?.()} + {@render children?.()}
diff --git a/packages/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte b/packages/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte index 704b2db..21730ac 100644 --- a/packages/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte +++ b/packages/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -1,20 +1,20 @@ diff --git a/packages/frontend/src/lib/components/ui/dialog/dialog-title.svelte b/packages/frontend/src/lib/components/ui/dialog/dialog-title.svelte index fd66ac3..4f59bc4 100644 --- a/packages/frontend/src/lib/components/ui/dialog/dialog-title.svelte +++ b/packages/frontend/src/lib/components/ui/dialog/dialog-title.svelte @@ -1,17 +1,17 @@ diff --git a/packages/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte b/packages/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte index d7fa2de..92a8e3c 100644 --- a/packages/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte +++ b/packages/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -1,8 +1,7 @@ diff --git a/packages/frontend/src/lib/components/ui/dialog/index.ts b/packages/frontend/src/lib/components/ui/dialog/index.ts index dce1d9d..07515e7 100644 --- a/packages/frontend/src/lib/components/ui/dialog/index.ts +++ b/packages/frontend/src/lib/components/ui/dialog/index.ts @@ -13,25 +13,25 @@ const Root = DialogPrimitive.Root; const Portal = DialogPrimitive.Portal; export { - Root, - Title, - Portal, - Footer, - Header, - Trigger, - Overlay, - Content, - Description, - Close, - // - Root as Dialog, - Title as DialogTitle, - Portal as DialogPortal, - Footer as DialogFooter, - Header as DialogHeader, - Trigger as DialogTrigger, - Overlay as DialogOverlay, - Content as DialogContent, - Description as DialogDescription, - Close as DialogClose, + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose, }; diff --git a/packages/frontend/src/lib/components/ui/file-drop-zone/file-drop-zone.svelte b/packages/frontend/src/lib/components/ui/file-drop-zone/file-drop-zone.svelte index f230b7c..e1efcb7 100644 --- a/packages/frontend/src/lib/components/ui/file-drop-zone/file-drop-zone.svelte +++ b/packages/frontend/src/lib/components/ui/file-drop-zone/file-drop-zone.svelte @@ -3,183 +3,174 @@ --> diff --git a/packages/frontend/src/lib/components/ui/file-drop-zone/index.ts b/packages/frontend/src/lib/components/ui/file-drop-zone/index.ts index 6673f9f..d2ecbf5 100644 --- a/packages/frontend/src/lib/components/ui/file-drop-zone/index.ts +++ b/packages/frontend/src/lib/components/ui/file-drop-zone/index.ts @@ -6,13 +6,13 @@ import FileDropZone from "./file-drop-zone.svelte"; import { type FileRejectedReason, type FileDropZoneProps } from "./types"; export const displaySize = (bytes: number): string => { - if (bytes < KILOBYTE) return `${bytes.toFixed(0)} B`; + if (bytes < KILOBYTE) return `${bytes.toFixed(0)} B`; - if (bytes < MEGABYTE) return `${(bytes / KILOBYTE).toFixed(0)} KB`; + if (bytes < MEGABYTE) return `${(bytes / KILOBYTE).toFixed(0)} KB`; - if (bytes < GIGABYTE) return `${(bytes / MEGABYTE).toFixed(0)} MB`; + if (bytes < GIGABYTE) return `${(bytes / MEGABYTE).toFixed(0)} MB`; - return `${(bytes / GIGABYTE).toFixed(0)} GB`; + return `${(bytes / GIGABYTE).toFixed(0)} GB`; }; // Utilities for working with file sizes diff --git a/packages/frontend/src/lib/components/ui/file-drop-zone/types.ts b/packages/frontend/src/lib/components/ui/file-drop-zone/types.ts index a48ebe6..9376a8e 100644 --- a/packages/frontend/src/lib/components/ui/file-drop-zone/types.ts +++ b/packages/frontend/src/lib/components/ui/file-drop-zone/types.ts @@ -6,46 +6,46 @@ import type { WithChildren } from "bits-ui"; import type { HTMLInputAttributes } from "svelte/elements"; export type FileRejectedReason = - | "Maximum file size exceeded" - | "File type not allowed" - | "Maximum files uploaded"; + | "Maximum file size exceeded" + | "File type not allowed" + | "Maximum files uploaded"; export type FileDropZonePropsWithoutHTML = WithChildren<{ - ref?: HTMLInputElement | null; - /** Called with the uploaded files when the user drops or clicks and selects their files. - * - * @param files - */ - onUpload: (files: File[]) => Promise; - /** The maximum amount files allowed to be uploaded */ - maxFiles?: number; - fileCount?: number; - /** The maximum size of a file in bytes */ - maxFileSize?: number; - /** Called when a file does not meet the upload criteria (size, or type) */ - onFileRejected?: (opts: { reason: FileRejectedReason; file: File }) => void; + ref?: HTMLInputElement | null; + /** Called with the uploaded files when the user drops or clicks and selects their files. + * + * @param files + */ + onUpload: (files: File[]) => Promise; + /** The maximum amount files allowed to be uploaded */ + maxFiles?: number; + fileCount?: number; + /** The maximum size of a file in bytes */ + maxFileSize?: number; + /** Called when a file does not meet the upload criteria (size, or type) */ + onFileRejected?: (opts: { reason: FileRejectedReason; file: File }) => void; - // just for extra documentation - /** Takes a comma separated list of one or more file types. - * - * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) - * - * ### Usage - * ```svelte - * - * ``` - * - * ### Common Values - * ```svelte - * - * - * - * ``` - */ - accept?: string; + // just for extra documentation + /** Takes a comma separated list of one or more file types. + * + * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) + * + * ### Usage + * ```svelte + * + * ``` + * + * ### Common Values + * ```svelte + * + * + * + * ``` + */ + accept?: string; }>; export type FileDropZoneProps = FileDropZonePropsWithoutHTML & - Omit; + Omit; diff --git a/packages/frontend/src/lib/components/ui/input/index.ts b/packages/frontend/src/lib/components/ui/input/index.ts index f47b6d3..ceb4b16 100644 --- a/packages/frontend/src/lib/components/ui/input/index.ts +++ b/packages/frontend/src/lib/components/ui/input/index.ts @@ -1,7 +1,7 @@ import Root from "./input.svelte"; export { - Root, - // - Root as Input, + Root, + // + Root as Input, }; diff --git a/packages/frontend/src/lib/components/ui/input/input.svelte b/packages/frontend/src/lib/components/ui/input/input.svelte index 99078c0..6fbc6ef 100644 --- a/packages/frontend/src/lib/components/ui/input/input.svelte +++ b/packages/frontend/src/lib/components/ui/input/input.svelte @@ -1,57 +1,51 @@ {#if type === "file"} - + {:else} - + {/if} diff --git a/packages/frontend/src/lib/components/ui/label/index.ts b/packages/frontend/src/lib/components/ui/label/index.ts index 8bfca0b..b0b23ce 100644 --- a/packages/frontend/src/lib/components/ui/label/index.ts +++ b/packages/frontend/src/lib/components/ui/label/index.ts @@ -1,7 +1,7 @@ import Root from "./label.svelte"; export { - Root, - // - Root as Label, + Root, + // + Root as Label, }; diff --git a/packages/frontend/src/lib/components/ui/label/label.svelte b/packages/frontend/src/lib/components/ui/label/label.svelte index bbed2bd..131612c 100644 --- a/packages/frontend/src/lib/components/ui/label/label.svelte +++ b/packages/frontend/src/lib/components/ui/label/label.svelte @@ -1,20 +1,20 @@ diff --git a/packages/frontend/src/lib/components/ui/select/index.ts b/packages/frontend/src/lib/components/ui/select/index.ts index 9e8d3e9..0c0a77e 100644 --- a/packages/frontend/src/lib/components/ui/select/index.ts +++ b/packages/frontend/src/lib/components/ui/select/index.ts @@ -13,25 +13,25 @@ import GroupHeading from "./select-group-heading.svelte"; const Root = SelectPrimitive.Root; export { - Root, - Group, - Label, - Item, - Content, - Trigger, - Separator, - ScrollDownButton, - ScrollUpButton, - GroupHeading, - // - Root as Select, - Group as SelectGroup, - Label as SelectLabel, - Item as SelectItem, - Content as SelectContent, - Trigger as SelectTrigger, - Separator as SelectSeparator, - ScrollDownButton as SelectScrollDownButton, - ScrollUpButton as SelectScrollUpButton, - GroupHeading as SelectGroupHeading, + Root, + Group, + Label, + Item, + Content, + Trigger, + Separator, + ScrollDownButton, + ScrollUpButton, + GroupHeading, + // + Root as Select, + Group as SelectGroup, + Label as SelectLabel, + Item as SelectItem, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator, + ScrollDownButton as SelectScrollDownButton, + ScrollUpButton as SelectScrollUpButton, + GroupHeading as SelectGroupHeading, }; diff --git a/packages/frontend/src/lib/components/ui/select/select-content.svelte b/packages/frontend/src/lib/components/ui/select/select-content.svelte index 2c4aa57..4b276f4 100644 --- a/packages/frontend/src/lib/components/ui/select/select-content.svelte +++ b/packages/frontend/src/lib/components/ui/select/select-content.svelte @@ -1,40 +1,40 @@ - + + - - - {@render children?.()} - - - + {@render children?.()} + + + diff --git a/packages/frontend/src/lib/components/ui/select/select-group-heading.svelte b/packages/frontend/src/lib/components/ui/select/select-group-heading.svelte index 3963427..debb7ff 100644 --- a/packages/frontend/src/lib/components/ui/select/select-group-heading.svelte +++ b/packages/frontend/src/lib/components/ui/select/select-group-heading.svelte @@ -1,21 +1,21 @@ - {@render children?.()} + {@render children?.()} diff --git a/packages/frontend/src/lib/components/ui/select/select-group.svelte b/packages/frontend/src/lib/components/ui/select/select-group.svelte index d90bab8..f3a7a79 100644 --- a/packages/frontend/src/lib/components/ui/select/select-group.svelte +++ b/packages/frontend/src/lib/components/ui/select/select-group.svelte @@ -1,9 +1,8 @@ diff --git a/packages/frontend/src/lib/components/ui/select/select-item.svelte b/packages/frontend/src/lib/components/ui/select/select-item.svelte index d9e1006..3b4a7d8 100644 --- a/packages/frontend/src/lib/components/ui/select/select-item.svelte +++ b/packages/frontend/src/lib/components/ui/select/select-item.svelte @@ -1,38 +1,38 @@ - {#snippet children({ selected, highlighted })} - - {#if selected} - - {/if} - - {#if childrenProp} - {@render childrenProp({ selected, highlighted })} - {:else} - {label || value} - {/if} - {/snippet} + {#snippet children({ selected, highlighted })} + + {#if selected} + + {/if} + + {#if childrenProp} + {@render childrenProp({ selected, highlighted })} + {:else} + {label || value} + {/if} + {/snippet} diff --git a/packages/frontend/src/lib/components/ui/select/select-label.svelte b/packages/frontend/src/lib/components/ui/select/select-label.svelte index 4527e3c..241e7a6 100644 --- a/packages/frontend/src/lib/components/ui/select/select-label.svelte +++ b/packages/frontend/src/lib/components/ui/select/select-label.svelte @@ -1,20 +1,20 @@
- {@render children?.()} + {@render children?.()}
diff --git a/packages/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte b/packages/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte index 0d6d446..c77fa6c 100644 --- a/packages/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte +++ b/packages/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -1,20 +1,20 @@ - + diff --git a/packages/frontend/src/lib/components/ui/select/select-scroll-up-button.svelte b/packages/frontend/src/lib/components/ui/select/select-scroll-up-button.svelte index ffac42f..1aac94a 100644 --- a/packages/frontend/src/lib/components/ui/select/select-scroll-up-button.svelte +++ b/packages/frontend/src/lib/components/ui/select/select-scroll-up-button.svelte @@ -1,20 +1,20 @@ - + diff --git a/packages/frontend/src/lib/components/ui/select/select-separator.svelte b/packages/frontend/src/lib/components/ui/select/select-separator.svelte index 4baa845..6319194 100644 --- a/packages/frontend/src/lib/components/ui/select/select-separator.svelte +++ b/packages/frontend/src/lib/components/ui/select/select-separator.svelte @@ -1,18 +1,18 @@ diff --git a/packages/frontend/src/lib/components/ui/select/select-trigger.svelte b/packages/frontend/src/lib/components/ui/select/select-trigger.svelte index 2819e58..7393eb0 100644 --- a/packages/frontend/src/lib/components/ui/select/select-trigger.svelte +++ b/packages/frontend/src/lib/components/ui/select/select-trigger.svelte @@ -1,29 +1,29 @@ - {@render children?.()} - + {@render children?.()} + diff --git a/packages/frontend/src/lib/components/ui/separator/index.ts b/packages/frontend/src/lib/components/ui/separator/index.ts index 82442d2..d66644e 100644 --- a/packages/frontend/src/lib/components/ui/separator/index.ts +++ b/packages/frontend/src/lib/components/ui/separator/index.ts @@ -1,7 +1,7 @@ import Root from "./separator.svelte"; export { - Root, - // - Root as Separator, + Root, + // + Root as Separator, }; diff --git a/packages/frontend/src/lib/components/ui/separator/separator.svelte b/packages/frontend/src/lib/components/ui/separator/separator.svelte index 6beeca0..683c53d 100644 --- a/packages/frontend/src/lib/components/ui/separator/separator.svelte +++ b/packages/frontend/src/lib/components/ui/separator/separator.svelte @@ -1,20 +1,20 @@ diff --git a/packages/frontend/src/lib/components/ui/slider/index.ts b/packages/frontend/src/lib/components/ui/slider/index.ts index 820f209..b3c90f9 100644 --- a/packages/frontend/src/lib/components/ui/slider/index.ts +++ b/packages/frontend/src/lib/components/ui/slider/index.ts @@ -1,7 +1,7 @@ import Root from "./slider.svelte"; export { - Root, - // - Root as Slider, + Root, + // + Root as Slider, }; diff --git a/packages/frontend/src/lib/components/ui/slider/slider.svelte b/packages/frontend/src/lib/components/ui/slider/slider.svelte index 2c0e9a4..7ca28dc 100644 --- a/packages/frontend/src/lib/components/ui/slider/slider.svelte +++ b/packages/frontend/src/lib/components/ui/slider/slider.svelte @@ -1,14 +1,14 @@ - {#snippet children({ thumbs })} - - - - {#each thumbs as thumb (thumb)} - - {/each} - {/snippet} + {#snippet children({ thumbs })} + + + + {#each thumbs as thumb (thumb)} + + {/each} + {/snippet} diff --git a/packages/frontend/src/lib/components/ui/sonner/sonner.svelte b/packages/frontend/src/lib/components/ui/sonner/sonner.svelte index 61c9e52..fea32fc 100644 --- a/packages/frontend/src/lib/components/ui/sonner/sonner.svelte +++ b/packages/frontend/src/lib/components/ui/sonner/sonner.svelte @@ -1,16 +1,13 @@ diff --git a/packages/frontend/src/lib/components/ui/tabs/index.ts b/packages/frontend/src/lib/components/ui/tabs/index.ts index 12d4327..4c728b6 100644 --- a/packages/frontend/src/lib/components/ui/tabs/index.ts +++ b/packages/frontend/src/lib/components/ui/tabs/index.ts @@ -4,13 +4,13 @@ import List from "./tabs-list.svelte"; import Trigger from "./tabs-trigger.svelte"; export { - Root, - Content, - List, - Trigger, - // - Root as Tabs, - Content as TabsContent, - List as TabsList, - Trigger as TabsTrigger, + Root, + Content, + List, + Trigger, + // + Root as Tabs, + Content as TabsContent, + List as TabsList, + Trigger as TabsTrigger, }; diff --git a/packages/frontend/src/lib/components/ui/tabs/tabs-content.svelte b/packages/frontend/src/lib/components/ui/tabs/tabs-content.svelte index b78abe7..c313e09 100644 --- a/packages/frontend/src/lib/components/ui/tabs/tabs-content.svelte +++ b/packages/frontend/src/lib/components/ui/tabs/tabs-content.svelte @@ -1,17 +1,17 @@ diff --git a/packages/frontend/src/lib/components/ui/tabs/tabs-list.svelte b/packages/frontend/src/lib/components/ui/tabs/tabs-list.svelte index 139811f..82d79a2 100644 --- a/packages/frontend/src/lib/components/ui/tabs/tabs-list.svelte +++ b/packages/frontend/src/lib/components/ui/tabs/tabs-list.svelte @@ -1,20 +1,16 @@ diff --git a/packages/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte b/packages/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte index 75b871a..a87b676 100644 --- a/packages/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte +++ b/packages/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -1,20 +1,20 @@ diff --git a/packages/frontend/src/lib/components/ui/tabs/tabs.svelte b/packages/frontend/src/lib/components/ui/tabs/tabs.svelte index 38a48f0..cdea1c7 100644 --- a/packages/frontend/src/lib/components/ui/tabs/tabs.svelte +++ b/packages/frontend/src/lib/components/ui/tabs/tabs.svelte @@ -1,19 +1,19 @@ diff --git a/packages/frontend/src/lib/components/ui/tags-input/tags-input-tag.svelte b/packages/frontend/src/lib/components/ui/tags-input/tags-input-tag.svelte index 60f01e9..68099cd 100644 --- a/packages/frontend/src/lib/components/ui/tags-input/tags-input-tag.svelte +++ b/packages/frontend/src/lib/components/ui/tags-input/tags-input-tag.svelte @@ -3,26 +3,26 @@ -->
- - {value} - - + + {value} + +
diff --git a/packages/frontend/src/lib/components/ui/tags-input/tags-input.svelte b/packages/frontend/src/lib/components/ui/tags-input/tags-input.svelte index c35a5ef..5e27496 100644 --- a/packages/frontend/src/lib/components/ui/tags-input/tags-input.svelte +++ b/packages/frontend/src/lib/components/ui/tags-input/tags-input.svelte @@ -3,209 +3,208 @@ -->
- {#each value as tag, i (tag)} - - {/each} - + {#each value as tag, i (tag)} + + {/each} +
diff --git a/packages/frontend/src/lib/components/ui/tags-input/types.ts b/packages/frontend/src/lib/components/ui/tags-input/types.ts index 4c95fb4..3fae9a2 100644 --- a/packages/frontend/src/lib/components/ui/tags-input/types.ts +++ b/packages/frontend/src/lib/components/ui/tags-input/types.ts @@ -5,9 +5,8 @@ import type { HTMLInputAttributes } from "svelte/elements"; export type TagsInputPropsWithoutHTML = { - value?: string[]; - validate?: (val: string, tags: string[]) => string | undefined; + value?: string[]; + validate?: (val: string, tags: string[]) => string | undefined; }; -export type TagsInputProps = TagsInputPropsWithoutHTML & - Omit; +export type TagsInputProps = TagsInputPropsWithoutHTML & Omit; diff --git a/packages/frontend/src/lib/components/ui/textarea/index.ts b/packages/frontend/src/lib/components/ui/textarea/index.ts index ace797a..c14b903 100644 --- a/packages/frontend/src/lib/components/ui/textarea/index.ts +++ b/packages/frontend/src/lib/components/ui/textarea/index.ts @@ -1,7 +1,7 @@ import Root from "./textarea.svelte"; export { - Root, - // - Root as Textarea, + Root, + // + Root as Textarea, }; diff --git a/packages/frontend/src/lib/components/ui/textarea/textarea.svelte b/packages/frontend/src/lib/components/ui/textarea/textarea.svelte index 4b0c73f..169faec 100644 --- a/packages/frontend/src/lib/components/ui/textarea/textarea.svelte +++ b/packages/frontend/src/lib/components/ui/textarea/textarea.svelte @@ -1,22 +1,22 @@ diff --git a/packages/frontend/src/lib/directus.ts b/packages/frontend/src/lib/directus.ts index 3d5fe32..2fb387a 100644 --- a/packages/frontend/src/lib/directus.ts +++ b/packages/frontend/src/lib/directus.ts @@ -1,3 +1,8 @@ // Re-export from api.ts for backwards compatibility // All components that import from $lib/directus continue to work -export { apiUrl as directusApiUrl, getAssetUrl, isModel, getGraphQLClient as getDirectusInstance } from "./api.js"; +export { + apiUrl as directusApiUrl, + getAssetUrl, + isModel, + getGraphQLClient as getDirectusInstance, +} from "./api.js"; diff --git a/packages/frontend/src/lib/i18n/index.ts b/packages/frontend/src/lib/i18n/index.ts index 0931b99..d132e23 100644 --- a/packages/frontend/src/lib/i18n/index.ts +++ b/packages/frontend/src/lib/i18n/index.ts @@ -6,6 +6,6 @@ const defaultLocale = "en"; addMessages("en", en); init({ - fallbackLocale: defaultLocale, - initialLocale: defaultLocale, + fallbackLocale: defaultLocale, + initialLocale: defaultLocale, }); diff --git a/packages/frontend/src/lib/i18n/locales/en.ts b/packages/frontend/src/lib/i18n/locales/en.ts index e7ea133..fa96e97 100644 --- a/packages/frontend/src/lib/i18n/locales/en.ts +++ b/packages/frontend/src/lib/i18n/locales/en.ts @@ -1,925 +1,921 @@ export default { - common: { - loading: "Loading...", - error: "Error", - success: "Success", - cancel: "Cancel", - save: "Save", - delete: "Delete", - edit: "Edit", - view: "View", - back: "Back", - next: "Next", - previous: "Previous", - search: "Search", - filter: "Filter", - sort: "Sort", - clear: "Clear", - submit: "Submit", - close: "Close", - open: "Open", - yes: "Yes", - no: "No", - my_profile: "My Profile", - anonymous: "Anonymous", - load_more: "Load More", - }, - header: { - home: "Home", - models: "Models", - videos: "Videos", - magazine: "Magazine", - about: "About", - login: "Log In", - login_hint: "Return to your passion", - signup: "Sign Up", - signup_hint: "Join now our community", - logout: "Log Out", - logout_hint: "Sign out of your account", - dashboard: "Dashboard", - dashboard_hint: "Your settings and more", - play: "Play", - play_hint: "Bring your toys", - profile: "Profile", - mailto: "sexy@pivoine.art", - x: "bordeaux1981", - youtube: "lovesting", - navigation: "Navigation", - account: "Account", - }, - brand: { - name: "SexyArt", - tagline: "Where Love Meets Artistry", - description: - "The premier destination for artistic adult content, intimate storytelling, and creative expression through video and magazine content.", - }, - home: { - hero: { - title: "Where Love Meets Artistry", - subtitle: "Artistry", - description: - "Experience the most intimate and beautiful love stories through our exclusive video content and magazine features.", - cta_videos: "Explore Videos", - cta_models: "Meet Our Models", - }, - featured_models: { - title: "Featured Models", - description: "Meet our most beloved creators", - rating: "rating", - videos: "videos", - view_profile: "View Profile", - join_community: "Join Our Community", - join_community_description: - "Become part of the most exclusive love and romance community. Access premium content, connect with models, and experience love like never before.", - }, - trending: { - title: "Trending Now", - description: "Most watched romantic content", - views: "views", - trending: "Latest", - }, - community: { - title: "Join Our Community", - description: - "Become part of the most exclusive love and romance community. Access premium content, connect with models, and experience love like never before.", - cta_join: "Start Your Journey", - cta_magazine: "Read Magazine", - }, - }, - me: { - title: "Dashboard", - welcome: "Welcome back, {name}", - view_profile: "View Public Profile", - settings: { - title: "Settings", - profile_title: "Profile Settings", - profile_subtitle: "Update your profile information", - avatar: "Avatar", - first_name: "First Name", - first_name_placeholder: "John", - artist_name: "Artist Name", - artist_name_placeholder: "Johnny", - description: "Description", - description_placeholder: "Your description", - tags: "Tags", - tags_placeholder: "Enter tags", - last_name: "Last Name", - last_name_placeholder: "Doe", - update_profile: "Update Profile", - updating_profile: "Updating Profile...", - toast_update: "Your settings have been updated!", - error: "Heads Up!", - privacy_title: "Privacy & Security", - privacy_subtitle: "Manage your account privacy and security settings", - update_security: "Update Security", - updating_security: "Updating Security...", - password_error: "The password has to match the confirmation password.", - email: "Email", - email_placeholder: "your@email.com", - password: "Password", - password_placeholder: "Create a strong password", - confirm_password: "Confirm Password", - confirm_password_placeholder: "Confirm your password", - }, - recordings: { - title: "Recordings", - description: "Manage your device recordings", - no_recordings: "You haven't created any recordings yet", - no_recordings_description: - "Start recording device patterns from the Play page to create interactive content", - go_to_play: "Go to Play", - loading: "Loading recordings...", - delete_confirm: "Are you sure you want to delete this recording?", - delete_success: "Recording deleted successfully", - delete_error: "Failed to delete recording", - }, - }, - recording_card: { - duration: "Duration", - events: "Events", - devices: "Devices", - created: "Created", - status_draft: "Draft", - status_published: "Published", - status_archived: "Archived", - play: "Play", - edit: "Edit", - delete: "Delete", - public: "Public", - private: "Private", - linked_video: "Linked to video", - }, - auth: { - login: { - title: "Sign In", - description: "Enter your credentials to access your account", - welcome: "Welcome back to your passion", - email: "Email", - email_placeholder: "your@email.com", - password: "Password", - password_placeholder: "Enter your password", - remember_me: "Remember me", - forgot_password: "Forgot password?", - signing_in: "Signing in...", - sign_in: "Sign In", - or_continue: "Or continue with", - google: "Google", - facebook: "Facebook", - no_account: "Don't have an account?", - sign_up_link: "Sign up now", - error: "Heads Up!", - }, - signup: { - title: "Create Account", - description: "Start your journey with us today", - welcome: "Join the most passionate community", - first_name: "First Name", - first_name_placeholder: "John", - last_name: "Last Name", - last_name_placeholder: "Doe", - email: "Email", - email_placeholder: "your@email.com", - account_type: "Account Type", - account_viewer: "Content Viewer", - account_creator: "Content Creator/Model", - password: "Password", - password_placeholder: "Create a strong password", - confirm_password: "Confirm Password", - confirm_password_placeholder: "Confirm your password", - terms_agreement: - "I agree to the {terms} and {privacy}. I confirm I am 18+ years old.", - terms_of_service: "Terms of Service", - privacy_policy: "Privacy Policy", - creating_account: "Creating account...", - create_account: "Create Account", - have_account: "Already have an account?", - sign_in_link: "Sign in here", - error: "Heads Up!", - agree_error: "You must confirm our terms of service and your age.", - password_error: "The password has to match the confirmation password.", - toast_register: "A verification email has been sent to {email}!", - toast_verify: "Your account has been activated!", - }, - password_request: { - title: "Password Request", - description: "Enter your email to reset your password", - welcome: "Return to your passion", - email: "Email", - email_placeholder: "your@email.com", - requesting: "Submitting...", - request: "Submit", - error: "Heads Up!", - toast_request: "A password reset email has been sent to {email}!", - }, - password_reset: { - title: "Password Reset", - description: "Enter your new password", - welcome: "Return to your passion now", - password: "Password", - password_placeholder: "Create a strong password", - confirm_password: "Confirm Password", - confirm_password_placeholder: "Confirm your password", - resetting: "Resetting...", - reset: "Reset", - error: "Heads Up!", - password_error: "The password has to match the confirmation password.", - toast_reset: "Your password has been reset!", - }, - }, - profile: { - member_since: "Member since {date}", - comments: "Comments", - likes: "Likes", - edit: "Edit Profile", - activity: "Activity", - }, - models: { - title: "Our Models", - description: - "Discover the most beautiful and talented creators sharing their passion and artistry.", - search_placeholder: "Search models...", - categories: { - all: "All Categories", - romantic: "Romantic", - artistic: "Artistic", - intimate: "Intimate", - }, - sort: { - popular: "Most Popular", - rating: "Highest Rated", - videos: "Most Videos", - name: "A-Z", - }, - online: "Online", - followers: "followers", - view_profile: "View Profile", - follow: "Follow", - no_results: "No models found matching your criteria.", - clear_filters: "Clear Filters", - back: "Back to Models", - joined: "Joined {join_date}", - comments: "Comments", - videos: "Videos", - photos: "Photos", - }, - videos: { - title: "Your Videos", - description: - "Explore our curated collection of intimate and artistic video content", - search_placeholder: "Search videos or models...", - categories: { - all: "All Categories", - romantic: "Romantic", - artistic: "Artistic", - intimate: "Intimate", - performance: "Performance", - }, - duration: { - all: "Any Duration", - short: "Short (< 10min)", - medium: "Medium (10-20min)", - long: "Long (20min+)", - }, - sort: { - trending: "Trending", - recent: "Most Recent", - popular: "Most Liked", - most_liked: "Most Liked", - most_played: "Most Played", - duration: "By Duration", - name: "A-Z", - }, - premium: "Premium", - views: "views", - watch: "Watch", - no_results: "No videos found matching your criteria.", - clear_filters: "Clear Filters", - comments: "Comments ({comments})", - hide: "Hide", - show: "Show", - add_comment_placeholder: "Add a comment...", - toast_comment: "Your comment has been sent", - comment: "Comment", - commenting: "Commenting...", - error: "Heads Up!", - back: "Back to Videos", - }, - magazine: { - title: "SexyArt Magazine", - description: - "Insights, stories, and inspiration from the world of love, art, and intimate expression", - search_placeholder: "Search articles...", - categories: { - all: "All Categories", - photography: "Photography", - production: "Production", - interview: "Interviews", - psychology: "Psychology", - trends: "Trends", - spotlight: "Spotlight", - }, - sort: { - recent: "Most Recent", - popular: "Most Popular", - featured: "Featured First", - name: "A-Z", - }, - featured: "Featured", - read_time: "{time} min read", - read_article: "Read Article", - no_results: "No articles found matching your criteria.", - clear_filters: "Clear Filters", - back: "Back to Magazine", - }, - tags: { - title: "{tag}", - description: 'Items tagged "{tag}".', - search_placeholder: "Search items...", - categories: { - all: "All Types", - video: "Video", - article: "Article", - model: "Model", - }, - view: "View {category}", - no_results: "No items found matching your criteria.", - clear_filters: "Clear Filters", - }, - dashboard: { - title: "Creator Dashboard", - welcome: "Welcome back, {name}", - view_profile: "View Public Profile", - tabs: { - overview: "Overview", - content: "Content", - upload: "Upload", - settings: "Settings", - }, - stats: { - total_views: "Total Views", - total_likes: "Total Likes", - subscribers: "Subscribers", - earnings: "Earnings", - }, - upload: { - title: "Upload New Content", - description: "Share your latest creations with your audience", - content_type: "Content Type", - video: "Video", - photo: "Photo", - drop_files: "Drop your {type} here or click to browse", - file_types: { - video: "MP4, MOV up to 2GB", - photo: "JPG, PNG up to 10MB", - }, - choose_file: "Choose File", - title_label: "Title", - title_placeholder: "Enter content title", - category: "Category", - description_label: "Description", - description_placeholder: "Describe your content...", - upload_content: "Upload Content", - }, - }, - about: { - title: "About SexyArt", - subtitle: - "Where passion meets artistry, and intimate storytelling becomes a celebration of human connection.", - join_community: "Join Our Community", - stats: { - members: "Active Members", - videos: "Premium Videos", - models: "Featured Models", - experience: "Industry Experience", - yearsFormatted: "{years} years", - }, - story: { - title: "Our Story", - subtitle: - "Born from a vision to transform how intimate content is created, shared, and appreciated", - description_part1: - "SexyArt was founded in 2019 with a simple yet powerful mission: to create a platform where intimate content could be appreciated as an art form, where creators could express their authentic selves, and where viewers could connect with content that celebrates love, passion, and human connection.", - description_part2: - "We recognized that the adult content industry needed a platform that prioritized artistic expression, creator empowerment, and community building. Our founders, coming from backgrounds in photography, digital media, and community management, set out to build something different.", - description_part3: - "Today, SexyArt is home to hundreds of talented creators and thousands of passionate community members who share our vision of elevating intimate content to new artistic heights.", - }, - values: { - title: "Our Values", - subtitle: - "The principles that guide everything we do and shape our community", - authentic_expression: { - title: "Authentic Expression", - description: - "We believe in celebrating genuine love, intimacy, and human connection through artistic expression.", - }, - safety_respect: { - title: "Safety & Respect", - description: - "Creating a secure environment where creators and viewers can explore content with confidence and respect.", - }, - artistic_excellence: { - title: "Artistic Excellence", - description: - "Promoting high-quality, artistic content that elevates intimate storytelling to an art form.", - }, - community_first: { - title: "Community First", - description: - "Building meaningful connections between creators and their audience through shared passion and appreciation.", - }, - }, - team: { - title: "Meet Our Team", - sebastian: { - name: "Sebastian Krüger", - role: "Founder & CEO", - image: "/img/sebastian.jpg", - bio: "Visionary leader with 15+ years in digital media and content creation.", - }, - valknar: { - name: "Valknar", - role: "Creative Director", - image: "/img/valknar.gif", - bio: "DJ and visual storyteller specializing in diffusion AI art.", - }, - subtitle: "The passionate individuals behind SexyArt's success", - }, - mission: { - title: "Our Mission", - description: - "To create the world's most respectful, artistic, and empowering platform for intimate content, where creators can thrive and audiences can discover meaningful connections through the art of love.", - cta_creator: "Become a Creator", - cta_community: "Join Our Community", - }, - contact: { - title: "Get in Touch", - description: - "Have questions about our platform or interested in partnering with us? We'd love to hear from you.", - general: { - title: "General Inquiries", - description: "Questions about our platform or services", - mailto: "sexy@pivoine.art", - }, - creators: { - title: "Creator Support", - description: "Support for our content creators", - mailto: "support@pivoine.art", - }, - }, - }, - faq: { - title: "Frequently Asked Questions", - description: - "Find answers to common questions about SexyArt, our platform, and services", - search_placeholder: "Search frequently asked questions...", - search_results: "Search Results ({count})", - no_results: "No questions found matching your search.", - clear_search: "Clear Search", - getting_started: { - title: "Getting Started", - questions: [ - { - question: "How do I create an account on SexyArt?", - answer: - "Creating an account is simple! Click the 'Join Now' button in the top navigation, fill out the registration form with your email and basic information, verify you're 18+, and agree to our terms. You'll receive a confirmation email to activate your account.", - }, - { - question: "What types of content can I find on SexyArt?", - answer: - "SexyArt features high-quality artistic adult content including intimate photography, romantic videos, artistic nude content, and creative adult entertainment. All content is created by verified models and creators who focus on artistic expression and storytelling.", - }, - { - question: "Is SexyArt safe and secure?", - answer: - "Yes! We use industry-standard encryption, secure payment processing, and strict privacy measures. All creators are verified, and we have comprehensive content moderation. Your personal information and viewing habits are kept completely private.", - }, - { - question: "Can I access SexyArt on mobile devices?", - answer: - "Absolutely! SexyArt is fully responsive and works perfectly on smartphones, tablets, and desktop computers. You can enjoy the same high-quality experience across all your devices.", - }, - ], - }, - creators: { - title: "For Creators & Models", - questions: [ - { - question: "How do I become a creator on SexyArt?", - answer: - "To become a creator, sign up for a Creator account during registration or upgrade your existing account. You'll need to verify your identity, provide tax information, and agree to our creator terms. Once approved, you can start uploading content and building your audience.", - }, - { - question: "How much can I earn as a creator?", - answer: - "Creator earnings vary based on content quality, audience engagement, and marketing efforts. Our creators typically earn between $500-$10,000+ per month. We offer competitive revenue sharing, with creators keeping 70-80% of their earnings after platform fees.", - }, - { - question: "What content guidelines do I need to follow?", - answer: - "All content must feature consenting adults 18+, be original or properly licensed, and comply with our community standards. We prohibit violent, non-consensual, or illegal content. Focus on artistic, creative, and high-quality productions for best results.", - }, - { - question: "How do I promote my content and gain followers?", - answer: - "Use engaging titles and descriptions, post consistently, interact with your audience through comments and messages, collaborate with other creators, and utilize our promotional tools. High-quality content and authentic engagement are key to building a loyal fanbase.", - }, - ], - }, - payments: { - title: "Payments & Subscriptions", - }, - privacy: { - title: "Privacy & Safety", - questions: [ - { - question: "How do you protect my privacy?", - answer: - "We use advanced encryption, never share personal information with third parties, offer anonymous browsing options, and allow you to control your privacy settings. Your viewing history and personal data are kept strictly confidential.", - }, - { - question: "Can I block or report inappropriate content?", - answer: - "Yes! Every piece of content has report and block options. Our moderation team reviews all reports within 24 hours. You can also block specific creators or users to customize your experience.", - }, - { - question: "How do you verify creator identities?", - answer: - "All creators must provide government-issued ID, proof of age (18+), and complete identity verification. We also require signed model releases for all content featuring multiple people. This ensures all content is legal and consensual.", - }, - { - question: "What if I forget my password?", - answer: - "Click 'Forgot Password' on the login page, enter your email address, and we'll send you a secure reset link. For additional security, you can enable two-factor authentication in your account settings.", - }, - ], - }, - technical: { - title: "Technical Support", - questions: [ - { - question: "Why is my video not loading?", - answer: - "Video issues are usually related to internet connection or browser settings. Try refreshing the page, clearing your browser cache, or switching to a different browser. For persistent issues, check our system status page or contact support.", - }, - { - question: "How do I update my account information?", - answer: - "Go to Account Settings from your profile menu. You can update your email, password, payment methods, and privacy preferences. Some changes may require email verification for security.", - }, - { - question: "Can I download content for offline viewing?", - answer: - "Premium subscribers can download select content for offline viewing within our mobile app. Downloaded content expires after 30 days and cannot be shared or transferred to other devices.", - }, - { - question: "How do I contact customer support?", - answer: - "You can reach our support team via email at support@sexyart.com, through the live chat feature (available 24/7), or by submitting a ticket through your account dashboard. We typically respond within 2-4 hours.", - }, - ], - }, - support: { - title: "Still Need Help?", - description: - "Can't find the answer you're looking for? Our support team is here to help you 24/7.", - contact: "Contact Support", - contact_email: "support@pivoine.art", - live_chat: "Live Chat", - }, - }, - imprint: { - title: "Imprint", - description: "Legal information and company details", - company_information: "Company Information", - company_name: { - title: "Company Name", - value: "SexyArt", - }, - legal_form: { - title: "Legal Form", - value: "-", - }, - registration_number: { - title: "Registration Number", - value: "-", - }, - tax_id: { - title: "Registration Tax ID", - value: "-", - }, - contact_information: "Contact Information", - registered_address: "Registered Address", - address: { - company: "SexyArt", - name: "Sebastian Krüger", - street: "Berlingerstraße 48", - city: "78333 Stockach", - country: "Germany", - }, - phone: { - title: "Phone", - value: "+49 (174) 8188918", - }, - email: { - title: "Email", - value: "admin@pivoine.art", - }, - website: { - title: "Website", - value: "pivoine.art", - }, - disclaimer: "Disclaimer", - disclaimer_text: [ - "The information contained on this website is for general information purposes only. While we endeavor to keep the information up to date and correct, we make no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability, or availability with respect to the website or the information, products, services, or related graphics contained on the website for any purpose.", - "This website contains adult content and is intended for mature audiences only. By accessing this site, you confirm that you are at least 18 years of age and that you are legally allowed to view such content in your jurisdiction.", - "All content on this platform is created by consenting adults and is protected by copyright laws. Unauthorized reproduction or distribution of any content is strictly prohibited.", - ], - last_updated: "Last updated: September 5, 2025", - }, - legal: { - title: "Legal Information", - description: "Our commitment to transparency, privacy, and user rights", - privacy: { - title: "Privacy Policy", - last_updated: "Last updated: September 5, 2025", - information: { - title: "1. Information We Collect", - text: [ - "Personal Information: When you create an account, we collect information such as your email address, username, and profile information you choose to provide.", - "Content Information: We collect information about the content you upload, view, and interact with on our platform.", - ], - }, - information_use: { - title: "2. How We Use Your Information", - subtitle: "We use your information to:", - text: [ - "
  • Provide and improve our services
  • Communicate with you about your account and our services
  • Personalize your experience on our platform
  • Ensure platform security and prevent fraud
  • Comply with legal obligations
  • ", - ], - }, - information_sharing: { - title: "3. Information Sharing", - subtitle: - "We do not sell your personal information. We may share your information in the following circumstances:", - text: [ - "
  • With your consent
  • With service providers who help us operate our platform
  • To comply with legal requirements
  • To protect our rights and the safety of our users
  • In connection with a business transaction
  • ", - ], - }, - security: { - title: "4. Data Security", - text: [ - "We implement appropriate technical and organizational measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction. This includes encryption, secure servers, and regular security audits.", - ], - }, - rights: { - title: "5. Your Rights", - subtitle: "You have the right to:", - text: [ - "
  • Access your personal information
  • Correct inaccurate information
  • Delete your account and personal information
  • Object to processing of your information
  • Data portability
  • Withdraw consent at any time
  • ", - ], - }, - }, - terms: { - title: "Terms of Service", - last_updated: "Last updated: September 5, 2025", - acceptance: { - title: "1. Acceptance of Terms", - text: [ - "By accessing and using SexyArt, you accept and agree to be bound by the terms and provision of this agreement. If you do not agree to abide by the above, please do not use this service.", - ], - }, - age: { - title: "2. Age Restriction", - text: [ - "You must be at least 18 years old to use this service. By using our platform, you represent and warrant that you are at least 18 years of age and have the legal capacity to enter into this agreement.", - ], - }, - accounts: { - title: "3. User Accounts", - subtitle: "When creating an account, you agree to:", - text: [ - "
  • Provide accurate and complete information
  • Maintain the security of your account credentials
  • Accept responsibility for all activities under your account
  • Notify us immediately of any unauthorized use
  • ", - ], - }, - content: { - title: "4. Content Guidelines", - subtitle: - "All content must comply with our community guidelines. Prohibited content includes:", - text: [ - "
  • Content involving minors
  • Non-consensual content
  • Violent or harmful content
  • Copyrighted material without permission
  • Spam or misleading content
  • ", - ], - }, - payment: { - title: "5. Payment Terms", - subtitle: "For paid services:", - text: [ - "
  • All payments are processed securely through third-party providers
  • Subscriptions renew automatically unless cancelled
  • Refunds are subject to our refund policy
  • Prices may change with 30 days notice
  • ", - ], - }, - termination: { - title: "6. Termination", - text: [ - "We reserve the right to terminate or suspend your account at any time for violations of these terms. You may also terminate your account at any time through your account settings.", - ], - }, - }, - community: { - title: "Community Guidelines", - description: "Creating a safe and respectful environment for all", - values: { - title: "Our Community Values", - text: [ - "SexyArt is built on respect, consent, and artistic expression. We believe in creating a space where creators and viewers can connect through shared appreciation for intimate art and storytelling.", - ], - }, - respect: { - title: "Respect and Consent", - text: [ - "
  • All content must be created with full consent of all participants
  • Respect creators' boundaries and content preferences
  • No harassment, bullying, or discriminatory behavior
  • Respect privacy and do not share personal information
  • ", - ], - }, - standards: { - title: "Content Standards", - text: [ - "
  • Content should celebrate love, intimacy, and human connection
  • Artistic and creative expression is encouraged
  • Content must comply with all applicable laws
  • No violent, degrading, or harmful content
  • ", - ], - }, - interaction: { - title: "Community Interaction", - text: [ - "
  • Engage respectfully in comments and messages
  • Support creators through positive feedback
  • Report inappropriate content or behavior
  • Help maintain a welcoming environment for all
  • ", - ], - }, - enforcement: { - title: "Enforcement", - text: [ - "Violations of our community guidelines may result in content removal, account suspension, or permanent ban. We review all reports and take appropriate action to maintain a safe environment.", - ], - }, - }, - cookie: { - title: "Cookie Policy", - description: "How we use cookies and similar technologies", - what: { - title: "What Are Cookies", - text: [ - "Cookies are small text files that are stored on your device when you visit our website. They help us provide you with a better experience by remembering your preferences and improving our services.", - ], - }, - types: { - title: "Types of Cookies We Use", - essential: { - title: "Essential Cookies", - text: [ - "These cookies are necessary for the website to function properly. They enable basic features like page navigation and access to secure areas.", - ], - }, - }, - managing: { - title: "Managing Cookies", - subtitle: "You can control cookies through:", - text: [ - "
  • Your browser settings
  • Third-party opt-out tools
  • ", - "Please note that disabling certain cookies may affect the functionality of our website.", - ], - }, - third_party: { - title: "Third-Party Cookies", - text: [ - "We may use third-party services that set their own cookies. These include analytics providers, payment processors, and content delivery networks. Please refer to their respective privacy policies for more information.", - ], - }, - }, - questions: "Questions About Our Legal Policies?", - questions_description: - "If you have any questions about our legal policies, please don't hesitate to contact us.", - questions_email: "support@pivoine.art", - }, - play: { - title: "SexyPlay", - description: "Bring your toys.", - scan: "Start Scan", - scanning: "Scanning...", - no_results: "No Devices founds", - }, - error: { - not_found: "Oops! Page Not Found", - common: "Oops! An Error Occured", - description: - "The page you're looking for seems to have vanished into the digital void. Don't worry, even in the world of love and art, sometimes we lose our way.", - go_home: "Go Home", - explore_videos: "Explore Videos", - quick_links: "Or try one of these popular sections:", - featured_models: "Featured Models", - magazine: "Magazine", - about_us: "About Us", - }, - footer: { - description: - "The premier destination for artistic adult content, intimate storytelling, and creative expression through video and magazine content.", - quick_links: "Quick Links", - models: "Models", - videos: "Videos", - magazine: "Magazine", - about: "About", - support: "Support", - contact_support: "Contact Support", - contact_support_email: "support@pivoine.art", - model_applications: "Model Applications", - model_applications_email: "sexy@pivoine.art", - contact: { - email: "sexy@pivoine.art", - x: "bordeaux1981", - youtube: "lovesting", - }, - faq: "FAQ", - legal: "Legal", - privacy_policy: "Privacy Policy", - terms_of_service: "Terms of Service", - imprint: "Imprint", - copyright: "© 2025 Valknar. All rights reserved. | 18+ Content Warning", - }, - sharing_popup: { - title: "Share Content", - description: "Choose how you'd like to share this {type}", - subtitle: "Share your content", - share: { - x: "Share on X (Twitter)", - facebook: "Share on Facebook", - email: "Share via Email", - whatsapp: "Share on WhatsApp", - telegram: "Share on Telegram", - copy: "Copy Link to Clipboard", - }, - success: { - x: "Opened X (Twitter) sharing window", - facebook: "Opened Facebook sharing window", - email: "Opened email client", - whatsapp: "Opened WhatsApp sharing", - telegram: "Opened Telegram sharing", - copy: "Copied link to clipboard", - }, - close: "Close", - }, - age_verification_dialog: { - title: "Age Verification", - description: - 'By clicking "Confirm", you verify that you are 18 years or older.', - age: "18+", - confirm: "Confirm", - exit: "Exit", - exit_url: "https://pivoine.art", - }, - sharing_popup_button: { - share: "Share", - }, - image_viewer: { - index: "Image {index} of {size}", - previous: "Previous", - next: "Next", - close: "Close", - download: "Download", - }, - device_card: { - active: "Active", - paused: "Paused", - current_value: "Current Value", - battery: "Battery", - last_seen: "Last seen", - connect: "Connect", - disconnect: "Disconnect", - actuator_types: { - unknown: "Unknown", - vibrate: "Vibrate", - rotate: "Rotate", - oscillate: "Oscillate", - constrict: "Constrict", - inflate: "Inflate", - position: "Position", - }, - }, - head: { - title: "SexyArt | {title}", - }, - gamification: { - leaderboard: "Leaderboard", - leaderboard_description: "Compete with other creators and players for the top spot", - leaderboard_subtitle: "Top creators and players ranked by activity points", - top_players: "Top Players", - no_rankings_yet: "No rankings yet. Be the first to earn points!", - points: "Points", - recordings: "Recordings", - plays: "Plays", - achievements: "Achievements", - rank: "Rank", - stats: "Stats", - how_it_works: "How It Works", - how_it_works_description: "Points are awarded for creating recordings, playing others' recordings, and engaging with the community. Rankings use time-weighted scoring to keep things dynamic.", - earn_by_creating: "Create Recordings", - earn_by_creating_desc: "Earn 50 points per published recording", - earn_by_playing: "Play & Complete", - earn_by_playing_desc: "Earn 10 points per play, 5 for completion", - stay_active: "Stay Active", - stay_active_desc: "Recent activity counts more toward your rank", - }, + common: { + loading: "Loading...", + error: "Error", + success: "Success", + cancel: "Cancel", + save: "Save", + delete: "Delete", + edit: "Edit", + view: "View", + back: "Back", + next: "Next", + previous: "Previous", + search: "Search", + filter: "Filter", + sort: "Sort", + clear: "Clear", + submit: "Submit", + close: "Close", + open: "Open", + yes: "Yes", + no: "No", + my_profile: "My Profile", + anonymous: "Anonymous", + load_more: "Load More", + }, + header: { + home: "Home", + models: "Models", + videos: "Videos", + magazine: "Magazine", + about: "About", + login: "Log In", + login_hint: "Return to your passion", + signup: "Sign Up", + signup_hint: "Join now our community", + logout: "Log Out", + logout_hint: "Sign out of your account", + dashboard: "Dashboard", + dashboard_hint: "Your settings and more", + play: "Play", + play_hint: "Bring your toys", + profile: "Profile", + mailto: "sexy@pivoine.art", + x: "bordeaux1981", + youtube: "lovesting", + navigation: "Navigation", + account: "Account", + }, + brand: { + name: "SexyArt", + tagline: "Where Love Meets Artistry", + description: + "The premier destination for artistic adult content, intimate storytelling, and creative expression through video and magazine content.", + }, + home: { + hero: { + title: "Where Love Meets Artistry", + subtitle: "Artistry", + description: + "Experience the most intimate and beautiful love stories through our exclusive video content and magazine features.", + cta_videos: "Explore Videos", + cta_models: "Meet Our Models", + }, + featured_models: { + title: "Featured Models", + description: "Meet our most beloved creators", + rating: "rating", + videos: "videos", + view_profile: "View Profile", + join_community: "Join Our Community", + join_community_description: + "Become part of the most exclusive love and romance community. Access premium content, connect with models, and experience love like never before.", + }, + trending: { + title: "Trending Now", + description: "Most watched romantic content", + views: "views", + trending: "Latest", + }, + community: { + title: "Join Our Community", + description: + "Become part of the most exclusive love and romance community. Access premium content, connect with models, and experience love like never before.", + cta_join: "Start Your Journey", + cta_magazine: "Read Magazine", + }, + }, + me: { + title: "Dashboard", + welcome: "Welcome back, {name}", + view_profile: "View Public Profile", + settings: { + title: "Settings", + profile_title: "Profile Settings", + profile_subtitle: "Update your profile information", + avatar: "Avatar", + first_name: "First Name", + first_name_placeholder: "John", + artist_name: "Artist Name", + artist_name_placeholder: "Johnny", + description: "Description", + description_placeholder: "Your description", + tags: "Tags", + tags_placeholder: "Enter tags", + last_name: "Last Name", + last_name_placeholder: "Doe", + update_profile: "Update Profile", + updating_profile: "Updating Profile...", + toast_update: "Your settings have been updated!", + error: "Heads Up!", + privacy_title: "Privacy & Security", + privacy_subtitle: "Manage your account privacy and security settings", + update_security: "Update Security", + updating_security: "Updating Security...", + password_error: "The password has to match the confirmation password.", + email: "Email", + email_placeholder: "your@email.com", + password: "Password", + password_placeholder: "Create a strong password", + confirm_password: "Confirm Password", + confirm_password_placeholder: "Confirm your password", + }, + recordings: { + title: "Recordings", + description: "Manage your device recordings", + no_recordings: "You haven't created any recordings yet", + no_recordings_description: + "Start recording device patterns from the Play page to create interactive content", + go_to_play: "Go to Play", + loading: "Loading recordings...", + delete_confirm: "Are you sure you want to delete this recording?", + delete_success: "Recording deleted successfully", + delete_error: "Failed to delete recording", + }, + }, + recording_card: { + duration: "Duration", + events: "Events", + devices: "Devices", + created: "Created", + status_draft: "Draft", + status_published: "Published", + status_archived: "Archived", + play: "Play", + edit: "Edit", + delete: "Delete", + public: "Public", + private: "Private", + linked_video: "Linked to video", + }, + auth: { + login: { + title: "Sign In", + description: "Enter your credentials to access your account", + welcome: "Welcome back to your passion", + email: "Email", + email_placeholder: "your@email.com", + password: "Password", + password_placeholder: "Enter your password", + remember_me: "Remember me", + forgot_password: "Forgot password?", + signing_in: "Signing in...", + sign_in: "Sign In", + or_continue: "Or continue with", + google: "Google", + facebook: "Facebook", + no_account: "Don't have an account?", + sign_up_link: "Sign up now", + error: "Heads Up!", + }, + signup: { + title: "Create Account", + description: "Start your journey with us today", + welcome: "Join the most passionate community", + first_name: "First Name", + first_name_placeholder: "John", + last_name: "Last Name", + last_name_placeholder: "Doe", + email: "Email", + email_placeholder: "your@email.com", + account_type: "Account Type", + account_viewer: "Content Viewer", + account_creator: "Content Creator/Model", + password: "Password", + password_placeholder: "Create a strong password", + confirm_password: "Confirm Password", + confirm_password_placeholder: "Confirm your password", + terms_agreement: "I agree to the {terms} and {privacy}. I confirm I am 18+ years old.", + terms_of_service: "Terms of Service", + privacy_policy: "Privacy Policy", + creating_account: "Creating account...", + create_account: "Create Account", + have_account: "Already have an account?", + sign_in_link: "Sign in here", + error: "Heads Up!", + agree_error: "You must confirm our terms of service and your age.", + password_error: "The password has to match the confirmation password.", + toast_register: "A verification email has been sent to {email}!", + toast_verify: "Your account has been activated!", + }, + password_request: { + title: "Password Request", + description: "Enter your email to reset your password", + welcome: "Return to your passion", + email: "Email", + email_placeholder: "your@email.com", + requesting: "Submitting...", + request: "Submit", + error: "Heads Up!", + toast_request: "A password reset email has been sent to {email}!", + }, + password_reset: { + title: "Password Reset", + description: "Enter your new password", + welcome: "Return to your passion now", + password: "Password", + password_placeholder: "Create a strong password", + confirm_password: "Confirm Password", + confirm_password_placeholder: "Confirm your password", + resetting: "Resetting...", + reset: "Reset", + error: "Heads Up!", + password_error: "The password has to match the confirmation password.", + toast_reset: "Your password has been reset!", + }, + }, + profile: { + member_since: "Member since {date}", + comments: "Comments", + likes: "Likes", + edit: "Edit Profile", + activity: "Activity", + }, + models: { + title: "Our Models", + description: + "Discover the most beautiful and talented creators sharing their passion and artistry.", + search_placeholder: "Search models...", + categories: { + all: "All Categories", + romantic: "Romantic", + artistic: "Artistic", + intimate: "Intimate", + }, + sort: { + popular: "Most Popular", + rating: "Highest Rated", + videos: "Most Videos", + name: "A-Z", + }, + online: "Online", + followers: "followers", + view_profile: "View Profile", + follow: "Follow", + no_results: "No models found matching your criteria.", + clear_filters: "Clear Filters", + back: "Back to Models", + joined: "Joined {join_date}", + comments: "Comments", + videos: "Videos", + photos: "Photos", + }, + videos: { + title: "Your Videos", + description: "Explore our curated collection of intimate and artistic video content", + search_placeholder: "Search videos or models...", + categories: { + all: "All Categories", + romantic: "Romantic", + artistic: "Artistic", + intimate: "Intimate", + performance: "Performance", + }, + duration: { + all: "Any Duration", + short: "Short (< 10min)", + medium: "Medium (10-20min)", + long: "Long (20min+)", + }, + sort: { + trending: "Trending", + recent: "Most Recent", + popular: "Most Liked", + most_liked: "Most Liked", + most_played: "Most Played", + duration: "By Duration", + name: "A-Z", + }, + premium: "Premium", + views: "views", + watch: "Watch", + no_results: "No videos found matching your criteria.", + clear_filters: "Clear Filters", + comments: "Comments ({comments})", + hide: "Hide", + show: "Show", + add_comment_placeholder: "Add a comment...", + toast_comment: "Your comment has been sent", + comment: "Comment", + commenting: "Commenting...", + error: "Heads Up!", + back: "Back to Videos", + }, + magazine: { + title: "SexyArt Magazine", + description: + "Insights, stories, and inspiration from the world of love, art, and intimate expression", + search_placeholder: "Search articles...", + categories: { + all: "All Categories", + photography: "Photography", + production: "Production", + interview: "Interviews", + psychology: "Psychology", + trends: "Trends", + spotlight: "Spotlight", + }, + sort: { + recent: "Most Recent", + popular: "Most Popular", + featured: "Featured First", + name: "A-Z", + }, + featured: "Featured", + read_time: "{time} min read", + read_article: "Read Article", + no_results: "No articles found matching your criteria.", + clear_filters: "Clear Filters", + back: "Back to Magazine", + }, + tags: { + title: "{tag}", + description: 'Items tagged "{tag}".', + search_placeholder: "Search items...", + categories: { + all: "All Types", + video: "Video", + article: "Article", + model: "Model", + }, + view: "View {category}", + no_results: "No items found matching your criteria.", + clear_filters: "Clear Filters", + }, + dashboard: { + title: "Creator Dashboard", + welcome: "Welcome back, {name}", + view_profile: "View Public Profile", + tabs: { + overview: "Overview", + content: "Content", + upload: "Upload", + settings: "Settings", + }, + stats: { + total_views: "Total Views", + total_likes: "Total Likes", + subscribers: "Subscribers", + earnings: "Earnings", + }, + upload: { + title: "Upload New Content", + description: "Share your latest creations with your audience", + content_type: "Content Type", + video: "Video", + photo: "Photo", + drop_files: "Drop your {type} here or click to browse", + file_types: { + video: "MP4, MOV up to 2GB", + photo: "JPG, PNG up to 10MB", + }, + choose_file: "Choose File", + title_label: "Title", + title_placeholder: "Enter content title", + category: "Category", + description_label: "Description", + description_placeholder: "Describe your content...", + upload_content: "Upload Content", + }, + }, + about: { + title: "About SexyArt", + subtitle: + "Where passion meets artistry, and intimate storytelling becomes a celebration of human connection.", + join_community: "Join Our Community", + stats: { + members: "Active Members", + videos: "Premium Videos", + models: "Featured Models", + experience: "Industry Experience", + yearsFormatted: "{years} years", + }, + story: { + title: "Our Story", + subtitle: + "Born from a vision to transform how intimate content is created, shared, and appreciated", + description_part1: + "SexyArt was founded in 2019 with a simple yet powerful mission: to create a platform where intimate content could be appreciated as an art form, where creators could express their authentic selves, and where viewers could connect with content that celebrates love, passion, and human connection.", + description_part2: + "We recognized that the adult content industry needed a platform that prioritized artistic expression, creator empowerment, and community building. Our founders, coming from backgrounds in photography, digital media, and community management, set out to build something different.", + description_part3: + "Today, SexyArt is home to hundreds of talented creators and thousands of passionate community members who share our vision of elevating intimate content to new artistic heights.", + }, + values: { + title: "Our Values", + subtitle: "The principles that guide everything we do and shape our community", + authentic_expression: { + title: "Authentic Expression", + description: + "We believe in celebrating genuine love, intimacy, and human connection through artistic expression.", + }, + safety_respect: { + title: "Safety & Respect", + description: + "Creating a secure environment where creators and viewers can explore content with confidence and respect.", + }, + artistic_excellence: { + title: "Artistic Excellence", + description: + "Promoting high-quality, artistic content that elevates intimate storytelling to an art form.", + }, + community_first: { + title: "Community First", + description: + "Building meaningful connections between creators and their audience through shared passion and appreciation.", + }, + }, + team: { + title: "Meet Our Team", + sebastian: { + name: "Sebastian Krüger", + role: "Founder & CEO", + image: "/img/sebastian.jpg", + bio: "Visionary leader with 15+ years in digital media and content creation.", + }, + valknar: { + name: "Valknar", + role: "Creative Director", + image: "/img/valknar.gif", + bio: "DJ and visual storyteller specializing in diffusion AI art.", + }, + subtitle: "The passionate individuals behind SexyArt's success", + }, + mission: { + title: "Our Mission", + description: + "To create the world's most respectful, artistic, and empowering platform for intimate content, where creators can thrive and audiences can discover meaningful connections through the art of love.", + cta_creator: "Become a Creator", + cta_community: "Join Our Community", + }, + contact: { + title: "Get in Touch", + description: + "Have questions about our platform or interested in partnering with us? We'd love to hear from you.", + general: { + title: "General Inquiries", + description: "Questions about our platform or services", + mailto: "sexy@pivoine.art", + }, + creators: { + title: "Creator Support", + description: "Support for our content creators", + mailto: "support@pivoine.art", + }, + }, + }, + faq: { + title: "Frequently Asked Questions", + description: "Find answers to common questions about SexyArt, our platform, and services", + search_placeholder: "Search frequently asked questions...", + search_results: "Search Results ({count})", + no_results: "No questions found matching your search.", + clear_search: "Clear Search", + getting_started: { + title: "Getting Started", + questions: [ + { + question: "How do I create an account on SexyArt?", + answer: + "Creating an account is simple! Click the 'Join Now' button in the top navigation, fill out the registration form with your email and basic information, verify you're 18+, and agree to our terms. You'll receive a confirmation email to activate your account.", + }, + { + question: "What types of content can I find on SexyArt?", + answer: + "SexyArt features high-quality artistic adult content including intimate photography, romantic videos, artistic nude content, and creative adult entertainment. All content is created by verified models and creators who focus on artistic expression and storytelling.", + }, + { + question: "Is SexyArt safe and secure?", + answer: + "Yes! We use industry-standard encryption, secure payment processing, and strict privacy measures. All creators are verified, and we have comprehensive content moderation. Your personal information and viewing habits are kept completely private.", + }, + { + question: "Can I access SexyArt on mobile devices?", + answer: + "Absolutely! SexyArt is fully responsive and works perfectly on smartphones, tablets, and desktop computers. You can enjoy the same high-quality experience across all your devices.", + }, + ], + }, + creators: { + title: "For Creators & Models", + questions: [ + { + question: "How do I become a creator on SexyArt?", + answer: + "To become a creator, sign up for a Creator account during registration or upgrade your existing account. You'll need to verify your identity, provide tax information, and agree to our creator terms. Once approved, you can start uploading content and building your audience.", + }, + { + question: "How much can I earn as a creator?", + answer: + "Creator earnings vary based on content quality, audience engagement, and marketing efforts. Our creators typically earn between $500-$10,000+ per month. We offer competitive revenue sharing, with creators keeping 70-80% of their earnings after platform fees.", + }, + { + question: "What content guidelines do I need to follow?", + answer: + "All content must feature consenting adults 18+, be original or properly licensed, and comply with our community standards. We prohibit violent, non-consensual, or illegal content. Focus on artistic, creative, and high-quality productions for best results.", + }, + { + question: "How do I promote my content and gain followers?", + answer: + "Use engaging titles and descriptions, post consistently, interact with your audience through comments and messages, collaborate with other creators, and utilize our promotional tools. High-quality content and authentic engagement are key to building a loyal fanbase.", + }, + ], + }, + payments: { + title: "Payments & Subscriptions", + }, + privacy: { + title: "Privacy & Safety", + questions: [ + { + question: "How do you protect my privacy?", + answer: + "We use advanced encryption, never share personal information with third parties, offer anonymous browsing options, and allow you to control your privacy settings. Your viewing history and personal data are kept strictly confidential.", + }, + { + question: "Can I block or report inappropriate content?", + answer: + "Yes! Every piece of content has report and block options. Our moderation team reviews all reports within 24 hours. You can also block specific creators or users to customize your experience.", + }, + { + question: "How do you verify creator identities?", + answer: + "All creators must provide government-issued ID, proof of age (18+), and complete identity verification. We also require signed model releases for all content featuring multiple people. This ensures all content is legal and consensual.", + }, + { + question: "What if I forget my password?", + answer: + "Click 'Forgot Password' on the login page, enter your email address, and we'll send you a secure reset link. For additional security, you can enable two-factor authentication in your account settings.", + }, + ], + }, + technical: { + title: "Technical Support", + questions: [ + { + question: "Why is my video not loading?", + answer: + "Video issues are usually related to internet connection or browser settings. Try refreshing the page, clearing your browser cache, or switching to a different browser. For persistent issues, check our system status page or contact support.", + }, + { + question: "How do I update my account information?", + answer: + "Go to Account Settings from your profile menu. You can update your email, password, payment methods, and privacy preferences. Some changes may require email verification for security.", + }, + { + question: "Can I download content for offline viewing?", + answer: + "Premium subscribers can download select content for offline viewing within our mobile app. Downloaded content expires after 30 days and cannot be shared or transferred to other devices.", + }, + { + question: "How do I contact customer support?", + answer: + "You can reach our support team via email at support@sexyart.com, through the live chat feature (available 24/7), or by submitting a ticket through your account dashboard. We typically respond within 2-4 hours.", + }, + ], + }, + support: { + title: "Still Need Help?", + description: + "Can't find the answer you're looking for? Our support team is here to help you 24/7.", + contact: "Contact Support", + contact_email: "support@pivoine.art", + live_chat: "Live Chat", + }, + }, + imprint: { + title: "Imprint", + description: "Legal information and company details", + company_information: "Company Information", + company_name: { + title: "Company Name", + value: "SexyArt", + }, + legal_form: { + title: "Legal Form", + value: "-", + }, + registration_number: { + title: "Registration Number", + value: "-", + }, + tax_id: { + title: "Registration Tax ID", + value: "-", + }, + contact_information: "Contact Information", + registered_address: "Registered Address", + address: { + company: "SexyArt", + name: "Sebastian Krüger", + street: "Berlingerstraße 48", + city: "78333 Stockach", + country: "Germany", + }, + phone: { + title: "Phone", + value: "+49 (174) 8188918", + }, + email: { + title: "Email", + value: "admin@pivoine.art", + }, + website: { + title: "Website", + value: "pivoine.art", + }, + disclaimer: "Disclaimer", + disclaimer_text: [ + "The information contained on this website is for general information purposes only. While we endeavor to keep the information up to date and correct, we make no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability, or availability with respect to the website or the information, products, services, or related graphics contained on the website for any purpose.", + "This website contains adult content and is intended for mature audiences only. By accessing this site, you confirm that you are at least 18 years of age and that you are legally allowed to view such content in your jurisdiction.", + "All content on this platform is created by consenting adults and is protected by copyright laws. Unauthorized reproduction or distribution of any content is strictly prohibited.", + ], + last_updated: "Last updated: September 5, 2025", + }, + legal: { + title: "Legal Information", + description: "Our commitment to transparency, privacy, and user rights", + privacy: { + title: "Privacy Policy", + last_updated: "Last updated: September 5, 2025", + information: { + title: "1. Information We Collect", + text: [ + "Personal Information: When you create an account, we collect information such as your email address, username, and profile information you choose to provide.", + "Content Information: We collect information about the content you upload, view, and interact with on our platform.", + ], + }, + information_use: { + title: "2. How We Use Your Information", + subtitle: "We use your information to:", + text: [ + "
  • Provide and improve our services
  • Communicate with you about your account and our services
  • Personalize your experience on our platform
  • Ensure platform security and prevent fraud
  • Comply with legal obligations
  • ", + ], + }, + information_sharing: { + title: "3. Information Sharing", + subtitle: + "We do not sell your personal information. We may share your information in the following circumstances:", + text: [ + "
  • With your consent
  • With service providers who help us operate our platform
  • To comply with legal requirements
  • To protect our rights and the safety of our users
  • In connection with a business transaction
  • ", + ], + }, + security: { + title: "4. Data Security", + text: [ + "We implement appropriate technical and organizational measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction. This includes encryption, secure servers, and regular security audits.", + ], + }, + rights: { + title: "5. Your Rights", + subtitle: "You have the right to:", + text: [ + "
  • Access your personal information
  • Correct inaccurate information
  • Delete your account and personal information
  • Object to processing of your information
  • Data portability
  • Withdraw consent at any time
  • ", + ], + }, + }, + terms: { + title: "Terms of Service", + last_updated: "Last updated: September 5, 2025", + acceptance: { + title: "1. Acceptance of Terms", + text: [ + "By accessing and using SexyArt, you accept and agree to be bound by the terms and provision of this agreement. If you do not agree to abide by the above, please do not use this service.", + ], + }, + age: { + title: "2. Age Restriction", + text: [ + "You must be at least 18 years old to use this service. By using our platform, you represent and warrant that you are at least 18 years of age and have the legal capacity to enter into this agreement.", + ], + }, + accounts: { + title: "3. User Accounts", + subtitle: "When creating an account, you agree to:", + text: [ + "
  • Provide accurate and complete information
  • Maintain the security of your account credentials
  • Accept responsibility for all activities under your account
  • Notify us immediately of any unauthorized use
  • ", + ], + }, + content: { + title: "4. Content Guidelines", + subtitle: + "All content must comply with our community guidelines. Prohibited content includes:", + text: [ + "
  • Content involving minors
  • Non-consensual content
  • Violent or harmful content
  • Copyrighted material without permission
  • Spam or misleading content
  • ", + ], + }, + payment: { + title: "5. Payment Terms", + subtitle: "For paid services:", + text: [ + "
  • All payments are processed securely through third-party providers
  • Subscriptions renew automatically unless cancelled
  • Refunds are subject to our refund policy
  • Prices may change with 30 days notice
  • ", + ], + }, + termination: { + title: "6. Termination", + text: [ + "We reserve the right to terminate or suspend your account at any time for violations of these terms. You may also terminate your account at any time through your account settings.", + ], + }, + }, + community: { + title: "Community Guidelines", + description: "Creating a safe and respectful environment for all", + values: { + title: "Our Community Values", + text: [ + "SexyArt is built on respect, consent, and artistic expression. We believe in creating a space where creators and viewers can connect through shared appreciation for intimate art and storytelling.", + ], + }, + respect: { + title: "Respect and Consent", + text: [ + "
  • All content must be created with full consent of all participants
  • Respect creators' boundaries and content preferences
  • No harassment, bullying, or discriminatory behavior
  • Respect privacy and do not share personal information
  • ", + ], + }, + standards: { + title: "Content Standards", + text: [ + "
  • Content should celebrate love, intimacy, and human connection
  • Artistic and creative expression is encouraged
  • Content must comply with all applicable laws
  • No violent, degrading, or harmful content
  • ", + ], + }, + interaction: { + title: "Community Interaction", + text: [ + "
  • Engage respectfully in comments and messages
  • Support creators through positive feedback
  • Report inappropriate content or behavior
  • Help maintain a welcoming environment for all
  • ", + ], + }, + enforcement: { + title: "Enforcement", + text: [ + "Violations of our community guidelines may result in content removal, account suspension, or permanent ban. We review all reports and take appropriate action to maintain a safe environment.", + ], + }, + }, + cookie: { + title: "Cookie Policy", + description: "How we use cookies and similar technologies", + what: { + title: "What Are Cookies", + text: [ + "Cookies are small text files that are stored on your device when you visit our website. They help us provide you with a better experience by remembering your preferences and improving our services.", + ], + }, + types: { + title: "Types of Cookies We Use", + essential: { + title: "Essential Cookies", + text: [ + "These cookies are necessary for the website to function properly. They enable basic features like page navigation and access to secure areas.", + ], + }, + }, + managing: { + title: "Managing Cookies", + subtitle: "You can control cookies through:", + text: [ + "
  • Your browser settings
  • Third-party opt-out tools
  • ", + "Please note that disabling certain cookies may affect the functionality of our website.", + ], + }, + third_party: { + title: "Third-Party Cookies", + text: [ + "We may use third-party services that set their own cookies. These include analytics providers, payment processors, and content delivery networks. Please refer to their respective privacy policies for more information.", + ], + }, + }, + questions: "Questions About Our Legal Policies?", + questions_description: + "If you have any questions about our legal policies, please don't hesitate to contact us.", + questions_email: "support@pivoine.art", + }, + play: { + title: "SexyPlay", + description: "Bring your toys.", + scan: "Start Scan", + scanning: "Scanning...", + no_results: "No Devices founds", + }, + error: { + not_found: "Oops! Page Not Found", + common: "Oops! An Error Occured", + description: + "The page you're looking for seems to have vanished into the digital void. Don't worry, even in the world of love and art, sometimes we lose our way.", + go_home: "Go Home", + explore_videos: "Explore Videos", + quick_links: "Or try one of these popular sections:", + featured_models: "Featured Models", + magazine: "Magazine", + about_us: "About Us", + }, + footer: { + description: + "The premier destination for artistic adult content, intimate storytelling, and creative expression through video and magazine content.", + quick_links: "Quick Links", + models: "Models", + videos: "Videos", + magazine: "Magazine", + about: "About", + support: "Support", + contact_support: "Contact Support", + contact_support_email: "support@pivoine.art", + model_applications: "Model Applications", + model_applications_email: "sexy@pivoine.art", + contact: { + email: "sexy@pivoine.art", + x: "bordeaux1981", + youtube: "lovesting", + }, + faq: "FAQ", + legal: "Legal", + privacy_policy: "Privacy Policy", + terms_of_service: "Terms of Service", + imprint: "Imprint", + copyright: "© 2025 Valknar. All rights reserved. | 18+ Content Warning", + }, + sharing_popup: { + title: "Share Content", + description: "Choose how you'd like to share this {type}", + subtitle: "Share your content", + share: { + x: "Share on X (Twitter)", + facebook: "Share on Facebook", + email: "Share via Email", + whatsapp: "Share on WhatsApp", + telegram: "Share on Telegram", + copy: "Copy Link to Clipboard", + }, + success: { + x: "Opened X (Twitter) sharing window", + facebook: "Opened Facebook sharing window", + email: "Opened email client", + whatsapp: "Opened WhatsApp sharing", + telegram: "Opened Telegram sharing", + copy: "Copied link to clipboard", + }, + close: "Close", + }, + age_verification_dialog: { + title: "Age Verification", + description: 'By clicking "Confirm", you verify that you are 18 years or older.', + age: "18+", + confirm: "Confirm", + exit: "Exit", + exit_url: "https://pivoine.art", + }, + sharing_popup_button: { + share: "Share", + }, + image_viewer: { + index: "Image {index} of {size}", + previous: "Previous", + next: "Next", + close: "Close", + download: "Download", + }, + device_card: { + active: "Active", + paused: "Paused", + current_value: "Current Value", + battery: "Battery", + last_seen: "Last seen", + connect: "Connect", + disconnect: "Disconnect", + actuator_types: { + unknown: "Unknown", + vibrate: "Vibrate", + rotate: "Rotate", + oscillate: "Oscillate", + constrict: "Constrict", + inflate: "Inflate", + position: "Position", + }, + }, + head: { + title: "SexyArt | {title}", + }, + gamification: { + leaderboard: "Leaderboard", + leaderboard_description: "Compete with other creators and players for the top spot", + leaderboard_subtitle: "Top creators and players ranked by activity points", + top_players: "Top Players", + no_rankings_yet: "No rankings yet. Be the first to earn points!", + points: "Points", + recordings: "Recordings", + plays: "Plays", + achievements: "Achievements", + rank: "Rank", + stats: "Stats", + how_it_works: "How It Works", + how_it_works_description: + "Points are awarded for creating recordings, playing others' recordings, and engaging with the community. Rankings use time-weighted scoring to keep things dynamic.", + earn_by_creating: "Create Recordings", + earn_by_creating_desc: "Earn 50 points per published recording", + earn_by_playing: "Play & Complete", + earn_by_playing_desc: "Earn 10 points per play, 5 for completion", + stay_active: "Stay Active", + stay_active_desc: "Recent activity counts more toward your rank", + }, }; diff --git a/packages/frontend/src/lib/logger.ts b/packages/frontend/src/lib/logger.ts index fb2c67b..0b0af1a 100644 --- a/packages/frontend/src/lib/logger.ts +++ b/packages/frontend/src/lib/logger.ts @@ -3,140 +3,137 @@ * Provides structured logging with context and request tracing */ -export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; +export type LogLevel = "debug" | "info" | "warn" | "error"; interface LogContext { - timestamp: string; - level: LogLevel; - message: string; - context?: Record; - requestId?: string; - userId?: string; - path?: string; - method?: string; - duration?: number; - error?: Error; + timestamp: string; + level: LogLevel; + message: string; + context?: Record; + requestId?: string; + userId?: string; + path?: string; + method?: string; + duration?: number; + error?: Error; } class Logger { - private isDev = process.env.NODE_ENV === 'development'; - private serviceName = 'sexy.pivoine.art'; + private isDev = process.env.NODE_ENV === "development"; + private serviceName = "sexy.pivoine.art"; - private formatLog(ctx: LogContext): string { - const { timestamp, level, message, context, requestId, userId, path, method, duration, error } = ctx; + private formatLog(ctx: LogContext): string { + const { timestamp, level, message, context, requestId, userId, path, method, duration, error } = + ctx; - const parts = [ - `[${timestamp}]`, - `[${level.toUpperCase()}]`, - requestId ? `[${requestId}]` : null, - method && path ? `${method} ${path}` : null, - message, - userId ? `user=${userId}` : null, - duration !== undefined ? `${duration}ms` : null, - ].filter(Boolean); + const parts = [ + `[${timestamp}]`, + `[${level.toUpperCase()}]`, + requestId ? `[${requestId}]` : null, + method && path ? `${method} ${path}` : null, + message, + userId ? `user=${userId}` : null, + duration !== undefined ? `${duration}ms` : null, + ].filter(Boolean); - let logString = parts.join(' '); + let logString = parts.join(" "); - if (context && Object.keys(context).length > 0) { - logString += ' ' + JSON.stringify(context); - } + if (context && Object.keys(context).length > 0) { + logString += " " + JSON.stringify(context); + } - if (error) { - logString += `\n Error: ${error.message}\n Stack: ${error.stack}`; - } + if (error) { + logString += `\n Error: ${error.message}\n Stack: ${error.stack}`; + } - return logString; - } + return logString; + } - private log(level: LogLevel, message: string, meta: Partial = {}) { - const timestamp = new Date().toISOString(); - const logContext: LogContext = { - timestamp, - level, - message, - ...meta, - }; + private log(level: LogLevel, message: string, meta: Partial = {}) { + const timestamp = new Date().toISOString(); + const logContext: LogContext = { + timestamp, + level, + message, + ...meta, + }; - const formattedLog = this.formatLog(logContext); + const formattedLog = this.formatLog(logContext); - switch (level) { - case 'debug': - if (this.isDev) console.debug(formattedLog); - break; - case 'info': - console.info(formattedLog); - break; - case 'warn': - console.warn(formattedLog); - break; - case 'error': - console.error(formattedLog); - break; - } - } + switch (level) { + case "debug": + if (this.isDev) console.debug(formattedLog); + break; + case "info": + console.info(formattedLog); + break; + case "warn": + console.warn(formattedLog); + break; + case "error": + console.error(formattedLog); + break; + } + } - debug(message: string, meta?: Partial) { - this.log('debug', message, meta); - } + debug(message: string, meta?: Partial) { + this.log("debug", message, meta); + } - info(message: string, meta?: Partial) { - this.log('info', message, meta); - } + info(message: string, meta?: Partial) { + this.log("info", message, meta); + } - warn(message: string, meta?: Partial) { - this.log('warn', message, meta); - } + warn(message: string, meta?: Partial) { + this.log("warn", message, meta); + } - error(message: string, meta?: Partial) { - this.log('error', message, meta); - } + error(message: string, meta?: Partial) { + this.log("error", message, meta); + } - // Request logging helper - request( - method: string, - path: string, - meta: Partial = {} - ) { - this.info('→ Request received', { method, path, ...meta }); - } + // Request logging helper + request(method: string, path: string, meta: Partial = {}) { + this.info("→ Request received", { method, path, ...meta }); + } - response( - method: string, - path: string, - status: number, - duration: number, - meta: Partial = {} - ) { - const level = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info'; - this.log(level, `← Response ${status}`, { method, path, duration, ...meta }); - } + response( + method: string, + path: string, + status: number, + duration: number, + meta: Partial = {}, + ) { + const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info"; + this.log(level, `← Response ${status}`, { method, path, duration, ...meta }); + } - // Authentication logging - auth(action: string, success: boolean, meta: Partial = {}) { - this.info(`🔐 Auth: ${action} ${success ? 'success' : 'failed'}`, meta); - } + // Authentication logging + auth(action: string, success: boolean, meta: Partial = {}) { + this.info(`🔐 Auth: ${action} ${success ? "success" : "failed"}`, meta); + } - // Startup logging - startup() { - const env = { - NODE_ENV: process.env.NODE_ENV, - PUBLIC_API_URL: process.env.PUBLIC_API_URL, - PUBLIC_URL: process.env.PUBLIC_URL, - PUBLIC_UMAMI_ID: process.env.PUBLIC_UMAMI_ID ? '***set***' : 'not set', - PUBLIC_UMAMI_SCRIPT: process.env.PUBLIC_UMAMI_SCRIPT || 'not set', - PORT: process.env.PORT || '3000', - HOST: process.env.HOST || '0.0.0.0', - }; + // Startup logging + startup() { + const env = { + NODE_ENV: process.env.NODE_ENV, + PUBLIC_API_URL: process.env.PUBLIC_API_URL, + PUBLIC_URL: process.env.PUBLIC_URL, + PUBLIC_UMAMI_ID: process.env.PUBLIC_UMAMI_ID ? "***set***" : "not set", + PUBLIC_UMAMI_SCRIPT: process.env.PUBLIC_UMAMI_SCRIPT || "not set", + PORT: process.env.PORT || "3000", + HOST: process.env.HOST || "0.0.0.0", + }; - console.log('\n' + '='.repeat(60)); - console.log('🍑 sexy.pivoine.art - Server Starting 💜'); - console.log('='.repeat(60)); - console.log('\n📋 Environment Configuration:'); - Object.entries(env).forEach(([key, value]) => { - console.log(` ${key}: ${value}`); - }); - console.log('\n' + '='.repeat(60) + '\n'); - } + console.log("\n" + "=".repeat(60)); + console.log("🍑 sexy.pivoine.art - Server Starting 💜"); + console.log("=".repeat(60)); + console.log("\n📋 Environment Configuration:"); + Object.entries(env).forEach(([key, value]) => { + console.log(` ${key}: ${value}`); + }); + console.log("\n" + "=".repeat(60) + "\n"); + } } // Singleton instance @@ -144,5 +141,5 @@ export const logger = new Logger(); // Generate request ID export function generateRequestId(): string { - return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } diff --git a/packages/frontend/src/lib/services.ts b/packages/frontend/src/lib/services.ts index bdf6a33..ecb0ee5 100644 --- a/packages/frontend/src/lib/services.ts +++ b/packages/frontend/src/lib/services.ts @@ -1,51 +1,51 @@ import { gql, GraphQLClient } from "graphql-request"; import { apiUrl, getGraphQLClient } from "$lib/api"; import type { - Analytics, - Article, - CurrentUser, - Model, - Recording, - Stats, - User, - Video, - VideoLikeStatus, - VideoLikeResponse, - VideoPlayResponse, + Analytics, + Article, + CurrentUser, + Model, + Recording, + Stats, + User, + Video, + VideoLikeStatus, + VideoLikeResponse, + VideoPlayResponse, } from "$lib/types"; import { logger } from "$lib/logger"; // Helper to log API calls async function loggedApiCall( - operationName: string, - operation: () => Promise, - context?: Record + operationName: string, + operation: () => Promise, + context?: Record, ): Promise { - const startTime = Date.now(); + const startTime = Date.now(); - try { - logger.debug(`🔄 API: ${operationName}`, { context }); - const result = await operation(); - const duration = Date.now() - startTime; - logger.info(`✅ API: ${operationName} succeeded`, { duration, context }); - return result; - } catch (error) { - const duration = Date.now() - startTime; - logger.error(`❌ API: ${operationName} failed`, { - duration, - context, - error: error instanceof Error ? error : new Error(String(error)), - }); - throw error; - } + try { + logger.debug(`🔄 API: ${operationName}`, { context }); + const result = await operation(); + const duration = Date.now() - startTime; + logger.info(`✅ API: ${operationName} succeeded`, { duration, context }); + return result; + } catch (error) { + const duration = Date.now() - startTime; + logger.error(`❌ API: ${operationName} failed`, { + duration, + context, + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } } // For server-side auth checks: forward cookie header manually function getAuthClient(token: string, fetchFn?: typeof globalThis.fetch) { - return new GraphQLClient(`${apiUrl}/graphql`, { - fetch: fetchFn || globalThis.fetch, - headers: { cookie: `session_token=${token}` }, - }); + return new GraphQLClient(`${apiUrl}/graphql`, { + fetch: fetchFn || globalThis.fetch, + headers: { cookie: `session_token=${token}` }, + }); } // ─── Auth ──────────────────────────────────────────────────────────────────── @@ -53,60 +53,86 @@ function getAuthClient(token: string, fetchFn?: typeof globalThis.fetch) { const ME_QUERY = gql` query Me { me { - id email first_name last_name artist_name slug description tags - role avatar banner email_verified date_created + id + email + first_name + last_name + artist_name + slug + description + tags + role + avatar + banner + email_verified + date_created } } `; export async function isAuthenticated(token: string, fetchFn?: typeof globalThis.fetch) { - return loggedApiCall( - "isAuthenticated", - async () => { - try { - const client = getAuthClient(token, fetchFn); - const data = await client.request<{ me: CurrentUser | null }>(ME_QUERY); - if (data.me) { - return { authenticated: true, user: data.me }; - } - return { authenticated: false }; - } catch { - return { authenticated: false }; - } - }, - { hasToken: !!token }, - ); + return loggedApiCall( + "isAuthenticated", + async () => { + try { + const client = getAuthClient(token, fetchFn); + const data = await client.request<{ me: CurrentUser | null }>(ME_QUERY); + if (data.me) { + return { authenticated: true, user: data.me }; + } + return { authenticated: false }; + } catch { + return { authenticated: false }; + } + }, + { hasToken: !!token }, + ); } const LOGIN_MUTATION = gql` mutation Login($email: String!, $password: String!) { login(email: $email, password: $password) { - id email first_name last_name artist_name slug description tags - role avatar banner email_verified date_created + id + email + first_name + last_name + artist_name + slug + description + tags + role + avatar + banner + email_verified + date_created } } `; export async function login(email: string, password: string) { - return loggedApiCall( - "login", - async () => { - const data = await getGraphQLClient().request<{ login: CurrentUser }>(LOGIN_MUTATION, { - email, - password, - }); - return data.login; - }, - { email }, - ); + return loggedApiCall( + "login", + async () => { + const data = await getGraphQLClient().request<{ login: CurrentUser }>(LOGIN_MUTATION, { + email, + password, + }); + return data.login; + }, + { email }, + ); } -const LOGOUT_MUTATION = gql`mutation Logout { logout }`; +const LOGOUT_MUTATION = gql` + mutation Logout { + logout + } +`; export async function logout() { - return loggedApiCall("logout", async () => { - await getGraphQLClient().request(LOGOUT_MUTATION); - }); + return loggedApiCall("logout", async () => { + await getGraphQLClient().request(LOGOUT_MUTATION); + }); } const REGISTER_MUTATION = gql` @@ -116,23 +142,23 @@ const REGISTER_MUTATION = gql` `; export async function register( - email: string, - password: string, - firstName: string, - lastName: string, + email: string, + password: string, + firstName: string, + lastName: string, ) { - return loggedApiCall( - "register", - async () => { - await getGraphQLClient().request(REGISTER_MUTATION, { - email, - password, - firstName, - lastName, - }); - }, - { email, firstName, lastName }, - ); + return loggedApiCall( + "register", + async () => { + await getGraphQLClient().request(REGISTER_MUTATION, { + email, + password, + firstName, + lastName, + }); + }, + { email, firstName, lastName }, + ); } const VERIFY_EMAIL_MUTATION = gql` @@ -142,13 +168,13 @@ const VERIFY_EMAIL_MUTATION = gql` `; export async function verify(token: string, fetchFn?: typeof globalThis.fetch) { - return loggedApiCall( - "verify", - async () => { - await getGraphQLClient(fetchFn).request(VERIFY_EMAIL_MUTATION, { token }); - }, - { hasToken: !!token }, - ); + return loggedApiCall( + "verify", + async () => { + await getGraphQLClient(fetchFn).request(VERIFY_EMAIL_MUTATION, { token }); + }, + { hasToken: !!token }, + ); } const REQUEST_PASSWORD_MUTATION = gql` @@ -158,13 +184,13 @@ const REQUEST_PASSWORD_MUTATION = gql` `; export async function requestPassword(email: string) { - return loggedApiCall( - "requestPassword", - async () => { - await getGraphQLClient().request(REQUEST_PASSWORD_MUTATION, { email }); - }, - { email }, - ); + return loggedApiCall( + "requestPassword", + async () => { + await getGraphQLClient().request(REQUEST_PASSWORD_MUTATION, { email }); + }, + { email }, + ); } const RESET_PASSWORD_MUTATION = gql` @@ -174,16 +200,16 @@ const RESET_PASSWORD_MUTATION = gql` `; export async function resetPassword(token: string, password: string) { - return loggedApiCall( - "resetPassword", - async () => { - await getGraphQLClient().request(RESET_PASSWORD_MUTATION, { - token, - newPassword: password, - }); - }, - { hasToken: !!token }, - ); + return loggedApiCall( + "resetPassword", + async () => { + await getGraphQLClient().request(RESET_PASSWORD_MUTATION, { + token, + newPassword: password, + }); + }, + { hasToken: !!token }, + ); } // ─── Articles ──────────────────────────────────────────────────────────────── @@ -191,41 +217,69 @@ export async function resetPassword(token: string, password: string) { const ARTICLES_QUERY = gql` query GetArticles { articles { - id slug title excerpt content image tags publish_date category featured - author { first_name last_name avatar description } + id + slug + title + excerpt + content + image + tags + publish_date + category + featured + author { + first_name + last_name + avatar + description + } } } `; export async function getArticles(fetchFn?: typeof globalThis.fetch) { - return loggedApiCall("getArticles", async () => { - const data = await getGraphQLClient(fetchFn).request<{ articles: Article[] }>(ARTICLES_QUERY); - return data.articles; - }); + return loggedApiCall("getArticles", async () => { + const data = await getGraphQLClient(fetchFn).request<{ articles: Article[] }>(ARTICLES_QUERY); + return data.articles; + }); } const ARTICLE_BY_SLUG_QUERY = gql` query GetArticleBySlug($slug: String!) { article(slug: $slug) { - id slug title excerpt content image tags publish_date category featured - author { first_name last_name avatar description } + id + slug + title + excerpt + content + image + tags + publish_date + category + featured + author { + first_name + last_name + avatar + description + } } } `; export async function getArticleBySlug(slug: string, fetchFn?: typeof globalThis.fetch) { - return loggedApiCall( - "getArticleBySlug", - async () => { - const data = await getGraphQLClient(fetchFn).request<{ article: Article | null }>( - ARTICLE_BY_SLUG_QUERY, - { slug }, - ); - if (!data.article) throw new Error("Article not found"); - return data.article; - }, - { slug }, - ); + return loggedApiCall( + "getArticleBySlug", + async () => { + const data = await getGraphQLClient(fetchFn).request<{ article: Article | null }>( + ARTICLE_BY_SLUG_QUERY, + { slug }, + ); + if (!data.article) throw new Error("Article not found"); + return data.article; + }, + { slug }, + ); } // ─── Videos ────────────────────────────────────────────────────────────────── @@ -233,75 +287,115 @@ export async function getArticleBySlug(slug: string, fetchFn?: typeof globalThis const VIDEOS_QUERY = gql` query GetVideos($modelId: String, $featured: Boolean, $limit: Int) { videos(modelId: $modelId, featured: $featured, limit: $limit) { - id slug title description image movie tags upload_date premium featured - likes_count plays_count - models { id artist_name slug avatar } - movie_file { id filename mime_type duration } + id + slug + title + description + image + movie + tags + upload_date + premium + featured + likes_count + plays_count + models { + id + artist_name + slug + avatar + } + movie_file { + id + filename + mime_type + duration + } } } `; export async function getVideos(fetchFn?: typeof globalThis.fetch) { - return loggedApiCall("getVideos", async () => { - const data = await getGraphQLClient(fetchFn).request<{ videos: Video[] }>(VIDEOS_QUERY); - return data.videos; - }); + return loggedApiCall("getVideos", async () => { + const data = await getGraphQLClient(fetchFn).request<{ videos: Video[] }>(VIDEOS_QUERY); + return data.videos; + }); } export async function getVideosForModel(id: string, fetchFn?: typeof globalThis.fetch) { - return loggedApiCall( - "getVideosForModel", - async () => { - const data = await getGraphQLClient(fetchFn).request<{ videos: Video[] }>(VIDEOS_QUERY, { - modelId: id, - }); - return data.videos; - }, - { modelId: id }, - ); + return loggedApiCall( + "getVideosForModel", + async () => { + const data = await getGraphQLClient(fetchFn).request<{ videos: Video[] }>(VIDEOS_QUERY, { + modelId: id, + }); + return data.videos; + }, + { modelId: id }, + ); } export async function getFeaturedVideos( - limit: number, - fetchFn: typeof globalThis.fetch = globalThis.fetch, + limit: number, + fetchFn: typeof globalThis.fetch = globalThis.fetch, ) { - return loggedApiCall( - "getFeaturedVideos", - async () => { - const data = await getGraphQLClient(fetchFn).request<{ videos: Video[] }>(VIDEOS_QUERY, { - featured: true, - limit, - }); - return data.videos; - }, - { limit }, - ); + return loggedApiCall( + "getFeaturedVideos", + async () => { + const data = await getGraphQLClient(fetchFn).request<{ videos: Video[] }>(VIDEOS_QUERY, { + featured: true, + limit, + }); + return data.videos; + }, + { limit }, + ); } const VIDEO_BY_SLUG_QUERY = gql` query GetVideoBySlug($slug: String!) { video(slug: $slug) { - id slug title description image movie tags upload_date premium featured - likes_count plays_count - models { id artist_name slug avatar } - movie_file { id filename mime_type duration } + id + slug + title + description + image + movie + tags + upload_date + premium + featured + likes_count + plays_count + models { + id + artist_name + slug + avatar + } + movie_file { + id + filename + mime_type + duration + } } } `; export async function getVideoBySlug(slug: string, fetchFn?: typeof globalThis.fetch) { - return loggedApiCall( - "getVideoBySlug", - async () => { - const data = await getGraphQLClient(fetchFn).request<{ video: Video | null }>( - VIDEO_BY_SLUG_QUERY, - { slug }, - ); - if (!data.video) throw new Error("Video not found"); - return data.video; - }, - { slug }, - ); + return loggedApiCall( + "getVideoBySlug", + async () => { + const data = await getGraphQLClient(fetchFn).request<{ video: Video | null }>( + VIDEO_BY_SLUG_QUERY, + { slug }, + ); + if (!data.video) throw new Error("Video not found"); + return data.video; + }, + { slug }, + ); } // ─── Models ────────────────────────────────────────────────────────────────── @@ -309,58 +403,78 @@ export async function getVideoBySlug(slug: string, fetchFn?: typeof globalThis.f const MODELS_QUERY = gql` query GetModels($featured: Boolean, $limit: Int) { models(featured: $featured, limit: $limit) { - id slug artist_name description avatar banner tags date_created - photos { id filename } + id + slug + artist_name + description + avatar + banner + tags + date_created + photos { + id + filename + } } } `; export async function getModels(fetchFn?: typeof globalThis.fetch) { - return loggedApiCall("getModels", async () => { - const data = await getGraphQLClient(fetchFn).request<{ models: Model[] }>(MODELS_QUERY); - return data.models; - }); + return loggedApiCall("getModels", async () => { + const data = await getGraphQLClient(fetchFn).request<{ models: Model[] }>(MODELS_QUERY); + return data.models; + }); } export async function getFeaturedModels( - limit = 3, - fetchFn: typeof globalThis.fetch = globalThis.fetch, + limit = 3, + fetchFn: typeof globalThis.fetch = globalThis.fetch, ) { - return loggedApiCall( - "getFeaturedModels", - async () => { - const data = await getGraphQLClient(fetchFn).request<{ models: Model[] }>(MODELS_QUERY, { - featured: true, - limit, - }); - return data.models; - }, - { limit }, - ); + return loggedApiCall( + "getFeaturedModels", + async () => { + const data = await getGraphQLClient(fetchFn).request<{ models: Model[] }>(MODELS_QUERY, { + featured: true, + limit, + }); + return data.models; + }, + { limit }, + ); } const MODEL_BY_SLUG_QUERY = gql` query GetModelBySlug($slug: String!) { model(slug: $slug) { - id slug artist_name description avatar banner tags date_created - photos { id filename } + id + slug + artist_name + description + avatar + banner + tags + date_created + photos { + id + filename + } } } `; export async function getModelBySlug(slug: string, fetchFn?: typeof globalThis.fetch) { - return loggedApiCall( - "getModelBySlug", - async () => { - const data = await getGraphQLClient(fetchFn).request<{ model: Model | null }>( - MODEL_BY_SLUG_QUERY, - { slug }, - ); - if (!data.model) throw new Error("Model not found"); - return data.model; - }, - { slug }, - ); + return loggedApiCall( + "getModelBySlug", + async () => { + const data = await getGraphQLClient(fetchFn).request<{ model: Model | null }>( + MODEL_BY_SLUG_QUERY, + { slug }, + ); + if (!data.model) throw new Error("Model not found"); + return data.model; + }, + { slug }, + ); } // ─── Profile ───────────────────────────────────────────────────────────────── @@ -380,76 +494,93 @@ const UPDATE_PROFILE_MUTATION = gql` description: $description tags: $tags ) { - id email first_name last_name artist_name slug description tags - role avatar banner date_created + id + email + first_name + last_name + artist_name + slug + description + tags + role + avatar + banner + date_created } } `; export async function updateProfile(user: Partial) { - return loggedApiCall( - "updateProfile", - async () => { - const data = await getGraphQLClient().request<{ updateProfile: User }>( - UPDATE_PROFILE_MUTATION, - { - firstName: user.first_name, - lastName: user.last_name, - artistName: user.artist_name, - description: user.description, - tags: user.tags, - }, - ); - return data.updateProfile; - }, - { userId: user.id }, - ); + return loggedApiCall( + "updateProfile", + async () => { + const data = await getGraphQLClient().request<{ updateProfile: User }>( + UPDATE_PROFILE_MUTATION, + { + firstName: user.first_name, + lastName: user.last_name, + artistName: user.artist_name, + description: user.description, + tags: user.tags, + }, + ); + return data.updateProfile; + }, + { userId: user.id }, + ); } // ─── Stats ─────────────────────────────────────────────────────────────────── const STATS_QUERY = gql` query GetStats { - stats { videos_count models_count viewers_count } + stats { + videos_count + models_count + viewers_count + } } `; export async function getStats(fetchFn?: typeof globalThis.fetch) { - return loggedApiCall("getStats", async () => { - const data = await getGraphQLClient(fetchFn).request<{ stats: Stats }>(STATS_QUERY); - return data.stats; - }); + return loggedApiCall("getStats", async () => { + const data = await getGraphQLClient(fetchFn).request<{ stats: Stats }>(STATS_QUERY); + return data.stats; + }); } // Stub — Directus folder concept dropped export async function getFolders(_fetchFn?: typeof globalThis.fetch) { - return loggedApiCall("getFolders", async () => []); + return loggedApiCall("getFolders", async () => []); } // ─── Files ─────────────────────────────────────────────────────────────────── export async function removeFile(id: string) { - return loggedApiCall( - "removeFile", - async () => { - // File deletion via REST DELETE /assets/:id (backend handles it) - const response = await fetch(`${apiUrl}/assets/${id}`, { method: "DELETE", credentials: "include" }); - if (!response.ok) throw new Error(`Failed to delete file: ${response.statusText}`); - }, - { fileId: id }, - ); + return loggedApiCall( + "removeFile", + async () => { + // File deletion via REST DELETE /assets/:id (backend handles it) + const response = await fetch(`${apiUrl}/assets/${id}`, { + method: "DELETE", + credentials: "include", + }); + if (!response.ok) throw new Error(`Failed to delete file: ${response.statusText}`); + }, + { fileId: id }, + ); } export async function uploadFile(data: FormData) { - return loggedApiCall("uploadFile", async () => { - const response = await fetch(`${apiUrl}/upload`, { - method: "POST", - body: data, - credentials: "include", - }); - if (!response.ok) throw new Error(`Upload failed: ${response.statusText}`); - return response.json(); - }); + return loggedApiCall("uploadFile", async () => { + const response = await fetch(`${apiUrl}/upload`, { + method: "POST", + body: data, + credentials: "include", + }); + if (!response.ok) throw new Error(`Upload failed: ${response.statusText}`); + return response.json(); + }); } // ─── Comments ──────────────────────────────────────────────────────────────── @@ -457,100 +588,150 @@ export async function uploadFile(data: FormData) { const COMMENTS_FOR_VIDEO_QUERY = gql` query CommentsForVideo($videoId: String!) { commentsForVideo(videoId: $videoId) { - id comment item_id user_id date_created - user { id first_name last_name avatar } + id + comment + item_id + user_id + date_created + user { + id + first_name + last_name + avatar + } } } `; export async function getCommentsForVideo(item: string, fetchFn?: typeof globalThis.fetch) { - return loggedApiCall( - "getCommentsForVideo", - async () => { - const data = await getGraphQLClient(fetchFn).request<{ - commentsForVideo: { id: number; comment: string; item_id: string; user_id: string; date_created: string; user: { id: string; first_name: string | null; last_name: string | null; avatar: string | null } | null }[]; - }>(COMMENTS_FOR_VIDEO_QUERY, { videoId: item }); - return data.commentsForVideo; - }, - { videoId: item }, - ); + return loggedApiCall( + "getCommentsForVideo", + async () => { + const data = await getGraphQLClient(fetchFn).request<{ + commentsForVideo: { + id: number; + comment: string; + item_id: string; + user_id: string; + date_created: string; + user: { + id: string; + first_name: string | null; + last_name: string | null; + avatar: string | null; + } | null; + }[]; + }>(COMMENTS_FOR_VIDEO_QUERY, { videoId: item }); + return data.commentsForVideo; + }, + { videoId: item }, + ); } const CREATE_COMMENT_MUTATION = gql` mutation CreateCommentForVideo($videoId: String!, $comment: String!) { createCommentForVideo(videoId: $videoId, comment: $comment) { - id comment item_id user_id date_created + id + comment + item_id + user_id + date_created } } `; export async function createCommentForVideo(item: string, comment: string) { - return loggedApiCall( - "createCommentForVideo", - async () => { - const data = await getGraphQLClient().request(CREATE_COMMENT_MUTATION, { - videoId: item, - comment, - }); - return data; - }, - { videoId: item, commentLength: comment.length }, - ); + return loggedApiCall( + "createCommentForVideo", + async () => { + const data = await getGraphQLClient().request(CREATE_COMMENT_MUTATION, { + videoId: item, + comment, + }); + return data; + }, + { videoId: item, commentLength: comment.length }, + ); } export async function countCommentsForModel( - _user_created: string, - _fetchFn?: typeof globalThis.fetch, + _user_created: string, + _fetchFn?: typeof globalThis.fetch, ) { - // Not directly available in new API, return 0 - return 0; + // Not directly available in new API, return 0 + return 0; } // ─── Tags ──────────────────────────────────────────────────────────────────── export async function getItemsByTag( - category: "video" | "article" | "model", - _tag: string, - fetchFn?: typeof globalThis.fetch, + category: "video" | "article" | "model", + _tag: string, + fetchFn?: typeof globalThis.fetch, ) { - return loggedApiCall( - "getItemsByTag", - async () => { - switch (category) { - case "video": - return getVideos(fetchFn); - case "model": - return getModels(fetchFn); - case "article": - return getArticles(fetchFn); - } - }, - { category }, - ); + return loggedApiCall( + "getItemsByTag", + async () => { + switch (category) { + case "video": + return getVideos(fetchFn); + case "model": + return getModels(fetchFn); + case "article": + return getArticles(fetchFn); + } + }, + { category }, + ); } // ─── Recordings ────────────────────────────────────────────────────────────── const RECORDINGS_QUERY = gql` - query GetRecordings($status: String, $tags: String, $linkedVideoId: String, $limit: Int, $page: Int) { - recordings(status: $status, tags: $tags, linkedVideoId: $linkedVideoId, limit: $limit, page: $page) { - id title description slug duration events device_info user_id status - tags linked_video featured public date_created date_updated + query GetRecordings( + $status: String + $tags: String + $linkedVideoId: String + $limit: Int + $page: Int + ) { + recordings( + status: $status + tags: $tags + linkedVideoId: $linkedVideoId + limit: $limit + page: $page + ) { + id + title + description + slug + duration + events + device_info + user_id + status + tags + linked_video + featured + public + date_created + date_updated } } `; export async function getRecordings(fetchFn?: typeof globalThis.fetch) { - return loggedApiCall( - "getRecordings", - async () => { - const data = await getGraphQLClient(fetchFn).request<{ recordings: Recording[] }>( - RECORDINGS_QUERY, - ); - return data.recordings; - }, - {}, - ); + return loggedApiCall( + "getRecordings", + async () => { + const data = await getGraphQLClient(fetchFn).request<{ recordings: Recording[] }>( + RECORDINGS_QUERY, + ); + return data.recordings; + }, + {}, + ); } const CREATE_RECORDING_MUTATION = gql` @@ -574,43 +755,56 @@ const CREATE_RECORDING_MUTATION = gql` status: $status linkedVideoId: $linkedVideoId ) { - id title description slug duration events device_info user_id status - tags linked_video featured public date_created date_updated + id + title + description + slug + duration + events + device_info + user_id + status + tags + linked_video + featured + public + date_created + date_updated } } `; export async function createRecording( - recording: { - title: string; - description?: string; - duration: number; - events: unknown[]; - device_info: unknown[]; - tags?: string[]; - status?: string; - }, - fetchFn?: typeof globalThis.fetch, + recording: { + title: string; + description?: string; + duration: number; + events: unknown[]; + device_info: unknown[]; + tags?: string[]; + status?: string; + }, + fetchFn?: typeof globalThis.fetch, ) { - return loggedApiCall( - "createRecording", - async () => { - const data = await getGraphQLClient(fetchFn).request<{ createRecording: Recording }>( - CREATE_RECORDING_MUTATION, - { - title: recording.title, - description: recording.description, - duration: recording.duration, - events: recording.events, - deviceInfo: recording.device_info, - tags: recording.tags, - status: recording.status, - }, - ); - return data.createRecording; - }, - { title: recording.title, eventCount: recording.events.length }, - ); + return loggedApiCall( + "createRecording", + async () => { + const data = await getGraphQLClient(fetchFn).request<{ createRecording: Recording }>( + CREATE_RECORDING_MUTATION, + { + title: recording.title, + description: recording.description, + duration: recording.duration, + events: recording.events, + deviceInfo: recording.device_info, + tags: recording.tags, + status: recording.status, + }, + ); + return data.createRecording; + }, + { title: recording.title, eventCount: recording.events.length }, + ); } const DELETE_RECORDING_MUTATION = gql` @@ -620,146 +814,179 @@ const DELETE_RECORDING_MUTATION = gql` `; export async function deleteRecording(id: string) { - return loggedApiCall( - "deleteRecording", - async () => { - await getGraphQLClient().request(DELETE_RECORDING_MUTATION, { id }); - }, - { id }, - ); + return loggedApiCall( + "deleteRecording", + async () => { + await getGraphQLClient().request(DELETE_RECORDING_MUTATION, { id }); + }, + { id }, + ); } const RECORDING_QUERY = gql` query GetRecording($id: String!) { recording(id: $id) { - id title description slug duration events device_info user_id status - tags linked_video featured public date_created date_updated + id + title + description + slug + duration + events + device_info + user_id + status + tags + linked_video + featured + public + date_created + date_updated } } `; export async function getRecording(id: string, fetchFn?: typeof globalThis.fetch) { - return loggedApiCall( - "getRecording", - async () => { - const data = await getGraphQLClient(fetchFn).request<{ recording: Recording | null }>( - RECORDING_QUERY, - { id }, - ); - return data.recording; - }, - { id }, - ); + return loggedApiCall( + "getRecording", + async () => { + const data = await getGraphQLClient(fetchFn).request<{ recording: Recording | null }>( + RECORDING_QUERY, + { id }, + ); + return data.recording; + }, + { id }, + ); } // ─── Video likes & plays ───────────────────────────────────────────────────── const LIKE_VIDEO_MUTATION = gql` mutation LikeVideo($videoId: String!) { - likeVideo(videoId: $videoId) { liked likes_count } + likeVideo(videoId: $videoId) { + liked + likes_count + } } `; export async function likeVideo(videoId: string) { - return loggedApiCall( - "likeVideo", - async () => { - const data = await getGraphQLClient().request<{ likeVideo: VideoLikeResponse }>( - LIKE_VIDEO_MUTATION, - { videoId }, - ); - return data.likeVideo; - }, - { videoId }, - ); + return loggedApiCall( + "likeVideo", + async () => { + const data = await getGraphQLClient().request<{ likeVideo: VideoLikeResponse }>( + LIKE_VIDEO_MUTATION, + { videoId }, + ); + return data.likeVideo; + }, + { videoId }, + ); } const UNLIKE_VIDEO_MUTATION = gql` mutation UnlikeVideo($videoId: String!) { - unlikeVideo(videoId: $videoId) { liked likes_count } + unlikeVideo(videoId: $videoId) { + liked + likes_count + } } `; export async function unlikeVideo(videoId: string) { - return loggedApiCall( - "unlikeVideo", - async () => { - const data = await getGraphQLClient().request<{ unlikeVideo: VideoLikeResponse }>( - UNLIKE_VIDEO_MUTATION, - { videoId }, - ); - return data.unlikeVideo; - }, - { videoId }, - ); + return loggedApiCall( + "unlikeVideo", + async () => { + const data = await getGraphQLClient().request<{ unlikeVideo: VideoLikeResponse }>( + UNLIKE_VIDEO_MUTATION, + { videoId }, + ); + return data.unlikeVideo; + }, + { videoId }, + ); } const VIDEO_LIKE_STATUS_QUERY = gql` query VideoLikeStatus($videoId: String!) { - videoLikeStatus(videoId: $videoId) { liked } + videoLikeStatus(videoId: $videoId) { + liked + } } `; export async function getVideoLikeStatus(videoId: string, fetchFn?: typeof globalThis.fetch) { - return loggedApiCall( - "getVideoLikeStatus", - async () => { - const data = await getGraphQLClient(fetchFn).request<{ videoLikeStatus: VideoLikeStatus }>( - VIDEO_LIKE_STATUS_QUERY, - { videoId }, - ); - return data.videoLikeStatus; - }, - { videoId }, - ); + return loggedApiCall( + "getVideoLikeStatus", + async () => { + const data = await getGraphQLClient(fetchFn).request<{ videoLikeStatus: VideoLikeStatus }>( + VIDEO_LIKE_STATUS_QUERY, + { videoId }, + ); + return data.videoLikeStatus; + }, + { videoId }, + ); } const RECORD_VIDEO_PLAY_MUTATION = gql` mutation RecordVideoPlay($videoId: String!, $sessionId: String) { recordVideoPlay(videoId: $videoId, sessionId: $sessionId) { - success play_id plays_count + success + play_id + plays_count } } `; export async function recordVideoPlay(videoId: string, sessionId?: string) { - return loggedApiCall( - "recordVideoPlay", - async () => { - const data = await getGraphQLClient().request<{ recordVideoPlay: VideoPlayResponse }>( - RECORD_VIDEO_PLAY_MUTATION, - { videoId, sessionId }, - ); - return data.recordVideoPlay; - }, - { videoId }, - ); + return loggedApiCall( + "recordVideoPlay", + async () => { + const data = await getGraphQLClient().request<{ recordVideoPlay: VideoPlayResponse }>( + RECORD_VIDEO_PLAY_MUTATION, + { videoId, sessionId }, + ); + return data.recordVideoPlay; + }, + { videoId }, + ); } const UPDATE_VIDEO_PLAY_MUTATION = gql` - mutation UpdateVideoPlay($videoId: String!, $playId: String!, $durationWatched: Int!, $completed: Boolean!) { - updateVideoPlay(videoId: $videoId, playId: $playId, durationWatched: $durationWatched, completed: $completed) + mutation UpdateVideoPlay( + $videoId: String! + $playId: String! + $durationWatched: Int! + $completed: Boolean! + ) { + updateVideoPlay( + videoId: $videoId + playId: $playId + durationWatched: $durationWatched + completed: $completed + ) } `; export async function updateVideoPlay( - videoId: string, - playId: string, - durationWatched: number, - completed: boolean, + videoId: string, + playId: string, + durationWatched: number, + completed: boolean, ) { - return loggedApiCall( - "updateVideoPlay", - async () => { - await getGraphQLClient().request(UPDATE_VIDEO_PLAY_MUTATION, { - videoId, - playId, - durationWatched, - completed, - }); - }, - { videoId, playId, durationWatched, completed }, - ); + return loggedApiCall( + "updateVideoPlay", + async () => { + await getGraphQLClient().request(UPDATE_VIDEO_PLAY_MUTATION, { + videoId, + playId, + durationWatched, + completed, + }); + }, + { videoId, playId, durationWatched, completed }, + ); } // ─── Analytics ─────────────────────────────────────────────────────────────── @@ -771,14 +998,14 @@ const ANALYTICS_QUERY = gql` `; export async function getAnalytics(fetchFn?: typeof globalThis.fetch) { - return loggedApiCall( - "getAnalytics", - async () => { - const data = await getGraphQLClient(fetchFn).request<{ analytics: Analytics | null }>( - ANALYTICS_QUERY, - ); - return data.analytics; - }, - {}, - ); + return loggedApiCall( + "getAnalytics", + async () => { + const data = await getGraphQLClient(fetchFn).request<{ analytics: Analytics | null }>( + ANALYTICS_QUERY, + ); + return data.analytics; + }, + {}, + ); } diff --git a/packages/frontend/src/lib/types.ts b/packages/frontend/src/lib/types.ts index 4b1b655..32b61b1 100644 --- a/packages/frontend/src/lib/types.ts +++ b/packages/frontend/src/lib/types.ts @@ -1,205 +1,205 @@ import { type ButtplugClientDevice } from "@sexy.pivoine.art/buttplug"; export interface User { - id: string; - first_name: string; - last_name: string; - artist_name: string; - slug: string; - email: string; - description: string; - tags: string[]; - avatar: string | File; - password: string; - directus_users_id?: User; + id: string; + first_name: string; + last_name: string; + artist_name: string; + slug: string; + email: string; + description: string; + tags: string[]; + avatar: string | File; + password: string; + directus_users_id?: User; } export interface CurrentUser extends User { - avatar: File; - role: "model" | "viewer" | "admin"; - policies: string[]; + avatar: File; + role: "model" | "viewer" | "admin"; + policies: string[]; } export interface AuthStatus { - authenticated: boolean; - user?: CurrentUser; - data?: { - refresh_token: string | null; - }; + authenticated: boolean; + user?: CurrentUser; + data?: { + refresh_token: string | null; + }; } export interface File { - id: string; - filesize: number; - title: string; - description: string; - duration: number; - directus_files_id?: File; + id: string; + filesize: number; + title: string; + description: string; + duration: number; + directus_files_id?: File; } export interface Article { - id: string; - slug: string; - title: string; - excerpt: string; - content: string; - image: string; - tags: string[]; - publish_date: Date; - author: { - first_name: string; - last_name: string; - avatar: string; - description?: string; - website?: string; - }; - category: string; - featured?: boolean; + id: string; + slug: string; + title: string; + excerpt: string; + content: string; + image: string; + tags: string[]; + publish_date: Date; + author: { + first_name: string; + last_name: string; + avatar: string; + description?: string; + website?: string; + }; + category: string; + featured?: boolean; } export interface Model { - id: string; - slug: string; - artist_name: string; - description: string; - avatar: string; - category: string; - tags: string[]; - join_date: Date; - featured?: boolean; - photos: File[]; - banner?: File; + id: string; + slug: string; + artist_name: string; + description: string; + avatar: string; + category: string; + tags: string[]; + join_date: Date; + featured?: boolean; + photos: File[]; + banner?: File; } export interface Video { - id: string; - slug: string; - title: string; - description: string; - image: string; - movie: File; - models: User[]; - tags: string[]; - upload_date: Date; - premium?: boolean; - featured?: boolean; - likes_count?: number; - plays_count?: number; - views_count?: number; + id: string; + slug: string; + title: string; + description: string; + image: string; + movie: File; + models: User[]; + tags: string[]; + upload_date: Date; + premium?: boolean; + featured?: boolean; + likes_count?: number; + plays_count?: number; + views_count?: number; } export interface Comment { - id: string; - comment: string; - item: string; - user_created: User; - date_created: Date; + id: string; + comment: string; + item: string; + user_created: User; + date_created: Date; } export interface Stats { - videos_count: number; - models_count: number; - viewers_count: number; + videos_count: number; + models_count: number; + viewers_count: number; } export interface DeviceActuator { - featureIndex: number; - outputType: string; - maxSteps: number; - descriptor: string; - value: number; + featureIndex: number; + outputType: string; + maxSteps: number; + descriptor: string; + value: number; } export interface BluetoothDevice { - id: string; - name: string; - actuators: DeviceActuator[]; - batteryLevel: number; - hasBattery: boolean; - isConnected: boolean; - lastSeen: Date; - info: ButtplugClientDevice; + id: string; + name: string; + actuators: DeviceActuator[]; + batteryLevel: number; + hasBattery: boolean; + isConnected: boolean; + lastSeen: Date; + info: ButtplugClientDevice; } export interface ShareContent { - title: string; - description: string; - url: string; - type: "video" | "model" | "article" | "link"; + title: string; + description: string; + url: string; + type: "video" | "model" | "article" | "link"; } export interface RecordedEvent { - timestamp: number; - deviceIndex: number; - deviceName: string; - actuatorIndex: number; - actuatorType: string; - value: number; + timestamp: number; + deviceIndex: number; + deviceName: string; + actuatorIndex: number; + actuatorType: string; + value: number; } export interface DeviceInfo { - name: string; - index: number; - capabilities: string[]; + name: string; + index: number; + capabilities: string[]; } export interface Recording { - id: string; - title: string; - description?: string; - slug: string; - duration: number; - events: RecordedEvent[]; - device_info: DeviceInfo[]; - user_created: string | User; - date_created: Date; - date_updated?: Date; - status: "draft" | "published" | "archived"; - tags?: string[]; - linked_video?: string | Video; - featured?: boolean; - public?: boolean; + id: string; + title: string; + description?: string; + slug: string; + duration: number; + events: RecordedEvent[]; + device_info: DeviceInfo[]; + user_created: string | User; + date_created: Date; + date_updated?: Date; + status: "draft" | "published" | "archived"; + tags?: string[]; + linked_video?: string | Video; + featured?: boolean; + public?: boolean; } export interface VideoLikeStatus { - liked: boolean; + liked: boolean; } export interface VideoPlayRecord { - id: string; - video_id: string; - duration_watched?: number; - completed: boolean; + id: string; + video_id: string; + duration_watched?: number; + completed: boolean; } export interface VideoLikeResponse { - liked: boolean; - likes_count: number; + liked: boolean; + likes_count: number; } export interface VideoPlayResponse { - success: boolean; - play_id: string; - plays_count: number; + success: boolean; + play_id: string; + plays_count: number; } export interface VideoAnalytics { - id: string; - title: string; - slug: string; - upload_date: Date; - likes: number; - plays: number; - completed_plays: number; - completion_rate: number; - avg_watch_time: number; + id: string; + title: string; + slug: string; + upload_date: Date; + likes: number; + plays: number; + completed_plays: number; + completion_rate: number; + avg_watch_time: number; } export interface Analytics { - total_videos: number; - total_likes: number; - total_plays: number; - plays_by_date: Record; - likes_by_date: Record; - videos: VideoAnalytics[]; + total_videos: number; + total_likes: number; + total_plays: number; + plays_by_date: Record; + likes_by_date: Record; + videos: VideoAnalytics[]; } diff --git a/packages/frontend/src/lib/utils.ts b/packages/frontend/src/lib/utils.ts index 1ed5d0d..c6ac84b 100644 --- a/packages/frontend/src/lib/utils.ts +++ b/packages/frontend/src/lib/utils.ts @@ -2,43 +2,41 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)); } // eslint-disable-next-line @typescript-eslint/no-explicit-any export type WithoutChild = T extends { child?: any } ? Omit : T; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type WithoutChildren = T extends { children?: any } - ? Omit - : T; +export type WithoutChildren = T extends { children?: any } ? Omit : T; export type WithoutChildrenOrChild = WithoutChildren>; export type WithElementRef = T & { - ref?: U | null; + ref?: U | null; }; export const calcReadingTime = (text: string) => { - const wordsPerMinute = 200; // Average case. - const textLength = text.split(" ").length; // Split by words - if (textLength > 0) { - return Math.ceil(textLength / wordsPerMinute); - } - return 0; + const wordsPerMinute = 200; // Average case. + const textLength = text.split(" ").length; // Split by words + if (textLength > 0) { + return Math.ceil(textLength / wordsPerMinute); + } + return 0; }; export const getUserInitials = (name: string) => { - if (!name) return "??"; - return name - .split(" ") - .map((word) => word.charAt(0)) - .join("") - .toUpperCase() - .slice(0, 2); + if (!name) return "??"; + return name + .split(" ") + .map((word) => word.charAt(0)) + .join("") + .toUpperCase() + .slice(0, 2); }; export const formatVideoDuration = (duration: number) => { - const hours = Math.floor(duration / 3600); - const minutes = Math.floor((duration - hours * 3600) / 60); - const seconds = duration - hours * 3600 - minutes * 60; + const hours = Math.floor(duration / 3600); + const minutes = Math.floor((duration - hours * 3600) / 60); + const seconds = duration - hours * 3600 - minutes * 60; - return `${hours < 10 ? "0" + hours : hours}:${minutes < 10 ? "0" + minutes : minutes}:${seconds < 10 ? "0" + seconds : seconds}`; + return `${hours < 10 ? "0" + hours : hours}:${minutes < 10 ? "0" + minutes : minutes}:${seconds < 10 ? "0" + seconds : seconds}`; }; diff --git a/packages/frontend/src/lib/utils/utils.ts b/packages/frontend/src/lib/utils/utils.ts index 8f6b51d..0a65080 100644 --- a/packages/frontend/src/lib/utils/utils.ts +++ b/packages/frontend/src/lib/utils/utils.ts @@ -6,16 +6,14 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)); } // eslint-disable-next-line @typescript-eslint/no-explicit-any export type WithoutChild = T extends { child?: any } ? Omit : T; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type WithoutChildren = T extends { children?: any } - ? Omit - : T; +export type WithoutChildren = T extends { children?: any } ? Omit : T; export type WithoutChildrenOrChild = WithoutChildren>; export type WithElementRef = T & { - ref?: U | null; + ref?: U | null; }; diff --git a/packages/frontend/src/routes/+error.svelte b/packages/frontend/src/routes/+error.svelte index 1a84906..4662501 100644 --- a/packages/frontend/src/routes/+error.svelte +++ b/packages/frontend/src/routes/+error.svelte @@ -1,123 +1,119 @@
    - + - -
    -
    - - +
    +
    + + + + +
    +
    - - -
    -
    - {page.status} -
    -
    - -
    -
    + {page.status} +
    +
    + +
    +
    - -
    -

    - {page.status === 404 ? $_("error.not_found") : $_("error.common")} -

    -

    - {$_("error.description")} -

    -
    + +
    +

    + {page.status === 404 ? $_("error.not_found") : $_("error.common")} +

    +

    + {$_("error.description")} +

    +
    - -
    - + +
    + - -
    + +
    - -
    -

    - {$_("error.quick_links")} -

    - -
    -
    -
    -
    + +
    +

    + {$_("error.quick_links")} +

    + +
    + +
    +
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    diff --git a/packages/frontend/src/routes/+layout.server.ts b/packages/frontend/src/routes/+layout.server.ts index c94572d..ea7326b 100644 --- a/packages/frontend/src/routes/+layout.server.ts +++ b/packages/frontend/src/routes/+layout.server.ts @@ -1,5 +1,5 @@ export async function load({ locals }) { - return { - authStatus: locals.authStatus, - }; + return { + authStatus: locals.authStatus, + }; } diff --git a/packages/frontend/src/routes/+layout.svelte b/packages/frontend/src/routes/+layout.svelte index 4aa8c04..d063335 100644 --- a/packages/frontend/src/routes/+layout.svelte +++ b/packages/frontend/src/routes/+layout.svelte @@ -1,29 +1,25 @@ - {#if import.meta.env.PROD && env.PUBLIC_UMAMI_ID && env.PUBLIC_UMAMI_SCRIPT} - - {/if} + {#if import.meta.env.PROD && env.PUBLIC_UMAMI_ID && env.PUBLIC_UMAMI_SCRIPT} + + {/if} @@ -31,48 +27,48 @@ let { children, data } = $props();
    - -
    - -
    -
    + +
    + +
    +
    - -
    -
    + +
    +
    - -
    -
    -
    + +
    +
    +
    - -
    -
    - -
    + +
    +
    + +
    - -
    - {@render children()} -
    + +
    + {@render children()} +
    - -
    + +
    diff --git a/packages/frontend/src/routes/+page.server.ts b/packages/frontend/src/routes/+page.server.ts index 6c5a1f3..5ef9003 100644 --- a/packages/frontend/src/routes/+page.server.ts +++ b/packages/frontend/src/routes/+page.server.ts @@ -1,7 +1,7 @@ import { getFeaturedModels, getFeaturedVideos } from "$lib/services"; export async function load({ fetch }) { - return { - models: await getFeaturedModels(3, fetch), - videos: await getFeaturedVideos(3, fetch), - }; + return { + models: await getFeaturedModels(3, fetch), + videos: await getFeaturedVideos(3, fetch), + }; } diff --git a/packages/frontend/src/routes/+page.svelte b/packages/frontend/src/routes/+page.svelte index 1c2cc62..51619e8 100644 --- a/packages/frontend/src/routes/+page.svelte +++ b/packages/frontend/src/routes/+page.svelte @@ -1,24 +1,20 @@ - + -
    +
    -
    +
    @@ -26,13 +22,11 @@ const { data } = $props();

    - {$_('home.hero.title')} + {$_("home.hero.title")}

    -

    - {$_('home.hero.description')} +

    + {$_("home.hero.description")}

    @@ -42,13 +36,13 @@ const { data } = $props(); href="/videos" > - {$_('home.hero.cta_videos')} + {$_("home.hero.cta_videos")} {$_("home.hero.cta_models")}
    @@ -68,10 +62,10 @@ const { data } = $props();

    - {$_('home.featured_models.title')} + {$_("home.featured_models.title")}

    - {$_('home.featured_models.description')} + {$_("home.featured_models.description")}

    @@ -83,7 +77,7 @@ const { data } = $props();
    {model.artist_name} @@ -107,8 +101,7 @@ const { data } = $props(); variant="ghost" size="sm" class="mt-4 w-full group-hover:bg-primary/10" - href="/models/{model.slug}" - >{$_('home.featured_models.view_profile')}{$_("home.featured_models.view_profile")} @@ -122,7 +115,7 @@ const { data } = $props();

    - {$_('home.trending.title')} + {$_("home.trending.title")}

    @@ -134,16 +127,14 @@ const { data } = $props(); >
    {video.title}
    -
    +
    {#if video.movie_file?.duration}{formatVideoDuration(video.movie_file.duration)}{/if}
    -
    +

    - {$_('home.featured_models.join_community')} + {$_("home.featured_models.join_community")}

    - {$_('home.featured_models.join_community_description')} + {$_("home.featured_models.join_community_description")}

    {$_("home.community.cta_join")} {$_("home.community.cta_magazine")}
    diff --git a/packages/frontend/src/routes/about/+page.server.ts b/packages/frontend/src/routes/about/+page.server.ts index 5964cb2..1356090 100644 --- a/packages/frontend/src/routes/about/+page.server.ts +++ b/packages/frontend/src/routes/about/+page.server.ts @@ -1,6 +1,6 @@ import { getStats } from "$lib/services"; export async function load({ fetch }) { - return { - stats: await getStats(fetch), - }; + return { + stats: await getStats(fetch), + }; } diff --git a/packages/frontend/src/routes/about/+page.svelte b/packages/frontend/src/routes/about/+page.svelte index 97765a4..3145215 100644 --- a/packages/frontend/src/routes/about/+page.svelte +++ b/packages/frontend/src/routes/about/+page.svelte @@ -1,312 +1,310 @@
    - + - -
    -
    -
    -
    -

    +
    +
    +
    +
    +

    + {$_("about.title")} +

    +

    + {$_("about.subtitle")} +

    +
    + +
    +
    +
    +
    + + +
    +
    +
    + {#each stats as stat (stat.icon)} +
    +
    + +
    +
    {stat.value}
    +
    {stat.label}
    +
    + {/each} +
    +
    +
    + + +
    +
    +
    +
    +

    + {$_("about.story.title")} +

    +

    + {$_("about.story.subtitle")} +

    +
    + +
    +
    +

    + {$_("about.story.description_part1")} +

    +

    + {$_("about.story.description_part2")} +

    +

    + {$_("about.story.description_part3")} +

    +
    +
    + Our story +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +

    + {$_("about.values.title")} +

    +

    + {$_("about.values.subtitle")} +

    +
    + +
    + {#each values as value (value.title)} + + +
    +
    - {$_("about.title")} -

    -

    - {$_("about.subtitle")} -

    -
    - +
    -
    -
    -
    - - -
    -
    -
    - {#each stats as stat (stat.icon)} -
    -
    - -
    -
    {stat.value}
    -
    {stat.label}
    -
    - {/each} -
    -
    -
    - - -
    -
    -
    -
    -

    - {$_("about.story.title")} -

    -

    - {$_("about.story.subtitle")} -

    +
    +

    {value.title}

    +

    + {value.description} +

    +
    + + + {/each} +
    +
    +
    -
    -
    -

    - {$_("about.story.description_part1")} -

    -

    - {$_("about.story.description_part2")} -

    -

    - {$_("about.story.description_part3")} -

    -
    -
    - Our story -
    -
    -
    -
    + +
    +
    +
    +

    + {$_("about.team.title")} +

    +

    + {$_("about.team.subtitle")} +

    +
    + +
    + {#each team as member (member.name)} + + + {member.name} +

    {member.name}

    +

    {member.role}

    +

    + {member.bio} +

    +
    +
    + {/each} +
    +
    +
    + + +
    +
    +
    +

    + {$_("about.mission.title")} +

    +

    + {$_("about.mission.description")} +

    +
    + + +
    -
    +
    +
    +
    - -
    -
    -
    -

    - {$_("about.values.title")} -

    -

    - {$_("about.values.subtitle")} -

    -
    - -
    - {#each values as value (value.title)} - - -
    -
    - -
    -
    -

    {value.title}

    -

    - {value.description} -

    -
    -
    -
    -
    - {/each} -
    + +
    +
    +
    +

    + {$_("about.contact.title")} +

    +

    + {$_("about.contact.description")} +

    +
    + + +

    + {$_("about.contact.general.title")} +

    +

    + {$_("about.contact.general.description")} +

    + {$_("about.contact.general.mailto")} +
    +
    + + +

    + {$_("about.contact.creators.title")} +

    +

    + {$_("about.contact.creators.description")} +

    + {$_("about.contact.creators.mailto")} +
    +
    -
    - - -
    -
    -
    -

    - {$_("about.team.title")} -

    -

    - {$_("about.team.subtitle")} -

    -
    - -
    - {#each team as member (member.name)} - - - {member.name} -

    {member.name}

    -

    {member.role}

    -

    - {member.bio} -

    -
    -
    - {/each} -
    -
    -
    - - -
    -
    -
    -

    - {$_("about.mission.title")} -

    -

    - {$_("about.mission.description")} -

    -
    - - - -
    -
    -
    -
    - - -
    -
    -
    -

    - {$_("about.contact.title")} -

    -

    - {$_("about.contact.description")} -

    -
    - - -

    - {$_("about.contact.general.title")} -

    -

    - {$_("about.contact.general.description")} -

    - {$_("about.contact.general.mailto")} -
    -
    - - -

    - {$_("about.contact.creators.title")} -

    -

    - {$_("about.contact.creators.description")} -

    - {$_("about.contact.creators.mailto")} -
    -
    -
    -
    -
    -
    +
    +
    +
    diff --git a/packages/frontend/src/routes/faq/+page.svelte b/packages/frontend/src/routes/faq/+page.svelte index dfd08c6..bab7424 100644 --- a/packages/frontend/src/routes/faq/+page.svelte +++ b/packages/frontend/src/routes/faq/+page.svelte @@ -1,358 +1,346 @@
    - + -
    - -
    -

    + +
    +

    + {$_("faq.title")} +

    +

    + {$_("faq.description")} +

    +
    + + +
    +
    + + +
    +
    + + {#if searchQuery.trim()} + +
    +

    + {$_("faq.search_results", { + values: { count: filteredQuestions().length }, + })} +

    +
    + {#each filteredQuestions() as question (question.id)} + - {$_("faq.title")} -

    -

    - {$_("faq.description")} -

    -
    - - -
    -
    - - -
    -
    - - {#if searchQuery.trim()} - -
    -

    - {$_("faq.search_results", { - values: { count: filteredQuestions().length }, - })} -

    -
    - {#each filteredQuestions() as question (question.id)} - - - - {#if expandedItems.has(question.id)} -
    -

    - {question.answer} -

    -
    - {/if} -
    -
    - {/each} -
    - {#if filteredQuestions.length === 0} -
    -

    {$_("faq.no_results")}

    - + + - {#if expandedItems.has(question.id)} -
    -

    - {question.answer} -

    -
    - {/if} -
    - {/each} -
    - - - {/each} -
    -
    - {/if} - - -
    - - -

    {$_("faq.support.title")}

    -

    - {$_("faq.support.description")} +

    {question.question}

    +
    + {#if expandedItems.has(question.id)} + + {:else} + + {/if} + + {#if expandedItems.has(question.id)} +
    +

    + {question.answer}

    -
    - - +
    +
    + {#each faqCategories as category (category.id)} + + + +
    + +
    + {category.title} +
    +
    + +
    + {#each category.questions as question (question.id)} +
    + + {#if expandedItems.has(question.id)} +
    +

    + {question.answer} +

    +
    + {/if} +
    + {/each} +
    +
    +
    + {/each} +
    +
    + {/if} + + +
    + + +

    {$_("faq.support.title")}

    +

    + {$_("faq.support.description")} +

    +
    + + -
    -
    -
    -
    +
    + +
    +
    diff --git a/packages/frontend/src/routes/imprint/+page.svelte b/packages/frontend/src/routes/imprint/+page.svelte index 38e7f26..d39ce55 100644 --- a/packages/frontend/src/routes/imprint/+page.svelte +++ b/packages/frontend/src/routes/imprint/+page.svelte @@ -1,102 +1,97 @@
    - + -
    -
    - -
    -

    - {$_("imprint.title")} -

    -

    - {$_("imprint.description")} -

    +
    +
    + +
    +

    + {$_("imprint.title")} +

    +

    + {$_("imprint.description")} +

    +
    + + + + + + + {$_("imprint.company_information")} + + + +
    +
    +

    + {$_("imprint.company_name.title")} +

    +

    + {$_("imprint.company_name.value")} +

    +
    +

    + {$_("imprint.legal_form.title")} +

    +

    + {$_("imprint.legal_form.value")} +

    +
    +
    +

    + {$_("imprint.registration_number.title")} +

    +

    + {$_("imprint.registration_number.value")} +

    +
    +
    +

    {$_("imprint.tax_id.title")}

    +

    {$_("imprint.tax_id.value")}

    +
    +
    +
    +
    - - - - - - {$_("imprint.company_information")} - - - -
    -
    -

    - {$_("imprint.company_name.title")} -

    -

    - {$_("imprint.company_name.value")} -

    -
    -
    -

    - {$_("imprint.legal_form.title")} -

    -

    - {$_("imprint.legal_form.value")} -

    -
    -
    -

    - {$_("imprint.registration_number.title")} -

    -

    - {$_("imprint.registration_number.value")} -

    -
    -
    -

    {$_("imprint.tax_id.title")}

    -

    {$_("imprint.tax_id.value")}

    -
    -
    -
    -
    - - - - - - - {$_("imprint.contact_information")} - - - -
    -
    -

    - {$_("imprint.registered_address")} -

    -
    -

    {$_("imprint.address.company")}

    -

    {$_("imprint.address.name")}

    -

    {$_("imprint.address.street")}

    -

    {$_("imprint.address.city")}

    -

    {$_("imprint.address.country")}

    -
    -
    - + + + + + {$_("imprint.contact_information")} + + + +
    +
    +

    + {$_("imprint.registered_address")} +

    +
    +

    {$_("imprint.address.company")}

    +

    {$_("imprint.address.name")}

    +

    {$_("imprint.address.street")}

    +

    {$_("imprint.address.city")}

    +

    {$_("imprint.address.country")}

    +
    +
    + -
    +
    - + -
    -
    - -
    -

    {$_("imprint.phone.title")}

    -

    {$_("imprint.phone.value")}

    -
    -
    -
    - -
    -

    {$_("imprint.email.title")}

    -

    {$_("imprint.email.value")}

    -
    -
    -
    - -
    -

    - {$_("imprint.website.title")} -

    -

    - {$_("imprint.website.value")} -

    -
    -
    -
    -
    -
    +
    +
    + +
    +

    {$_("imprint.phone.title")}

    +

    {$_("imprint.phone.value")}

    +
    +
    +
    + +
    +

    {$_("imprint.email.title")}

    +

    {$_("imprint.email.value")}

    +
    +
    +
    + +
    +

    + {$_("imprint.website.title")} +

    +

    + {$_("imprint.website.value")} +

    +
    +
    +
    + + - - + - - + - - + - - - - {$_("imprint.disclaimer")} - - -

    - {$_("imprint.disclaimer_text.0")} -

    -

    - {$_("imprint.disclaimer_text.1")} -

    -

    - {$_("imprint.disclaimer_text.2")} -

    -
    -
    + + + + {$_("imprint.disclaimer")} + + +

    + {$_("imprint.disclaimer_text.0")} +

    +

    + {$_("imprint.disclaimer_text.1")} +

    +

    + {$_("imprint.disclaimer_text.2")} +

    +
    +
    - -
    -

    {$_("imprint.last_updated")}

    -
    -
    + +
    +

    {$_("imprint.last_updated")}

    +
    +
    diff --git a/packages/frontend/src/routes/leaderboard/+page.server.ts b/packages/frontend/src/routes/leaderboard/+page.server.ts index e3ac94a..cf819c1 100644 --- a/packages/frontend/src/routes/leaderboard/+page.server.ts +++ b/packages/frontend/src/routes/leaderboard/+page.server.ts @@ -6,55 +6,61 @@ import { getGraphQLClient } from "$lib/api"; const LEADERBOARD_QUERY = gql` query Leaderboard($limit: Int, $offset: Int) { leaderboard(limit: $limit, offset: $offset) { - user_id display_name avatar - total_weighted_points total_raw_points - recordings_count playbacks_count achievements_count rank + user_id + display_name + avatar + total_weighted_points + total_raw_points + recordings_count + playbacks_count + achievements_count + rank } } `; export const load: PageServerLoad = async ({ fetch, url, locals }) => { - // Guard: Redirect to login if not authenticated - if (!locals.authStatus.authenticated) { - throw redirect(302, "/login"); - } + // Guard: Redirect to login if not authenticated + if (!locals.authStatus.authenticated) { + throw redirect(302, "/login"); + } - try { - const limit = parseInt(url.searchParams.get("limit") || "100"); - const offset = parseInt(url.searchParams.get("offset") || "0"); + try { + const limit = parseInt(url.searchParams.get("limit") || "100"); + const offset = parseInt(url.searchParams.get("offset") || "0"); - const client = getGraphQLClient(fetch); - const data = await client.request<{ - leaderboard: { - user_id: string; - display_name: string | null; - avatar: string | null; - total_weighted_points: number | null; - total_raw_points: number | null; - recordings_count: number | null; - playbacks_count: number | null; - achievements_count: number | null; - rank: number; - }[]; - }>(LEADERBOARD_QUERY, { limit, offset }); + const client = getGraphQLClient(fetch); + const data = await client.request<{ + leaderboard: { + user_id: string; + display_name: string | null; + avatar: string | null; + total_weighted_points: number | null; + total_raw_points: number | null; + recordings_count: number | null; + playbacks_count: number | null; + achievements_count: number | null; + rank: number; + }[]; + }>(LEADERBOARD_QUERY, { limit, offset }); - return { - leaderboard: data.leaderboard || [], - pagination: { - limit, - offset, - hasMore: data.leaderboard?.length === limit, - }, - }; - } catch (error) { - console.error("Leaderboard load error:", error); - return { - leaderboard: [], - pagination: { - limit: 100, - offset: 0, - hasMore: false, - }, - }; - } + return { + leaderboard: data.leaderboard || [], + pagination: { + limit, + offset, + hasMore: data.leaderboard?.length === limit, + }, + }; + } catch (error) { + console.error("Leaderboard load error:", error); + return { + leaderboard: [], + pagination: { + limit: 100, + offset: 0, + hasMore: false, + }, + }; + } }; diff --git a/packages/frontend/src/routes/leaderboard/+page.svelte b/packages/frontend/src/routes/leaderboard/+page.svelte index 5746ede..e796667 100644 --- a/packages/frontend/src/routes/leaderboard/+page.svelte +++ b/packages/frontend/src/routes/leaderboard/+page.svelte @@ -1,192 +1,204 @@ - +
    - + -
    - -
    -
    -

    {$_("gamification.leaderboard")}

    -

    {$_("gamification.leaderboard_subtitle")}

    -
    - -
    +
    + +
    +
    +

    {$_("gamification.leaderboard")}

    +

    {$_("gamification.leaderboard_subtitle")}

    +
    + +
    - - - - - - {$_("gamification.top_players")} - - - - {#if data.leaderboard.length === 0} -
    - -

    {$_("gamification.no_rankings_yet")}

    -
    - {:else} - - - {#if data.pagination.hasMore} -
    - -
    - {/if} - {/if} -
    -
    + + {#if data.pagination.hasMore} +
    + +
    + {/if} + {/if} + + - - - -

    - - {$_("gamification.how_it_works")} -

    -

    - {$_("gamification.how_it_works_description")} -

    -
    -
    - -
    -
    {$_("gamification.earn_by_creating")}
    -
    {$_("gamification.earn_by_creating_desc")}
    -
    -
    -
    - -
    -
    {$_("gamification.earn_by_playing")}
    -
    {$_("gamification.earn_by_playing_desc")}
    -
    -
    -
    - -
    -
    {$_("gamification.stay_active")}
    -
    {$_("gamification.stay_active_desc")}
    -
    -
    -
    -
    -
    -
    + + + +

    + + {$_("gamification.how_it_works")} +

    +

    + {$_("gamification.how_it_works_description")} +

    +
    +
    + +
    +
    {$_("gamification.earn_by_creating")}
    +
    {$_("gamification.earn_by_creating_desc")}
    +
    +
    +
    + +
    +
    {$_("gamification.earn_by_playing")}
    +
    {$_("gamification.earn_by_playing_desc")}
    +
    +
    +
    + +
    +
    {$_("gamification.stay_active")}
    +
    {$_("gamification.stay_active_desc")}
    +
    +
    +
    +
    +
    +
    diff --git a/packages/frontend/src/routes/legal/+page.svelte b/packages/frontend/src/routes/legal/+page.svelte index 46be1a2..24335c5 100644 --- a/packages/frontend/src/routes/legal/+page.svelte +++ b/packages/frontend/src/routes/legal/+page.svelte @@ -1,366 +1,355 @@
    - + -
    -
    - -
    -

    - {$_("legal.title")} -

    -

    - {$_("legal.description")} -

    -
    +
    +
    + +
    +

    + {$_("legal.title")} +

    +

    + {$_("legal.description")} +

    +
    - - - - - - {$_("legal.privacy.title")} - - - - {$_("legal.terms.title")} - - - - {$_("legal.community.title")} - - - - {$_("legal.cookie.title")} - - + + + + + + {$_("legal.privacy.title")} + + + + {$_("legal.terms.title")} + + + + {$_("legal.community.title")} + + + + {$_("legal.cookie.title")} + + - - - - - - - {$_("legal.privacy.title")} - -

    - {$_("legal.privacy.last_updated")} -

    -
    - -
    -

    - {$_("legal.privacy.information.title")} -

    -
    -

    - {@html $_("legal.privacy.information.text.0")} -

    -

    - {@html $_("legal.privacy.information.text.1")} -

    - + + + + + + {$_("legal.privacy.title")} + +

    + {$_("legal.privacy.last_updated")} +

    +
    + +
    +

    + {$_("legal.privacy.information.title")} +

    +
    +

    + {@html $_("legal.privacy.information.text.0")} +

    +

    + {@html $_("legal.privacy.information.text.1")} +

    + - -
    -
    +
    +
    - + -
    -

    - {$_("legal.privacy.information_use.title")} -

    -
    -

    {$_("legal.privacy.information_use.subtitle")}

    -
      - {@html $_("legal.privacy.information_use.text.0")} -
    -
    -
    +
    +

    + {$_("legal.privacy.information_use.title")} +

    +
    +

    {$_("legal.privacy.information_use.subtitle")}

    +
      + {@html $_("legal.privacy.information_use.text.0")} +
    +
    +
    - + -
    -

    - {$_("legal.privacy.information_sharing.title")} -

    -
    -

    - {$_("legal.privacy.information_sharing.subtitle")} -

    -
      - {@html $_("legal.privacy.information_sharing.text.0")} -
    -
    -
    +
    +

    + {$_("legal.privacy.information_sharing.title")} +

    +
    +

    + {$_("legal.privacy.information_sharing.subtitle")} +

    +
      + {@html $_("legal.privacy.information_sharing.text.0")} +
    +
    +
    - + -
    -

    - {$_("legal.privacy.security.title")} -

    -

    - {@html $_("legal.privacy.security.text.0")} -

    -
    +
    +

    + {$_("legal.privacy.security.title")} +

    +

    + {@html $_("legal.privacy.security.text.0")} +

    +
    - + -
    -

    - {$_("legal.privacy.rights.title")} -

    -
    -

    {$_("legal.privacy.rights.subtitle")}

    -
      - {@html $_("legal.privacy.rights.text.0")} -
    -
    -
    -
    -
    -
    +
    +

    + {$_("legal.privacy.rights.title")} +

    +
    +

    {$_("legal.privacy.rights.subtitle")}

    +
      + {@html $_("legal.privacy.rights.text.0")} +
    +
    +
    + + + - - - - - - - {$_("legal.terms.title")} - -

    - {$_("legal.terms.last_updated")} -

    -
    - -
    -

    - {$_("legal.terms.acceptance.title")} -

    -

    - {@html $_("legal.terms.acceptance.text.0")} -

    -
    + + + + + + + {$_("legal.terms.title")} + +

    + {$_("legal.terms.last_updated")} +

    +
    + +
    +

    + {$_("legal.terms.acceptance.title")} +

    +

    + {@html $_("legal.terms.acceptance.text.0")} +

    +
    - + -
    -

    - {$_("legal.terms.age.title")} -

    -

    - {@html $_("legal.terms.age.text.0")} -

    -
    +
    +

    + {$_("legal.terms.age.title")} +

    +

    + {@html $_("legal.terms.age.text.0")} +

    +
    - + -
    -

    - {$_("legal.terms.accounts.title")} -

    -
    -

    {$_("legal.terms.accounts.subtitle")}

    -
      - {@html $_("legal.terms.accounts.text.0")} -
    -
    -
    +
    +

    + {$_("legal.terms.accounts.title")} +

    +
    +

    {$_("legal.terms.accounts.subtitle")}

    +
      + {@html $_("legal.terms.accounts.text.0")} +
    +
    +
    - + -
    -

    - {$_("legal.terms.content.title")} -

    -
    -

    - {$_("legal.terms.content.subtitle")} -

    -
      - {@html $_("legal.terms.content.text.0")} -
    -
    -
    +
    +

    + {$_("legal.terms.content.title")} +

    +
    +

    + {$_("legal.terms.content.subtitle")} +

    +
      + {@html $_("legal.terms.content.text.0")} +
    +
    +
    - + -
    -

    - {$_("legal.terms.payment.title")} -

    -
    -

    {$_("legal.terms.payment.subtitle")}

    -
      - {@html $_("legal.terms.payment.text.0")} -
    -
    -
    +
    +

    + {$_("legal.terms.payment.title")} +

    +
    +

    {$_("legal.terms.payment.subtitle")}

    +
      + {@html $_("legal.terms.payment.text.0")} +
    +
    +
    - + -
    -

    - {$_("legal.terms.termination.title")} -

    -

    - {@html $_("legal.terms.termination.text.0")} -

    -
    -
    -
    -
    +
    +

    + {$_("legal.terms.termination.title")} +

    +

    + {@html $_("legal.terms.termination.text.0")} +

    +
    +
    +
    +
    - - - - - - - {$_("legal.community.title")} - -

    - {$_("legal.community.description")} -

    -
    - -
    -

    - {$_("legal.community.values.title")} -

    -

    - {@html $_("legal.community.values.text.0")} -

    -
    + + + + + + + {$_("legal.community.title")} + +

    + {$_("legal.community.description")} +

    +
    + +
    +

    + {$_("legal.community.values.title")} +

    +

    + {@html $_("legal.community.values.text.0")} +

    +
    - + -
    -

    - {$_("legal.community.respect.title")} -

    -
    -
      - {@html $_("legal.community.respect.text.0")} -
    -
    -
    +
    +

    + {$_("legal.community.respect.title")} +

    +
    +
      + {@html $_("legal.community.respect.text.0")} +
    +
    +
    - + -
    -

    - {$_("legal.community.standards.title")} -

    -
    -
      - {@html $_("legal.community.standards.text.0")} -
    -
    -
    +
    +

    + {$_("legal.community.standards.title")} +

    +
    +
      + {@html $_("legal.community.standards.text.0")} +
    +
    +
    - + -
    -

    - {$_("legal.community.interaction.title")} -

    -
    -
      - {@html $_("legal.community.interaction.text.0")} -
    -
    -
    +
    +

    + {$_("legal.community.interaction.title")} +

    +
    +
      + {@html $_("legal.community.interaction.text.0")} +
    +
    +
    - + -
    -

    - {$_("legal.community.enforcement.title")} -

    -

    - {@html $_("legal.community.enforcement.text.0")} -

    -
    -
    -
    -
    +
    +

    + {$_("legal.community.enforcement.title")} +

    +

    + {@html $_("legal.community.enforcement.text.0")} +

    +
    +
    +
    +
    - - - - - - - {$_("legal.cookie.title")} - -

    - {$_("legal.cookie.description")} -

    -
    - -
    -

    - {$_("legal.cookie.what.title")} -

    -

    - {@html $_("legal.cookie.what.text.0")} -

    -
    + + + + + + + {$_("legal.cookie.title")} + +

    + {$_("legal.cookie.description")} +

    +
    + +
    +

    + {$_("legal.cookie.what.title")} +

    +

    + {@html $_("legal.cookie.what.text.0")} +

    +
    - + -
    -

    - {$_("legal.cookie.types.title")} -

    -
    -
    -

    - {$_("legal.cookie.types.essential.title")} -

    -

    - {@html $_("legal.cookie.types.essential.text.0")} -

    -
    - -
    -
    +
    +
    - + -
    -

    - {$_("legal.cookie.managing.title")} -

    -
    -

    {$_("legal.cookie.managing.subtitle")}

    -
      - {@html $_("legal.cookie.managing.text.0")} -
    -

    - {@html $_("legal.cookie.managing.text.1")} -

    -
    -
    +
    +

    + {$_("legal.cookie.managing.title")} +

    +
    +

    {$_("legal.cookie.managing.subtitle")}

    +
      + {@html $_("legal.cookie.managing.text.0")} +
    +

    + {@html $_("legal.cookie.managing.text.1")} +

    +
    +
    - + -
    -

    - {$_("legal.cookie.third_party.title")} -

    -

    - {@html $_("legal.cookie.third_party.text.0")} -

    -
    - - - - +
    +

    + {$_("legal.cookie.third_party.title")} +

    +

    + {@html $_("legal.cookie.third_party.text.0")} +

    +
    + + + + - - - -

    - {$_("legal.questions")} -

    -

    - {$_("legal.questions_description")} -

    - {$_("legal.questions_email")} -
    -
    -
    + + + +

    + {$_("legal.questions")} +

    +

    + {$_("legal.questions_description")} +

    + {$_("legal.questions_email")} +
    +
    +
    diff --git a/packages/frontend/src/routes/login/+page.server.ts b/packages/frontend/src/routes/login/+page.server.ts index c94572d..ea7326b 100644 --- a/packages/frontend/src/routes/login/+page.server.ts +++ b/packages/frontend/src/routes/login/+page.server.ts @@ -1,5 +1,5 @@ export async function load({ locals }) { - return { - authStatus: locals.authStatus, - }; + return { + authStatus: locals.authStatus, + }; } diff --git a/packages/frontend/src/routes/login/+page.svelte b/packages/frontend/src/routes/login/+page.svelte index 032e5b1..57ae856 100644 --- a/packages/frontend/src/routes/login/+page.svelte +++ b/packages/frontend/src/routes/login/+page.svelte @@ -1,163 +1,163 @@
    - + -
    - -
    -
    - - +
    + +
    +
    + +
    +

    {$_("auth.login.welcome")}

    +
    + + + + {$_("auth.login.title")} + {$_("auth.login.description")} + + +
    + +
    + + +
    + + +
    + +
    + +
    -

    {$_("auth.login.welcome")}

    -
    +
    - - - {$_("auth.login.title")} - {$_("auth.login.description")} - - - - -
    - - -
    - - -
    - -
    - - -
    -
    - - - - {#if isError} -
    - - {$_("auth.login.error")} - {error} - -
    - {/if} + {#if isError} +
    + + {$_( + "auth.login.error", + )} + {error} + +
    + {/if} - - - + + + - - + - - + - -
    -

    - {$_("auth.login.no_account")} - {$_("auth.login.sign_up_link")} -

    -
    -
    -
    -
    + +
    +

    + {$_("auth.login.no_account")} + {$_("auth.login.sign_up_link")} +

    +
    + + +
    diff --git a/packages/frontend/src/routes/magazine/+page.server.ts b/packages/frontend/src/routes/magazine/+page.server.ts index 51a648a..d06414e 100644 --- a/packages/frontend/src/routes/magazine/+page.server.ts +++ b/packages/frontend/src/routes/magazine/+page.server.ts @@ -1,6 +1,6 @@ import { getArticles } from "$lib/services"; export async function load({ fetch }) { - return { - articles: await getArticles(fetch), - }; + return { + articles: await getArticles(fetch), + }; } diff --git a/packages/frontend/src/routes/magazine/+page.svelte b/packages/frontend/src/routes/magazine/+page.svelte index 3bb9815..6a8b3f6 100644 --- a/packages/frontend/src/routes/magazine/+page.svelte +++ b/packages/frontend/src/routes/magazine/+page.svelte @@ -1,63 +1,51 @@ - +
    {

    - {$_('magazine.title')} + {$_("magazine.title")}

    - {$_('magazine.description')} + {$_("magazine.description")}

    @@ -99,7 +87,7 @@ const filteredArticles = $derived(() => { class="icon-[ri--search-line] absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" > @@ -111,42 +99,28 @@ const filteredArticles = $derived(() => { class="w-full md:w-48 bg-background/50 border-primary/20 focus:border-primary" > - {categoryFilter === 'all' - ? $_('magazine.categories.all') - : categoryFilter === 'photography' - ? $_('magazine.categories.photography') - : categoryFilter === 'production' - ? $_('magazine.categories.production') - : categoryFilter === 'interview' - ? $_('magazine.categories.interview') - : categoryFilter === 'psychology' - ? $_('magazine.categories.psychology') - : categoryFilter === 'trends' - ? $_('magazine.categories.trends') - : $_('magazine.categories.spotlight')} + {categoryFilter === "all" + ? $_("magazine.categories.all") + : categoryFilter === "photography" + ? $_("magazine.categories.photography") + : categoryFilter === "production" + ? $_("magazine.categories.production") + : categoryFilter === "interview" + ? $_("magazine.categories.interview") + : categoryFilter === "psychology" + ? $_("magazine.categories.psychology") + : categoryFilter === "trends" + ? $_("magazine.categories.trends") + : $_("magazine.categories.spotlight")} - {$_('magazine.categories.all')} - {$_('magazine.categories.photography')} - {$_('magazine.categories.production')} - {$_('magazine.categories.interview')} - {$_('magazine.categories.psychology')} - {$_('magazine.categories.trends')} - {$_('magazine.categories.spotlight')} + {$_("magazine.categories.all")} + {$_("magazine.categories.photography")} + {$_("magazine.categories.production")} + {$_("magazine.categories.interview")} + {$_("magazine.categories.psychology")} + {$_("magazine.categories.trends")} + {$_("magazine.categories.spotlight")} @@ -155,25 +129,21 @@ const filteredArticles = $derived(() => { - {sortBy === 'recent' - ? $_('magazine.sort.recent') - : sortBy === 'popular' - ? $_('magazine.sort.popular') - : sortBy === 'featured' - ? $_('magazine.sort.featured') - : $_('magazine.sort.name')} + {sortBy === "recent" + ? $_("magazine.sort.recent") + : sortBy === "popular" + ? $_("magazine.sort.popular") + : sortBy === "featured" + ? $_("magazine.sort.featured") + : $_("magazine.sort.name")} - {$_('magazine.sort.recent')} + {$_("magazine.sort.recent")} - {$_('magazine.sort.featured')} - {$_('magazine.sort.name')} + {$_("magazine.sort.featured")} + {$_("magazine.sort.name")}
    @@ -183,21 +153,21 @@ const filteredArticles = $derived(() => {
    - {#if featuredArticle && categoryFilter === 'all' && !searchQuery} + {#if featuredArticle && categoryFilter === "all" && !searchQuery}
    {featuredArticle.title}
    - {$_('magazine.featured')} + {$_("magazine.featured")}
    @@ -208,13 +178,9 @@ const filteredArticles = $derived(() => { {featuredArticle.category}
    -

    +

    @@ -223,26 +189,20 @@ const filteredArticles = $derived(() => {

    {featuredArticle.author.first_name}

    {featuredArticle.author.first_name}

    -
    - {timeAgo.format( - new Date(featuredArticle.publish_date) - )} +
    + {timeAgo.format(new Date(featuredArticle.publish_date))} {$_('magazine.read_time', { + >{$_("magazine.read_time", { values: { - time: calcReadingTime(featuredArticle.content) - } + time: calcReadingTime(featuredArticle.content), + }, })}
    @@ -250,8 +210,7 @@ const filteredArticles = $derived(() => {
    {$_("magazine.read_article")}
    @@ -267,7 +226,7 @@ const filteredArticles = $derived(() => { >
    {article.title} @@ -287,7 +246,7 @@ const filteredArticles = $derived(() => {
    - {$_('magazine.featured')} + {$_("magazine.featured")}
    {/if} @@ -307,9 +266,7 @@ const filteredArticles = $derived(() => { > {article.title} -

    +

    {article.excerpt}

    @@ -330,23 +287,21 @@ const filteredArticles = $derived(() => {
    {article.author.first_name}

    {article.author.first_name}

    -
    +
    {timeAgo.format(new Date(article.publish_date))}
    - {$_('magazine.read_time', { - values: { time: calcReadingTime(article.content) } + {$_("magazine.read_time", { + values: { time: calcReadingTime(article.content) }, })}
    @@ -356,8 +311,7 @@ const filteredArticles = $derived(() => { variant="outline" size="sm" class="w-full mt-4 border-primary/20 hover:bg-primary/10" - href="/magazine/{article.slug}" - >{$_('magazine.read_article')}{$_("magazine.read_article")} @@ -367,17 +321,17 @@ const filteredArticles = $derived(() => { {#if filteredArticles().length === 0}

    - {$_('magazine.no_results')} + {$_("magazine.no_results")}

    {/if} diff --git a/packages/frontend/src/routes/magazine/[slug]/+page.server.ts b/packages/frontend/src/routes/magazine/[slug]/+page.server.ts index 4245a28..a2165a3 100644 --- a/packages/frontend/src/routes/magazine/[slug]/+page.server.ts +++ b/packages/frontend/src/routes/magazine/[slug]/+page.server.ts @@ -1,12 +1,12 @@ import { error } from "@sveltejs/kit"; import { getArticleBySlug } from "$lib/services.js"; export async function load({ fetch, params, locals }) { - try { - return { - article: await getArticleBySlug(params.slug, fetch), - authStatus: locals.authStatus, - }; - } catch { - error(404, "Article not found"); - } + try { + return { + article: await getArticleBySlug(params.slug, fetch), + authStatus: locals.authStatus, + }; + } catch { + error(404, "Article not found"); + } } diff --git a/packages/frontend/src/routes/magazine/[slug]/+page.svelte b/packages/frontend/src/routes/magazine/[slug]/+page.svelte index 5c8db4a..435626d 100644 --- a/packages/frontend/src/routes/magazine/[slug]/+page.svelte +++ b/packages/frontend/src/routes/magazine/[slug]/+page.svelte @@ -1,86 +1,84 @@
    - + -
    -
    - -
    - -
    - +
    +
    + +
    + +
    + - -
    - - {data.article.category} - -
    + +
    + + {data.article.category} + +
    - -

    - {data.article.title} -

    + +

    + {data.article.title} +

    - -

    - {data.article.excerpt} -

    + +

    + {data.article.excerpt} +

    - -
    -
    - - {timeAgo.format(new Date(data.article.publish_date))} -
    -
    - - {$_("magazine.read_time", { - values: { - time: calcReadingTime(data.article.content), - }, - })} -
    - +
    +
    + + {timeAgo.format(new Date(data.article.publish_date))} +
    +
    + + {$_("magazine.read_time", { + values: { + time: calcReadingTime(data.article.content), + }, + })} +
    + -
    +
    - - + - - -
    + +
    - -
    - {data.article.title} -
    + +
    + {data.article.title} +
    - - - -
    - {@html data.article.content} -
    -
    -
    + + + +
    + {@html data.article.content} +
    +
    +
    - -
    -
    - - Tags -
    -
    - {#each data.article.tags as tag (tag)} - - #{tag} - - {/each} -
    -
    + +
    +
    + + Tags +
    +
    + {#each data.article.tags as tag (tag)} + + #{tag} + + {/each} +
    +
    - - - -
    - {data.article.author.first_name} -
    -

    - About {data.article.author.first_name} -

    - {#if data.article.author.description} -

    - {data.article.author.description} -

    - {/if} - {#if data.article.author.website} -
    - - {data.article.author.website} - - + + +
    + {data.article.author.first_name} +
    +

    + About {data.article.author.first_name} +

    + {#if data.article.author.description} +

    + {data.article.author.description} +

    + {/if} + {#if data.article.author.website} + - {/if} -
    -
    -
    -
    -
    +
    + {/if} +
    +
    + + + - -
    + + +
    +
    diff --git a/packages/frontend/src/routes/me/+page.server.ts b/packages/frontend/src/routes/me/+page.server.ts index 075464a..60b69a9 100644 --- a/packages/frontend/src/routes/me/+page.server.ts +++ b/packages/frontend/src/routes/me/+page.server.ts @@ -3,23 +3,23 @@ import { getAnalytics, getFolders, getRecordings } from "$lib/services"; import { isModel } from "$lib/directus"; export async function load({ locals, fetch }) { - // Redirect to login if not authenticated - if (!locals.authStatus.authenticated) { - throw redirect(302, "/login"); - } + // Redirect to login if not authenticated + if (!locals.authStatus.authenticated) { + throw redirect(302, "/login"); + } - const recordings = await getRecordings(fetch).catch(() => []); + const recordings = await getRecordings(fetch).catch(() => []); - const analytics = isModel(locals.authStatus.user) - ? await getAnalytics(fetch).catch(() => null) - : null; + const analytics = isModel(locals.authStatus.user) + ? await getAnalytics(fetch).catch(() => null) + : null; - const folders = await getFolders(fetch).catch(() => []); + const folders = await getFolders(fetch).catch(() => []); - return { - authStatus: locals.authStatus, - folders, - recordings, - analytics, - }; + return { + authStatus: locals.authStatus, + folders, + recordings, + analytics, + }; } diff --git a/packages/frontend/src/routes/me/+page.svelte b/packages/frontend/src/routes/me/+page.svelte index 12be37d..f3f3853 100644 --- a/packages/frontend/src/routes/me/+page.svelte +++ b/packages/frontend/src/routes/me/+page.svelte @@ -1,659 +1,643 @@
    - -
    - -
    -
    -
    -

    - {$_("me.title")} -

    -

    - {$_("me.welcome", { - values: { name: data.authStatus.user!.artist_name }, - })} -

    + +
    + +
    +
    +
    +

    + {$_("me.title")} +

    +

    + {$_("me.welcome", { + values: { name: data.authStatus.user!.artist_name }, + })} +

    +
    + {#if isModel(data.authStatus.user!)} + + {/if} +
    +
    + + + + + + + {$_("me.settings.title")} + + + + {$_("me.recordings.title")} + + {#if data.analytics} + + + Analytics + + {/if} + + + + +
    + + + + {$_("me.settings.profile_title")} + {$_("me.settings.profile_subtitle")} + + +
    +
    + + + {#if avatar} +
    +
    +
    + {avatar.name} +
    +
    + {avatar.name} + {displaySize(avatar.size)} +
    +
    +
    + +
    +
    + {/if}
    - {#if isModel(data.authStatus.user!)} - + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + +