From 3cfea40fbd3d80c0f289edeaa25f2de0fefa5c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20K=C3=B6ksal?= <182091242+koksalenes@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:05:43 +0300 Subject: [PATCH] refactor: eslint and prettier conflict will be resolved and formatted --- .prettierignore | 4 + .prettierrc | 2 +- AGENTS.md | 5 + app/(auth)/layout.tsx | 6 +- app/(auth)/login/login-form.tsx | 13 +- app/(auth)/login/page.tsx | 19 +- app/(auth)/register/page.tsx | 8 +- app/(auth)/register/register-page-client.tsx | 47 +- app/(auth)/verify-email/page.tsx | 8 +- app/(dashboard)/dashboard/loading.tsx | 8 +- app/(dashboard)/dashboard/page.tsx | 268 ++-- app/(dashboard)/dashboard/project-filter.tsx | 22 +- app/(dashboard)/error.tsx | 18 +- .../feedback/feedback-page-client.tsx | 58 +- app/(dashboard)/layout.tsx | 6 +- app/(dashboard)/not-found.tsx | 6 +- .../projects/[projectId]/loading.tsx | 8 +- .../projects/[projectId]/not-found.tsx | 9 +- app/(dashboard)/projects/[projectId]/page.tsx | 18 +- .../[projectId]/project-content-client.tsx | 22 +- .../projects/[projectId]/settings/loading.tsx | 6 +- .../settings/project-settings-page-client.tsx | 971 ++++++------ .../share/project-share-page-client.tsx | 619 ++++---- .../compare/compare-versions-page-client.tsx | 314 ++-- .../videos/[videoId]/compare/loading.tsx | 8 +- .../[projectId]/videos/[videoId]/error.tsx | 19 +- .../[projectId]/videos/[videoId]/layout.tsx | 6 +- .../[projectId]/videos/[videoId]/loading.tsx | 8 +- .../videos/[videoId]/not-found.tsx | 6 +- .../share/video-share-page-client.tsx | 69 +- .../videos/new/new-video-page-client.tsx | 264 ++-- .../projects/new/new-project-page-client.tsx | 50 +- app/(dashboard)/settings/loading.tsx | 8 +- .../settings/settings-page-client.tsx | 719 +++++---- .../workspaces/[workspaceId]/page.tsx | 17 +- .../new/new-workspace-project-page-client.tsx | 10 +- .../workspace-settings-page-client.tsx | 14 +- app/(dashboard)/workspaces/loading.tsx | 8 +- .../new/new-workspace-page-client.tsx | 11 +- app/(dashboard)/workspaces/page.tsx | 11 +- .../workspaces/workspaces-client.tsx | 8 +- app/admin/feedback/[feedbackId]/page.tsx | 68 +- app/admin/feedback/page.tsx | 148 +- app/admin/layout.tsx | 131 +- app/admin/page.tsx | 484 +++--- app/admin/users/page.tsx | 794 +++++----- app/api/admin/feedback/[feedbackId]/route.ts | 120 +- app/api/approvals/[requestId]/cancel/route.ts | 63 +- .../approvals/[requestId]/decision/route.ts | 168 +- app/api/auth/[...nextauth]/route.ts | 10 +- app/api/auth/register/route.ts | 284 ++-- app/api/auth/verify-email/resend/route.ts | 101 +- app/api/auth/verify-email/route.ts | 38 +- app/api/billing/route.ts | 3 +- app/api/comments/[commentId]/route.ts | 647 ++++---- app/api/feedback/route.ts | 59 +- app/api/feedback/upload/route.ts | 2 +- app/api/onboarding/complete/route.ts | 34 +- .../[projectId]/members/[memberId]/route.ts | 200 +-- app/api/projects/[projectId]/members/route.ts | 318 ++-- app/api/projects/[projectId]/route.ts | 418 ++--- .../[projectId]/tags/[tagId]/route.ts | 200 +-- app/api/projects/[projectId]/tags/route.ts | 224 +-- .../[projectId]/videos/[videoId]/route.ts | 500 +++--- .../videos/[videoId]/share/route.ts | 96 +- .../[videoId]/versions/[versionId]/route.ts | 264 ++-- .../videos/[videoId]/versions/route.ts | 330 ++-- .../[projectId]/videos/bunny-init/route.ts | 269 ++-- app/api/projects/[projectId]/videos/route.ts | 320 ++-- app/api/projects/route.ts | 360 ++--- app/api/search/route.ts | 13 +- app/api/settings/notifications/route.ts | 383 ++--- app/api/stripe/webhook/route.ts | 9 +- app/api/upload/audio/[filename]/route.ts | 12 +- app/api/upload/audio/route.ts | 47 +- app/api/upload/image/[filename]/route.ts | 170 +- app/api/upload/image/route.ts | 322 ++-- .../versions/[versionId]/approvals/route.ts | 95 +- .../versions/[versionId]/comments/route.ts | 977 ++++++------ .../versions/[versionId]/download/route.ts | 59 +- .../assets/[assetId]/download/route.ts | 8 +- .../[videoId]/assets/[assetId]/route.ts | 19 +- .../[videoId]/assets/bunny-init/route.ts | 38 +- app/api/videos/[videoId]/assets/route.ts | 89 +- app/api/watch/[videoId]/progress/route.ts | 301 ++-- app/api/watch/[videoId]/route.ts | 391 ++--- app/api/watch/[videoId]/upload-token/route.ts | 20 +- .../[workspaceId]/members/[memberId]/route.ts | 258 +-- .../workspaces/[workspaceId]/members/route.ts | 406 ++--- app/api/workspaces/[workspaceId]/route.ts | 416 ++--- app/api/workspaces/route.ts | 254 ++- app/error.tsx | 18 +- app/globals.css | 22 +- app/layout.tsx | 66 +- app/onboarding/layout.tsx | 10 +- app/onboarding/onboarding-wizard.tsx | 1381 +++++++++-------- app/onboarding/page.tsx | 82 +- app/privacy/page.tsx | 243 ++- app/refund/page.tsx | 134 +- app/signout/page.tsx | 4 +- app/terms/page.tsx | 205 ++- app/watch/[videoId]/error.tsx | 18 +- app/watch/[videoId]/loading.tsx | 8 +- app/watch/[videoId]/not-found.tsx | 9 +- app/watch/[videoId]/session/route.ts | 5 +- bun.lock | 250 ++- commitlint.config.js | 148 +- components/LandingPage.tsx | 401 +++-- components/admin/delete-feedback-button.tsx | 6 +- components/admin/refresh-r2-stats-button.tsx | 8 +- components/annotation-canvas.tsx | 368 +++-- components/error-boundary.tsx | 26 +- components/example.tsx | 24 +- components/guest-gate.tsx | 6 +- components/keyboard-shortcuts-modal.tsx | 11 +- components/layout/header.tsx | 95 +- components/linkify.tsx | 50 +- components/members-management-page.tsx | 39 +- components/search-modal.tsx | 185 ++- components/share-link-bootstrap.tsx | 5 +- components/share-link-unlock.tsx | 10 +- components/ui/alert-dialog.tsx | 122 +- components/ui/avatar.tsx | 71 +- components/ui/badge.tsx | 38 +- components/ui/button.tsx | 57 +- components/ui/card.tsx | 89 +- components/ui/dialog.tsx | 87 +- components/ui/dropdown-menu.tsx | 114 +- components/ui/field.tsx | 171 +- components/ui/input-group.tsx | 115 +- components/ui/input.tsx | 12 +- components/ui/label.tsx | 19 +- components/ui/progress.tsx | 5 +- components/ui/radio-group.tsx | 20 +- components/ui/select.tsx | 93 +- components/ui/separator.tsx | 16 +- components/ui/sheet.tsx | 82 +- components/ui/skeleton.tsx | 10 +- components/ui/table.tsx | 78 +- components/ui/tabs.tsx | 60 +- components/ui/textarea.tsx | 12 +- components/ui/tooltip.tsx | 28 +- components/video-card.tsx | 62 +- components/video-drag-drop-uploader.tsx | 311 ++-- components/video-page-content.tsx | 137 +- .../video-page/approval-request-dialog.tsx | 43 +- .../video-page/approval-requests-panel.tsx | 88 +- components/video-page/asset-list-section.tsx | 60 +- components/video-page/assets-pane.tsx | 697 ++++++--- .../video-page/bunny-preview-player.tsx | 1133 +++++++------- components/video-page/comment-composer.tsx | 89 +- components/video-page/comment-rich-text.tsx | 16 +- components/video-page/comments-pane.tsx | 1336 +++++++++------- .../video-page/compare-versions-dialog.tsx | 20 +- components/video-page/download-controls.tsx | 16 +- components/video-page/guest-name-gate.tsx | 6 +- components/video-page/hooks/use-approvals.ts | 149 +- .../video-page/hooks/use-comment-actions.ts | 1113 +++++++------ .../video-page/hooks/use-comment-media.ts | 86 +- .../video-page/hooks/use-download-actions.ts | 131 +- .../video-page/hooks/use-version-actions.ts | 16 +- .../video-page/hooks/use-video-assets.ts | 305 ++-- .../video-page/hooks/use-video-page-data.ts | 20 +- .../video-page/hooks/use-video-player.ts | 333 ++-- .../video-page/hooks/use-watch-progress.ts | 145 +- .../video-page/image-preview-dialog.tsx | 14 +- components/video-page/player-core.tsx | 61 +- components/video-page/types.ts | 6 +- .../video-page/version-actions-dialog.tsx | 72 +- components/video-page/video-page-header.tsx | 62 +- components/video-page/video-page-loading.tsx | 34 +- docker-compose.yml | 34 +- eslint.config.mjs | 16 +- lib/admin-stats.ts | 737 ++++----- lib/api-response.ts | 45 +- lib/approval-workflow.ts | 9 +- lib/auth.ts | 72 +- lib/billing.ts | 138 +- lib/bunny-cdn.ts | 4 +- lib/bunny-download.ts | 17 +- lib/bunny-stream-cleanup.ts | 26 +- lib/bunny-upload-token.ts | 26 +- lib/comment-export.ts | 8 +- lib/db.ts | 6 +- lib/email-brand.ts | 10 +- lib/email-verification.ts | 149 +- lib/guest-identity.ts | 16 +- lib/guest-upload-token.ts | 52 +- lib/image-upload-validation.ts | 57 +- lib/invitations.ts | 164 +- lib/notifications.ts | 663 ++++---- lib/r2-cleanup.ts | 254 +-- lib/r2-media-proxy.ts | 16 +- lib/r2.ts | 3 +- lib/rate-limit.ts | 305 ++-- lib/route-access.ts | 15 +- lib/share-links.ts | 59 +- lib/share-session.ts | 11 +- lib/storage-quota.ts | 11 +- lib/utils.ts | 6 +- lib/validation.ts | 88 +- lib/video-assets.ts | 48 +- lib/video-providers/bunny.ts | 95 +- lib/video-providers/direct.ts | 12 +- lib/video-providers/index.ts | 20 +- lib/video-providers/metadata-cache.ts | 6 +- lib/video-providers/youtube.ts | 18 +- next.config.ts | 4 +- package.json | 3 +- postcss.config.mjs | 2 +- prisma.config.ts | 10 +- prisma/seed.ts | 563 +++---- scripts/bunny-orphan-cleanup.ts | 42 +- scripts/docker-db-bootstrap.ts | 8 +- scripts/expired-billing-cleanup.ts | 38 +- scripts/r2-orphan-cleanup.ts | 42 +- scripts/self-host-bootstrap.ts | 4 +- types/next-auth.d.ts | 24 +- types/youtube.d.ts | 7 +- 219 files changed, 16638 insertions(+), 13663 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..67f6b0d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +.next/ +node_modules/ +prisma/migrations/ +bun.lock diff --git a/.prettierrc b/.prettierrc index 075c59e..95c5299 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,6 @@ { "semi": true, - "singleQuote": false, + "singleQuote": true, "tabWidth": 2, "useTabs": false, "trailingComma": "es5", diff --git a/AGENTS.md b/AGENTS.md index f53fc1c..f10d06c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,15 +1,18 @@ # AGENTS.md ## Must-follow constraints + - Use `bun` only. Do not use `npm` or `pnpm`. - Do not start the dev server (`bun run dev`); assume it is already running. - If you change `prisma/schema.prisma`, run `bun run db:generate`. - In App Router dynamic routes, keep `params` typed as `Promise<...>` and `await params` in handlers/pages. ## Validation before finishing + - Run `bun run check`. ## Repo-specific conventions + - Use `auth()` from `@/lib/auth` for server-side session reads. - Use `checkProjectAccess()` / `checkWorkspaceAccess()` for authorization instead of ad-hoc role checks. - For API responses, use `successResponse` / `apiErrors` from `@/lib/api-response`. @@ -17,10 +20,12 @@ - In Prisma raw SQL, use `$executeRaw` for statements that return no rows (e.g. `pg_advisory_xact_lock`). Using `$queryRaw` on void-returning functions causes a Prisma deserialization error (`Failed to deserialize column of type 'void'`). ## Important locations + - Custom SQL managed by Prisma migrations: `prisma/migrations/*/migration.sql`. - Shared API response helpers: `lib/api-response.ts`. - Auth + access-control helpers: `lib/auth.ts`. ## Change safety rules + - Prefer backward-compatible API changes unless explicitly asked to break contracts. - For multi-step DB writes, use Prisma transactions. diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx index 4d74432..4bbe51a 100644 --- a/app/(auth)/layout.tsx +++ b/app/(auth)/layout.tsx @@ -1,11 +1,7 @@ import { redirect } from 'next/navigation'; import { auth } from '@/lib/auth'; -export default async function AuthLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default async function AuthLayout({ children }: { children: React.ReactNode }) { const session = await auth(); // If already logged in, redirect to dashboard diff --git a/app/(auth)/login/login-form.tsx b/app/(auth)/login/login-form.tsx index b3fbddf..2237429 100644 --- a/app/(auth)/login/login-form.tsx +++ b/app/(auth)/login/login-form.tsx @@ -27,7 +27,8 @@ const ERROR_MESSAGES: Record = { // Generic message — avoid confirming whether a credentials account exists for this email OAuthAccountNotLinked: 'Sign-in failed. Please try a different method or contact support.', OAuthCallbackError: 'OAuth sign-in failed. Please try again.', - OAuthEmailNotVerified: 'Your OAuth account email is not verified. Please verify it with your provider and try again.', + OAuthEmailNotVerified: + 'Your OAuth account email is not verified. Please verify it with your provider and try again.', InvalidVerificationToken: 'The verification link is invalid or has expired.', VerificationFailed: 'Email verification failed. Please try again.', Default: 'Something went wrong. Please try again.', @@ -109,7 +110,8 @@ function LoginFormInner({ googleEnabled, githubEnabled }: LoginFormInnerProps) { {showSuccess && (
- Account created successfully! Please check your email to verify your address before signing in. + Account created successfully! Please check your email to verify your address before + signing in.
)} @@ -172,7 +174,12 @@ function LoginFormInner({ googleEnabled, githubEnabled }: LoginFormInnerProps) { {oauthLoading === 'github' ? ( ) : ( -
@@ -26,12 +22,15 @@ export default function LoginPage() {

By continuing, you agree to our{' '} - Terms of Service - {' '}and{' '} - Privacy Policy + + Terms of Service + {' '} + and{' '} + + Privacy Policy +

); } - diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index 0351415..6e310c0 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -2,12 +2,8 @@ import { isInviteCodeRequired } from '@/lib/feature-flags'; import RegisterPageClient from './register-page-client'; export default function RegisterPage() { - const googleEnabled = Boolean( - process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET, - ); - const githubEnabled = Boolean( - process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET, - ); + const googleEnabled = Boolean(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET); + const githubEnabled = Boolean(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET); return ( searchParams.get('invitationToken') || '', [searchParams]); @@ -122,9 +126,7 @@ export default function RegisterPageClient({ requireInviteCode, googleEnabled, g Create Account - - Join OpenFrame to collaborate on video projects - + Join OpenFrame to collaborate on video projects {/* OAuth Buttons */} @@ -142,10 +144,22 @@ export default function RegisterPageClient({ requireInviteCode, googleEnabled, g ) : ( )} Continue with Google @@ -162,7 +176,12 @@ export default function RegisterPageClient({ requireInviteCode, googleEnabled, g {oauthLoading === 'github' ? ( ) : ( -

By continuing, you agree to our{' '} - Terms of Service - {' '}and{' '} - Privacy Policy + + Terms of Service + {' '} + and{' '} + + Privacy Policy +

diff --git a/app/(auth)/verify-email/page.tsx b/app/(auth)/verify-email/page.tsx index b9b8640..0cd6cde 100644 --- a/app/(auth)/verify-email/page.tsx +++ b/app/(auth)/verify-email/page.tsx @@ -55,8 +55,8 @@ function VerifyEmailContent() { We sent a verification link to{' '} - {emailParam ? {emailParam} : 'your email address'}. - Click the link to activate your account. + {emailParam ? {emailParam} : 'your email address'}. Click the link to + activate your account. @@ -75,7 +75,9 @@ function VerifyEmailContent() {
- Didn't receive it? + + Didn't receive it? +
diff --git a/app/(dashboard)/dashboard/loading.tsx b/app/(dashboard)/dashboard/loading.tsx index c395cae..fff3ecf 100644 --- a/app/(dashboard)/dashboard/loading.tsx +++ b/app/(dashboard)/dashboard/loading.tsx @@ -1,5 +1,5 @@ -import { Skeleton } from "@/components/ui/skeleton" -import { Card, CardHeader, CardContent } from "@/components/ui/card" +import { Skeleton } from '@/components/ui/skeleton'; +import { Card, CardHeader, CardContent } from '@/components/ui/card'; function ProjectCardSkeleton() { return ( @@ -24,7 +24,7 @@ function ProjectCardSkeleton() {
- ) + ); } export default function DashboardLoading() { @@ -44,5 +44,5 @@ export default function DashboardLoading() { ))} - ) + ); } diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx index bc2768e..60b5a17 100644 --- a/app/(dashboard)/dashboard/page.tsx +++ b/app/(dashboard)/dashboard/page.tsx @@ -2,162 +2,162 @@ import { auth } from '@/lib/auth'; import { redirect } from 'next/navigation'; import { db } from '@/lib/db'; import { Prisma } from '@prisma/client'; -import { hasCollaboratorBillingBackedAccess, requireBillingAccessOrRedirect } from '@/lib/route-access'; +import { + hasCollaboratorBillingBackedAccess, + requireBillingAccessOrRedirect, +} from '@/lib/route-access'; import { DashboardClient } from './dashboard-client'; import { buildBillingAccessWhereInput } from '@/lib/billing'; import { isBunnyUploadsEnabled } from '@/lib/feature-flags'; export default async function DashboardPage({ - searchParams, + searchParams, }: { - searchParams: Promise<{ ws?: string; sort?: string; page?: string }> + searchParams: Promise<{ ws?: string; sort?: string; page?: string }>; }) { - const session = await auth(); - if (!session?.user?.id) { - redirect('/login'); - } + const session = await auth(); + if (!session?.user?.id) { + redirect('/login'); + } - const hasCollaboratorAccess = await hasCollaboratorBillingBackedAccess(session.user.id); - if (!hasCollaboratorAccess) { - await requireBillingAccessOrRedirect({ userId: session.user.id }); - } + const hasCollaboratorAccess = await hasCollaboratorBillingBackedAccess(session.user.id); + if (!hasCollaboratorAccess) { + await requireBillingAccessOrRedirect({ userId: session.user.id }); + } - const userOnboarding = await db.user.findUnique({ - where: { id: session.user.id }, - select: { onboardingCompletedAt: true }, - }); - if (!userOnboarding?.onboardingCompletedAt) { - redirect('/onboarding'); - } + const userOnboarding = await db.user.findUnique({ + where: { id: session.user.id }, + select: { onboardingCompletedAt: true }, + }); + if (!userOnboarding?.onboardingCompletedAt) { + redirect('/onboarding'); + } - const resolvedSearchParams = await searchParams; - const { ws, sort, page: pageParam } = resolvedSearchParams || {}; + const resolvedSearchParams = await searchParams; + const { ws, sort, page: pageParam } = resolvedSearchParams || {}; - const page = Number(pageParam) || 1; - const pageSize = 20; - const skip = (page - 1) * pageSize; - const orderByDirection = sort === 'asc' ? 'asc' : 'desc'; + const page = Number(pageParam) || 1; + const pageSize = 20; + const skip = (page - 1) * pageSize; + const orderByDirection = sort === 'asc' ? 'asc' : 'desc'; - // Base permission where clause - const baseWhere: Prisma.ProjectWhereInput = { - OR: [ - { ownerId: session.user.id }, - { members: { some: { userId: session.user.id } } }, - { - workspace: { - owner: buildBillingAccessWhereInput(), - OR: [ - { ownerId: session.user.id }, - { members: { some: { userId: session.user.id } } }, - ], - }, - }, - ], + // Base permission where clause + const baseWhere: Prisma.ProjectWhereInput = { + OR: [ + { ownerId: session.user.id }, + { members: { some: { userId: session.user.id } } }, + { workspace: { - owner: buildBillingAccessWhereInput(), + owner: buildBillingAccessWhereInput(), + OR: [{ ownerId: session.user.id }, { members: { some: { userId: session.user.id } } }], }, - }; + }, + ], + workspace: { + owner: buildBillingAccessWhereInput(), + }, + }; - // Build unique workspace list for filter (Needs an unbounded list of accessible workspaces) - const accessibleProjects = await db.project.findMany({ - where: baseWhere, - select: { + // Build unique workspace list for filter (Needs an unbounded list of accessible workspaces) + const accessibleProjects = await db.project.findMany({ + where: baseWhere, + select: { + workspace: { + select: { id: true, name: true }, + }, + }, + distinct: ['workspaceId'], + }); + + const [creatableWorkspaces, editableProject] = await Promise.all([ + db.workspace.count({ + where: { + OR: [ + { ownerId: session.user.id }, + { members: { some: { userId: session.user.id, role: 'ADMIN' } } }, + ], + }, + }), + db.project.findFirst({ + where: { + OR: [ + { ownerId: session.user.id }, + { members: { some: { userId: session.user.id, role: 'ADMIN' } } }, + { workspace: { - select: { id: true, name: true } - } - }, - distinct: ['workspaceId'] - }); - - const [creatableWorkspaces, editableProject] = await Promise.all([ - db.workspace.count({ - where: { - OR: [ - { ownerId: session.user.id }, - { members: { some: { userId: session.user.id, role: 'ADMIN' } } }, - ], + OR: [ + { ownerId: session.user.id }, + { members: { some: { userId: session.user.id, role: 'ADMIN' } } }, + ], }, - }), - db.project.findFirst({ - where: { - OR: [ - { ownerId: session.user.id }, - { members: { some: { userId: session.user.id, role: 'ADMIN' } } }, - { - workspace: { - OR: [ - { ownerId: session.user.id }, - { members: { some: { userId: session.user.id, role: 'ADMIN' } } }, - ], - }, - }, - ], - }, - select: { id: true }, - }), - ]); - const canCreateProjects = creatableWorkspaces > 0; - const canUploadVideos = Boolean(editableProject); + }, + ], + }, + select: { id: true }, + }), + ]); + const canCreateProjects = creatableWorkspaces > 0; + const canUploadVideos = Boolean(editableProject); - const workspaceMap = new Map(); - for (const project of accessibleProjects) { - if (project.workspace) { - workspaceMap.set(project.workspace.id, project.workspace.name); - } + const workspaceMap = new Map(); + for (const project of accessibleProjects) { + if (project.workspace) { + workspaceMap.set(project.workspace.id, project.workspace.name); } - const workspaces = Array.from(workspaceMap, ([id, name]) => ({ id, name })); + } + const workspaces = Array.from(workspaceMap, ([id, name]) => ({ id, name })); - // Final query constraints - const queryWhere: Prisma.ProjectWhereInput = { - ...baseWhere, - ...(ws && ws !== 'all' ? { workspaceId: ws } : {}) - }; + // Final query constraints + const queryWhere: Prisma.ProjectWhereInput = { + ...baseWhere, + ...(ws && ws !== 'all' ? { workspaceId: ws } : {}), + }; - const [projects, totalProjects] = await Promise.all([ - db.project.findMany({ - skip, - take: pageSize, - where: queryWhere, - include: { - workspace: { - select: { id: true, name: true }, - }, - _count: { - select: { - videos: true, - members: true, - }, - }, - }, - orderBy: { updatedAt: orderByDirection }, - }), - db.project.count({ - where: queryWhere - }) - ]); + const [projects, totalProjects] = await Promise.all([ + db.project.findMany({ + skip, + take: pageSize, + where: queryWhere, + include: { + workspace: { + select: { id: true, name: true }, + }, + _count: { + select: { + videos: true, + members: true, + }, + }, + }, + orderBy: { updatedAt: orderByDirection }, + }), + db.project.count({ + where: queryWhere, + }), + ]); - const totalPages = Math.ceil(totalProjects / pageSize); + const totalPages = Math.ceil(totalProjects / pageSize); - const serializedProjects = projects.map((p) => ({ - id: p.id, - name: p.name, - description: p.description, - visibility: p.visibility, - updatedAt: p.updatedAt.toISOString(), - workspaceId: p.workspace?.id ?? null, - workspaceName: p.workspace?.name ?? null, - memberCount: p._count.members + 1, - videoCount: p._count.videos, - })); + const serializedProjects = projects.map((p) => ({ + id: p.id, + name: p.name, + description: p.description, + visibility: p.visibility, + updatedAt: p.updatedAt.toISOString(), + workspaceId: p.workspace?.id ?? null, + workspaceName: p.workspace?.name ?? null, + memberCount: p._count.members + 1, + videoCount: p._count.videos, + })); - return ( - - ); + return ( + + ); } diff --git a/app/(dashboard)/dashboard/project-filter.tsx b/app/(dashboard)/dashboard/project-filter.tsx index c9c5cb6..ebe3165 100644 --- a/app/(dashboard)/dashboard/project-filter.tsx +++ b/app/(dashboard)/dashboard/project-filter.tsx @@ -3,7 +3,18 @@ import { useCallback } from 'react'; import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import Link from 'next/link'; -import { Plus, FolderOpen, Clock, Users, Globe, Lock, UserPlus, Building2, ArrowUp, ArrowDown } from 'lucide-react'; +import { + Plus, + FolderOpen, + Clock, + Users, + Globe, + Lock, + UserPlus, + Building2, + ArrowUp, + ArrowDown, +} from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -62,13 +73,18 @@ function VisibilityIcon({ visibility }: { visibility: string }) { type SortOrder = 'desc' | 'asc'; -export function ProjectFilter({ projects, workspaces, totalPages, canCreateProjects }: ProjectFilterProps) { +export function ProjectFilter({ + projects, + workspaces, + totalPages, + canCreateProjects, +}: ProjectFilterProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const selectedWorkspace = searchParams.get('ws') || 'all'; - const sortOrder = searchParams.get('sort') as SortOrder || 'desc'; + const sortOrder = (searchParams.get('sort') as SortOrder) || 'desc'; const page = Number(searchParams.get('page')) || 1; const createQueryString = useCallback( diff --git a/app/(dashboard)/error.tsx b/app/(dashboard)/error.tsx index db3b0ce..39c3624 100644 --- a/app/(dashboard)/error.tsx +++ b/app/(dashboard)/error.tsx @@ -1,8 +1,8 @@ -"use client"; +'use client'; -import { useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { AlertTriangle } from "lucide-react"; +import { useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { AlertTriangle } from 'lucide-react'; export default function DashboardError({ error, @@ -12,7 +12,7 @@ export default function DashboardError({ reset: () => void; }) { useEffect(() => { - console.error("Dashboard error:", error); + console.error('Dashboard error:', error); }, [error]); return ( @@ -23,17 +23,13 @@ export default function DashboardError({

Something went wrong loading the dashboard. Your projects and videos are safe.

- {error.digest && ( -

- Error ID: {error.digest} -

- )} + {error.digest &&

Error ID: {error.digest}

}
-
diff --git a/app/(dashboard)/feedback/feedback-page-client.tsx b/app/(dashboard)/feedback/feedback-page-client.tsx index bcae3d8..1e1281a 100644 --- a/app/(dashboard)/feedback/feedback-page-client.tsx +++ b/app/(dashboard)/feedback/feedback-page-client.tsx @@ -10,7 +10,13 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; type FeedbackCategory = 'BUG' | 'FEATURE' | 'OTHER'; type TabValue = 'feedback' | 'review'; @@ -70,7 +76,10 @@ export default function FeedbackPage() { for (const file of allowedFiles) { const isImage = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'].includes(file.type); if (!isImage) { - setStatus({ type: 'error', message: 'Unsupported screenshot format. Use JPG, PNG, WEBP, or GIF.' }); + setStatus({ + type: 'error', + message: 'Unsupported screenshot format. Use JPG, PNG, WEBP, or GIF.', + }); continue; } if (file.size > 10 * 1024 * 1024) { @@ -92,7 +101,9 @@ export default function FeedbackPage() { const targetUrl = feedbackScreenshotPreviewUrls[index]; if (targetUrl) URL.revokeObjectURL(targetUrl); setFeedbackScreenshotFiles((prev) => prev.filter((_, currentIndex) => currentIndex !== index)); - setFeedbackScreenshotPreviewUrls((prev) => prev.filter((_, currentIndex) => currentIndex !== index)); + setFeedbackScreenshotPreviewUrls((prev) => + prev.filter((_, currentIndex) => currentIndex !== index) + ); }; const clearFeedbackScreenshots = () => { @@ -169,7 +180,10 @@ export default function FeedbackPage() { setReviewMessage(''); setReviewRating('5'); setAllowShowcase(false); - setStatus({ type: 'success', message: 'Review submitted. Thank you for sharing your experience.' }); + setStatus({ + type: 'success', + message: 'Review submitted. Thank you for sharing your experience.', + }); } catch { setStatus({ type: 'error', message: 'Failed to submit review' }); } finally { @@ -180,7 +194,10 @@ export default function FeedbackPage() { return (
- + Back to Dashboard @@ -189,7 +206,8 @@ export default function FeedbackPage() { Feedback & Review - Send product feedback, report bugs, or share a review we can feature on the landing page. + Send product feedback, report bugs, or share a review we can feature on the landing + page. @@ -205,7 +223,11 @@ export default function FeedbackPage() {
)} - setActiveTab(value as TabValue)} className="w-full"> + setActiveTab(value as TabValue)} + className="w-full" + > @@ -308,7 +330,11 @@ export default function FeedbackPage() {
@@ -332,7 +358,11 @@ export default function FeedbackPage() {
- @@ -369,11 +399,17 @@ export default function FeedbackPage() { onChange={(event) => setAllowShowcase(event.target.checked)} disabled={isSubmittingReview} /> - I allow OpenFrame to potentially showcase this review on the landing page. + + I allow OpenFrame to potentially showcase this review on the landing page. + diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index fcf000c..b186a81 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -2,11 +2,7 @@ import { Header } from '@/components/layout'; import { auth } from '@/lib/auth'; import { hasAppNavigationAccess } from '@/lib/route-access'; -export default async function DashboardLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default async function DashboardLayout({ children }: { children: React.ReactNode }) { const session = await auth(); const showAppNavigation = session?.user?.id ? await hasAppNavigationAccess(session.user.id) diff --git a/app/(dashboard)/not-found.tsx b/app/(dashboard)/not-found.tsx index dcee26c..1427824 100644 --- a/app/(dashboard)/not-found.tsx +++ b/app/(dashboard)/not-found.tsx @@ -1,6 +1,6 @@ -import Link from "next/link"; -import { Button } from "@/components/ui/button"; -import { FileQuestion } from "lucide-react"; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { FileQuestion } from 'lucide-react'; export default function DashboardNotFound() { return ( diff --git a/app/(dashboard)/projects/[projectId]/loading.tsx b/app/(dashboard)/projects/[projectId]/loading.tsx index 1313c16..6f2e682 100644 --- a/app/(dashboard)/projects/[projectId]/loading.tsx +++ b/app/(dashboard)/projects/[projectId]/loading.tsx @@ -1,5 +1,5 @@ -import { Skeleton } from "@/components/ui/skeleton" -import { Card, CardContent } from "@/components/ui/card" +import { Skeleton } from '@/components/ui/skeleton'; +import { Card, CardContent } from '@/components/ui/card'; function VideoCardSkeleton() { return ( @@ -14,7 +14,7 @@ function VideoCardSkeleton() {
- ) + ); } export default function ProjectLoading() { @@ -49,5 +49,5 @@ export default function ProjectLoading() { ))} - ) + ); } diff --git a/app/(dashboard)/projects/[projectId]/not-found.tsx b/app/(dashboard)/projects/[projectId]/not-found.tsx index f04dbb8..afc3de6 100644 --- a/app/(dashboard)/projects/[projectId]/not-found.tsx +++ b/app/(dashboard)/projects/[projectId]/not-found.tsx @@ -1,6 +1,6 @@ -import Link from "next/link"; -import { Button } from "@/components/ui/button"; -import { FolderX } from "lucide-react"; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { FolderX } from 'lucide-react'; export default function ProjectNotFound() { return ( @@ -9,7 +9,8 @@ export default function ProjectNotFound() {

Project Not Found

- The project you're looking for doesn't exist or you don't have access to it. + The project you're looking for doesn't exist or you don't have access to + it.

diff --git a/app/(dashboard)/projects/[projectId]/page.tsx b/app/(dashboard)/projects/[projectId]/page.tsx index f359c55..087634d 100644 --- a/app/(dashboard)/projects/[projectId]/page.tsx +++ b/app/(dashboard)/projects/[projectId]/page.tsx @@ -1,8 +1,6 @@ import Link from 'next/link'; import { notFound, redirect } from 'next/navigation'; -import { - ArrowLeft, -} from 'lucide-react'; +import { ArrowLeft } from 'lucide-react'; import { GuestGate } from '@/components/guest-gate'; import { auth, checkProjectAccess } from '@/lib/auth'; import { db } from '@/lib/db'; @@ -120,8 +118,8 @@ export default async function ProjectPage({ params, searchParams }: ProjectPageP }, }), db.video.count({ - where: { projectId: project.id } - }) + where: { projectId: project.id }, + }), ]); const totalPages = Math.ceil(totalVideos / pageSize); @@ -132,7 +130,8 @@ export default async function ProjectPage({ params, searchParams }: ProjectPageP return { id: video.id, title: video.title, - thumbnailUrl: activeVersion?.thumbnailUrl || 'https://via.placeholder.com/320x180?text=No+Thumbnail', + thumbnailUrl: + activeVersion?.thumbnailUrl || 'https://via.placeholder.com/320x180?text=No+Thumbnail', currentVersion: video._count.versions, commentCount: activeVersion?._count.comments || 0, duration: formatDuration(activeVersion?.duration), @@ -141,7 +140,12 @@ export default async function ProjectPage({ params, searchParams }: ProjectPageP }; }); - const canEdit = access.canEdit && (isOwner || project.members[0]?.role === 'ADMIN' || workspaceRole === 'OWNER' || workspaceRole === 'ADMIN'); + const canEdit = + access.canEdit && + (isOwner || + project.members[0]?.role === 'ADMIN' || + workspaceRole === 'OWNER' || + workspaceRole === 'ADMIN'); const isAuthenticated = !!session?.user?.id; const projectData = { diff --git a/app/(dashboard)/projects/[projectId]/project-content-client.tsx b/app/(dashboard)/projects/[projectId]/project-content-client.tsx index ef56c0d..c02edb8 100644 --- a/app/(dashboard)/projects/[projectId]/project-content-client.tsx +++ b/app/(dashboard)/projects/[projectId]/project-content-client.tsx @@ -57,7 +57,7 @@ export function ProjectContentClient({ canEdit, isOwner, totalPages, - currentPage + currentPage, }: ProjectContentClientProps) { const router = useRouter(); const searchParams = useSearchParams(); @@ -115,7 +115,10 @@ export function ProjectContentClient({
{project.workspace && ( - + {project.workspace.name} @@ -221,16 +224,13 @@ export function ProjectContentClient({ {/* Pagination */} {totalPages > 1 && (
- @@ -245,7 +245,7 @@ export function ProjectContentClient({ {currentPage < totalPages ? ( Next ) : ( - "Next" + 'Next' )}
diff --git a/app/(dashboard)/projects/[projectId]/settings/loading.tsx b/app/(dashboard)/projects/[projectId]/settings/loading.tsx index da924ee..a112fc9 100644 --- a/app/(dashboard)/projects/[projectId]/settings/loading.tsx +++ b/app/(dashboard)/projects/[projectId]/settings/loading.tsx @@ -1,5 +1,5 @@ -import { Skeleton } from "@/components/ui/skeleton" -import { Card, CardHeader, CardContent } from "@/components/ui/card" +import { Skeleton } from '@/components/ui/skeleton'; +import { Card, CardHeader, CardContent } from '@/components/ui/card'; export default function ProjectSettingsLoading() { return ( @@ -63,5 +63,5 @@ export default function ProjectSettingsLoading() {
- ) + ); } diff --git a/app/(dashboard)/projects/[projectId]/settings/project-settings-page-client.tsx b/app/(dashboard)/projects/[projectId]/settings/project-settings-page-client.tsx index 44a27c8..be96574 100644 --- a/app/(dashboard)/projects/[projectId]/settings/project-settings-page-client.tsx +++ b/app/(dashboard)/projects/[projectId]/settings/project-settings-page-client.tsx @@ -3,507 +3,544 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; -import { ArrowLeft, Loader2, Globe, Lock, UserPlus, Trash2, AlertTriangle, Settings, Save, Tag, Plus, X } from 'lucide-react'; +import { + ArrowLeft, + Loader2, + Globe, + Lock, + UserPlus, + Trash2, + AlertTriangle, + Settings, + Save, + Tag, + Plus, + X, +} from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } from '@/components/ui/alert-dialog'; type Visibility = 'PRIVATE' | 'INVITE' | 'PUBLIC'; -const visibilityOptions: { value: Visibility; label: string; description: string; icon: React.ReactNode }[] = [ - { - value: 'PRIVATE', - label: 'Private', - description: 'Only you can access this project', - icon: , - }, - { - value: 'INVITE', - label: 'Invite Only', - description: 'Share with specific people via email', - icon: , - }, - { - value: 'PUBLIC', - label: 'Public', - description: 'Anyone with the link can view', - icon: , - }, +const visibilityOptions: { + value: Visibility; + label: string; + description: string; + icon: React.ReactNode; +}[] = [ + { + value: 'PRIVATE', + label: 'Private', + description: 'Only you can access this project', + icon: , + }, + { + value: 'INVITE', + label: 'Invite Only', + description: 'Share with specific people via email', + icon: , + }, + { + value: 'PUBLIC', + label: 'Public', + description: 'Anyone with the link can view', + icon: , + }, ]; interface ProjectSettingsPageProps { - projectId: string; + projectId: string; } interface CommentTag { - id: string; - name: string; - color: string; - position: number; + id: string; + name: string; + color: string; + position: number; } export default function ProjectSettingsPageClient({ projectId }: ProjectSettingsPageProps) { - const router = useRouter(); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); - const [deleteConfirmation, setDeleteConfirmation] = useState(''); - const [formData, setFormData] = useState({ - name: '', - description: '', - visibility: 'PRIVATE' as Visibility, - }); + const router = useRouter(); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [deleteConfirmation, setDeleteConfirmation] = useState(''); + const [formData, setFormData] = useState({ + name: '', + description: '', + visibility: 'PRIVATE' as Visibility, + }); - // Tag management state - const [tags, setTags] = useState([]); - const [newTagName, setNewTagName] = useState(''); - const [newTagColor, setNewTagColor] = useState('#3B82F6'); - const [isAddingTag, setIsAddingTag] = useState(false); - const [editingTagId, setEditingTagId] = useState(null); - const [editTagName, setEditTagName] = useState(''); - const [editTagColor, setEditTagColor] = useState(''); + // Tag management state + const [tags, setTags] = useState([]); + const [newTagName, setNewTagName] = useState(''); + const [newTagColor, setNewTagColor] = useState('#3B82F6'); + const [isAddingTag, setIsAddingTag] = useState(false); + const [editingTagId, setEditingTagId] = useState(null); + const [editTagName, setEditTagName] = useState(''); + const [editTagColor, setEditTagColor] = useState(''); - useEffect(() => { - // Fetch project data - fetch(`/api/projects/${projectId}`) - .then((res) => res.json()) - .then((data) => { - if (data.error) { - setError(data.error); - } else { - const project = data.data; - setFormData({ - name: project.name || '', - description: project.description || '', - visibility: project.visibility || 'PRIVATE', - }); - } - }) - .catch(() => setError('Failed to load project')) - .finally(() => setIsLoading(false)); - - // Fetch tags - fetch(`/api/projects/${projectId}/tags`) - .then((res) => res.json()) - .then((data) => { - if (Array.isArray(data.data)) { - setTags(data.data); - } - }) - .catch(() => { /* Silent fail - tags are optional */ }); - }, [projectId]); - - const handleSave = async (e: React.FormEvent) => { - e.preventDefault(); - setIsSaving(true); - setError(''); - setSuccess(''); - - try { - const response = await fetch(`/api/projects/${projectId}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(formData), - }); - - const data = await response.json(); - - if (!response.ok) { - setError(data.error || 'Failed to update project'); - return; - } - - setSuccess('Project settings saved successfully'); - setTimeout(() => setSuccess(''), 3000); - } catch { - setError('Something went wrong. Please try again.'); - } finally { - setIsSaving(false); + useEffect(() => { + // Fetch project data + fetch(`/api/projects/${projectId}`) + .then((res) => res.json()) + .then((data) => { + if (data.error) { + setError(data.error); + } else { + const project = data.data; + setFormData({ + name: project.name || '', + description: project.description || '', + visibility: project.visibility || 'PRIVATE', + }); } - }; + }) + .catch(() => setError('Failed to load project')) + .finally(() => setIsLoading(false)); - const handleAddTag = async () => { - if (!newTagName.trim()) return; - setIsAddingTag(true); - try { - const res = await fetch(`/api/projects/${projectId}/tags`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: newTagName.trim(), color: newTagColor }), - }); - if (res.ok) { - const data = await res.json(); - const newTag = data.data; - setTags([...tags, newTag]); - setNewTagName(''); - setNewTagColor('#3B82F6'); - } - } catch { - // Silent fail - } finally { - setIsAddingTag(false); + // Fetch tags + fetch(`/api/projects/${projectId}/tags`) + .then((res) => res.json()) + .then((data) => { + if (Array.isArray(data.data)) { + setTags(data.data); } - }; + }) + .catch(() => { + /* Silent fail - tags are optional */ + }); + }, [projectId]); - const handleUpdateTag = async (tagId: string) => { - if (!editTagName.trim()) return; - try { - const res = await fetch(`/api/projects/${projectId}/tags/${tagId}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: editTagName.trim(), color: editTagColor }), - }); - if (res.ok) { - const data = await res.json(); - const updated = data.data; - setTags(tags.map((t) => (t.id === tagId ? updated : t))); - setEditingTagId(null); - } - } catch { - // Silent fail - } - }; + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSaving(true); + setError(''); + setSuccess(''); - const handleDeleteTag = async (tagId: string) => { - try { - const res = await fetch(`/api/projects/${projectId}/tags/${tagId}`, { - method: 'DELETE', - }); - if (res.ok) { - setTags(tags.filter((t) => t.id !== tagId)); - } - } catch { - // Silent fail - } - }; + try { + const response = await fetch(`/api/projects/${projectId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData), + }); - const handleDelete = async () => { - if (deleteConfirmation !== formData.name) { - setError('Project name does not match'); - return; - } + const data = await response.json(); - setIsDeleting(true); - setError(''); + if (!response.ok) { + setError(data.error || 'Failed to update project'); + return; + } - try { - const response = await fetch(`/api/projects/${projectId}`, { - method: 'DELETE', - }); + setSuccess('Project settings saved successfully'); + setTimeout(() => setSuccess(''), 3000); + } catch { + setError('Something went wrong. Please try again.'); + } finally { + setIsSaving(false); + } + }; - if (!response.ok) { - const data = await response.json(); - setError(data.error || 'Failed to delete project'); - return; - } + const handleAddTag = async () => { + if (!newTagName.trim()) return; + setIsAddingTag(true); + try { + const res = await fetch(`/api/projects/${projectId}/tags`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: newTagName.trim(), color: newTagColor }), + }); + if (res.ok) { + const data = await res.json(); + const newTag = data.data; + setTags([...tags, newTag]); + setNewTagName(''); + setNewTagColor('#3B82F6'); + } + } catch { + // Silent fail + } finally { + setIsAddingTag(false); + } + }; - router.push('/dashboard'); - } catch { - setError('Something went wrong. Please try again.'); - } finally { - setIsDeleting(false); - } - }; + const handleUpdateTag = async (tagId: string) => { + if (!editTagName.trim()) return; + try { + const res = await fetch(`/api/projects/${projectId}/tags/${tagId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: editTagName.trim(), color: editTagColor }), + }); + if (res.ok) { + const data = await res.json(); + const updated = data.data; + setTags(tags.map((t) => (t.id === tagId ? updated : t))); + setEditingTagId(null); + } + } catch { + // Silent fail + } + }; - if (isLoading) { - return ( -
- -
- ); + const handleDeleteTag = async (tagId: string) => { + try { + const res = await fetch(`/api/projects/${projectId}/tags/${tagId}`, { + method: 'DELETE', + }); + if (res.ok) { + setTags(tags.filter((t) => t.id !== tagId)); + } + } catch { + // Silent fail + } + }; + + const handleDelete = async () => { + if (deleteConfirmation !== formData.name) { + setError('Project name does not match'); + return; } + setIsDeleting(true); + setError(''); + + try { + const response = await fetch(`/api/projects/${projectId}`, { + method: 'DELETE', + }); + + if (!response.ok) { + const data = await response.json(); + setError(data.error || 'Failed to delete project'); + return; + } + + router.push('/dashboard'); + } catch { + setError('Something went wrong. Please try again.'); + } finally { + setIsDeleting(false); + } + }; + + if (isLoading) { return ( -
-
-
- - - Back to Project - -
- -
- {/* General Settings */} - - -
- -
- Project Settings - - Update your project details and access settings - -
- -
-
- - setFormData(prev => ({ ...prev, name: e.target.value }))} - required - disabled={isSaving} - className="h-11" - /> -
- -
- -