From 8c9e3e1adb654aeb0774c59114c6b8e414fbc3eb Mon Sep 17 00:00:00 2001 From: OmarMcAdam Date: Mon, 24 Mar 2025 13:01:51 -0700 Subject: [PATCH 01/18] add nextjs docker build stuffs --- apps/web/next.config.mjs | 2 ++ apps/web/package.json | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 5c7a0035a..5c0a90a36 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -100,6 +100,8 @@ const nextConfig = { env: { appVersion: version, }, + // If the DOCKER_BUILD environment variable is set to true, we are output nextjs to standalone ready for docker deployment + output: process.env.DOCKER_BUILD === "true" ? "standalone" : undefined, }; export default nextConfig; diff --git a/apps/web/package.json b/apps/web/package.json index a7b4a012e..f60d37336 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "dotenv -e ../../.env -- next dev", "build": "next build", + "build:docker": "DOCKER_BUILD=true next build", "start": "next start", "lint": "next lint" }, @@ -99,4 +100,4 @@ "engines": { "node": "20" } -} +} \ No newline at end of file From e8963afd0d7e18af23a9aa57b45fbe39eb975289 Mon Sep 17 00:00:00 2001 From: OmarMcAdam Date: Wed, 26 Mar 2025 17:37:06 -0700 Subject: [PATCH 02/18] docker image build setup --- .npmrc | 3 +- Dockerfile | 82 ++ .../app/api/selfhosted/migrations/route.ts | 51 + apps/web/instrumentation.ts | 51 + apps/web/next.config.mjs | 11 +- apps/web/package.json | 3 +- docker-compose.template.yml | 86 ++ package.json | 4 +- packages/database/.gitignore | 1 - packages/database/auth/auth-options.ts | 7 +- packages/database/index.ts | 5 +- .../migrations/0000_brown_sunfire.sql | 191 +++ .../migrations/meta/0000_snapshot.json | 1264 +++++++++++++++++ .../database/migrations/meta/_journal.json | 13 + pnpm-lock.yaml | 494 ++++++- turbo.json | 62 +- 16 files changed, 2260 insertions(+), 68 deletions(-) create mode 100644 Dockerfile create mode 100644 apps/web/app/api/selfhosted/migrations/route.ts create mode 100644 apps/web/instrumentation.ts create mode 100644 docker-compose.template.yml create mode 100644 packages/database/migrations/0000_brown_sunfire.sql create mode 100644 packages/database/migrations/meta/0000_snapshot.json create mode 100644 packages/database/migrations/meta/_journal.json diff --git a/.npmrc b/.npmrc index 1560b99cc..bb7b7ccd8 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -auto-install-peers = true \ No newline at end of file +auto-install-peers = true +inject-workspace-packages=true \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..bab501913 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,82 @@ +# syntax=docker.io/docker/dockerfile:1 + +FROM node:20-alpine AS base +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + + +# 1. Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat + + +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ +COPY /patches ./patches +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \ + else echo "Lockfile not found." && exit 1; \ + fi + + +# 2. Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/patches ./patches +COPY . . + +# build-time only variables +ARG DOCKER_BUILD=true + +# Build and runtime variables +ENV DOCKER_BUILD=true +ENV DATABASE_ENCRYPTION_KEY=8439f729756f4d591032e9d4a1dd366423581a82af0c191187582a39aab935f6 +ENV NEXTAUTH_SECRET=8439f729756f4d591032e9d4a1dd366423581a82af0c191187582a39aab935f6 +ENV NODE_ENV=production +ENV PORT=3000 +ENV NEXT_PUBLIC_WEB_URL=http://localhost:3000 +ENV NEXTAUTH_URL=${NEXT_PUBLIC_WEB_URL} +ENV DATABASE_URL=mysql://root:@localhost:3306/planetscale +ENV DATABASE_MIGRATION_URL=mysql://root:@localhost:3306/planetscale +ENV CAP_AWS_ACCESS_KEY=capS3root +ENV CAP_AWS_SECRET_KEY=capS3root +ENV NEXT_PUBLIC_CAP_AWS_BUCKET=capso +ENV NEXT_PUBLIC_CAP_AWS_REGION=us-east-1 +ENV NEXT_PUBLIC_CAP_AWS_ENDPOINT=http://localhost:3902 + +ENV GOOGLE_CLIENT_ID="" +ENV GOOGLE_CLIENT_SECRET="" +ENV RESEND_API_KEY="" +ENV DEEPGRAM_API_KEY="" + + +RUN corepack enable pnpm && pnpm run build:web + +# 3. Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nextjs -u 1001 + + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static +COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public +COPY --from=builder --chown=nextjs:nodejs /app/packages/database/migrations ./apps/web/migrations + + +USER nextjs + +EXPOSE 3000 + +CMD HOSTNAME="0.0.0.0" node apps/web/server.js \ No newline at end of file diff --git a/apps/web/app/api/selfhosted/migrations/route.ts b/apps/web/app/api/selfhosted/migrations/route.ts new file mode 100644 index 000000000..023f13a48 --- /dev/null +++ b/apps/web/app/api/selfhosted/migrations/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from "next/server"; +import { db } from "@cap/database"; +import { migrate } from "drizzle-orm/mysql2/migrator"; +import path from "path"; + +const migrations = { + run: false, +}; + +export async function POST(request: Request) { + if (process.env.NEXT_RUNTIME === "nodejs") { + if (migrations.run) { + console.log(" ✅ DB migrations triggered but already run, skipping"); + return NextResponse.json({ + message: "✅ DB migrations already run, skipping", + }); + } + + const isDockerBuild = process.env.DOCKER_BUILD === "true"; + if (isDockerBuild) { + try { + console.log("🔍 DB migrations triggered"); + console.log("💿 Running DB migrations..."); + const cwd = process.cwd(); + // @ts-ignore + await migrate(db, { + migrationsFolder: path.join(process.cwd(), "/migrations"), + }); + migrations.run = true; + console.log("💿 Migrations run successfully!"); + return NextResponse.json({ + message: "✅ DB migrations run successfully!", + }); + } catch (error) { + console.error("🚨 MIGRATION_FAILED", { error }); + return NextResponse.json( + { + message: "🚨 DB migrations failed", + error: error instanceof Error ? error.message : String(error), + }, + { status: 500 } + ); + } + } + } + migrations.run = true; + + return NextResponse.json({ + message: "DB migrations dont need to run in this environment", + }); +} diff --git a/apps/web/instrumentation.ts b/apps/web/instrumentation.ts new file mode 100644 index 000000000..2cca2d5aa --- /dev/null +++ b/apps/web/instrumentation.ts @@ -0,0 +1,51 @@ +// This file is used to run database migrations in the docker builds or other self hosting environments. +// It is not suitable (a.k.a DEADLY) for serverless environments where the server will be restarted on each request. + +export async function register() { + if (process.env.NEXT_RUNTIME === "nodejs") { + console.log("Waiting 5 seconds to run database migrations"); + + // Function to trigger migrations with retry logic + const triggerMigrations = async (retryCount = 0, maxRetries = 3) => { + try { + const response = await fetch( + "http://localhost:3000/api/selfhosted/migrations", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + } + ); + + // This will throw an error if the response status is not ok + response.ok || + (await Promise.reject(new Error(`HTTP error ${response.status}`))); + + const responseData = await response.json(); + console.log( + "✅ Migrations triggered successfully:", + responseData.message + ); + } catch (error) { + console.error( + `🚨 Error triggering migrations (attempt ${retryCount + 1}):`, + error + ); + if (retryCount < maxRetries - 1) { + console.log( + `🔄 Retrying in 5 seconds... (${retryCount + 1}/${maxRetries})` + ); + setTimeout(() => triggerMigrations(retryCount + 1, maxRetries), 5000); + } else { + console.error(`🚨 All ${maxRetries} migration attempts failed.`); + process.exit(1); // Exit with error code if all attempts fail + } + } + }; + + // Add a timeout to trigger migrations after 5 seconds on server start + setTimeout(() => triggerMigrations(), 5000); + } + return; +} diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 5c0a90a36..af4f1c2b1 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -21,6 +21,7 @@ const nextConfig = { ignoreBuildErrors: true, }, experimental: { + instrumentationHook: true, optimizePackageImports: ["@cap/ui", "@cap/utils", "@cap/web-api-contract"], serverComponentsExternalPackages: [ "@react-email/components", @@ -61,15 +62,15 @@ const nextConfig = { destination: "https://l.cap.so/api/commercial/:path*", }, { - source: '/s/:videoId', - destination: '/s/:videoId', + source: "/s/:videoId", + destination: "/s/:videoId", has: [ { - type: 'host', - value: '(?!cap\.so|cap\.link).*', + type: "host", + value: "(?!cap.so|cap.link).*", }, ], - } + }, ]; }, async redirects() { diff --git a/apps/web/package.json b/apps/web/package.json index f60d37336..a1b428d69 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,7 +5,8 @@ "scripts": { "dev": "dotenv -e ../../.env -- next dev", "build": "next build", - "build:docker": "DOCKER_BUILD=true next build", + "build:web": "next build", + "build:web:docker": "cd ../.. && docker build -t cap-web-docker . --no-cache --progress=plain", "start": "next start", "lint": "next lint" }, diff --git a/docker-compose.template.yml b/docker-compose.template.yml new file mode 100644 index 000000000..100269d10 --- /dev/null +++ b/docker-compose.template.yml @@ -0,0 +1,86 @@ +name: cap-so-docker-template + +# THIS IS A TEMPLATE FOR DOCKER COMPOSE FILES +# IT IS NOT MEANT FOR PRODUCTION DEPLOYMENT WITHOUT MODIFICATIONS TO ENVIRONMENT VARIABLES +# IT IS MEANT FOR LOCAL EVALUATION AND DEVELOPMENT PURPOSES ONLY + +services: + cap-web: + container_name: cap-web + image: cap-web-docker:latest + restart: unless-stopped + environment: + DATABASE_URL: 'mysql://root:@ps-mysql:3306/planetscale?ssl={"rejectUnauthorized":false}' + DATABASE_MIGRATION_URL: 'mysql://root:@ps-mysql:3306/planetscale?ssl={"rejectUnauthorized":false}' + NEXTAUTH_URL: http://localhost:3000 + DOCKER_BUILD: true + # CHANGE THESE TO YOUR OWN VALUES + DATABASE_ENCRYPTION_KEY: c7a1e98e1e5e4cec5a3fbf5eaf502d262fb99807ae2be8ee70537409e29cb6f9 + NEXTAUTH_SECRET: c7a1e98e1e5e4cec5a3fbf5eaf502d262fb99807ae2be8ee70537409e29cb6f9 + # CHANGE THESE TO MATCH YOUR MINIO or S3 STORAGE SETUP + CAP_AWS_ACCESS_KEY: capS3root + CAP_AWS_SECRET_KEY: capS3root + CAP_AWS_BUCKET: capso + CAP_AWS_REGION: us-east-1 + CAP_AWS_ENDPOINT: http://minio:3902 + ports: + - 3000:3000 + + ps-mysql: + container_name: mysql-primary-db + image: mysql:8.0 + restart: unless-stopped + environment: + # CHANGE THESE TO YOUR OWN VALUES - BE SURE TO SET A PASSWORD FOR THE ROOT USER + MYSQL_DATABASE: planetscale + MYSQL_ROOT_HOST: "%" + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + command: + [ + "--max_connections=1000", + "--default-authentication-plugin=mysql_native_password", + ] + expose: + - 3306 + volumes: + - ps-mysql:/var/lib/mysql + + # Local S3 Strorage + minio: + container_name: minio-storage + image: "bitnami/minio:latest" + restart: unless-stopped + ports: + - "3902:3902" + - "3903:3903" + environment: + - MINIO_API_PORT_NUMBER=3902 + - MINIO_CONSOLE_PORT_NUMBER=3903 + # CHANGE THESE TO YOUR OWN VALUES + - MINIO_ROOT_USER=capS3root + - MINIO_ROOT_PASSWORD=capS3root + volumes: + - minio-data:/bitnami/minio/data + - minio-certs:/certs + + # DISABLE THIS IF YOU ARE NOT USING MINIO VIA THIS DOCKER COMPOSE FILE + createbuckets: + container_name: minio-bucket-creation + image: minio/mc + depends_on: + - minio + # CHANGE THE ENTRYPIONT "/usr/bin/mc alias set myminio http://minio:3902 capS3root capS3root;" TO USE MINIO USERNAME AND PASSWORD. + entrypoint: > + /bin/sh -c " + sleep 10; + /usr/bin/mc alias set myminio http://minio:3902 capS3root capS3root; + /usr/bin/mc mb myminio/capso; + echo '{\"Version\": \"2012-10-17\",\"Statement\": [{\"Effect\": \"Allow\",\"Principal\": {\"AWS\": [\"*\"]},\"Action\": [\"s3:GetObject\"],\"Resource\": [\"arn:aws:s3:::capso/*\"]}]}' > /tmp/policy.json; + /usr/bin/mc anonymous set-json /tmp/policy.json myminio/capso; + exit 0; + " +volumes: + ps-mysql: + # REMOVE THESE IF YOU ARE NOT USING MINIO VIA THIS DOCKER COMPOSE FILE + minio-data: + minio-certs: diff --git a/package.json b/package.json index 9e84ea62f..43548fc63 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,8 @@ "type": "module", "scripts": { "build": "dotenv -e .env -- turbo run build", + "build:web": "turbo run build:web", + "build:web:docker": "turbo run build:web:docker", "db:studio": "dotenv -e .env -- pnpm --dir packages/database db:studio", "dev": "(pnpm run docker:up > /dev/null &) && sleep 5 && trap 'pnpm run docker:stop' EXIT && dotenv -e .env -- turbo run dev --env-mode=loose --ui tui", "dev:windows": "start /b cmd /c \"pnpm run docker:up > nul\" && timeout /t 5 /nobreak > nul && dotenv -e .env -- turbo run dev --env-mode=loose --ui tui", @@ -38,4 +40,4 @@ "@kobalte/core@0.13.7": "patches/@kobalte__core@0.13.7.patch" } } -} +} \ No newline at end of file diff --git a/packages/database/.gitignore b/packages/database/.gitignore index 7598f9f53..e69de29bb 100644 --- a/packages/database/.gitignore +++ b/packages/database/.gitignore @@ -1 +0,0 @@ -/migrations \ No newline at end of file diff --git a/packages/database/auth/auth-options.ts b/packages/database/auth/auth-options.ts index ed9134ef0..c55ddcd47 100644 --- a/packages/database/auth/auth-options.ts +++ b/packages/database/auth/auth-options.ts @@ -57,8 +57,11 @@ export const authOptions: NextAuthOptions = { }), EmailProvider({ sendVerificationRequest({ identifier, url }) { - console.log({ NODE_ENV }); - if (NODE_ENV === "development") { + if ( + NODE_ENV === "development" || + !serverEnv.RESEND_API_KEY || + serverEnv.RESEND_API_KEY === "" + ) { console.log(`Login link: ${url}`); } else { sendEmail({ diff --git a/packages/database/index.ts b/packages/database/index.ts index f633f34f9..f7ccd21e2 100644 --- a/packages/database/index.ts +++ b/packages/database/index.ts @@ -1,12 +1,12 @@ import { drizzle } from "drizzle-orm/planetscale-serverless"; import { Client, Config } from "@planetscale/database"; -import { NODE_ENV, serverEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; const URL = serverEnv.DATABASE_URL; let fetchHandler: Promise | undefined = undefined; -if (NODE_ENV === "development" && URL.startsWith("mysql://")) { +if (URL.startsWith("mysql://")) { fetchHandler = import("@mattrax/mysql-planetscale").then((m) => m.createFetchHandler(URL) ); @@ -14,6 +14,7 @@ if (NODE_ENV === "development" && URL.startsWith("mysql://")) { export const connection = new Client({ url: URL, + fetch: async (input, init) => { return await ((await fetchHandler) || fetch)(input, init); }, diff --git a/packages/database/migrations/0000_brown_sunfire.sql b/packages/database/migrations/0000_brown_sunfire.sql new file mode 100644 index 000000000..fdba906d5 --- /dev/null +++ b/packages/database/migrations/0000_brown_sunfire.sql @@ -0,0 +1,191 @@ +CREATE TABLE `accounts` ( + `id` varchar(15) NOT NULL, + `userId` varchar(15) NOT NULL, + `type` varchar(255) NOT NULL, + `provider` varchar(255) NOT NULL, + `providerAccountId` varchar(255) NOT NULL, + `access_token` text, + `expires_in` int, + `id_token` text, + `refresh_token` text, + `refresh_token_expires_in` int, + `scope` varchar(255), + `token_type` varchar(255), + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + `tempColumn` text, + CONSTRAINT `accounts_id` PRIMARY KEY(`id`), + CONSTRAINT `accounts_id_unique` UNIQUE(`id`) +); +--> statement-breakpoint +CREATE TABLE `comments` ( + `id` varchar(15) NOT NULL, + `type` varchar(6) NOT NULL, + `content` text NOT NULL, + `timestamp` float, + `authorId` varchar(15) NOT NULL, + `videoId` varchar(15) NOT NULL, + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + `parentCommentId` varchar(15), + CONSTRAINT `comments_id` PRIMARY KEY(`id`), + CONSTRAINT `comments_id_unique` UNIQUE(`id`) +); +--> statement-breakpoint +CREATE TABLE `s3_buckets` ( + `id` varchar(15) NOT NULL, + `ownerId` varchar(15) NOT NULL, + `region` text NOT NULL, + `endpoint` text, + `bucketName` text NOT NULL, + `accessKeyId` text NOT NULL, + `secretAccessKey` text NOT NULL, + `provider` text NOT NULL DEFAULT ('aws'), + CONSTRAINT `s3_buckets_id` PRIMARY KEY(`id`), + CONSTRAINT `s3_buckets_id_unique` UNIQUE(`id`) +); +--> statement-breakpoint +CREATE TABLE `sessions` ( + `id` varchar(15) NOT NULL, + `sessionToken` varchar(255) NOT NULL, + `userId` varchar(15) NOT NULL, + `expires` datetime NOT NULL, + `created_at` timestamp NOT NULL DEFAULT (now()), + `updated_at` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `sessions_id` PRIMARY KEY(`id`), + CONSTRAINT `sessions_id_unique` UNIQUE(`id`), + CONSTRAINT `sessions_sessionToken_unique` UNIQUE(`sessionToken`), + CONSTRAINT `session_token_idx` UNIQUE(`sessionToken`) +); +--> statement-breakpoint +CREATE TABLE `shared_videos` ( + `id` varchar(15) NOT NULL, + `videoId` varchar(15) NOT NULL, + `spaceId` varchar(15) NOT NULL, + `sharedByUserId` varchar(15) NOT NULL, + `sharedAt` timestamp NOT NULL DEFAULT (now()), + CONSTRAINT `shared_videos_id` PRIMARY KEY(`id`), + CONSTRAINT `shared_videos_id_unique` UNIQUE(`id`) +); +--> statement-breakpoint +CREATE TABLE `space_invites` ( + `id` varchar(15) NOT NULL, + `spaceId` varchar(15) NOT NULL, + `invitedEmail` varchar(255) NOT NULL, + `invitedByUserId` varchar(15) NOT NULL, + `role` varchar(255) NOT NULL, + `status` varchar(255) NOT NULL DEFAULT 'pending', + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + `expiresAt` timestamp, + CONSTRAINT `space_invites_id` PRIMARY KEY(`id`), + CONSTRAINT `space_invites_id_unique` UNIQUE(`id`) +); +--> statement-breakpoint +CREATE TABLE `space_members` ( + `id` varchar(15) NOT NULL, + `userId` varchar(15) NOT NULL, + `spaceId` varchar(15) NOT NULL, + `role` varchar(255) NOT NULL, + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `space_members_id` PRIMARY KEY(`id`), + CONSTRAINT `space_members_id_unique` UNIQUE(`id`) +); +--> statement-breakpoint +CREATE TABLE `spaces` ( + `id` varchar(15) NOT NULL, + `name` varchar(255) NOT NULL, + `ownerId` varchar(15) NOT NULL, + `metadata` json, + `allowedEmailDomain` varchar(255), + `customDomain` varchar(255), + `domainVerified` timestamp, + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + `workosOrganizationId` varchar(255), + `workosConnectionId` varchar(255), + CONSTRAINT `spaces_id` PRIMARY KEY(`id`), + CONSTRAINT `spaces_id_unique` UNIQUE(`id`) +); +--> statement-breakpoint +CREATE TABLE `users` ( + `id` varchar(15) NOT NULL, + `name` varchar(255), + `lastName` varchar(255), + `email` varchar(255) NOT NULL, + `emailVerified` timestamp, + `image` varchar(255), + `stripeCustomerId` varchar(255), + `stripeSubscriptionId` varchar(255), + `thirdPartyStripeSubscriptionId` varchar(255), + `stripeSubscriptionStatus` varchar(255), + `stripeSubscriptionPriceId` varchar(255), + `activeSpaceId` varchar(15), + `created_at` timestamp NOT NULL DEFAULT (now()), + `updated_at` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + `onboarding_completed_at` timestamp, + `customBucket` varchar(15), + `inviteQuota` int NOT NULL DEFAULT 1, + CONSTRAINT `users_id` PRIMARY KEY(`id`), + CONSTRAINT `users_id_unique` UNIQUE(`id`), + CONSTRAINT `users_email_unique` UNIQUE(`email`), + CONSTRAINT `email_idx` UNIQUE(`email`) +); +--> statement-breakpoint +CREATE TABLE `verification_tokens` ( + `identifier` varchar(255) NOT NULL, + `token` varchar(255) NOT NULL, + `expires` datetime NOT NULL, + `created_at` timestamp NOT NULL DEFAULT (now()), + `updated_at` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `verification_tokens_identifier` PRIMARY KEY(`identifier`), + CONSTRAINT `verification_tokens_token_unique` UNIQUE(`token`) +); +--> statement-breakpoint +CREATE TABLE `videos` ( + `id` varchar(15) NOT NULL, + `ownerId` varchar(15) NOT NULL, + `name` varchar(255) NOT NULL DEFAULT 'My Video', + `awsRegion` varchar(255), + `awsBucket` varchar(255), + `bucket` varchar(15), + `metadata` json, + `public` boolean NOT NULL DEFAULT true, + `videoStartTime` varchar(255), + `audioStartTime` varchar(255), + `xStreamInfo` text, + `jobId` varchar(255), + `jobStatus` varchar(255), + `isScreenshot` boolean NOT NULL DEFAULT false, + `skipProcessing` boolean NOT NULL DEFAULT false, + `transcriptionStatus` varchar(255), + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + `source` json NOT NULL DEFAULT ('{"type":"MediaConvert"}'), + CONSTRAINT `videos_id` PRIMARY KEY(`id`), + CONSTRAINT `videos_id_unique` UNIQUE(`id`) +); +--> statement-breakpoint +CREATE INDEX `user_id_idx` ON `accounts` (`userId`);--> statement-breakpoint +CREATE INDEX `provider_account_id_idx` ON `accounts` (`providerAccountId`);--> statement-breakpoint +CREATE INDEX `video_id_idx` ON `comments` (`videoId`);--> statement-breakpoint +CREATE INDEX `author_id_idx` ON `comments` (`authorId`);--> statement-breakpoint +CREATE INDEX `parent_comment_id_idx` ON `comments` (`parentCommentId`);--> statement-breakpoint +CREATE INDEX `user_id_idx` ON `sessions` (`userId`);--> statement-breakpoint +CREATE INDEX `video_id_idx` ON `shared_videos` (`videoId`);--> statement-breakpoint +CREATE INDEX `space_id_idx` ON `shared_videos` (`spaceId`);--> statement-breakpoint +CREATE INDEX `shared_by_user_id_idx` ON `shared_videos` (`sharedByUserId`);--> statement-breakpoint +CREATE INDEX `video_id_space_id_idx` ON `shared_videos` (`videoId`,`spaceId`);--> statement-breakpoint +CREATE INDEX `space_id_idx` ON `space_invites` (`spaceId`);--> statement-breakpoint +CREATE INDEX `invited_email_idx` ON `space_invites` (`invitedEmail`);--> statement-breakpoint +CREATE INDEX `invited_by_user_id_idx` ON `space_invites` (`invitedByUserId`);--> statement-breakpoint +CREATE INDEX `status_idx` ON `space_invites` (`status`);--> statement-breakpoint +CREATE INDEX `user_id_idx` ON `space_members` (`userId`);--> statement-breakpoint +CREATE INDEX `space_id_idx` ON `space_members` (`spaceId`);--> statement-breakpoint +CREATE INDEX `user_id_space_id_idx` ON `space_members` (`userId`,`spaceId`);--> statement-breakpoint +CREATE INDEX `owner_id_idx` ON `spaces` (`ownerId`);--> statement-breakpoint +CREATE INDEX `custom_domain_idx` ON `spaces` (`customDomain`);--> statement-breakpoint +CREATE INDEX `id_idx` ON `videos` (`id`);--> statement-breakpoint +CREATE INDEX `owner_id_idx` ON `videos` (`ownerId`);--> statement-breakpoint +CREATE INDEX `is_public_idx` ON `videos` (`public`); \ No newline at end of file diff --git a/packages/database/migrations/meta/0000_snapshot.json b/packages/database/migrations/meta/0000_snapshot.json new file mode 100644 index 000000000..fed508679 --- /dev/null +++ b/packages/database/migrations/meta/0000_snapshot.json @@ -0,0 +1,1264 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "3d91bc9d-85c3-45eb-b6b6-e1db45b8151d", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_in": { + "name": "expires_in", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_in": { + "name": "refresh_token_expires_in", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "tempColumn": { + "name": "tempColumn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "provider_account_id_idx": { + "name": "provider_account_id_idx", + "columns": [ + "providerAccountId" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "accounts_id": { + "name": "accounts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "accounts_id_unique": { + "name": "accounts_id_unique", + "columns": [ + "id" + ] + } + } + }, + "comments": { + "name": "comments", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorId": { + "name": "authorId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "videoId": { + "name": "videoId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "parentCommentId": { + "name": "parentCommentId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "video_id_idx": { + "name": "video_id_idx", + "columns": [ + "videoId" + ], + "isUnique": false + }, + "author_id_idx": { + "name": "author_id_idx", + "columns": [ + "authorId" + ], + "isUnique": false + }, + "parent_comment_id_idx": { + "name": "parent_comment_id_idx", + "columns": [ + "parentCommentId" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "comments_id": { + "name": "comments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "comments_id_unique": { + "name": "comments_id_unique", + "columns": [ + "id" + ] + } + } + }, + "s3_buckets": { + "name": "s3_buckets", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ownerId": { + "name": "ownerId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bucketName": { + "name": "bucketName", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accessKeyId": { + "name": "accessKeyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('aws')" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "s3_buckets_id": { + "name": "s3_buckets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "s3_buckets_id_unique": { + "name": "s3_buckets_id_unique", + "columns": [ + "id" + ] + } + } + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sessionToken": { + "name": "sessionToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "datetime", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + "sessionToken" + ], + "isUnique": true + }, + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "sessions_id_unique": { + "name": "sessions_id_unique", + "columns": [ + "id" + ] + }, + "sessions_sessionToken_unique": { + "name": "sessions_sessionToken_unique", + "columns": [ + "sessionToken" + ] + } + } + }, + "shared_videos": { + "name": "shared_videos", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "videoId": { + "name": "videoId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "spaceId": { + "name": "spaceId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sharedByUserId": { + "name": "sharedByUserId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sharedAt": { + "name": "sharedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "video_id_idx": { + "name": "video_id_idx", + "columns": [ + "videoId" + ], + "isUnique": false + }, + "space_id_idx": { + "name": "space_id_idx", + "columns": [ + "spaceId" + ], + "isUnique": false + }, + "shared_by_user_id_idx": { + "name": "shared_by_user_id_idx", + "columns": [ + "sharedByUserId" + ], + "isUnique": false + }, + "video_id_space_id_idx": { + "name": "video_id_space_id_idx", + "columns": [ + "videoId", + "spaceId" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "shared_videos_id": { + "name": "shared_videos_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "shared_videos_id_unique": { + "name": "shared_videos_id_unique", + "columns": [ + "id" + ] + } + } + }, + "space_invites": { + "name": "space_invites", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "spaceId": { + "name": "spaceId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invitedEmail": { + "name": "invitedEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invitedByUserId": { + "name": "invitedByUserId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "space_id_idx": { + "name": "space_id_idx", + "columns": [ + "spaceId" + ], + "isUnique": false + }, + "invited_email_idx": { + "name": "invited_email_idx", + "columns": [ + "invitedEmail" + ], + "isUnique": false + }, + "invited_by_user_id_idx": { + "name": "invited_by_user_id_idx", + "columns": [ + "invitedByUserId" + ], + "isUnique": false + }, + "status_idx": { + "name": "status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "space_invites_id": { + "name": "space_invites_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "space_invites_id_unique": { + "name": "space_invites_id_unique", + "columns": [ + "id" + ] + } + } + }, + "space_members": { + "name": "space_members", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "spaceId": { + "name": "spaceId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "space_id_idx": { + "name": "space_id_idx", + "columns": [ + "spaceId" + ], + "isUnique": false + }, + "user_id_space_id_idx": { + "name": "user_id_space_id_idx", + "columns": [ + "userId", + "spaceId" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "space_members_id": { + "name": "space_members_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "space_members_id_unique": { + "name": "space_members_id_unique", + "columns": [ + "id" + ] + } + } + }, + "spaces": { + "name": "spaces", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ownerId": { + "name": "ownerId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowedEmailDomain": { + "name": "allowedEmailDomain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customDomain": { + "name": "customDomain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domainVerified": { + "name": "domainVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "workosOrganizationId": { + "name": "workosOrganizationId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workosConnectionId": { + "name": "workosConnectionId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "owner_id_idx": { + "name": "owner_id_idx", + "columns": [ + "ownerId" + ], + "isUnique": false + }, + "custom_domain_idx": { + "name": "custom_domain_idx", + "columns": [ + "customDomain" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "spaces_id": { + "name": "spaces_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "spaces_id_unique": { + "name": "spaces_id_unique", + "columns": [ + "id" + ] + } + } + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastName": { + "name": "lastName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "thirdPartyStripeSubscriptionId": { + "name": "thirdPartyStripeSubscriptionId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stripeSubscriptionStatus": { + "name": "stripeSubscriptionStatus", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stripeSubscriptionPriceId": { + "name": "stripeSubscriptionPriceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "activeSpaceId": { + "name": "activeSpaceId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customBucket": { + "name": "customBucket", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inviteQuota": { + "name": "inviteQuota", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "email_idx": { + "name": "email_idx", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_id_unique": { + "name": "users_id_unique", + "columns": [ + "id" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ] + } + } + }, + "verification_tokens": { + "name": "verification_tokens", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "datetime", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_tokens_identifier": { + "name": "verification_tokens_identifier", + "columns": [ + "identifier" + ] + } + }, + "uniqueConstraints": { + "verification_tokens_token_unique": { + "name": "verification_tokens_token_unique", + "columns": [ + "token" + ] + } + } + }, + "videos": { + "name": "videos", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ownerId": { + "name": "ownerId", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'My Video'" + }, + "awsRegion": { + "name": "awsRegion", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "awsBucket": { + "name": "awsBucket", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bucket": { + "name": "bucket", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "videoStartTime": { + "name": "videoStartTime", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audioStartTime": { + "name": "audioStartTime", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "xStreamInfo": { + "name": "xStreamInfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jobId": { + "name": "jobId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jobStatus": { + "name": "jobStatus", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "isScreenshot": { + "name": "isScreenshot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "skipProcessing": { + "name": "skipProcessing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transcriptionStatus": { + "name": "transcriptionStatus", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "source": { + "name": "source", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"type\":\"MediaConvert\"}')" + } + }, + "indexes": { + "id_idx": { + "name": "id_idx", + "columns": [ + "id" + ], + "isUnique": false + }, + "owner_id_idx": { + "name": "owner_id_idx", + "columns": [ + "ownerId" + ], + "isUnique": false + }, + "is_public_idx": { + "name": "is_public_idx", + "columns": [ + "public" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "videos_id": { + "name": "videos_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "videos_id_unique": { + "name": "videos_id_unique", + "columns": [ + "id" + ] + } + } + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json new file mode 100644 index 000000000..89f7557ac --- /dev/null +++ b/packages/database/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "5", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1743020179593, + "tag": "0000_brown_sunfire", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e7b5b406..7a14b399c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3,6 +3,7 @@ lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false + injectWorkspacePackages: true patchedDependencies: '@kobalte/core@0.13.7': @@ -39,16 +40,16 @@ importers: dependencies: '@cap/database': specifier: workspace:* - version: link:../../packages/database + version: file:packages/database(@babel/core@7.25.2)(@cloudflare/workers-types@4.20250214.0)(@opentelemetry/api@1.9.0)(@types/react@18.3.9)(mysql2@3.11.3)(nodemailer@6.9.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) '@cap/ui': specifier: workspace:* - version: link:../../packages/ui + version: file:packages/ui(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2)) '@cap/ui-solid': specifier: workspace:* - version: link:../../packages/ui-solid + version: file:packages/ui-solid(@fontsource/geist-sans@5.1.0) '@cap/utils': specifier: workspace:* - version: link:../../packages/utils + version: file:packages/utils(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2)) '@cap/web-api-contract': specifier: workspace:* version: link:../../packages/web-api-contract @@ -266,7 +267,7 @@ importers: dependencies: '@cap/ui-solid': specifier: workspace:* - version: link:../../packages/ui-solid + version: file:packages/ui-solid(@fontsource/geist-sans@5.1.0) postcss-pseudo-companion-classes: specifier: ^0.1.1 version: 0.1.1(postcss@8.4.47) @@ -418,13 +419,13 @@ importers: version: link:../../packages/env '@cap/ui': specifier: workspace:* - version: link:../../packages/ui + version: file:packages/ui(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.7.2)))(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2)) '@cap/utils': specifier: workspace:* version: link:../../packages/utils '@cap/web-api-contract': specifier: workspace:* - version: link:../../packages/web-api-contract + version: file:packages/web-api-contract(@types/node@20.16.9) '@deepgram/sdk': specifier: ^3.3.4 version: 3.7.0 @@ -726,10 +727,10 @@ importers: version: 5.0.7 next: specifier: 14.2.9 - version: 14.2.9(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.9(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-auth: specifier: ^4.24.5 - version: 4.24.8(next@14.2.9(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.24.8(next@14.2.9(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-email: specifier: ^1.10.1 version: 1.10.1 @@ -739,7 +740,7 @@ importers: devDependencies: '@cap/ui': specifier: workspace:* - version: link:../ui + version: file:packages/ui(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2)) '@cap/utils': specifier: workspace:* version: link:../utils @@ -798,7 +799,7 @@ importers: dependencies: '@cap/utils': specifier: workspace:* - version: link:../utils + version: file:packages/utils(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2)) '@kobalte/tailwindcss': specifier: ^0.9.0 version: 0.9.0(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.7.2))) @@ -1396,6 +1397,29 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@cap/database@file:packages/database': + resolution: {directory: packages/database, type: directory} + engines: {node: '20'} + + '@cap/env@file:packages/env': + resolution: {directory: packages/env, type: directory} + + '@cap/ui-solid@file:packages/ui-solid': + resolution: {directory: packages/ui-solid, type: directory} + peerDependencies: + '@fontsource/geist-sans': ^5.0.3 + + '@cap/ui@file:packages/ui': + resolution: {directory: packages/ui, type: directory} + engines: {node: '20'} + + '@cap/utils@file:packages/utils': + resolution: {directory: packages/utils, type: directory} + engines: {node: '20'} + + '@cap/web-api-contract@file:packages/web-api-contract': + resolution: {directory: packages/web-api-contract, type: directory} + '@chromatic-com/storybook@1.9.0': resolution: {integrity: sha512-vYQ+TcfktEE3GHnLZXHCzXF/sN9dw+KivH8a5cmPyd9YtQs7fZtHrEgsIjWpYycXiweKMo1Lm1RZsjxk8DH3rA==} engines: {node: '>=16.0.0', yarn: '>=1.22.18'} @@ -4655,10 +4679,10 @@ packages: react-dom: optional: true - '@storybook/builder-vite@9.0.0-alpha.4': - resolution: {integrity: sha512-TxCLKB+nHkoooKzcJN8QKM2D8l9A8t5/qPQkStkadP5xdkoMC9CHYuq0Anu55mtbzZQnuDtS09lT3laGqswRuw==} + '@storybook/builder-vite@9.0.0-alpha.10': + resolution: {integrity: sha512-gPpW5WcxvtH1aTY5KqtUMtsExjxuHRvuD+Fsw+n6HLOGVZsBs8m/uJ7ujZMnUmSsR2w/DJI5mk9uWEB01Vasdw==} peerDependencies: - storybook: ^9.0.0-alpha.4 + storybook: ^9.0.0-alpha.10 vite: ^4.0.0 || ^5.0.0 || ^6.0.0 '@storybook/core@8.3.3': @@ -4669,10 +4693,10 @@ packages: peerDependencies: storybook: ^8.3.3 - '@storybook/csf-plugin@9.0.0-alpha.4': - resolution: {integrity: sha512-Zq65uk267XRMr/DeAoOj1Yom6tuMPAOJo+GRw6qglX3NPxjc5eheGkFbT74P4CGXxYX9D5jDe9hY4CGBLNkACw==} + '@storybook/csf-plugin@9.0.0-alpha.10': + resolution: {integrity: sha512-p8ho/3H07TYXn+hY9uExhXy4DyNr154PmB6kSJ/7vLZkWnHXzeP54xDq4/WT9FNCF8FwwtDzRDSoJRiPqTP0kQ==} peerDependencies: - storybook: ^9.0.0-alpha.4 + storybook: ^9.0.0-alpha.10 '@storybook/csf@0.1.11': resolution: {integrity: sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==} @@ -12753,6 +12777,147 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@cap/database@file:packages/database(@babel/core@7.25.2)(@cloudflare/workers-types@4.20250214.0)(@opentelemetry/api@1.9.0)(@types/react@18.3.9)(mysql2@3.11.3)(nodemailer@6.9.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)': + dependencies: + '@cap/env': file:packages/env(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2)) + '@mattrax/mysql-planetscale': 0.0.3 + '@paralleldrive/cuid2': 2.2.2 + '@planetscale/database': 1.19.0 + '@react-email/components': 0.0.13(@types/react@18.3.9)(react@18.3.1) + '@react-email/render': 0.0.11 + drizzle-orm: 0.30.9(@cloudflare/workers-types@4.20250214.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/react@18.3.9)(mysql2@3.11.3)(react@18.3.1) + nanoid: 5.0.7 + next: 14.2.9(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-auth: 4.24.8(next@14.2.9(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-email: 1.10.1 + resend: 4.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - '@auth/core' + - '@aws-sdk/client-rds-data' + - '@babel/core' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@playwright/test' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/react' + - '@types/sql.js' + - '@vercel/postgres' + - '@xata.io/client' + - babel-plugin-macros + - better-sqlite3 + - bun-types + - encoding + - expo-sqlite + - knex + - kysely + - mysql2 + - nodemailer + - pg + - postgres + - react + - react-dom + - sass + - sql.js + - sqlite3 + - typescript + - valibot + + '@cap/env@file:packages/env(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2))': + dependencies: + '@t3-oss/env-nextjs': 0.12.0(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2))(zod@3.24.1) + zod: 3.24.1 + transitivePeerDependencies: + - typescript + - valibot + + '@cap/ui-solid@file:packages/ui-solid(@fontsource/geist-sans@5.1.0)': + dependencies: + '@fontsource/geist-sans': 5.1.0 + '@kobalte/core': 0.13.7(patch_hash=daf56e70aa12211e66d61eca48f0538cc1963c967982714f3abc09110083cf0d)(solid-js@1.9.3) + cva: class-variance-authority@0.7.0 + solid-js: 1.9.3 + tailwindcss: 3.4.13(ts-node@10.9.2(@types/node@22.12.0)(typescript@5.7.2)) + transitivePeerDependencies: + - ts-node + + '@cap/ui@file:packages/ui(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.7.2)))(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2))': + dependencies: + '@cap/utils': file:packages/utils(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2)) + '@kobalte/tailwindcss': 0.9.0(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.7.2))) + '@radix-ui/react-dialog': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-navigation-menu': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-switch': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tailwindcss/typography': 0.5.15(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.9)(typescript@5.7.2))) + class-variance-authority: 0.7.0 + cmdk: 0.2.1(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + lucide-react: 0.294.0(react@18.3.1) + react-hook-form: 7.53.0(react@18.3.1) + react-loading-skeleton: 3.5.0(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - aws-crt + - react + - react-dom + - tailwindcss + - typescript + - valibot + + '@cap/ui@file:packages/ui(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2))': + dependencies: + '@cap/utils': file:packages/utils(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2)) + '@kobalte/tailwindcss': 0.9.0(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@22.12.0)(typescript@5.7.2))) + '@radix-ui/react-dialog': 1.1.1(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': 2.1.1(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': 2.1.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-navigation-menu': 1.2.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': 1.1.1(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-switch': 1.1.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tailwindcss/typography': 0.5.15(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@22.12.0)(typescript@5.7.2))) + class-variance-authority: 0.7.0 + cmdk: 0.2.1(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + lucide-react: 0.294.0(react@18.3.1) + react-hook-form: 7.53.0(react@18.3.1) + react-loading-skeleton: 3.5.0(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - aws-crt + - react + - react-dom + - tailwindcss + - typescript + - valibot + + '@cap/utils@file:packages/utils(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2))': + dependencies: + '@aws-sdk/client-s3': 3.658.1 + '@cap/env': file:packages/env(typescript@5.7.2)(valibot@1.0.0-rc.1(typescript@5.7.2)) + clsx: 2.1.1 + stripe: 14.25.0 + tailwind-merge: 2.5.2 + transitivePeerDependencies: + - aws-crt + - typescript + - valibot + + '@cap/web-api-contract@file:packages/web-api-contract(@types/node@20.16.9)': + dependencies: + '@ts-rest/core': 3.51.0(@types/node@20.16.9)(zod@3.24.1) + zod: 3.24.1 + transitivePeerDependencies: + - '@types/node' + '@chromatic-com/storybook@1.9.0(react@18.3.1)': dependencies: chromatic: 11.10.4 @@ -14638,6 +14803,15 @@ snapshots: '@radix-ui/primitive@1.1.0': {} + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-arrow@1.1.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14647,6 +14821,18 @@ snapshots: '@types/react': 18.3.9 '@types/react-dom': 19.0.4(@types/react@18.3.9) + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.9)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-collection@1.1.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) @@ -14710,6 +14896,28 @@ snapshots: transitivePeerDependencies: - '@types/react' + '@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.9)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.9)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-dialog@1.1.1(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -14749,6 +14957,19 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.9)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -14762,6 +14983,21 @@ snapshots: '@types/react': 18.3.9 '@types/react-dom': 19.0.4(@types/react@18.3.9) + '@radix-ui/react-dropdown-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-menu': 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.9)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-dropdown-menu@2.1.1(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -14797,6 +15033,17 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.9)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) @@ -14821,6 +15068,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.9 + '@radix-ui/react-label@2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-label@2.1.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14830,6 +15086,32 @@ snapshots: '@types/react': 18.3.9 '@types/react-dom': 19.0.4(@types/react@18.3.9) + '@radix-ui/react-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.9)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.9)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-menu@2.1.1(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -14856,6 +15138,28 @@ snapshots: '@types/react': 18.3.9 '@types/react-dom': 19.0.4(@types/react@18.3.9) + '@radix-ui/react-navigation-menu@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-navigation-menu@1.2.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -14878,6 +15182,29 @@ snapshots: '@types/react': 18.3.9 '@types/react-dom': 19.0.4(@types/react@18.3.9) + '@radix-ui/react-popover@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.9)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.9)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-popover@1.1.1(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -14901,6 +15228,24 @@ snapshots: '@types/react': 18.3.9 '@types/react-dom': 19.0.4(@types/react@18.3.9) + '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-popper@1.2.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14926,6 +15271,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.9)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.1.1(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14944,6 +15299,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.9)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.1.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) @@ -14961,6 +15326,15 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.9)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive@2.0.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.1.0(@types/react@18.3.9)(react@18.3.1) @@ -14970,6 +15344,23 @@ snapshots: '@types/react': 18.3.9 '@types/react-dom': 19.0.4(@types/react@18.3.9) + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.9)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -15008,6 +15399,21 @@ snapshots: optionalDependencies: '@types/react': 18.3.9 + '@radix-ui/react-switch@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.9)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.9)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-switch@1.1.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -15091,6 +15497,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.9 + '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@types/react-dom': 18.3.0 + '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.4(@types/react@18.3.9))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -15942,9 +16357,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@9.0.0-alpha.4(storybook@8.3.3)(vite@5.4.8(@types/node@22.12.0)(terser@5.34.0))': + '@storybook/builder-vite@9.0.0-alpha.10(storybook@8.3.3)(vite@5.4.8(@types/node@22.12.0)(terser@5.34.0))': dependencies: - '@storybook/csf-plugin': 9.0.0-alpha.4(storybook@8.3.3) + '@storybook/csf-plugin': 9.0.0-alpha.10(storybook@8.3.3) browser-assert: 1.2.1 storybook: 8.3.3 ts-dedent: 2.2.0 @@ -15975,7 +16390,7 @@ snapshots: storybook: 8.3.3 unplugin: 1.16.1 - '@storybook/csf-plugin@9.0.0-alpha.4(storybook@8.3.3)': + '@storybook/csf-plugin@9.0.0-alpha.10(storybook@8.3.3)': dependencies: storybook: 8.3.3 unplugin: 1.16.1 @@ -18726,7 +19141,7 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1) eslint-plugin-import: 2.30.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1) eslint-plugin-react: 7.37.0(eslint@8.57.1) @@ -18755,25 +19170,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(eslint@8.57.1))(eslint@8.57.1): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.3.7(supports-color@5.5.0) - enhanced-resolve: 5.17.1 - eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) - fast-glob: 3.3.2 - get-tsconfig: 4.8.1 - is-bun-module: 1.2.1 - is-glob: 4.0.3 - optionalDependencies: - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-node - - eslint-import-resolver-webpack - - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 @@ -18819,7 +19215,7 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -22054,13 +22450,13 @@ snapshots: optionalDependencies: nodemailer: 6.9.15 - next-auth@4.24.8(next@14.2.9(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-auth@4.24.8(next@14.2.9(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.6 '@panva/hkdf': 1.2.1 cookie: 0.5.0 jose: 4.15.9 - next: 14.2.9(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.9(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) oauth: 0.9.15 openid-client: 5.7.0 preact: 10.24.1 @@ -22109,7 +22505,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.25.2)(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 14.2.3 '@next/swc-darwin-x64': 14.2.3 @@ -22125,7 +22521,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@14.2.9(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.9(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.9 '@swc/helpers': 0.5.5 @@ -22135,7 +22531,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.25.2)(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 14.2.9 '@next/swc-darwin-x64': 14.2.9 @@ -23767,7 +24163,7 @@ snapshots: storybook-solidjs-vite@1.0.0-beta.2(storybook@8.3.3)(vite@5.4.8(@types/node@22.12.0)(terser@5.34.0)): dependencies: - '@storybook/builder-vite': 9.0.0-alpha.4(storybook@8.3.3)(vite@5.4.8(@types/node@22.12.0)(terser@5.34.0)) + '@storybook/builder-vite': 9.0.0-alpha.10(storybook@8.3.3)(vite@5.4.8(@types/node@22.12.0)(terser@5.34.0)) transitivePeerDependencies: - storybook - vite @@ -23922,10 +24318,12 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.1(react@18.3.1): + styled-jsx@5.1.1(@babel/core@7.25.2)(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 + optionalDependencies: + '@babel/core': 7.25.2 subtitles-parser-vtt@0.1.0: {} diff --git a/turbo.json b/turbo.json index 2a0cef434..70fedba45 100644 --- a/turbo.json +++ b/turbo.json @@ -1,12 +1,57 @@ { "$schema": "https://turbo.build/schema.json", - "globalDependencies": [".env", ".env"], - "globalEnv": ["*"], + "globalDependencies": [ + ".env", + ".env" + ], + "globalEnv": [ + "*" + ], "tasks": { "build": { - "inputs": ["**/*.ts", "**/*.tsx", "!src-tauri/**", "!node_modules/**"], - "dependsOn": ["^build"], - "outputs": [".next/**", "!.next/cache/**"] + "inputs": [ + "**/*.ts", + "**/*.tsx", + "!src-tauri/**", + "!node_modules/**" + ], + "dependsOn": [ + "^build" + ], + "outputs": [ + ".next/**", + "!.next/cache/**" + ] + }, + "build:web:docker": { + "inputs": [ + "**/*.ts", + "**/*.tsx", + "!src-tauri/**", + "!node_modules/**" + ], + "dependsOn": [ + "^build" + ], + "outputs": [ + ".next/**", + "!.next/cache/**" + ] + }, + "build:web": { + "inputs": [ + "**/*.ts", + "**/*.tsx", + "!src-tauri/**", + "!node_modules/**" + ], + "dependsOn": [ + "^build" + ], + "outputs": [ + ".next/**", + "!.next/cache/**" + ] }, "lint": { "cache": false @@ -28,9 +73,12 @@ "cache": false }, "dev": { - "dependsOn": ["db:push", "db:generate"], + "dependsOn": [ + "db:push", + "db:generate" + ], "cache": false, "persistent": true } } -} +} \ No newline at end of file From ebbefa2a2471f7b9e9cfc6ae3c2a069c2c938b85 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 28 Mar 2025 23:00:56 +0800 Subject: [PATCH 03/18] start removing NEXT_PUBLIC envs in favour of server --- apps/web/app/blog/[slug]/page.tsx | 5 ++- apps/web/app/blog/_components/Share.tsx | 10 +++--- apps/web/app/dashboard/caps/Caps.tsx | 1 + .../app/dashboard/caps/components/CapCard.tsx | 31 ++++++------------- .../caps/components/CapCardActions.tsx | 16 +++------- .../caps/components/CapPagination.tsx | 14 +++------ apps/web/app/dashboard/caps/page.tsx | 5 +++ apps/web/app/dashboard/layout.tsx | 1 + 8 files changed, 36 insertions(+), 47 deletions(-) diff --git a/apps/web/app/blog/[slug]/page.tsx b/apps/web/app/blog/[slug]/page.tsx index 5904d765c..b565429c0 100644 --- a/apps/web/app/blog/[slug]/page.tsx +++ b/apps/web/app/blog/[slug]/page.tsx @@ -87,7 +87,10 @@ export default async function PostPage({ params }: PostProps) {
- +
diff --git a/apps/web/app/blog/_components/Share.tsx b/apps/web/app/blog/_components/Share.tsx index 83c5580ed..cbe5f2350 100644 --- a/apps/web/app/blog/_components/Share.tsx +++ b/apps/web/app/blog/_components/Share.tsx @@ -11,10 +11,10 @@ interface ShareProps { title: string; }; }; + url: string; } -export function Share({ post }: ShareProps) { - const shareUrl = `${clientEnv.NEXT_PUBLIC_WEB_URL}/blog/${post.slug}`; +export function Share({ post, url }: ShareProps) { const [copied, setCopied] = useState(false); return ( @@ -25,7 +25,7 @@ export function Share({ post }: ShareProps) {
diff --git a/apps/web/app/dashboard/caps/Caps.tsx b/apps/web/app/dashboard/caps/Caps.tsx index 9e8c4b4f9..b34de2555 100644 --- a/apps/web/app/dashboard/caps/Caps.tsx +++ b/apps/web/app/dashboard/caps/Caps.tsx @@ -17,7 +17,6 @@ type VideoData = { totalReactions: number; sharedSpaces: { id: string; name: string }[]; ownerName: string; - url: string; }[]; export const Caps = ({ diff --git a/apps/web/app/dashboard/caps/components/CapCard.tsx b/apps/web/app/dashboard/caps/components/CapCard.tsx index 46b813d88..e9e5769c8 100644 --- a/apps/web/app/dashboard/caps/components/CapCard.tsx +++ b/apps/web/app/dashboard/caps/components/CapCard.tsx @@ -20,7 +20,6 @@ interface CapCardProps { totalReactions: number; sharedSpaces: { id: string; name: string }[]; ownerName: string; - url: string; }; analytics: number; onDelete: (videoId: string) => Promise; diff --git a/apps/web/app/dashboard/caps/page.tsx b/apps/web/app/dashboard/caps/page.tsx index 9c1fd72b4..efa776ef9 100644 --- a/apps/web/app/dashboard/caps/page.tsx +++ b/apps/web/app/dashboard/caps/page.tsx @@ -12,6 +12,7 @@ import { desc, eq, sql, count, or } from "drizzle-orm"; import { getCurrentUser } from "@cap/database/auth/session"; import { Metadata } from "next"; import { redirect } from "next/navigation"; +import { clientEnv, serverEnv } from "@cap/env"; export const metadata: Metadata = { title: "My Caps — Cap", @@ -97,11 +98,6 @@ export default async function CapsPage({ ...video, sharedSpaces: video.sharedSpaces.filter((space) => space.id !== null), ownerName: video.ownerName ?? "", - url: activeSpace?.space.customDomain - ? `https://${activeSpace.space.customDomain}/s/${cap.id}` - : clientEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production" - ? `https://cap.link/${cap.id}` - : `${clientEnv.NEXT_PUBLIC_WEB_URL}/s/${cap.id}`, })); return ( diff --git a/apps/web/app/dashboard/shared-caps/components/SharedCapCard.tsx b/apps/web/app/dashboard/shared-caps/components/SharedCapCard.tsx index 156bec6ee..f06a8dfd4 100644 --- a/apps/web/app/dashboard/shared-caps/components/SharedCapCard.tsx +++ b/apps/web/app/dashboard/shared-caps/components/SharedCapCard.tsx @@ -5,6 +5,7 @@ import moment from "moment"; import { Tooltip } from "react-tooltip"; import { serverEnv, clientEnv, NODE_ENV } from "@cap/env"; import { useSharedContext } from "@/app/dashboard/_components/DynamicSharedLayout"; +import { usePublicEnv } from "@/utils/public-env"; interface SharedCapCardProps { cap: { id: string; @@ -27,6 +28,7 @@ export const SharedCapCard: React.FC = ({ const [isEditing, setIsEditing] = useState(false); const [title, setTitle] = useState(cap.name); const { activeSpace } = useSharedContext(); + const publicEnv = usePublicEnv(); const displayCount = analytics === 0 @@ -45,7 +47,7 @@ export const SharedCapCard: React.FC = ({ ? `https://${activeSpace.space.customDomain}/s/${cap.id}` : clientEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production" ? `https://cap.link/${cap.id}` - : `${clientEnv.NEXT_PUBLIC_WEB_URL}/s/${cap.id}` + : `${publicEnv.webUrl}/s/${cap.id}` } > - - -
- - {children} -
-
- -
+ + +
+ + {children} +
+
+ +
+
diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index a760866fc..e7298ca88 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -139,7 +139,7 @@ export const Share: React.FC = ({
Recorded with diff --git a/apps/web/app/s/[videoId]/_components/ShareHeader.tsx b/apps/web/app/s/[videoId]/_components/ShareHeader.tsx index 8ec5911a1..79e1b01cb 100644 --- a/apps/web/app/s/[videoId]/_components/ShareHeader.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareHeader.tsx @@ -33,14 +33,11 @@ export const ShareHeader = ({ const handleBlur = async () => { setIsEditing(false); - const response = await fetch( - `${clientEnv.NEXT_PUBLIC_WEB_URL}/api/video/title`, - { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ title, videoId: data.id }), - } - ); + const response = await fetch(`/api/video/title`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title, videoId: data.id }), + }); if (response.status === 429) { toast.error("Too many requests - please try again later."); @@ -91,7 +88,7 @@ export const ShareHeader = ({ ? `https://${customDomain}/s/${data.id}` : clientEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production" ? `https://cap.link/${data.id}` - : `${clientEnv.NEXT_PUBLIC_WEB_URL}/s/${data.id}`; + : `${location.origin}/s/${data.id}`; }; const getDisplayLink = () => { @@ -99,7 +96,7 @@ export const ShareHeader = ({ ? `${customDomain}/s/${data.id}` : clientEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production" ? `cap.link/${data.id}` - : `${clientEnv.NEXT_PUBLIC_WEB_URL}/s/${data.id}`; + : `${location.origin}/s/${data.id}`; }; return ( @@ -175,7 +172,7 @@ export const ShareHeader = ({
+ +
+
+ )}
); }; diff --git a/apps/web/app/dashboard/caps/components/CapCard.tsx b/apps/web/app/dashboard/caps/components/CapCard.tsx index 63ec9516c..5ea889380 100644 --- a/apps/web/app/dashboard/caps/components/CapCard.tsx +++ b/apps/web/app/dashboard/caps/components/CapCard.tsx @@ -7,12 +7,13 @@ import { Tooltip } from "@/components/Tooltip"; import { VideoThumbnail } from "@/components/VideoThumbnail"; import { usePublicEnv } from "@/utils/public-env"; import { VideoMetadata } from "@cap/database/types"; -import { clientEnv, NODE_ENV } from "@cap/env"; +import { buildEnv, NODE_ENV } from "@cap/env"; import { Button } from "@cap/ui"; import { faChevronDown, faLink, faTrash, + faCheck, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import moment from "moment"; @@ -21,7 +22,7 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import { toast } from "react-hot-toast"; -interface CapCardProps { +export interface CapCardProps { cap: { id: string; ownerId: string; @@ -37,6 +38,9 @@ interface CapCardProps { onDelete: (videoId: string) => Promise; userId: string; userSpaces: { id: string; name: string }[]; + isSelected?: boolean; + onSelectToggle?: () => void; + anyCapSelected?: boolean; } export const CapCard = ({ @@ -45,6 +49,9 @@ export const CapCard = ({ onDelete, userId, userSpaces, + isSelected = false, + onSelectToggle, + anyCapSelected = false, }: CapCardProps) => { const effectiveDate = cap.metadata?.customCreatedAt ? new Date(cap.metadata.customCreatedAt) @@ -204,6 +211,14 @@ export const CapCard = ({ } }; + const handleSelectClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (onSelectToggle) { + onSelectToggle(); + } + }; + const { webUrl } = usePublicEnv(); return ( @@ -217,15 +232,27 @@ export const CapCard = ({ userSpaces={userSpaces} onSharingUpdated={handleSharingUpdated} /> -
-
+
+
+ + {/* Selection checkbox */} +
+
+ {isSelected && ( + + )} +
+
+ = ({ 1 diff --git a/apps/web/app/dashboard/caps/page.tsx b/apps/web/app/dashboard/caps/page.tsx index 0cdb4054a..01ea59afa 100644 --- a/apps/web/app/dashboard/caps/page.tsx +++ b/apps/web/app/dashboard/caps/page.tsx @@ -69,7 +69,7 @@ export default async function CapsPage({ ownerName: users.name, effectiveDate: sql` COALESCE( - JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), + JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), ${videos.createdAt} ) `, @@ -90,7 +90,7 @@ export default async function CapsPage({ ) .orderBy( desc(sql`COALESCE( - JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), + JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), ${videos.createdAt} )`) ) diff --git a/apps/web/app/dashboard/page.tsx b/apps/web/app/dashboard/page.tsx index a10e569e7..39ee39b67 100644 --- a/apps/web/app/dashboard/page.tsx +++ b/apps/web/app/dashboard/page.tsx @@ -1,5 +1,3 @@ -"use server"; - import { redirect } from "next/navigation"; export default async function DashboardWebsitePage() { diff --git a/apps/web/app/dashboard/settings/workspace/components/CustomDomain.tsx b/apps/web/app/dashboard/settings/workspace/components/CustomDomain.tsx index 495e7db30..0bc2cf556 100644 --- a/apps/web/app/dashboard/settings/workspace/components/CustomDomain.tsx +++ b/apps/web/app/dashboard/settings/workspace/components/CustomDomain.tsx @@ -1,4 +1,3 @@ -export const dynamic = "force-dynamic"; export const revalidate = 0; export const fetchCache = "force-no-store"; @@ -13,6 +12,7 @@ import { Check, CheckCircle, Copy, XCircle } from "lucide-react"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { toast } from "react-hot-toast"; +import { UpgradeModal } from "@/components/UpgradeModal"; type DomainVerification = { type: string; @@ -53,9 +53,10 @@ export function CustomDomain() { ); const [domainConfig, setDomainConfig] = useState(null); const [copiedField, setCopiedField] = useState<"name" | "value" | null>(null); + const [showUpgradeModal, setShowUpgradeModal] = useState(false); const initialCheckDone = useRef(false); const pollInterval = useRef(); - const POLL_INTERVAL = 5000; // 5 seconds + const POLL_INTERVAL = 5000; const cleanDomain = (input: string) => { if (!input) return ""; @@ -71,7 +72,6 @@ export function CustomDomain() { const withoutHash = withoutQuery.split("#")[0] || ""; const cleanedDomain = withoutHash.trim(); - // Check for valid domain with optional subdomains const hasTLD = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test( cleanedDomain @@ -103,7 +103,7 @@ export function CustomDomain() { if (showToasts) { if (data.verified) { toast.success("Domain is verified!"); - // Stop polling once verified + if (pollInterval.current) { clearInterval(pollInterval.current); pollInterval.current = undefined; @@ -124,23 +124,18 @@ export function CustomDomain() { }; useEffect(() => { - // Start polling if we have a custom domain and it's not verified if (activeSpace?.space.customDomain && !isVerified) { - // Clear any existing interval if (pollInterval.current) { clearInterval(pollInterval.current); } - // Check immediately checkVerification(false); - // Start polling pollInterval.current = setInterval(() => { checkVerification(false); }, POLL_INTERVAL); } - // Cleanup interval if domain becomes verified or component unmounts return () => { if (pollInterval.current) { clearInterval(pollInterval.current); @@ -149,7 +144,6 @@ export function CustomDomain() { }; }, [activeSpace?.space.customDomain, isVerified]); - // Initial check when component mounts useEffect(() => { if (!initialCheckDone.current && activeSpace?.space.customDomain) { initialCheckDone.current = true; @@ -161,28 +155,7 @@ export function CustomDomain() { e.preventDefault(); if (!isSubscribed) { - toast.error( - (t) => ( - - Please upgrade to{" "} -
{ - e.preventDefault(); - toast.dismiss(t.id); - router.push("/pricing"); - }} - > - Cap Pro - {" "} - to use custom domains - - ), - { - duration: 5000, - } - ); + setShowUpgradeModal(true); return; } @@ -193,7 +166,7 @@ export function CustomDomain() { } setLoading(true); - setDomain(cleanedDomain); // Update the input to show the cleaned domain + setDomain(cleanedDomain); try { const data = await updateDomain( @@ -204,16 +177,13 @@ export function CustomDomain() { toast.success("Domain settings updated"); router.refresh(); - // Set initial domain config from the response setDomainConfig(data.status); setIsVerified(data.verified); - // Trigger a refresh after 1 second to get DNS config setTimeout(() => { checkVerification(false); }, 1000); - // Start polling pollInterval.current = setInterval(() => { checkVerification(false); }, POLL_INTERVAL); @@ -226,28 +196,7 @@ export function CustomDomain() { const handleRemoveDomain = async () => { if (!isSubscribed) { - toast.error( - (t) => ( - - Please upgrade to{" "} - { - e.preventDefault(); - toast.dismiss(t.id); - router.push("/pricing"); - }} - > - Cap Pro - {" "} - to use custom domains - - ), - { - duration: 5000, - } - ); + setShowUpgradeModal(true); return; } @@ -257,7 +206,6 @@ export function CustomDomain() { try { await removeWorkspaceDomain(activeSpace?.space.id as string); - // Clear polling when domain is removed if (pollInterval.current) { clearInterval(pollInterval.current); pollInterval.current = undefined; @@ -534,6 +482,12 @@ export function CustomDomain() {
)}
+ {showUpgradeModal && ( + + )}
); } diff --git a/apps/web/app/dashboard/shared-caps/components/SharedCapCard.tsx b/apps/web/app/dashboard/shared-caps/components/SharedCapCard.tsx index b1a3fe3b1..2a0b263f9 100644 --- a/apps/web/app/dashboard/shared-caps/components/SharedCapCard.tsx +++ b/apps/web/app/dashboard/shared-caps/components/SharedCapCard.tsx @@ -4,7 +4,7 @@ import { Tooltip } from "@/components/Tooltip"; import { VideoThumbnail } from "@/components/VideoThumbnail"; import { usePublicEnv } from "@/utils/public-env"; import { VideoMetadata } from "@cap/database/types"; -import { clientEnv, NODE_ENV } from "@cap/env"; +import { NODE_ENV } from "@cap/env"; import { faBuilding, faUser } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import moment from "moment"; diff --git a/apps/web/app/docs/[...slug]/page.tsx b/apps/web/app/docs/[...slug]/page.tsx index 7d91aee25..06994b39c 100644 --- a/apps/web/app/docs/[...slug]/page.tsx +++ b/apps/web/app/docs/[...slug]/page.tsx @@ -1,6 +1,6 @@ import type { DocMetadata } from "@/utils/blog"; import { getDocs } from "@/utils/blog"; -import { clientEnv, serverEnv } from "@cap/env"; +import { buildEnv } from "@cap/env"; import type { Metadata } from "next"; import { MDXRemote } from "next-mdx-remote/rsc"; import Image from "next/image"; @@ -49,7 +49,7 @@ export async function generateMetadata( if (!doc) return; const { title, summary, image } = doc.metadata; - const ogImage = image ? `${serverEnv.WEB_URL}${image}` : undefined; + const ogImage = image ? `${buildEnv.NEXT_PUBLIC_WEB_URL}${image}` : undefined; const description = summary || title; return { @@ -59,7 +59,7 @@ export async function generateMetadata( title, description, type: "article", - url: `${serverEnv.WEB_URL}/docs/${fullSlug}`, + url: `${buildEnv.NEXT_PUBLIC_WEB_URL}/docs/${fullSlug}`, ...(ogImage && { images: [{ url: ogImage }], }), diff --git a/apps/web/app/docs/[slug]/page.tsx b/apps/web/app/docs/[slug]/page.tsx index 48f4c2de9..5c03b9b29 100644 --- a/apps/web/app/docs/[slug]/page.tsx +++ b/apps/web/app/docs/[slug]/page.tsx @@ -1,5 +1,5 @@ import { getDocs } from "@/utils/blog"; -import { clientEnv, serverEnv } from "@cap/env"; +import { buildEnv } from "@cap/env"; import type { Metadata } from "next"; import { MDXRemote } from "next-mdx-remote/rsc"; import Image from "next/image"; @@ -20,7 +20,7 @@ export async function generateMetadata({ } let { title, summary: description, image } = doc.metadata; - let ogImage = image ? `${serverEnv.WEB_URL}${image}` : undefined; + let ogImage = image ? `${buildEnv.NEXT_PUBLIC_WEB_URL}${image}` : undefined; return { title, @@ -29,7 +29,7 @@ export async function generateMetadata({ title, description, type: "article", - url: `${serverEnv.WEB_URL}/docs/${doc.slug}`, + url: `${buildEnv.NEXT_PUBLIC_WEB_URL}/docs/${doc.slug}`, ...(ogImage && { images: [ { diff --git a/apps/web/app/invite/[inviteId]/page.tsx b/apps/web/app/invite/[inviteId]/page.tsx index 728991b5e..f379e741f 100644 --- a/apps/web/app/invite/[inviteId]/page.tsx +++ b/apps/web/app/invite/[inviteId]/page.tsx @@ -1,4 +1,3 @@ -"use server"; import { db } from "@cap/database"; import { eq } from "drizzle-orm"; import { spaceInvites, spaces, users } from "@cap/database/schema"; diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 7d7b10598..3fa1d43ec 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -3,7 +3,7 @@ import { BentoScript } from "@/components/BentoScript"; import { Footer } from "@/components/Footer"; import { Navbar } from "@/components/Navbar"; import { getCurrentUser } from "@cap/database/auth/session"; -import { serverEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import crypto from "crypto"; import type { Metadata } from "next"; @@ -54,9 +54,9 @@ export default async function RootLayout({ }) { const user = await getCurrentUser(); let intercomHash = ""; - if (serverEnv.INTERCOM_SECRET) { + if (serverEnv().INTERCOM_SECRET) { intercomHash = crypto - .createHmac("sha256", serverEnv.INTERCOM_SECRET) + .createHmac("sha256", serverEnv().INTERCOM_SECRET) .update(user?.id ?? "") .digest("hex"); } @@ -93,8 +93,8 @@ export default async function RootLayout({ diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx new file mode 100644 index 000000000..af27fe9da --- /dev/null +++ b/apps/web/app/not-found.tsx @@ -0,0 +1,19 @@ +export default function NotFound() { + return ( +
+

404

+

+ Oops, we couldn't find this page +

+

+ Please contact the Cap team if this seems like a mistake:{" "} + + hello@cap.so + +

+
+ ); +} diff --git a/apps/web/app/providers.tsx b/apps/web/app/providers.tsx index 34f9967d0..bb8e3b7cf 100644 --- a/apps/web/app/providers.tsx +++ b/apps/web/app/providers.tsx @@ -4,15 +4,15 @@ import { createContext, PropsWithChildren, useEffect } from "react"; import { identifyUser, initAnonymousUser, trackEvent } from "./utils/analytics"; import posthog from "posthog-js"; import { PostHogProvider as PHProvider } from "posthog-js/react"; -import { clientEnv, serverEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; import PostHogPageView from "./PosthogPageView"; import Intercom from "@intercom/messenger-js-sdk"; import { usePathname } from "next/navigation"; export function PostHogProvider({ children }: { children: React.ReactNode }) { useEffect(() => { - const key = clientEnv.NEXT_PUBLIC_POSTHOG_KEY; - const host = clientEnv.NEXT_PUBLIC_POSTHOG_HOST; + const key = buildEnv.NEXT_PUBLIC_POSTHOG_KEY; + const host = buildEnv.NEXT_PUBLIC_POSTHOG_HOST; if (key && host) { try { diff --git a/apps/web/app/robots.ts b/apps/web/app/robots.ts index bdbbeab43..c0d4e8698 100644 --- a/apps/web/app/robots.ts +++ b/apps/web/app/robots.ts @@ -1,7 +1,6 @@ import { seoPages } from "@/lib/seo-pages"; import { MetadataRoute } from "next"; -export const dynamic = "force-dynamic"; export const revalidate = 0; export default function robots(): MetadataRoute.Robots { diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index 03d383d78..8effc642f 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -30,10 +30,6 @@ interface ShareProps { data: VideoWithSpaceInfo; user: typeof userSelectProps | null; comments: CommentWithAuthor[]; - individualFiles: { - fileName: string; - url: string; - }[]; initialAnalytics: { views: number; comments: number; @@ -47,13 +43,11 @@ export const Share: React.FC = ({ data, user, comments, - individualFiles, initialAnalytics, customDomain, domainVerified, }) => { const [analytics, setAnalytics] = useState(initialAnalytics); - // Use custom date from metadata if it exists, similar to CapCard.tsx const effectiveDate = data.metadata?.customCreatedAt ? new Date(data.metadata.customCreatedAt) : data.createdAt; @@ -83,7 +77,6 @@ export const Share: React.FC = ({ fetchViewCount(); }, [data.id]); - // Update analytics when comments change useEffect(() => { setAnalytics((prev) => ({ ...prev, @@ -98,7 +91,6 @@ export const Share: React.FC = ({ diff --git a/apps/web/app/s/[videoId]/_components/ShareHeader.tsx b/apps/web/app/s/[videoId]/_components/ShareHeader.tsx index 485752e02..7366895f5 100644 --- a/apps/web/app/s/[videoId]/_components/ShareHeader.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareHeader.tsx @@ -1,30 +1,25 @@ -import { Button, LogoBadge } from "@cap/ui"; +import { Button } from "@cap/ui"; import { videos } from "@cap/database/schema"; import moment from "moment"; import { userSelectProps } from "@cap/database/auth/session"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { toast } from "react-hot-toast"; -import { Copy, Loader2 } from "lucide-react"; -import JSZip from "jszip"; -import { saveAs } from "file-saver"; -import { clientEnv, NODE_ENV } from "@cap/env"; +import { Copy, Globe2 } from "lucide-react"; +import { buildEnv, NODE_ENV } from "@cap/env"; import { editTitle } from "@/actions/videos/edit-title"; import { usePublicEnv } from "@/utils/public-env"; +import { isUserOnProPlan } from "@cap/utils"; +import { UpgradeModal } from "@/components/UpgradeModal"; export const ShareHeader = ({ data, user, - individualFiles, customDomain, domainVerified, }: { data: typeof videos.$inferSelect; user: typeof userSelectProps | null; - individualFiles?: { - fileName: string; - url: string; - }[]; customDomain: string | null; domainVerified: boolean; }) => { @@ -32,6 +27,8 @@ export const ShareHeader = ({ const [isEditing, setIsEditing] = useState(false); const [title, setTitle] = useState(data.name); const [isDownloading, setIsDownloading] = useState(false); + const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); + const { webUrl } = usePublicEnv(); const handleBlur = async () => { setIsEditing(false); @@ -55,35 +52,10 @@ export const ShareHeader = ({ } }; - const downloadZip = async () => { - if (!individualFiles) return; - - setIsDownloading(true); - const zip = new JSZip(); - - try { - for (const file of individualFiles) { - const response = await fetch(file.url); - const blob = await response.blob(); - zip.file(file.fileName, blob); - } - - const content = await zip.generateAsync({ type: "blob" }); - saveAs(content, `${data.id}.zip`); - } catch (error) { - console.error("Error downloading zip:", error); - toast.error("Failed to download files. Please try again."); - } finally { - setIsDownloading(false); - } - }; - - const { webUrl } = usePublicEnv(); - const getVideoLink = () => { return customDomain && domainVerified ? `https://${customDomain}/s/${data.id}` - : clientEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production" + : buildEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production" ? `https://cap.link/${data.id}` : `${webUrl}/s/${data.id}`; }; @@ -91,18 +63,24 @@ export const ShareHeader = ({ const getDisplayLink = () => { return customDomain && domainVerified ? `${customDomain}/s/${data.id}` - : clientEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production" + : buildEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production" ? `cap.link/${data.id}` : `${webUrl}/s/${data.id}`; }; + const isUserPro = user + ? isUserOnProPlan({ + subscriptionStatus: user.stripeSubscriptionStatus, + }) + : false; + return ( <>
-
+
{isEditing ? ( ) : (

- {(user !== null || - (individualFiles && individualFiles.length > 0)) && ( -
- {individualFiles && individualFiles.length > 0 && ( -
- + {user !== null && !isUserPro && ( + -
- )} - + + Connect a custom domain + + )} +
{user !== null && (
)} @@ -180,6 +152,10 @@ export const ShareHeader = ({ )}
+ ); }; diff --git a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx index 23ebd5939..8420add0a 100644 --- a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx @@ -26,6 +26,7 @@ import { fromVtt, Subtitle } from "subtitles-parser-vtt"; import { MP4VideoPlayer } from "./MP4VideoPlayer"; import { VideoPlayer } from "./VideoPlayer"; import { usePublicEnv } from "@/utils/public-env"; +import { UpgradeModal } from "@/components/UpgradeModal"; declare global { interface Window { @@ -73,6 +74,7 @@ export const ShareVideo = forwardRef< const [subtitlesVisible, setSubtitlesVisible] = useState(true); const [isTranscriptionProcessing, setIsTranscriptionProcessing] = useState(false); + const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); // Scrubbing preview states const [showPreview, setShowPreview] = useState(false); @@ -1420,27 +1422,31 @@ export const ShareVideo = forwardRef< subscriptionStatus: user.stripeSubscriptionStatus, }) && ( )} +
); }); diff --git a/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx b/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx index bedaac9c7..15483d98d 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx @@ -15,7 +15,7 @@ interface TranscriptEntry { id: number; timestamp: string; text: string; - startTime: number; // in seconds + startTime: number; } const parseVTT = (vttContent: string): TranscriptEntry[] => { @@ -69,16 +69,13 @@ const parseVTT = (vttContent: string): TranscriptEntry[] => { const trimmedLine = line.trim(); - // Skip WEBVTT header if (trimmedLine === "WEBVTT") continue; - // Parse cue ID (number) if (/^\d+$/.test(trimmedLine)) { currentId = parseInt(trimmedLine, 10); continue; } - // Parse timestamp line if (trimmedLine.includes("-->")) { const [startTimeStr, endTimeStr] = trimmedLine.split(" --> "); if (!startTimeStr || !endTimeStr) continue; @@ -94,7 +91,6 @@ const parseVTT = (vttContent: string): TranscriptEntry[] => { continue; } - // Parse text content if (currentEntry.timestamp && !currentEntry.text) { currentEntry.text = trimmedLine; if ( @@ -118,6 +114,7 @@ export const Transcript: React.FC = ({ data, onSeek }) => { const [selectedEntry, setSelectedEntry] = useState(null); const [isTranscriptionProcessing, setIsTranscriptionProcessing] = useState(false); + const [hasTimedOut, setHasTimedOut] = useState(false); const publicEnv = usePublicEnv(); @@ -144,11 +141,19 @@ export const Transcript: React.FC = ({ data, onSeek }) => { setIsLoading(false); }; + const videoCreationTime = new Date(data.createdAt).getTime(); + const fiveMinutesInMs = 5 * 60 * 1000; + const isVideoOlderThanFiveMinutes = + Date.now() - videoCreationTime > fiveMinutesInMs; + if (data.transcriptionStatus === "COMPLETE") { fetchTranscript(); + } else if (isVideoOlderThanFiveMinutes && !data.transcriptionStatus) { + setIsLoading(false); + setHasTimedOut(true); } else { const startTime = Date.now(); - const maxDuration = 2 * 60 * 1000; // 2 minutes + const maxDuration = 2 * 60 * 1000; const intervalId = setInterval(() => { if (Date.now() - startTime > maxDuration) { @@ -180,11 +185,11 @@ export const Transcript: React.FC = ({ data, onSeek }) => { data.bucket, data.awsBucket, data.transcriptionStatus, + data.createdAt, ]); const handleReset = () => { setIsLoading(true); - // Re-fetch the transcript const fetchTranscript = async () => { const transcriptionUrl = data.bucket && data.awsBucket !== publicEnv.awsBucket @@ -208,7 +213,6 @@ export const Transcript: React.FC = ({ data, onSeek }) => { const handleTranscriptClick = (entry: TranscriptEntry) => { setSelectedEntry(entry.id); - // Use the onSeek callback to handle video seeking if (onSeek) { onSeek(entry.startTime); } @@ -217,7 +221,28 @@ export const Transcript: React.FC = ({ data, onSeek }) => { if (isLoading) { return (
-
+ + + + +
); } @@ -226,14 +251,37 @@ export const Transcript: React.FC = ({ data, onSeek }) => { return (
- +
+ + + + + +

Transcription in progress...

); } - if (!transcriptData.length) { + if (hasTimedOut || (!transcriptData.length && !isTranscriptionProcessing)) { return (
diff --git a/apps/web/app/s/[videoId]/page.tsx b/apps/web/app/s/[videoId]/page.tsx index 0b3f116d1..6130941b1 100644 --- a/apps/web/app/s/[videoId]/page.tsx +++ b/apps/web/app/s/[videoId]/page.tsx @@ -13,8 +13,10 @@ import { getCurrentUser, userSelectProps } from "@cap/database/auth/session"; import type { Metadata, ResolvingMetadata } from "next"; import { notFound } from "next/navigation"; import { ImageViewer } from "./_components/ImageViewer"; -import { clientEnv, serverEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; import { getVideoAnalytics } from "@/actions/videos/get-analytics"; +import { transcribeVideo } from "@/actions/videos/transcribe"; +import { getScreenshot } from "@/actions/screenshots/get-screenshot"; export const dynamic = "auto"; export const dynamicParams = true; @@ -49,7 +51,7 @@ export async function generateMetadata( "[generateMetadata] Fetching video metadata for videoId:", videoId ); - const query = await db.select().from(videos).where(eq(videos.id, videoId)); + const query = await db().select().from(videos).where(eq(videos.id, videoId)); if (query.length === 0) { console.log("[generateMetadata] No video found for videoId:", videoId); @@ -59,35 +61,42 @@ export async function generateMetadata( const video = query[0]; if (!video) { - console.log( - "[generateMetadata] Video object is null for videoId:", - videoId - ); return notFound(); } if (video.public === false) { - console.log( - "[generateMetadata] Video is private, returning private metadata" - ); return { title: "Cap: This video is private", description: "This video is private and cannot be shared.", openGraph: { - images: [`${serverEnv.WEB_URL}/api/video/og?videoId=${videoId}`], + images: [ + { + url: new URL( + `/api/video/og?videoId=${videoId}`, + buildEnv.NEXT_PUBLIC_WEB_URL + ).toString(), + width: 1200, + height: 630, + }, + ], }, }; } - console.log( - "[generateMetadata] Returning public metadata for video:", - video.name - ); return { title: video.name + " | Cap Recording", description: "Watch this video on Cap", openGraph: { - images: [`${serverEnv.WEB_URL}/api/video/og?videoId=${videoId}`], + images: [ + { + url: new URL( + `/api/video/og?videoId=${videoId}`, + buildEnv.NEXT_PUBLIC_WEB_URL + ).toString(), + width: 1200, + height: 630, + }, + ], }, }; } @@ -170,36 +179,9 @@ export default async function ShareVideoPage(props: Props) { } } - const videoSource = video.source as (typeof videos.$inferSelect)["source"]; - - if ( - video.jobId === null && - video.skipProcessing === false && - videoSource.type === "MediaConvert" - ) { - console.log("[ShareVideoPage] Creating MUX job for video:", videoId); - const res = await fetch( - `${serverEnv.WEB_URL}/api/upload/mux/create?videoId=${videoId}&userId=${video.ownerId}`, - { - method: "GET", - credentials: "include", - cache: "no-store", - } - ); - - await res.json(); - } - if (video.transcriptionStatus !== "COMPLETE") { console.log("[ShareVideoPage] Starting transcription for video:", videoId); - fetch( - `${serverEnv.WEB_URL}/api/video/transcribe?videoId=${videoId}&userId=${video.ownerId}`, - { - method: "GET", - credentials: "include", - cache: "no-store", - } - ); + await transcribeVideo(videoId, video.ownerId); } if (video.public === false && userId !== video.ownerId) { @@ -228,25 +210,22 @@ export default async function ShareVideoPage(props: Props) { let screenshotUrl; if (video.isScreenshot === true) { console.log("[ShareVideoPage] Fetching screenshot for video:", videoId); - const res = await fetch( - `${serverEnv.WEB_URL}/api/screenshot?userId=${video.ownerId}&screenshotId=${videoId}`, - { - method: "GET", - credentials: "include", - cache: "no-store", - } - ); - const data = await res.json(); - screenshotUrl = data.url; - - return ( - - ); + try { + const data = await getScreenshot(video.ownerId, videoId); + screenshotUrl = data.url; + + return ( + + ); + } catch (error) { + console.error("[ShareVideoPage] Error fetching screenshot:", error); + return

Failed to load screenshot

; + } } console.log("[ShareVideoPage] Fetching analytics for video:", videoId); @@ -258,11 +237,9 @@ export default async function ShareVideoPage(props: Props) { reactions: commentsQuery.filter((c) => c.type === "emoji").length, }; - // Fetch custom domain information let customDomain: string | null = null; let domainVerified = false; - // Check if the video is shared with a space if (video.sharedSpace?.spaceId) { const spaceData = await db .select({ @@ -275,14 +252,12 @@ export default async function ShareVideoPage(props: Props) { if (spaceData.length > 0 && spaceData[0] && spaceData[0].customDomain) { customDomain = spaceData[0].customDomain; - // Handle domainVerified which could be a Date or boolean if (spaceData[0].domainVerified !== null) { - domainVerified = true; // If it exists (not null), consider it verified + domainVerified = true; } } } - // If no custom domain from shared space, check the owner's space if (!customDomain && video.ownerId) { const ownerSpaces = await db .select({ @@ -299,14 +274,12 @@ export default async function ShareVideoPage(props: Props) { ownerSpaces[0].customDomain ) { customDomain = ownerSpaces[0].customDomain; - // Handle domainVerified which could be a Date or boolean if (ownerSpaces[0].domainVerified !== null) { - domainVerified = true; // If it exists (not null), consider it verified + domainVerified = true; } } } - // Get space members if the video is shared with a space const membersList = video.sharedSpace?.spaceId ? await db .select({ @@ -327,7 +300,6 @@ export default async function ShareVideoPage(props: Props) { data={videoWithSpaceInfo} user={user} comments={commentsQuery} - individualFiles={[]} // individualFiles} initialAnalytics={initialAnalytics} customDomain={customDomain} domainVerified={domainVerified} diff --git a/apps/web/app/tools/convert/[conversionPath]/page.tsx b/apps/web/app/tools/convert/[conversionPath]/page.tsx new file mode 100644 index 000000000..f4969ae98 --- /dev/null +++ b/apps/web/app/tools/convert/[conversionPath]/page.tsx @@ -0,0 +1,127 @@ +import { Metadata } from "next"; +import { + MediaFormatConverter, + parseFormats, + CONVERSION_CONFIGS, +} from "@/components/tools/MediaFormatConverter"; +import { ToolsPageTemplate } from "@/components/tools/ToolsPageTemplate"; +import { notFound } from "next/navigation"; +import { ToolPageContent } from "@/components/tools/types"; + +interface ConversionPageProps { + params: { + conversionPath: string; + }; +} + +export async function generateMetadata({ + params, +}: ConversionPageProps): Promise { + const { conversionPath } = params; + + if (!CONVERSION_CONFIGS[conversionPath]) { + return { + title: "Conversion Not Supported | Cap", + description: + "This conversion type is not supported by our free online tools.", + }; + } + + const { sourceFormat, targetFormat } = parseFormats(conversionPath); + const config = CONVERSION_CONFIGS[conversionPath]; + const sourceUpper = sourceFormat.toUpperCase(); + const targetUpper = targetFormat.toUpperCase(); + + return { + title: `${sourceUpper} to ${targetUpper} Converter | Free Online Tool | Cap`, + description: `${config.description( + sourceFormat, + targetFormat + )} Free online converter with no uploads needed for maximum privacy.`, + openGraph: { + title: `${sourceUpper} to ${targetUpper} Converter | Free Online Tool`, + description: `Convert ${sourceUpper} files to ${targetUpper} format directly in your browser. No uploads required, processing happens locally for maximum privacy.`, + images: [ + { + url: "/og.png", + width: 1200, + height: 630, + alt: `Cap ${sourceUpper} to ${targetUpper} Converter Tool`, + }, + ], + }, + }; +} + +export async function generateStaticParams() { + return Object.keys(CONVERSION_CONFIGS).map((path) => ({ + conversionPath: path, + })); +} + +export default function ConversionPage({ params }: ConversionPageProps) { + const { conversionPath } = params; + + if (!CONVERSION_CONFIGS[conversionPath]) { + notFound(); + } + + const { sourceFormat, targetFormat } = parseFormats(conversionPath); + const config = CONVERSION_CONFIGS[conversionPath]; + + const pageContent: ToolPageContent = { + title: config.title(sourceFormat, targetFormat), + description: config.description(sourceFormat, targetFormat), + featuresTitle: "Features", + featuresDescription: + "Our free online converter offers several advantages over other conversion services:", + features: [ + { + title: "100% Private", + description: + "Your files never leave your device. All processing happens right in your browser.", + }, + { + title: "No Installation Required", + description: + "No need to download or install any software. Just open the page and start converting.", + }, + { + title: "High Quality Conversion", + description: + "We use industry-standard FFmpeg technology to ensure high-quality conversion results.", + }, + ], + faqs: [ + { + question: `How does the ${sourceFormat.toUpperCase()} to ${targetFormat.toUpperCase()} converter work?`, + answer: `Our converter uses WebAssembly to run FFmpeg directly in your browser. When you upload a ${sourceFormat.toUpperCase()} file, it gets processed locally on your device and converted to ${targetFormat.toUpperCase()} format without ever being sent to a server.`, + }, + { + question: "Is there a file size limit?", + answer: + "Yes, currently we limit file sizes to 500MB to ensure smooth performance in the browser. For larger files, you might need to use a desktop application.", + }, + { + question: "Why should I use this converter instead of others?", + answer: + "Unlike many online converters that require uploading your files to their servers, our tool processes everything locally. This means your files never leave your device, providing maximum privacy and security.", + }, + ], + cta: { + title: "Cap is the open source Loom alternative", + description: + "Record, edit, and share video messages with Cap. 100% open source and privacy focused.", + buttonText: "Download Cap Free", + }, + }; + + return ( + + } + /> + ); +} diff --git a/apps/web/app/tools/convert/avi-to-mp4/page.tsx b/apps/web/app/tools/convert/avi-to-mp4/page.tsx new file mode 100644 index 000000000..19a8600b0 --- /dev/null +++ b/apps/web/app/tools/convert/avi-to-mp4/page.tsx @@ -0,0 +1,89 @@ +import { MediaFormatConverter } from "@/components/tools/MediaFormatConverter"; +import { ToolsPageTemplate } from "@/components/tools/ToolsPageTemplate"; +import { ToolPageContent } from "@/components/tools/types"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "AVI to MP4 Converter | Free Online Video Converter | Cap", + description: + "Convert AVI videos to MP4 format directly in your browser. Free online converter with no uploads needed for maximum privacy and security.", + openGraph: { + title: "AVI to MP4 Converter | Free Online Video Converter", + description: + "Convert outdated AVI videos to modern MP4 format. Process files locally in your browser with no uploads for maximum privacy.", + images: [ + { + url: "/og.png", + width: 1200, + height: 630, + alt: "Cap AVI to MP4 Converter Tool", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "AVI to MP4 Converter | Free Online Video Converter", + description: + "Convert AVI videos to MP4 format for better compatibility. No uploads required, completely private and secure.", + images: ["/og.png"], + }, +}; + +export default function AVIToMP4Page() { + const pageContent: ToolPageContent = { + title: "AVI to MP4 Converter", + description: "Convert AVI videos to MP4 format directly in your browser", + featuresTitle: "Features", + featuresDescription: + "Our free online converter offers several advantages over other conversion services:", + features: [ + { + title: "100% Private", + description: + "Your files never leave your device. All processing happens right in your browser.", + }, + { + title: "No Installation Required", + description: + "No need to download or install any software. Just open the page and start converting.", + }, + { + title: "High Quality Conversion", + description: + "We use industry-standard FFmpeg technology to ensure high-quality conversion results.", + }, + ], + faqs: [ + { + question: "How does the AVI to MP4 converter work?", + answer: + "Our converter uses WebAssembly to run FFmpeg directly in your browser. When you upload an AVI file, it gets processed locally on your device and converted to MP4 format without ever being sent to a server.", + }, + { + question: "Is there a file size limit?", + answer: + "Yes, currently we limit file sizes to 500MB to ensure smooth performance in the browser. For larger files, you might need to use a desktop application.", + }, + { + question: "Why convert AVI to MP4?", + answer: + "AVI is an older format with limited compatibility on modern devices. MP4 offers better compression, smaller file sizes, and widespread support on virtually all platforms and devices.", + }, + ], + cta: { + title: "Cap is the open source Loom alternative", + description: + "Record, edit, and share video messages with Cap. 100% open source and privacy focused.", + buttonText: "Download Cap Free", + }, + }; + + return ( + + } + /> + ); +} diff --git a/apps/web/app/tools/convert/layout.tsx b/apps/web/app/tools/convert/layout.tsx new file mode 100644 index 000000000..61acd223a --- /dev/null +++ b/apps/web/app/tools/convert/layout.tsx @@ -0,0 +1,7 @@ +export default function ConvertToolsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return
{children}
; +} diff --git a/apps/web/app/tools/convert/mkv-to-mp4/page.tsx b/apps/web/app/tools/convert/mkv-to-mp4/page.tsx new file mode 100644 index 000000000..7aa2d39ad --- /dev/null +++ b/apps/web/app/tools/convert/mkv-to-mp4/page.tsx @@ -0,0 +1,89 @@ +import { MediaFormatConverter } from "@/components/tools/MediaFormatConverter"; +import { ToolsPageTemplate } from "@/components/tools/ToolsPageTemplate"; +import { ToolPageContent } from "@/components/tools/types"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "MKV to MP4 Converter | Free Online Video Converter | Cap", + description: + "Convert MKV videos to widely supported MP4 format directly in your browser. Free online converter with no uploads needed for maximum privacy.", + openGraph: { + title: "MKV to MP4 Converter | Free Online Video Converter", + description: + "Convert MKV videos to the widely compatible MP4 format. Process files locally in your browser with no uploads for maximum privacy.", + images: [ + { + url: "/og.png", + width: 1200, + height: 630, + alt: "Cap MKV to MP4 Converter Tool", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "MKV to MP4 Converter | Free Online Video Converter", + description: + "Convert MKV videos to MP4 format for better compatibility. No uploads required, completely private and secure.", + images: ["/og.png"], + }, +}; + +export default function MKVToMP4Page() { + const pageContent: ToolPageContent = { + title: "MKV to MP4 Converter", + description: "Convert MKV videos to MP4 format directly in your browser", + featuresTitle: "Features", + featuresDescription: + "Our free online converter offers several advantages over other conversion services:", + features: [ + { + title: "100% Private", + description: + "Your files never leave your device. All processing happens right in your browser.", + }, + { + title: "No Installation Required", + description: + "No need to download or install any software. Just open the page and start converting.", + }, + { + title: "High Quality Conversion", + description: + "We use industry-standard FFmpeg technology to ensure high-quality conversion results.", + }, + ], + faqs: [ + { + question: "How does the MKV to MP4 converter work?", + answer: + "Our converter uses WebAssembly to run FFmpeg directly in your browser. When you upload an MKV file, it gets processed locally on your device and converted to MP4 format without ever being sent to a server.", + }, + { + question: "Is there a file size limit?", + answer: + "Yes, currently we limit file sizes to 500MB to ensure smooth performance in the browser. For larger files, you might need to use a desktop application.", + }, + { + question: "Why convert MKV to MP4?", + answer: + "While MKV is a versatile container format, MP4 offers better compatibility with a wider range of devices and software. Converting MKV to MP4 ensures your videos can be played on virtually any device or platform.", + }, + ], + cta: { + title: "Cap is the open source Loom alternative", + description: + "Record, edit, and share video messages with Cap. 100% open source and privacy focused.", + buttonText: "Download Cap Free", + }, + }; + + return ( + + } + /> + ); +} diff --git a/apps/web/app/tools/convert/mov-to-mp4/page.tsx b/apps/web/app/tools/convert/mov-to-mp4/page.tsx new file mode 100644 index 000000000..0df1961a1 --- /dev/null +++ b/apps/web/app/tools/convert/mov-to-mp4/page.tsx @@ -0,0 +1,89 @@ +import { MediaFormatConverter } from "@/components/tools/MediaFormatConverter"; +import { ToolsPageTemplate } from "@/components/tools/ToolsPageTemplate"; +import { ToolPageContent } from "@/components/tools/types"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "MOV to MP4 Converter | Free Online Video Converter | Cap", + description: + "Convert Apple QuickTime MOV videos to MP4 format directly in your browser. Free online converter with no uploads needed for maximum privacy.", + openGraph: { + title: "MOV to MP4 Converter | Free Online Video Converter", + description: + "Convert Apple QuickTime MOV videos to the widely compatible MP4 format right in your browser. No uploads, no installation required.", + images: [ + { + url: "/og.png", + width: 1200, + height: 630, + alt: "Cap MOV to MP4 Converter Tool", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "MOV to MP4 Converter | Free Online Video Converter", + description: + "Convert Apple QuickTime MOV videos to MP4 format directly in your browser. No uploads required for maximum privacy.", + images: ["/og.png"], + }, +}; + +export default function MOVToMP4Page() { + const pageContent: ToolPageContent = { + title: "MOV to MP4 Converter", + description: "Convert MOV videos to MP4 format directly in your browser", + featuresTitle: "Features", + featuresDescription: + "Our free online converter offers several advantages over other conversion services:", + features: [ + { + title: "100% Private", + description: + "Your files never leave your device. All processing happens right in your browser.", + }, + { + title: "No Installation Required", + description: + "No need to download or install any software. Just open the page and start converting.", + }, + { + title: "High Quality Conversion", + description: + "We use industry-standard FFmpeg technology to ensure high-quality conversion results.", + }, + ], + faqs: [ + { + question: "How does the MOV to MP4 converter work?", + answer: + "Our converter uses WebAssembly to run FFmpeg directly in your browser. When you upload a MOV file, it gets processed locally on your device and converted to MP4 format without ever being sent to a server.", + }, + { + question: "Is there a file size limit?", + answer: + "Yes, currently we limit file sizes to 500MB to ensure smooth performance in the browser. For larger files, you might need to use a desktop application.", + }, + { + question: "Why convert MOV to MP4?", + answer: + "MP4 offers much better compatibility across devices and platforms than MOV, which is primarily used on Apple devices. Converting to MP4 ensures your videos can be viewed anywhere.", + }, + ], + cta: { + title: "Cap is the open source Loom alternative", + description: + "Record, edit, and share video messages with Cap. 100% open source and privacy focused.", + buttonText: "Download Cap Free", + }, + }; + + return ( + + } + /> + ); +} diff --git a/apps/web/app/tools/convert/mp4-to-gif/page.tsx b/apps/web/app/tools/convert/mp4-to-gif/page.tsx new file mode 100644 index 000000000..e478f2431 --- /dev/null +++ b/apps/web/app/tools/convert/mp4-to-gif/page.tsx @@ -0,0 +1,90 @@ +import { MediaFormatConverter } from "@/components/tools/MediaFormatConverter"; +import { ToolsPageTemplate } from "@/components/tools/ToolsPageTemplate"; +import { ToolPageContent } from "@/components/tools/types"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "MP4 to GIF Converter | Free Online Animated GIF Maker | Cap", + description: + "Convert MP4 videos to animated GIF images directly in your browser. Create high-quality GIFs with our free online converter, no uploads needed.", + openGraph: { + title: "MP4 to GIF Converter | Free Online Animated GIF Maker", + description: + "Convert MP4 videos to animated GIF images directly in your browser. No uploads required, processing happens locally for maximum privacy.", + images: [ + { + url: "/og.png", + width: 1200, + height: 630, + alt: "Cap MP4 to GIF Converter Tool", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "MP4 to GIF Converter | Free Online Animated GIF Maker", + description: + "Convert MP4 videos to animated GIF images directly in your browser. No uploads required, maximum privacy.", + images: ["/og.png"], + }, +}; + +export default function MP4ToGIFPage() { + const pageContent: ToolPageContent = { + title: "MP4 to GIF Converter", + description: + "Convert MP4 videos to animated GIF images directly in your browser", + featuresTitle: "Features", + featuresDescription: + "Our free online converter offers several advantages over other conversion services:", + features: [ + { + title: "100% Private", + description: + "Your files never leave your device. All processing happens right in your browser.", + }, + { + title: "No Installation Required", + description: + "No need to download or install any software. Just open the page and start converting.", + }, + { + title: "High Quality Conversion", + description: + "We use industry-standard FFmpeg technology to create optimized GIFs from your videos.", + }, + ], + faqs: [ + { + question: "How does the MP4 to GIF converter work?", + answer: + "Our converter uses WebAssembly to run FFmpeg directly in your browser. When you upload an MP4 file, it gets processed locally on your device and converted to an animated GIF without ever being sent to a server.", + }, + { + question: "Is there a file size limit?", + answer: + "Yes, currently we limit file sizes to 500MB to ensure smooth performance in the browser. For larger files, you might need to use a desktop application.", + }, + { + question: "What quality settings are used for the GIF?", + answer: + "We use optimal settings for web-friendly GIFs with a frame rate of 10 FPS and appropriate resizing to balance quality and file size.", + }, + ], + cta: { + title: "Cap is the open source Loom alternative", + description: + "Record, edit, and share video messages with Cap. 100% open source and privacy focused.", + buttonText: "Download Cap Free", + }, + }; + + return ( + + } + /> + ); +} diff --git a/apps/web/app/tools/convert/mp4-to-mp3/page.tsx b/apps/web/app/tools/convert/mp4-to-mp3/page.tsx new file mode 100644 index 000000000..645c9fc8e --- /dev/null +++ b/apps/web/app/tools/convert/mp4-to-mp3/page.tsx @@ -0,0 +1,89 @@ +import { MediaFormatConverter } from "@/components/tools/MediaFormatConverter"; +import { ToolsPageTemplate } from "@/components/tools/ToolsPageTemplate"; +import { ToolPageContent } from "@/components/tools/types"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "MP4 to MP3 Converter | Extract Audio from Video | Cap", + description: + "Extract audio from MP4 videos and save as MP3 files directly in your browser. No uploads required, completely private and secure.", + openGraph: { + title: "MP4 to MP3 Converter | Extract Audio from Video", + description: + "Extract audio from MP4 videos and save as MP3 files. Process videos locally in your browser with no uploads for maximum privacy.", + images: [ + { + url: "/og.png", + width: 1200, + height: 630, + alt: "Cap MP4 to MP3 Converter Tool", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "MP4 to MP3 Converter | Extract Audio from Video", + description: + "Extract audio from MP4 videos and save as high-quality MP3 files. No uploads required, completely private and secure.", + images: ["/og.png"], + }, +}; + +export default function MP4ToMP3Page() { + const pageContent: ToolPageContent = { + title: "MP4 to MP3 Converter", + description: "Extract audio from MP4 videos and save as MP3 files", + featuresTitle: "Features", + featuresDescription: + "Our free online converter offers several advantages over other conversion services:", + features: [ + { + title: "100% Private", + description: + "Your files never leave your device. All processing happens right in your browser.", + }, + { + title: "No Installation Required", + description: + "No need to download or install any software. Just open the page and start converting.", + }, + { + title: "High Quality Conversion", + description: + "We use industry-standard FFmpeg technology to ensure high-quality audio extraction.", + }, + ], + faqs: [ + { + question: "How does the MP4 to MP3 converter work?", + answer: + "Our converter uses WebAssembly to run FFmpeg directly in your browser. When you upload an MP4 file, it extracts the audio track locally on your device and saves it as an MP3 file without ever being sent to a server.", + }, + { + question: "Is there a file size limit?", + answer: + "Yes, currently we limit file sizes to 500MB to ensure smooth performance in the browser. For larger files, you might need to use a desktop application.", + }, + { + question: "Will this affect the audio quality?", + answer: + "Our converter extracts the audio at a high bitrate of 192kbps, which maintains excellent quality while keeping file sizes reasonable.", + }, + ], + cta: { + title: "Cap is the open source Loom alternative", + description: + "Record, edit, and share video messages with Cap. 100% open source and privacy focused.", + buttonText: "Download Cap Free", + }, + }; + + return ( + + } + /> + ); +} diff --git a/apps/web/app/tools/convert/mp4-to-webm/page.tsx b/apps/web/app/tools/convert/mp4-to-webm/page.tsx new file mode 100644 index 000000000..1b717f074 --- /dev/null +++ b/apps/web/app/tools/convert/mp4-to-webm/page.tsx @@ -0,0 +1,89 @@ +import { MediaFormatConverter } from "@/components/tools/MediaFormatConverter"; +import { ToolsPageTemplate } from "@/components/tools/ToolsPageTemplate"; +import { ToolPageContent } from "@/components/tools/types"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "MP4 to WebM Converter | Free Online Video Converter | Cap", + description: + "Convert MP4 videos to WebM format for better web compatibility directly in your browser. Free online tool with no uploads required.", + openGraph: { + title: "MP4 to WebM Converter | Free Online Video Converter", + description: + "Convert MP4 videos to WebM format for better web compatibility. Process videos locally in your browser with no uploads required.", + images: [ + { + url: "/og.png", + width: 1200, + height: 630, + alt: "Cap MP4 to WebM Converter Tool", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "MP4 to WebM Converter | Free Online Video Converter", + description: + "Convert MP4 videos to WebM format for better web compatibility. Process locally with no uploads for maximum privacy.", + images: ["/og.png"], + }, +}; + +export default function MP4ToWebMPage() { + const pageContent: ToolPageContent = { + title: "MP4 to WebM Converter", + description: "Convert MP4 videos to WebM format directly in your browser", + featuresTitle: "Features", + featuresDescription: + "Our free online converter offers several advantages over other conversion services:", + features: [ + { + title: "100% Private", + description: + "Your files never leave your device. All processing happens right in your browser.", + }, + { + title: "No Installation Required", + description: + "No need to download or install any software. Just open the page and start converting.", + }, + { + title: "High Quality Conversion", + description: + "We use industry-standard FFmpeg technology to ensure high-quality conversion results.", + }, + ], + faqs: [ + { + question: "How does the MP4 to WebM converter work?", + answer: + "Our converter uses WebAssembly to run FFmpeg directly in your browser. When you upload an MP4 file, it gets processed locally on your device and converted to WebM format without ever being sent to a server.", + }, + { + question: "Is there a file size limit?", + answer: + "Yes, currently we limit file sizes to 500MB to ensure smooth performance in the browser. For larger files, you might need to use a desktop application.", + }, + { + question: "Why convert to WebM format?", + answer: + "WebM is an excellent format for web videos, offering good compression with high quality. It's well-supported by modern browsers and uses less bandwidth than many other formats.", + }, + ], + cta: { + title: "Cap is the open source Loom alternative", + description: + "Record, edit, and share video messages with Cap. 100% open source and privacy focused.", + buttonText: "Download Cap Free", + }, + }; + + return ( + + } + /> + ); +} diff --git a/apps/web/app/tools/convert/page.tsx b/apps/web/app/tools/convert/page.tsx new file mode 100644 index 000000000..98120010c --- /dev/null +++ b/apps/web/app/tools/convert/page.tsx @@ -0,0 +1,134 @@ +import { Metadata } from "next"; +import Link from "next/link"; + +export const metadata: Metadata = { + title: "File Conversion Tools | Free Online Converters", + description: + "Free online file conversion tools. Convert between various file formats directly in your browser with no uploads needed.", +}; + +interface ConversionTool { + title: string; + description: string; + href: string; + icon: string; +} + +const conversionTools: ConversionTool[] = [ + { + title: "WebM to MP4", + description: "Convert WebM videos to MP4 format directly in your browser", + href: "/tools/convert/webm-to-mp4", + icon: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z", + }, + { + title: "MP4 to MP3", + description: "Extract audio from MP4 videos and save as MP3 files", + href: "/tools/convert/mp4-to-mp3", + icon: "M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3", + }, + { + title: "MP4 to GIF", + description: "Convert MP4 videos to animated GIF images", + href: "/tools/convert/mp4-to-gif", + icon: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z", + }, + { + title: "MOV to MP4", + description: "Convert MOV videos to more compatible MP4 format", + href: "/tools/convert/mov-to-mp4", + icon: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z", + }, + { + title: "AVI to MP4", + description: "Convert AVI videos to modern MP4 format", + href: "/tools/convert/avi-to-mp4", + icon: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z", + }, + { + title: "MKV to MP4", + description: "Convert MKV videos to widely supported MP4 format", + href: "/tools/convert/mkv-to-mp4", + icon: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z", + }, + { + title: "MP4 to WebM", + description: + "Convert MP4 videos to WebM format for better web compatibility", + href: "/tools/convert/mp4-to-webm", + icon: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z", + }, +]; + +export default function ConvertToolsPage() { + return ( +
+
+
+ + + + + Back to All Tools + +
+ +

+ File Conversion Tools +

+

+ Our free online conversion tools help you transform files between + different formats without uploading them to any server. All + conversions happen directly in your browser for maximum privacy and + security. +

+ +
+ {conversionTools.map((tool) => ( + +
+
+ + + +
+

+ {tool.title} +

+
+

{tool.description}

+ + ))} +
+
+
+ ); +} diff --git a/apps/web/app/tools/convert/webm-to-mp4/page.tsx b/apps/web/app/tools/convert/webm-to-mp4/page.tsx new file mode 100644 index 000000000..9293e23f9 --- /dev/null +++ b/apps/web/app/tools/convert/webm-to-mp4/page.tsx @@ -0,0 +1,89 @@ +import { MediaFormatConverter } from "@/components/tools/MediaFormatConverter"; +import { ToolsPageTemplate } from "@/components/tools/ToolsPageTemplate"; +import { ToolPageContent } from "@/components/tools/types"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "WebM to MP4 Converter | Free Online Video Converter | Cap", + description: + "Convert WebM videos to MP4 format directly in your browser with our free online converter. No uploads, no installation, 100% private.", + openGraph: { + title: "WebM to MP4 Converter | Free Online Video Converter", + description: + "Convert WebM videos to MP4 format directly in your browser. No uploads required, processing happens locally for maximum privacy.", + images: [ + { + url: "/og.png", + width: 1200, + height: 630, + alt: "Cap WebM to MP4 Converter Tool", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "WebM to MP4 Converter | Free Online Video Converter", + description: + "Convert WebM videos to MP4 format directly in your browser. No uploads required, processing happens locally for maximum privacy.", + images: ["/og.png"], + }, +}; + +export default function WebmToMp4Page() { + const pageContent: ToolPageContent = { + title: "WebM to MP4 Converter", + description: "Convert WebM videos to MP4 format directly in your browser", + featuresTitle: "Features", + featuresDescription: + "Our free online converter offers several advantages over other conversion services:", + features: [ + { + title: "100% Private", + description: + "Your files never leave your device. All processing happens right in your browser.", + }, + { + title: "No Installation Required", + description: + "No need to download or install any software. Just open the page and start converting.", + }, + { + title: "High Quality Conversion", + description: + "We use industry-standard FFmpeg technology to ensure high-quality conversion results.", + }, + ], + faqs: [ + { + question: "How does the WebM to MP4 converter work?", + answer: + "Our converter uses WebAssembly to run FFmpeg directly in your browser. When you upload a WebM file, it gets processed locally on your device and converted to MP4 format without ever being sent to a server.", + }, + { + question: "Is there a file size limit?", + answer: + "Yes, currently we limit file sizes to 500MB to ensure smooth performance in the browser. For larger files, you might need to use a desktop application.", + }, + { + question: "Why should I use this converter instead of others?", + answer: + "Unlike many online converters that require uploading your files to their servers, our tool processes everything locally. This means your files never leave your device, providing maximum privacy and security.", + }, + ], + cta: { + title: "Cap is the open source Loom alternative", + description: + "Record, edit, and share video messages with Cap. 100% open source and privacy focused.", + buttonText: "Download Cap Free", + }, + }; + + return ( + + } + /> + ); +} diff --git a/apps/web/app/tools/layout.tsx b/apps/web/app/tools/layout.tsx new file mode 100644 index 000000000..90b8c3d67 --- /dev/null +++ b/apps/web/app/tools/layout.tsx @@ -0,0 +1,7 @@ +export default function ToolsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return
{children}
; +} diff --git a/apps/web/app/tools/page.tsx b/apps/web/app/tools/page.tsx new file mode 100644 index 000000000..376837806 --- /dev/null +++ b/apps/web/app/tools/page.tsx @@ -0,0 +1,12 @@ +import { Metadata } from "next"; +import { ToolsPageContent } from "../../components/ToolsPageContent"; + +export const metadata: Metadata = { + title: "Online Tools | Free Browser-Based Utilities", + description: + "Discover Cap's collection of free online tools for file conversion, video editing, and more. All tools run directly in your browser for maximum privacy.", +}; + +export default function ToolsPage() { + return ; +} diff --git a/apps/web/components/Footer.tsx b/apps/web/components/Footer.tsx index bab493bf7..15d0ae966 100644 --- a/apps/web/components/Footer.tsx +++ b/apps/web/components/Footer.tsx @@ -2,7 +2,7 @@ import { usePathname } from "next/navigation"; import { Logo, LogoBadge } from "@cap/ui"; - +import Link from "next/link"; type FooterLink = { label: string; href: string; @@ -20,7 +20,6 @@ const footerLinks = { href: "https://github.com/CapSoftware/Cap", isExternal: true, }, - { label: "Self-hosting", href: "/self-hosting" }, { label: "Join the community", href: "https://discord.gg/y8gdQ3WRN3", @@ -28,7 +27,10 @@ const footerLinks = { }, ] as FooterLink[], help: [ + { label: "About", href: "/about" }, + { label: "Testimonials", href: "/testimonials" }, { label: "FAQs", href: "/faq" }, + { label: "Self-hosting", href: "/self-hosting" }, { label: "Email Support", href: "mailto:hello@cap.so" }, { label: "Chat Support", @@ -54,6 +56,14 @@ const footerLinks = { isExternal: true, }, ] as FooterLink[], + tools: [ + { label: "WebM to MP4", href: "/tools/convert/webm-to-mp4" }, + { label: "MOV to MP4", href: "/tools/convert/mov-to-mp4" }, + { label: "AVI to MP4", href: "/tools/convert/avi-to-mp4" }, + { label: "MP4 to GIF", href: "/tools/convert/mp4-to-gif" }, + { label: "MP4 to MP3", href: "/tools/convert/mp4-to-mp3" }, + { label: "MP4 to WebM", href: "/tools/convert/mp4-to-webm" }, + ] as FooterLink[], useCases: [ { label: "Remote Team Collaboration", @@ -102,11 +112,12 @@ export const Footer = () => { style={{ boxShadow: "0px 2px 8px rgba(18, 22, 31, 0.02)" }} className="mx-auto max-w-[1400px] bg-gray-100 border-[1px] border-gray-200 p-8 lg:p-12 rounded-[20px] mb-10 relative overflow-hidden" > -
-
+
+ {/* Logo Column */} +
-

+

Cap is the open source alternative to Loom. Lightweight, powerful, and cross-platform. Record and share in seconds.

@@ -123,57 +134,15 @@ export const Footer = () => {
-
-

Product

- -
-
-

Help

- -
-
-

Socials

- -
-
-
+ + {/* Links Container Column */} +
+
+ {/* Product Column */}
-

Additional Links

-
+ + {/* Help Column */}
-

Use Cases

-
+ + {/* Socials Column */} +
+

Socials

+ + +
+ + Tools + + +
diff --git a/apps/web/components/ToolsPageContent.tsx b/apps/web/components/ToolsPageContent.tsx new file mode 100644 index 000000000..b2d0edfad --- /dev/null +++ b/apps/web/components/ToolsPageContent.tsx @@ -0,0 +1,162 @@ +"use client"; + +import Link from "next/link"; +import { Button } from "@cap/ui"; +import { useEffect } from "react"; + +interface ToolCategory { + title: string; + description: string; + href: string; + icon: string; +} + +const toolCategories: ToolCategory[] = [ + { + title: "File Conversion", + description: + "Convert between different file formats directly in your browser", + href: "/tools/convert", + icon: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z", + }, +]; + +export function ToolsPageContent() { + useEffect(() => { + const animateClouds = () => { + const cloud1 = document.getElementById("cloud-1"); + const cloud2 = document.getElementById("cloud-2"); + + if (cloud1 && cloud2) { + cloud1.animate( + [ + { transform: "translateX(0) translateY(0)" }, + { transform: "translateX(-10px) translateY(5px)" }, + { transform: "translateX(10px) translateY(-5px)" }, + { transform: "translateX(0) translateY(0)" }, + ], + { + duration: 15000, + iterations: Infinity, + easing: "ease-in-out", + } + ); + + cloud2.animate( + [ + { transform: "translateX(0) translateY(0)" }, + { transform: "translateX(10px) translateY(-5px)" }, + { transform: "translateX(-10px) translateY(5px)" }, + { transform: "translateX(0) translateY(0)" }, + ], + { + duration: 18000, + iterations: Infinity, + easing: "ease-in-out", + } + ); + } + }; + + animateClouds(); + }, []); + + return ( +
+
+
+

+ Try our free tools +

+

+ Powerful browser-based utilities that run directly on your device. + No uploads, no installations, maximum privacy. +

+
+ +
+ {toolCategories.map((category) => ( + +
+
+ + + +
+

+ {category.title} +

+

{category.description}

+
+ + ))} +
+ +
+
+ Footer Cloud One +
+
+ Footer Cloud Two +
+
+
+

+ The open source Loom alternative +

+

+ Cap is lightweight, powerful, and cross-platform. Record and + share securely in seconds with custom S3 bucket support. +

+
+
+ +
+
+
+
+
+ ); +} diff --git a/apps/web/components/UpgradeModal.tsx b/apps/web/components/UpgradeModal.tsx new file mode 100644 index 000000000..fb7c28fb2 --- /dev/null +++ b/apps/web/components/UpgradeModal.tsx @@ -0,0 +1,283 @@ +"use client"; + +import { useState } from "react"; +import { Button, Dialog, DialogContent, DialogTitle, Switch } from "@cap/ui"; +import { + Share2, + Database, + Shield, + Users, + Infinity, + Lock, + BarChart3, + Headphones, + X, + Minus, + Plus, + Globe, + Sparkles, +} from "lucide-react"; +import { getProPlanId } from "@cap/utils"; +import { useRouter } from "next/navigation"; +import toast from "react-hot-toast"; +import { useRive } from "@rive-app/react-canvas"; +import { AnimatePresence, motion } from "framer-motion"; + +interface UpgradeModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const modalVariants = { + hidden: { + opacity: 0, + scale: 0.95, + y: 10, + }, + visible: { + opacity: 1, + scale: 1, + y: 0, + transition: { + type: "spring", + duration: 0.4, + damping: 25, + stiffness: 500, + }, + }, + exit: { + opacity: 0, + scale: 0.95, + y: 10, + transition: { + duration: 0.2, + }, + }, +}; + +export const UpgradeModal = ({ open, onOpenChange }: UpgradeModalProps) => { + const [proLoading, setProLoading] = useState(false); + const [isAnnual, setIsAnnual] = useState(true); + const [proQuantity, setProQuantity] = useState(1); + const { push } = useRouter(); + + const pricePerUser = isAnnual ? 6 : 9; + const totalPrice = pricePerUser * proQuantity; + const billingText = isAnnual ? "billed annually" : "billed monthly"; + + const { RiveComponent: Pro, rive: riveInstance } = useRive({ + src: "/rive/pricing.riv", + artboard: "pro", + animations: ["items-coming-out"], + autoplay: true, + }); + + const handleProHover = () => { + if (riveInstance) { + riveInstance.play(["items-coming-out"]); + } + }; + + const proFeatures = [ + { + icon: , + title: "Custom domain", + description: "Connect your own domain to Cap", + }, + { + icon: , + title: "Unlimited sharing", + description: "Cloud storage & shareable links", + }, + { + icon: , + title: "Custom storage", + description: "Connect your own S3 bucket", + }, + { + icon: , + title: "Commercial license", + description: "Commercial license for desktop app automatically included", + }, + { + icon: , + title: "Team features", + description: "Collaborate with your team and create shared spaces", + }, + { + icon: , + title: "Cap AI (Coming Soon)", + description: "Automatic video chapters, summaries & more", + }, + { + icon: , + title: "Unlimited views", + description: "No limits on video views", + }, + { + icon: , + title: "Password protected videos", + description: "Enhanced security for your content", + }, + { + icon: , + title: "Analytics", + description: "Video viewing insights", + }, + { + icon: , + title: "Priority support", + description: "Get help when you need it", + }, + ]; + + const planCheckout = async () => { + setProLoading(true); + + const planId = getProPlanId(isAnnual ? "yearly" : "monthly"); + + const response = await fetch(`/api/settings/billing/subscribe`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ priceId: planId, quantity: proQuantity }), + }); + const data = await response.json(); + + if (data.auth === false) { + localStorage.setItem("pendingPriceId", planId); + localStorage.setItem("pendingQuantity", proQuantity.toString()); + push(`/login?next=/dashboard`); + return; + } + + if (data.subscription === true) { + toast.success("You are already on the Cap Pro plan"); + onOpenChange(false); + } + + if (data.url) { + window.location.href = data.url; + } + + setProLoading(false); + }; + + return ( + + + + {open && ( + +
+ Upgrade to Cap Pro +
+ +
+
+ +

+ Upgrade to Cap Pro +

+
+ +

+ You can cancel anytime. Early adopter pricing locked in. +

+ +
+
+

${totalPrice}/mo

+ + {proQuantity === 1 ? ( + `per user, ${billingText}` + ) : ( + <> + for {proQuantity} users,{" "} + {billingText} + + )} + +
+ +
+
+ Annual billing + setIsAnnual(!isAnnual)} + /> +
+ +
+ Users: +
+ +
+ {proQuantity} +
+ +
+
+
+
+ + +
+ +
+

+ Here's what's included +

+ +
+ {proFeatures.map((feature, index) => ( +
+
{feature.icon}
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} +
+
+
+ )} +
+
+
+ ); +}; diff --git a/apps/web/components/UsageButton.tsx b/apps/web/components/UsageButton.tsx index 2cfdd460f..abfcf0287 100644 --- a/apps/web/components/UsageButton.tsx +++ b/apps/web/components/UsageButton.tsx @@ -4,6 +4,8 @@ import { faArrowUp, faCheck } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import clsx from "clsx"; import Link from "next/link"; +import { useState } from "react"; +import { UpgradeModal } from "./UpgradeModal"; export const UsageButton = ({ subscribed, @@ -12,42 +14,89 @@ export const UsageButton = ({ subscribed: boolean; collapsed: boolean; }) => { - return ( - - - + + + ); + } + + return ( + <> + +
+ - - + +
+
+ + ); }; diff --git a/apps/web/components/forms/server.ts b/apps/web/components/forms/server.ts index 9672d58b4..57799fb65 100644 --- a/apps/web/components/forms/server.ts +++ b/apps/web/components/forms/server.ts @@ -12,13 +12,13 @@ export async function createSpace(args: { name: string }) { if (!user) throw new Error("Unauthorized"); const spaceId = nanoId(); - await db.insert(spaces).values({ + await db().insert(spaces).values({ id: spaceId, ownerId: user.id, name: args.name, }); - await db.insert(spaceMembers).values({ + await db().insert(spaceMembers).values({ id: nanoId(), userId: user.id, role: "owner", diff --git a/apps/web/components/tools/MediaFormatConverter.tsx b/apps/web/components/tools/MediaFormatConverter.tsx new file mode 100644 index 000000000..8a31bf877 --- /dev/null +++ b/apps/web/components/tools/MediaFormatConverter.tsx @@ -0,0 +1,818 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { Button } from "@cap/ui"; +import { FFmpeg } from "@ffmpeg/ffmpeg"; +import { fetchFile } from "@ffmpeg/util"; +import { trackEvent } from "@/app/utils/analytics"; +import { useRouter, usePathname } from "next/navigation"; +import Link from "next/link"; + +export const SUPPORTED_FORMATS = { + video: ["mp4", "webm", "mov", "avi", "mkv"], + audio: ["mp3"], + image: ["gif"], +}; + +export const FORMAT_GROUPS = { + video: ["mp4", "webm", "mov", "avi", "mkv"], + audio: ["mp3"], + image: ["gif"], +}; + +export const CONVERSION_CONFIGS: Record< + string, + { + acceptType: string; + command: (input: string, output: string) => string[]; + outputType: string; + title: (source: string, target: string) => string; + description: (source: string, target: string) => string; + } +> = { + "webm-to-mp4": { + acceptType: "video/webm", + command: (input, output) => [ + "-i", + input, + "-c:v", + "libx264", + "-preset", + "medium", + "-crf", + "23", + "-c:a", + "aac", + "-b:a", + "128k", + output, + ], + outputType: "video/mp4", + title: (source, target) => + `${source.toUpperCase()} to ${target.toUpperCase()} Converter`, + description: (source, target) => + `Convert ${source.toUpperCase()} videos to ${target.toUpperCase()} format directly in your browser`, + }, + "mp4-to-webm": { + acceptType: "video/mp4", + command: (input, output) => [ + "-i", + input, + "-c:v", + "libvpx", + "-crf", + "30", + "-b:v", + "0", + "-c:a", + "libvorbis", + output, + ], + outputType: "video/webm", + title: (source, target) => + `${source.toUpperCase()} to ${target.toUpperCase()} Converter`, + description: (source, target) => + `Convert ${source.toUpperCase()} videos to ${target.toUpperCase()} format directly in your browser`, + }, + "mov-to-mp4": { + acceptType: "video/quicktime", + command: (input, output) => [ + "-i", + input, + "-c:v", + "libx264", + "-preset", + "fast", + "-crf", + "23", + "-movflags", + "+faststart", + "-pix_fmt", + "yuv420p", + "-c:a", + "aac", + "-b:a", + "128k", + "-y", + output, + ], + outputType: "video/mp4", + title: (source, target) => + `${source.toUpperCase()} to ${target.toUpperCase()} Converter`, + description: (source, target) => + `Convert ${source.toUpperCase()} videos to ${target.toUpperCase()} format directly in your browser`, + }, + "avi-to-mp4": { + acceptType: "video/x-msvideo", + command: (input, output) => [ + "-i", + input, + "-c:v", + "libx264", + "-preset", + "medium", + "-crf", + "23", + "-c:a", + "aac", + "-b:a", + "128k", + output, + ], + outputType: "video/mp4", + title: (source, target) => + `${source.toUpperCase()} to ${target.toUpperCase()} Converter`, + description: (source, target) => + `Convert ${source.toUpperCase()} videos to ${target.toUpperCase()} format directly in your browser`, + }, + "mkv-to-mp4": { + acceptType: "video/x-matroska", + command: (input, output) => [ + "-i", + input, + "-c:v", + "libx264", + "-preset", + "medium", + "-crf", + "23", + "-c:a", + "aac", + "-b:a", + "128k", + output, + ], + outputType: "video/mp4", + title: (source, target) => + `${source.toUpperCase()} to ${target.toUpperCase()} Converter`, + description: (source, target) => + `Convert ${source.toUpperCase()} videos to ${target.toUpperCase()} format directly in your browser`, + }, + + "mp4-to-mp3": { + acceptType: "video/mp4", + command: (input, output) => [ + "-i", + input, + "-vn", + "-ar", + "44100", + "-ac", + "2", + "-b:a", + "192k", + output, + ], + outputType: "audio/mp3", + title: (source, target) => + `${source.toUpperCase()} to ${target.toUpperCase()} Converter`, + description: (source, target) => + `Extract audio from ${source.toUpperCase()} videos and save as ${target.toUpperCase()} files`, + }, + + "mp4-to-gif": { + acceptType: "video/mp4", + command: (input, output) => [ + "-i", + input, + "-vf", + "fps=10,scale=320:-1:flags=lanczos", + "-c:v", + "gif", + output, + ], + outputType: "image/gif", + title: (source, target) => + `${source.toUpperCase()} to ${target.toUpperCase()} Converter`, + description: (source, target) => + `Convert ${source.toUpperCase()} videos to animated ${target.toUpperCase()} images`, + }, +}; + +export const parseFormats = ( + conversionPath: string +): { sourceFormat: string; targetFormat: string } => { + const parts = conversionPath.split("-to-"); + return { + sourceFormat: parts[0] || "webm", + targetFormat: parts[1] || "mp4", + }; +}; + +export const getMimeType = (format: string): string => { + switch (format) { + case "mp4": + return "video/mp4"; + case "webm": + return "video/webm"; + case "mov": + return "video/quicktime"; + case "avi": + return "video/x-msvideo"; + case "mkv": + return "video/x-matroska"; + case "mp3": + return "audio/mp3"; + case "gif": + return "image/gif"; + default: + return ""; + } +}; + +export const getAcceptAttribute = (format: string): string => { + switch (format) { + case "mp4": + return "video/mp4"; + case "webm": + return "video/webm"; + case "mov": + return "video/quicktime"; + case "avi": + return "video/x-msvideo"; + case "mkv": + return "video/x-matroska"; + case "mp3": + return "audio/mp3"; + case "gif": + return "image/gif"; + default: + return ""; + } +}; + +interface MediaFormatConverterProps { + initialConversionPath: string; +} + +export const MediaFormatConverter = ({ + initialConversionPath, +}: MediaFormatConverterProps) => { + const router = useRouter(); + const pathname = usePathname() || ""; + + const { sourceFormat = "webm", targetFormat = "mp4" } = parseFormats( + initialConversionPath + ); + + const [file, setFile] = useState(null); + const [isConverting, setIsConverting] = useState(false); + const [progress, setProgress] = useState(0); + const [outputUrl, setOutputUrl] = useState(null); + const [error, setError] = useState(null); + const [ffmpegLoaded, setFfmpegLoaded] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [currentSourceFormat, setCurrentSourceFormat] = useState(sourceFormat); + const [currentTargetFormat, setCurrentTargetFormat] = useState(targetFormat); + + const conversionPath = `${currentSourceFormat}-to-${currentTargetFormat}`; + const config = CONVERSION_CONFIGS[conversionPath]; + + const ffmpegRef = useRef(null); + const fileInputRef = useRef(null); + + useEffect(() => { + if ( + sourceFormat !== currentSourceFormat || + targetFormat !== currentTargetFormat + ) { + try { + const basePath = pathname.split("/").slice(0, -1).join("/"); + const newPath = `${basePath}/${currentSourceFormat}-to-${currentTargetFormat}`; + router.push(newPath); + } catch (error) { + console.error("Error updating URL:", error); + } + } + }, [ + currentSourceFormat, + currentTargetFormat, + pathname, + router, + sourceFormat, + targetFormat, + ]); + + useEffect(() => { + const loadFFmpeg = async () => { + try { + const ffmpegInstance = new FFmpeg(); + ffmpegRef.current = ffmpegInstance; + + ffmpegInstance.on("progress", ({ progress }: { progress: number }) => { + setProgress(Math.round(progress * 100)); + }); + + await ffmpegInstance.load(); + setFfmpegLoaded(true); + trackEvent(`${conversionPath}_tool_loaded`); + } catch (err) { + setError("Failed to load FFmpeg. Please try again later."); + console.error("FFmpeg loading error:", err); + } + }; + + loadFFmpeg(); + + return () => { + if (outputUrl) { + URL.revokeObjectURL(outputUrl); + } + }; + }, [conversionPath]); + + const handleFileChange = (e: React.ChangeEvent) => { + const selectedFile = e.target.files?.[0]; + if (selectedFile) { + validateAndSetFile(selectedFile); + } + }; + + const validateAndSetFile = (selectedFile: File) => { + setError(null); + setOutputUrl(null); + + const expectedMimeType = getMimeType(currentSourceFormat); + + let isValidType = false; + + if (currentSourceFormat === "mov") { + isValidType = + selectedFile.type === "video/quicktime" || + selectedFile.type === "video/mov" || + selectedFile.name.toLowerCase().endsWith(".mov"); + } else if (currentSourceFormat === "mkv") { + isValidType = + selectedFile.type === "video/x-matroska" || + selectedFile.name.toLowerCase().endsWith(".mkv"); + } else if (currentSourceFormat === "avi") { + isValidType = + selectedFile.type === "video/x-msvideo" || + selectedFile.type === "video/avi" || + selectedFile.name.toLowerCase().endsWith(".avi"); + } else { + isValidType = + selectedFile.type === expectedMimeType || + selectedFile.type.includes(currentSourceFormat) || + selectedFile.name.toLowerCase().endsWith(`.${currentSourceFormat}`); + } + + if (!isValidType) { + setError(`Please select a ${currentSourceFormat.toUpperCase()} file.`); + trackEvent(`${conversionPath}_invalid_file_type`, { + fileType: selectedFile.type, + }); + return; + } + + if (selectedFile.size > 500 * 1024 * 1024) { + setError("File size exceeds 500MB limit."); + trackEvent(`${conversionPath}_file_too_large`, { + fileSize: selectedFile.size, + }); + return; + } + + setFile(selectedFile); + trackEvent(`${conversionPath}_file_selected`, { + fileSize: selectedFile.size, + }); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + + const droppedFile = e.dataTransfer.files[0]; + if (droppedFile) { + validateAndSetFile(droppedFile); + } + }; + + const convertFile = async () => { + if (!file || !ffmpegLoaded || !ffmpegRef.current || !config) return; + + setIsConverting(true); + setError(null); + setProgress(0); + + trackEvent(`${conversionPath}_conversion_started`, { + fileSize: file.size, + fileName: file.name, + }); + + try { + const ffmpeg = ffmpegRef.current; + const inputFileName = `input.${currentSourceFormat}`; + const outputFileName = `output.${currentTargetFormat}`; + + console.log(`Starting conversion: ${conversionPath}`); + console.log(`Input file: ${file.name}, size: ${file.size} bytes`); + + await ffmpeg.writeFile(inputFileName, await fetchFile(file)); + console.log("File written to FFmpeg virtual filesystem"); + + const command = config.command(inputFileName, outputFileName); + console.log("FFmpeg command:", command); + + await ffmpeg.exec(command); + console.log("FFmpeg command executed"); + + const data = await ffmpeg.readFile(outputFileName); + console.log(`Output data received, type: ${typeof data}`); + + if (!data) { + throw new Error( + "Conversion resulted in an empty file. Please try again." + ); + } + + const blob = new Blob([data], { type: config.outputType }); + console.log(`Output blob created, size: ${blob.size} bytes`); + + if (blob.size < 1024 && file.size > 10 * 1024) { + throw new Error( + "Conversion produced an unusually small file. It may be corrupted." + ); + } + + const url = URL.createObjectURL(blob); + + setOutputUrl(url); + + trackEvent(`${conversionPath}_conversion_completed`, { + fileSize: file.size, + fileName: file.name, + outputSize: blob.size, + conversionTime: Date.now(), + }); + + await ffmpeg.deleteFile(inputFileName); + await ffmpeg.deleteFile(outputFileName); + } catch (err: any) { + console.error("Detailed conversion error:", err); + + let errorMessage = "Conversion failed: "; + if (err.message) { + errorMessage += err.message; + } else if (typeof err === "string") { + errorMessage += err; + } else { + errorMessage += "Unknown error occurred during conversion"; + } + + setError(errorMessage); + + trackEvent(`${conversionPath}_conversion_failed`, { + fileSize: file.size, + fileName: file.name, + error: err.message || "Unknown error", + }); + } finally { + setIsConverting(false); + } + }; + + const handleDownload = () => { + if (!outputUrl || !file) return; + + const fileExtension = `.${currentSourceFormat}`; + const newExtension = `.${currentTargetFormat}`; + const downloadFileName = file.name.replace( + new RegExp(`${fileExtension}$`), + newExtension + ); + + trackEvent(`${conversionPath}_download_clicked`, { + fileName: downloadFileName, + }); + + const link = document.createElement("a"); + link.href = outputUrl; + link.download = downloadFileName; + link.click(); + }; + + const resetConverter = () => { + if (outputUrl) { + URL.revokeObjectURL(outputUrl); + } + setFile(null); + setOutputUrl(null); + setProgress(0); + setError(null); + + trackEvent(`${conversionPath}_reset`); + + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + const getValidSourceFormats = () => { + return Object.keys(CONVERSION_CONFIGS) + .map((path) => { + const { sourceFormat } = parseFormats(path); + return sourceFormat; + }) + .filter((value, index, self) => self.indexOf(value) === index); + }; + + const getValidTargetFormats = (source: string) => { + return Object.keys(CONVERSION_CONFIGS) + .filter((path) => path.startsWith(`${source}-to-`)) + .map((path) => { + const { targetFormat } = parseFormats(path); + return targetFormat; + }); + }; + + const validSourceFormats = getValidSourceFormats(); + const validTargetFormats = getValidTargetFormats(currentSourceFormat); + + const handleSourceFormatChange = (newSourceFormat: string) => { + setCurrentSourceFormat(newSourceFormat); + const newValidTargets = getValidTargetFormats(newSourceFormat); + if ( + newValidTargets.length > 0 && + !newValidTargets.includes(currentTargetFormat) + ) { + if (newValidTargets[0]) { + setCurrentTargetFormat(newValidTargets[0]); + } + } + resetConverter(); + }; + + const handleTargetFormatChange = (newTargetFormat: string) => { + setCurrentTargetFormat(newTargetFormat); + resetConverter(); + }; + + if (!config) { + return ( +
+

Unsupported conversion: {conversionPath}

+ + ← Back to Conversion Tools + +
+ ); + } + + return ( +
+

+ {config.title(currentSourceFormat, currentTargetFormat)} +

+ + {/* Format Selector */} +
+
+
+ + From: + +
+ {validSourceFormats.map((format) => ( + { + e.preventDefault(); + handleSourceFormatChange(format); + }} + className={`px-3 py-1.5 rounded-md text-sm font-medium text-center min-w-[60px] ${ + currentSourceFormat === format + ? "bg-blue-600 text-white" + : "bg-gray-200 text-gray-800 hover:bg-gray-300" + }`} + aria-label={`Convert from ${format.toUpperCase()} format`} + > + {format.toUpperCase()} + + ))} +
+
+ + +
+ +
+ +
+ + To: + +
+ {validTargetFormats.map((format) => ( + { + e.preventDefault(); + handleTargetFormatChange(format); + }} + className={`px-3 py-1.5 rounded-md text-sm font-medium text-center min-w-[60px] ${ + currentTargetFormat === format + ? "bg-blue-600 text-white" + : "bg-gray-200 text-gray-800 hover:bg-gray-300" + }`} + aria-label={`Convert to ${format.toUpperCase()} format`} + > + {format.toUpperCase()} + + ))} +
+
+
+
+ +
fileInputRef.current?.click()} + style={{ minHeight: "200px" }} + > + + +
+ {!file ? ( + <> + + + +

+ Drag and drop your {currentSourceFormat.toUpperCase()} file here +

+

+ or click to browse (max 500MB) +

+ + ) : ( + <> + + + +

{file.name}

+

+ {(file.size / (1024 * 1024)).toFixed(2)} MB +

+ + )} +
+
+ + {error && ( +
+ {error} +
+ )} + + {isConverting && ( +
+

+ Converting... {progress}% +

+
+
+
+
+ )} + + {outputUrl && ( +
+

+ Conversion complete! +

+ {config.outputType.startsWith("video/") && ( + + )} + {config.outputType.startsWith("audio/") && ( + + )} + {config.outputType.startsWith("image/") && ( + Converted GIF + )} +
+ +
+
+ )} + +
+ {file && !isConverting && !outputUrl && ( + + )} + + {(file || outputUrl) && ( + + )} +
+ + {!ffmpegLoaded && !error && ( +
+

Loading conversion engine...

+
+
+ )} + +
+

+ This converter works entirely in your browser. Your files are never + uploaded to any server. +

+

+ The conversion is performed using FFmpeg, which runs locally on your + device. +

+
+
+ ); +}; diff --git a/apps/web/components/tools/ToolsPageTemplate.tsx b/apps/web/components/tools/ToolsPageTemplate.tsx new file mode 100644 index 000000000..0f0e5b695 --- /dev/null +++ b/apps/web/components/tools/ToolsPageTemplate.tsx @@ -0,0 +1,342 @@ +"use client"; + +import { Button } from "@cap/ui"; +import { ToolPageContent } from "@/components/tools/types"; +import { useEffect, ReactNode } from "react"; +import { motion } from "framer-motion"; + +const renderHTML = (content: string) => { + const styledContent = content.replace( + /; +}; + +const LeftBlueHue = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; + +export const ToolsPageTemplate = ({ + content, + toolComponent, +}: { + content: ToolPageContent; + toolComponent: ReactNode; +}) => { + useEffect(() => { + const animateClouds = () => { + const cloud4 = document.getElementById("cloud-4"); + const cloud5 = document.getElementById("cloud-5"); + + if (cloud4 && cloud5) { + cloud4.animate( + [ + { transform: "translateX(0) translateY(0)" }, + { transform: "translateX(-10px) translateY(5px)" }, + { transform: "translateX(10px) translateY(-5px)" }, + { transform: "translateX(0) translateY(0)" }, + ], + { + duration: 15000, + iterations: Infinity, + easing: "ease-in-out", + } + ); + + cloud5.animate( + [ + { transform: "translateX(0) translateY(0)" }, + { transform: "translateX(10px) translateY(-5px)" }, + { transform: "translateX(-10px) translateY(5px)" }, + { transform: "translateX(0) translateY(0)" }, + ], + { + duration: 18000, + iterations: Infinity, + easing: "ease-in-out", + } + ); + } + }; + + animateClouds(); + }, []); + + return ( + <> + {/* Compact Hero Section */} +
+
+
+

+ {content.title} +

+

+ {content.description} +

+
+
+ + {/* Simplified Background Elements */} +
+ + + {/* Reduced number of clouds for cleaner look */} + + + +
+ + {/** Right Blue Hue */} +
+
+ + {/* Tool Container - Now positioned for visibility above the fold */} +
+
+ {toolComponent} +
+
+ + {/* Main Content - Features & FAQ */} +
+ {/* Features Section */} +
+
+

+ {content.featuresTitle} + +

+

+ {renderHTML(content.featuresDescription)} +

+
+
+ {content.features.map( + ( + feature: { title: string; description: string }, + index: number + ) => ( +
+
+ + {index + 1} + +
+

+ {feature.title} +

+

+ {renderHTML(feature.description)} +

+
+ ) + )} +
+
+ + {/* FAQ Section */} + {content.faqs && ( +
+
+

+ Frequently Asked Questions + +

+
+
+ {content.faqs.map( + (faq: { question: string; answer: string }, index: number) => ( +
+

+ {faq.question} +

+
+ {renderHTML(faq.answer)} +
+
+ ) + )} +
+
+ )} + + {/* Clean CTA Section */} +
+
+ Footer Cloud +
+
+ Footer Cloud +
+
+
+

+ {content.cta.title} +

+

+ {content.cta.description} +

+
+
+ +
+
+
+
+ + + + ); +}; diff --git a/apps/web/components/tools/types.ts b/apps/web/components/tools/types.ts new file mode 100644 index 000000000..3d93913e4 --- /dev/null +++ b/apps/web/components/tools/types.ts @@ -0,0 +1,19 @@ +export interface ToolPageContent { + title: string; + description: string; + featuresTitle: string; + featuresDescription: string; + features: { + title: string; + description: string; + }[]; + faqs?: { + question: string; + answer: string; + }[]; + cta: { + title: string; + description: string; + buttonText: string; + }; +} \ No newline at end of file diff --git a/apps/web/components/ui/Testimonials.tsx b/apps/web/components/ui/Testimonials.tsx index f4d595d34..8b1d469d8 100644 --- a/apps/web/components/ui/Testimonials.tsx +++ b/apps/web/components/ui/Testimonials.tsx @@ -2,6 +2,7 @@ import { testimonials, Testimonial } from "../../data/testimonials"; import Image from "next/image"; +import { motion } from "framer-motion"; interface TestimonialsProps { amount?: number; @@ -20,24 +21,46 @@ export const Testimonials = ({ ? testimonials.slice(0, amount) : testimonials; + const getRandomDelay = () => 0.15 + Math.random() * 0.3; + return (
{showHeader && ( <> -

+ {title} -

-

+ + {subtitle} -

+ )}
{displayedTestimonials.map((testimonial, i) => ( -
+ -
+ ))}
@@ -50,11 +73,13 @@ interface TestimonialCardProps { const TestimonialCard = ({ testimonial }: TestimonialCardProps) => { return ( -
@@ -78,6 +103,6 @@ const TestimonialCard = ({ testimonial }: TestimonialCardProps) => {

{testimonial.content}

-
+ ); }; diff --git a/apps/web/content/changelog/56.mdx b/apps/web/content/changelog/56.mdx index c04bfcb0a..c2b4af412 100644 --- a/apps/web/content/changelog/56.mdx +++ b/apps/web/content/changelog/56.mdx @@ -1,12 +1,12 @@ --- -title: AAC Audio, Compression Options, and Bug Fixes +title: Higher Quality Recordings, Compression Options & Bug Fixes app: Cap Desktop -publishedAt: "2025-04-16" +publishedAt: "2025-04-24" version: 0.3.42 image: --- -- Exported videos now use AAC audio for compatability with social sites +- Exported videos now use AAC audio for compatibility with social sites - Video quality can be controlled with compression options - Recordings on macOS now use native resolution again - Recording bitrate is no longer a fixed value, so higher resolution displays should use higher bitrates diff --git a/apps/web/instrumentation.ts b/apps/web/instrumentation.ts index 00428a5bf..84d0a0133 100644 --- a/apps/web/instrumentation.ts +++ b/apps/web/instrumentation.ts @@ -1,7 +1,7 @@ // This file is used to run database migrations in the docker builds or other self hosting environments. // It is not suitable (a.k.a DEADLY) for serverless environments where the server will be restarted on each request. -import { serverEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; export async function register() { if (process.env.NEXT_RUNTIME === "nodejs") { @@ -11,7 +11,7 @@ export async function register() { const triggerMigrations = async (retryCount = 0, maxRetries = 3) => { try { const response = await fetch( - `${serverEnv.WEB_URL}/api/selfhosted/migrations`, + `${serverEnv().WEB_URL}/api/selfhosted/migrations`, { method: "POST", headers: { diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 4822a969a..a388edf79 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -3,17 +3,17 @@ import type { NextRequest } from "next/server"; import { db } from "@cap/database"; import { spaces } from "@cap/database/schema"; import { eq } from "drizzle-orm"; -import { serverEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; import { notFound } from "next/navigation"; -const mainDomains = [ - "cap.so", - "cap.link", - "localhost", - serverEnv.WEB_URL, - serverEnv.VERCEL_URL, - serverEnv.VERCEL_BRANCH_URL, - serverEnv.VERCEL_PROJECT_PRODUCTION_URL, +const mainOrigins = [ + "https://cap.so", + "https://cap.link", + "http://localhost", + serverEnv().WEB_URL, + serverEnv().VERCEL_URL, + serverEnv().VERCEL_BRANCH_URL, + serverEnv().VERCEL_PROJECT_PRODUCTION_URL, ].filter(Boolean) as string[]; export async function middleware(request: NextRequest) { @@ -21,57 +21,30 @@ export async function middleware(request: NextRequest) { const hostname = url.hostname; const path = url.pathname; - const webUrl = new URL(serverEnv.WEB_URL).hostname; + const webUrl = new URL(serverEnv().WEB_URL).hostname; - if (mainDomains.some((d) => hostname.includes(d))) { + if ( + buildEnv.NEXT_PUBLIC_IS_CAP !== "true" || + mainOrigins.some((d) => url.origin === d) + ) { // We just let the request go through for main domains, page-level logic will handle redirects return NextResponse.next(); } - // We're on a custom domain at this point - // Only allow /s/ routes for custom domains - if (!path.startsWith("/s/")) { - const url = new URL(request.url); - url.hostname = webUrl; - return NextResponse.redirect(url); - } - - // Check if we have a cached verification - const verifiedDomain = request.cookies.get("verified_domain"); - if (verifiedDomain?.value === hostname) { - // Domain is verified from cache, handle CORS for API routes - // if (path.startsWith("/api/")) { - // if (request.method === "OPTIONS") { - // const response = new NextResponse(null, { status: 204 }); - // response.headers.set("Access-Control-Allow-Origin", "*"); - // response.headers.set( - // "Access-Control-Allow-Methods", - // "GET, POST, PUT, DELETE, OPTIONS" - // ); - // response.headers.set( - // "Access-Control-Allow-Headers", - // "Content-Type, Authorization" - // ); - // response.headers.set("Access-Control-Max-Age", "86400"); - // return response; - // } + try { + // We're on a custom domain at this point + // Only allow /s/ routes for custom domains + if (!path.startsWith("/s/")) { + const url = new URL(request.url); + url.hostname = webUrl; + console.log({ url }); + return NextResponse.redirect(url); + } - // const response = NextResponse.next(); - // response.headers.set("Access-Control-Allow-Origin", "*"); - // response.headers.set( - // "Access-Control-Allow-Methods", - // "GET, POST, PUT, DELETE, OPTIONS" - // ); - // response.headers.set( - // "Access-Control-Allow-Headers", - // "Content-Type, Authorization" - // ); - // return response; - // } - return NextResponse.next(); - } + // Check if we have a cached verification + const verifiedDomain = request.cookies.get("verified_domain"); + if (verifiedDomain?.value === hostname) return NextResponse.next(); - try { // Query the space with this custom domain const [space] = await db .select() @@ -85,50 +58,6 @@ export async function middleware(request: NextRequest) { return NextResponse.redirect(url); } - // Domain is verified at this point, handle CORS for API routes - // if (path.startsWith("/api/")) { - // if (request.method === "OPTIONS") { - // const response = new NextResponse(null, { status: 204 }); - // response.headers.set("Access-Control-Allow-Origin", "*"); - // response.headers.set( - // "Access-Control-Allow-Methods", - // "GET, POST, PUT, DELETE, OPTIONS" - // ); - // response.headers.set( - // "Access-Control-Allow-Headers", - // "Content-Type, Authorization" - // ); - // response.headers.set("Access-Control-Max-Age", "86400"); - // // Set verification cookie - // response.cookies.set("verified_domain", hostname, { - // httpOnly: true, - // secure: process.env.NODE_ENV === "production", - // sameSite: "strict", - // maxAge: 3600, // Cache for 1 hour - // }); - // return response; - // } - - // const response = NextResponse.next(); - // response.headers.set("Access-Control-Allow-Origin", "*"); - // response.headers.set( - // "Access-Control-Allow-Methods", - // "GET, POST, PUT, DELETE, OPTIONS" - // ); - // response.headers.set( - // "Access-Control-Allow-Headers", - // "Content-Type, Authorization" - // ); - // // Set verification cookie - // response.cookies.set("verified_domain", hostname, { - // httpOnly: true, - // secure: process.env.NODE_ENV === "production", - // sameSite: "strict", - // maxAge: 3600, // Cache for 1 hour - // }); - // return response; - // } - // Set verification cookie for non-API routes too const response = NextResponse.next(); response.cookies.set("verified_domain", hostname, { diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index af4f1c2b1..4d91a0c40 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -14,6 +14,10 @@ const nextConfig = { reactStrictMode: true, swcMinify: true, transpilePackages: ["@cap/ui", "@cap/utils", "@cap/web-api-contract"], + webpack: (config) => { + config.optimization.minimizer = []; + return config; + }, eslint: { ignoreDuringBuilds: true, }, diff --git a/apps/web/package.json b/apps/web/package.json index aaa922895..c536fe26f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -62,7 +62,6 @@ "jszip": "^3.10.1", "lodash": "^4.17.21", "lucide-react": "^0.294.0", - "masonic": "^4.1.0", "moment": "^2.30.1", "next": "14.2.3", "next-auth": "^4.24.5", @@ -78,7 +77,6 @@ "react-hook-form": "^7.49.2", "react-hot-toast": "^2.4.1", "react-player": "^2.14.1", - "react-responsive-masonry": "^2.7.1", "react-rnd": "^10.4.1", "react-scroll-parallax": "^3.4.5", "react-tooltip": "^5.26.3", diff --git a/apps/web/public/rive/pricing.riv b/apps/web/public/rive/pricing.riv new file mode 100644 index 0000000000000000000000000000000000000000..78c4c6f4617b6d42ee0ff49fe9dc832d40894c91 GIT binary patch literal 61357 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 literal 0 HcmV?d00001 diff --git a/apps/web/utils/cors.ts b/apps/web/utils/cors.ts index 618b50aea..c9885b6f5 100644 --- a/apps/web/utils/cors.ts +++ b/apps/web/utils/cors.ts @@ -1,39 +1,39 @@ -import { clientEnv, serverEnv } from "@cap/env"; +import { buildEnv } from "@cap/env"; export const allowedOrigins = [ - serverEnv.WEB_URL, - "http://localhost:3001", - "http://localhost:3000", - "tauri://localhost", - "http://tauri.localhost", - "https://tauri.localhost", - "https://cap.so", - "https://www.cap.so", - "https://cap.link", - "https://www.cap.link", + buildEnv.NEXT_PUBLIC_WEB_URL, + "http://localhost:3001", + "http://localhost:3000", + "tauri://localhost", + "http://tauri.localhost", + "https://tauri.localhost", + "https://cap.so", + "https://www.cap.so", + "https://cap.link", + "https://www.cap.link", ]; export function getCorsHeaders(origin: string | null, originalOrigin: string) { - return { - "Access-Control-Allow-Origin": - origin && allowedOrigins.includes(origin) - ? origin - : allowedOrigins.includes(originalOrigin) - ? originalOrigin - : "null", - "Access-Control-Allow-Credentials": "true", - }; + return { + "Access-Control-Allow-Origin": + origin && allowedOrigins.includes(origin) + ? origin + : allowedOrigins.includes(originalOrigin) + ? originalOrigin + : "null", + "Access-Control-Allow-Credentials": "true", + }; } export function getOptionsHeaders( - origin: string | null, - originalOrigin: string, - methods = "GET, OPTIONS" + origin: string | null, + originalOrigin: string, + methods = "GET, OPTIONS" ) { - return { - ...getCorsHeaders(origin, originalOrigin), - "Access-Control-Allow-Methods": methods, - "Access-Control-Allow-Headers": - "Content-Type, Authorization, sentry-trace, baggage", - }; + return { + ...getCorsHeaders(origin, originalOrigin), + "Access-Control-Allow-Methods": methods, + "Access-Control-Allow-Headers": + "Content-Type, Authorization, sentry-trace, baggage", + }; } diff --git a/apps/web/utils/dub.ts b/apps/web/utils/dub.ts index 82517d99a..fdefc646a 100644 --- a/apps/web/utils/dub.ts +++ b/apps/web/utils/dub.ts @@ -1,6 +1,7 @@ import { Dub } from "dub"; import { serverEnv } from "@cap/env"; -export const dub = new Dub({ - token: serverEnv.DUB_API_KEY, -}); +export const dub = () => + new Dub({ + token: serverEnv().DUB_API_KEY, + }); diff --git a/apps/web/utils/helpers.ts b/apps/web/utils/helpers.ts index ffc12fcb0..47f81dd4d 100644 --- a/apps/web/utils/helpers.ts +++ b/apps/web/utils/helpers.ts @@ -2,17 +2,17 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; import { type NextRequest } from "next/server"; import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; -import { clientEnv, serverEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; export function classNames(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)); } // Base allowed origins export const allowedOrigins = [ - serverEnv.WEB_URL, - "https://cap.link", - "cap.link", + buildEnv.NEXT_PUBLIC_WEB_URL, + "https://cap.link", + "cap.link", ]; export function getHeaders(origin: string) { @@ -28,43 +28,43 @@ export function getHeaders(origin: string) { const rateLimitMap = new Map(); export function rateLimitMiddleware( - limit: number, - request: NextRequest | Promise, - headersList: ReadonlyHeaders + limit: number, + request: NextRequest | Promise, + headersList: ReadonlyHeaders ) { - const ip = headersList.get("x-forwarded-for"); - const windowMs = 60 * 1000; + const ip = headersList.get("x-forwarded-for"); + const windowMs = 60 * 1000; - if (!rateLimitMap.has(ip)) { - rateLimitMap.set(ip, { - count: 0, - lastReset: Date.now(), - }); - } + if (!rateLimitMap.has(ip)) { + rateLimitMap.set(ip, { + count: 0, + lastReset: Date.now(), + }); + } - const ipData = rateLimitMap.get(ip) as { - count: number; - lastReset: number; - }; + const ipData = rateLimitMap.get(ip) as { + count: number; + lastReset: number; + }; - if (Date.now() - ipData.lastReset > windowMs) { - ipData.count = 0; - ipData.lastReset = Date.now(); - } + if (Date.now() - ipData.lastReset > windowMs) { + ipData.count = 0; + ipData.lastReset = Date.now(); + } - if (ipData.count >= limit) { - return new Response("Too many requests", { - status: 429, - }); - } + if (ipData.count >= limit) { + return new Response("Too many requests", { + status: 429, + }); + } - ipData.count += 1; + ipData.count += 1; - return request; + return request; } export const CACHE_CONTROL_HEADERS = { - "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate", - Pragma: "no-cache", - Expires: "0", + "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate", + Pragma: "no-cache", + Expires: "0", }; diff --git a/apps/web/utils/s3.ts b/apps/web/utils/s3.ts index 351085515..2555280e2 100644 --- a/apps/web/utils/s3.ts +++ b/apps/web/utils/s3.ts @@ -2,101 +2,98 @@ import { S3Client } from "@aws-sdk/client-s3"; import type { s3Buckets } from "@cap/database/schema"; import type { InferSelectModel } from "drizzle-orm"; import { decrypt } from "@cap/database/crypto"; -import { clientEnv, serverEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; type S3Config = { - endpoint?: string | null; - region?: string; - accessKeyId?: string; - secretAccessKey?: string; - forcePathStyle?: boolean; + endpoint?: string | null; + region?: string; + accessKeyId?: string; + secretAccessKey?: string; + forcePathStyle?: boolean; } | null; async function tryDecrypt( - text: string | null | undefined + text: string | null | undefined ): Promise { - if (!text) return undefined; - try { - const decrypted = await decrypt(text); - return decrypted; - } catch (error) { - return text; - } + if (!text) return undefined; + try { + const decrypted = await decrypt(text); + return decrypted; + } catch (error) { + return text; + } } export async function getS3Config(config?: S3Config) { - if (!config) { - return { - endpoint: serverEnv.CAP_AWS_ENDPOINT, - region: serverEnv.CAP_AWS_REGION, - credentials: { - accessKeyId: serverEnv.CAP_AWS_ACCESS_KEY ?? "", - secretAccessKey: serverEnv.CAP_AWS_SECRET_KEY ?? "", - }, - forcePathStyle: - serverEnv.CAP_AWS_ENDPOINT?.includes("localhost"), - }; - } + if (!config) { + return { + endpoint: serverEnv().CAP_AWS_ENDPOINT, + region: serverEnv().CAP_AWS_REGION, + credentials: { + accessKeyId: serverEnv().CAP_AWS_ACCESS_KEY ?? "", + secretAccessKey: serverEnv().CAP_AWS_SECRET_KEY ?? "", + }, + forcePathStyle: serverEnv().CAP_AWS_ENDPOINT?.includes("localhost"), + }; + } - const endpoint = config.endpoint - ? await tryDecrypt(config.endpoint) - : serverEnv.CAP_AWS_ENDPOINT; + const endpoint = config.endpoint + ? await tryDecrypt(config.endpoint) + : serverEnv().CAP_AWS_ENDPOINT; - const region = - (await tryDecrypt(config.region)) ?? serverEnv.CAP_AWS_REGION; + const region = + (await tryDecrypt(config.region)) ?? serverEnv().CAP_AWS_REGION; - const finalRegion = endpoint?.includes("localhost") ? "us-east-1" : region; + const finalRegion = endpoint?.includes("localhost") ? "us-east-1" : region; - const isLocalOrMinio = - endpoint?.includes("localhost") || endpoint?.includes("127.0.0.1"); + const isLocalOrMinio = + endpoint?.includes("localhost") || endpoint?.includes("127.0.0.1"); - return { - endpoint, - region: finalRegion, - credentials: { - accessKeyId: - (await tryDecrypt(config.accessKeyId)) ?? - serverEnv.CAP_AWS_ACCESS_KEY ?? - "", - secretAccessKey: - (await tryDecrypt(config.secretAccessKey)) ?? - serverEnv.CAP_AWS_SECRET_KEY ?? - "", - }, - forcePathStyle: config.forcePathStyle ?? true, - useArnRegion: false, - requestHandler: { - connectionTimeout: isLocalOrMinio ? 5000 : 10000, - socketTimeout: isLocalOrMinio ? 30000 : 60000, - }, - }; + return { + endpoint, + region: finalRegion, + credentials: { + accessKeyId: + (await tryDecrypt(config.accessKeyId)) ?? + serverEnv().CAP_AWS_ACCESS_KEY ?? + "", + secretAccessKey: + (await tryDecrypt(config.secretAccessKey)) ?? + serverEnv().CAP_AWS_SECRET_KEY ?? + "", + }, + forcePathStyle: config.forcePathStyle ?? true, + useArnRegion: false, + requestHandler: { + connectionTimeout: isLocalOrMinio ? 5000 : 10000, + socketTimeout: isLocalOrMinio ? 30000 : 60000, + }, + }; } export async function getS3Bucket( - bucket?: InferSelectModel | null + bucket?: InferSelectModel | null ) { - if (!bucket?.bucketName) { - return serverEnv.CAP_AWS_BUCKET || ""; - } + if (!bucket?.bucketName) { + return serverEnv().CAP_AWS_BUCKET || ""; + } - return ( - ((await tryDecrypt(bucket.bucketName)) ?? - serverEnv.CAP_AWS_BUCKET) || - "" - ); + return ( + ((await tryDecrypt(bucket.bucketName)) ?? serverEnv().CAP_AWS_BUCKET) || "" + ); } export async function createS3Client(config?: S3Config) { - const s3Config = await getS3Config(config); - const isLocalOrMinio = - s3Config.endpoint?.includes("localhost") || - s3Config.endpoint?.includes("127.0.0.1"); + const s3Config = await getS3Config(config); + const isLocalOrMinio = + s3Config.endpoint?.includes("localhost") || + s3Config.endpoint?.includes("127.0.0.1"); - return [ - new S3Client({ - ...s3Config, - maxAttempts: isLocalOrMinio ? 5 : 3, - }), - s3Config, - ] as const; + return [ + new S3Client({ + ...s3Config, + maxAttempts: isLocalOrMinio ? 5 : 3, + }), + s3Config, + ] as const; } diff --git a/apps/web/utils/video/upload/helpers.ts b/apps/web/utils/video/upload/helpers.ts index 08d437822..9b2a82015 100644 --- a/apps/web/utils/video/upload/helpers.ts +++ b/apps/web/utils/video/upload/helpers.ts @@ -1,62 +1,59 @@ -import { clientEnv, serverEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; export async function uploadToS3({ - filename, - blobData, - userId, - duration, - resolution, - videoCodec, - audioCodec, - awsBucket, - awsRegion, + filename, + blobData, + userId, + duration, + resolution, + videoCodec, + audioCodec, + awsBucket, + awsRegion, }: { - filename: string; - blobData: Blob; - userId: string; - duration?: string; - resolution?: string; - videoCodec?: string; - audioCodec?: string; - awsBucket: string; - awsRegion: string; + filename: string; + blobData: Blob; + userId: string; + duration?: string; + resolution?: string; + videoCodec?: string; + audioCodec?: string; + awsBucket: string; + awsRegion: string; }) { - const response = await fetch( - `${serverEnv.WEB_URL}/api/upload/signed`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - userId: userId, - fileKey: filename, - duration: duration, - resolution: resolution, - videoCodec: videoCodec, - audioCodec: audioCodec, - awsBucket: awsBucket, - awsRegion: awsRegion, - }), - } - ); + const response = await fetch(`${serverEnv().WEB_URL}/api/upload/signed`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + userId: userId, + fileKey: filename, + duration: duration, + resolution: resolution, + videoCodec: videoCodec, + audioCodec: audioCodec, + awsBucket: awsBucket, + awsRegion: awsRegion, + }), + }); - const { presignedPostData } = await response.json(); + const { presignedPostData } = await response.json(); - const formData = new FormData(); - Object.entries(presignedPostData.fields).forEach(([key, value]) => { - formData.append(key, value as string); - }); - formData.append("file", blobData); + const formData = new FormData(); + Object.entries(presignedPostData.fields).forEach(([key, value]) => { + formData.append(key, value as string); + }); + formData.append("file", blobData); - const uploadResponse = await fetch(presignedPostData.url, { - method: "POST", - body: formData, - }); + const uploadResponse = await fetch(presignedPostData.url, { + method: "POST", + body: formData, + }); - if (!uploadResponse.ok) { - return false; - } + if (!uploadResponse.ok) { + return false; + } - return true; + return true; } diff --git a/package.json b/package.json index 0d88b46a4..1c525af6d 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,8 @@ "env-setup": "node scripts/env-cli.js" }, "devDependencies": { - "@clack/core": "^0.4.1", "@clack/prompts": "^0.10.0", - "@turbo/gen": "^1.9.7", - "@types/aws-lambda": "8.10.148", "dotenv-cli": "latest", - "eslint": "^7.32.0", - "extract-zip": "^2.0.1", "prettier": "^2.5.1", "turbo": "^2.3.4", "typescript": "^5.7.2" @@ -44,8 +39,5 @@ "patchedDependencies": { "@kobalte/core@0.13.7": "patches/@kobalte__core@0.13.7.patch" } - }, - "dependencies": { - "sst": "3.10.13" } } diff --git a/packages/database/auth/auth-options.ts b/packages/database/auth/auth-options.ts index c55ddcd47..87856f867 100644 --- a/packages/database/auth/auth-options.ts +++ b/packages/database/auth/auth-options.ts @@ -15,138 +15,152 @@ export const config = { maxDuration: 120, }; -const secret = serverEnv.NEXTAUTH_SECRET; +export const authOptions = (): NextAuthOptions => { + let _adapter; + let _providers; -export const authOptions: NextAuthOptions = { - adapter: DrizzleAdapter(db), - debug: true, - session: { - strategy: "jwt", - }, - secret: secret as string, - pages: { - signIn: "/login", - }, - providers: [ - GoogleProvider({ - clientId: serverEnv.GOOGLE_CLIENT_ID!, - clientSecret: serverEnv.GOOGLE_CLIENT_SECRET!, - authorization: { - params: { - scope: [ - "https://www.googleapis.com/auth/userinfo.email", - "https://www.googleapis.com/auth/userinfo.profile", - ].join(" "), - prompt: "select_account", + return { + get adapter() { + if (_adapter) return _adapter; + _adapter = DrizzleAdapter(db()); + return _adapter; + }, + debug: true, + session: { + strategy: "jwt", + }, + get secret() { + return serverEnv().NEXTAUTH_SECRET; + }, + pages: { + signIn: "/login", + }, + get providers() { + if (_providers) return _providers; + _providers = [ + GoogleProvider({ + clientId: serverEnv().GOOGLE_CLIENT_ID!, + clientSecret: serverEnv().GOOGLE_CLIENT_SECRET!, + authorization: { + params: { + scope: [ + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", + ].join(" "), + prompt: "select_account", + }, + }, + }), + WorkOSProvider({ + clientId: serverEnv().WORKOS_CLIENT_ID as string, + clientSecret: serverEnv().WORKOS_API_KEY as string, + profile(profile) { + return { + id: profile.id, + name: profile.first_name + ? `${profile.first_name} ${profile.last_name || ""}` + : profile.email?.split("@")[0] || profile.id, + email: profile.email, + image: profile.profile_picture_url, + }; + }, + }), + EmailProvider({ + sendVerificationRequest({ identifier, url }) { + if ( + NODE_ENV === "development" || + !serverEnv().RESEND_API_KEY || + serverEnv().RESEND_API_KEY === "" + ) { + console.log(`Login link: ${url}`); + } else { + sendEmail({ + email: identifier, + subject: `Your Cap Login Link`, + react: LoginLink({ url, email: identifier }), + }); + } + }, + }), + ]; + + return _providers; + }, + cookies: { + sessionToken: { + name: `next-auth.session-token`, + options: { + httpOnly: true, + sameSite: "none", + path: "/", + secure: true, }, }, - }), - WorkOSProvider({ - clientId: serverEnv.WORKOS_CLIENT_ID as string, - clientSecret: serverEnv.WORKOS_API_KEY as string, - profile(profile) { - return { - id: profile.id, - name: profile.first_name - ? `${profile.first_name} ${profile.last_name || ""}` - : profile.email?.split("@")[0] || profile.id, - email: profile.email, - image: profile.profile_picture_url, - }; - }, - }), - EmailProvider({ - sendVerificationRequest({ identifier, url }) { - if ( - NODE_ENV === "development" || - !serverEnv.RESEND_API_KEY || - serverEnv.RESEND_API_KEY === "" - ) { - console.log(`Login link: ${url}`); - } else { - sendEmail({ - email: identifier, - subject: `Your Cap Login Link`, - react: LoginLink({ url, email: identifier }), - }); - } - }, - }), - ], - cookies: { - sessionToken: { - name: `next-auth.session-token`, - options: { - httpOnly: true, - sameSite: "none", - path: "/", - secure: true, - }, }, - }, - events: { - async signIn({ user, account, isNewUser }) { - if (isNewUser) { - // Create initial space for the user - const spaceId = nanoId(); + events: { + async signIn({ user, account, isNewUser }) { + if (isNewUser) { + // Create initial space for the user + const spaceId = nanoId(); - // Create space - await db.insert(spaces).values({ - id: spaceId, - name: "My Space", - ownerId: user.id, - }); + // Create space + await db.insert(spaces).values({ + id: spaceId, + name: "My Space", + ownerId: user.id, + }); - // Add user as member of the space - await db.insert(spaceMembers).values({ - id: nanoId(), - userId: user.id, - spaceId: spaceId, - role: "owner", - }); + // Add user as member of the space + await db.insert(spaceMembers).values({ + id: nanoId(), + userId: user.id, + spaceId: spaceId, + role: "owner", + }); - // Update user's activeSpaceId - await db - .update(users) - .set({ activeSpaceId: spaceId }) - .where(eq(users.id, user.id)); - } + // Update user's activeSpaceId + await db + .update(users) + .set({ activeSpaceId: spaceId }) + .where(eq(users.id, user.id)); + } + }, }, - }, - callbacks: { - async session({ token, session }) { - if (!session.user) return session; + callbacks: { + async session({ token, session }) { + if (!session.user) return session; - if (token) { - session.user.id = token.id; - session.user.name = token.name; - session.user.email = token.email; - session.user.image = token.picture; - } + if (token) { + session.user.id = token.id; + session.user.name = token.name; + session.user.email = token.email; + session.user.image = token.picture; + } - return session; - }, - async jwt({ token, user }) { - const [dbUser] = await db - .select() - .from(users) - .where(eq(users.email, token.email || "")) - .limit(1); + return session; + }, + async jwt({ token, user }) { + const [dbUser] = await db + .select() + .from(users) + .where(eq(users.email, token.email || "")) + .limit(1); - if (!dbUser) { - if (user) { - token.id = user?.id; + if (!dbUser) { + if (user) { + token.id = user?.id; + } + return token; } - return token; - } - return { - id: dbUser.id, - name: dbUser.name, - lastName: dbUser.lastName, - email: dbUser.email, - picture: dbUser.image, - }; + return { + id: dbUser.id, + name: dbUser.name, + lastName: dbUser.lastName, + email: dbUser.email, + picture: dbUser.image, + }; + }, }, - }, + }; }; diff --git a/packages/database/auth/drizzle-adapter.ts b/packages/database/auth/drizzle-adapter.ts index 3538997e1..f347358aa 100644 --- a/packages/database/auth/drizzle-adapter.ts +++ b/packages/database/auth/drizzle-adapter.ts @@ -25,8 +25,8 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { const row = rows[0]; if (!row) throw new Error("User not found"); - if (STRIPE_AVAILABLE) { - const customer = await stripe.customers.create({ + if (STRIPE_AVAILABLE()) { + const customer = await stripe().customers.create({ email: userData.email, metadata: { userId: nanoId(), diff --git a/packages/database/crypto.ts b/packages/database/crypto.ts index 6aa74cb5d..7d16510ab 100644 --- a/packages/database/crypto.ts +++ b/packages/database/crypto.ts @@ -6,28 +6,31 @@ const SALT_LENGTH = 16; const KEY_LENGTH = 32; const ITERATIONS = 100000; -const ENCRYPTION_KEY = serverEnv.DATABASE_ENCRYPTION_KEY as string; - -// Verify the encryption key is valid hex and correct length -try { - const keyBuffer = Buffer.from(ENCRYPTION_KEY, "hex"); - if (keyBuffer.length !== KEY_LENGTH) { - throw new Error( - `Encryption key must be ${KEY_LENGTH} bytes (${ - KEY_LENGTH * 2 - } hex characters)` - ); - } -} catch (error: unknown) { - if (error instanceof Error) { - throw new Error(`Invalid encryption key format: ${error.message}`); +const ENCRYPTION_KEY = () => { + const key = serverEnv().DATABASE_ENCRYPTION_KEY; + // Verify the encryption key is valid hex and correct length + try { + const keyBuffer = Buffer.from(key, "hex"); + if (keyBuffer.length !== KEY_LENGTH) { + throw new Error( + `Encryption key must be ${KEY_LENGTH} bytes (${ + KEY_LENGTH * 2 + } hex characters)` + ); + } + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error(`Invalid encryption key format: ${error.message}`); + } + throw new Error("Invalid encryption key format"); } - throw new Error("Invalid encryption key format"); -} + + return key; +}; async function deriveKey(salt: Uint8Array): Promise { // Convert hex string to ArrayBuffer for Web Crypto API - const keyBuffer = Buffer.from(ENCRYPTION_KEY, "hex"); + const keyBuffer = Buffer.from(ENCRYPTION_KEY(), "hex"); const keyMaterial = await crypto.subtle.importKey( "raw", diff --git a/packages/database/drizzle.config.ts b/packages/database/drizzle.config.ts index dfc8589fb..4195dc069 100644 --- a/packages/database/drizzle.config.ts +++ b/packages/database/drizzle.config.ts @@ -1,7 +1,7 @@ import { serverEnv } from "@cap/env"; import type { Config } from "drizzle-kit"; -const URL = serverEnv.DATABASE_MIGRATION_URL ?? serverEnv.DATABASE_URL; +const URL = serverEnv().DATABASE_MIGRATION_URL ?? serverEnv().DATABASE_URL; if (!URL) throw new Error("DATABASE_URL or DATABASE_MIGRATION_URL must be set!"); diff --git a/packages/database/emails/config.ts b/packages/database/emails/config.ts index 4d8838c48..dc163ddc2 100644 --- a/packages/database/emails/config.ts +++ b/packages/database/emails/config.ts @@ -1,48 +1,48 @@ -import { clientEnv, serverEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; import { JSXElementConstructor, ReactElement } from "react"; import { Resend } from "resend"; -export const resend = serverEnv.RESEND_API_KEY - ? new Resend(serverEnv.RESEND_API_KEY) - : null; +export const resend = () => + serverEnv().RESEND_API_KEY ? new Resend(serverEnv().RESEND_API_KEY) : null; // Augment the CreateEmailOptions type to include scheduledAt type EmailOptions = { - from: string; - to: string | string[]; - subject: string; - react: ReactElement>; - scheduledAt?: string; + from: string; + to: string | string[]; + subject: string; + react: ReactElement>; + scheduledAt?: string; }; export const sendEmail = async ({ - email, - subject, - react, - marketing, - test, - scheduledAt, + email, + subject, + react, + marketing, + test, + scheduledAt, }: { - email: string; - subject: string; - react: ReactElement>; - marketing?: boolean; - test?: boolean; - scheduledAt?: string; + email: string; + subject: string; + react: ReactElement>; + marketing?: boolean; + test?: boolean; + scheduledAt?: string; }) => { - if (!resend) { - return Promise.resolve(); - } + const r = resend(); + if (!r) { + return Promise.resolve(); + } - return resend.emails.send({ - from: marketing - ? "Richie from Cap " - : clientEnv.NEXT_PUBLIC_IS_CAP - ? "Cap Auth " - : `auth@${serverEnv.WEB_URL}`, - to: test ? "delivered@resend.dev" : email, - subject, - react, - scheduledAt, - } as EmailOptions) as any; + return r.emails.send({ + from: marketing + ? "Richie from Cap " + : buildEnv.NEXT_PUBLIC_IS_CAP + ? "Cap Auth " + : `auth@${buildEnv.NEXT_PUBLIC_WEB_URL}`, + to: test ? "delivered@resend.dev" : email, + subject, + react, + scheduledAt, + } as EmailOptions) as any; }; diff --git a/packages/database/index.ts b/packages/database/index.ts index f7ccd21e2..5866114ae 100644 --- a/packages/database/index.ts +++ b/packages/database/index.ts @@ -2,22 +2,32 @@ import { drizzle } from "drizzle-orm/planetscale-serverless"; import { Client, Config } from "@planetscale/database"; import { serverEnv } from "@cap/env"; -const URL = serverEnv.DATABASE_URL; +function createDrizzle() { + const URL = serverEnv().DATABASE_URL; -let fetchHandler: Promise | undefined = undefined; + let fetchHandler: Promise | undefined = undefined; -if (URL.startsWith("mysql://")) { - fetchHandler = import("@mattrax/mysql-planetscale").then((m) => - m.createFetchHandler(URL) - ); -} + if (URL.startsWith("mysql://")) { + fetchHandler = import("@mattrax/mysql-planetscale").then((m) => + m.createFetchHandler(URL) + ); + } + + const connection = new Client({ + url: URL, + fetch: async (input, init) => { + return await ((await fetchHandler) || fetch)(input, init); + }, + }); -export const connection = new Client({ - url: URL, + return drizzle(connection); +} - fetch: async (input, init) => { - return await ((await fetchHandler) || fetch)(input, init); - }, -}); +let _cached: ReturnType | undefined = undefined; -export const db = drizzle(connection); +export const db = () => { + if (!_cached) { + _cached = createDrizzle(); + } + return _cached; +}; diff --git a/packages/env/build.ts b/packages/env/build.ts new file mode 100644 index 000000000..f8ad86437 --- /dev/null +++ b/packages/env/build.ts @@ -0,0 +1,33 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; + +export const NODE_ENV = process.env.NODE_ENV as string; + +// Environment variables that are needed in the build process, and may be incorrect on the client. +// Some are only provided by `NEXT_PUBLIC`, but others can be provdied at runtime +export const buildEnv = createEnv({ + client: { + NEXT_PUBLIC_IS_CAP: z.string().optional(), + NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), + NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(), + NEXT_PUBLIC_WEB_URL: z.string(), + NEXT_PUBLIC_CAP_AWS_BUCKET: z.string(), + NEXT_PUBLIC_CAP_AWS_REGION: z.string(), + NEXT_PUBLIC_CAP_AWS_ENDPOINT: z.string().optional(), + NEXT_PUBLIC_CAP_AWS_BUCKET_URL: z.string().optional(), + }, + runtimeEnv: { + NEXT_PUBLIC_IS_CAP: process.env.NEXT_PUBLIC_IS_CAP, + NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, + NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, + NEXT_PUBLIC_WEB_URL: process.env.WEB_URL ?? process.env.NEXT_PUBLIC_WEB_URL, + NEXT_PUBLIC_CAP_AWS_BUCKET: + process.env.CAP_AWS_BUCKET ?? process.env.NEXT_PUBLIC_CAP_AWS_BUCKET, + NEXT_PUBLIC_CAP_AWS_REGION: + process.env.CAP_AWS_REGION ?? process.env.NEXT_PUBLIC_CAP_AWS_REGION, + NEXT_PUBLIC_CAP_AWS_ENDPOINT: + process.env.CAP_AWS_REGION ?? process.env.NEXT_PUBLIC_CAP_AWS_ENDPOINT, + NEXT_PUBLIC_CAP_AWS_BUCKET_URL: + process.env.CAP_AWS_URL ?? process.env.NEXT_PUBLIC_CAP_AWS_BUCKET_URL, + }, +}); diff --git a/packages/env/client.ts b/packages/env/client.ts deleted file mode 100644 index 7770b7d6a..000000000 --- a/packages/env/client.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createEnv } from "@t3-oss/env-nextjs"; -import { z } from "zod"; - -export const NODE_ENV = process.env.NODE_ENV as string; - -export const clientEnv = createEnv({ - client: { - NEXT_PUBLIC_IS_CAP: z.string().optional(), - NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), - NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(), - }, - runtimeEnv: { - NEXT_PUBLIC_IS_CAP: process.env.NEXT_PUBLIC_IS_CAP, - NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, - NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, - }, -}); diff --git a/packages/env/index.ts b/packages/env/index.ts index 40db60cf6..51282f4fb 100644 --- a/packages/env/index.ts +++ b/packages/env/index.ts @@ -1,2 +1,2 @@ -export { clientEnv, NODE_ENV } from "./client"; +export { buildEnv, NODE_ENV } from "./build"; export { serverEnv } from "./server"; diff --git a/packages/env/server.ts b/packages/env/server.ts index 0e7e0b7f3..19b1fd83e 100644 --- a/packages/env/server.ts +++ b/packages/env/server.ts @@ -1,43 +1,54 @@ import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; -export const serverEnv = createEnv({ - server: { - NODE_ENV: z.string(), - DATABASE_URL: z.string(), - DATABASE_MIGRATION_URL: z.string().optional(), - DATABASE_ENCRYPTION_KEY: z.string().optional(), - WEB_URL: z.string(), - CAP_AWS_BUCKET: z.string(), - CAP_AWS_REGION: z.string(), - CAP_AWS_ENDPOINT: z.string().optional(), - CAP_AWS_BUCKET_URL: z.string().optional(), - CAP_AWS_ACCESS_KEY: z.string(), - CAP_AWS_SECRET_KEY: z.string(), - CAP_AWS_MEDIACONVERT_ROLE_ARN: z.string().optional(), - CAP_CLOUDFRONT_DISTRIBUTION_ID: z.string().optional(), - NEXTAUTH_SECRET: z.string(), - NEXTAUTH_URL: z.string(), - GOOGLE_CLIENT_ID: z.string().optional(), - GOOGLE_CLIENT_SECRET: z.string().optional(), - WORKOS_CLIENT_ID: z.string().optional(), - WORKOS_API_KEY: z.string().optional(), - DUB_API_KEY: z.string().optional(), - RESEND_API_KEY: z.string().optional(), - DEEPGRAM_API_KEY: z.string().optional(), - NEXT_LOOPS_KEY: z.string().optional(), - STRIPE_SECRET_KEY_TEST: z.string().optional(), - STRIPE_SECRET_KEY_LIVE: z.string().optional(), - STRIPE_WEBHOOK_SECRET: z.string().optional(), - DISCORD_FEEDBACK_WEBHOOK_URL: z.string().optional(), - INTERCOM_SECRET: z.string().optional(), - VERCEL_TEAM_ID: z.string().optional(), - VERCEL_PROJECT_ID: z.string().optional(), - VERCEL_AUTH_TOKEN: z.string().optional(), - VERCEL_URL: z.string().optional(), - VERCEL_BRANCH_URL: z.string().optional(), - VERCEL_PROJECT_PRODUCTION_URL: z.string().optional(), - DOCKER_BUILD: z.string().optional(), - }, - experimental__runtimeEnv: process.env, -}); +function createServerEnv() { + return createEnv({ + server: { + NODE_ENV: z.string(), + DATABASE_URL: z.string(), + WEB_URL: z.string(), + DATABASE_MIGRATION_URL: z.string().optional(), + DATABASE_ENCRYPTION_KEY: z.string().optional(), + CAP_AWS_BUCKET: z.string(), + CAP_AWS_REGION: z.string(), + CAP_AWS_BUCKET_URL: z.string().optional(), + CAP_AWS_ACCESS_KEY: z.string(), + CAP_AWS_SECRET_KEY: z.string(), + CAP_AWS_ENDPOINT: z.string().optional(), + CAP_AWS_MEDIACONVERT_ROLE_ARN: z.string().optional(), + CAP_CLOUDFRONT_DISTRIBUTION_ID: z.string().optional(), + NEXTAUTH_SECRET: z.string(), + NEXTAUTH_URL: z.string(), + GOOGLE_CLIENT_ID: z.string().optional(), + GOOGLE_CLIENT_SECRET: z.string().optional(), + WORKOS_CLIENT_ID: z.string().optional(), + WORKOS_API_KEY: z.string().optional(), + DUB_API_KEY: z.string().optional(), + RESEND_API_KEY: z.string().optional(), + DEEPGRAM_API_KEY: z.string().optional(), + NEXT_LOOPS_KEY: z.string().optional(), + STRIPE_SECRET_KEY_TEST: z.string().optional(), + STRIPE_SECRET_KEY_LIVE: z.string().optional(), + STRIPE_WEBHOOK_SECRET: z.string().optional(), + DISCORD_FEEDBACK_WEBHOOK_URL: z.string().optional(), + INTERCOM_SECRET: z.string().optional(), + VERCEL_TEAM_ID: z.string().optional(), + VERCEL_PROJECT_ID: z.string().optional(), + VERCEL_AUTH_TOKEN: z.string().optional(), + VERCEL_URL: z.string().optional(), + VERCEL_BRANCH_URL: z.string().optional(), + VERCEL_PROJECT_PRODUCTION_URL: z.string().optional(), + DOCKER_BUILD: z.string().optional(), + }, + experimental__runtimeEnv: { + ...process.env, + }, + }); +} + +let _cached: ReturnType | undefined; +export const serverEnv = () => { + if (_cached) return _cached; + _cached = createServerEnv(); + return _cached; +}; diff --git a/packages/utils/src/constants/s3.ts b/packages/utils/src/constants/s3.ts index bd525dcd7..ba1b7546b 100644 --- a/packages/utils/src/constants/s3.ts +++ b/packages/utils/src/constants/s3.ts @@ -1,12 +1,12 @@ -import { serverEnv } from "@cap/env"; +import { buildEnv } from "@cap/env"; export const S3_BUCKET_URL = (() => { - const fixedUrl = serverEnv.CAP_AWS_BUCKET_URL; - const endpoint = serverEnv.CAP_AWS_ENDPOINT; - const bucket = serverEnv.CAP_AWS_BUCKET; - const region = serverEnv.CAP_AWS_REGION; + const fixedUrl = buildEnv.NEXT_PUBLIC_CAP_AWS_BUCKET_URL; + const endpoint = buildEnv.NEXT_PUBLIC_CAP_AWS_ENDPOINT; + const bucket = buildEnv.NEXT_PUBLIC_CAP_AWS_BUCKET; + const region = buildEnv.NEXT_PUBLIC_CAP_AWS_REGION; - if (fixedUrl) return fixedUrl; - if (endpoint) return `${endpoint}/${bucket}`; - return `s3.${region}.amazonaws.com/${bucket}`; + if (fixedUrl) return fixedUrl; + if (endpoint) return `${endpoint}/${bucket}`; + return `s3.${region}.amazonaws.com/${bucket}`; })(); diff --git a/packages/utils/src/lib/stripe/stripe.ts b/packages/utils/src/lib/stripe/stripe.ts index 24e9f68ea..53795c710 100644 --- a/packages/utils/src/lib/stripe/stripe.ts +++ b/packages/utils/src/lib/stripe/stripe.ts @@ -1,13 +1,16 @@ import { serverEnv } from "@cap/env"; import Stripe from "stripe"; -const key = - serverEnv.STRIPE_SECRET_KEY_TEST ?? serverEnv.STRIPE_SECRET_KEY_LIVE ?? ""; -export const STRIPE_AVAILABLE = key !== ""; -export const stripe = new Stripe(key, { - apiVersion: "2023-10-16", - appInfo: { - name: "Cap", - version: "0.1.0", - }, -}); +const key = () => + serverEnv().STRIPE_SECRET_KEY_TEST ?? + serverEnv().STRIPE_SECRET_KEY_LIVE ?? + ""; +export const STRIPE_AVAILABLE = () => key() !== ""; +export const stripe = () => + new Stripe(key(), { + apiVersion: "2023-10-16", + appInfo: { + name: "Cap", + version: "0.1.0", + }, + }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 724811942..313d7d57c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,32 +13,13 @@ patchedDependencies: importers: .: - dependencies: - sst: - specifier: 3.10.13 - version: 3.10.13 devDependencies: - '@clack/core': - specifier: ^0.4.1 - version: 0.4.1 '@clack/prompts': specifier: ^0.10.0 version: 0.10.0 - '@turbo/gen': - specifier: ^1.9.7 - version: 1.13.4(@types/node@22.12.0)(typescript@5.7.2) - '@types/aws-lambda': - specifier: 8.10.148 - version: 8.10.148 dotenv-cli: specifier: latest version: 8.0.0 - eslint: - specifier: ^7.32.0 - version: 7.32.0 - extract-zip: - specifier: ^2.0.1 - version: 2.0.1 prettier: specifier: ^2.5.1 version: 2.8.8 @@ -571,9 +552,6 @@ importers: lucide-react: specifier: ^0.294.0 version: 0.294.0(react@18.3.1) - masonic: - specifier: ^4.1.0 - version: 4.1.0(react@18.3.1) moment: specifier: ^2.30.1 version: 2.30.1 @@ -619,9 +597,6 @@ importers: react-player: specifier: ^2.14.1 version: 2.16.0(react@18.3.1) - react-responsive-masonry: - specifier: ^2.7.1 - version: 2.7.1 react-rnd: specifier: ^10.4.1 version: 10.4.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1500,10 +1475,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime-corejs3@7.25.6': - resolution: {integrity: sha512-Gz0Nrobx8szge6kQQ5Z5MX9L3ObqNwCQY1PSwSNzreFL7aHGxv8Fp2j3ETV6/wWdbiV+mW6OSm8oQhg3Tcsniw==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.25.6': resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} engines: {node: '>=6.9.0'} @@ -3155,18 +3126,6 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@essentials/memoize-one@1.1.0': - resolution: {integrity: sha512-HMkuIkKNe0EWSUpZhlaq9+5Yp47YhrMhxLMnXTRnEyE5N4xKLspAvMGjUFdi794VnEF1EcOZFS8rdROeujrgag==} - - '@essentials/one-key-map@1.2.0': - resolution: {integrity: sha512-C2H7zHVcsoipDv4VKY5uUcv5ilsK+uEgEj+WeOdN5oz/Qj1/OZIzCdle90gDzj0xnGQrmZ9qDujwD7AkBb5k9A==} - - '@essentials/raf@1.2.0': - resolution: {integrity: sha512-AWJvpprE2o7ATMb7HBYMVUVmPJBCt2wZp2rY7d+rAcNSMvzLbDepy9KFeqqrPZh+s9aIpbw1LgmuAW7kuRFgrQ==} - - '@essentials/request-timeout@1.3.0': - resolution: {integrity: sha512-lKZPhKScNFnR1MBnk4+sxshk46fpvdN+Uh1LlKWFO5g1ocuz4EcknNIL7tm/rsCAs/+xMWiBTwbDUvm+pDNlXw==} - '@fal-works/esbuild-plugin-global-externals@2.1.2': resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} @@ -5180,41 +5139,6 @@ packages: peerDependencies: react: 18.2.0 - '@react-hook/debounce@3.0.0': - resolution: {integrity: sha512-ir/kPrSfAzY12Gre0sOHkZ2rkEmM4fS5M5zFxCi4BnCeXh2nvx9Ujd+U4IGpKCuPA+EQD0pg1eK2NGLvfWejag==} - peerDependencies: - react: '>=16.8' - - '@react-hook/event@1.2.6': - resolution: {integrity: sha512-JUL5IluaOdn5w5Afpe/puPa1rj8X6udMlQ9dt4hvMuKmTrBS1Ya6sb4sVgvfe2eU4yDuOfAhik8xhbcCekbg9Q==} - peerDependencies: - react: '>=16.8' - - '@react-hook/latest@1.0.3': - resolution: {integrity: sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==} - peerDependencies: - react: '>=16.8' - - '@react-hook/passive-layout-effect@1.2.1': - resolution: {integrity: sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==} - peerDependencies: - react: '>=16.8' - - '@react-hook/throttle@2.2.0': - resolution: {integrity: sha512-LJ5eg+yMV8lXtqK3lR+OtOZ2WH/EfWvuiEEu0M3bhR7dZRfTyEJKxH1oK9uyBxiXPtWXiQggWbZirMCXam51tg==} - peerDependencies: - react: '>=16.8' - - '@react-hook/window-scroll@1.3.0': - resolution: {integrity: sha512-LdYnCL22pFI+LTs85Fi2OQHSKWkzIuHFgv8lA+wwuaPxLOEhWR5bzJ21iygUH9X4meeLVRZKEbfpYi3OWWD4GQ==} - peerDependencies: - react: '>=16.8' - - '@react-hook/window-size@3.1.1': - resolution: {integrity: sha512-yWnVS5LKnOUIrEsI44oz3bIIUYqflamPL27n+k/PC//PsX/YeWBky09oPeAoc9As6jSH16Wgo8plI+ECZaHk3g==} - peerDependencies: - react: '>=16.8' - '@remix-run/router@1.19.2': resolution: {integrity: sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==} engines: {node: '>=14.0.0'} @@ -5975,10 +5899,10 @@ packages: react-dom: optional: true - '@storybook/builder-vite@9.0.0-beta.1': - resolution: {integrity: sha512-OALGrbqmpvLib/0yQl3Q1ggmTqOtMENURwwyhNe3Vb3HGQ8O+srDPPQ/xk0fSy7FO1YOoybZQHBpExOdfCpEKg==} + '@storybook/builder-vite@9.0.0-beta.4': + resolution: {integrity: sha512-D718PrP4rbppHUE7HsosbHxTQNGRal4BazV6RCrDWnu4+UG64qur3KztjxeGZdU+Sv7UEF5BJiYEokQHH0aBNw==} peerDependencies: - storybook: ^9.0.0-beta.1 + storybook: ^9.0.0-beta.4 vite: ^5.0.0 || ^6.0.0 '@storybook/core@8.3.3': @@ -5989,10 +5913,10 @@ packages: peerDependencies: storybook: ^8.3.3 - '@storybook/csf-plugin@9.0.0-beta.1': - resolution: {integrity: sha512-Od2EyWGiozSDy0IGBdYYo7U+x46rYTYO0Mm0wimAxtotSjIC03b91BVGFXeYxDN/Cd2ObpfKQlDnque8HrrBXw==} + '@storybook/csf-plugin@9.0.0-beta.4': + resolution: {integrity: sha512-eHyYQBWW5kwn8HtaDooIeT0nuhQysafyHL5QfPlyErAzDn1oVR+0EDWPlEgXy0pzqVzY3Axh3SqZ37ccA7kIeA==} peerDependencies: - storybook: ^9.0.0-beta.1 + storybook: ^9.0.0-beta.4 '@storybook/csf@0.1.11': resolution: {integrity: sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==} @@ -6317,9 +6241,6 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@tootallnate/quickjs-emscripten@0.23.0': - resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} - '@total-typescript/ts-reset@0.6.1': resolution: {integrity: sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==} @@ -6357,14 +6278,6 @@ packages: resolution: {integrity: sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==} engines: {node: ^16.14.0 || >=18.0.0} - '@turbo/gen@1.13.4': - resolution: {integrity: sha512-PK38N1fHhDUyjLi0mUjv0RbX0xXGwDLQeRSGsIlLcVpP1B5fwodSIwIYXc9vJok26Yne94BX5AGjueYsUT3uUw==} - hasBin: true - - '@turbo/workspaces@1.13.4': - resolution: {integrity: sha512-3uYg2b5TWCiupetbDFMbBFMHl33xQTvp5DNg0fZSYal73Z9AlFH9yWabHWMYw6ywmwM1evkYRpTVA2n7GgqT5A==} - hasBin: true - '@types/accepts@1.3.7': resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} @@ -6446,9 +6359,6 @@ packages: '@types/fluent-ffmpeg@2.1.26': resolution: {integrity: sha512-0JVF3wdQG+pN0ImwWD0bNgJiKF2OHg/7CDBHw5UIbRTvlnkgGHK6V5doE54ltvhud4o31/dEiHm23CAlxFiUQg==} - '@types/glob@7.2.0': - resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} - '@types/google-protobuf@3.15.12': resolution: {integrity: sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==} @@ -6470,9 +6380,6 @@ packages: '@types/http-errors@2.0.4': resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - '@types/inquirer@6.5.0': - resolution: {integrity: sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==} - '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -6527,9 +6434,6 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/minimatch@5.1.2': - resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - '@types/minimist@1.2.5': resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} @@ -6611,12 +6515,6 @@ packages: '@types/supertest@6.0.2': resolution: {integrity: sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==} - '@types/through@0.0.33': - resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} - - '@types/tinycolor2@1.4.6': - resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} - '@types/tmp@0.2.6': resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} @@ -6635,9 +6533,6 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript-eslint/eslint-plugin@5.62.0': resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7107,10 +7002,6 @@ packages: ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - ast-types@0.13.4: - resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} - engines: {node: '>=4'} - ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} @@ -7229,10 +7120,6 @@ packages: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} - basic-ftp@5.0.5: - resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} - engines: {node: '>=10.0.0'} - before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} @@ -7312,9 +7199,6 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - buffer-crc32@1.0.0: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} @@ -7379,9 +7263,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camel-case@3.0.0: - resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} - camel-case@4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} @@ -7442,9 +7323,6 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - change-case@3.1.0: - resolution: {integrity: sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==} - char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -7461,9 +7339,6 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -7540,10 +7415,6 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} - cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} - client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -7691,9 +7562,6 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - constant-case@2.0.0: - resolution: {integrity: sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==} - content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -7753,9 +7621,6 @@ packages: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} - core-js-pure@3.38.1: - resolution: {integrity: sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ==} - core-js@3.40.0: resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==} @@ -7834,10 +7699,6 @@ packages: data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} - data-uri-to-buffer@6.0.2: - resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} - engines: {node: '>= 14'} - data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -7963,10 +7824,6 @@ packages: resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} engines: {node: '>= 0.4'} - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -7996,14 +7853,6 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - degenerator@5.0.1: - resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} - engines: {node: '>= 14'} - - del@5.1.0: - resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} - engines: {node: '>=8'} - delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -8118,9 +7967,6 @@ packages: domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} - dot-case@2.1.1: - resolution: {integrity: sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==} - dot-prop@9.0.0: resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} engines: {node: '>=18'} @@ -8558,11 +8404,6 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - escodegen@2.1.0: - resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} - engines: {node: '>=6.0'} - hasBin: true - eslint-config-airbnb-base@15.0.0: resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} engines: {node: ^10.12.0 || >=12.0.0} @@ -8901,15 +8742,6 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - - extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - fast-content-type-parse@2.0.1: resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} @@ -8958,9 +8790,6 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdir@6.4.3: resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} peerDependencies: @@ -8972,10 +8801,6 @@ packages: fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} - figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} - file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -9091,10 +8916,6 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - fs-extra@11.1.1: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} engines: {node: '>=14.14'} @@ -9200,10 +9021,6 @@ packages: get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} - get-uri@6.0.3: - resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} - engines: {node: '>= 14'} - giget@2.0.0: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} hasBin: true @@ -9264,10 +9081,6 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} - globby@10.0.2: - resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} - engines: {node: '>=8'} - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -9301,10 +9114,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - gradient-string@2.0.2: - resolution: {integrity: sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==} - engines: {node: '>=10'} - graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -9322,11 +9131,6 @@ packages: h3@1.15.1: resolution: {integrity: sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA==} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} - engines: {node: '>=0.4.7'} - hasBin: true - hanji@0.0.5: resolution: {integrity: sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==} @@ -9409,9 +9213,6 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - header-case@1.0.1: - resolution: {integrity: sha512-i0q9mkOeSuhXw6bGgiQCCBgY/jlZuV/7dZXyZ9c6LcBrqwvT8eT719E9uxE5LiZftdl+z81Ugbg/VvXV4OJOeQ==} - heap@0.2.7: resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} @@ -9610,14 +9411,6 @@ packages: inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} - inquirer@7.3.3: - resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} - engines: {node: '>=8.0.0'} - - inquirer@8.2.6: - resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} - engines: {node: '>=12.0.0'} - internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} @@ -9782,9 +9575,6 @@ packages: is-lambda@1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} - is-lower-case@1.1.3: - resolution: {integrity: sha512-+5A1e/WJpLLXZEDlgz4G//WYSHyQBD32qa4Jd3Lw06qQlv3fJHnp3YIHjTQSGzHMgzmVKz2ZP3rBxTHkPw/lxA==} - is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -9804,10 +9594,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-cwd@2.2.0: - resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} - engines: {node: '>=6'} - is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} @@ -9878,9 +9664,6 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - is-upper-case@1.1.2: - resolution: {integrity: sha512-GQYSJMgfeAmVwh9ixyk888l7OIhNAGKtY6QA+IrWlu9MDTCaXmeozOZ2S9Knj7bQwBO/H6J2kb+pbyTUiMNbsw==} - is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -9914,10 +9697,6 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - isbinaryfile@4.0.10: - resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} - engines: {node: '>= 8.0.0'} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -10330,10 +10109,6 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. - lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -10358,10 +10133,6 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - log-symbols@3.0.0: - resolution: {integrity: sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==} - engines: {node: '>=8'} - log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -10382,12 +10153,6 @@ packages: loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} - lower-case-first@1.0.2: - resolution: {integrity: sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==} - - lower-case@1.1.4: - resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} - lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -10485,11 +10250,6 @@ packages: peerDependencies: react: '>= 0.14.0' - masonic@4.1.0: - resolution: {integrity: sha512-3RNbAG5qLve7qNtGp1UM/u7vI39jO73ZFHDBAg3xl8AVh7A6Ikx7I7mBeC0NY0h1r1jJn2Wqeol1QMa09MQbyQ==} - peerDependencies: - react: '>=16.8' - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -10961,10 +10721,6 @@ packages: resolution: {integrity: sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==} engines: {node: '>= 18'} - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -11021,9 +10777,6 @@ packages: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true - mute-stream@0.0.8: - resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - mux-embed@5.6.0: resolution: {integrity: sha512-LZdUIikPPY8A/MArjP+VJFEl8fixJXjC9cQElNEGx8PSU1fSL/4jEvaP2SFaIW8h815eXV47h+A/LEIcyoEZKA==} @@ -11072,13 +10825,6 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - - netmask@2.0.2: - resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} - engines: {node: '>= 0.4.0'} - next-auth@4.24.8: resolution: {integrity: sha512-SLt3+8UCtklsotnz2p+nB4aN3IHNmpsQFAZ24VLxGotWGzSxkBh192zxNhm/J5wgkcrDWVp0bwqvW0HksK/Lcw==} peerDependencies: @@ -11162,9 +10908,6 @@ packages: xml2js: optional: true - no-case@2.3.2: - resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} - no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -11209,10 +10952,6 @@ packages: node-mock-http@1.0.0: resolution: {integrity: sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==} - node-plop@0.26.3: - resolution: {integrity: sha512-Cov028YhBZ5aB7MdMWJEmwyBig43aGL5WT4vdoB28Oitau1zZAcHUn8Sgfk9HM33TqhtLJ9PlM/O0Mv+QpV/4Q==} - engines: {node: '>=8.9.4'} - node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} @@ -11414,18 +11153,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ora@4.1.1: - resolution: {integrity: sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A==} - engines: {node: '>=8'} - ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -11454,10 +11185,6 @@ packages: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-map@3.0.0: - resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} - engines: {node: '>=8'} - p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} @@ -11466,14 +11193,6 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - pac-proxy-agent@7.0.2: - resolution: {integrity: sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==} - engines: {node: '>= 14'} - - pac-resolver@7.0.1: - resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} - engines: {node: '>= 14'} - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -11492,9 +11211,6 @@ packages: resolution: {integrity: sha512-facVMEBnUynzMN7hCSqyNpF6uyCpVIl4XAUyTR9D8q2JlhgyPY6bZtj/OkFk3+Cpka1TnYCppQb8BzDWHtSaZg==} engines: {node: '>=12'} - param-case@2.1.1: - resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -11520,15 +11236,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - pascal-case@2.0.1: - resolution: {integrity: sha512-qjS4s8rBOJa2Xm0jmxXiyh1+OFf6ekCWOvUaRgAQSktzlTbMotS0nmG9gyYAybCWBcuP4fsBeRCKNwGBnMe2OQ==} - pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} - path-case@2.1.1: - resolution: {integrity: sha512-Ou0N05MioItesaLr9q8TtHVWmJ6fxWdqKB2RohFmNWVyJ+2zeKIeDNWAN6B/Pe7wpzWChhZX6nONYmOnMeJQ/Q==} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -11587,9 +11297,6 @@ packages: peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} - pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -11828,10 +11535,6 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - proxy-agent@6.4.0: - resolution: {integrity: sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==} - engines: {node: '>= 14'} - proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -11891,9 +11594,6 @@ packages: radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} - raf-schd@4.0.3: - resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} - randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -11912,10 +11612,6 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - re-resizable@6.10.0: resolution: {integrity: sha512-hysSK0xmA5nz24HBVztlk4yCqCLCvS32E6ZpWxVKop9x3tqCa4yAj1++facrmkOf62JsJHjmjABdKxXofYioCw==} peerDependencies: @@ -12033,9 +11729,6 @@ packages: '@types/react': optional: true - react-responsive-masonry@2.7.1: - resolution: {integrity: sha512-Q+u+nOH87PzjqGFd2PgTcmLpHPZnCmUPREHYoNBc8dwJv6fi51p9U6hqwG8g/T8MN86HrFjrU+uQU6yvETU7cA==} - react-rnd@10.4.13: resolution: {integrity: sha512-Vgbf0iihspcQ6nkaFhpOGWfmnuVbhkhoB0hBbYl8aRDA4horsQHESc4E1z7O/P27kFFjK2aqM0u5CGzfr9gEZA==} peerDependencies: @@ -12183,13 +11876,6 @@ packages: resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} engines: {node: '>=8'} - registry-auth-token@3.3.2: - resolution: {integrity: sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==} - - registry-url@3.1.0: - resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} - engines: {node: '>=0.10.0'} - rehype-external-links@3.0.0: resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==} @@ -12346,20 +12032,9 @@ packages: rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} - run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rxjs@6.6.7: - resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} - engines: {npm: '>=2.0.0'} - - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -12427,9 +12102,6 @@ packages: resolution: {integrity: sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==} engines: {node: '>= 18'} - sentence-case@2.1.1: - resolution: {integrity: sha512-ENl7cYHaK/Ktwk5OTD+aDbQ3uC8IByu/6Bkg+HDv8Mm+XnBnppVNalcfJTNsp1ibstKh030/JKQQWglDvtKwEQ==} - seq-queue@0.0.5: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} @@ -12560,9 +12232,6 @@ packages: smob@1.5.0: resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} - snake-case@2.1.0: - resolution: {integrity: sha512-FMR5YoPFwOLuh4rRz92dywJjyKYZNLpMn1R5ujVpIYkbA9p01fq8RMg0FkO4M+Yobt4MjHeLTJVm5xFFBHSV2Q==} - socks-proxy-agent@8.0.4: resolution: {integrity: sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==} engines: {node: '>= 14'} @@ -12831,10 +12500,6 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -12919,9 +12584,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swap-case@1.1.2: - resolution: {integrity: sha512-BAmWG6/bx8syfc6qXPprof3Mn5vQgf5dwdUNJhsNqU9WdPt5P+ES/wQ5bxfijy8zwZgZZHslC3iAsxsuQMCzJQ==} - symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -13010,9 +12672,6 @@ packages: peerDependencies: tslib: ^2 - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - timers-ext@0.1.8: resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} engines: {node: '>=0.12'} @@ -13023,9 +12682,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinycolor2@1.6.0: - resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyexec@0.3.0: resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} @@ -13036,9 +12692,6 @@ packages: resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} - tinygradient@1.1.5: - resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} - tinypool@1.0.2: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -13051,9 +12704,6 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} - title-case@2.1.1: - resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} - tldts-core@6.1.47: resolution: {integrity: sha512-6SWyFMnlst1fEt7GQVAAu16EGgFK0cLouH/2Mk6Ftlwhv3Ol40L0dlpGMcnnNiiOMyD2EV/aF3S+U2nKvvLvrA==} @@ -13061,10 +12711,6 @@ packages: resolution: {integrity: sha512-R/K2tZ5MiY+mVrnSkNJkwqYT2vUv1lcT6wJvd2emGaMJ7PHUGRY4e3tUsdFCXgqxi2QgbHjL3yJgXCo40v9Hxw==} hasBin: true - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - tmp@0.2.3: resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} engines: {node: '>=14.14'} @@ -13121,9 +12767,6 @@ packages: resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - trie-memoize@1.2.0: - resolution: {integrity: sha512-hEDLVEP1FCgaRtt0oZDJdz2lK9uK7WlB7ASswt9U9cqruSNueVigtRGxI97hevKlViqhAcRgNgzuY/m8FCCMcg==} - trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -13343,11 +12986,6 @@ packages: ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} - uglify-js@3.19.3: - resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} - engines: {node: '>=0.8.0'} - hasBin: true - ultrahtml@1.5.3: resolution: {integrity: sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==} @@ -13621,15 +13259,6 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - update-check@1.5.4: - resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==} - - upper-case-first@1.1.2: - resolution: {integrity: sha512-wINKYvI3Db8dtjikdAqoBbZoP6Q+PZUyfMR7pmwHzjC2quzSkUq5DmPrTtPEqHaz8AGtmsB4TqwapMTM1QAQOQ==} - - upper-case@1.1.3: - resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} - uqr@0.1.2: resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} @@ -14028,10 +13657,6 @@ packages: '@cloudflare/workers-types': optional: true - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -14109,9 +13734,6 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -15086,11 +14708,6 @@ snapshots: '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/runtime-corejs3@7.25.6': - dependencies: - core-js-pure: 3.38.1 - regenerator-runtime: 0.14.1 - '@babel/runtime@7.25.6': dependencies: regenerator-runtime: 0.14.1 @@ -16288,16 +15905,6 @@ snapshots: '@eslint/js@8.57.1': {} - '@essentials/memoize-one@1.1.0': {} - - '@essentials/one-key-map@1.2.0': {} - - '@essentials/raf@1.2.0': {} - - '@essentials/request-timeout@1.3.0': - dependencies: - '@essentials/raf': 1.2.0 - '@fal-works/esbuild-plugin-global-externals@2.1.2': {} '@fastify/busboy@2.1.1': {} @@ -18845,41 +18452,6 @@ snapshots: dependencies: react: 18.3.1 - '@react-hook/debounce@3.0.0(react@18.3.1)': - dependencies: - '@react-hook/latest': 1.0.3(react@18.3.1) - react: 18.3.1 - - '@react-hook/event@1.2.6(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-hook/latest@1.0.3(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-hook/passive-layout-effect@1.2.1(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-hook/throttle@2.2.0(react@18.3.1)': - dependencies: - '@react-hook/latest': 1.0.3(react@18.3.1) - react: 18.3.1 - - '@react-hook/window-scroll@1.3.0(react@18.3.1)': - dependencies: - '@react-hook/event': 1.2.6(react@18.3.1) - '@react-hook/throttle': 2.2.0(react@18.3.1) - react: 18.3.1 - - '@react-hook/window-size@3.1.1(react@18.3.1)': - dependencies: - '@react-hook/debounce': 3.0.0(react@18.3.1) - '@react-hook/event': 1.2.6(react@18.3.1) - '@react-hook/throttle': 2.2.0(react@18.3.1) - react: 18.3.1 - '@remix-run/router@1.19.2': {} '@rive-app/canvas@2.26.7': {} @@ -19773,9 +19345,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@9.0.0-beta.1(storybook@8.3.3)(vite@6.1.2(@types/node@22.12.0)(jiti@2.4.2)(terser@5.34.0)(yaml@2.7.0))': + '@storybook/builder-vite@9.0.0-beta.4(storybook@8.3.3)(vite@6.1.2(@types/node@22.12.0)(jiti@2.4.2)(terser@5.34.0)(yaml@2.7.0))': dependencies: - '@storybook/csf-plugin': 9.0.0-beta.1(storybook@8.3.3) + '@storybook/csf-plugin': 9.0.0-beta.4(storybook@8.3.3) browser-assert: 1.2.1 storybook: 8.3.3 ts-dedent: 2.2.0 @@ -19806,7 +19378,7 @@ snapshots: storybook: 8.3.3 unplugin: 1.16.1 - '@storybook/csf-plugin@9.0.0-beta.1(storybook@8.3.3)': + '@storybook/csf-plugin@9.0.0-beta.4(storybook@8.3.3)': dependencies: storybook: 8.3.3 unplugin: 1.16.1 @@ -20176,8 +19748,6 @@ snapshots: dependencies: '@testing-library/dom': 9.3.4 - '@tootallnate/quickjs-emscripten@0.23.0': {} - '@total-typescript/ts-reset@0.6.1': {} '@ts-rest/core@3.52.1(@types/node@20.16.9)(zod@3.24.2)': @@ -20207,41 +19777,6 @@ snapshots: '@tufjs/canonical-json': 2.0.0 minimatch: 9.0.5 - '@turbo/gen@1.13.4(@types/node@22.12.0)(typescript@5.7.2)': - dependencies: - '@turbo/workspaces': 1.13.4 - chalk: 2.4.2 - commander: 10.0.1 - fs-extra: 10.1.0 - inquirer: 8.2.6 - minimatch: 9.0.5 - node-plop: 0.26.3 - proxy-agent: 6.4.0 - ts-node: 10.9.2(@types/node@22.12.0)(typescript@5.7.2) - update-check: 1.5.4 - validate-npm-package-name: 5.0.1 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - - '@types/node' - - supports-color - - typescript - - '@turbo/workspaces@1.13.4': - dependencies: - chalk: 2.4.2 - commander: 10.0.1 - execa: 5.1.1 - fast-glob: 3.3.2 - fs-extra: 10.1.0 - gradient-string: 2.0.2 - inquirer: 8.2.6 - js-yaml: 4.1.0 - ora: 4.1.1 - rimraf: 3.0.2 - semver: 7.7.1 - update-check: 1.5.4 - '@types/accepts@1.3.7': dependencies: '@types/node': 20.16.9 @@ -20346,11 +19881,6 @@ snapshots: dependencies: '@types/node': 20.16.9 - '@types/glob@7.2.0': - dependencies: - '@types/minimatch': 5.1.2 - '@types/node': 20.16.9 - '@types/google-protobuf@3.15.12': {} '@types/graceful-fs@4.1.9': @@ -20371,11 +19901,6 @@ snapshots: '@types/http-errors@2.0.4': {} - '@types/inquirer@6.5.0': - dependencies: - '@types/through': 0.0.33 - rxjs: 6.6.7 - '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -20438,8 +19963,6 @@ snapshots: '@types/mime@1.3.5': {} - '@types/minimatch@5.1.2': {} - '@types/minimist@1.2.5': {} '@types/morgan@1.9.9': @@ -20534,12 +20057,6 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 - '@types/through@0.0.33': - dependencies: - '@types/node': 20.16.9 - - '@types/tinycolor2@1.4.6': {} - '@types/tmp@0.2.6': {} '@types/unist@2.0.11': {} @@ -20554,11 +20071,6 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@types/yauzl@2.10.3': - dependencies: - '@types/node': 20.16.9 - optional: true - '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)': dependencies: '@eslint-community/regexpp': 4.11.1 @@ -21202,10 +20714,6 @@ snapshots: ast-types-flow@0.0.8: {} - ast-types@0.13.4: - dependencies: - tslib: 2.8.1 - ast-types@0.16.1: dependencies: tslib: 2.8.1 @@ -21409,8 +20917,6 @@ snapshots: dependencies: safe-buffer: 5.1.2 - basic-ftp@5.0.5: {} - before-after-hook@2.2.3: {} before-after-hook@3.0.2: {} @@ -21522,8 +21028,6 @@ snapshots: dependencies: node-int64: 0.4.0 - buffer-crc32@0.2.13: {} - buffer-crc32@1.0.0: {} buffer-from@1.1.2: {} @@ -21616,11 +21120,6 @@ snapshots: callsites@3.1.0: {} - camel-case@3.0.0: - dependencies: - no-case: 2.3.2 - upper-case: 1.1.3 - camel-case@4.1.2: dependencies: pascal-case: 3.1.2 @@ -21690,27 +21189,6 @@ snapshots: chalk@5.3.0: {} - change-case@3.1.0: - dependencies: - camel-case: 3.0.0 - constant-case: 2.0.0 - dot-case: 2.1.1 - header-case: 1.0.1 - is-lower-case: 1.1.3 - is-upper-case: 1.1.2 - lower-case: 1.1.4 - lower-case-first: 1.0.2 - no-case: 2.3.2 - param-case: 2.1.1 - pascal-case: 2.0.1 - path-case: 2.1.1 - sentence-case: 2.1.1 - snake-case: 2.1.0 - swap-case: 1.1.2 - title-case: 2.1.1 - upper-case: 1.1.3 - upper-case-first: 1.1.2 - char-regex@1.0.2: {} character-entities-html4@2.1.0: {} @@ -21721,8 +21199,6 @@ snapshots: character-reference-invalid@2.0.1: {} - chardet@0.7.0: {} - check-error@2.1.1: {} chokidar@3.5.3: @@ -21793,8 +21269,6 @@ snapshots: cli-spinners@2.9.2: {} - cli-width@3.0.0: {} - client-only@0.0.1: {} clipanion@3.2.1(typanion@3.14.0): @@ -21924,11 +21398,6 @@ snapshots: consola@3.4.2: {} - constant-case@2.0.0: - dependencies: - snake-case: 2.1.0 - upper-case: 1.1.3 - content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -21983,8 +21452,6 @@ snapshots: dependencies: is-what: 4.1.16 - core-js-pure@3.38.1: {} - core-js@3.40.0: {} core-util-is@1.0.3: {} @@ -22067,8 +21534,6 @@ snapshots: data-uri-to-buffer@2.0.2: {} - data-uri-to-buffer@6.0.2: {} - data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -22170,8 +21635,6 @@ snapshots: which-collection: 1.0.2 which-typed-array: 1.1.15 - deep-extend@0.6.0: {} - deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -22198,23 +21661,6 @@ snapshots: defu@6.1.4: {} - degenerator@5.0.1: - dependencies: - ast-types: 0.13.4 - escodegen: 2.1.0 - esprima: 4.0.1 - - del@5.1.0: - dependencies: - globby: 10.0.2 - graceful-fs: 4.2.11 - is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 3.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - delayed-stream@1.0.0: {} denque@2.1.0: {} @@ -22304,10 +21750,6 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - dot-case@2.1.1: - dependencies: - no-case: 2.3.2 - dot-prop@9.0.0: dependencies: type-fest: 4.26.1 @@ -22891,14 +22333,6 @@ snapshots: escape-string-regexp@5.0.0: {} - escodegen@2.1.0: - dependencies: - esprima: 4.0.1 - estraverse: 5.3.0 - esutils: 2.0.3 - optionalDependencies: - source-map: 0.6.1 - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.30.0)(eslint@8.57.1): dependencies: confusing-browser-globals: 1.0.11 @@ -23515,22 +22949,6 @@ snapshots: extend@3.0.2: {} - external-editor@3.1.0: - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 - - extract-zip@2.0.1: - dependencies: - debug: 4.3.7(supports-color@5.5.0) - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - fast-content-type-parse@2.0.1: {} fast-deep-equal@2.0.1: {} @@ -23581,10 +22999,6 @@ snapshots: dependencies: bser: 2.1.1 - fd-slicer@1.1.0: - dependencies: - pend: 1.2.0 - fdir@6.4.3(picomatch@3.0.1): optionalDependencies: picomatch: 3.0.1 @@ -23595,10 +23009,6 @@ snapshots: fflate@0.4.8: {} - figures@3.2.0: - dependencies: - escape-string-regexp: 1.0.5 - file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -23718,12 +23128,6 @@ snapshots: fresh@2.0.0: {} - fs-extra@10.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - fs-extra@11.1.1: dependencies: graceful-fs: 4.2.11 @@ -23837,15 +23241,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - get-uri@6.0.3: - dependencies: - basic-ftp: 5.0.5 - data-uri-to-buffer: 6.0.2 - debug: 4.4.0 - fs-extra: 11.2.0 - transitivePeerDependencies: - - supports-color - giget@2.0.0: dependencies: citty: 0.1.6 @@ -23934,17 +23329,6 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 - globby@10.0.2: - dependencies: - '@types/glob': 7.2.0 - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - glob: 7.2.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - globby@11.1.0: dependencies: array-union: 2.1.0 @@ -23993,11 +23377,6 @@ snapshots: graceful-fs@4.2.11: {} - gradient-string@2.0.2: - dependencies: - chalk: 4.1.2 - tinygradient: 1.1.5 - graphemer@1.4.0: {} gray-matter@4.0.3: @@ -24036,15 +23415,6 @@ snapshots: ufo: 1.5.4 uncrypto: 0.1.3 - handlebars@4.7.8: - dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.19.3 - hanji@0.0.5: dependencies: lodash.throttle: 4.1.1 @@ -24191,11 +23561,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - header-case@1.0.1: - dependencies: - no-case: 2.3.2 - upper-case: 1.1.3 - heap@0.2.7: {} helmet@7.1.0: {} @@ -24373,40 +23738,6 @@ snapshots: inline-style-parser@0.2.4: {} - inquirer@7.3.3: - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - run-async: 2.4.1 - rxjs: 6.6.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - - inquirer@8.2.6: - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.1 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 6.2.0 - internal-slot@1.0.7: dependencies: es-errors: 1.3.0 @@ -24558,10 +23889,6 @@ snapshots: is-lambda@1.0.1: {} - is-lower-case@1.1.3: - dependencies: - lower-case: 1.1.4 - is-map@2.0.3: {} is-module@1.0.0: {} @@ -24574,8 +23901,6 @@ snapshots: is-number@7.0.0: {} - is-path-cwd@2.2.0: {} - is-path-inside@3.0.3: {} is-plain-obj@1.1.0: {} @@ -24629,10 +23954,6 @@ snapshots: is-unicode-supported@0.1.0: {} - is-upper-case@1.1.2: - dependencies: - upper-case: 1.1.3 - is-weakmap@2.0.2: {} is-weakref@1.0.2: @@ -24662,8 +23983,6 @@ snapshots: isarray@2.0.5: {} - isbinaryfile@4.0.10: {} - isexe@2.0.0: {} isexe@3.1.1: {} @@ -25277,8 +24596,6 @@ snapshots: lodash.defaults@4.2.0: {} - lodash.get@4.4.2: {} - lodash.includes@4.3.0: {} lodash.isarguments@3.1.0: {} @@ -25295,10 +24612,6 @@ snapshots: lodash@4.17.21: {} - log-symbols@3.0.0: - dependencies: - chalk: 2.4.2 - log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -25318,12 +24631,6 @@ snapshots: loupe@3.1.3: {} - lower-case-first@1.0.2: - dependencies: - lower-case: 1.1.4 - - lower-case@1.1.4: {} - lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -25427,21 +24734,6 @@ snapshots: dependencies: react: 18.3.1 - masonic@4.1.0(react@18.3.1): - dependencies: - '@essentials/memoize-one': 1.1.0 - '@essentials/one-key-map': 1.2.0 - '@essentials/request-timeout': 1.3.0 - '@react-hook/event': 1.2.6(react@18.3.1) - '@react-hook/latest': 1.0.3(react@18.3.1) - '@react-hook/passive-layout-effect': 1.2.1(react@18.3.1) - '@react-hook/throttle': 2.2.0(react@18.3.1) - '@react-hook/window-scroll': 1.3.0(react@18.3.1) - '@react-hook/window-size': 3.1.1(react@18.3.1) - raf-schd: 4.0.3 - react: 18.3.1 - trie-memoize: 1.2.0 - math-intrinsics@1.1.0: {} mdast-util-definitions@5.1.2: @@ -26387,10 +25679,6 @@ snapshots: minipass: 7.1.2 rimraf: 5.0.10 - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - mkdirp@1.0.4: {} mkdirp@3.0.1: {} @@ -26446,8 +25734,6 @@ snapshots: mustache@4.2.0: {} - mute-stream@0.0.8: {} - mux-embed@5.6.0: {} mux.js@7.0.3: @@ -26491,10 +25777,6 @@ snapshots: negotiator@1.0.0: {} - neo-async@2.6.2: {} - - netmask@2.0.2: {} - next-auth@4.24.8(next@14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.6 @@ -26716,10 +25998,6 @@ snapshots: - supports-color - uploadthing - no-case@2.3.2: - dependencies: - lower-case: 1.1.4 - no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -26762,20 +26040,6 @@ snapshots: node-mock-http@1.0.0: {} - node-plop@0.26.3: - dependencies: - '@babel/runtime-corejs3': 7.25.6 - '@types/inquirer': 6.5.0 - change-case: 3.1.0 - del: 5.1.0 - globby: 10.0.2 - handlebars: 4.7.8 - inquirer: 7.3.3 - isbinaryfile: 4.0.10 - lodash.get: 4.4.2 - mkdirp: 0.5.6 - resolve: 1.22.8 - node-releases@2.0.18: {} nodemailer@6.9.15: {} @@ -27024,17 +26288,6 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - ora@4.1.1: - dependencies: - chalk: 3.0.0 - cli-cursor: 3.1.0 - cli-spinners: 2.9.2 - is-interactive: 1.0.0 - log-symbols: 3.0.0 - mute-stream: 0.0.8 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - ora@5.4.1: dependencies: bl: 4.1.0 @@ -27047,8 +26300,6 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 - os-tmpdir@1.0.2: {} - p-cancelable@2.1.1: {} p-limit@2.3.0: @@ -27075,34 +26326,12 @@ snapshots: dependencies: p-limit: 4.0.0 - p-map@3.0.0: - dependencies: - aggregate-error: 3.1.0 - p-map@4.0.0: dependencies: aggregate-error: 3.1.0 p-try@2.2.0: {} - pac-proxy-agent@7.0.2: - dependencies: - '@tootallnate/quickjs-emscripten': 0.23.0 - agent-base: 7.1.1 - debug: 4.4.0 - get-uri: 6.0.3 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.5 - pac-resolver: 7.0.1 - socks-proxy-agent: 8.0.4 - transitivePeerDependencies: - - supports-color - - pac-resolver@7.0.1: - dependencies: - degenerator: 5.0.1 - netmask: 2.0.2 - package-json-from-dist@1.0.1: {} package-manager-detector@0.2.0: {} @@ -27136,10 +26365,6 @@ snapshots: dependencies: bezier-easing: 2.1.0 - param-case@2.1.1: - dependencies: - no-case: 2.3.2 - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -27179,20 +26404,11 @@ snapshots: parseurl@1.3.3: {} - pascal-case@2.0.1: - dependencies: - camel-case: 3.0.0 - upper-case-first: 1.1.2 - pascal-case@3.1.2: dependencies: no-case: 3.0.4 tslib: 2.8.1 - path-case@2.1.1: - dependencies: - no-case: 2.3.2 - path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -27228,8 +26444,6 @@ snapshots: peberminta@0.9.0: {} - pend@1.2.0: {} - perfect-debounce@1.0.0: {} periscopic@3.1.0: @@ -27467,19 +26681,6 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-agent@6.4.0: - dependencies: - agent-base: 7.1.1 - debug: 4.4.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.5 - lru-cache: 7.18.3 - pac-proxy-agent: 7.0.2 - proxy-from-env: 1.1.0 - socks-proxy-agent: 8.0.4 - transitivePeerDependencies: - - supports-color - proxy-from-env@1.1.0: {} pstree.remy@1.1.8: {} @@ -27523,8 +26724,6 @@ snapshots: radix3@1.1.2: {} - raf-schd@4.0.3: {} - randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -27550,13 +26749,6 @@ snapshots: defu: 6.1.4 destr: 2.0.3 - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - re-resizable@6.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -27687,8 +26879,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.9 - react-responsive-masonry@2.7.1: {} - react-rnd@10.4.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: re-resizable: 6.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -27882,15 +27072,6 @@ snapshots: regexpp@3.2.0: {} - registry-auth-token@3.3.2: - dependencies: - rc: 1.2.8 - safe-buffer: 5.2.1 - - registry-url@3.1.0: - dependencies: - rc: 1.2.8 - rehype-external-links@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -28144,20 +27325,10 @@ snapshots: rrweb-cssom@0.7.1: {} - run-async@2.4.1: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - rxjs@6.6.7: - dependencies: - tslib: 1.14.1 - - rxjs@7.8.1: - dependencies: - tslib: 2.8.1 - sade@1.8.1: dependencies: mri: 1.2.0 @@ -28245,11 +27416,6 @@ snapshots: transitivePeerDependencies: - supports-color - sentence-case@2.1.1: - dependencies: - no-case: 2.3.2 - upper-case-first: 1.1.2 - seq-queue@0.0.5: {} serialize-javascript@6.0.2: @@ -28435,10 +27601,6 @@ snapshots: smob@1.5.0: {} - snake-case@2.1.0: - dependencies: - no-case: 2.3.2 - socks-proxy-agent@8.0.4: dependencies: agent-base: 7.1.1 @@ -28616,7 +27778,7 @@ snapshots: storybook-solidjs-vite@1.0.0-beta.2(storybook@8.3.3)(vite@6.1.2(@types/node@22.12.0)(jiti@2.4.2)(terser@5.34.0)(yaml@2.7.0)): dependencies: - '@storybook/builder-vite': 9.0.0-beta.1(storybook@8.3.3)(vite@6.1.2(@types/node@22.12.0)(jiti@2.4.2)(terser@5.34.0)(yaml@2.7.0)) + '@storybook/builder-vite': 9.0.0-beta.4(storybook@8.3.3)(vite@6.1.2(@types/node@22.12.0)(jiti@2.4.2)(terser@5.34.0)(yaml@2.7.0)) transitivePeerDependencies: - storybook - vite @@ -28749,8 +27911,6 @@ snapshots: dependencies: min-indent: 1.0.1 - strip-json-comments@2.0.1: {} - strip-json-comments@3.1.1: {} strip-literal@2.1.0: @@ -28843,11 +28003,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swap-case@1.1.2: - dependencies: - lower-case: 1.1.4 - upper-case: 1.1.3 - symbol-tree@3.2.4: {} system-architecture@0.1.0: {} @@ -29006,8 +28161,6 @@ snapshots: dependencies: tslib: 2.8.1 - through@2.3.8: {} - timers-ext@0.1.8: dependencies: es5-ext: 0.10.64 @@ -29017,8 +28170,6 @@ snapshots: tinybench@2.9.0: {} - tinycolor2@1.6.0: {} - tinyexec@0.3.0: {} tinyexec@0.3.2: {} @@ -29028,32 +28179,18 @@ snapshots: fdir: 6.4.3(picomatch@4.0.2) picomatch: 4.0.2 - tinygradient@1.1.5: - dependencies: - '@types/tinycolor2': 1.4.6 - tinycolor2: 1.6.0 - tinypool@1.0.2: {} tinyrainbow@1.2.0: {} tinyspy@3.0.2: {} - title-case@2.1.1: - dependencies: - no-case: 2.3.2 - upper-case: 1.1.3 - tldts-core@6.1.47: {} tldts@6.1.47: dependencies: tldts-core: 6.1.47 - tmp@0.0.33: - dependencies: - os-tmpdir: 1.0.2 - tmp@0.2.3: {} tmpl@1.0.5: {} @@ -29097,8 +28234,6 @@ snapshots: treeverse@3.0.0: {} - trie-memoize@1.2.0: {} - trim-lines@3.0.1: {} trim-newlines@3.0.1: {} @@ -29167,6 +28302,7 @@ snapshots: typescript: 5.7.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optional: true ts-pattern@5.5.0: {} @@ -29308,9 +28444,6 @@ snapshots: ufo@1.5.4: {} - uglify-js@3.19.3: - optional: true - ultrahtml@1.5.3: {} unbox-primitive@1.0.2: @@ -29614,17 +28747,6 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.0 - update-check@1.5.4: - dependencies: - registry-auth-token: 3.3.2 - registry-url: 3.1.0 - - upper-case-first@1.1.2: - dependencies: - upper-case: 1.1.3 - - upper-case@1.1.3: {} - uqr@0.1.2: {} uri-js@4.4.1: @@ -30135,12 +29257,6 @@ snapshots: - bufferutil - utf-8-validate - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -30205,11 +29321,6 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yauzl@2.10.0: - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - yn@3.1.1: {} yocto-queue@0.1.0: {} From 4e6ee67e4ab11e096c4ee39910a0451a0c0cfaf4 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 28 Apr 2025 17:24:07 -0700 Subject: [PATCH 10/18] fix db --- apps/web/actions/videos/edit-title.ts | 5 ++- packages/database/auth/auth-options.ts | 4 +-- packages/database/auth/drizzle-adapter.ts | 44 ++++++++++++----------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/apps/web/actions/videos/edit-title.ts b/apps/web/actions/videos/edit-title.ts index 425f84fd5..727a7db77 100644 --- a/apps/web/actions/videos/edit-title.ts +++ b/apps/web/actions/videos/edit-title.ts @@ -30,7 +30,10 @@ export async function editTitle(videoId: string, title: string) { } try { - await db.update(videos).set({ name: title }).where(eq(videos.id, videoId)); + await db() + .update(videos) + .set({ name: title }) + .where(eq(videos.id, videoId)); revalidatePath("/dashboard/caps"); revalidatePath("/dashboard/shared-caps"); diff --git a/packages/database/auth/auth-options.ts b/packages/database/auth/auth-options.ts index 87856f867..96a442da0 100644 --- a/packages/database/auth/auth-options.ts +++ b/packages/database/auth/auth-options.ts @@ -104,14 +104,14 @@ export const authOptions = (): NextAuthOptions => { const spaceId = nanoId(); // Create space - await db.insert(spaces).values({ + await db().insert(spaces).values({ id: spaceId, name: "My Space", ownerId: user.id, }); // Add user as member of the space - await db.insert(spaceMembers).values({ + await db().insert(spaceMembers).values({ id: nanoId(), userId: user.id, spaceId: spaceId, diff --git a/packages/database/auth/drizzle-adapter.ts b/packages/database/auth/drizzle-adapter.ts index f347358aa..2934e6203 100644 --- a/packages/database/auth/drizzle-adapter.ts +++ b/packages/database/auth/drizzle-adapter.ts @@ -9,7 +9,7 @@ import { serverEnv } from "@cap/env"; export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return { async createUser(userData: any) { - await db.insert(users).values({ + await db().insert(users).values({ id: nanoId(), email: userData.email, emailVerified: userData.emailVerified, @@ -82,7 +82,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { }, async updateUser({ id, ...userData }) { if (!id) throw new Error("User not found"); - await db.update(users).set(userData).where(eq(users.id, id)); + await db().update(users).set(userData).where(eq(users.id, id)); const rows = await db .select() .from(users) @@ -93,23 +93,25 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row; }, async deleteUser(userId) { - await db.delete(users).where(eq(users.id, userId)); + await db().delete(users).where(eq(users.id, userId)); }, async linkAccount(account: any) { - await db.insert(accounts).values({ - id: nanoId(), - userId: account.userId, - type: account.type, - provider: account.provider, - providerAccountId: account.providerAccountId, - access_token: account.access_token, - expires_in: account.expires_in as number, - id_token: account.id_token, - refresh_token: account.refresh_token, - refresh_token_expires_in: account.refresh_token_expires_in as number, - scope: account.scope, - token_type: account.token_type, - }); + await db() + .insert(accounts) + .values({ + id: nanoId(), + userId: account.userId, + type: account.type, + provider: account.provider, + providerAccountId: account.providerAccountId, + access_token: account.access_token, + expires_in: account.expires_in as number, + id_token: account.id_token, + refresh_token: account.refresh_token, + refresh_token_expires_in: account.refresh_token_expires_in as number, + scope: account.scope, + token_type: account.token_type, + }); }, async unlinkAccount({ providerAccountId, provider }: any) { await db @@ -122,7 +124,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { ); }, async createSession(data) { - await db.insert(sessions).values({ + await db().insert(sessions).values({ id: nanoId(), expires: data.expires, sessionToken: data.sessionToken, @@ -180,7 +182,9 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row; }, async deleteSession(sessionToken) { - await db.delete(sessions).where(eq(sessions.sessionToken, sessionToken)); + await db() + .delete(sessions) + .where(eq(sessions.sessionToken, sessionToken)); }, async createVerificationToken(verificationToken) { console.log({ verificationToken }); @@ -218,7 +222,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { } // If the token does not exist, proceed to create a new one - await db.insert(verificationTokens).values({ + await db().insert(verificationTokens).values({ expires: verificationToken.expires, identifier: verificationToken.identifier, token: verificationToken.token, From 6498debf8a27d7d232928a9a7835b67b266f98ec Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 28 Apr 2025 17:30:43 -0700 Subject: [PATCH 11/18] allow customising VITE_SERVER_URL for desktop --- scripts/env-cli.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/env-cli.js b/scripts/env-cli.js index a1c5dd997..8b31d4f8c 100644 --- a/scripts/env-cli.js +++ b/scripts/env-cli.js @@ -153,8 +153,6 @@ async function main() { envs.CAP_AWS_REGION = DOCKER_S3_ENVS.region; envs.CAP_AWS_ENDPOINT = DOCKER_S3_ENVS.endpoint; } - } else { - envs.VITE_SERVER_URL = "https://cap.so"; } if (hasDesktop) { @@ -162,6 +160,14 @@ async function main() { const values = await group( { + VITE_SERVER_URL: () => { + if (!hasWeb) + return text({ + message: "VITE_SERVER_URL", + placeholder: "http://cap.so", + defaultValue: "http://cap.so", + }); + }, VITE_VERCEL_AUTOMATION_BYPASS_SECRET: () => { if (!hasWeb) return text({ From a16105cb26f216ae374ac11bcb4bf384da2a382d Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 28 Apr 2025 17:37:32 -0700 Subject: [PATCH 12/18] more db() --- .../web/actions/screenshots/get-screenshot.ts | 2 +- apps/web/actions/videos/delete.ts | 11 +++--- apps/web/actions/videos/edit-date.ts | 2 +- apps/web/actions/videos/get-og-image.tsx | 2 +- apps/web/actions/videos/transcribe.ts | 8 ++--- apps/web/actions/workspace/check-domain.ts | 4 +-- apps/web/actions/workspace/get-space.ts | 4 +-- apps/web/actions/workspace/manage-billing.ts | 2 +- apps/web/actions/workspace/remove-domain.ts | 2 +- apps/web/actions/workspace/remove-invite.ts | 17 ++++------ apps/web/actions/workspace/update-details.ts | 2 +- apps/web/actions/workspace/update-domain.ts | 4 +-- apps/web/app/api/caps/share/route.ts | 4 +-- apps/web/app/api/desktop/app.ts | 4 +-- apps/web/app/api/desktop/s3/config/app.ts | 6 ++-- apps/web/app/api/desktop/video/app.ts | 6 ++-- apps/web/app/api/email/new-comment/route.ts | 10 +++--- apps/web/app/api/invite/accept/route.ts | 6 ++-- apps/web/app/api/playlist/route.ts | 2 +- apps/web/app/api/revalidate/route.ts | 2 +- apps/web/app/api/screenshot/route.ts | 2 +- .../app/api/settings/billing/manage/route.ts | 2 +- .../api/settings/billing/subscribe/route.ts | 2 +- .../app/api/settings/billing/usage/route.ts | 2 +- apps/web/app/api/settings/onboarding/route.ts | 6 ++-- apps/web/app/api/settings/user/name/route.ts | 2 +- apps/web/app/api/thumbnail/route.ts | 2 +- .../api/upload/multipart/[...route]/route.ts | 2 +- apps/web/app/api/upload/mux/create/route.ts | 4 +-- apps/web/app/api/upload/signed/route.ts | 2 +- .../web/app/api/video/comment/delete/route.ts | 4 +-- apps/web/app/api/video/delete/route.ts | 4 +-- apps/web/app/api/video/domain-info/route.ts | 8 ++--- apps/web/app/api/video/metadata/route.ts | 2 +- apps/web/app/api/webhooks/stripe/route.ts | 14 ++++---- .../_components/AdminNavbar/server.ts | 4 +-- apps/web/app/dashboard/admin/actions.ts | 2 +- apps/web/app/dashboard/caps/page.tsx | 6 ++-- apps/web/app/dashboard/layout.tsx | 10 +++--- apps/web/app/dashboard/shared-caps/page.tsx | 14 ++++---- apps/web/app/invite/[inviteId]/page.tsx | 2 +- apps/web/app/s/[videoId]/page.tsx | 12 +++---- apps/web/components/forms/server.ts | 2 +- apps/web/middleware.ts | 2 +- packages/database/auth/auth-options.ts | 4 +-- packages/database/auth/drizzle-adapter.ts | 34 +++++++++---------- packages/database/auth/session.ts | 2 +- 47 files changed, 123 insertions(+), 129 deletions(-) diff --git a/apps/web/actions/screenshots/get-screenshot.ts b/apps/web/actions/screenshots/get-screenshot.ts index 247b74e4c..ee8ddd976 100644 --- a/apps/web/actions/screenshots/get-screenshot.ts +++ b/apps/web/actions/screenshots/get-screenshot.ts @@ -17,7 +17,7 @@ export async function getScreenshot(userId: string, screenshotId: string) { throw new Error("userId or screenshotId not supplied"); } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets }) .from(videos) .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id)) diff --git a/apps/web/actions/videos/delete.ts b/apps/web/actions/videos/delete.ts index 7c77904fe..2ee8d27f1 100644 --- a/apps/web/actions/videos/delete.ts +++ b/apps/web/actions/videos/delete.ts @@ -4,10 +4,7 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { s3Buckets, videos } from "@cap/database/schema"; import { db } from "@cap/database"; import { and, eq } from "drizzle-orm"; -import { - DeleteObjectsCommand, - ListObjectsV2Command, -} from "@aws-sdk/client-s3"; +import { DeleteObjectsCommand, ListObjectsV2Command } from "@aws-sdk/client-s3"; import { createS3Client, getS3Bucket } from "@/utils/s3"; export async function deleteVideo(videoId: string) { @@ -22,7 +19,7 @@ export async function deleteVideo(videoId: string) { }; } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets }) .from(videos) .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id)) @@ -43,7 +40,7 @@ export async function deleteVideo(videoId: string) { }; } - await db + await db() .delete(videos) .where(and(eq(videos.id, videoId), eq(videos.ownerId, userId))); @@ -82,4 +79,4 @@ export async function deleteVideo(videoId: string) { message: "Failed to delete video", }; } -} \ No newline at end of file +} diff --git a/apps/web/actions/videos/edit-date.ts b/apps/web/actions/videos/edit-date.ts index b08c9fed0..4a6d508b4 100644 --- a/apps/web/actions/videos/edit-date.ts +++ b/apps/web/actions/videos/edit-date.ts @@ -46,7 +46,7 @@ export async function editDate(videoId: string, date: string) { customCreatedAt: newDate.toISOString(), }; - await db + await db() .update(videos) .set({ metadata: updatedMetadata, diff --git a/apps/web/actions/videos/get-og-image.tsx b/apps/web/actions/videos/get-og-image.tsx index c79fbaf74..cfae27374 100644 --- a/apps/web/actions/videos/get-og-image.tsx +++ b/apps/web/actions/videos/get-og-image.tsx @@ -160,7 +160,7 @@ export async function generateVideoOgImage(videoId: string) { } async function getData(videoId: string) { - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets, diff --git a/apps/web/actions/videos/transcribe.ts b/apps/web/actions/videos/transcribe.ts index cd335c709..fe42e9938 100644 --- a/apps/web/actions/videos/transcribe.ts +++ b/apps/web/actions/videos/transcribe.ts @@ -34,7 +34,7 @@ export async function transcribeVideo( }; } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets, @@ -78,7 +78,7 @@ export async function transcribeVideo( }; } - await db + await db() .update(videos) .set({ transcriptionStatus: "PROCESSING" }) .where(eq(videos.id, videoId)); @@ -111,7 +111,7 @@ export async function transcribeVideo( await s3Client.send(uploadCommand); - await db + await db() .update(videos) .set({ transcriptionStatus: "COMPLETE" }) .where(eq(videos.id, videoId)); @@ -121,7 +121,7 @@ export async function transcribeVideo( message: "VTT file generated and uploaded successfully", }; } catch (error) { - await db + await db() .update(videos) .set({ transcriptionStatus: "ERROR" }) .where(eq(videos.id, videoId)); diff --git a/apps/web/actions/workspace/check-domain.ts b/apps/web/actions/workspace/check-domain.ts index c3b060acd..b8acc4ca8 100644 --- a/apps/web/actions/workspace/check-domain.ts +++ b/apps/web/actions/workspace/check-domain.ts @@ -30,14 +30,14 @@ export async function checkWorkspaceDomain(spaceId: string) { const status = await checkDomainStatus(space.customDomain); if (status.verified && !space.domainVerified) { - await db + await db() .update(spaces) .set({ domainVerified: new Date(), }) .where(eq(spaces.id, spaceId)); } else if (!status.verified && space.domainVerified) { - await db + await db() .update(spaces) .set({ domainVerified: null, diff --git a/apps/web/actions/workspace/get-space.ts b/apps/web/actions/workspace/get-space.ts index 8ea9b235e..32f5b2fb0 100644 --- a/apps/web/actions/workspace/get-space.ts +++ b/apps/web/actions/workspace/get-space.ts @@ -9,7 +9,7 @@ export async function getSpace(spaceId: string) { throw new Error("Space ID is required"); } - const [space] = await db + const [space] = await db() .select({ workosOrganizationId: spaces.workosOrganizationId, workosConnectionId: spaces.workosConnectionId, @@ -27,4 +27,4 @@ export async function getSpace(spaceId: string) { connectionId: space.workosConnectionId, name: space.name, }; -} \ No newline at end of file +} diff --git a/apps/web/actions/workspace/manage-billing.ts b/apps/web/actions/workspace/manage-billing.ts index ee866f867..904959d17 100644 --- a/apps/web/actions/workspace/manage-billing.ts +++ b/apps/web/actions/workspace/manage-billing.ts @@ -23,7 +23,7 @@ export async function manageBilling() { }, }); - await db + await db() .update(users) .set({ stripeCustomerId: customer.id, diff --git a/apps/web/actions/workspace/remove-domain.ts b/apps/web/actions/workspace/remove-domain.ts index eb4cbb57b..a7de90654 100644 --- a/apps/web/actions/workspace/remove-domain.ts +++ b/apps/web/actions/workspace/remove-domain.ts @@ -39,7 +39,7 @@ export async function removeWorkspaceDomain(spaceId: string) { ); } - await db + await db() .update(spaces) .set({ customDomain: null, diff --git a/apps/web/actions/workspace/remove-invite.ts b/apps/web/actions/workspace/remove-invite.ts index 58d0e2f95..6e04c2ab5 100644 --- a/apps/web/actions/workspace/remove-invite.ts +++ b/apps/web/actions/workspace/remove-invite.ts @@ -1,4 +1,4 @@ -'use server'; +"use server"; import { getCurrentUser } from "@cap/database/auth/session"; import { spaces, spaceInvites } from "@cap/database/schema"; @@ -6,17 +6,14 @@ import { db } from "@cap/database"; import { eq, and } from "drizzle-orm"; import { revalidatePath } from "next/cache"; -export async function removeWorkspaceInvite( - inviteId: string, - spaceId: string -) { +export async function removeWorkspaceInvite(inviteId: string, spaceId: string) { const user = await getCurrentUser(); if (!user) { throw new Error("Unauthorized"); } - const space = await db + const space = await db() .select() .from(spaces) .where(eq(spaces.id, spaceId)) @@ -30,7 +27,7 @@ export async function removeWorkspaceInvite( throw new Error("Only the owner can remove workspace invites"); } - const result = await db + const result = await db() .delete(spaceInvites) .where( and(eq(spaceInvites.id, inviteId), eq(spaceInvites.spaceId, spaceId)) @@ -40,7 +37,7 @@ export async function removeWorkspaceInvite( throw new Error("Invite not found"); } - revalidatePath('/dashboard/settings/workspace'); - + revalidatePath("/dashboard/settings/workspace"); + return { success: true }; -} \ No newline at end of file +} diff --git a/apps/web/actions/workspace/update-details.ts b/apps/web/actions/workspace/update-details.ts index 4c7b9926e..513cab465 100644 --- a/apps/web/actions/workspace/update-details.ts +++ b/apps/web/actions/workspace/update-details.ts @@ -27,7 +27,7 @@ export async function updateWorkspaceDetails( throw new Error("Only the owner can update workspace details"); } - await db + await db() .update(spaces) .set({ name: workspaceName, diff --git a/apps/web/actions/workspace/update-domain.ts b/apps/web/actions/workspace/update-domain.ts index 632ff026b..664c865ec 100644 --- a/apps/web/actions/workspace/update-domain.ts +++ b/apps/web/actions/workspace/update-domain.ts @@ -30,7 +30,7 @@ export async function updateDomain(domain: string, spaceId: string) { throw new Error(addDomainResponse.error.message); } - await db + await db() .update(spaces) .set({ customDomain: domain, @@ -41,7 +41,7 @@ export async function updateDomain(domain: string, spaceId: string) { const status = await checkDomainStatus(domain); if (status.verified) { - await db + await db() .update(spaces) .set({ domainVerified: new Date(), diff --git a/apps/web/app/api/caps/share/route.ts b/apps/web/app/api/caps/share/route.ts index 67b83a9b6..b53c33bec 100644 --- a/apps/web/app/api/caps/share/route.ts +++ b/apps/web/app/api/caps/share/route.ts @@ -21,7 +21,7 @@ export async function POST(request: NextRequest) { } // Get current shared spaces - const currentSharedSpaces = await db + const currentSharedSpaces = await db() .select() .from(sharedVideos) .where(eq(sharedVideos.videoId, capId)); @@ -29,7 +29,7 @@ export async function POST(request: NextRequest) { // Remove spaces that are no longer shared for (const sharedSpace of currentSharedSpaces) { if (!spaceIds.includes(sharedSpace.spaceId)) { - await db + await db() .delete(sharedVideos) .where( and( diff --git a/apps/web/app/api/desktop/app.ts b/apps/web/app/api/desktop/app.ts index bf3886674..a177fde9d 100644 --- a/apps/web/app/api/desktop/app.ts +++ b/apps/web/app/api/desktop/app.ts @@ -68,7 +68,7 @@ app.get("/plan", async (c) => { ); if (activeSubscription) { isSubscribed = true; - await db + await db() .update(users) .set({ stripeSubscriptionStatus: activeSubscription.status, @@ -119,7 +119,7 @@ app.post( metadata: { userId: user.id }, }); - await db + await db() .update(users) .set({ stripeCustomerId: customer.id }) .where(eq(users.id, user.id)); diff --git a/apps/web/app/api/desktop/s3/config/app.ts b/apps/web/app/api/desktop/s3/config/app.ts index 851965083..67af6a2cc 100644 --- a/apps/web/app/api/desktop/s3/config/app.ts +++ b/apps/web/app/api/desktop/s3/config/app.ts @@ -42,14 +42,14 @@ app.post( }; // Check if user already has a bucket config - const [existingBucket] = await db + const [existingBucket] = await db() .select() .from(s3Buckets) .where(eq(s3Buckets.ownerId, user.id)); if (existingBucket) { // Update existing config - await db + await db() .update(s3Buckets) .set(encryptedConfig) .where(eq(s3Buckets.id, existingBucket.id)); @@ -96,7 +96,7 @@ app.get("/get", async (c) => { const user = c.get("user"); try { - const [bucket] = await db + const [bucket] = await db() .select() .from(s3Buckets) .where(eq(s3Buckets.ownerId, user.id)); diff --git a/apps/web/app/api/desktop/video/app.ts b/apps/web/app/api/desktop/video/app.ts index d4c0507dd..70748525f 100644 --- a/apps/web/app/api/desktop/video/app.ts +++ b/apps/web/app/api/desktop/video/app.ts @@ -39,7 +39,7 @@ app.get( if (!isUpgraded && duration && duration > 300) return c.json({ error: "upgrade_required" }, { status: 403 }); - const [bucket] = await db + const [bucket] = await db() .select() .from(s3Buckets) .where(eq(s3Buckets.ownerId, user.id)); @@ -53,7 +53,7 @@ app.get( })} ${date.getFullYear()}`; if (videoId !== undefined) { - const [video] = await db + const [video] = await db() .select() .from(videos) .where(eq(videos.id, videoId)); @@ -99,7 +99,7 @@ app.get( // Check if this is the user's first video and send the first shareable link email try { - const videoCount = await db + const videoCount = await db() .select({ count: count() }) .from(videos) .where(eq(videos.ownerId, user.id)); diff --git a/apps/web/app/api/email/new-comment/route.ts b/apps/web/app/api/email/new-comment/route.ts index 8d76362e8..134e8ddfb 100644 --- a/apps/web/app/api/email/new-comment/route.ts +++ b/apps/web/app/api/email/new-comment/route.ts @@ -25,7 +25,7 @@ export async function POST(request: NextRequest) { try { console.log(`Fetching comment details for commentId: ${commentId}`); // Get the comment details - const commentDetails = await db + const commentDetails = await db() .select({ id: comments.id, content: comments.content, @@ -68,7 +68,7 @@ export async function POST(request: NextRequest) { console.log(`Fetching video details for videoId: ${comment.videoId}`); // Get the video details - const videoDetails = await db + const videoDetails = await db() .select({ id: videos.id, name: videos.name, @@ -97,7 +97,7 @@ export async function POST(request: NextRequest) { console.log(`Fetching owner details for userId: ${video.ownerId}`); // Get the video owner's email - const ownerDetails = await db + const ownerDetails = await db() .select({ id: users.id, email: users.email, @@ -128,7 +128,7 @@ export async function POST(request: NextRequest) { let commenterName = "Anonymous"; if (comment.authorId) { console.log(`Fetching commenter details for userId: ${comment.authorId}`); - const commenterDetails = await db + const commenterDetails = await db() .select({ id: users.id, name: users.name, @@ -178,7 +178,7 @@ export async function POST(request: NextRequest) { console.log( `Checking for recent comments since ${fifteenMinutesAgo.toISOString()}` ); - const recentComments = await db + const recentComments = await db() .select({ id: comments.id, }) diff --git a/apps/web/app/api/invite/accept/route.ts b/apps/web/app/api/invite/accept/route.ts index 20d8fb632..d3e2e7170 100644 --- a/apps/web/app/api/invite/accept/route.ts +++ b/apps/web/app/api/invite/accept/route.ts @@ -15,7 +15,7 @@ export async function POST(request: NextRequest) { try { // Find the invite - const [invite] = await db + const [invite] = await db() .select() .from(spaceInvites) .where(eq(spaceInvites.id, inviteId)); @@ -30,7 +30,7 @@ export async function POST(request: NextRequest) { } // Get the space owner's subscription ID - const [spaceOwner] = await db + const [spaceOwner] = await db() .select({ stripeSubscriptionId: users.stripeSubscriptionId, }) @@ -53,7 +53,7 @@ export async function POST(request: NextRequest) { }); // Update the user's thirdPartyStripeSubscriptionId - await db + await db() .update(users) .set({ thirdPartyStripeSubscriptionId: spaceOwner.stripeSubscriptionId, diff --git a/apps/web/app/api/playlist/route.ts b/apps/web/app/api/playlist/route.ts index 9e8a8d344..09b75e7a7 100644 --- a/apps/web/app/api/playlist/route.ts +++ b/apps/web/app/api/playlist/route.ts @@ -48,7 +48,7 @@ export async function GET(request: NextRequest) { ); } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets }) .from(videos) .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id)) diff --git a/apps/web/app/api/revalidate/route.ts b/apps/web/app/api/revalidate/route.ts index 2f24caad8..d6afc0645 100644 --- a/apps/web/app/api/revalidate/route.ts +++ b/apps/web/app/api/revalidate/route.ts @@ -21,7 +21,7 @@ export async function POST(request: NextRequest) { }); } - const [video] = await db + const [video] = await db() .select() .from(videos) .where(eq(videos.id, videoId)); diff --git a/apps/web/app/api/screenshot/route.ts b/apps/web/app/api/screenshot/route.ts index 1313cc647..f2badabea 100644 --- a/apps/web/app/api/screenshot/route.ts +++ b/apps/web/app/api/screenshot/route.ts @@ -41,7 +41,7 @@ export async function GET(request: NextRequest) { ); } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets }) .from(videos) .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id)) diff --git a/apps/web/app/api/settings/billing/manage/route.ts b/apps/web/app/api/settings/billing/manage/route.ts index 5201d478d..0862fde29 100644 --- a/apps/web/app/api/settings/billing/manage/route.ts +++ b/apps/web/app/api/settings/billing/manage/route.ts @@ -24,7 +24,7 @@ export async function POST(request: NextRequest) { }, }); - await db + await db() .update(users) .set({ stripeCustomerId: customer.id, diff --git a/apps/web/app/api/settings/billing/subscribe/route.ts b/apps/web/app/api/settings/billing/subscribe/route.ts index ab722fe2d..489d19094 100644 --- a/apps/web/app/api/settings/billing/subscribe/route.ts +++ b/apps/web/app/api/settings/billing/subscribe/route.ts @@ -56,7 +56,7 @@ export async function POST(request: NextRequest) { console.log("Created Stripe customer:", customer.id); - await db + await db() .update(users) .set({ stripeCustomerId: customer.id, diff --git a/apps/web/app/api/settings/billing/usage/route.ts b/apps/web/app/api/settings/billing/usage/route.ts index 2a0bb87e8..7fcc81883 100644 --- a/apps/web/app/api/settings/billing/usage/route.ts +++ b/apps/web/app/api/settings/billing/usage/route.ts @@ -12,7 +12,7 @@ export async function GET(request: NextRequest) { return Response.json({ auth: false }, { status: 401 }); } - const numberOfVideos = await db + const numberOfVideos = await db() .select({ count: count() }) .from(videos) .where(eq(videos.ownerId, user.id)); diff --git a/apps/web/app/api/settings/onboarding/route.ts b/apps/web/app/api/settings/onboarding/route.ts index 0e88b433c..4e1e37453 100644 --- a/apps/web/app/api/settings/onboarding/route.ts +++ b/apps/web/app/api/settings/onboarding/route.ts @@ -15,7 +15,7 @@ export async function POST(request: NextRequest) { return Response.json({ error: true }, { status: 401 }); } - await db + await db() .update(users) .set({ name: firstName, @@ -28,7 +28,7 @@ export async function POST(request: NextRequest) { fullName += ` ${lastName}`; } - const [space] = await db + const [space] = await db() .select() .from(spaces) .where(or(eq(spaces.ownerId, user.id), eq(spaceMembers.userId, user.id))) @@ -52,7 +52,7 @@ export async function POST(request: NextRequest) { spaceId, }); - await db + await db() .update(users) .set({ activeSpaceId: spaceId }) .where(eq(users.id, user.id)); diff --git a/apps/web/app/api/settings/user/name/route.ts b/apps/web/app/api/settings/user/name/route.ts index 70de72548..3f2c51d5b 100644 --- a/apps/web/app/api/settings/user/name/route.ts +++ b/apps/web/app/api/settings/user/name/route.ts @@ -14,7 +14,7 @@ export async function POST(request: NextRequest) { return Response.json({ error: true }, { status: 401 }); } - await db + await db() .update(users) .set({ name: firstName, diff --git a/apps/web/app/api/thumbnail/route.ts b/apps/web/app/api/thumbnail/route.ts index 6047753a9..6bc4af261 100644 --- a/apps/web/app/api/thumbnail/route.ts +++ b/apps/web/app/api/thumbnail/route.ts @@ -30,7 +30,7 @@ export async function GET(request: NextRequest) { ); } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets, diff --git a/apps/web/app/api/upload/multipart/[...route]/route.ts b/apps/web/app/api/upload/multipart/[...route]/route.ts index 8ab1803bf..719f48280 100644 --- a/apps/web/app/api/upload/multipart/[...route]/route.ts +++ b/apps/web/app/api/upload/multipart/[...route]/route.ts @@ -322,7 +322,7 @@ app.post( ); async function getUserBucketWithClient(userId: string) { - const [bucket] = await db + const [bucket] = await db() .select() .from(s3Buckets) .where(eq(s3Buckets.ownerId, userId)); diff --git a/apps/web/app/api/upload/mux/create/route.ts b/apps/web/app/api/upload/mux/create/route.ts index e17645cef..191b2f2fc 100644 --- a/apps/web/app/api/upload/mux/create/route.ts +++ b/apps/web/app/api/upload/mux/create/route.ts @@ -66,7 +66,7 @@ export async function GET(request: NextRequest) { ); } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets, @@ -148,7 +148,7 @@ export async function GET(request: NextRequest) { ); if (videoSegmentKeys.length > 149) { - await db + await db() .update(videos) .set({ skipProcessing: true }) .where(eq(videos.id, videoId)); diff --git a/apps/web/app/api/upload/signed/route.ts b/apps/web/app/api/upload/signed/route.ts index 166c5c66a..0e20d5aa1 100644 --- a/apps/web/app/api/upload/signed/route.ts +++ b/apps/web/app/api/upload/signed/route.ts @@ -48,7 +48,7 @@ export async function POST(request: NextRequest) { } try { - const [bucket] = await db + const [bucket] = await db() .select() .from(s3Buckets) .where(eq(s3Buckets.ownerId, user.id)); diff --git a/apps/web/app/api/video/comment/delete/route.ts b/apps/web/app/api/video/comment/delete/route.ts index 22ca9ca58..7b361146d 100644 --- a/apps/web/app/api/video/comment/delete/route.ts +++ b/apps/web/app/api/video/comment/delete/route.ts @@ -20,7 +20,7 @@ export async function DELETE(request: NextRequest) { try { // First, verify that the comment belongs to the user - const query = await db + const query = await db() .select() .from(comments) .where(and(eq(comments.id, commentId), eq(comments.authorId, user.id))); @@ -36,7 +36,7 @@ export async function DELETE(request: NextRequest) { } // Delete the comment and all its replies - await db + await db() .delete(comments) .where( or(eq(comments.id, commentId), eq(comments.parentCommentId, commentId)) diff --git a/apps/web/app/api/video/delete/route.ts b/apps/web/app/api/video/delete/route.ts index 7f82a8d12..49e84ee62 100644 --- a/apps/web/app/api/video/delete/route.ts +++ b/apps/web/app/api/video/delete/route.ts @@ -24,7 +24,7 @@ export async function DELETE(request: NextRequest) { return Response.json({ error: true }, { status: 401 }); } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets }) .from(videos) .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id)) @@ -51,7 +51,7 @@ export async function DELETE(request: NextRequest) { ); } - await db + await db() .delete(videos) .where(and(eq(videos.id, videoId), eq(videos.ownerId, userId))); diff --git a/apps/web/app/api/video/domain-info/route.ts b/apps/web/app/api/video/domain-info/route.ts index 56c7c2147..c45f49a86 100644 --- a/apps/web/app/api/video/domain-info/route.ts +++ b/apps/web/app/api/video/domain-info/route.ts @@ -13,7 +13,7 @@ export async function GET(request: NextRequest) { try { // First, get the video to find the owner or shared space - const video = await db + const video = await db() .select({ id: videos.id, ownerId: videos.ownerId, @@ -32,7 +32,7 @@ export async function GET(request: NextRequest) { } // Check if the video is shared with a space - const sharedVideo = await db + const sharedVideo = await db() .select({ spaceId: sharedVideos.spaceId, }) @@ -47,7 +47,7 @@ export async function GET(request: NextRequest) { // If we have a space ID, get the space's custom domain if (spaceId) { - const space = await db + const space = await db() .select({ customDomain: spaces.customDomain, domainVerified: spaces.domainVerified, @@ -65,7 +65,7 @@ export async function GET(request: NextRequest) { } // If no shared space or no custom domain, check the owner's space - const ownerSpaces = await db + const ownerSpaces = await db() .select({ customDomain: spaces.customDomain, domainVerified: spaces.domainVerified, diff --git a/apps/web/app/api/video/metadata/route.ts b/apps/web/app/api/video/metadata/route.ts index 0ed74391d..22c095469 100644 --- a/apps/web/app/api/video/metadata/route.ts +++ b/apps/web/app/api/video/metadata/route.ts @@ -30,7 +30,7 @@ export async function PUT(request: NextRequest) { return Response.json({ error: true }, { status: 401 }); } - await db + await db() .update(videos) .set({ metadata: metadata, diff --git a/apps/web/app/api/webhooks/stripe/route.ts b/apps/web/app/api/webhooks/stripe/route.ts index 0a97147de..77caed4d6 100644 --- a/apps/web/app/api/webhooks/stripe/route.ts +++ b/apps/web/app/api/webhooks/stripe/route.ts @@ -29,7 +29,7 @@ async function findUserWithRetry( // Try finding by userId first if available if (userId) { console.log(`Attempting to find user by ID: ${userId}`); - const userById = await db + const userById = await db() .select() .from(users) .where(eq(users.id, userId)) @@ -46,7 +46,7 @@ async function findUserWithRetry( // If not found by ID or no ID provided, try email if (email) { console.log(`Attempting to find user by email: ${email}`); - const userByEmail = await db + const userByEmail = await db() .select() .from(users) .where(eq(users.email, email)) @@ -182,7 +182,7 @@ export const POST = async (req: Request) => { inviteQuota, }); - await db + await db() .update(users) .set({ stripeSubscriptionId: session.subscription as string, @@ -308,7 +308,7 @@ export const POST = async (req: Request) => { inviteQuota, }); - await db + await db() .update(users) .set({ stripeSubscriptionId: subscription.id, @@ -336,7 +336,7 @@ export const POST = async (req: Request) => { if (!foundUserId) { console.log("No user found in metadata, checking customer email"); if ("email" in customer && customer.email) { - const userByEmail = await db + const userByEmail = await db() .select() .from(users) .where(eq(users.email, customer.email)) @@ -363,7 +363,7 @@ export const POST = async (req: Request) => { } } - const userResult = await db + const userResult = await db() .select() .from(users) .where(eq(users.id, foundUserId)); @@ -373,7 +373,7 @@ export const POST = async (req: Request) => { return new Response("No user found", { status: 400 }); } - await db + await db() .update(users) .set({ stripeSubscriptionId: subscription.id, diff --git a/apps/web/app/dashboard/_components/AdminNavbar/server.ts b/apps/web/app/dashboard/_components/AdminNavbar/server.ts index 0d0e14eab..44617ba32 100644 --- a/apps/web/app/dashboard/_components/AdminNavbar/server.ts +++ b/apps/web/app/dashboard/_components/AdminNavbar/server.ts @@ -10,7 +10,7 @@ export async function updateActiveSpace(spaceId: string) { const user = await getCurrentUser(); if (!user) throw new Error("Unauthorized"); - const [space] = await db + const [space] = await db() .select({ space: spaces }) .from(spaces) .innerJoin( @@ -21,7 +21,7 @@ export async function updateActiveSpace(spaceId: string) { if (!space) throw new Error("Space not found"); - await db + await db() .update(users) .set({ activeSpaceId: space.space.id }) .where(eq(users.id, user.id)); diff --git a/apps/web/app/dashboard/admin/actions.ts b/apps/web/app/dashboard/admin/actions.ts index 677d4da51..faf128f1c 100644 --- a/apps/web/app/dashboard/admin/actions.ts +++ b/apps/web/app/dashboard/admin/actions.ts @@ -9,7 +9,7 @@ export async function lookupUserById(data: FormData) { const currentUser = await getCurrentUser(); if (!currentUser?.email.endsWith("@cap.so")) return; - const [user] = await db + const [user] = await db() .select() .from(users) .where(eq(users.id, data.get("id") as string)); diff --git a/apps/web/app/dashboard/caps/page.tsx b/apps/web/app/dashboard/caps/page.tsx index 01ea59afa..25428a30e 100644 --- a/apps/web/app/dashboard/caps/page.tsx +++ b/apps/web/app/dashboard/caps/page.tsx @@ -39,14 +39,14 @@ export default async function CapsPage({ const limit = Number(searchParams.limit) || 15; const offset = (page - 1) * limit; - const totalCountResult = await db + const totalCountResult = await db() .select({ count: count() }) .from(videos) .where(eq(videos.ownerId, userId)); const totalCount = totalCountResult[0]?.count || 0; - const videoData = await db + const videoData = await db() .select({ id: videos.id, ownerId: videos.ownerId, @@ -97,7 +97,7 @@ export default async function CapsPage({ .limit(limit) .offset(offset); - const userSpaces = await db + const userSpaces = await db() .select({ id: spaces.id, name: spaces.name, diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx index 9c24901ca..205e97111 100644 --- a/apps/web/app/dashboard/layout.tsx +++ b/apps/web/app/dashboard/layout.tsx @@ -36,7 +36,7 @@ export default async function DashboardLayout({ redirect("/onboarding"); } - const spacesWithMembers = await db + const spacesWithMembers = await db() .select({ space: spaces, member: spaceMembers, @@ -57,7 +57,7 @@ export default async function DashboardLayout({ let spaceInvitesData: (typeof spaceInvites.$inferSelect)[] = []; if (spaceIds.length > 0) { - spaceInvitesData = await db + spaceInvitesData = await db() .select() .from(spaceInvites) .where(inArray(spaceInvites.spaceId, spaceIds)); @@ -73,7 +73,7 @@ export default async function DashboardLayout({ return acc; }, []) .map(async (space) => { - const allMembers = await db + const allMembers = await db() .select({ member: spaceMembers, user: { @@ -87,7 +87,7 @@ export default async function DashboardLayout({ .leftJoin(users, eq(spaceMembers.userId, users.id)) .where(eq(spaceMembers.spaceId, space.id)); - const owner = await db + const owner = await db() .select({ inviteQuota: users.inviteQuota, }) @@ -95,7 +95,7 @@ export default async function DashboardLayout({ .where(eq(users.id, space.ownerId)) .then((result) => result[0]); - const totalInvitesResult = await db + const totalInvitesResult = await db() .select({ value: sql` ${count(spaceMembers.id)} + ${count(spaceInvites.id)} diff --git a/apps/web/app/dashboard/shared-caps/page.tsx b/apps/web/app/dashboard/shared-caps/page.tsx index b3294e830..bb9378a5a 100644 --- a/apps/web/app/dashboard/shared-caps/page.tsx +++ b/apps/web/app/dashboard/shared-caps/page.tsx @@ -31,7 +31,7 @@ export default async function SharedCapsPage({ let activeSpaceId = user?.activeSpaceId; if (!activeSpaceId) { // Get the first available space if activeSpaceId doesn't exist - const firstSpace = await db + const firstSpace = await db() .select({ id: spaces.id }) .from(spaces) .innerJoin(spaceMembers, eq(spaces.id, spaceMembers.spaceId)) @@ -49,14 +49,14 @@ export default async function SharedCapsPage({ const offset = (page - 1) * limit; - const totalCountResult = await db + const totalCountResult = await db() .select({ count: count() }) .from(sharedVideos) .where(eq(sharedVideos.spaceId, activeSpaceId)); const totalCount = totalCountResult[0]?.count || 0; - const sharedVideoData = await db + const sharedVideoData = await db() .select({ id: videos.id, ownerId: videos.ownerId, @@ -68,7 +68,7 @@ export default async function SharedCapsPage({ ownerName: users.name, effectiveDate: sql` COALESCE( - JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), + JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), ${videos.createdAt} ) `, @@ -88,7 +88,7 @@ export default async function SharedCapsPage({ ) .orderBy( desc(sql`COALESCE( - JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), + JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), ${videos.createdAt} )`) ) @@ -115,7 +115,7 @@ export default async function SharedCapsPage({ console.log(processedSharedVideoData); // Debug: Check if there are any shared videos for this space - const debugSharedVideos = await db + const debugSharedVideos = await db() .select({ id: sharedVideos.id, videoId: sharedVideos.videoId, @@ -129,7 +129,7 @@ export default async function SharedCapsPage({ // Debug: Check if the videos exist if (debugSharedVideos.length > 0) { - const debugVideos = await db + const debugVideos = await db() .select({ id: videos.id, name: videos.name, diff --git a/apps/web/app/invite/[inviteId]/page.tsx b/apps/web/app/invite/[inviteId]/page.tsx index f379e741f..8ec1afa51 100644 --- a/apps/web/app/invite/[inviteId]/page.tsx +++ b/apps/web/app/invite/[inviteId]/page.tsx @@ -25,7 +25,7 @@ export async function generateMetadata({ params }: Props): Promise { } async function getInviteDetails(inviteId: string) { - const query = await db + const query = await db() .select({ invite: spaceInvites, spaceName: spaces.name, diff --git a/apps/web/app/s/[videoId]/page.tsx b/apps/web/app/s/[videoId]/page.tsx index 6130941b1..16a43d9a7 100644 --- a/apps/web/app/s/[videoId]/page.tsx +++ b/apps/web/app/s/[videoId]/page.tsx @@ -110,7 +110,7 @@ export default async function ShareVideoPage(props: Props) { const userId = user?.id as string | undefined; console.log("[ShareVideoPage] Current user:", userId); - const videoWithSpace = await db + const videoWithSpace = await db() .select({ id: videos.id, name: videos.name, @@ -148,7 +148,7 @@ export default async function ShareVideoPage(props: Props) { } if (video.sharedSpace?.spaceId) { - const space = await db + const space = await db() .select() .from(spaces) .where(eq(spaces.id, video.sharedSpace.spaceId)) @@ -190,7 +190,7 @@ export default async function ShareVideoPage(props: Props) { } console.log("[ShareVideoPage] Fetching comments for video:", videoId); - const commentsQuery: CommentWithAuthor[] = await db + const commentsQuery: CommentWithAuthor[] = await db() .select({ id: comments.id, content: comments.content, @@ -241,7 +241,7 @@ export default async function ShareVideoPage(props: Props) { let domainVerified = false; if (video.sharedSpace?.spaceId) { - const spaceData = await db + const spaceData = await db() .select({ customDomain: spaces.customDomain, domainVerified: spaces.domainVerified, @@ -259,7 +259,7 @@ export default async function ShareVideoPage(props: Props) { } if (!customDomain && video.ownerId) { - const ownerSpaces = await db + const ownerSpaces = await db() .select({ customDomain: spaces.customDomain, domainVerified: spaces.domainVerified, @@ -281,7 +281,7 @@ export default async function ShareVideoPage(props: Props) { } const membersList = video.sharedSpace?.spaceId - ? await db + ? await db() .select({ userId: spaceMembers.userId, }) diff --git a/apps/web/components/forms/server.ts b/apps/web/components/forms/server.ts index 57799fb65..c430eb848 100644 --- a/apps/web/components/forms/server.ts +++ b/apps/web/components/forms/server.ts @@ -25,7 +25,7 @@ export async function createSpace(args: { name: string }) { spaceId, }); - await db + await db() .update(users) .set({ activeSpaceId: spaceId }) .where(eq(users.id, user.id)); diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index a388edf79..8acddcd6f 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -46,7 +46,7 @@ export async function middleware(request: NextRequest) { if (verifiedDomain?.value === hostname) return NextResponse.next(); // Query the space with this custom domain - const [space] = await db + const [space] = await db() .select() .from(spaces) .where(eq(spaces.customDomain, hostname)); diff --git a/packages/database/auth/auth-options.ts b/packages/database/auth/auth-options.ts index 96a442da0..27dc22e20 100644 --- a/packages/database/auth/auth-options.ts +++ b/packages/database/auth/auth-options.ts @@ -119,7 +119,7 @@ export const authOptions = (): NextAuthOptions => { }); // Update user's activeSpaceId - await db + await db() .update(users) .set({ activeSpaceId: spaceId }) .where(eq(users.id, user.id)); @@ -140,7 +140,7 @@ export const authOptions = (): NextAuthOptions => { return session; }, async jwt({ token, user }) { - const [dbUser] = await db + const [dbUser] = await db() .select() .from(users) .where(eq(users.email, token.email || "")) diff --git a/packages/database/auth/drizzle-adapter.ts b/packages/database/auth/drizzle-adapter.ts index 2934e6203..39776e49e 100644 --- a/packages/database/auth/drizzle-adapter.ts +++ b/packages/database/auth/drizzle-adapter.ts @@ -17,7 +17,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { image: userData.image, activeSpaceId: "", }); - const rows = await db + const rows = await db() .select() .from(users) .where(eq(users.email, userData.email)) @@ -33,7 +33,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { }, }); - await db + await db() .update(users) .set({ stripeCustomerId: customer.id, @@ -44,7 +44,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row; }, async getUser(id) { - const rows = await db + const rows = await db() .select() .from(users) .where(eq(users.id, id)) @@ -53,7 +53,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row ?? null; }, async getUserByEmail(email) { - const rows = await db + const rows = await db() .select() .from(users) .where(eq(users.email, email)) @@ -66,7 +66,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row ?? null; }, async getUserByAccount({ providerAccountId, provider }) { - const rows = await db + const rows = await db() .select() .from(users) .innerJoin(accounts, eq(users.id, accounts.userId)) @@ -83,7 +83,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { async updateUser({ id, ...userData }) { if (!id) throw new Error("User not found"); await db().update(users).set(userData).where(eq(users.id, id)); - const rows = await db + const rows = await db() .select() .from(users) .where(eq(users.id, id)) @@ -114,7 +114,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { }); }, async unlinkAccount({ providerAccountId, provider }: any) { - await db + await db() .delete(accounts) .where( and( @@ -130,7 +130,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { sessionToken: data.sessionToken, userId: data.userId, }); - const rows = await db + const rows = await db() .select() .from(sessions) .where(eq(sessions.sessionToken, data.sessionToken)) @@ -140,7 +140,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row; }, async getSessionAndUser(sessionToken) { - const rows = await db + const rows = await db() .select({ user: users, session: { @@ -168,11 +168,11 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { }; }, async updateSession(session) { - await db + await db() .update(sessions) .set(session) .where(eq(sessions.sessionToken, session.sessionToken)); - const rows = await db + const rows = await db() .select() .from(sessions) .where(eq(sessions.sessionToken, session.sessionToken)) @@ -189,7 +189,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { async createVerificationToken(verificationToken) { console.log({ verificationToken }); // First, check if a token for the given identifier already exists - const existingTokens = await db + const existingTokens = await db() .select() .from(verificationTokens) .where(eq(verificationTokens.identifier, verificationToken.identifier)) @@ -199,7 +199,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { // or handle it based on your business logic if (existingTokens.length > 0) { // For example, updating the existing token: - await db + await db() .update(verificationTokens) .set({ token: verificationToken.token, @@ -211,7 +211,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { ); // Return the updated token - return await db + return await db() .select() .from(verificationTokens) .where( @@ -229,7 +229,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { }); // Retrieve and return the newly created token - const rows = await db + const rows = await db() .select() .from(verificationTokens) .where(eq(verificationTokens.token, verificationToken.token)) @@ -239,14 +239,14 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row; }, async useVerificationToken({ identifier, token }) { - const rows = await db + const rows = await db() .select() .from(verificationTokens) .where(eq(verificationTokens.token, token)) .limit(1); const row = rows[0]; if (!row) return null; - await db + await db() .delete(verificationTokens) .where( and( diff --git a/packages/database/auth/session.ts b/packages/database/auth/session.ts index 51f278c84..a8c6206d8 100644 --- a/packages/database/auth/session.ts +++ b/packages/database/auth/session.ts @@ -17,7 +17,7 @@ export const getCurrentUser = async (session?: Session) => { return null; } - const [currentUser] = await db + const [currentUser] = await db() .select() .from(users) .where(eq(users.id, _session?.user.id)); From 531925e125ef6a2a1699fa4f1842a2e6c474174c Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 28 Apr 2025 17:54:42 -0700 Subject: [PATCH 13/18] fix enough ts issues --- apps/web/actions/workspace/manage-billing.ts | 4 +- apps/web/app/api/desktop/app.ts | 11 +- apps/web/app/api/desktop/session/app.ts | 2 +- .../app/api/settings/billing/manage/route.ts | 4 +- .../api/settings/billing/subscribe/route.ts | 4 +- apps/web/app/api/webhooks/stripe/route.ts | 14 +- apps/web/app/blog/_components/Share.tsx | 1 - .../shared-caps/components/SharedCapCard.tsx | 4 +- apps/web/app/layout.tsx | 5 +- apps/web/app/s/[videoId]/Share.tsx | 1 - .../app/s/[videoId]/_components/Toolbar.tsx | 1 - apps/web/utils/video/ffmpeg/helpers.ts | 324 +++++++++--------- packages/database/auth/auth-options.ts | 6 +- packages/database/auth/drizzle-adapter.ts | 78 ++--- packages/database/auth/session.ts | 4 +- packages/database/crypto.ts | 15 +- packages/ui-solid/package.json | 2 +- packages/ui-solid/src/{index.ts => index.tsx} | 0 18 files changed, 241 insertions(+), 239 deletions(-) rename packages/ui-solid/src/{index.ts => index.tsx} (100%) diff --git a/apps/web/actions/workspace/manage-billing.ts b/apps/web/actions/workspace/manage-billing.ts index 904959d17..c93a8f238 100644 --- a/apps/web/actions/workspace/manage-billing.ts +++ b/apps/web/actions/workspace/manage-billing.ts @@ -16,7 +16,7 @@ export async function manageBilling() { } if (!user.stripeCustomerId) { - const customer = await stripe.customers.create({ + const customer = await stripe().customers.create({ email: user.email, metadata: { userId: user.id, @@ -33,7 +33,7 @@ export async function manageBilling() { customerId = customer.id; } - const { url } = await stripe.billingPortal.sessions.create({ + const { url } = await stripe().billingPortal.sessions.create({ customer: customerId as string, return_url: `${serverEnv().WEB_URL}/dashboard/settings/workspace`, }); diff --git a/apps/web/app/api/desktop/app.ts b/apps/web/app/api/desktop/app.ts index a177fde9d..cf06101c3 100644 --- a/apps/web/app/api/desktop/app.ts +++ b/apps/web/app/api/desktop/app.ts @@ -60,7 +60,7 @@ app.get("/plan", async (c) => { if (!isSubscribed && !user.stripeSubscriptionId && user.stripeCustomerId) { try { - const subscriptions = await stripe.subscriptions.list({ + const subscriptions = await stripe().subscriptions.list({ customer: user.stripeCustomerId, }); const activeSubscription = subscriptions.data.find( @@ -82,9 +82,10 @@ app.get("/plan", async (c) => { } let intercomHash = ""; - if (serverEnv().INTERCOM_SECRET) { + const intercomSecret = serverEnv().INTERCOM_SECRET; + if (intercomSecret) { intercomHash = crypto - .createHmac("sha256", serverEnv().INTERCOM_SECRET) + .createHmac("sha256", intercomSecret) .update(user?.id ?? "") .digest("hex"); } @@ -114,7 +115,7 @@ app.post( if (user.stripeCustomerId === null) { console.log("[POST] Creating new Stripe customer"); - const customer = await stripe.customers.create({ + const customer = await stripe().customers.create({ email: user.email, metadata: { userId: user.id }, }); @@ -129,7 +130,7 @@ app.post( } console.log("[POST] Creating checkout session"); - const checkoutSession = await stripe.checkout.sessions.create({ + const checkoutSession = await stripe().checkout.sessions.create({ customer: customerId as string, line_items: [{ price: priceId, quantity: 1 }], mode: "subscription", diff --git a/apps/web/app/api/desktop/session/app.ts b/apps/web/app/api/desktop/session/app.ts index fd29623fa..800cada4a 100644 --- a/apps/web/app/api/desktop/session/app.ts +++ b/apps/web/app/api/desktop/session/app.ts @@ -31,7 +31,7 @@ app.get( serverEnv().WEB_URL }${url.pathname}${url.search}`; - const session = await getServerSession(authOptions); + const session = await getServerSession(authOptions()); if (!session) return c.redirect(loginRedirectUrl); const token = getCookie(c, "next-auth.session-token"); diff --git a/apps/web/app/api/settings/billing/manage/route.ts b/apps/web/app/api/settings/billing/manage/route.ts index 0862fde29..9c47b94c9 100644 --- a/apps/web/app/api/settings/billing/manage/route.ts +++ b/apps/web/app/api/settings/billing/manage/route.ts @@ -17,7 +17,7 @@ export async function POST(request: NextRequest) { } if (!user.stripeCustomerId) { - const customer = await stripe.customers.create({ + const customer = await stripe().customers.create({ email: user.email, metadata: { userId: user.id, @@ -34,7 +34,7 @@ export async function POST(request: NextRequest) { customerId = customer.id; } - const { url } = await stripe.billingPortal.sessions.create({ + const { url } = await stripe().billingPortal.sessions.create({ customer: customerId as string, return_url: `${serverEnv().WEB_URL}/dashboard/settings/workspace`, }); diff --git a/apps/web/app/api/settings/billing/subscribe/route.ts b/apps/web/app/api/settings/billing/subscribe/route.ts index 489d19094..9f617aac5 100644 --- a/apps/web/app/api/settings/billing/subscribe/route.ts +++ b/apps/web/app/api/settings/billing/subscribe/route.ts @@ -47,7 +47,7 @@ export async function POST(request: NextRequest) { if (!user.stripeCustomerId) { console.log("Creating new Stripe customer for user:", user.id); - const customer = await stripe.customers.create({ + const customer = await stripe().customers.create({ email: user.email, metadata: { userId: user.id, @@ -68,7 +68,7 @@ export async function POST(request: NextRequest) { } console.log("Creating checkout session for customer:", customerId); - const checkoutSession = await stripe.checkout.sessions.create({ + const checkoutSession = await stripe().checkout.sessions.create({ customer: customerId as string, line_items: [{ price: priceId, quantity: quantity }], mode: "subscription", diff --git a/apps/web/app/api/webhooks/stripe/route.ts b/apps/web/app/api/webhooks/stripe/route.ts index 77caed4d6..350b12ad3 100644 --- a/apps/web/app/api/webhooks/stripe/route.ts +++ b/apps/web/app/api/webhooks/stripe/route.ts @@ -99,7 +99,7 @@ export const POST = async (req: Request) => { status: 400, }); } - event = stripe.webhooks.constructEvent(buf, sig, webhookSecret); + event = stripe().webhooks.constructEvent(buf, sig, webhookSecret); console.log(`✅ Event received: ${event.type}`); } catch (err: any) { console.log(`❌ Error message: ${err.message}`); @@ -117,7 +117,7 @@ export const POST = async (req: Request) => { subscriptionId: session.subscription, }); - const customer = await stripe.customers.retrieve( + const customer = await stripe().customers.retrieve( session.customer as string ); console.log("Retrieved customer:", { @@ -162,7 +162,7 @@ export const POST = async (req: Request) => { name: dbUser.name, }); - const subscription = await stripe.subscriptions.retrieve( + const subscription = await stripe().subscriptions.retrieve( session.subscription as string ); console.log("Retrieved subscription:", { @@ -235,7 +235,7 @@ export const POST = async (req: Request) => { customerId: subscription.customer, }); - const customer = await stripe.customers.retrieve( + const customer = await stripe().customers.retrieve( subscription.customer as string ); console.log("Retrieved customer:", { @@ -281,7 +281,7 @@ export const POST = async (req: Request) => { }); // Get all active subscriptions for this customer - const subscriptions = await stripe.subscriptions.list({ + const subscriptions = await stripe().subscriptions.list({ customer: customer.id, status: "active", }); @@ -326,7 +326,7 @@ export const POST = async (req: Request) => { if (event.type === "customer.subscription.deleted") { const subscription = event.data.object as Stripe.Subscription; - const customer = await stripe.customers.retrieve( + const customer = await stripe().customers.retrieve( subscription.customer as string ); let foundUserId; @@ -346,7 +346,7 @@ export const POST = async (req: Request) => { foundUserId = userByEmail[0].id; console.log(`User found by email: ${foundUserId}`); // Update customer metadata with userId - await stripe.customers.update(customer.id, { + await stripe().customers.update(customer.id, { metadata: { userId: foundUserId }, }); } else { diff --git a/apps/web/app/blog/_components/Share.tsx b/apps/web/app/blog/_components/Share.tsx index cbe5f2350..2215cf933 100644 --- a/apps/web/app/blog/_components/Share.tsx +++ b/apps/web/app/blog/_components/Share.tsx @@ -2,7 +2,6 @@ import toast from "react-hot-toast"; import { useState } from "react"; -import { clientEnv } from "@cap/env"; interface ShareProps { post: { diff --git a/apps/web/app/dashboard/shared-caps/components/SharedCapCard.tsx b/apps/web/app/dashboard/shared-caps/components/SharedCapCard.tsx index 2a0b263f9..e8fcb9f5f 100644 --- a/apps/web/app/dashboard/shared-caps/components/SharedCapCard.tsx +++ b/apps/web/app/dashboard/shared-caps/components/SharedCapCard.tsx @@ -4,7 +4,7 @@ import { Tooltip } from "@/components/Tooltip"; import { VideoThumbnail } from "@/components/VideoThumbnail"; import { usePublicEnv } from "@/utils/public-env"; import { VideoMetadata } from "@cap/database/types"; -import { NODE_ENV } from "@cap/env"; +import { buildEnv, NODE_ENV } from "@cap/env"; import { faBuilding, faUser } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import moment from "moment"; @@ -49,7 +49,7 @@ export const SharedCapCard: React.FC = ({ href={ activeSpace?.space.customDomain && activeSpace.space.domainVerified ? `https://${activeSpace.space.customDomain}/s/${cap.id}` - : clientEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production" + : buildEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production" ? `https://cap.link/${cap.id}` : `${publicEnv.webUrl}/s/${cap.id}` } diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 3fa1d43ec..34b4165b5 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -54,9 +54,10 @@ export default async function RootLayout({ }) { const user = await getCurrentUser(); let intercomHash = ""; - if (serverEnv().INTERCOM_SECRET) { + const intercomSecret = serverEnv().INTERCOM_SECRET; + if (intercomSecret) { intercomHash = crypto - .createHmac("sha256", serverEnv().INTERCOM_SECRET) + .createHmac("sha256", intercomSecret) .update(user?.id ?? "") .digest("hex"); } diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index 8effc642f..57e984ec2 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -2,7 +2,6 @@ import { userSelectProps } from "@cap/database/auth/session"; import { comments as commentsSchema, videos } from "@cap/database/schema"; -import { clientEnv } from "@cap/env"; import { Logo } from "@cap/ui"; import { useEffect, useRef, useState } from "react"; import { ShareHeader } from "./_components/ShareHeader"; diff --git a/apps/web/app/s/[videoId]/_components/Toolbar.tsx b/apps/web/app/s/[videoId]/_components/Toolbar.tsx index 5ac8c634e..27826165d 100644 --- a/apps/web/app/s/[videoId]/_components/Toolbar.tsx +++ b/apps/web/app/s/[videoId]/_components/Toolbar.tsx @@ -7,7 +7,6 @@ import { useRouter } from "next/navigation"; import { Button } from "@cap/ui"; import toast from "react-hot-toast"; import { AuthOverlay } from "./AuthOverlay"; -import { clientEnv } from "@cap/env"; // million-ignore export const Toolbar = ({ diff --git a/apps/web/utils/video/ffmpeg/helpers.ts b/apps/web/utils/video/ffmpeg/helpers.ts index 836fae483..f715fb88e 100644 --- a/apps/web/utils/video/ffmpeg/helpers.ts +++ b/apps/web/utils/video/ffmpeg/helpers.ts @@ -1,190 +1,190 @@ import { S3_BUCKET_URL } from "@cap/utils"; import { FFmpeg } from "@ffmpeg/ffmpeg"; -import { clientEnv } from "@cap/env"; export const playlistToMp4 = async ( - userId: string, - videoId: string, - videoName: string + userId: string, + videoId: string, + videoName: string ) => { - const ffmpeg = new FFmpeg(); - await ffmpeg.load(); - - if (!ffmpeg) { - throw new Error("FFmpeg not loaded"); - } - - const videoFetch = await fetch( - `/api/video/playlistUrl?userId=${userId}&videoId=${videoId}` - ); - - if (videoFetch.status !== 200) { - throw new Error("Could not fetch video"); - } - - const video = await videoFetch.json(); - - if (!video.playlistOne) { - throw new Error("Video does not have a valid video playlist"); - } - - // Fetch the video playlist data - const videoResponse = await fetch(video.playlistOne); - const videoData = await videoResponse.text(); - const videoUrls = videoData - .split("\n") - .filter((line) => line && !line.startsWith("#")); - - // Download video files and write to FFmpeg FS - for (const [index, url] of videoUrls.entries()) { - const fullUrl = url.startsWith("https") - ? url - : `${S3_BUCKET_URL}/${userId}/${videoId}/output/${url}`; - const segmentResponse = await fetch(fullUrl); - const segmentData = new Uint8Array(await segmentResponse.arrayBuffer()); - await ffmpeg.writeFile(`video${index}.ts`, segmentData); - } - - // Concatenate all video files using FFmpeg - const videoConcatList = videoUrls - .map((_, index) => `file 'video${index}.ts'`) - .join("\n"); - await ffmpeg.writeFile("videolist.txt", videoConcatList); - - if (video.playlistTwo) { - // Fetch the audio playlist data if available - const audioResponse = await fetch(video.playlistTwo); - const audioData = await audioResponse.text(); - const audioUrls = audioData - .split("\n") - .filter((line) => line && !line.startsWith("#")); - - // Download audio files and write to FFmpeg FS - for (const [index, url] of audioUrls.entries()) { - const fullUrl = url.startsWith("https") - ? url - : `${S3_BUCKET_URL}/tzv973qb6ghnznf/z3ha0dv61q5hrdw/output/${url}`; - const segmentResponse = await fetch(fullUrl); - const segmentData = new Uint8Array(await segmentResponse.arrayBuffer()); - await ffmpeg.writeFile(`audio${index}.ts`, segmentData); - } - - // Concatenate all audio files using FFmpeg - const audioConcatList = audioUrls - .map((_, index) => `file 'audio${index}.ts'`) - .join("\n"); - await ffmpeg.writeFile("audiolist.txt", audioConcatList); - - // Merge video and audio into final MP4 - await ffmpeg.exec([ - "-f", - "concat", - "-safe", - "0", - "-i", - "videolist.txt", - "-c", - "copy", - "temp_video.mp4", - ]); - await ffmpeg.exec([ - "-f", - "concat", - "-safe", - "0", - "-i", - "audiolist.txt", - "-c", - "copy", - "temp_audio.mp4", - ]); - await ffmpeg.exec([ - "-i", - "temp_video.mp4", - "-i", - "temp_audio.mp4", - "-c:v", - "copy", - "-c:a", - "aac", - "-async", - "1", // Adjusts audio to match the number of video frames - "-vsync", - "1", // Ensures frames are handled correctly - "-copyts", // Copy timestamps - videoName + ".mp4", - ]); - } else { - // Only video available, process as single MP4 - await ffmpeg.exec([ - "-f", - "concat", - "-safe", - "0", - "-i", - "videolist.txt", - "-c", - "copy", - videoName + ".mp4", - ]); - } - - // Read the result and create a Blob - const mp4Data = await ffmpeg.readFile(videoName + ".mp4"); - const mp4Blob = new Blob([mp4Data], { type: "video/mp4" }); - - return mp4Blob; + const ffmpeg = new FFmpeg(); + await ffmpeg.load(); + + if (!ffmpeg) { + throw new Error("FFmpeg not loaded"); + } + + const videoFetch = await fetch( + `/api/video/playlistUrl?userId=${userId}&videoId=${videoId}` + ); + + if (videoFetch.status !== 200) { + throw new Error("Could not fetch video"); + } + + const video = await videoFetch.json(); + + if (!video.playlistOne) { + throw new Error("Video does not have a valid video playlist"); + } + + // Fetch the video playlist data + const videoResponse = await fetch(video.playlistOne); + const videoData = await videoResponse.text(); + const videoUrls = videoData + .split("\n") + .filter((line) => line && !line.startsWith("#")); + + // Download video files and write to FFmpeg FS + for (const [index, url] of videoUrls.entries()) { + const fullUrl = url.startsWith("https") + ? url + : `${S3_BUCKET_URL}/${userId}/${videoId}/output/${url}`; + const segmentResponse = await fetch(fullUrl); + const segmentData = new Uint8Array(await segmentResponse.arrayBuffer()); + await ffmpeg.writeFile(`video${index}.ts`, segmentData); + } + + // Concatenate all video files using FFmpeg + const videoConcatList = videoUrls + .map((_, index) => `file 'video${index}.ts'`) + .join("\n"); + await ffmpeg.writeFile("videolist.txt", videoConcatList); + + if (video.playlistTwo) { + // Fetch the audio playlist data if available + const audioResponse = await fetch(video.playlistTwo); + const audioData = await audioResponse.text(); + const audioUrls = audioData + .split("\n") + .filter((line) => line && !line.startsWith("#")); + + // Download audio files and write to FFmpeg FS + for (const [index, url] of audioUrls.entries()) { + const fullUrl = url.startsWith("https") + ? url + : `${S3_BUCKET_URL}/tzv973qb6ghnznf/z3ha0dv61q5hrdw/output/${url}`; + const segmentResponse = await fetch(fullUrl); + const segmentData = new Uint8Array(await segmentResponse.arrayBuffer()); + await ffmpeg.writeFile(`audio${index}.ts`, segmentData); + } + + // Concatenate all audio files using FFmpeg + const audioConcatList = audioUrls + .map((_, index) => `file 'audio${index}.ts'`) + .join("\n"); + await ffmpeg.writeFile("audiolist.txt", audioConcatList); + + // Merge video and audio into final MP4 + await ffmpeg.exec([ + "-f", + "concat", + "-safe", + "0", + "-i", + "videolist.txt", + "-c", + "copy", + "temp_video.mp4", + ]); + await ffmpeg.exec([ + "-f", + "concat", + "-safe", + "0", + "-i", + "audiolist.txt", + "-c", + "copy", + "temp_audio.mp4", + ]); + await ffmpeg.exec([ + "-i", + "temp_video.mp4", + "-i", + "temp_audio.mp4", + "-c:v", + "copy", + "-c:a", + "aac", + "-async", + "1", // Adjusts audio to match the number of video frames + "-vsync", + "1", // Ensures frames are handled correctly + "-copyts", // Copy timestamps + videoName + ".mp4", + ]); + } else { + // Only video available, process as single MP4 + await ffmpeg.exec([ + "-f", + "concat", + "-safe", + "0", + "-i", + "videolist.txt", + "-c", + "copy", + videoName + ".mp4", + ]); + } + + // Read the result and create a Blob + const mp4Data = await ffmpeg.readFile(videoName + ".mp4"); + const mp4Blob = new Blob([mp4Data], { type: "video/mp4" }); + + return mp4Blob; }; export function generateM3U8Playlist( - urls: { - url: string; - duration: string; - resolution?: string; - bandwidth?: string; - }[] + urls: { + url: string; + duration: string; + resolution?: string; + bandwidth?: string; + }[] ) { - const baseM3U8Content = `#EXTM3U + const baseM3U8Content = `#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:5 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-PLAYLIST-TYPE:VOD `; - let m3u8Content = baseM3U8Content; - urls.forEach((segment) => { - const { url, duration } = segment; - m3u8Content += `#EXTINF:${duration},\n${url.replace( - "https://capso.s3.us-east-1.amazonaws.com", - "https://v.cap.so" - )}\n`; - }); + let m3u8Content = baseM3U8Content; + urls.forEach((segment) => { + const { url, duration } = segment; + m3u8Content += `#EXTINF:${duration},\n${url.replace( + "https://capso.s3.us-east-1.amazonaws.com", + "https://v.cap.so" + )}\n`; + }); - m3u8Content += "#EXT-X-ENDLIST"; + m3u8Content += "#EXT-X-ENDLIST"; - return m3u8Content; + return m3u8Content; } export async function generateMasterPlaylist( - resolution: string, - bandwidth: string, - videoPlaylistUrl: string, - audioPlaylistUrl: string | null, - xStreamInfo: string + resolution: string, + bandwidth: string, + videoPlaylistUrl: string, + audioPlaylistUrl: string | null, + xStreamInfo: string ) { - const streamInfo = xStreamInfo - ? xStreamInfo + ',AUDIO="audio"' - : `BANDWIDTH=${bandwidth},RESOLUTION=${resolution},AUDIO="audio"`; - const masterPlaylist = `#EXTM3U + const streamInfo = xStreamInfo + ? xStreamInfo + ',AUDIO="audio"' + : `BANDWIDTH=${bandwidth},RESOLUTION=${resolution},AUDIO="audio"`; + const masterPlaylist = `#EXTM3U #EXT-X-VERSION:4 #EXT-X-INDEPENDENT-SEGMENTS -${audioPlaylistUrl - ? `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="Audio",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",URI="${audioPlaylistUrl}"` - : "" - } +${ + audioPlaylistUrl + ? `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="Audio",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",URI="${audioPlaylistUrl}"` + : "" +} #EXT-X-STREAM-INF:${streamInfo} ${videoPlaylistUrl} `; - return masterPlaylist; + return masterPlaylist; } diff --git a/packages/database/auth/auth-options.ts b/packages/database/auth/auth-options.ts index 27dc22e20..e9dd5daef 100644 --- a/packages/database/auth/auth-options.ts +++ b/packages/database/auth/auth-options.ts @@ -10,14 +10,16 @@ import { LoginLink } from "../emails/login-link"; import { nanoId } from "../helpers"; import WorkOSProvider from "next-auth/providers/workos"; import { NODE_ENV, serverEnv } from "@cap/env"; +import type { Adapter } from "next-auth/adapters"; +import type { Provider } from "next-auth/providers/index"; export const config = { maxDuration: 120, }; export const authOptions = (): NextAuthOptions => { - let _adapter; - let _providers; + let _adapter: Adapter | undefined; + let _providers: Provider[] | undefined; return { get adapter() { diff --git a/packages/database/auth/drizzle-adapter.ts b/packages/database/auth/drizzle-adapter.ts index 39776e49e..f347358aa 100644 --- a/packages/database/auth/drizzle-adapter.ts +++ b/packages/database/auth/drizzle-adapter.ts @@ -9,7 +9,7 @@ import { serverEnv } from "@cap/env"; export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return { async createUser(userData: any) { - await db().insert(users).values({ + await db.insert(users).values({ id: nanoId(), email: userData.email, emailVerified: userData.emailVerified, @@ -17,7 +17,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { image: userData.image, activeSpaceId: "", }); - const rows = await db() + const rows = await db .select() .from(users) .where(eq(users.email, userData.email)) @@ -33,7 +33,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { }, }); - await db() + await db .update(users) .set({ stripeCustomerId: customer.id, @@ -44,7 +44,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row; }, async getUser(id) { - const rows = await db() + const rows = await db .select() .from(users) .where(eq(users.id, id)) @@ -53,7 +53,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row ?? null; }, async getUserByEmail(email) { - const rows = await db() + const rows = await db .select() .from(users) .where(eq(users.email, email)) @@ -66,7 +66,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row ?? null; }, async getUserByAccount({ providerAccountId, provider }) { - const rows = await db() + const rows = await db .select() .from(users) .innerJoin(accounts, eq(users.id, accounts.userId)) @@ -82,8 +82,8 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { }, async updateUser({ id, ...userData }) { if (!id) throw new Error("User not found"); - await db().update(users).set(userData).where(eq(users.id, id)); - const rows = await db() + await db.update(users).set(userData).where(eq(users.id, id)); + const rows = await db .select() .from(users) .where(eq(users.id, id)) @@ -93,28 +93,26 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row; }, async deleteUser(userId) { - await db().delete(users).where(eq(users.id, userId)); + await db.delete(users).where(eq(users.id, userId)); }, async linkAccount(account: any) { - await db() - .insert(accounts) - .values({ - id: nanoId(), - userId: account.userId, - type: account.type, - provider: account.provider, - providerAccountId: account.providerAccountId, - access_token: account.access_token, - expires_in: account.expires_in as number, - id_token: account.id_token, - refresh_token: account.refresh_token, - refresh_token_expires_in: account.refresh_token_expires_in as number, - scope: account.scope, - token_type: account.token_type, - }); + await db.insert(accounts).values({ + id: nanoId(), + userId: account.userId, + type: account.type, + provider: account.provider, + providerAccountId: account.providerAccountId, + access_token: account.access_token, + expires_in: account.expires_in as number, + id_token: account.id_token, + refresh_token: account.refresh_token, + refresh_token_expires_in: account.refresh_token_expires_in as number, + scope: account.scope, + token_type: account.token_type, + }); }, async unlinkAccount({ providerAccountId, provider }: any) { - await db() + await db .delete(accounts) .where( and( @@ -124,13 +122,13 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { ); }, async createSession(data) { - await db().insert(sessions).values({ + await db.insert(sessions).values({ id: nanoId(), expires: data.expires, sessionToken: data.sessionToken, userId: data.userId, }); - const rows = await db() + const rows = await db .select() .from(sessions) .where(eq(sessions.sessionToken, data.sessionToken)) @@ -140,7 +138,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row; }, async getSessionAndUser(sessionToken) { - const rows = await db() + const rows = await db .select({ user: users, session: { @@ -168,11 +166,11 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { }; }, async updateSession(session) { - await db() + await db .update(sessions) .set(session) .where(eq(sessions.sessionToken, session.sessionToken)); - const rows = await db() + const rows = await db .select() .from(sessions) .where(eq(sessions.sessionToken, session.sessionToken)) @@ -182,14 +180,12 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row; }, async deleteSession(sessionToken) { - await db() - .delete(sessions) - .where(eq(sessions.sessionToken, sessionToken)); + await db.delete(sessions).where(eq(sessions.sessionToken, sessionToken)); }, async createVerificationToken(verificationToken) { console.log({ verificationToken }); // First, check if a token for the given identifier already exists - const existingTokens = await db() + const existingTokens = await db .select() .from(verificationTokens) .where(eq(verificationTokens.identifier, verificationToken.identifier)) @@ -199,7 +195,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { // or handle it based on your business logic if (existingTokens.length > 0) { // For example, updating the existing token: - await db() + await db .update(verificationTokens) .set({ token: verificationToken.token, @@ -211,7 +207,7 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { ); // Return the updated token - return await db() + return await db .select() .from(verificationTokens) .where( @@ -222,14 +218,14 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { } // If the token does not exist, proceed to create a new one - await db().insert(verificationTokens).values({ + await db.insert(verificationTokens).values({ expires: verificationToken.expires, identifier: verificationToken.identifier, token: verificationToken.token, }); // Retrieve and return the newly created token - const rows = await db() + const rows = await db .select() .from(verificationTokens) .where(eq(verificationTokens.token, verificationToken.token)) @@ -239,14 +235,14 @@ export function DrizzleAdapter(db: PlanetScaleDatabase): Adapter { return row; }, async useVerificationToken({ identifier, token }) { - const rows = await db() + const rows = await db .select() .from(verificationTokens) .where(eq(verificationTokens.token, token)) .limit(1); const row = rows[0]; if (!row) return null; - await db() + await db .delete(verificationTokens) .where( and( diff --git a/packages/database/auth/session.ts b/packages/database/auth/session.ts index a8c6206d8..2684e8c84 100644 --- a/packages/database/auth/session.ts +++ b/packages/database/auth/session.ts @@ -5,13 +5,13 @@ import { db } from "../"; import { users } from "../schema"; export const getSession = async () => { - const session = await getServerSession(authOptions); + const session = await getServerSession(authOptions()); return session; }; export const getCurrentUser = async (session?: Session) => { - const _session = session ?? (await getServerSession(authOptions)); + const _session = session ?? (await getServerSession(authOptions())); if (!_session) { return null; diff --git a/packages/database/crypto.ts b/packages/database/crypto.ts index 7d16510ab..96ab6cbcd 100644 --- a/packages/database/crypto.ts +++ b/packages/database/crypto.ts @@ -8,6 +8,8 @@ const ITERATIONS = 100000; const ENCRYPTION_KEY = () => { const key = serverEnv().DATABASE_ENCRYPTION_KEY; + if (!key) return; + // Verify the encryption key is valid hex and correct length try { const keyBuffer = Buffer.from(key, "hex"); @@ -29,8 +31,11 @@ const ENCRYPTION_KEY = () => { }; async function deriveKey(salt: Uint8Array): Promise { + const key = ENCRYPTION_KEY(); + if (!key) throw new Error("Encryption key is not available"); + // Convert hex string to ArrayBuffer for Web Crypto API - const keyBuffer = Buffer.from(ENCRYPTION_KEY(), "hex"); + const keyBuffer = Buffer.from(key, "hex"); const keyMaterial = await crypto.subtle.importKey( "raw", @@ -76,9 +81,9 @@ export async function encrypt(text: string): Promise { // Combine salt, IV, and encrypted content const result = Buffer.concat([ - Buffer.from(salt), - Buffer.from(iv), - Buffer.from(encrypted), + Buffer.from(salt as any) as any, + Buffer.from(iv as any) as any, + Buffer.from(encrypted as any) as any, ]); return result.toString("base64"); @@ -104,7 +109,7 @@ export async function decrypt(encryptedText: string): Promise { const content = encrypted.subarray(SALT_LENGTH + IV_LENGTH); // Derive the same key using the extracted salt - const key = await deriveKey(salt); + const key = await deriveKey(salt as Uint8Array); const decrypted = await crypto.subtle.decrypt( { diff --git a/packages/ui-solid/package.json b/packages/ui-solid/package.json index 14496be2f..1162d10b9 100644 --- a/packages/ui-solid/package.json +++ b/packages/ui-solid/package.json @@ -4,7 +4,7 @@ "main": "./src/index.ts", "types": "./src/index.ts", "exports": { - ".": "./src/index.ts", + ".": "./src/index.tsx", "./tailwind": "./tailwind.config.js", "./main.css": "./src/main.css", "./vite": "./vite.js", diff --git a/packages/ui-solid/src/index.ts b/packages/ui-solid/src/index.tsx similarity index 100% rename from packages/ui-solid/src/index.ts rename to packages/ui-solid/src/index.tsx From 4fcb3f6778fc385464ddee03b508ada0d375f028 Mon Sep 17 00:00:00 2001 From: OmarMcAdam Date: Mon, 28 Apr 2025 18:04:56 -0700 Subject: [PATCH 14/18] docker-build: update envs --- Dockerfile | 7 +++++-- apps/web/app/api/selfhosted/migrations/route.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2688ab54b..0a17fc678 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,6 @@ COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ COPY /patches ./patches - # Install dependencies based on lockfile RUN if [ -f yarn.lock ]; then \ yarn --frozen-lockfile; \ @@ -31,7 +30,6 @@ RUN if [ -f yarn.lock ]; then \ else \ echo "Lockfile not found." && exit 1; \ fi - # Use mount cache for pnpm if pnpm-lock exists RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ if [ -f pnpm-lock.yaml ]; then \ @@ -45,8 +43,12 @@ WORKDIR /app COPY --from=deps /app/patches ./patches COPY . . + # build-time only variables ARG DOCKER_BUILD=true +ENV NEXT_PUBLIC_WEB_URL=http://localhost:3000 +ENV NEXT_PUBLIC_CAP_AWS_BUCKET=capso +ENV NEXT_PUBLIC_CAP_AWS_REGION=us-east-1 RUN corepack enable pnpm && pnpm i && pnpm run build:web @@ -55,6 +57,7 @@ RUN corepack enable pnpm RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm i RUN pnpm run build:web + # 3. Production image, copy all the files and run next FROM base AS runner WORKDIR /app diff --git a/apps/web/app/api/selfhosted/migrations/route.ts b/apps/web/app/api/selfhosted/migrations/route.ts index 91b8b07c2..064b12e4c 100644 --- a/apps/web/app/api/selfhosted/migrations/route.ts +++ b/apps/web/app/api/selfhosted/migrations/route.ts @@ -23,8 +23,8 @@ export async function POST(request: Request) { console.log("🔍 DB migrations triggered"); console.log("💿 Running DB migrations..."); const cwd = process.cwd(); - // @ts-ignore - await migrate(db, { + + await migrate(db(), { migrationsFolder: path.join(process.cwd(), "/migrations"), }); migrations.run = true; From f6b6282f27084f8b5b212bcdb720acb91395ad2b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 29 Apr 2025 18:42:42 -0700 Subject: [PATCH 15/18] all done :) --- .dockerignore | 2 + Dockerfile | 44 +------------ .../app/api/selfhosted/migrations/route.ts | 62 +++++++++---------- .../app/api/settings/billing/usage/route.ts | 4 +- .../app/api/video/transcribe/status/route.ts | 2 + .../_components/AdminNavbar/AdminNavItems.tsx | 7 ++- apps/web/app/dashboard/layout.tsx | 2 + apps/web/app/layout.tsx | 16 ++--- apps/web/app/login/page.tsx | 2 + apps/web/app/onboarding/page.tsx | 2 + apps/web/app/pricing/page.tsx | 7 ++- .../s/[videoId]/_components/ShareVideo.tsx | 2 +- .../[videoId]/_components/tabs/Transcript.tsx | 1 - apps/web/app/tools/layout.tsx | 8 ++- apps/web/components/BentoScript.tsx | 3 +- apps/web/instrumentation.ts | 47 +++++++++++++- apps/web/next.config.mjs | 4 -- apps/web/utils/s3.ts | 2 +- docker-compose.template.yml | 21 +------ package.json | 2 +- packages/database/emails/config.ts | 4 +- packages/env/build.ts | 2 +- packages/env/server.ts | 11 ++++ packages/ui/package.json | 3 +- packages/utils/package.json | 3 +- scripts/env-cli.js | 4 ++ 26 files changed, 142 insertions(+), 125 deletions(-) diff --git a/.dockerignore b/.dockerignore index 2f7896d1d..f3006cb25 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,3 @@ target/ +node_modules/ +crates/ diff --git a/Dockerfile b/Dockerfile index 0a17fc678..c04b6071c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,60 +1,20 @@ # syntax=docker.io/docker/dockerfile:1 FROM node:20-alpine AS base -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable - -# 1. Install dependencies only when needed -FROM base AS deps -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat - - -WORKDIR /app - -# Install dependencies based on the preferred package manager -COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ - -COPY /patches ./patches - - -# Install dependencies based on lockfile -RUN if [ -f yarn.lock ]; then \ - yarn --frozen-lockfile; \ - elif [ -f package-lock.json ]; then \ - npm ci; \ - elif [ -f pnpm-lock.yaml ]; then \ - corepack enable pnpm; \ - else \ - echo "Lockfile not found." && exit 1; \ - fi -# Use mount cache for pnpm if pnpm-lock exists -RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ - if [ -f pnpm-lock.yaml ]; then \ - pnpm i --frozen-lockfile; \ - fi - - -# 2. Rebuild the source code only when needed FROM base AS builder WORKDIR /app -COPY --from=deps /app/patches ./patches COPY . . +RUN corepack enable pnpm +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm i --frozen-lockfile -# build-time only variables ARG DOCKER_BUILD=true ENV NEXT_PUBLIC_WEB_URL=http://localhost:3000 ENV NEXT_PUBLIC_CAP_AWS_BUCKET=capso ENV NEXT_PUBLIC_CAP_AWS_REGION=us-east-1 -RUN corepack enable pnpm && pnpm i && pnpm run build:web - -# We re-install packages instead of copy from deps due to an issue with pnpm and the way it installs app packages under certain conditions -RUN corepack enable pnpm -RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm i RUN pnpm run build:web diff --git a/apps/web/app/api/selfhosted/migrations/route.ts b/apps/web/app/api/selfhosted/migrations/route.ts index 064b12e4c..db28d63f0 100644 --- a/apps/web/app/api/selfhosted/migrations/route.ts +++ b/apps/web/app/api/selfhosted/migrations/route.ts @@ -8,42 +8,40 @@ const migrations = { run: false, }; -export async function POST(request: Request) { - if (process.env.NEXT_RUNTIME === "nodejs") { - if (migrations.run) { - console.log(" ✅ DB migrations triggered but already run, skipping"); - return NextResponse.json({ - message: "✅ DB migrations already run, skipping", - }); - } +export async function POST() { + if (migrations.run) { + console.log(" ✅ DB migrations triggered but already run, skipping"); + return NextResponse.json({ + message: "✅ DB migrations already run, skipping", + }); + } - const isDockerBuild = serverEnv().DOCKER_BUILD === "true"; - if (isDockerBuild) { - try { - console.log("🔍 DB migrations triggered"); - console.log("💿 Running DB migrations..."); - const cwd = process.cwd(); + const isDockerBuild = serverEnv().DOCKER_BUILD === "true"; + if (isDockerBuild) { + try { + console.log("🔍 DB migrations triggered"); + console.log("💿 Running DB migrations..."); - await migrate(db(), { - migrationsFolder: path.join(process.cwd(), "/migrations"), - }); - migrations.run = true; - console.log("💿 Migrations run successfully!"); - return NextResponse.json({ - message: "✅ DB migrations run successfully!", - }); - } catch (error) { - console.error("🚨 MIGRATION_FAILED", { error }); - return NextResponse.json( - { - message: "🚨 DB migrations failed", - error: error instanceof Error ? error.message : String(error), - }, - { status: 500 } - ); - } + await migrate(db() as any, { + migrationsFolder: path.join(process.cwd(), "/migrations"), + }); + migrations.run = true; + console.log("💿 Migrations run successfully!"); + return NextResponse.json({ + message: "✅ DB migrations run successfully!", + }); + } catch (error) { + console.error("🚨 MIGRATION_FAILED", { error }); + return NextResponse.json( + { + message: "🚨 DB migrations failed", + error: error instanceof Error ? error.message : String(error), + }, + { status: 500 } + ); } } + migrations.run = true; return NextResponse.json({ diff --git a/apps/web/app/api/settings/billing/usage/route.ts b/apps/web/app/api/settings/billing/usage/route.ts index 7fcc81883..0452694e2 100644 --- a/apps/web/app/api/settings/billing/usage/route.ts +++ b/apps/web/app/api/settings/billing/usage/route.ts @@ -5,7 +5,9 @@ import { count, eq } from "drizzle-orm"; import { db } from "@cap/database"; import { videos } from "@cap/database/schema"; -export async function GET(request: NextRequest) { +export const dynamic = "force-dynamic"; + +export async function GET() { const user = await getCurrentUser(); if (!user) { diff --git a/apps/web/app/api/video/transcribe/status/route.ts b/apps/web/app/api/video/transcribe/status/route.ts index e7f9ee7d5..bdcb93c99 100644 --- a/apps/web/app/api/video/transcribe/status/route.ts +++ b/apps/web/app/api/video/transcribe/status/route.ts @@ -5,6 +5,8 @@ import { count, eq } from "drizzle-orm"; import { db } from "@cap/database"; import { videos } from "@cap/database/schema"; +export const dynamic = "force-dynamic"; + export async function GET(request: NextRequest) { const user = await getCurrentUser(); const url = new URL(request.url); diff --git a/apps/web/app/dashboard/_components/AdminNavbar/AdminNavItems.tsx b/apps/web/app/dashboard/_components/AdminNavbar/AdminNavItems.tsx index 2bfe8abda..16438c810 100644 --- a/apps/web/app/dashboard/_components/AdminNavbar/AdminNavItems.tsx +++ b/apps/web/app/dashboard/_components/AdminNavbar/AdminNavItems.tsx @@ -39,6 +39,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import clsx from "clsx"; import { motion } from "framer-motion"; import { updateActiveSpace } from "./server"; +import { buildEnv } from "@cap/env"; export const AdminNavItems = ({ collapsed }: { collapsed?: boolean }) => { const pathname = usePathname(); @@ -70,12 +71,12 @@ export const AdminNavItems = ({ collapsed }: { collapsed?: boolean }) => { icon: faBuilding, subNav: [], }, - ...(user.email.endsWith("@cap.so") + ...(buildEnv.NEXT_PUBLIC_IS_CAP && user.email.endsWith("@cap.so") ? [ { - name: "Admin", + name: "Admin Dev", href: "/dashboard/admin", - icon: faBuilding, // Using Building icon as a fallback + icon: faBuilding, subNav: [], }, ] diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx index 205e97111..aeae1a2ed 100644 --- a/apps/web/app/dashboard/layout.tsx +++ b/apps/web/app/dashboard/layout.tsx @@ -21,6 +21,8 @@ export type Space = { totalInvites: number; }; +export const dynamic = "force-dynamic"; + export default async function DashboardLayout({ children, }: { diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 34b4165b5..c19fc6192 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -52,15 +52,15 @@ export default async function RootLayout({ }: { children: React.ReactNode; }) { - const user = await getCurrentUser(); + const user = undefined; // await getCurrentUser(); let intercomHash = ""; - const intercomSecret = serverEnv().INTERCOM_SECRET; - if (intercomSecret) { - intercomHash = crypto - .createHmac("sha256", intercomSecret) - .update(user?.id ?? "") - .digest("hex"); - } + // const intercomSecret = serverEnv().INTERCOM_SECRET; + // if (intercomSecret) { + // intercomHash = crypto + // .createHmac("sha256", intercomSecret) + // .update(user?.id ?? "") + // .digest("hex"); + // } return ( diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index b77e9f99d..8065d9022 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -2,6 +2,8 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { redirect } from "next/navigation"; import { LoginForm } from "./form"; +export const dynamic = "force-dynamic"; + export default async function LoginPage() { const session = await getCurrentUser(); if (session) { diff --git a/apps/web/app/onboarding/page.tsx b/apps/web/app/onboarding/page.tsx index bd8c1d641..d714fe13b 100644 --- a/apps/web/app/onboarding/page.tsx +++ b/apps/web/app/onboarding/page.tsx @@ -3,6 +3,8 @@ import { LogoBadge } from "@cap/ui"; import { redirect } from "next/navigation"; import { Onboarding } from "./Onboarding"; +export const dynamic = "force-dynamic"; + export default async function OnboardingPage() { const user = await getCurrentUser(); diff --git a/apps/web/app/pricing/page.tsx b/apps/web/app/pricing/page.tsx index 8654ee4f6..23fb6756e 100644 --- a/apps/web/app/pricing/page.tsx +++ b/apps/web/app/pricing/page.tsx @@ -1,10 +1,15 @@ import { PricingPage } from "@/components/pages/PricingPage"; import { Metadata } from "next"; +import { Suspense } from "react"; export const metadata: Metadata = { title: "Early Adopter Pricing — Cap", }; export default function App() { - return ; + return ( + + + + ); } diff --git a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx index 8420add0a..472371781 100644 --- a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx @@ -3,7 +3,7 @@ import { userSelectProps } from "@cap/database/auth/session"; import { comments as commentsSchema, videos } from "@cap/database/schema"; import { NODE_ENV } from "@cap/env"; import { Logo, LogoSpinner } from "@cap/ui"; -import { isUserOnProPlan, S3_BUCKET_URL } from "@cap/utils"; +import { isUserOnProPlan } from "@cap/utils"; import clsx from "clsx"; import { Maximize, diff --git a/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx b/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx index 15483d98d..0748159d8 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx @@ -3,7 +3,6 @@ import { videos } from "@cap/database/schema"; import { useState, useEffect } from "react"; import { MessageSquare } from "lucide-react"; -import { S3_BUCKET_URL } from "@cap/utils"; import { usePublicEnv } from "@/utils/public-env"; interface TranscriptProps { diff --git a/apps/web/app/tools/layout.tsx b/apps/web/app/tools/layout.tsx index 90b8c3d67..bb8f26805 100644 --- a/apps/web/app/tools/layout.tsx +++ b/apps/web/app/tools/layout.tsx @@ -1,7 +1,13 @@ +import { Suspense } from "react"; + export default function ToolsLayout({ children, }: { children: React.ReactNode; }) { - return
{children}
; + return ( + +
{children}
+
+ ); } diff --git a/apps/web/components/BentoScript.tsx b/apps/web/components/BentoScript.tsx index a044c641e..a820cbb94 100644 --- a/apps/web/components/BentoScript.tsx +++ b/apps/web/components/BentoScript.tsx @@ -17,7 +17,6 @@ export function BentoScript({ user?: typeof users.$inferSelect | null; }) { const pathname = usePathname(); - const searchParams = useSearchParams(); useEffect(() => { if (window.bento !== undefined) { @@ -26,7 +25,7 @@ export function BentoScript({ } window.bento.view(); } - }, [pathname, searchParams]); + }, [pathname]); return (