33
44import { Button } from "@/components/ui/button" ;
55import { Card , CardContent , CardDescription , CardHeader , CardTitle } from "@/components/ui/card" ;
6- import { PlusCircle , Search , Filter , FolderKanban , Flame } from "lucide-react" ;
6+ import { PlusCircle , Search , Filter , FolderKanban , Flame , MoreHorizontal , Copy , Link as LinkIcon , ChevronDown } from "lucide-react" ;
77import Link from "next/link" ;
88import { Input } from "@/components/ui/input" ;
9- import { useEffect , useState , useCallback } from "react" ;
10- import type { Project } from "@/types" ;
9+ import { useEffect , useState , useCallback , useActionState } from "react" ;
10+ import type { Project , DuplicateProjectFormState } from "@/types" ;
1111import { useAuth } from "@/hooks/useAuth" ;
1212import { Skeleton } from "@/components/ui/skeleton" ;
1313import { useRouter } from "next/navigation" ;
1414import { fetchProjectsAction } from "./actions" ;
15+ import { duplicateProjectAction } from "./[id]/actions" ;
1516import { cn } from "@/lib/utils" ;
17+ import { DropdownMenu , DropdownMenuContent , DropdownMenuItem , DropdownMenuSeparator , DropdownMenuTrigger } from "@/components/ui/dropdown-menu" ;
18+ import { Dialog , DialogContent , DialogDescription , DialogFooter , DialogHeader , DialogTitle , DialogTrigger , DialogClose } from "@/components/ui/dialog" ;
19+ import { Checkbox } from "@/components/ui/checkbox" ;
20+ import { Label } from "@/components/ui/label" ;
21+ import { useToast } from "@/hooks/use-toast" ;
22+ import { Loader2 } from "lucide-react" ;
1623
1724
1825export default function ProjectsPage ( ) {
1926 const { user, isLoading : authLoading } = useAuth ( ) ;
2027 const router = useRouter ( ) ;
28+ const { toast } = useToast ( ) ;
2129 const [ projects , setProjects ] = useState < Project [ ] > ( [ ] ) ;
2230 const [ isLoadingProjects , setIsLoadingProjects ] = useState ( true ) ;
2331 const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
32+
33+ const [ projectToDuplicate , setProjectToDuplicate ] = useState < Project | null > ( null ) ;
34+ const [ duplicateFormState , duplicateFormAction , isDuplicating ] = useActionState ( duplicateProjectAction , { message : "" , error : "" } ) ;
35+
2436
2537 const loadProjects = useCallback ( async ( ) => {
2638 if ( user && ! authLoading ) {
@@ -45,6 +57,19 @@ export default function ProjectsPage() {
4557 loadProjects ( ) ;
4658 } , [ loadProjects ] ) ;
4759
60+ useEffect ( ( ) => {
61+ if ( ! isDuplicating && duplicateFormState ) {
62+ if ( duplicateFormState . message && ! duplicateFormState . error ) {
63+ toast ( { title : "Success!" , description : duplicateFormState . message } ) ;
64+ setProjectToDuplicate ( null ) ; // Close dialog
65+ loadProjects ( ) ; // Refresh the list
66+ }
67+ if ( duplicateFormState . error ) {
68+ toast ( { variant : "destructive" , title : "Duplication Error" , description : duplicateFormState . error } ) ;
69+ }
70+ }
71+ } , [ duplicateFormState , isDuplicating , toast , loadProjects ] ) ;
72+
4873 if ( authLoading && isLoadingProjects ) {
4974 return (
5075 < div className = "space-y-6" >
@@ -150,35 +175,94 @@ export default function ProjectsPage() {
150175 ) : (
151176 < div className = "grid gap-4 md:grid-cols-2 lg:grid-cols-3" >
152177 { filteredProjects . map ( ( project ) => (
153- < Card
154- key = { project . uuid }
155- className = { cn (
156- "hover:shadow-lg transition-shadow flex flex-col" ,
157- project . isUrgent && "border-destructive ring-1 ring-destructive"
158- ) }
159- >
160- < CardHeader className = "flex-grow" >
161- < div className = "flex justify-between items-start" >
162- < CardTitle className = "hover:text-primary" >
163- < Link href = { `/projects/${ project . uuid } ` } > { project . name } </ Link >
164- </ CardTitle >
165- { project . isUrgent && < Flame className = "h-5 w-5 text-destructive flex-shrink-0" /> }
166- </ div >
167- < CardDescription className = "h-12 overflow-hidden text-ellipsis line-clamp-2" >
168- { project . description || "No description provided." }
169- </ CardDescription >
170- </ CardHeader >
171- < CardContent >
172- < div className = "text-xs text-muted-foreground space-y-0.5" >
173- < p > Owner: < span className = "font-medium text-foreground" > { project . ownerUuid === user ?. uuid ? 'You' : 'Other' } </ span > </ p >
174- < p > Updated: < span className = "font-medium text-foreground" > { new Date ( project . updatedAt ) . toLocaleDateString ( ) } </ span > </ p >
175- < p > Status: < span className = { cn ( "font-medium" , project . isPrivate ? "text-foreground" : "text-green-600" ) } > { project . isPrivate ? 'Private' : 'Public' } </ span > </ p >
176- </ div >
177- < Button variant = "outline" size = "sm" className = "w-full mt-3" asChild >
178- < Link href = { `/projects/${ project . uuid } ` } > View Details</ Link >
179- </ Button >
180- </ CardContent >
181- </ Card >
178+ < Dialog key = { project . uuid } open = { projectToDuplicate ?. uuid === project . uuid } onOpenChange = { ( open ) => ! open && setProjectToDuplicate ( null ) } >
179+ < Card
180+ className = { cn (
181+ "hover:shadow-lg transition-shadow flex flex-col" ,
182+ project . isUrgent && "border-destructive ring-1 ring-destructive"
183+ ) }
184+ >
185+ < CardHeader className = "flex-grow" >
186+ < div className = "flex justify-between items-start" >
187+ < CardTitle className = "hover:text-primary" >
188+ < Link href = { `/projects/${ project . uuid } ` } > { project . name } </ Link >
189+ </ CardTitle >
190+ { project . isUrgent && < Flame className = "h-5 w-5 text-destructive flex-shrink-0" /> }
191+ </ div >
192+ < CardDescription className = "h-12 overflow-hidden text-ellipsis line-clamp-2" >
193+ { project . description || "No description provided." }
194+ </ CardDescription >
195+ </ CardHeader >
196+ < CardContent >
197+ < div className = "text-xs text-muted-foreground space-y-0.5" >
198+ < p > Owner: < span className = "font-medium text-foreground" > { project . ownerUuid === user ?. uuid ? 'You' : 'Other' } </ span > </ p >
199+ < p > Updated: < span className = "font-medium text-foreground" > { new Date ( project . updatedAt ) . toLocaleDateString ( ) } </ span > </ p >
200+ < p > Status: < span className = { cn ( "font-medium" , project . isPrivate ? "text-foreground" : "text-green-600" ) } > { project . isPrivate ? 'Private' : 'Public' } </ span > </ p >
201+ </ div >
202+ < div className = "mt-3" >
203+ < DropdownMenu >
204+ < DropdownMenuTrigger asChild >
205+ < Button variant = "outline" size = "sm" className = "w-full" >
206+ Actions < ChevronDown className = "ml-auto h-4 w-4" />
207+ </ Button >
208+ </ DropdownMenuTrigger >
209+ < DropdownMenuContent align = "end" className = "w-48" >
210+ < DropdownMenuItem asChild >
211+ < Link href = { `/projects/${ project . uuid } ` } >
212+ < FolderKanban className = "mr-2 h-4 w-4" />
213+ < span > View Details</ span >
214+ </ Link >
215+ </ DropdownMenuItem >
216+ < DropdownMenuItem onSelect = { ( ) => navigator . clipboard . writeText ( `${ window . location . origin } /projects/${ project . uuid } ` ) } >
217+ < LinkIcon className = "mr-2 h-4 w-4" />
218+ < span > Copy Link</ span >
219+ </ DropdownMenuItem >
220+ < DropdownMenuSeparator />
221+ < DialogTrigger asChild >
222+ < DropdownMenuItem onSelect = { ( e ) => { e . preventDefault ( ) ; setProjectToDuplicate ( project ) ; } } >
223+ < Copy className = "mr-2 h-4 w-4" />
224+ < span > Duplicate</ span >
225+ </ DropdownMenuItem >
226+ </ DialogTrigger >
227+ </ DropdownMenuContent >
228+ </ DropdownMenu >
229+ </ div >
230+ </ CardContent >
231+ </ Card >
232+ < DialogContent >
233+ < DialogHeader >
234+ < DialogTitle > Duplicate Project: { project . name } </ DialogTitle >
235+ < DialogDescription >
236+ This will create a new project with a copy of all tasks, documents, and settings. Members and integrations will not be copied.
237+ </ DialogDescription >
238+ </ DialogHeader >
239+ < form action = { duplicateFormAction } >
240+ < input type = "hidden" name = "originalProjectUuid" value = { project . uuid } />
241+ < div className = "space-y-4 my-4" >
242+ { project . githubRepoUrl && (
243+ < div className = "flex items-start space-x-3 rounded-md border p-3" >
244+ < Checkbox id = { `fork-repo-${ project . uuid } ` } name = "forkGithubRepo" />
245+ < div className = "grid gap-1.5 leading-none" >
246+ < Label htmlFor = { `fork-repo-${ project . uuid } ` } >
247+ Also fork the GitHub repository
248+ </ Label >
249+ < p className = "text-sm text-muted-foreground" >
250+ This will fork < span className = "font-mono bg-muted/50 px-1 py-0.5 rounded text-xs" > { project . githubRepoName } </ span > to your GitHub account. Requires a connected GitHub account.
251+ </ p >
252+ </ div >
253+ </ div >
254+ ) }
255+ </ div >
256+ < DialogFooter >
257+ < DialogClose asChild > < Button type = "button" variant = "ghost" disabled = { isDuplicating } > Cancel</ Button > </ DialogClose >
258+ < Button type = "submit" disabled = { isDuplicating } >
259+ { isDuplicating && < Loader2 className = "mr-2 h-4 w-4 animate-spin" /> }
260+ Duplicate Project
261+ </ Button >
262+ </ DialogFooter >
263+ </ form >
264+ </ DialogContent >
265+ </ Dialog >
182266 ) ) }
183267 </ div >
184268 ) }
@@ -187,4 +271,3 @@ export default function ProjectsPage() {
187271 </ div >
188272 ) ;
189273}
190-
0 commit comments