diff --git a/apps/backoffice/src/app/(protected)/hackathon-dashboard/page.tsx b/apps/backoffice/src/app/(protected)/hackathon-dashboard/page.tsx new file mode 100644 index 0000000..c9262c8 --- /dev/null +++ b/apps/backoffice/src/app/(protected)/hackathon-dashboard/page.tsx @@ -0,0 +1,32 @@ +import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'; +import { FC, ReactElement } from 'react'; + +export const HackathonDashboardPage: FC = (): ReactElement => { + return ( + +

Dashboard

+ +
+ {/* Participant */} +
+

+ 1261 +

+

Total Participants

+
+ {/* Team */} +
+

206

+

Total Teams

+
+ {/* Project Submitted */} +
+

0

+

Total Project Submitted

+
+
+
+ ); +}; + +export default HackathonDashboardPage; diff --git a/apps/backoffice/src/app/(protected)/hackathon-submissions/page.tsx b/apps/backoffice/src/app/(protected)/hackathon-submissions/page.tsx new file mode 100644 index 0000000..ea8a53b --- /dev/null +++ b/apps/backoffice/src/app/(protected)/hackathon-submissions/page.tsx @@ -0,0 +1,128 @@ +import { FC, ReactElement, useState } from 'react'; +import { + BackofficeWrapper, + DataTable, +} from '@imphnen-frontend-service/ui/organisms'; +import { + ColumnDef, + getCoreRowModel, + getPaginationRowModel, + PaginationState, + RowSelectionState, + useReactTable, +} from '@tanstack/react-table'; +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { cn } from '@imphnen-frontend-service/utils'; +import { EditOutlined } from '@ant-design/icons'; + +export const HackathonUsersPage: FC = (): ReactElement => { + const [rowSelection, setRowSelection] = useState({}); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 9, + }); + + const mockData: any[] = Array.from({ length: 90 }, (_, i) => ({ + id: i + 1, + project_name: `Project ${i + 1}`, + repository_url: `https://github.com/user/repo${i + 1}`, + demo_url: `https://demo.example.com/project${i + 1}`, + presentation_url: `https://slides.example.com/project${i + 1}`, + })); + + type UserStatus = 'active' | 'inactive'; + + interface SubmissionType { + id: number; + project_name: string; + repository_url: string; + demo_url: string; + presentation_url: string; + } + + const columns: ColumnDef[] = [ + { + header: 'Project Name', + accessorKey: 'project_name', + }, + { + header: 'Repository URL', + accessorKey: 'repository_url', + }, + { + header: 'Demo URL', + accessorKey: 'demo_url', + }, + { + header: 'Presentation URL', + accessorKey: 'presentation_url', + }, + { + header: 'Action', + meta: { cellClassName: cn('w-72') }, + cell: ({ row }) => ( + + ), + }, + ]; + + const table = useReactTable({ + data: mockData, + columns, + state: { + pagination, + rowSelection, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onPaginationChange: setPagination, + pageCount: Math.ceil(mockData.length / pagination.pageSize), + manualPagination: false, + }); + + return ( + +

+ Project Submission +

+ {/* Filters and actions */} +
+
+ + + +
+ + {/* Table */} + +
+ + {/* Modals extracted into shared backoffice components */} +
+ ); +}; + +export default HackathonUsersPage; diff --git a/apps/backoffice/src/app/(protected)/hackathon-teams/page.tsx b/apps/backoffice/src/app/(protected)/hackathon-teams/page.tsx new file mode 100644 index 0000000..eb52968 --- /dev/null +++ b/apps/backoffice/src/app/(protected)/hackathon-teams/page.tsx @@ -0,0 +1,210 @@ +import { FC, ReactElement, useState } from 'react'; +import { + BackofficeWrapper, + DataTable, +} from '@imphnen-frontend-service/ui/organisms'; +import { + ColumnDef, + getCoreRowModel, + getPaginationRowModel, + PaginationState, + RowSelectionState, + useReactTable, +} from '@tanstack/react-table'; +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { cn } from '@imphnen-frontend-service/utils'; +import { useTeams } from '@imphnen-frontend-service/service'; +import { EditOutlined } from '@ant-design/icons'; + +export const HackathonTeamsPage: FC = (): ReactElement => { + const { data: teamsData } = useTeams(); + + const [rowSelection, setRowSelection] = useState({}); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 9, + }); + + const mockData: TeamType[] = Array.from({ length: 90 }, (_, i) => ({ + id: `team-${i + 1}`, + name: `Team ${i + 1} - ${i % 3 === 0 ? 'Innovators' : 'Hackers'}`, + city: i % 2 === 0 ? 'Jakarta' : 'Bandung', + visibility: i % 4 === 0 ? 'private' : 'public', + member_count: Math.floor(Math.random() * 4) + 1, + has_submission: i % 3 !== 0, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + leader: { + user: { + fullname: `Leader User ${i}`, + email: `leader${i}@example.com`, + }, + }, + })); + + interface TeamType { + id: string; + name: string; + city: string; + visibility: 'public' | 'private'; + member_count: number; + has_submission: boolean; + created_at: string; + updated_at: string; + leader?: { + user: { + fullname: string; + email: string; + }; + }; + } + + const columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'ID', + }, + { + accessorKey: 'name', + header: 'Team Name', + }, + { + accessorKey: 'city', + header: 'City', + }, + { + accessorKey: 'visibility', + header: 'Visibility', + cell: ({ row }) => { + const isPublic = row.original.visibility === 'public'; + return ( + + {isPublic ? 'Public' : 'Private'} + + ); + }, + }, + { + accessorKey: 'member_count', + header: 'Members', + }, + { + id: 'leader', + header: 'Leader', + cell: ({ row }) => { + const leader = row.original.leader?.user; + return leader ? ( +
+
+ {leader.fullname} +
+
{leader.email}
+
+ ) : ( + - + ); + }, + }, + { + accessorKey: 'has_submission', + header: 'Submitted', + cell: ({ row }) => { + const hasSubmission = row.original.has_submission; + return ( + + {hasSubmission ? 'Yes' : 'No'} + + ); + }, + }, + { + accessorKey: 'updated_at', + header: 'Last Updated', + cell: ({ row }) => { + return new Date(row.original.updated_at).toLocaleDateString(); + }, + }, + { + id: 'actions', + header: 'Action', + meta: { cellClassName: cn('w-48') }, + cell: ({ row }) => ( +
+ +
+ ), + }, + ]; + + const table = useReactTable({ + data: mockData, + columns, + state: { + pagination, + rowSelection, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onPaginationChange: setPagination, + pageCount: Math.ceil(mockData.length / pagination.pageSize), + manualPagination: false, + }); + + return ( + +

+ Team Management +

+ {/* Filters and actions */} +
+
+ + + +
+ {/* Table */} + +
+ {/* Modals extracted into shared backoffice components */} +
+ ); +}; + +export default HackathonTeamsPage; diff --git a/apps/backoffice/src/app/(protected)/hackathon-users/_components/modal-user-detail.tsx b/apps/backoffice/src/app/(protected)/hackathon-users/_components/modal-user-detail.tsx new file mode 100644 index 0000000..8182c5f --- /dev/null +++ b/apps/backoffice/src/app/(protected)/hackathon-users/_components/modal-user-detail.tsx @@ -0,0 +1,651 @@ +import { FC, useState, useEffect, useMemo, useRef } from 'react'; +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { cn } from '@imphnen-frontend-service/utils'; +import { + UserOutlined, + EnvironmentOutlined, + CalendarOutlined, + SaveOutlined, + CloseOutlined, + ExclamationOutlined, + CameraOutlined, + DeleteOutlined, + UploadOutlined, +} from '@ant-design/icons'; + +interface UserType { + id: string; + avatar?: string; + fullname: string; + bio?: string; + location: string; + is_active: boolean; + skills: string[]; + created_at: string; + updated_at: string; +} + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + user: UserType | null; +} + +const ModalUserDetail: FC = ({ isOpen, onClose, user }) => { + const [formData, setFormData] = useState(null); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [showAvatarMenu, setShowAvatarMenu] = useState(false); + const fileInputRef = useRef(null); + + // Initialize form data when modal opens + useEffect(() => { + if (isOpen) { + if (user) { + // Edit existing user + setFormData({ ...user }); + } else { + // Create new user + setFormData({ + id: '', // Will be generated by backend + fullname: '', + bio: '', + location: '', + is_active: true, + skills: [], + avatar: undefined, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }); + } + } + }, [isOpen, user]); + + // Check if form has changes + const hasChanges = useMemo(() => { + if (!formData) return false; + if (!user) return true; // New user always has changes + return ( + formData.fullname !== user.fullname || + formData.location !== user.location || + formData.is_active !== user.is_active || + formData.avatar !== user.avatar || + JSON.stringify(formData.skills) !== JSON.stringify(user.skills) || + formData.bio !== user.bio + ); + }, [formData, user]); + + // Check if required fields are filled + const isFormValid = useMemo(() => { + if (!formData) return false; + return formData.fullname.trim() !== '' && formData.location.trim() !== ''; + }, [formData]); + + const canSave = hasChanges && isFormValid; + + if (!isOpen || !formData) return null; + + const handleInputChange = ( + field: keyof UserType, + value: string | boolean | string[] | undefined + ) => { + setFormData((prev) => (prev ? { ...prev, [field]: value } : null)); + }; + + const handleSkillsChange = (skills: string[]) => { + setFormData((prev) => (prev ? { ...prev, skills } : null)); + }; + + const handleSave = () => { + if (!formData) return; + + if (user) { + // Update existing user + console.log('Update user data:', formData); + } else { + // Create new user + console.log('Create new user:', formData); + } + // Here you would typically make an API call to save the data + onClose(); + }; + + const handleCancel = () => { + if (user) { + setFormData({ ...user }); // Reset to original for edit mode + } + onClose(); + }; + + const handleDeleteAccount = () => { + if (!user) return; // Can't delete new user + console.log('Delete user:', user.id); + setShowDeleteConfirm(false); + onClose(); + }; + + const handleAvatarUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + // Validate file type + if (!file.type.startsWith('image/')) { + alert('Please select an image file'); + return; + } + + // Validate file size (max 5MB) + if (file.size > 5 * 1024 * 1024) { + alert('Image size must be less than 5MB'); + return; + } + + // Create preview URL + const reader = new FileReader(); + reader.onload = (e) => { + const avatarUrl = e.target?.result as string; + handleInputChange('avatar', avatarUrl); + setShowAvatarMenu(false); + }; + reader.readAsDataURL(file); + } + }; + + const handleRemoveAvatar = () => { + handleInputChange('avatar', undefined); + setShowAvatarMenu(false); + }; + + const triggerFileUpload = () => { + fileInputRef.current?.click(); + }; + + const availableSkills = [ + 'Frontend Developer', + 'Backend Developer', + 'Full Stack Developer', + 'DevOps Engineer', + 'UI/UX Designer', + 'Product Manager', + 'Data Scientist', + 'Mobile Developer', + ]; + + return ( +
+
{ + setShowAvatarMenu(false); + onClose(); + }} + /> +
+
e.stopPropagation()} + > + {/* Hidden File Input */} + + {/* Header */} +
+
+ {/* Interactive User Avatar */} +
+
+ {formData.avatar ? ( + {formData.fullname} + ) : ( + + )} +
+ + {/* Avatar Hover Overlay */} + + + {/* Avatar Menu Dropdown */} + {showAvatarMenu && ( +
+ + {formData.avatar && ( + + )} +
+ )} +
+
+
+

