feat(hackathon): draft data table column & API Contract
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
import { FC, ReactElement, useState } from 'react';
|
||||
import ModalUserDetail from '../../../components/modal-user-detail';
|
||||
import ModalSuspendOrBan from '../../../components/modal-suspend-or-ban';
|
||||
import ModalDeleteUser from '../../../components/modal-delete-user';
|
||||
import {
|
||||
BackofficeWrapper,
|
||||
DataTable,
|
||||
@@ -16,13 +13,9 @@ import {
|
||||
} from '@tanstack/react-table';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
|
||||
export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [showSuspendModal, setShowSuspendModal] = useState(false);
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
@@ -31,80 +24,38 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
|
||||
const mockData: any[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
||||
email: 'fullname23@gmail.com',
|
||||
rating: 4.5,
|
||||
status: i % 2 === 0 ? 'active' : 'inactive',
|
||||
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 UserType {
|
||||
interface SubmissionType {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
rating: number;
|
||||
status: UserStatus;
|
||||
project_name: string;
|
||||
repository_url: string;
|
||||
demo_url: string;
|
||||
presentation_url: string;
|
||||
}
|
||||
|
||||
const columns: ColumnDef<UserType>[] = [
|
||||
const columns: ColumnDef<SubmissionType>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
meta: { cellClassName: cn('w-20') },
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={row.getIsSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
header: 'Project Name',
|
||||
accessorKey: 'project_name',
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
header: 'Repository URL',
|
||||
accessorKey: 'repository_url',
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
header: 'Email',
|
||||
accessorKey: 'email',
|
||||
header: 'Demo URL',
|
||||
accessorKey: 'demo_url',
|
||||
},
|
||||
{
|
||||
id: 'rating',
|
||||
header: 'Rating',
|
||||
accessorKey: 'rating',
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const statusColors: Record<UserStatus, string> = {
|
||||
active: 'bg-success-200 text-success-500',
|
||||
inactive: 'bg-danger-200 text-danger-500',
|
||||
};
|
||||
const statusText: Record<UserStatus, string> = {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||
>
|
||||
{statusText[status]}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
header: 'Presentation URL',
|
||||
accessorKey: 'presentation_url',
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
@@ -113,17 +64,12 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={
|
||||
(e) => {}
|
||||
// {
|
||||
// e.stopPropagation();
|
||||
// setSelectedUserId(row.original.id);
|
||||
// setShowDetail(true);
|
||||
// }
|
||||
}
|
||||
className="flex items-center gap-2 w-max"
|
||||
onClick={() => {
|
||||
// View detail logic
|
||||
}}
|
||||
>
|
||||
<SearchOutlined className="text-[16px]" /> Lihat Detail & Action
|
||||
<EditOutlined className="text-base" /> View & Manage
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
@@ -175,18 +121,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
</section>
|
||||
|
||||
{/* Modals extracted into shared backoffice components */}
|
||||
<ModalUserDetail
|
||||
isOpen={showDetailModal}
|
||||
onClose={() => setShowDetailModal(false)}
|
||||
/>
|
||||
<ModalSuspendOrBan
|
||||
isOpen={showSuspendModal}
|
||||
onClose={() => setShowSuspendModal(false)}
|
||||
/>
|
||||
<ModalDeleteUser
|
||||
isOpen={showDeleteModal}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
/>
|
||||
</BackofficeWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { FC, ReactElement, useState } from 'react';
|
||||
import ModalUserDetail from '../../../components/modal-user-detail';
|
||||
import ModalSuspendOrBan from '../../../components/modal-suspend-or-ban';
|
||||
import ModalDeleteUser from '../../../components/modal-delete-user';
|
||||
import {
|
||||
BackofficeWrapper,
|
||||
DataTable,
|
||||
@@ -16,11 +13,11 @@ import {
|
||||
} 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 [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [showSuspendModal, setShowSuspendModal] = useState(false);
|
||||
const { data: teamsData } = useTeams();
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
@@ -64,34 +61,12 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
|
||||
const columns: ColumnDef<TeamType>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
meta: { cellClassName: cn('w-12') },
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
checked={row.getIsSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Team Name',
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{row.original.name}</div>
|
||||
<div className="text-xs text-gray-500">{row.original.id}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'city',
|
||||
@@ -105,10 +80,10 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'px-2 py-1 rounded-full text-xs font-medium',
|
||||
'py-2 px-4 text-sm rounded-2xl text-center',
|
||||
isPublic
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
? 'bg-info-200 text-info-700'
|
||||
: 'bg-gray-200 text-gray-700'
|
||||
)}
|
||||
>
|
||||
{isPublic ? 'Public' : 'Private'}
|
||||
@@ -119,12 +94,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
{
|
||||
accessorKey: 'member_count',
|
||||
header: 'Members',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium">{row.getValue('member_count')}</span>
|
||||
<span className="text-gray-500 text-xs">members</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'leader',
|
||||
@@ -139,22 +108,22 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
<div className="text-xs text-gray-500">{leader.email}</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400 italic">No leader</span>
|
||||
<span className="text-gray-400 italic">-</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'has_submission',
|
||||
header: 'Submission',
|
||||
header: 'Submitted',
|
||||
cell: ({ row }) => {
|
||||
const hasSubmission = row.original.has_submission;
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'px-2 py-1 rounded-full text-xs font-medium',
|
||||
'py-2 px-4 text-sm rounded-2xl text-center',
|
||||
hasSubmission
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-orange-100 text-orange-700'
|
||||
? 'bg-success-200 text-success-700'
|
||||
: 'bg-danger-200 text-danger-700'
|
||||
)}
|
||||
>
|
||||
{hasSubmission ? 'Yes' : 'No'}
|
||||
@@ -166,11 +135,7 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
accessorKey: 'updated_at',
|
||||
header: 'Last Updated',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<span className="text-sm text-gray-500">
|
||||
{new Date(row.original.updated_at).toLocaleDateString()}
|
||||
</span>
|
||||
);
|
||||
return new Date(row.original.updated_at).toLocaleDateString();
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -180,25 +145,14 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="text"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="text-primary-600 hover:text-primary-700 p-0"
|
||||
className="flex items-center gap-2 w-max"
|
||||
onClick={() => {
|
||||
// View detail logic
|
||||
}}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<span className="text-gray-300">|</span>
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
className="text-gray-600 hover:text-gray-700 p-0"
|
||||
onClick={() => {
|
||||
// Manage logic
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
<EditOutlined className="text-base" /> View & Manage
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
@@ -249,18 +203,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
<DataTable data={mockData} columns={columns} table={table} />
|
||||
</section>
|
||||
{/* Modals extracted into shared backoffice components */}
|
||||
<ModalUserDetail
|
||||
isOpen={showDetailModal}
|
||||
onClose={() => setShowDetailModal(false)}
|
||||
/>
|
||||
<ModalSuspendOrBan
|
||||
isOpen={showSuspendModal}
|
||||
onClose={() => setShowSuspendModal(false)}
|
||||
/>
|
||||
<ModalDeleteUser
|
||||
isOpen={showDeleteModal}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
/>
|
||||
</BackofficeWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose }) => {
|
||||
<div className="bg-white rounded-lg shadow w-full max-w-3xl">
|
||||
<div className="border-b px-6 py-4 flex justify-between items-center">
|
||||
<h2 className="text-p3 font-semibold">User Detail</h2>
|
||||
<button className="p-2" onClick={onClose}>
|
||||
<button className="p-2 cursor-pointer" onClick={onClose}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,7 +1,5 @@
|
||||
import { FC, ReactElement, useState } from 'react';
|
||||
import ModalUserDetail from '../../../components/modal-user-detail';
|
||||
import ModalSuspendOrBan from '../../../components/modal-suspend-or-ban';
|
||||
import ModalDeleteUser from '../../../components/modal-delete-user';
|
||||
import ModalUserDetail from './_components/modal-user-detail';
|
||||
import {
|
||||
BackofficeWrapper,
|
||||
DataTable,
|
||||
@@ -16,12 +14,11 @@ import {
|
||||
} from '@tanstack/react-table';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
// Removed unused SearchOutlined icon after schema revision
|
||||
|
||||
export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [showSuspendModal, setShowSuspendModal] = useState(false);
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
@@ -29,82 +26,68 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
pageSize: 9,
|
||||
});
|
||||
|
||||
const mockData: any[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
||||
email: 'fullname23@gmail.com',
|
||||
rating: 4.5,
|
||||
status: i % 2 === 0 ? 'active' : 'inactive',
|
||||
}));
|
||||
|
||||
type UserStatus = 'active' | 'inactive';
|
||||
|
||||
// Mock data aligned to API user schema
|
||||
interface UserType {
|
||||
id: number;
|
||||
name: string;
|
||||
id: string;
|
||||
fullname: string;
|
||||
email: string;
|
||||
rating: number;
|
||||
status: UserStatus;
|
||||
is_active: boolean;
|
||||
location: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
avatar?: string;
|
||||
phone_number?: string;
|
||||
}
|
||||
|
||||
const mockData: UserType[] = Array.from({ length: 50 }, (_, i) => ({
|
||||
id: `24db9e4d-ca4c-46aa-ac36-8ef04bbe015f`,
|
||||
fullname: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
||||
email: `user${i + 1}@example.com`,
|
||||
is_active: i % 5 !== 0,
|
||||
location: i % 2 === 0 ? 'Jakarta' : 'Bandung',
|
||||
created_at: new Date(Date.now() - i * 86400000).toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
avatar: undefined,
|
||||
phone_number: '+62-812-0000-000',
|
||||
}));
|
||||
|
||||
const columns: ColumnDef<UserType>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
meta: { cellClassName: cn('w-20') },
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={row.getIsSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
accessorKey: 'fullname',
|
||||
header: 'Full Name',
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
header: 'Email',
|
||||
accessorKey: 'email',
|
||||
header: 'Email',
|
||||
},
|
||||
{
|
||||
id: 'rating',
|
||||
header: 'Rating',
|
||||
accessorKey: 'rating',
|
||||
accessorKey: 'location',
|
||||
header: 'Location',
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
accessorKey: 'is_active',
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const statusColors: Record<UserStatus, string> = {
|
||||
active: 'bg-success-200 text-success-500',
|
||||
inactive: 'bg-danger-200 text-danger-500',
|
||||
};
|
||||
const statusText: Record<UserStatus, string> = {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
};
|
||||
return (
|
||||
cell: ({ row }) => (
|
||||
<div
|
||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||
className={cn(
|
||||
'py-2 px-4 text-sm rounded-2xl text-center',
|
||||
row.original.is_active
|
||||
? 'bg-success-200 text-success-700'
|
||||
: 'bg-danger-200 text-danger-700'
|
||||
)}
|
||||
>
|
||||
{statusText[status]}
|
||||
{row.original.is_active ? 'Active' : 'Inactive'}
|
||||
</div>
|
||||
);
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Joined',
|
||||
cell: ({ row }) => new Date(row.original.created_at).toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
@@ -113,17 +96,12 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={
|
||||
(e) => {}
|
||||
// {
|
||||
// e.stopPropagation();
|
||||
// setSelectedUserId(row.original.id);
|
||||
// setShowDetail(true);
|
||||
// }
|
||||
}
|
||||
className="flex items-center gap-2 w-max"
|
||||
onClick={() => {
|
||||
// View detail logic
|
||||
}}
|
||||
>
|
||||
<SearchOutlined className="text-[16px]" /> Lihat Detail & Action
|
||||
<EditOutlined className="text-base" /> View & Manage
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
@@ -161,10 +139,10 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
<select className="border border-neutral-200 rounded-md px-3 py-2 text-label1 w-full sm:w-40">
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="suspended">Suspended</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
</select>
|
||||
<select className="border border-neutral-200 rounded-md px-3 py-2 text-label1 w-full sm:w-40">
|
||||
<option value="all">All City</option>
|
||||
<option value="all">All Location</option>
|
||||
<option value="jakarta">Jakarta</option>
|
||||
<option value="bandung">Bandung</option>
|
||||
</select>
|
||||
@@ -174,19 +152,11 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
<DataTable data={mockData} columns={columns} table={table} />
|
||||
</section>
|
||||
|
||||
{/* Modals extracted into shared backoffice components */}
|
||||
{/* Modals component */}
|
||||
<ModalUserDetail
|
||||
isOpen={showDetailModal}
|
||||
onClose={() => setShowDetailModal(false)}
|
||||
/>
|
||||
<ModalSuspendOrBan
|
||||
isOpen={showSuspendModal}
|
||||
onClose={() => setShowSuspendModal(false)}
|
||||
/>
|
||||
<ModalDeleteUser
|
||||
isOpen={showDeleteModal}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
/>
|
||||
</BackofficeWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ModalDeleteUser: FC<ModalProps> = ({ isOpen, onClose }) => {
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 z-50">
|
||||
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg shadow w-full max-w-md">
|
||||
<div className="border-b px-6 py-4 flex justify-between items-center">
|
||||
<h2 className="text-p3 font-semibold">Delete User</h2>
|
||||
<button className="p-2" onClick={onClose}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<p className="text-label2 text-neutral-700">
|
||||
Are you sure you want to delete this user? This action cannot be
|
||||
undone.
|
||||
</p>
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<button className="px-3 py-2 rounded-md border" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="px-3 py-2 rounded-md bg-red-600 text-white">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalDeleteUser;
|
||||
@@ -1,42 +0,0 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ModalSuspendOrBan: FC<ModalProps> = ({ isOpen, onClose }) => {
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 z-50">
|
||||
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg shadow w-full max-w-lg">
|
||||
<div className="border-b px-6 py-4 flex justify-between items-center">
|
||||
<h2 className="text-p3 font-semibold">Suspend or Ban User</h2>
|
||||
<button className="p-2" onClick={onClose}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 flex flex-col gap-4">
|
||||
<label className="text-label2 text-neutral-700">Reason</label>
|
||||
<textarea
|
||||
className="border border-neutral-200 rounded-md px-3 py-2"
|
||||
rows={4}
|
||||
/>
|
||||
<div className="flex justify-end gap-2 mt-2">
|
||||
<button className="px-3 py-2 rounded-md border" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="px-3 py-2 rounded-md bg-amber-600 text-white">
|
||||
Suspend
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalSuspendOrBan;
|
||||
@@ -0,0 +1,58 @@
|
||||
# Hackathon Backoffice API Contract
|
||||
|
||||
## GET /api/v1/dashboard
|
||||
|
||||
### Purpose
|
||||
|
||||
Single endpoint delivering aggregated metrics for the IMPHNEN x Kolosal.ai Hackathon dashboard.
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
- Requires admin (backoffice) scope: e.g. `role=admin`
|
||||
- 401 if unauthenticated, 403 if authenticated but lacking required scope.
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
GET /api/v1/hackathon/dashboard
|
||||
```
|
||||
|
||||
### Response Schema
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"data": {
|
||||
"total_participants": 1261,
|
||||
"total_teams": 206,
|
||||
"total_submissions": 0 // Total project submitted
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Field Types
|
||||
|
||||
| Path | Type | Notes |
|
||||
| ------------------------- | ------- | --------------------- |
|
||||
| `data.total_participants` | integer | >= 0 |
|
||||
| `data.total_teams` | integer | >= 0 |
|
||||
| `data.total_submissions` | integer | <= `data.total_teams` |
|
||||
|
||||
### Errors
|
||||
|
||||
| Status | Code | Message | Notes |
|
||||
| ------ | ---------------- | ----------------------------- | --------------------- |
|
||||
| 401 | `unauthorized` | `authentication required` | Missing/invalid token |
|
||||
| 403 | `forbidden` | `insufficient permissions` | Lacks required scope |
|
||||
| 429 | `rate_limited` | `too many dashboard requests` | Rate limiting |
|
||||
| 500 | `internal_error` | `unexpected server error` | Unhandled exception |
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- Suggested: 30 requests / minute / admin user.
|
||||
- Return 429 with `Retry-After` header.
|
||||
|
||||
---
|
||||
|
||||
**Revision History**
|
||||
|
||||
- v1.0.0 (2025-11-30): Initial contract drafted.
|
||||
@@ -1,15 +1,16 @@
|
||||
import { cn } from "@imphnen-frontend-service/utils"
|
||||
import { Icon } from '@iconify/react'
|
||||
import { FC, ReactElement, ReactNode } from "react"
|
||||
import { Button } from "../../atoms"
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { FC, ReactElement, ReactNode } from 'react';
|
||||
import { Button } from '../../atoms';
|
||||
import { useAuthStore } from '@imphnen-frontend-service/service';
|
||||
|
||||
export type TBackofficeWrapperProps = {
|
||||
children: ReactNode
|
||||
title?: string
|
||||
className?: string
|
||||
classHeader?: string
|
||||
classTitle?: string
|
||||
}
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
className?: string;
|
||||
classHeader?: string;
|
||||
classTitle?: string;
|
||||
};
|
||||
|
||||
export const BackofficeWrapper: FC<TBackofficeWrapperProps> = ({
|
||||
children,
|
||||
@@ -18,34 +19,52 @@ export const BackofficeWrapper: FC<TBackofficeWrapperProps> = ({
|
||||
classHeader,
|
||||
classTitle,
|
||||
}): ReactElement => {
|
||||
const { session } = useAuthStore();
|
||||
const user = session?.user;
|
||||
|
||||
return (
|
||||
<main className={cn("w-full px-[48px] py-[40px] flex flex-col gap-8", className)}>
|
||||
<header className={cn("bg-white py-5 px-7 rounded-md shadow flex items-center justify-between", classHeader)}>
|
||||
<h1 className={cn("text-[19px] text-primary-500 font-semibold", classTitle)}>{title}</h1>
|
||||
<main
|
||||
className={cn(
|
||||
'w-full px-[48px] py-[40px] flex flex-col gap-8',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<header
|
||||
className={cn(
|
||||
'bg-white py-5 px-7 rounded-md shadow flex items-center justify-between',
|
||||
classHeader
|
||||
)}
|
||||
>
|
||||
<h1
|
||||
className={cn(
|
||||
'text-[19px] text-primary-500 font-semibold',
|
||||
classTitle
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
<div className="flex items-center gap-x-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="max-h-full p-3"
|
||||
>
|
||||
<Button type="button" variant="secondary" className="max-h-full p-3">
|
||||
<Icon icon="mdi:bell-outline" className="size-6" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-x-6">
|
||||
<div className="text-neutral-600 font-medium">
|
||||
<p className="text-p3">Rizal Syaepulloh</p>
|
||||
<p className="text-label1">Super Admin</p>
|
||||
<p className="text-p3">{user?.fullname || 'Full Name'}</p>
|
||||
<p className="text-label1">Admin</p>
|
||||
</div>
|
||||
<div className="size-12 rounded-full overflow-hidden">
|
||||
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="size-full object-cover" />
|
||||
<img
|
||||
src={user?.avatar || '/images/asd687hwq6nds4dfjj2983.webp'}
|
||||
alt="Profile"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
{children}
|
||||
</section>
|
||||
<section>{children}</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user