From 6eea327083d3c6f25297e2e172a29f3d1adf2afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0pek?= Date: Wed, 25 Feb 2026 16:42:47 +0300 Subject: [PATCH] refactor(dashboard): centralize access guards and split interactive pages into client components --- .../feedback/feedback-page-client.tsx | 387 ++++++ app/(dashboard)/feedback/page.tsx | 390 +----- .../projects/[projectId]/members/page.tsx | 19 +- app/(dashboard)/projects/[projectId]/page.tsx | 3 + .../projects/[projectId]/settings/page.tsx | 516 +------- .../settings/project-settings-page-client.tsx | 509 ++++++++ .../projects/[projectId]/share/page.tsx | 340 +---- .../share/project-share-page-client.tsx | 333 +++++ .../compare/compare-versions-page-client.tsx | 1081 ++++++++++++++++ .../videos/[videoId]/compare/page.tsx | 1092 +---------------- .../[projectId]/videos/[videoId]/page.tsx | 32 +- .../videos/[videoId]/share/page.tsx | 336 +---- .../share/video-share-page-client.tsx | 322 +++++ .../videos/new/new-video-page-client.tsx | 531 ++++++++ .../projects/[projectId]/videos/new/page.tsx | 538 +------- .../projects/new/new-project-page-client.tsx | 282 +++++ app/(dashboard)/projects/new/page.tsx | 285 +---- app/(dashboard)/settings/page.tsx | 516 +------- .../settings/settings-page-client.tsx | 513 ++++++++ .../workspaces/[workspaceId]/members/page.tsx | 19 +- .../workspaces/[workspaceId]/page.tsx | 2 +- .../new/new-workspace-project-page-client.tsx | 181 +++ .../[workspaceId]/projects/new/page.tsx | 187 +-- .../[workspaceId]/settings/page.tsx | 273 +---- .../workspace-settings-page-client.tsx | 266 ++++ .../new/new-workspace-page-client.tsx | 129 ++ app/(dashboard)/workspaces/new/page.tsx | 132 +- components/members-management-page.tsx | 13 +- lib/route-access.ts | 171 +++ 29 files changed, 4859 insertions(+), 4539 deletions(-) create mode 100644 app/(dashboard)/feedback/feedback-page-client.tsx create mode 100644 app/(dashboard)/projects/[projectId]/settings/project-settings-page-client.tsx create mode 100644 app/(dashboard)/projects/[projectId]/share/project-share-page-client.tsx create mode 100644 app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/compare-versions-page-client.tsx create mode 100644 app/(dashboard)/projects/[projectId]/videos/[videoId]/share/video-share-page-client.tsx create mode 100644 app/(dashboard)/projects/[projectId]/videos/new/new-video-page-client.tsx create mode 100644 app/(dashboard)/projects/new/new-project-page-client.tsx create mode 100644 app/(dashboard)/settings/settings-page-client.tsx create mode 100644 app/(dashboard)/workspaces/[workspaceId]/projects/new/new-workspace-project-page-client.tsx create mode 100644 app/(dashboard)/workspaces/[workspaceId]/settings/workspace-settings-page-client.tsx create mode 100644 app/(dashboard)/workspaces/new/new-workspace-page-client.tsx create mode 100644 lib/route-access.ts diff --git a/app/(dashboard)/feedback/feedback-page-client.tsx b/app/(dashboard)/feedback/feedback-page-client.tsx new file mode 100644 index 0000000..bcae3d8 --- /dev/null +++ b/app/(dashboard)/feedback/feedback-page-client.tsx @@ -0,0 +1,387 @@ +'use client'; + +import { ChangeEvent, FormEvent, useState } from 'react'; +import Link from 'next/link'; +import Image from 'next/image'; +import { ArrowLeft, Bug, Image as ImageIcon, Loader2, MessageSquareQuote, 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; + +type FeedbackCategory = 'BUG' | 'FEATURE' | 'OTHER'; +type TabValue = 'feedback' | 'review'; + +interface FormStatus { + type: 'success' | 'error'; + message: string; +} + +export default function FeedbackPage() { + const [activeTab, setActiveTab] = useState('feedback'); + const [status, setStatus] = useState(null); + const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false); + const [isSubmittingReview, setIsSubmittingReview] = useState(false); + + const [feedbackTitle, setFeedbackTitle] = useState(''); + const [feedbackCategory, setFeedbackCategory] = useState('BUG'); + const [feedbackMessage, setFeedbackMessage] = useState(''); + const [feedbackScreenshotFiles, setFeedbackScreenshotFiles] = useState([]); + const [feedbackScreenshotPreviewUrls, setFeedbackScreenshotPreviewUrls] = useState([]); + + const [reviewTitle, setReviewTitle] = useState(''); + const [reviewMessage, setReviewMessage] = useState(''); + const [reviewRating, setReviewRating] = useState('5'); + const [allowShowcase, setAllowShowcase] = useState(false); + + const uploadFeedbackScreenshot = async (file: File): Promise => { + const formData = new FormData(); + formData.append('image', file); + + const response = await fetch('/api/feedback/upload', { + method: 'POST', + body: formData, + }); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || 'Failed to upload screenshot'); + } + + return data.data.url as string; + }; + + const handleFeedbackScreenshotChange = (event: ChangeEvent) => { + const selectedFiles = Array.from(event.target.files ?? []); + if (selectedFiles.length === 0) return; + + const remainingSlots = 5 - feedbackScreenshotFiles.length; + if (remainingSlots <= 0) { + setStatus({ type: 'error', message: 'You can upload up to 5 screenshots.' }); + return; + } + + const allowedFiles = selectedFiles.slice(0, remainingSlots); + const nextFiles: File[] = []; + const nextPreviewUrls: string[] = []; + + 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.' }); + continue; + } + if (file.size > 10 * 1024 * 1024) { + setStatus({ type: 'error', message: 'Each screenshot must be smaller than 10MB.' }); + continue; + } + nextFiles.push(file); + nextPreviewUrls.push(URL.createObjectURL(file)); + } + + if (nextFiles.length > 0) { + setFeedbackScreenshotFiles((prev) => [...prev, ...nextFiles]); + setFeedbackScreenshotPreviewUrls((prev) => [...prev, ...nextPreviewUrls]); + } + event.target.value = ''; + }; + + const removeFeedbackScreenshot = (index: number) => { + const targetUrl = feedbackScreenshotPreviewUrls[index]; + if (targetUrl) URL.revokeObjectURL(targetUrl); + setFeedbackScreenshotFiles((prev) => prev.filter((_, currentIndex) => currentIndex !== index)); + setFeedbackScreenshotPreviewUrls((prev) => prev.filter((_, currentIndex) => currentIndex !== index)); + }; + + const clearFeedbackScreenshots = () => { + feedbackScreenshotPreviewUrls.forEach((url) => URL.revokeObjectURL(url)); + setFeedbackScreenshotFiles([]); + setFeedbackScreenshotPreviewUrls([]); + }; + + const handleFeedbackSubmit = async (event: FormEvent) => { + event.preventDefault(); + setStatus(null); + setIsSubmittingFeedback(true); + + try { + const screenshotUrls = await Promise.all( + feedbackScreenshotFiles.map((file) => uploadFeedbackScreenshot(file)) + ); + + const response = await fetch('/api/feedback', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'FEEDBACK', + category: feedbackCategory, + title: feedbackTitle, + message: feedbackMessage, + screenshotUrls, + }), + }); + + const data = await response.json(); + if (!response.ok) { + setStatus({ type: 'error', message: data.error || 'Failed to submit feedback' }); + return; + } + + setFeedbackTitle(''); + setFeedbackCategory('BUG'); + setFeedbackMessage(''); + clearFeedbackScreenshots(); + setStatus({ type: 'success', message: 'Feedback submitted. Thank you.' }); + } catch { + setStatus({ type: 'error', message: 'Failed to submit feedback' }); + } finally { + setIsSubmittingFeedback(false); + } + }; + + const handleReviewSubmit = async (event: FormEvent) => { + event.preventDefault(); + setStatus(null); + setIsSubmittingReview(true); + + try { + const response = await fetch('/api/feedback', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'REVIEW', + title: reviewTitle, + message: reviewMessage, + rating: Number(reviewRating), + allowShowcase, + }), + }); + + const data = await response.json(); + if (!response.ok) { + setStatus({ type: 'error', message: data.error || 'Failed to submit review' }); + return; + } + + setReviewTitle(''); + setReviewMessage(''); + setReviewRating('5'); + setAllowShowcase(false); + setStatus({ type: 'success', message: 'Review submitted. Thank you for sharing your experience.' }); + } catch { + setStatus({ type: 'error', message: 'Failed to submit review' }); + } finally { + setIsSubmittingReview(false); + } + }; + + return ( +
+
+ + + Back to Dashboard + + + + + Feedback & Review + + Send product feedback, report bugs, or share a review we can feature on the landing page. + + + + {status && ( +
+ {status.message} +
+ )} + + setActiveTab(value as TabValue)} className="w-full"> + + + + Feedback + + + + Review + + + + +
+
+ + setFeedbackTitle(event.target.value)} + placeholder="Short summary" + minLength={3} + maxLength={120} + required + disabled={isSubmittingFeedback} + /> +
+ +
+ + +
+ +
+ +