+ {user ? 'Edit User Profile' : 'Create New User'} +

+ {user && ( + + Hover avatar to change + + )} +
+
+ {user + ? `Make changes to ${ + formData.fullname || 'this user' + }'s profile information` + : 'Fill in the information below to create a new user account'} +
+
+
+ +
+ + {/* Content */} +
setShowAvatarMenu(false)}> +
+ {/* Left Column - Basic Info */} +
+
+

+ Basic Information +

+
+ {/* Full Name - Required */} +
+ +
+ + + handleInputChange('fullname', e.target.value) + } + className={cn( + 'w-full border rounded-lg px-3 py-2 text-sm focus:border-primary-500 focus:outline-none', + formData.fullname.trim() === '' + ? 'border-red-300 bg-red-50' + : 'border-neutral-300' + )} + placeholder="Enter full name" + /> + {formData.fullname.trim() === '' && ( +

+ Full name is required +

+ )} +
+
+ + {/* Location - Required */} +
+ +
+ + + {formData.location.trim() === '' && ( +

+ Location is required +

+ )} +
+
+ + {/* Joined Date - Read Only - Only show for existing users */} + {user && ( +
+ +
+

+ Joined Date +

+

+ {new Date(formData.created_at).toLocaleDateString( + 'en-US', + { + year: 'numeric', + month: 'long', + day: 'numeric', + } + )} +

+
+
+ )} +
+
+ + {/* Bio Section - Optional */} +
+

+ Bio{' '} + + (Optional) + +

+