diff options
Diffstat (limited to 'components/project/ProjectList.tsx')
| -rw-r--r-- | components/project/ProjectList.tsx | 221 |
1 files changed, 147 insertions, 74 deletions
diff --git a/components/project/ProjectList.tsx b/components/project/ProjectList.tsx index 4a4f7962..9dec7e77 100644 --- a/components/project/ProjectList.tsx +++ b/components/project/ProjectList.tsx @@ -2,13 +2,12 @@ 'use client'; import { useState, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import { useForm } from 'react-hook-form'; -import { - Plus, - Folder, - Users, - Globe, +import { + Plus, + Folder, + Users, + Globe, Lock, Crown, Calendar, @@ -34,6 +33,7 @@ import { Switch } from '@/components/ui/switch'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useToast } from '@/hooks/use-toast'; import { cn } from '@/lib/utils'; +import { useRouter, usePathname } from "next/navigation" interface Project { id: string; @@ -65,11 +65,16 @@ export function ProjectList() { const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - + const pathname = usePathname() + + const internal = pathname?.includes('evcp') + + console.log(projects) + const router = useRouter(); const { toast } = useToast(); - // React Hook Form 설정 + // React Hook Form setup const { register, handleSubmit, @@ -100,8 +105,8 @@ export function ProjectList() { setProjects(data); } catch (error) { toast({ - title: '오류', - description: '프로젝트 목록을 불러올 수 없습니다.', + title: 'Error', + description: 'Unable to load project list.', variant: 'destructive', }); } @@ -116,25 +121,25 @@ export function ProjectList() { body: JSON.stringify(data), }); - if (!response.ok) throw new Error('프로젝트 생성 실패'); + if (!response.ok) throw new Error('Failed to create project'); const project = await response.json(); - + toast({ - title: '성공', - description: '프로젝트가 생성되었습니다.', + title: 'Success', + description: 'Project has been created.', }); setCreateDialogOpen(false); reset(); fetchProjects(); - - // 생성된 프로젝트로 이동 + + // Navigate to created project router.push(`/evcp/data-room/${project.id}`); } catch (error) { toast({ - title: '오류', - description: '프로젝트 생성에 실패했습니다.', + title: 'Error', + description: 'Failed to create project.', variant: 'destructive', }); } finally { @@ -150,19 +155,20 @@ export function ProjectList() { }; const filteredProjects = { - owned: projects.owned?.filter(p => + owned: projects.owned?.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()) - ), - member: projects.member?.filter(p => + ) || [], // Return empty array instead of undefined + member: projects.member?.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()) - ), - public: projects.public?.filter(p => + ) || [], + public: projects.public?.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()) - ), + ) || [], }; + const ProjectCard = ({ project, role }: { project: Project; role?: string }) => ( - <Card + <Card className="cursor-pointer hover:shadow-lg transition-shadow" onClick={() => router.push(`/evcp/data-room/${project.id}/files`)} > @@ -182,7 +188,7 @@ export function ProjectList() { )} </div> <CardDescription className="line-clamp-2"> - {project.description || '설명이 없습니다'} + {project.description || 'No description'} </CardDescription> </CardHeader> <CardContent> @@ -217,26 +223,30 @@ export function ProjectList() { return ( <> - {/* 헤더 */} + {/* Header */} <div className="flex items-center justify-between mb-6"> <div> - <h1 className="text-3xl font-bold">프로젝트</h1> + <h1 className="text-3xl font-bold">Projects</h1> <p className="text-muted-foreground mt-1"> - 파일을 관리하고 팀과 협업하세요 + Manage files and collaborate with your team </p> </div> - {/* <Button onClick={() => setCreateDialogOpen(true)}> - <Plus className="h-4 w-4 mr-2" /> - 새 프로젝트 - </Button> */} + {internal && + <Button onClick={() => setCreateDialogOpen(true)}> + <Plus className="h-4 w-4 mr-2" /> + New Project + </Button> + + } + </div> - {/* 검색 및 필터 */} + {/* Search and Filter */} <div className="flex items-center gap-3 mb-6"> <div className="relative flex-1 max-w-md"> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Input - placeholder="프로젝트 검색..." + placeholder="Search projects..." className="pl-9" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} @@ -251,22 +261,85 @@ export function ProjectList() { </Button> </div> - {/* 프로젝트 목록 */} + {/* Project List */} <Tabs defaultValue="owned" className="space-y-6"> <TabsList> + {internal && + <TabsTrigger value="owned"> + My Projects ({filteredProjects.owned?.length}) + </TabsTrigger> + } + <TabsTrigger value="member"> - 참여 프로젝트 ({filteredProjects.member?.length}) + Joined Projects ({filteredProjects.member?.length}) </TabsTrigger> <TabsTrigger value="public"> - 공개 프로젝트 ({filteredProjects.public?.length}) + Public Projects ({filteredProjects.public?.length}) </TabsTrigger> </TabsList> + {/* My Projects Tab */} + {internal && + <TabsContent value="owned"> + {filteredProjects.owned?.length === 0 ? ( + <div className="text-center py-12"> + <Crown className="h-12 w-12 text-muted-foreground mx-auto mb-3" /> + <p className="text-muted-foreground">You don't own any projects</p> + <Button + className="mt-4" + onClick={() => setCreateDialogOpen(true)} + > + <Plus className="h-4 w-4 mr-2" /> + Create your first project + </Button> + </div> + ) : viewMode === 'grid' ? ( + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> + {filteredProjects.owned?.map(project => ( + <ProjectCard key={project.id} project={project} role="owner" /> + ))} + </div> + ) : ( + <div className="space-y-2"> + {filteredProjects.owned?.map(project => ( + <Card + key={project.id} + className="cursor-pointer hover:shadow transition-shadow" + onClick={() => router.push(`/evcp/data-room/${project.id}/files`)} + > + <CardContent className="flex items-center justify-between p-4"> + <div className="flex items-center gap-3"> + <Folder className="h-5 w-5 text-blue-500" /> + <div> + <p className="font-medium">{project.code} {project.name}</p> + <p className="text-sm text-muted-foreground"> + {project.description || 'No description'} + </p> + </div> + </div> + <div className="flex items-center gap-2"> + <Badge variant="secondary">Owner</Badge> + {project.isPublic ? ( + <Globe className="h-4 w-4 text-green-500" /> + ) : ( + <Lock className="h-4 w-4 text-gray-500" /> + )} + </div> + </CardContent> + </Card> + ))} + </div> + )} + </TabsContent> + + + } + <TabsContent value="member"> {filteredProjects.member?.length === 0 ? ( <div className="text-center py-12"> <Users className="h-12 w-12 text-muted-foreground mx-auto mb-3" /> - <p className="text-muted-foreground">참여 중인 프로젝트가 없습니다</p> + <p className="text-muted-foreground">You are not a member of any projects</p> </div> ) : viewMode === 'grid' ? ( <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> @@ -277,7 +350,7 @@ export function ProjectList() { ) : ( <div className="space-y-2"> {filteredProjects.member?.map(project => ( - <Card + <Card key={project.id} className="cursor-pointer hover:shadow transition-shadow" onClick={() => router.push(`/evcp/data-room/${project.id}/files`)} @@ -286,9 +359,9 @@ export function ProjectList() { <div className="flex items-center gap-3"> <Folder className="h-5 w-5 text-blue-500" /> <div> - <p className="font-medium">{project.name}</p> + <p className="font-medium">{project.code} {project.name}</p> <p className="text-sm text-muted-foreground"> - {project.description || '설명이 없습니다'} + {project.description || 'No description'} </p> </div> </div> @@ -311,7 +384,7 @@ export function ProjectList() { {filteredProjects.public?.length === 0 ? ( <div className="text-center py-12"> <Globe className="h-12 w-12 text-muted-foreground mx-auto mb-3" /> - <p className="text-muted-foreground">공개 프로젝트가 없습니다</p> + <p className="text-muted-foreground">No public projects</p> </div> ) : viewMode === 'grid' ? ( <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> @@ -322,7 +395,7 @@ export function ProjectList() { ) : ( <div className="space-y-2"> {filteredProjects.public?.map(project => ( - <Card + <Card key={project.id} className="cursor-pointer hover:shadow transition-shadow" onClick={() => router.push(`/evcp/data-room/${project.id}/files`)} @@ -331,13 +404,13 @@ export function ProjectList() { <div className="flex items-center gap-3"> <Globe className="h-5 w-5 text-green-500" /> <div> - <p className="font-medium">{project.name}</p> + <p className="font-medium">{project.code} {project.name}</p> <p className="text-sm text-muted-foreground"> - {project.description || '설명이 없습니다'} + {project.description || 'No description'} </p> </div> </div> - <Badge variant="outline">공개</Badge> + <Badge variant="outline">Public</Badge> </CardContent> </Card> ))} @@ -346,32 +419,32 @@ export function ProjectList() { </TabsContent> </Tabs> - {/* 프로젝트 생성 다이얼로그 */} + {/* Create Project Dialog */} <Dialog open={createDialogOpen} onOpenChange={handleDialogClose}> <DialogContent> <DialogHeader> - <DialogTitle>새 프로젝트 만들기</DialogTitle> + <DialogTitle>Create New Project</DialogTitle> <DialogDescription> - 팀과 파일을 공유할 새 프로젝트를 생성합니다 + Create a new project to share files with your team </DialogDescription> </DialogHeader> - + <form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <div> <Label htmlFor="code"> - 프로젝트 코드 <span className="text-red-500">*</span> + Project Code <span className="text-red-500">*</span> </Label> <Input id="code" {...register('code', { - required: '프로젝트 코드는 필수입니다', + required: 'Project code is required', minLength: { value: 2, - message: '프로젝트 코드는 최소 2자 이상이어야 합니다', + message: 'Project code must be at least 2 characters', }, pattern: { value: /^[A-Z0-9]+$/, - message: '프로젝트 코드는 대문자와 숫자만 사용 가능합니다', + message: 'Project code can only contain uppercase letters and numbers', }, })} placeholder="SN1001" @@ -384,52 +457,52 @@ export function ProjectList() { <div> <Label htmlFor="name"> - 프로젝트 이름 <span className="text-red-500">*</span> + Project Name <span className="text-red-500">*</span> </Label> <Input id="name" {...register('name', { - required: '프로젝트 이름은 필수입니다', + required: 'Project name is required', minLength: { value: 2, - message: '프로젝트 이름은 최소 2자 이상이어야 합니다', + message: 'Project name must be at least 2 characters', }, maxLength: { value: 50, - message: '프로젝트 이름은 50자를 초과할 수 없습니다', + message: 'Project name cannot exceed 50 characters', }, })} - placeholder="예: FNLG" + placeholder="e.g. FNLG" className={errors.name ? 'border-red-500' : ''} /> {errors.name && ( <p className="text-sm text-red-500 mt-1">{errors.name.message}</p> )} </div> - + <div> - <Label htmlFor="description">설명 (선택)</Label> + <Label htmlFor="description">Description (Optional)</Label> <Input id="description" {...register('description', { maxLength: { value: 200, - message: '설명은 200자를 초과할 수 없습니다', + message: 'Description cannot exceed 200 characters', }, })} - placeholder="프로젝트에 대한 간단한 설명" + placeholder="Brief description of the project" className={errors.description ? 'border-red-500' : ''} /> {errors.description && ( <p className="text-sm text-red-500 mt-1">{errors.description.message}</p> )} </div> - + <div className="flex items-center justify-between"> <div> - <Label htmlFor="public">공개 프로젝트</Label> + <Label htmlFor="public">Public Project</Label> <p className="text-sm text-muted-foreground"> - 모든 사용자가 이 프로젝트를 볼 수 있습니다 + All users can view this project </p> </div> <Switch @@ -438,21 +511,21 @@ export function ProjectList() { onCheckedChange={(checked) => setValue('isPublic', checked)} /> </div> - + <DialogFooter> - <Button - type="button" - variant="outline" + <Button + type="button" + variant="outline" onClick={() => handleDialogClose(false)} disabled={isSubmitting} > - 취소 + Cancel </Button> - <Button + <Button type="submit" disabled={!isValid || isSubmitting} > - {isSubmitting ? '생성 중...' : '프로젝트 생성'} + {isSubmitting ? 'Creating...' : 'Create Project'} </Button> </DialogFooter> </form> |
