From 63dfb7299f4d00725f446859cf0bd9657cc45fb7 Mon Sep 17 00:00:00 2001 From: Maulana Sodiqin Date: Sun, 12 Apr 2026 23:01:47 +0700 Subject: [PATCH] feat(backoffice): redesign backoffice UI dengan shadcn/ui Menyeluruh merancang ulang apps/backoffice memakai pola shadcn/ui sambil mempertahankan seluruh palette warna existing (primary/neutral/success/ info/warning/danger) dan typography Bai Jamjuree. UI library (libs/ui): - Atoms existing (Button, Input, Label, Textarea, Card, Dialog, Drawer, Form, Select, Toggle) dirombak ke pola shadcn, API dipertahankan agar sibling apps (gacha, dimentorin, hackathon, qrcampaign) tidak break - Select atom beralih dari native ke Radix Select + NativeSelect fallback - 18 atom baru: Badge, Avatar, Separator, Skeleton, Tabs, Tooltip, DropdownMenu, Popover, AlertDialog, Checkbox, RadioGroup, Switch, ScrollArea, Sheet, Sidebar, Breadcrumb, Table, + use-mobile hook - DataTable, Filter, BackofficeWrapper, Pagination, Modal (alias Dialog) direstyle dengan token baru - CSS variable layer shadcn ditambahkan (--color-primary, --color-sidebar-*, dll) dipetakan ke palette existing; tw-animate-css di-import untuk transisi Backoffice shell: - Sidebar bersarang SidebarProvider + SidebarInset, built-in mobile Sheet, user DropdownMenu (Avatar + logout), ikon lucide-react Pages redesign (19 list/dashboard/settings): - accounts, cms-events, cms-testimonials, dashboard, dashboard-dimentorin (dengan recharts di Card), feedback-review-dimentorin, gacha-roll, hackathon-dashboard, hackathon-submissions, hackathon-teams, hackathon-users, permissions, prizes, roadmap-dimentorin, roles, session-dimentorin, settings-dimentorin (Tabs), transactions, users-dimentorin (Tabs mentor/mentee) - Pola unified: BackofficeWrapper > Card (CardHeader toolbar + CardContent DataTable) + AlertDialog untuk delete confirm - Form accounts_/$id diredesign; form pages lain tetap pakai ControlledInputField yang sekarang berbasis shadcn Input Dependencies: - +20 radix-ui primitives, lucide-react, cmdk, input-otp - Select native patched di dimentorin (profile-sidebar, schedule step) untuk tetap kompatibel via NativeSelect alias Verifikasi: - nx build backoffice: sukses (2.33s, 4801 modules) - nx build dimentorin/gacha/hackathon/qrcampaign: semua sukses (zero regresi dari rewrite libs/ui) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/list-helpers.tsx | 79 + apps/backoffice/src/components/sidebar.tsx | 455 ++--- apps/backoffice/src/index.css | 40 + apps/backoffice/src/routes/_authenticated.tsx | 69 +- .../chart/session-status.tsx | 15 +- .../settings-dimentorin/general.tsx | 2 +- .../settings-dimentorin/payment.tsx | 2 +- .../src/routes/_authenticated/accounts.tsx | 206 +- .../routes/_authenticated/accounts_/$id.tsx | 188 +- .../src/routes/_authenticated/cms-events.tsx | 272 +-- .../_authenticated/cms-testimonials.tsx | 274 +-- .../_authenticated/dashboard-dimentorin.tsx | 336 +-- .../src/routes/_authenticated/dashboard.tsx | 332 ++- .../feedback-review-dimentorin.tsx | 282 +-- .../src/routes/_authenticated/gacha-roll.tsx | 248 +-- .../_authenticated/hackathon-dashboard.tsx | 95 +- .../_authenticated/hackathon-submissions.tsx | 309 ++- .../routes/_authenticated/hackathon-teams.tsx | 460 ++--- .../routes/_authenticated/hackathon-users.tsx | 533 ++--- .../src/routes/_authenticated/permissions.tsx | 243 +-- .../src/routes/_authenticated/prizes.tsx | 288 ++- .../_authenticated/roadmap-dimentorin.tsx | 341 ++-- .../src/routes/_authenticated/roles.tsx | 237 +-- .../_authenticated/session-dimentorin.tsx | 238 ++- .../session-dimentorin_/$id.tsx | 2 +- .../_authenticated/settings-dimentorin.tsx | 116 +- .../routes/_authenticated/transactions.tsx | 267 ++- .../_authenticated/users-dimentorin.tsx | 410 ++-- .../routes/_public/auth/register-mentor.tsx | 2 +- .../modals/appointment/steps/schedule.tsx | 2 +- .../_components/profile/profile-sidebar.tsx | 2 +- components.json | 21 + .../src/atoms/alert-dialog/alert-dialog.tsx | 158 ++ libs/ui/src/atoms/alert-dialog/index.ts | 1 + libs/ui/src/atoms/avatar/avatar.tsx | 52 + libs/ui/src/atoms/avatar/index.ts | 1 + libs/ui/src/atoms/badge/badge.tsx | 52 + libs/ui/src/atoms/badge/index.ts | 1 + libs/ui/src/atoms/breadcrumb/breadcrumb.tsx | 107 + libs/ui/src/atoms/breadcrumb/index.ts | 1 + libs/ui/src/atoms/button/button.tsx | 73 +- libs/ui/src/atoms/checkbox/checkbox.tsx | 39 + libs/ui/src/atoms/checkbox/index.ts | 1 + libs/ui/src/atoms/dialog/dialog.tsx | 4 +- .../src/atoms/dropdown-menu/dropdown-menu.tsx | 274 +++ libs/ui/src/atoms/dropdown-menu/index.ts | 1 + libs/ui/src/atoms/index.ts | 18 +- libs/ui/src/atoms/input/input.tsx | 134 +- libs/ui/src/atoms/popover/index.ts | 1 + libs/ui/src/atoms/popover/popover.tsx | 49 + libs/ui/src/atoms/radio-group/index.ts | 1 + libs/ui/src/atoms/radio-group/radio-group.tsx | 47 + libs/ui/src/atoms/scroll-area/index.ts | 1 + libs/ui/src/atoms/scroll-area/scroll-area.tsx | 57 + libs/ui/src/atoms/select/select.tsx | 270 ++- libs/ui/src/atoms/separator/index.ts | 1 + libs/ui/src/atoms/separator/separator.tsx | 27 + libs/ui/src/atoms/sheet/index.ts | 1 + libs/ui/src/atoms/sheet/sheet.tsx | 140 ++ libs/ui/src/atoms/sidebar/index.ts | 2 + libs/ui/src/atoms/sidebar/sidebar.tsx | 681 +++++++ libs/ui/src/atoms/sidebar/use-mobile.ts | 21 + libs/ui/src/atoms/skeleton/index.ts | 1 + libs/ui/src/atoms/skeleton/skeleton.tsx | 14 + libs/ui/src/atoms/table/index.ts | 1 + libs/ui/src/atoms/table/table.tsx | 115 ++ libs/ui/src/atoms/tabs/index.ts | 1 + libs/ui/src/atoms/tabs/tabs.tsx | 69 + libs/ui/src/atoms/textarea/textarea.tsx | 84 +- libs/ui/src/atoms/toggle/toggle.tsx | 104 +- libs/ui/src/atoms/tooltip/index.ts | 1 + libs/ui/src/atoms/tooltip/tooltip.tsx | 63 + libs/ui/src/index.css | 40 + .../src/molecules/input-field/input-field.tsx | 142 +- libs/ui/src/molecules/modal/modal.tsx | 185 +- .../src/molecules/pagination/pagination.tsx | 180 +- .../molecules/select-field/select-field.tsx | 133 +- .../backoffice-wrapper/backoffice-wrapper.tsx | 151 +- libs/ui/src/organisms/datatable/datatable.tsx | 371 ++-- libs/ui/src/organisms/filter/filter.tsx | 99 +- package-lock.json | 1800 +++++++++++++++++ package.json | 22 + 82 files changed, 7962 insertions(+), 4196 deletions(-) create mode 100644 apps/backoffice/src/components/list-helpers.tsx create mode 100644 components.json create mode 100644 libs/ui/src/atoms/alert-dialog/alert-dialog.tsx create mode 100644 libs/ui/src/atoms/alert-dialog/index.ts create mode 100644 libs/ui/src/atoms/avatar/avatar.tsx create mode 100644 libs/ui/src/atoms/avatar/index.ts create mode 100644 libs/ui/src/atoms/badge/badge.tsx create mode 100644 libs/ui/src/atoms/badge/index.ts create mode 100644 libs/ui/src/atoms/breadcrumb/breadcrumb.tsx create mode 100644 libs/ui/src/atoms/breadcrumb/index.ts create mode 100644 libs/ui/src/atoms/checkbox/checkbox.tsx create mode 100644 libs/ui/src/atoms/checkbox/index.ts create mode 100644 libs/ui/src/atoms/dropdown-menu/dropdown-menu.tsx create mode 100644 libs/ui/src/atoms/dropdown-menu/index.ts create mode 100644 libs/ui/src/atoms/popover/index.ts create mode 100644 libs/ui/src/atoms/popover/popover.tsx create mode 100644 libs/ui/src/atoms/radio-group/index.ts create mode 100644 libs/ui/src/atoms/radio-group/radio-group.tsx create mode 100644 libs/ui/src/atoms/scroll-area/index.ts create mode 100644 libs/ui/src/atoms/scroll-area/scroll-area.tsx create mode 100644 libs/ui/src/atoms/separator/index.ts create mode 100644 libs/ui/src/atoms/separator/separator.tsx create mode 100644 libs/ui/src/atoms/sheet/index.ts create mode 100644 libs/ui/src/atoms/sheet/sheet.tsx create mode 100644 libs/ui/src/atoms/sidebar/index.ts create mode 100644 libs/ui/src/atoms/sidebar/sidebar.tsx create mode 100644 libs/ui/src/atoms/sidebar/use-mobile.ts create mode 100644 libs/ui/src/atoms/skeleton/index.ts create mode 100644 libs/ui/src/atoms/skeleton/skeleton.tsx create mode 100644 libs/ui/src/atoms/table/index.ts create mode 100644 libs/ui/src/atoms/table/table.tsx create mode 100644 libs/ui/src/atoms/tabs/index.ts create mode 100644 libs/ui/src/atoms/tabs/tabs.tsx create mode 100644 libs/ui/src/atoms/tooltip/index.ts create mode 100644 libs/ui/src/atoms/tooltip/tooltip.tsx diff --git a/apps/backoffice/src/components/list-helpers.tsx b/apps/backoffice/src/components/list-helpers.tsx new file mode 100644 index 0000000..ebe8f1b --- /dev/null +++ b/apps/backoffice/src/components/list-helpers.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import type { Row, Table } from '@tanstack/react-table'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + Checkbox, +} from '@imphnen-frontend-service/ui/atoms'; + +/** Renders a header checkbox that toggles all rows (with indeterminate support). */ +export function SelectAllCheckbox({ table }: { table: Table }) { + return ( + + table.toggleAllRowsSelected(!!v && v !== 'indeterminate') + } + aria-label="Select all" + /> + ); +} + +/** Per-row checkbox. */ +export function RowSelectCheckbox({ row }: { row: Row }) { + return ( + row.toggleSelected(!!v)} + aria-label="Select row" + /> + ); +} + +type DeleteConfirmProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: () => void; + title?: string; + description?: string; +}; + +export function DeleteConfirmDialog({ + open, + onOpenChange, + onConfirm, + title = 'Hapus data ini?', + description = 'Tindakan ini tidak dapat dibatalkan. Data akan dihapus permanen.', +}: DeleteConfirmProps) { + return ( + + + + {title} + {description} + + + Batal + + Hapus + + + + + ); +} diff --git a/apps/backoffice/src/components/sidebar.tsx b/apps/backoffice/src/components/sidebar.tsx index c5b8d7e..ff71b4d 100644 --- a/apps/backoffice/src/components/sidebar.tsx +++ b/apps/backoffice/src/components/sidebar.tsx @@ -1,324 +1,233 @@ -import { - AppstoreOutlined, - AuditOutlined, - BookOutlined, - CalendarOutlined, - CommentOutlined, - DownOutlined, - InboxOutlined, - LogoutOutlined, - MessageOutlined, - ReadOutlined, - ReloadOutlined, - RightOutlined, - ScheduleOutlined, - SettingOutlined, - StockOutlined, - UsergroupAddOutlined, - UserOutlined, - UserSwitchOutlined, -} from '@ant-design/icons'; -import { Button } from '@imphnen-frontend-service/ui/atoms'; +'use client'; + import { FC, ReactElement, useState } from 'react'; import { useLocation, useNavigate } from '@tanstack/react-router'; -import { cn, For } from '@imphnen-frontend-service/utils'; -import { useSession } from '@imphnen-frontend-service/service'; +import { + LayoutDashboard, + Users, + UsersRound, + ClipboardCheck, + BookOpen, + MessageSquare, + MessageCircle, + Calendar, + CalendarClock, + Settings, + BarChart3, + RefreshCcw, + Inbox, + UserCog, + UserPlus, + ShieldCheck, + KeyRound, + User, + ChevronDown, + ChevronRight, +} from 'lucide-react'; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from '@imphnen-frontend-service/ui/atoms'; +import { cn } from '@imphnen-frontend-service/utils'; -type MenuItem = { +type MenuLink = { label: string; - href?: string; - icon?: ReactElement; - children?: Array<{ label: string; href: string; icon?: ReactElement }>; + href: string; + icon: React.ComponentType<{ className?: string }>; }; +type MenuGroup = { + label: string; + icon: React.ComponentType<{ className?: string }>; + children: MenuLink[]; +}; + +type MenuItem = MenuLink | MenuGroup; + const MENUS: MenuItem[] = [ { label: 'Hackathon', - icon: , + icon: BarChart3, children: [ - { - label: 'Dashboard', - href: '/hackathon-dashboard', - icon: , - }, - { - label: 'Users', - href: '/hackathon-users', - icon: , - }, - { - label: 'Teams', - href: '/hackathon-teams', - icon: , - }, - { - label: 'Submissions', - href: '/hackathon-submissions', - icon: , - }, + { label: 'Dashboard', href: '/hackathon-dashboard', icon: LayoutDashboard }, + { label: 'Users', href: '/hackathon-users', icon: Users }, + { label: 'Teams', href: '/hackathon-teams', icon: UsersRound }, + { label: 'Submissions', href: '/hackathon-submissions', icon: ClipboardCheck }, ], }, { label: 'Dimentorin', - icon: , + icon: BookOpen, children: [ - { - label: 'Dashboard - Dimentorin', - href: '/dashboard-dimentorin', - icon: , - }, - { - label: 'User - Dimentorin', - href: '/users-dimentorin', - icon: , - }, - { - label: 'Session - Dimentorin', - href: '/session-dimentorin', - icon: , - }, - { - label: 'Content & Roadmap', - href: '/roadmap-dimentorin', - icon: , - }, - { - label: 'Feedback & Review', - href: '/feedback-review-dimentorin', - icon: , - }, - { - label: 'Settings - Dimentorin', - href: '/settings-dimentorin', - icon: , - }, + { label: 'Dashboard', href: '/dashboard-dimentorin', icon: LayoutDashboard }, + { label: 'Users', href: '/users-dimentorin', icon: UserCog }, + { label: 'Session', href: '/session-dimentorin', icon: CalendarClock }, + { label: 'Content & Roadmap', href: '/roadmap-dimentorin', icon: BookOpen }, + { label: 'Feedback & Review', href: '/feedback-review-dimentorin', icon: MessageSquare }, + { label: 'Settings', href: '/settings-dimentorin', icon: Settings }, ], }, { label: 'Gacha', - icon: , + icon: RefreshCcw, children: [ - { - label: 'Dashboard & Set Gacha', - href: '/dashboard', - icon: , - }, - { - label: 'Gacha Roll', - href: '/gacha-roll', - icon: , - }, - { - label: 'Validasi Transaksi', - href: '/transactions', - icon: , - }, - { - label: 'Data Pengiriman Hadiah', - href: '/prizes', - icon: , - }, + { label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }, + { label: 'Gacha Roll', href: '/gacha-roll', icon: RefreshCcw }, + { label: 'Validasi Transaksi', href: '/transactions', icon: ClipboardCheck }, + { label: 'Data Pengiriman', href: '/prizes', icon: Inbox }, ], }, { label: 'CMS', - icon: , + icon: BookOpen, children: [ - { - label: 'Events', - href: '/cms-events', - icon: , - }, - { - label: 'Testimonials', - href: '/cms-testimonials', - icon: , - }, + { label: 'Events', href: '/cms-events', icon: Calendar }, + { label: 'Testimonials', href: '/cms-testimonials', icon: MessageCircle }, ], }, - { - label: 'Permissions', - href: '/permissions', - icon: , - }, - { - label: 'Roles', - href: '/roles', - icon: , - }, - { - label: 'Data Akun', - href: '/accounts', - icon: , - }, ]; -interface SidebarProps { - isOpen?: boolean; - onClose?: () => void; -} +const FLAT_MENUS: MenuLink[] = [ + { label: 'Permissions', href: '/permissions', icon: ShieldCheck }, + { label: 'Roles', href: '/roles', icon: KeyRound }, + { label: 'Data Akun', href: '/accounts', icon: User }, +]; -export const BackofficeSidebar: FC = ({ - isOpen = false, - onClose, -}): ReactElement => { - const { signOut } = useSession(); +const isMenuGroup = (item: MenuItem): item is MenuGroup => + (item as MenuGroup).children !== undefined; + +export const BackofficeSidebar: FC = (): ReactElement => { const location = useLocation(); const navigate = useNavigate(); + + const isActive = (path: string) => { + if (path === '/dashboard' && location.pathname === '/dashboard-dimentorin') { + return false; + } + return location.pathname.includes(path); + }; + const [openGroups, setOpenGroups] = useState>(() => { const initial: Record = {}; MENUS.forEach((menu) => { - if (menu.children?.some((child) => child.href && location.pathname.includes(child.href))) { + if ( + isMenuGroup(menu) && + menu.children.some((child) => isActive(child.href)) + ) { initial[menu.label] = true; } }); return initial; }); - const isActive = (path: string) => { - if (path === '/dashboard' && location.pathname === '/dashboard-dimentorin') - return false; - return location.pathname.includes(path); - }; - const toggleGroup = (groupLabel: string) => { - setOpenGroups((prev) => ({ ...prev, [groupLabel]: !prev[groupLabel] })); - }; + const toggleGroup = (label: string) => + setOpenGroups((prev) => ({ ...prev, [label]: !prev[label] })); - const sidebarContent = ( -
-
-
+ return ( + + +
IMPHNEN Logo - {onClose && ( - - )}
- - -
- -
-
- -
-
- ); - - return ( - <> -
- {sidebarContent} -
- - {isOpen && ( -
-
-
- {sidebarContent} -
-
- )} - + + {isMenuGroup(menu) && open && ( + + {menu.children.map((child) => { + const ChildIcon = child.icon; + return ( + + + + + + ); + })} + + )} + + ); + })} + + + + + System + + + {FLAT_MENUS.map((menu) => { + const Icon = menu.icon; + return ( + + navigate({ to: menu.href })} + > + + {menu.label} + + + ); + })} + + + + + ); }; diff --git a/apps/backoffice/src/index.css b/apps/backoffice/src/index.css index fd9d7f2..211a328 100644 --- a/apps/backoffice/src/index.css +++ b/apps/backoffice/src/index.css @@ -1,5 +1,6 @@ @import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap'); @import 'tailwindcss'; +@import 'tw-animate-css'; @source "../../../libs/ui/**/*.{ts,tsx}"; @theme { @@ -96,6 +97,45 @@ /* ~10px */ --text-label3: 0.677rem; /* ~8px */ + + /* shadcn/ui semantic tokens — aliased to existing palette (keep colors consistent) */ + --color-background: #ffffff; + --color-foreground: #3d3d3d; + --color-card: #ffffff; + --color-card-foreground: #3d3d3d; + --color-popover: #ffffff; + --color-popover-foreground: #3d3d3d; + --color-primary: #23a1eb; + --color-primary-foreground: #ffffff; + --color-secondary: #e7e7e7; + --color-secondary-foreground: #3d3d3d; + --color-muted: #f6f6f6; + --color-muted-foreground: #6d6d6d; + --color-accent: #e1f0fd; + --color-accent-foreground: #085f9c; + --color-destructive: #ff5242; + --color-destructive-foreground: #ffffff; + --color-border: #d1d1d1; + --color-input: #d1d1d1; + --color-ring: #3eb0f2; + + --color-sidebar: #ffffff; + --color-sidebar-foreground: #4f4f4f; + --color-sidebar-primary: #23a1eb; + --color-sidebar-primary-foreground: #ffffff; + --color-sidebar-accent: #f0f8ff; + --color-sidebar-accent-foreground: #085f9c; + --color-sidebar-border: #e7e7e7; + --color-sidebar-ring: #3eb0f2; + + --color-chart-1: #23a1eb; + --color-chart-2: #35ba43; + --color-chart-3: #04acf3; + --color-chart-4: #ffed27; + --color-chart-5: #ff5242; + + --radius: 8px; + --font-sans: 'Bai Jamjuree', sans-serif; } @layer base { diff --git a/apps/backoffice/src/routes/_authenticated.tsx b/apps/backoffice/src/routes/_authenticated.tsx index 673b7f0..a2135ac 100644 --- a/apps/backoffice/src/routes/_authenticated.tsx +++ b/apps/backoffice/src/routes/_authenticated.tsx @@ -1,63 +1,28 @@ -import { createFileRoute, Outlet, redirect } from '@tanstack/react-router' -import { SessionToken } from '@imphnen-frontend-service/service' -import { useState } from 'react' -import { BackofficeSidebar } from '../components/sidebar' +import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'; +import { SessionToken } from '@imphnen-frontend-service/service'; +import { + SidebarInset, + SidebarProvider, +} from '@imphnen-frontend-service/ui/atoms'; +import { BackofficeSidebar } from '../components/sidebar'; export const Route = createFileRoute('/_authenticated')({ beforeLoad: () => { - const session = SessionToken.get() + const session = SessionToken.get(); if (!session?.token?.access_token) { - throw redirect({ to: '/auth/login' }) + throw redirect({ to: '/auth/login' }); } }, component: AuthenticatedLayout, -}) +}); function AuthenticatedLayout() { - const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) - return ( -
-
- setMobileSidebarOpen(false)} - /> -
-
- -

- IMPHNEN Backoffice -

-
- - -
-
-
- ) + + + + + + + ); } diff --git a/apps/backoffice/src/routes/_authenticated/_components/dashboard-dimentorin/chart/session-status.tsx b/apps/backoffice/src/routes/_authenticated/_components/dashboard-dimentorin/chart/session-status.tsx index e0bfcd0..572dbda 100644 --- a/apps/backoffice/src/routes/_authenticated/_components/dashboard-dimentorin/chart/session-status.tsx +++ b/apps/backoffice/src/routes/_authenticated/_components/dashboard-dimentorin/chart/session-status.tsx @@ -1,5 +1,4 @@ import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts" -import { PieLabelProps } from "recharts/types/polar/Pie" type ChartProps = { name: string @@ -14,14 +13,20 @@ const chartData: ChartProps[] = [ ] const RADIAN = Math.PI / 180; -const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent }: PieLabelProps) => { +const renderCustomizedLabel = (props: any) => { + const cx = props.cx ?? 0 + const cy = props.cy ?? 0 + const midAngle = props.midAngle ?? 0 + const innerRadius = props.innerRadius ?? 0 + const outerRadius = props.outerRadius ?? 0 + const percent = props.percent ?? 0 const radius = innerRadius + (outerRadius - innerRadius) * 0.5; - const x = cx + radius * Math.cos(-(midAngle ?? 0) * RADIAN); - const y = cy + radius * Math.sin(-(midAngle ?? 0) * RADIAN); + const x = cx + radius * Math.cos(-midAngle * RADIAN); + const y = cy + radius * Math.sin(-midAngle * RADIAN); return ( cx ? 'start' : 'end'} dominantBaseline="central"> - {`${((percent ?? 1) * 100).toFixed(0)}%`} + {`${(percent * 100).toFixed(0)}%`} ); }; diff --git a/apps/backoffice/src/routes/_authenticated/_components/settings-dimentorin/general.tsx b/apps/backoffice/src/routes/_authenticated/_components/settings-dimentorin/general.tsx index 69ebee8..016ae4a 100644 --- a/apps/backoffice/src/routes/_authenticated/_components/settings-dimentorin/general.tsx +++ b/apps/backoffice/src/routes/_authenticated/_components/settings-dimentorin/general.tsx @@ -1,4 +1,4 @@ -import { Button, Input, Select, ToggleInput } from "@imphnen-frontend-service/ui/atoms" +import { Button, Input, NativeSelect as Select, ToggleInput } from "@imphnen-frontend-service/ui/atoms" import { cn } from "@imphnen-frontend-service/utils" import { FC } from "react" diff --git a/apps/backoffice/src/routes/_authenticated/_components/settings-dimentorin/payment.tsx b/apps/backoffice/src/routes/_authenticated/_components/settings-dimentorin/payment.tsx index a2d3099..8e74b31 100644 --- a/apps/backoffice/src/routes/_authenticated/_components/settings-dimentorin/payment.tsx +++ b/apps/backoffice/src/routes/_authenticated/_components/settings-dimentorin/payment.tsx @@ -1,4 +1,4 @@ -import { Button, Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms" +import { Button, Input, NativeSelect as Select, Textarea } from "@imphnen-frontend-service/ui/atoms" import { cn } from "@imphnen-frontend-service/utils" import { FC } from "react" diff --git a/apps/backoffice/src/routes/_authenticated/accounts.tsx b/apps/backoffice/src/routes/_authenticated/accounts.tsx index fba1fad..e02b142 100644 --- a/apps/backoffice/src/routes/_authenticated/accounts.tsx +++ b/apps/backoffice/src/routes/_authenticated/accounts.tsx @@ -1,13 +1,23 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' -import * as React from 'react' -import { Fragment, useState } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { Filter as FilterIcon, Search, Pencil } from 'lucide-react'; import { - FilterOutlined, - SearchOutlined, - EditOutlined, -} from '@ant-design/icons' -import { Button, Input } from '@imphnen-frontend-service/ui/atoms' -import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms' + Button, + Card, + CardContent, + CardHeader, + Checkbox, + Input, + Popover, + PopoverContent, + PopoverTrigger, + Badge, +} from '@imphnen-frontend-service/ui/atoms'; +import { + DataTable, + Filter, + BackofficeWrapper, +} from '@imphnen-frontend-service/ui/organisms'; import { ColumnDef, getCoreRowModel, @@ -15,107 +25,96 @@ import { PaginationState, useReactTable, RowSelectionState, -} from '@tanstack/react-table' +} from '@tanstack/react-table'; import { useUserList, TUsersListItem, -} from '@imphnen-frontend-service/service' +} from '@imphnen-frontend-service/service'; export const Route = createFileRoute('/_authenticated/accounts')({ component: AccountsPage, -}) +}); function AccountsPage() { - const navigate = useNavigate() - const [search, setSearch] = useState('') - + const navigate = useNavigate(); + const [search, setSearch] = React.useState(''); const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 9, - }) - - const [rowSelection, setRowSelection] = React.useState({}) - const [showFilter, setShowFilter] = useState(false) + pageSize: 10, + }); + const [rowSelection, setRowSelection] = + React.useState({}); + const [showFilter, setShowFilter] = React.useState(false); const { data: usersData, isLoading } = useUserList({ search, page: pagination.pageIndex + 1, per_page: pagination.pageSize, - }) + }); - const users: TUsersListItem[] = usersData?.data ?? [] - const totalItems = usersData?.meta?.total ?? users.length + const users: TUsersListItem[] = usersData?.data ?? []; + const totalItems = usersData?.meta?.total ?? users.length; const columns: ColumnDef[] = [ { id: 'select', header: ({ table }) => ( - + table.toggleAllRowsSelected(!!v && v !== 'indeterminate') + } + aria-label="Select all" /> ), cell: ({ row }) => ( - row.toggleSelected(!!v)} + aria-label="Select row" /> ), }, - { - header: 'No', - accessorKey: 'id', - }, - { - header: 'Nama Lengkap', - accessorKey: 'fullname', - }, - { - header: 'Email', - accessorKey: 'email', - }, - { - header: 'Role', - accessorKey: 'role', - }, + { header: 'No', accessorKey: 'id' }, + { header: 'Nama Lengkap', accessorKey: 'fullname' }, + { header: 'Email', accessorKey: 'email' }, + { header: 'Role', accessorKey: 'role' }, { header: 'Status', accessorKey: 'is_active', cell: ({ row }) => ( - + {row.original.is_active ? 'Aktif' : 'Tidak Aktif'} - + ), }, { header: 'Action', cell: ({ row }) => ( ), }, - ] + ]; const table = useReactTable({ data: users, columns, - state: { - pagination, - rowSelection, - }, + state: { pagination, rowSelection }, enableRowSelection: true, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), @@ -123,52 +122,65 @@ function AccountsPage() { onPaginationChange: setPagination, pageCount: Math.ceil(totalItems / pagination.pageSize), manualPagination: true, - }) + }); return ( - -
-
-

Data Akun

-
-
-
-
+ + + +
+
+ setSearch(e.target.value)} /> -
- -
-
-
- - {showFilter && ( -
- setShowFilter(false)} options={[]} /> -
- )}
+ + + + + + setShowFilter(false)} + options={[ + { id: 'all', value: 'all', label: 'Semua' }, + { id: 'active', value: 'active', label: 'Aktif' }, + { id: 'inactive', value: 'inactive', label: 'Tidak Aktif' }, + ]} + /> + +
+
+ {isLoading ? ( -
Loading...
+
+ Memuat data… +
) : ( - + + setPagination((prev) => ({ ...prev, pageIndex: p - 1 })) + } + /> )} -
-
-
- ) + + + + ); } diff --git a/apps/backoffice/src/routes/_authenticated/accounts_/$id.tsx b/apps/backoffice/src/routes/_authenticated/accounts_/$id.tsx index dc6317d..1f91525 100644 --- a/apps/backoffice/src/routes/_authenticated/accounts_/$id.tsx +++ b/apps/backoffice/src/routes/_authenticated/accounts_/$id.tsx @@ -1,110 +1,122 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' -import { useEffect, useState } from 'react' -import { toast } from 'sonner' -import { ArrowLeftOutlined } from '@ant-design/icons' -import { Button } from '@imphnen-frontend-service/ui/atoms' -import { InputField } from '@imphnen-frontend-service/ui/molecules' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { toast } from 'sonner'; +import { ArrowLeft } from 'lucide-react'; +import { + Button, + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '@imphnen-frontend-service/ui/atoms'; +import { InputField } from '@imphnen-frontend-service/ui/molecules'; +import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'; import { useUserList, useUpdateUserById, -} from '@imphnen-frontend-service/service' +} from '@imphnen-frontend-service/service'; export const Route = createFileRoute('/_authenticated/accounts_/$id')({ component: AccountsEditPage, -}) +}); function AccountsEditPage() { - const { id } = Route.useParams() - const navigate = useNavigate() - const updateUser = useUpdateUserById() + const { id } = Route.useParams(); + const navigate = useNavigate(); + const updateUser = useUpdateUserById(); - const { data: usersData, isLoading } = useUserList({ search: '', per_page: 100 }) - const user = usersData?.data?.find((u) => u.id === id) + const { data: usersData, isLoading } = useUserList({ + search: '', + per_page: 100, + }); + const user = usersData?.data?.find((u) => u.id === id); - const [fullName, setFullName] = useState('') - const [email, setEmail] = useState('') + const [fullName, setFullName] = React.useState(''); + const [email, setEmail] = React.useState(''); - useEffect(() => { + React.useEffect(() => { if (user) { - setFullName(user.fullname) - setEmail(user.email) + setFullName(user.fullname); + setEmail(user.email); } - }, [user]) + }, [user]); const handleSubmit = async () => { try { - await updateUser.mutateAsync({ id, data: { fullname: fullName, email } }) - toast.success('Data akun berhasil diperbarui') - navigate({ to: '/accounts' }) + await updateUser.mutateAsync({ + id, + data: { fullname: fullName, email }, + }); + toast.success('Data akun berhasil diperbarui'); + navigate({ to: '/accounts' }); } catch (error) { - console.log(error) - toast.error('Data akun gagal diperbarui') + console.log(error); + toast.error('Data akun gagal diperbarui'); } - } - - if (isLoading) { - return ( -
-
Loading...
-
- ) - } + }; return ( -
-
-
- -

Edit Data Akun

-
- -
-
- setFullName(e.target.value)} - size="lg" - className="w-full" - /> - setEmail(e.target.value)} - size="lg" - className="w-full" - /> - -
- - -
-
-
+ +
+ + + + Edit Data Akun + + + {isLoading ? ( +
+ Memuat data… +
+ ) : ( +
+ setFullName(e.target.value)} + size="md" + /> + setEmail(e.target.value)} + size="md" + /> +
+ )} +
+ + + + +
-
- ) + + ); } diff --git a/apps/backoffice/src/routes/_authenticated/cms-events.tsx b/apps/backoffice/src/routes/_authenticated/cms-events.tsx index 99563ad..4fe76d6 100644 --- a/apps/backoffice/src/routes/_authenticated/cms-events.tsx +++ b/apps/backoffice/src/routes/_authenticated/cms-events.tsx @@ -1,13 +1,27 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' -import { Fragment, useState } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { Search, Pencil, Trash2, Plus } from 'lucide-react'; import { - SearchOutlined, - EditOutlined, - DeleteOutlined, - PlusOutlined, -} from '@ant-design/icons' -import { Button, Input } from '@imphnen-frontend-service/ui/atoms' -import { DataTable } from '@imphnen-frontend-service/ui/organisms' + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + Badge, + Button, + Card, + CardContent, + CardHeader, + Checkbox, + Input, +} from '@imphnen-frontend-service/ui/atoms'; +import { + DataTable, + BackofficeWrapper, +} from '@imphnen-frontend-service/ui/organisms'; import { ColumnDef, getCoreRowModel, @@ -15,76 +29,77 @@ import { PaginationState, RowSelectionState, useReactTable, -} from '@tanstack/react-table' +} from '@tanstack/react-table'; import { useEventList, useDeleteEvent, TEventsListItem, -} from '@imphnen-frontend-service/service' -import React from 'react' -import { toast } from 'sonner' +} from '@imphnen-frontend-service/service'; +import { toast } from 'sonner'; export const Route = createFileRoute('/_authenticated/cms-events')({ component: CmsEventsPage, -}) +}); function CmsEventsPage() { - const navigate = useNavigate() - const [search, setSearch] = useState('') - const [deleteId, setDeleteId] = useState(null) - + const navigate = useNavigate(); + const [search, setSearch] = React.useState(''); + const [deleteId, setDeleteId] = React.useState(null); const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 9, - }) - - const [rowSelection, setRowSelection] = React.useState({}) + pageSize: 10, + }); + const [rowSelection, setRowSelection] = + React.useState({}); const { data: eventsData, isLoading } = useEventList({ search, page: pagination.pageIndex + 1, per_page: pagination.pageSize, - }) - const deleteEvent = useDeleteEvent() + }); + const deleteEvent = useDeleteEvent(); - const events: TEventsListItem[] = eventsData?.data ?? [] - const totalItems = eventsData?.meta?.total ?? events.length + const events: TEventsListItem[] = eventsData?.data ?? []; + const totalItems = eventsData?.meta?.total ?? events.length; const handleDelete = async (id: string) => { try { - await deleteEvent.mutateAsync(id) - toast.success('Data event berhasil dihapus') - setDeleteId(null) + await deleteEvent.mutateAsync(id); + toast.success('Data event berhasil dihapus'); + setDeleteId(null); } catch (error) { - console.log(error) - toast.error('Data event gagal dihapus') + console.log(error); + toast.error('Data event gagal dihapus'); } - } + }; const columns: ColumnDef[] = [ { id: 'select', header: ({ table }) => ( - + table.toggleAllRowsSelected(!!v && v !== 'indeterminate') + } + aria-label="Select all" /> ), cell: ({ row }) => ( - row.toggleSelected(!!v)} + aria-label="Select row" /> ), }, - { - header: 'Name', - accessorKey: 'name', - }, + { header: 'Name', accessorKey: 'name' }, { header: 'Location', accessorKey: 'location', @@ -112,81 +127,49 @@ function CmsEventsPage() { header: 'Online', accessorKey: 'is_online', cell: ({ row }) => ( - + {row.original.is_online ? 'Online' : 'Offline'} - + ), }, { header: 'Action', cell: ({ row }) => ( -
+
+ - {deleteId === row.original.id ? ( -
- Yakin? - - -
- ) : ( - - )}
), }, - ] + ]; const table = useReactTable({ data: events, columns, - state: { - pagination, - rowSelection, - }, + state: { pagination, rowSelection }, enableRowSelection: true, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), @@ -194,53 +177,74 @@ function CmsEventsPage() { onPaginationChange: setPagination, pageCount: Math.ceil(totalItems / pagination.pageSize), manualPagination: true, - }) + }); return ( - -
-
-

CMS Events

-
- -
-
-
+ + + +
+
+ setSearch(e.target.value)} /> -
- -
-
-
-
+
- +
+ {isLoading ? ( -
Loading...
+
+ Memuat data… +
) : ( + setPagination((prev) => ({ ...prev, pageIndex: p - 1 })) + } /> )} -
-
-
- ) + + + + !o && setDeleteId(null)} + > + + + Hapus event ini? + + Tindakan ini tidak dapat dibatalkan. Event akan dihapus permanen. + + + + Batal + deleteId && handleDelete(deleteId)} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Hapus + + + + + + ); } diff --git a/apps/backoffice/src/routes/_authenticated/cms-testimonials.tsx b/apps/backoffice/src/routes/_authenticated/cms-testimonials.tsx index 6702a6d..edf0244 100644 --- a/apps/backoffice/src/routes/_authenticated/cms-testimonials.tsx +++ b/apps/backoffice/src/routes/_authenticated/cms-testimonials.tsx @@ -1,13 +1,26 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' -import { Fragment, useState } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { Search, Pencil, Trash2, Plus } from 'lucide-react'; import { - SearchOutlined, - EditOutlined, - DeleteOutlined, - PlusOutlined, -} from '@ant-design/icons' -import { Button, Input } from '@imphnen-frontend-service/ui/atoms' -import { DataTable } from '@imphnen-frontend-service/ui/organisms' + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + Button, + Card, + CardContent, + CardHeader, + Checkbox, + Input, +} from '@imphnen-frontend-service/ui/atoms'; +import { + DataTable, + BackofficeWrapper, +} from '@imphnen-frontend-service/ui/organisms'; import { ColumnDef, getCoreRowModel, @@ -15,86 +28,84 @@ import { PaginationState, RowSelectionState, useReactTable, -} from '@tanstack/react-table' +} from '@tanstack/react-table'; import { useTestimonialList, useDeleteTestimonial, TTestimonialsListItem, -} from '@imphnen-frontend-service/service' -import React from 'react' -import { toast } from 'sonner' +} from '@imphnen-frontend-service/service'; +import { toast } from 'sonner'; export const Route = createFileRoute('/_authenticated/cms-testimonials')({ component: CmsTestimonialsPage, -}) +}); function CmsTestimonialsPage() { - const navigate = useNavigate() - const [search, setSearch] = useState('') - const [deleteId, setDeleteId] = useState(null) - + const navigate = useNavigate(); + const [search, setSearch] = React.useState(''); + const [deleteId, setDeleteId] = React.useState(null); const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 9, - }) - - const [rowSelection, setRowSelection] = React.useState({}) + pageSize: 10, + }); + const [rowSelection, setRowSelection] = + React.useState({}); const { data: testimonialsData, isLoading } = useTestimonialList({ search, page: pagination.pageIndex + 1, per_page: pagination.pageSize, - }) - const deleteTestimonial = useDeleteTestimonial() + }); + const deleteTestimonial = useDeleteTestimonial(); - const testimonials: TTestimonialsListItem[] = testimonialsData?.data ?? [] - const totalItems = testimonialsData?.meta?.total ?? testimonials.length + const testimonials: TTestimonialsListItem[] = testimonialsData?.data ?? []; + const totalItems = testimonialsData?.meta?.total ?? testimonials.length; const handleDelete = async (id: string) => { try { - await deleteTestimonial.mutateAsync(id) - toast.success('Data testimonial berhasil dihapus') - setDeleteId(null) + await deleteTestimonial.mutateAsync(id); + toast.success('Data testimonial berhasil dihapus'); + setDeleteId(null); } catch (error) { - console.log(error) - toast.error('Data testimonial gagal dihapus') + console.log(error); + toast.error('Data testimonial gagal dihapus'); } - } + }; const columns: ColumnDef[] = [ { id: 'select', header: ({ table }) => ( - + table.toggleAllRowsSelected(!!v && v !== 'indeterminate') + } /> ), cell: ({ row }) => ( - row.toggleSelected(!!v)} /> ), }, - { - header: 'User', - accessorKey: 'user_fullname', - }, - { - header: 'Role', - accessorKey: 'role', - }, + { header: 'User', accessorKey: 'user_fullname' }, + { header: 'Role', accessorKey: 'role' }, { header: 'Content', accessorKey: 'content', cell: ({ row }) => { - const content = row.original.content - return content.length > 80 ? `${content.substring(0, 80)}...` : content + const content = row.original.content; + return content.length > 80 + ? `${content.substring(0, 80)}…` + : content; }, }, { @@ -110,67 +121,41 @@ function CmsTestimonialsPage() { { header: 'Action', cell: ({ row }) => ( -
+
+ - {deleteId === row.original.id ? ( -
- Yakin? - - -
- ) : ( - - )}
), }, - ] + ]; const table = useReactTable({ data: testimonials, columns, - state: { - pagination, - rowSelection, - }, + state: { pagination, rowSelection }, enableRowSelection: true, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), @@ -178,53 +163,78 @@ function CmsTestimonialsPage() { onPaginationChange: setPagination, pageCount: Math.ceil(totalItems / pagination.pageSize), manualPagination: true, - }) + }); return ( - -
-
-

CMS Testimonials

-
- -
-
-
+ + + +
+
+ setSearch(e.target.value)} /> -
- -
-
-
-
+
- +
+ {isLoading ? ( -
Loading...
+
+ Memuat data… +
) : ( + setPagination((prev) => ({ ...prev, pageIndex: p - 1 })) + } /> )} -
-
-
- ) + + + + !o && setDeleteId(null)} + > + + + Hapus testimonial ini? + + Tindakan ini tidak dapat dibatalkan. Testimonial akan dihapus + permanen. + + + + Batal + deleteId && handleDelete(deleteId)} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Hapus + + + + + + ); } diff --git a/apps/backoffice/src/routes/_authenticated/dashboard-dimentorin.tsx b/apps/backoffice/src/routes/_authenticated/dashboard-dimentorin.tsx index 0d00b8d..c14795f 100644 --- a/apps/backoffice/src/routes/_authenticated/dashboard-dimentorin.tsx +++ b/apps/backoffice/src/routes/_authenticated/dashboard-dimentorin.tsx @@ -1,152 +1,212 @@ -import { createFileRoute } from '@tanstack/react-router' -import { Button } from '@imphnen-frontend-service/ui/atoms' -import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms' -import { For } from '@imphnen-frontend-service/utils' -import { ReactElement } from 'react' -import { UserGrowthChart } from './_components/dashboard-dimentorin/chart/user-growth' -import { SessionStatusChart } from './_components/dashboard-dimentorin/chart/session-status' -import { useMentorList, useUserList, useMySessions } from '@imphnen-frontend-service/service' +import { createFileRoute } from '@tanstack/react-router'; +import * as React from 'react'; +import { Users, UserCog, CalendarClock, Activity, CircleCheck } from 'lucide-react'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@imphnen-frontend-service/ui/atoms'; +import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'; +import { UserGrowthChart } from './_components/dashboard-dimentorin/chart/user-growth'; +import { SessionStatusChart } from './_components/dashboard-dimentorin/chart/session-status'; +import { + useMentorList, + useUserList, + useMySessions, +} from '@imphnen-frontend-service/service'; export const Route = createFileRoute('/_authenticated/dashboard-dimentorin')({ component: DashboardDimentorinPage, -}) +}); -function DashboardDimentorinPage(): ReactElement { - const { data: mentorData } = useMentorList({ per_page: 5, sort_by: 'rating', order: 'desc' }) - const { data: userData } = useUserList({ per_page: 1 }) - const { data: sessionsData } = useMySessions() +type StatCardProps = { + icon: React.ComponentType<{ className?: string }>; + label: string; + value: React.ReactNode; +}; - const totalMentors = mentorData?.meta?.total ?? 0 - const totalUsers = userData?.meta?.total ?? 0 - const totalSessions = sessionsData?.total ?? 0 - const topMentors = mentorData?.data ?? [] +function StatCard({ icon: Icon, label, value }: StatCardProps) { + return ( + + +
+ +
+
+ + {value} + + {label} +
+
+
+ ); +} - const overviewStats = [ - { label: 'Total Users', value: totalUsers }, - { label: 'Total Mentors', value: totalMentors }, - { label: 'Total Sessions', value: totalSessions }, - { label: 'Active Mentors', value: topMentors.filter((m) => m.status === 'active').length }, - { label: 'Completed Sessions', value: sessionsData?.sessions?.filter((s) => s.status === 'completed').length ?? 0 }, - ] +function DashboardDimentorinPage() { + const { data: mentorData } = useMentorList({ + per_page: 5, + sort_by: 'rating', + order: 'desc', + }); + const { data: userData } = useUserList({ per_page: 1 }); + const { data: sessionsData } = useMySessions(); + + const totalMentors = mentorData?.meta?.total ?? 0; + const totalUsers = userData?.meta?.total ?? 0; + const totalSessions = sessionsData?.total ?? 0; + const topMentors = mentorData?.data ?? []; + const activeMentors = topMentors.filter((m) => m.status === 'active').length; + const completedSessions = + sessionsData?.sessions?.filter((s) => s.status === 'completed').length ?? 0; + + const topTopics = React.useMemo(() => { + const sessions = sessionsData?.sessions ?? []; + const topicCount: Record = {}; + sessions.forEach((s) => { + topicCount[s.topic] = (topicCount[s.topic] ?? 0) + 1; + }); + return Object.entries(topicCount) + .sort(([, a], [, b]) => b - a) + .slice(0, 5); + }, [sessionsData]); return ( - -

Overview

+ +
+ + + + + +
-
-
- +
+ + + User Growth + Pertumbuhan user dari waktu ke waktu + + + + + + + + Session Status + Distribusi status sesi + + + + + +
-
- - {(stat, index) => ( -
-

{stat.value}

-

{stat.label}

-
- )} -
-
-
- -
- - -
-
-
-

User Growth

-
-
- -
-
-

Session Status

- -
-
-
- -
-
-

Top 5 Mentors

- -
- - - - - - - - - - - {topMentors.slice(0, 5).map((mentor, index) => ( - - - - - - ))} - {topMentors.length === 0 && ( - - - - )} - -
No.Nama LengkapAvg Rating
{index + 1}{mentor.fullname ?? '-'}{mentor.rating?.toFixed(1) ?? '-'}
Belum ada data
-
-
- -
-

Top Booked Mentoring Topics

- -
- - - - - - - - - - - {(() => { - const sessions = sessionsData?.sessions ?? [] - const topicCount: Record = {} - sessions.forEach((s) => { - topicCount[s.topic] = (topicCount[s.topic] ?? 0) + 1 - }) - const topTopics = Object.entries(topicCount) - .sort(([, a], [, b]) => b - a) - .slice(0, 5) - if (topTopics.length === 0) { - return ( - - - - ) - } - return topTopics.map(([topic, count], index) => ( - - - - - +
+ + + Top 5 Mentors + Mentor dengan rating tertinggi + + +
+
No.TopikTotal Sesi
Belum ada data
{index + 1}{topic}{count}
+ + + No. + Nama Lengkap + Avg Rating + + + + {topMentors.length === 0 ? ( + + + Belum ada data + + + ) : ( + topMentors.slice(0, 5).map((mentor, index) => ( + + {index + 1} + {mentor.fullname ?? '-'} + + {mentor.rating?.toFixed(1) ?? '-'} + + )) - })()} - -
+ )} + +
-
-
-
+ + + + + Top Booked Topics + Topik mentoring paling banyak dibooking + + +
+ + + + No. + Topik + Total Sesi + + + + {topTopics.length === 0 ? ( + + + Belum ada data + + + ) : ( + topTopics.map(([topic, count], index) => ( + + {index + 1} + {topic} + {count} + + )) + )} + +
+
+
+
+
- ) + ); } diff --git a/apps/backoffice/src/routes/_authenticated/dashboard.tsx b/apps/backoffice/src/routes/_authenticated/dashboard.tsx index 1e45a52..53765f5 100644 --- a/apps/backoffice/src/routes/_authenticated/dashboard.tsx +++ b/apps/backoffice/src/routes/_authenticated/dashboard.tsx @@ -1,195 +1,185 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; import { - PlusOutlined, - ReloadOutlined, - UsergroupAddOutlined, - UsergroupDeleteOutlined, - UserSwitchOutlined, - DeleteOutlined, -} from '@ant-design/icons' -import { Button } from '@imphnen-frontend-service/ui/atoms' -import { Fragment, useState } from 'react' + Plus, + UsersRound, + UserMinus, + UserCog, + RefreshCcw, + Pencil, + Trash2, + MoreHorizontal, +} from 'lucide-react'; +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@imphnen-frontend-service/ui/atoms'; +import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'; import { useUserList, useGachaItemList, useDeleteGachaItem, TGachaItemDto, -} from '@imphnen-frontend-service/service' -import { toast } from 'sonner' +} from '@imphnen-frontend-service/service'; +import { toast } from 'sonner'; +import { DeleteConfirmDialog } from '../../components/list-helpers'; export const Route = createFileRoute('/_authenticated/dashboard')({ component: DashboardPage, -}) +}); + +type StatCardProps = { + icon: React.ComponentType<{ className?: string }>; + label: string; + value: React.ReactNode; +}; + +function StatCard({ icon: Icon, label, value }: StatCardProps) { + return ( + + +
+ +
+
+ + {value} + + {label} +
+
+
+ ); +} function DashboardPage() { - const navigate = useNavigate() - const [deleteId, setDeleteId] = useState(null) + const navigate = useNavigate(); + const [deleteId, setDeleteId] = React.useState(null); - const { data: usersData } = useUserList({ per_page: 1 }) - const { data: gachaItemsData } = useGachaItemList({ per_page: 9 }) - const deleteItem = useDeleteGachaItem() + const { data: usersData } = useUserList({ per_page: 1 }); + const { data: gachaItemsData } = useGachaItemList({ per_page: 9 }); + const deleteItem = useDeleteGachaItem(); - const totalUsers = usersData?.meta?.total ?? 0 - const gachaItems: TGachaItemDto[] = gachaItemsData?.data ?? [] + const totalUsers = usersData?.meta?.total ?? 0; + const gachaItems: TGachaItemDto[] = gachaItemsData?.data ?? []; const handleDelete = async (id: string) => { try { - await deleteItem.mutateAsync(id) - toast.success('Item berhasil dihapus') - setDeleteId(null) + await deleteItem.mutateAsync(id); + toast.success('Item berhasil dihapus'); + setDeleteId(null); } catch (error) { - console.log(error) - toast.error('Item gagal dihapus') + console.log(error); + toast.error('Item gagal dihapus'); } - } + }; return ( - -
-
-

Dashboard

-
+ +
+ + + + +
-
-
-
-

- Summary -

-
-
-
- -
-
-

{totalUsers}

-

Participants

-
-
- -
-
- -
-
-

{gachaItemsData?.meta?.total ?? 0}

-

- Gacha Items -

-
-
- -
-
- -
-
-

-

-

Redeem

-
-
- -
-
- -
-
-

-

-

- Inactive Users -

-
-
-
-
- -
-
-

- Gacha Items -

- -
- -
- {gachaItems.map((item) => ( -
-
-
-

- {item.name} -

-
- {item.id} -
-
-
- - {deleteId === item.id ? ( -
- Yakin? - - -
- ) : ( - - )} -
-
- - {item.name} -
- ))} -
-
+ + +
+
+ Gacha Items + + Daftar item yang tersedia di gacha + +
+
+
+ + {gachaItems.length === 0 ? ( +

+ Belum ada item gacha. +

+ ) : ( +
    + {gachaItems.map((item) => ( +
  • +
    +

    + {item.name} +

    +

    + {item.id} +

    +
    + + + + + + + navigate({ + to: '/dashboard/$id', + params: { id: item.id }, + }) + } + > + + Edit + + setDeleteId(item.id)} + > + + Hapus + + + +
  • + ))} +
+ )} +
+
- -
-
-
- ) + !o && setDeleteId(null)} + onConfirm={() => deleteId && handleDelete(deleteId)} + title="Hapus item gacha ini?" + /> +
+ ); } diff --git a/apps/backoffice/src/routes/_authenticated/feedback-review-dimentorin.tsx b/apps/backoffice/src/routes/_authenticated/feedback-review-dimentorin.tsx index 2814065..e5ac32f 100644 --- a/apps/backoffice/src/routes/_authenticated/feedback-review-dimentorin.tsx +++ b/apps/backoffice/src/routes/_authenticated/feedback-review-dimentorin.tsx @@ -1,62 +1,91 @@ -import { createFileRoute } from '@tanstack/react-router' -import { SearchOutlined } from '@ant-design/icons' -import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms' -import { BackofficeWrapper, DataTable } from '@imphnen-frontend-service/ui/organisms' -import { cn, For } from '@imphnen-frontend-service/utils' -import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from '@tanstack/react-table' -import { ReactElement, useState } from 'react' -import { useMySessions, TSessionListItem } from '@imphnen-frontend-service/service' +import { createFileRoute } from '@tanstack/react-router'; +import * as React from 'react'; +import { Search, MessageSquare } from 'lucide-react'; +import { + Badge, + Button, + Card, + CardContent, + CardHeader, + Input, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@imphnen-frontend-service/ui/atoms'; +import { + BackofficeWrapper, + DataTable, +} from '@imphnen-frontend-service/ui/organisms'; +import { cn } from '@imphnen-frontend-service/utils'; +import { + ColumnDef, + getCoreRowModel, + getPaginationRowModel, + PaginationState, + RowSelectionState, + useReactTable, +} from '@tanstack/react-table'; +import { useMySessions, TSessionListItem } from '@imphnen-frontend-service/service'; +import { + SelectAllCheckbox, + RowSelectCheckbox, +} from '../../components/list-helpers'; -const TABS = { - MENTORING: 'Mentoring', - PLATFORM: 'Platform' -} as const -type Tabs = typeof TABS[keyof typeof TABS] - -export const Route = createFileRoute('/_authenticated/feedback-review-dimentorin')({ +export const Route = createFileRoute( + '/_authenticated/feedback-review-dimentorin' +)({ component: FeedbackReviewDimentorinPage, -}) +}); -function FeedbackReviewDimentorinPage(): ReactElement { - const [activeTab, setActiveTab] = useState(TABS.MENTORING) +function FeedbackReviewDimentorinPage() { + const [activeTab, setActiveTab] = React.useState<'mentoring' | 'platform'>( + 'mentoring' + ); + const [ratingFilter, setRatingFilter] = React.useState('all'); + const [statusFilter, setStatusFilter] = React.useState('all'); - const [rowSelection, setRowSelection] = useState({}) - const [pagination, setPagination] = useState({ + const [rowSelection, setRowSelection] = + React.useState({}); + const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 9, - }) + pageSize: 10, + }); const { data: sessionsData, isLoading } = useMySessions( - activeTab === TABS.MENTORING ? { status: 'completed' } : undefined - ) + activeTab === 'mentoring' ? { status: 'completed' } : undefined + ); - const sessions: TSessionListItem[] = activeTab === TABS.MENTORING - ? (sessionsData?.sessions ?? []) - : [] - const totalItems = activeTab === TABS.MENTORING - ? (sessionsData?.total ?? sessions.length) - : 0 + const allSessions: TSessionListItem[] = + activeTab === 'mentoring' ? (sessionsData?.sessions ?? []) : []; + + const sessions = React.useMemo(() => { + return allSessions.filter((s) => { + if (statusFilter !== 'all') { + const hasRating = !!s.rating; + if (statusFilter === 'done' && !hasRating) return false; + if (statusFilter === 'todo' && hasRating) return false; + } + if (ratingFilter !== 'all' && String(s.rating ?? '') !== ratingFilter) + return false; + return true; + }); + }, [allSessions, statusFilter, ratingFilter]); + + const totalItems = + activeTab === 'mentoring' ? (sessionsData?.total ?? sessions.length) : 0; const columns: ColumnDef[] = [ { id: 'select', - meta: { cellClassName: cn('w-20') }, - header: ({ table }) => ( - - ), - cell: ({ row }) => ( - - ), + meta: { cellClassName: cn('w-10') }, + header: ({ table }) => , + cell: ({ row }) => , }, { id: 'name', @@ -81,39 +110,29 @@ function FeedbackReviewDimentorinPage(): ReactElement { header: 'Status', accessorKey: 'status', cell: ({ row }) => { - const hasRating = !!row.original.rating + const hasRating = !!row.original.rating; return ( -
+ {hasRating ? 'Done' : 'To Do'} -
- ) + + ); }, }, { header: 'Action', - meta: { cellClassName: cn('w-72') }, cell: () => ( - ), }, - ] + ]; const table = useReactTable({ data: sessions, columns, - state: { - pagination, - rowSelection, - }, + state: { pagination, rowSelection }, enableRowSelection: true, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), @@ -121,64 +140,85 @@ function FeedbackReviewDimentorinPage(): ReactElement { onPaginationChange: setPagination, pageCount: Math.ceil(totalItems / pagination.pageSize), manualPagination: true, - }) + }); return ( - -
-

Feedback

-
- - {(tab) => ( - - )} - -
-
- -
-
-
- -
- + + + + { + setActiveTab(v as 'mentoring' | 'platform'); + setPagination((p) => ({ ...p, pageIndex: 0 })); + }} + > +
+ + Mentoring + Platform + +
+
+ + +
+ + +
-
- - -
- {isLoading ? ( -
Loading...
- ) : activeTab === TABS.PLATFORM ? ( -
- Platform feedback tidak tersedia -
- ) : ( - - )} -
+ + {isLoading ? ( +
+ Memuat data… +
+ ) : ( + + setPagination((prev) => ({ ...prev, pageIndex: p - 1 })) + } + /> + )} +
+ +
+ Platform feedback belum tersedia +
+
+
+ + - ) + ); } diff --git a/apps/backoffice/src/routes/_authenticated/gacha-roll.tsx b/apps/backoffice/src/routes/_authenticated/gacha-roll.tsx index b4fb439..ddbf5ba 100644 --- a/apps/backoffice/src/routes/_authenticated/gacha-roll.tsx +++ b/apps/backoffice/src/routes/_authenticated/gacha-roll.tsx @@ -1,14 +1,17 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' -import * as React from 'react' -import { Fragment, useState } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { Search, Pencil, Trash2, Plus } from 'lucide-react'; import { - SearchOutlined, - EditOutlined, - DeleteOutlined, - PlusOutlined, -} from '@ant-design/icons' -import { Button, Input } from '@imphnen-frontend-service/ui/atoms' -import { DataTable } from '@imphnen-frontend-service/ui/organisms' + Button, + Card, + CardContent, + CardHeader, + Input, +} from '@imphnen-frontend-service/ui/atoms'; +import { + DataTable, + BackofficeWrapper, +} from '@imphnen-frontend-service/ui/organisms'; import { ColumnDef, getCoreRowModel, @@ -16,143 +19,101 @@ import { PaginationState, useReactTable, RowSelectionState, -} from '@tanstack/react-table' +} from '@tanstack/react-table'; import { useGachaItemList, useDeleteGachaItem, TGachaItemDto, -} from '@imphnen-frontend-service/service' -import { toast } from 'sonner' +} from '@imphnen-frontend-service/service'; +import { toast } from 'sonner'; +import { + SelectAllCheckbox, + RowSelectCheckbox, + DeleteConfirmDialog, +} from '../../components/list-helpers'; export const Route = createFileRoute('/_authenticated/gacha-roll')({ component: GachaRollPage, -}) +}); function GachaRollPage() { - const navigate = useNavigate() - const [search, setSearch] = useState('') - const [deleteId, setDeleteId] = useState(null) - + const navigate = useNavigate(); + const [search, setSearch] = React.useState(''); + const [deleteId, setDeleteId] = React.useState(null); const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 9, - }) - - const [rowSelection, setRowSelection] = React.useState({}) + pageSize: 10, + }); + const [rowSelection, setRowSelection] = + React.useState({}); const { data: itemsData, isLoading } = useGachaItemList({ search, page: pagination.pageIndex + 1, per_page: pagination.pageSize, - }) - const deleteItem = useDeleteGachaItem() + }); + const deleteItem = useDeleteGachaItem(); - const items: TGachaItemDto[] = itemsData?.data ?? [] - const totalItems = itemsData?.meta?.total ?? items.length + const items: TGachaItemDto[] = itemsData?.data ?? []; + const totalItems = itemsData?.meta?.total ?? items.length; const handleDelete = async (id: string) => { try { - await deleteItem.mutateAsync(id) - toast.success('Item berhasil dihapus') - setDeleteId(null) + await deleteItem.mutateAsync(id); + toast.success('Item berhasil dihapus'); + setDeleteId(null); } catch (error) { - console.log(error) - toast.error('Item gagal dihapus') + console.log(error); + toast.error('Item gagal dihapus'); } - } + }; const columns: ColumnDef[] = [ { id: 'select', - header: ({ table }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - header: 'No', - accessorKey: 'id', - }, - { - header: 'Nama Item', - accessorKey: 'name', + header: ({ table }) => , + cell: ({ row }) => , }, + { header: 'No', accessorKey: 'id' }, + { header: 'Nama Item', accessorKey: 'name' }, { header: 'Action', cell: ({ row }) => ( -
+
+ - {deleteId === row.original.id ? ( -
- Yakin? - - -
- ) : ( - - )}
), }, - ] + ]; const table = useReactTable({ data: items, columns, - state: { - pagination, - rowSelection, - }, + state: { pagination, rowSelection }, enableRowSelection: true, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), @@ -160,48 +121,61 @@ function GachaRollPage() { onPaginationChange: setPagination, pageCount: Math.ceil(totalItems / pagination.pageSize), manualPagination: true, - }) + }); return ( - -
-
-

Gacha Roll

-
- -
-
-
+ + + +
+
+ setSearch(e.target.value)} /> -
- -
-
-
-
+
- +
+ {isLoading ? ( -
Loading...
+
+ Memuat data… +
) : ( - + + setPagination((prev) => ({ ...prev, pageIndex: p - 1 })) + } + /> )} -
-
-
- ) + + + + !o && setDeleteId(null)} + onConfirm={() => deleteId && handleDelete(deleteId)} + title="Hapus item gacha ini?" + /> + + ); } diff --git a/apps/backoffice/src/routes/_authenticated/hackathon-dashboard.tsx b/apps/backoffice/src/routes/_authenticated/hackathon-dashboard.tsx index e3893dd..9f47ae2 100644 --- a/apps/backoffice/src/routes/_authenticated/hackathon-dashboard.tsx +++ b/apps/backoffice/src/routes/_authenticated/hackathon-dashboard.tsx @@ -1,61 +1,82 @@ -import { createFileRoute } from '@tanstack/react-router' -import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms' -import { FC, ReactElement } from 'react' -import { useQuery } from '@tanstack/react-query' +import { createFileRoute } from '@tanstack/react-router'; +import * as React from 'react'; +import { UsersRound, UserCog, ClipboardCheck } from 'lucide-react'; +import { useQuery } from '@tanstack/react-query'; +import { + Card, + CardContent, +} from '@imphnen-frontend-service/ui/atoms'; +import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'; import { getAdminUsers, getAdminTeams, getAdminSubmissions, -} from '@imphnen-frontend-service/service' +} from '@imphnen-frontend-service/service'; export const Route = createFileRoute('/_authenticated/hackathon-dashboard')({ component: HackathonDashboardPage, -}) +}); + +type StatCardProps = { + icon: React.ComponentType<{ className?: string }>; + label: string; + value: React.ReactNode; +}; + +function StatCard({ icon: Icon, label, value }: StatCardProps) { + return ( + + +
+ +
+
+ + {value} + + {label} +
+
+
+ ); +} function HackathonDashboardPage() { const { data: usersData } = useQuery({ queryKey: ['admin-users-count'], queryFn: () => getAdminUsers({ page: 1, per_page: 1 }), - }) - + }); const { data: teamsData } = useQuery({ queryKey: ['admin-teams-count'], queryFn: () => getAdminTeams({ page: 1, per_page: 1 }), - }) - + }); const { data: submissionsData } = useQuery({ queryKey: ['admin-submissions-count'], queryFn: () => getAdminSubmissions({ page: 1, per_page: 1 }), - }) + }); - const totalParticipants = usersData?.meta?.total_data ?? '??' - const totalTeams = teamsData?.meta?.total_data ?? '??' - const totalSubmissions = submissionsData?.meta?.total_data ?? '??' + const totalParticipants = usersData?.meta?.total_data ?? '—'; + const totalTeams = teamsData?.meta?.total_data ?? '—'; + const totalSubmissions = submissionsData?.meta?.total_data ?? '—'; return ( - -

Dashboard

- -
-
-

- {totalParticipants} -

-

Total Participants

-
-
-

- {totalTeams} -

-

Total Teams

-
-
-

- {totalSubmissions} -

-

Total Project Submitted

-
+ +
+ + +
- ) + ); } diff --git a/apps/backoffice/src/routes/_authenticated/hackathon-submissions.tsx b/apps/backoffice/src/routes/_authenticated/hackathon-submissions.tsx index d9fc06f..1050863 100644 --- a/apps/backoffice/src/routes/_authenticated/hackathon-submissions.tsx +++ b/apps/backoffice/src/routes/_authenticated/hackathon-submissions.tsx @@ -1,34 +1,27 @@ -import { createFileRoute } from '@tanstack/react-router' -import { - FC, - ReactElement, - useState, - useEffect, - useMemo, - useCallback, -} from 'react' -import SubmissionModal from './_components/hackathon-submissions/submission-modal' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { Search, Eye } from 'lucide-react'; +import SubmissionModal from './_components/hackathon-submissions/submission-modal'; import { BackofficeWrapper, DataTable, -} from '@imphnen-frontend-service/ui/organisms' -import { ColumnDef } from '@tanstack/react-table' -import { Button } from '@imphnen-frontend-service/ui/atoms' -import { cn } from '@imphnen-frontend-service/utils' +} from '@imphnen-frontend-service/ui/organisms'; +import { ColumnDef } from '@tanstack/react-table'; import { - SearchOutlined, - FilterOutlined, - LoadingOutlined, - EyeOutlined, -} from '@ant-design/icons' -import { useQuery } from '@tanstack/react-query' + Badge, + Button, + Card, + CardContent, + CardHeader, + Input, +} from '@imphnen-frontend-service/ui/atoms'; +import { useQuery } from '@tanstack/react-query'; import { getAdminSubmissions, TAdminSubmissionItem, -} from '@imphnen-frontend-service/service' -import { useNavigate } from '@tanstack/react-router' +} from '@imphnen-frontend-service/service'; -type SubmissionType = TAdminSubmissionItem +type SubmissionType = TAdminSubmissionItem; export const Route = createFileRoute('/_authenticated/hackathon-submissions')({ component: HackathonSubmissionsPage, @@ -38,20 +31,20 @@ export const Route = createFileRoute('/_authenticated/hackathon-submissions')({ per_page: Number(search.per_page) || 10, status: (search.status as string) || 'all', }), -}) +}); function HackathonSubmissionsPage() { - const searchParams = Route.useSearch() - const navigate = useNavigate() - const currentPage = Math.max(1, searchParams.page) - const searchQuery = searchParams.search || '' - const perPage = searchParams.per_page || 10 - const statusFilter = searchParams.status || 'all' + const searchParams = Route.useSearch(); + const navigate = useNavigate(); + const currentPage = Math.max(1, searchParams.page); + const searchQuery = searchParams.search || ''; + const perPage = searchParams.per_page || 10; + const statusFilter = searchParams.status || 'all'; - const [showSubmissionModal, setShowSubmissionModal] = useState(false) + const [showSubmissionModal, setShowSubmissionModal] = React.useState(false); const [selectedSubmission, setSelectedSubmission] = - useState(null) - const [globalFilter, setGlobalFilter] = useState(searchQuery) + React.useState(null); + const [globalFilter, setGlobalFilter] = React.useState(searchQuery); const { data: submissionsResponse, @@ -74,12 +67,12 @@ function HackathonSubmissionsPage() { }), staleTime: 30000, gcTime: 5 * 60 * 1000, - }) + }); - const totalData = submissionsResponse?.meta?.total_data || 0 - const totalPages = submissionsResponse?.meta?.total_page || 1 + const totalData = submissionsResponse?.meta?.total_data || 0; + const totalPages = submissionsResponse?.meta?.total_page || 1; - const handlePageChange = useCallback( + const handlePageChange = React.useCallback( (newPage: number) => { navigate({ search: { @@ -88,23 +81,23 @@ function HackathonSubmissionsPage() { search: searchQuery || undefined, status: statusFilter !== 'all' ? statusFilter : undefined, } as any, - }) - window.scrollTo({ top: 0, behavior: 'smooth' }) + }); + window.scrollTo({ top: 0, behavior: 'smooth' }); }, [navigate, perPage, searchQuery, statusFilter] - ) + ); - useEffect(() => { + React.useEffect(() => { if (!isLoading && totalPages > 0 && currentPage > totalPages) { - navigate({ search: { page: totalPages } as any }) + navigate({ search: { page: totalPages } as any }); } - }, [currentPage, totalPages, navigate, isLoading]) + }, [currentPage, totalPages, navigate, isLoading]); - useEffect(() => { - setGlobalFilter(searchQuery) - }, [searchQuery]) + React.useEffect(() => { + setGlobalFilter(searchQuery); + }, [searchQuery]); - const handleSearch = useCallback(() => { + const handleSearch = React.useCallback(() => { navigate({ search: { page: 1, @@ -112,56 +105,29 @@ function HackathonSubmissionsPage() { search: globalFilter.trim() || undefined, status: statusFilter !== 'all' ? statusFilter : undefined, } as any, - }) - }, [globalFilter, navigate, perPage, statusFilter]) + }); + }, [globalFilter, navigate, perPage, statusFilter]); - const handleSearchKeyPress = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleSearch() - } - }, - [handleSearch] - ) + const filteredData = React.useMemo(() => { + return ( + ((submissionsResponse?.data as any)?.data as SubmissionType[]) ?? + (submissionsResponse?.data as SubmissionType[]) ?? + [] + ); + }, [submissionsResponse]); - const handlePerPageChange = useCallback( - (newPerPage: number) => { - navigate({ - search: { - page: 1, - per_page: newPerPage, - search: searchQuery || undefined, - status: statusFilter !== 'all' ? statusFilter : undefined, - } as any, - }) - }, - [navigate, searchQuery, statusFilter] - ) + const statusVariants: Record = { + submitted: 'success', + pending: 'warning', + }; - const handleShowSubmissionModal = useCallback( - (submission: SubmissionType) => { - setSelectedSubmission(submission) - setShowSubmissionModal(true) - }, - [] - ) - - const handleCloseSubmissionModal = useCallback(() => { - setShowSubmissionModal(false) - setSelectedSubmission(null) - }, []) - - const filteredData = useMemo(() => { - return submissionsResponse?.data?.data || submissionsResponse?.data || [] - }, [submissionsResponse]) - - const columns: ColumnDef[] = useMemo( + const columns: ColumnDef[] = React.useMemo( () => [ { accessorKey: 'project_name', header: 'Project Name', cell: ({ row }) => ( - + {row.original.project_name} ), @@ -171,7 +137,7 @@ function HackathonSubmissionsPage() { accessorKey: 'team_id', header: 'Team ID', cell: ({ row }) => ( - + {row.original.team_id} ), @@ -180,30 +146,21 @@ function HackathonSubmissionsPage() { { accessorKey: 'status', header: 'Status', - cell: ({ row }) => { - const status = row.original.status - return ( - - {status.charAt(0).toUpperCase() + status.slice(1)} - - ) - }, + cell: ({ row }) => ( + + {row.original.status} + + ), enableSorting: true, }, { accessorKey: 'submitted_at', header: 'Submitted', cell: ({ row }) => ( - + {new Date(row.original.submitted_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', @@ -217,109 +174,87 @@ function HackathonSubmissionsPage() { { id: 'actions', header: 'Actions', - meta: { cellClassName: cn('w-48') }, cell: ({ row }) => ( ), enableSorting: false, }, ], - [handleShowSubmissionModal] - ) + [] + ); return ( - -

- Project Submissions -

-
-
-
-
- - + + +
+
+ + setGlobalFilter(e.target.value)} - onKeyPress={handleSearchKeyPress} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} />
- -
- +
+ + + {isLoading ? ( +
+ Memuat submissions…
- - {} - { -} -
-
- - {} - { -} - - {isLoading ? ( -
- - - Loading submissions... - -
- ) : filteredData.length > 0 ? ( - <> -
- Showing {filteredData.length} of {totalData} submissions (Page{' '} - {currentPage} of {totalPages}) - {isFetching && ( - (Updating...) - )} + ) : filteredData.length > 0 ? ( + <> +
+ Menampilkan {filteredData.length} dari {totalData} submissions + (page {currentPage} / {totalPages}) + {isFetching && ( + Updating… + )} +
+ + + ) : ( +
+ Tidak ada submissions.
- - - ) : ( -
- No submissions found. Try adjusting your filters. -
- )} -
+ )} + + {selectedSubmission && ( { + setShowSubmissionModal(false); + setSelectedSubmission(null); + }} submission={selectedSubmission} /> )}
- ) + ); } diff --git a/apps/backoffice/src/routes/_authenticated/hackathon-teams.tsx b/apps/backoffice/src/routes/_authenticated/hackathon-teams.tsx index 765a3f2..5e7e0d3 100644 --- a/apps/backoffice/src/routes/_authenticated/hackathon-teams.tsx +++ b/apps/backoffice/src/routes/_authenticated/hackathon-teams.tsx @@ -1,37 +1,30 @@ -import { createFileRoute } from '@tanstack/react-router' -import { - FC, - ReactElement, - useState, - useEffect, - useMemo, - useCallback, -} from 'react' -import ModalTeamDetail from './_components/hackathon-teams/modal-team-detail-new' -import { CityFilterSelect } from '../../components/city-filter-select' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { Search, Plus, Users as TeamIcon, Pencil, X } from 'lucide-react'; +import ModalTeamDetail from './_components/hackathon-teams/modal-team-detail-new'; import { BackofficeWrapper, DataTable, -} from '@imphnen-frontend-service/ui/organisms' -import { ColumnDef } from '@tanstack/react-table' -import { Button } from '@imphnen-frontend-service/ui/atoms' -import { cn } from '@imphnen-frontend-service/utils' +} from '@imphnen-frontend-service/ui/organisms'; +import { ColumnDef } from '@tanstack/react-table'; import { - EditOutlined, - TeamOutlined, - SearchOutlined, - FilterOutlined, - PlusOutlined, - LoadingOutlined, -} from '@ant-design/icons' -import { useQuery } from '@tanstack/react-query' + Avatar, + AvatarFallback, + AvatarImage, + Badge, + Button, + Card, + CardContent, + CardHeader, + Input, +} from '@imphnen-frontend-service/ui/atoms'; +import { useQuery } from '@tanstack/react-query'; import { getAdminTeams, TAdminTeamItem, -} from '@imphnen-frontend-service/service' -import { useNavigate } from '@tanstack/react-router' +} from '@imphnen-frontend-service/service'; -type TeamType = TAdminTeamItem +type TeamType = TAdminTeamItem; export const Route = createFileRoute('/_authenticated/hackathon-teams')({ component: HackathonTeamsPage, @@ -40,22 +33,21 @@ export const Route = createFileRoute('/_authenticated/hackathon-teams')({ search: (search.search as string) || '', per_page: Number(search.per_page) || 10, }), -}) +}); function HackathonTeamsPage() { - const searchParams = Route.useSearch() - const navigate = useNavigate() - const currentPage = Math.max(1, searchParams.page) - const searchQuery = searchParams.search || '' - const perPage = searchParams.per_page || 10 - const [showDetailModal, setShowDetailModal] = useState(false) - const [showNewTeamModal, setShowNewTeamModal] = useState(false) - const [selectedTeam, setSelectedTeam] = useState(null) - useState(null) - const [globalFilter, setGlobalFilter] = useState(searchQuery) + const searchParams = Route.useSearch(); + const navigate = useNavigate(); + const currentPage = Math.max(1, searchParams.page); + const searchQuery = searchParams.search || ''; + const perPage = searchParams.per_page || 10; - const [visibilityFilter, setVisibilityFilter] = useState('all') - const [cityFilter, setCityFilter] = useState('all') + const [showDetailModal, setShowDetailModal] = React.useState(false); + const [showNewTeamModal, setShowNewTeamModal] = React.useState(false); + const [selectedTeam, setSelectedTeam] = React.useState(null); + const [globalFilter, setGlobalFilter] = React.useState(searchQuery); + const [visibilityFilter, setVisibilityFilter] = React.useState('all'); + const [cityFilter, setCityFilter] = React.useState('all'); const { data: teamsResponse, @@ -78,12 +70,12 @@ function HackathonTeamsPage() { }), staleTime: 30000, gcTime: 5 * 60 * 1000, - }) + }); - const totalData = teamsResponse?.meta?.total_data || 0 - const totalPages = teamsResponse?.meta?.total_page || 1 + const totalData = teamsResponse?.meta?.total_data || 0; + const totalPages = teamsResponse?.meta?.total_page || 1; - const handlePageChange = useCallback( + const handlePageChange = React.useCallback( (newPage: number) => { navigate({ search: { @@ -91,142 +83,97 @@ function HackathonTeamsPage() { per_page: perPage !== 10 ? perPage : undefined, search: searchQuery || undefined, } as any, - }) - window.scrollTo({ top: 0, behavior: 'smooth' }) + }); + window.scrollTo({ top: 0, behavior: 'smooth' }); }, [navigate, perPage, searchQuery] - ) + ); - useEffect(() => { + React.useEffect(() => { if (!isLoading && totalPages > 0 && currentPage > totalPages) { - navigate({ search: { page: totalPages } as any }) + navigate({ search: { page: totalPages } as any }); } - }, [currentPage, totalPages, navigate, isLoading]) + }, [currentPage, totalPages, navigate, isLoading]); - useEffect(() => { - setGlobalFilter(searchQuery) - }, [searchQuery]) + React.useEffect(() => { + setGlobalFilter(searchQuery); + }, [searchQuery]); - const handleSearch = useCallback(() => { + const handleSearch = React.useCallback(() => { navigate({ search: { page: 1, per_page: perPage !== 10 ? perPage : undefined, search: globalFilter.trim() || undefined, } as any, - }) - }, [globalFilter, navigate, perPage]) + }); + }, [globalFilter, navigate, perPage]); - const handleSearchKeyPress = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleSearch() - } - }, - [handleSearch] - ) + const filteredData = React.useMemo(() => { + return ( + ((teamsResponse?.data as any)?.data as TeamType[]) ?? + (teamsResponse?.data as TeamType[]) ?? + [] + ); + }, [teamsResponse]); - const handlePerPageChange = useCallback( - (newPerPage: number) => { - navigate({ - search: { - page: 1, - per_page: newPerPage, - search: searchQuery || undefined, - } as any, - }) - }, - [navigate, searchQuery] - ) + const handleShowDetailModal = React.useCallback((team: TeamType) => { + setSelectedTeam(team); + setShowDetailModal(true); + }, []); - const handleShowDetailModal = useCallback((team: TeamType) => { - setSelectedTeam(team) - setShowDetailModal(true) - }, []) - - const handleCloseDetailModal = useCallback(() => { - setShowDetailModal(false) - setSelectedTeam(null) - }, []) - - const handleShowNewTeamModal = useCallback(() => { - setShowNewTeamModal(true) - }, []) - - const handleCloseNewTeamModal = useCallback(() => { - setShowNewTeamModal(false) - }, []) - - const filteredData = useMemo(() => { - return teamsResponse?.data?.data || teamsResponse?.data || [] - }, [teamsResponse]) - - const columns: ColumnDef[] = useMemo( + const columns: ColumnDef[] = React.useMemo( () => [ { accessorKey: 'name', header: 'Team', - cell: ({ row }) => { - const team = row.original - return ( -
-
- {team.logo ? ( - {team.name} - ) : ( - - )} -
-
-

- {team.name} -

-
+ cell: ({ row }) => ( +
+ + + + + + +
+

+ {row.original.name} +

- ) - }, +
+ ), enableSorting: true, }, { accessorKey: 'city', header: 'City', cell: ({ row }) => ( - {row.original.city} + {row.original.city} ), enableSorting: true, }, { accessorKey: 'visibility', header: 'Visibility', - cell: ({ row }) => { - const isPublic = row.original.visibility === 'public' - return ( - - {isPublic ? 'Public' : 'Private'} - - ) - }, + cell: ({ row }) => ( + + {row.original.visibility === 'public' ? 'Public' : 'Private'} + + ), enableSorting: true, }, { id: 'leader', header: 'Leader ID', cell: ({ row }) => ( -
+
{row.original.leader_id}
), @@ -236,7 +183,7 @@ function HackathonTeamsPage() { accessorKey: 'created_at', header: 'Created', cell: ({ row }) => ( - + {new Date(row.original.created_at).toLocaleDateString('en-UK', { year: 'numeric', month: 'short', @@ -250,168 +197,127 @@ function HackathonTeamsPage() { { id: 'actions', header: 'Actions', - meta: { cellClassName: cn('w-48') }, cell: ({ row }) => ( -
- -
+ ), enableSorting: false, }, ], [handleShowDetailModal] - ) + ); + + const hasActiveFilters = visibilityFilter !== 'all' || cityFilter !== 'all'; return ( - -

- Team Management -

-
-
-
-
- - + + +
+
+ + setGlobalFilter(e.target.value)} - onKeyPress={handleSearchKeyPress} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} />
- -
- -
- - {} - { -} - - {} - { -} -
- -
-
-
- - {(visibilityFilter !== 'all' || cityFilter !== 'all') && ( -
- Active filters: - - {visibilityFilter !== 'all' && ( - - Visibility: {visibilityFilter} - + + + {hasActiveFilters && ( +
+ + Active filters: - )} - - {cityFilter !== 'all' && ( - - City: {cityFilter} - - - )} - - -
- )} - - {isLoading ? ( -
- - Loading teams... -
- ) : filteredData.length > 0 ? ( - <> -
- Showing {filteredData.length} of {totalData} teams (Page{' '} - {currentPage} of {totalPages}) - {isFetching && ( - (Updating...) + {visibilityFilter !== 'all' && ( + + Visibility: {visibilityFilter} + + )} + {cityFilter !== 'all' && ( + + City: {cityFilter} + + + )} +
- - - ) : ( -
- No teams found. Try adjusting your filters. -
- )} -
+ )} + {isLoading ? ( +
+ Memuat data teams… +
+ ) : filteredData.length > 0 ? ( + <> +
+ Menampilkan {filteredData.length} dari {totalData} teams (page{' '} + {currentPage} / {totalPages}) + {isFetching && ( + Updating… + )} +
+ + + ) : ( +
+ Tidak ada team. Coba ubah filter pencarian. +
+ )} + + { + setShowDetailModal(false); + setSelectedTeam(null); + }} team={selectedTeam} /> - setShowNewTeamModal(false)} team={null} />
- ) + ); } diff --git a/apps/backoffice/src/routes/_authenticated/hackathon-users.tsx b/apps/backoffice/src/routes/_authenticated/hackathon-users.tsx index 80a332a..c271ec6 100644 --- a/apps/backoffice/src/routes/_authenticated/hackathon-users.tsx +++ b/apps/backoffice/src/routes/_authenticated/hackathon-users.tsx @@ -1,48 +1,31 @@ -import { createFileRoute } from '@tanstack/react-router' -import { - FC, - ReactElement, - useState, - useEffect, - useMemo, - useCallback, -} from 'react' -import ModalUserDetail from './_components/hackathon-users/modal-user-detail' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { Search, Plus, User, Pencil, X } from 'lucide-react'; +import ModalUserDetail from './_components/hackathon-users/modal-user-detail'; import { BackofficeWrapper, DataTable, -} from '@imphnen-frontend-service/ui/organisms' -import { ColumnDef } from '@tanstack/react-table' -import { Button } from '@imphnen-frontend-service/ui/atoms' -import { cn } from '@imphnen-frontend-service/utils' +} from '@imphnen-frontend-service/ui/organisms'; +import { ColumnDef } from '@tanstack/react-table'; import { - EditOutlined, - UserOutlined, - SearchOutlined, - FilterOutlined, - PlusOutlined, - LoadingOutlined, -} from '@ant-design/icons' -import { CityFilterSelect } from '../../components/city-filter-select' -import { useQuery } from '@tanstack/react-query' + Avatar, + AvatarFallback, + AvatarImage, + Badge, + Button, + Card, + CardContent, + CardHeader, + Input, +} from '@imphnen-frontend-service/ui/atoms'; +import { cn } from '@imphnen-frontend-service/utils'; +import { useQuery } from '@tanstack/react-query'; import { getAdminUsers, TAdminUserItem, -} from '@imphnen-frontend-service/service' -import { useNavigate } from '@tanstack/react-router' +} from '@imphnen-frontend-service/service'; -type UserType = TAdminUserItem - -const skillsOptions = [ - 'Frontend Developer', - 'Backend Developer', - 'Full Stack Developer', - 'DevOps Engineer', - 'UI/UX Designer', - 'Product Manager', - 'Data Scientist', - 'Mobile Developer', -] +type UserType = TAdminUserItem; export const Route = createFileRoute('/_authenticated/hackathon-users')({ component: HackathonUsersPage, @@ -51,36 +34,28 @@ export const Route = createFileRoute('/_authenticated/hackathon-users')({ search: (search.search as string) || '', per_page: Number(search.per_page) || 10, }), -}) +}); function HackathonUsersPage() { - const searchParams = Route.useSearch() - const navigate = useNavigate() - const currentPage = Math.max(1, searchParams.page) - const searchQuery = searchParams.search || '' - const perPage = searchParams.per_page || 10 - const [showDetailModal, setShowDetailModal] = useState(false) - const [showNewUserModal, setShowNewUserModal] = useState(false) - const [selectedUser, setSelectedUser] = useState(null) - const [globalFilter, setGlobalFilter] = useState(searchQuery) + const searchParams = Route.useSearch(); + const navigate = useNavigate(); + const currentPage = Math.max(1, searchParams.page); + const searchQuery = searchParams.search || ''; + const perPage = searchParams.per_page || 10; - const [statusFilter, setStatusFilter] = useState('all') - const [cityFilter, setCityFilter] = useState('all') - const [skillsFilter, setSkillsFilter] = useState([]) + const [showDetailModal, setShowDetailModal] = React.useState(false); + const [showNewUserModal, setShowNewUserModal] = React.useState(false); + const [selectedUser, setSelectedUser] = React.useState(null); + const [globalFilter, setGlobalFilter] = React.useState(searchQuery); + const [statusFilter, setStatusFilter] = React.useState('all'); + const [skillsFilter, setSkillsFilter] = React.useState([]); const { data: usersResponse, isLoading, isFetching, } = useQuery({ - queryKey: [ - 'admin-users', - currentPage, - perPage, - cityFilter, - statusFilter, - searchQuery, - ], + queryKey: ['admin-users', currentPage, perPage, statusFilter, searchQuery], queryFn: () => getAdminUsers({ page: currentPage, @@ -89,12 +64,12 @@ function HackathonUsersPage() { }), staleTime: 30000, gcTime: 5 * 60 * 1000, - }) + }); - const totalData = usersResponse?.meta?.total_data || 0 - const totalPages = usersResponse?.meta?.total_page || 1 + const totalData = usersResponse?.meta?.total_data || 0; + const totalPages = usersResponse?.meta?.total_page || 1; - const handlePageChange = useCallback( + const handlePageChange = React.useCallback( (newPage: number) => { navigate({ search: { @@ -102,112 +77,73 @@ function HackathonUsersPage() { per_page: perPage !== 10 ? perPage : undefined, search: searchQuery || undefined, } as any, - }) - window.scrollTo({ top: 0, behavior: 'smooth' }) + }); + window.scrollTo({ top: 0, behavior: 'smooth' }); }, [navigate, perPage, searchQuery] - ) + ); - useEffect(() => { + React.useEffect(() => { if (!isLoading && totalPages > 0 && currentPage > totalPages) { - navigate({ search: { page: totalPages } as any }) + navigate({ search: { page: totalPages } as any }); } - }, [currentPage, totalPages, navigate, isLoading]) + }, [currentPage, totalPages, navigate, isLoading]); - useEffect(() => { - setGlobalFilter(searchQuery) - }, [searchQuery]) + React.useEffect(() => { + setGlobalFilter(searchQuery); + }, [searchQuery]); - const handleSearch = useCallback(() => { + const handleSearch = React.useCallback(() => { navigate({ search: { page: 1, per_page: perPage !== 10 ? perPage : undefined, search: globalFilter.trim() || undefined, } as any, - }) - }, [globalFilter, navigate, perPage]) + }); + }, [globalFilter, navigate, perPage]); - const handleSearchKeyPress = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleSearch() - } - }, - [handleSearch] - ) + const handleShowDetailModal = React.useCallback((user: UserType) => { + setSelectedUser(user); + setShowDetailModal(true); + }, []); - const handlePerPageChange = useCallback( - (newPerPage: number) => { - navigate({ - search: { - page: 1, - per_page: newPerPage, - search: searchQuery || undefined, - } as any, - }) - }, - [navigate, searchQuery] - ) - - const handleShowDetailModal = useCallback((user: UserType) => { - setSelectedUser(user) - setShowDetailModal(true) - }, []) - - const handleCloseDetailModal = useCallback(() => { - setShowDetailModal(false) - setSelectedUser(null) - }, []) - - const handleShowNewUserModal = useCallback(() => { - setShowNewUserModal(true) - }, []) - - const handleCloseNewUserModal = useCallback(() => { - setShowNewUserModal(false) - }, []) - - const filteredData = useMemo(() => { - const usersData = usersResponse?.data?.data || usersResponse?.data || [] - return usersData.filter((user: UserType) => { + const filteredData = React.useMemo(() => { + const usersData: UserType[] = + ((usersResponse?.data as any)?.data as UserType[]) ?? + (usersResponse?.data as UserType[]) ?? + []; + return usersData.filter((user) => { if (statusFilter !== 'all') { - const isActive = statusFilter === 'active' - if (user.is_active !== isActive) return false + const isActive = statusFilter === 'active'; + if (user.is_active !== isActive) return false; } - if (skillsFilter.length > 0) { - const userSkills = user.skills || [] + const userSkills = user.skills || []; const hasMatchingSkill = skillsFilter.some((skill) => userSkills.includes(skill) - ) - if (!hasMatchingSkill) return false + ); + if (!hasMatchingSkill) return false; } + return true; + }); + }, [usersResponse, statusFilter, skillsFilter]); - return true - }) - }, [usersResponse, statusFilter, skillsFilter]) - - const columns: ColumnDef[] = useMemo( + const columns: ColumnDef[] = React.useMemo( () => [ { accessorKey: 'fullname', header: 'User', cell: ({ row }) => (
-
- {row.original.avatar ? ( - {row.original.fullname} - ) : ( - - )} -
+ + + + + +
-

+

{row.original.fullname}

@@ -219,30 +155,22 @@ function HackathonUsersPage() { accessorKey: 'skills', header: 'Skills', cell: ({ row }) => { - const skills = row.original.skills || [] + const skills = row.original.skills || []; + if (skills.length === 0) { + return -; + } return ( -
- {skills.length > 0 ? ( - <> - {skills.slice(0, 2).map((skill, index) => ( - - {skill.replace(' Developer', '').replace(' Engineer', '')} - - ))} - {skills.length > 2 && ( - - +{skills.length - 2} - - )} - - ) : ( - - +
+ {skills.slice(0, 2).map((skill, index) => ( + + {skill.replace(' Developer', '').replace(' Engineer', '')} + + ))} + {skills.length > 2 && ( + +{skills.length - 2} )}
- ) + ); }, enableSorting: false, }, @@ -250,7 +178,7 @@ function HackathonUsersPage() { accessorKey: 'location', header: 'Location', cell: ({ row }) => ( - {row.original.location} + {row.original.location} ), enableSorting: true, }, @@ -259,16 +187,16 @@ function HackathonUsersPage() { header: 'Status', cell: ({ row }) => (
-
{row.original.is_active ? 'Active' : 'Inactive'} @@ -277,18 +205,18 @@ function HackathonUsersPage() { ), enableSorting: true, sortingFn: (rowA, rowB) => { - const aActive = rowA.original.is_active - const bActive = rowB.original.is_active - if (aActive && !bActive) return -1 - if (!aActive && bActive) return 1 - return 0 + const a = rowA.original.is_active; + const b = rowB.original.is_active; + if (a && !b) return -1; + if (!a && b) return 1; + return 0; }, }, { accessorKey: 'created_at', header: 'Joined', cell: ({ row }) => ( - + {new Date(row.original.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', @@ -302,194 +230,135 @@ function HackathonUsersPage() { { id: 'actions', header: 'Actions', - meta: { cellClassName: cn('w-48') }, cell: ({ row }) => ( -
- - { -} -
+ ), enableSorting: false, }, ], [handleShowDetailModal] - ) + ); + + const hasActiveFilters = statusFilter !== 'all' || skillsFilter.length > 0; return ( - -

- User Management -

-
-
-
-
- - + + +
+
+ + setGlobalFilter(e.target.value)} - onKeyPress={handleSearchKeyPress} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} />
- -
- -
- - {} - { -} - - {} - { -} - - {} - { -} -
- -
-
-
- - {(skillsFilter.length > 0 || - statusFilter !== 'all' || - cityFilter !== 'all') && ( -
- Active filters: - - {statusFilter !== 'all' && ( - - Status: {statusFilter} - + + + {hasActiveFilters && ( +
+ + Active filters: - )} - - {cityFilter !== 'all' && ( - - City: {cityFilter} - - - )} - - {skillsFilter.map((skill) => ( - - {skill.replace(' Developer', '').replace(' Engineer', '')} - - - ))} - - -
- )} - - {isLoading ? ( -
- - Loading users... -
- ) : filteredData.length > 0 ? ( - <> -
- Showing {filteredData.length} of {totalData} users (Page{' '} - {currentPage} of {totalPages}) - {isFetching && ( - (Updating...) + {statusFilter !== 'all' && ( + + Status: {statusFilter} + + )} + {skillsFilter.map((skill) => ( + + {skill.replace(' Developer', '').replace(' Engineer', '')} + + + ))} +
- - - ) : ( -
- No users found. Try adjusting your filters. -
- )} -
+ )} + {isLoading ? ( +
+ Memuat data users… +
+ ) : filteredData.length > 0 ? ( + <> +
+ Menampilkan {filteredData.length} dari {totalData} users (page{' '} + {currentPage} / {totalPages}) + {isFetching && ( + Updating… + )} +
+ + + ) : ( +
+ Tidak ada user. Coba ubah filter pencarian. +
+ )} + + { + setShowDetailModal(false); + setSelectedUser(null); + }} user={selectedUser} /> - setShowNewUserModal(false)} user={null} />
- ) + ); } diff --git a/apps/backoffice/src/routes/_authenticated/permissions.tsx b/apps/backoffice/src/routes/_authenticated/permissions.tsx index cfe7c74..62fb2e1 100644 --- a/apps/backoffice/src/routes/_authenticated/permissions.tsx +++ b/apps/backoffice/src/routes/_authenticated/permissions.tsx @@ -1,13 +1,17 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' -import { Fragment, useState } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { Search, Pencil, Trash2, Plus } from 'lucide-react'; import { - SearchOutlined, - EditOutlined, - DeleteOutlined, - PlusOutlined, -} from '@ant-design/icons' -import { Button, Input } from '@imphnen-frontend-service/ui/atoms' -import { DataTable } from '@imphnen-frontend-service/ui/organisms' + Button, + Card, + CardContent, + CardHeader, + Input, +} from '@imphnen-frontend-service/ui/atoms'; +import { + DataTable, + BackofficeWrapper, +} from '@imphnen-frontend-service/ui/organisms'; import { ColumnDef, getCoreRowModel, @@ -15,144 +19,101 @@ import { PaginationState, RowSelectionState, useReactTable, -} from '@tanstack/react-table' +} from '@tanstack/react-table'; import { usePermissionList, useDeletePermission, TPermissionItem, -} from '@imphnen-frontend-service/service' -import React from 'react' -import { toast } from 'sonner' +} from '@imphnen-frontend-service/service'; +import { toast } from 'sonner'; +import { + SelectAllCheckbox, + RowSelectCheckbox, + DeleteConfirmDialog, +} from '../../components/list-helpers'; export const Route = createFileRoute('/_authenticated/permissions')({ component: PermissionsPage, -}) +}); function PermissionsPage() { - const navigate = useNavigate() - const [search, setSearch] = useState('') - const [deleteId, setDeleteId] = useState(null) - + const navigate = useNavigate(); + const [search, setSearch] = React.useState(''); + const [deleteId, setDeleteId] = React.useState(null); const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 9, - }) - - const [rowSelection, setRowSelection] = React.useState({}) + pageSize: 10, + }); + const [rowSelection, setRowSelection] = + React.useState({}); const { data: permissionsData, isLoading } = usePermissionList({ search, page: pagination.pageIndex + 1, per_page: pagination.pageSize, - }) - const deletePermission = useDeletePermission() + }); + const deletePermission = useDeletePermission(); - const permissions: TPermissionItem[] = permissionsData?.data ?? [] - const totalItems = permissionsData?.meta?.total ?? permissions.length + const permissions: TPermissionItem[] = permissionsData?.data ?? []; + const totalItems = permissionsData?.meta?.total ?? permissions.length; const handleDelete = async (id: string) => { try { - await deletePermission.mutateAsync(id) - toast.success('Data permissions berhasil dihapus') - setDeleteId(null) + await deletePermission.mutateAsync(id); + toast.success('Data permission berhasil dihapus'); + setDeleteId(null); } catch (error) { - console.log(error) - toast.error('Data permissions gagal dihapus') + console.log(error); + toast.error('Data permission gagal dihapus'); } - } + }; const columns: ColumnDef[] = [ { id: 'select', - header: ({ table }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - header: 'No', - accessorKey: 'id', - }, - { - header: 'Name', - accessorKey: 'name', + header: ({ table }) => , + cell: ({ row }) => , }, + { header: 'No', accessorKey: 'id' }, + { header: 'Name', accessorKey: 'name' }, { header: 'Action', cell: ({ row }) => ( -
+
+ - {deleteId === row.original.id ? ( -
- Yakin? - - -
- ) : ( - - )}
), }, - ] + ]; const table = useReactTable({ data: permissions, columns, - state: { - pagination, - rowSelection, - }, + state: { pagination, rowSelection }, enableRowSelection: true, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), @@ -160,53 +121,61 @@ function PermissionsPage() { onPaginationChange: setPagination, pageCount: Math.ceil(totalItems / pagination.pageSize), manualPagination: true, - }) + }); return ( - -
-
-

Permissions

-
- -
-
-
+ + + +
+
+ setSearch(e.target.value)} /> -
- -
-
-
-
+
- +
+ {isLoading ? ( -
Loading...
+
+ Memuat data… +
) : ( + setPagination((prev) => ({ ...prev, pageIndex: p - 1 })) + } /> )} -
-
-
- ) + + + + !o && setDeleteId(null)} + onConfirm={() => deleteId && handleDelete(deleteId)} + title="Hapus permission ini?" + /> + + ); } diff --git a/apps/backoffice/src/routes/_authenticated/prizes.tsx b/apps/backoffice/src/routes/_authenticated/prizes.tsx index 9f18bd5..1e56f0c 100644 --- a/apps/backoffice/src/routes/_authenticated/prizes.tsx +++ b/apps/backoffice/src/routes/_authenticated/prizes.tsx @@ -1,13 +1,22 @@ -import { createFileRoute } from '@tanstack/react-router' -import * as React from 'react' -import { FC, Fragment, ReactElement, useState } from 'react' +import { createFileRoute } from '@tanstack/react-router'; +import * as React from 'react'; +import { Filter as FilterIcon, Search, ClipboardCheck } from 'lucide-react'; import { - FilterOutlined, - SearchOutlined, - AuditOutlined, -} from '@ant-design/icons' -import { Button, Input } from '@imphnen-frontend-service/ui/atoms' -import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms' + Badge, + Button, + Card, + CardContent, + CardHeader, + Input, + Popover, + PopoverContent, + PopoverTrigger, +} from '@imphnen-frontend-service/ui/atoms'; +import { + DataTable, + Filter, + BackofficeWrapper, +} from '@imphnen-frontend-service/ui/organisms'; import { ColumnDef, getCoreRowModel, @@ -15,19 +24,23 @@ import { PaginationState, useReactTable, RowSelectionState, -} from '@tanstack/react-table' -import ModalProcessDelivery from './_components/prizes/modal-process-item' +} from '@tanstack/react-table'; +import ModalProcessDelivery from './_components/prizes/modal-process-item'; +import { + SelectAllCheckbox, + RowSelectCheckbox, +} from '../../components/list-helpers'; -type OrderValid = 'valid' | 'invalid' | 'unchecked' -type Status = 'undelivered' | 'delivered' +type OrderValid = 'valid' | 'invalid' | 'unchecked'; +type Status = 'undelivered' | 'delivered'; interface Prize { - id: number - name: string - orderValid: OrderValid - items: string - address: string - status: Status + id: number; + name: string; + orderValid: OrderValid; + items: string; + address: string; + status: Status; } const items = [ @@ -37,7 +50,7 @@ const items = [ 'Sticker Isi 3', 'Sticker Isi 5', 'Gelang Karet', -] +]; const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({ id: i + 1, @@ -45,141 +58,108 @@ const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({ orderValid: (i % 3 === 0 ? 'invalid' : i % 5 === 0 - ? 'unchecked' - : 'valid') as OrderValid, + ? 'unchecked' + : 'valid') as OrderValid, items: items[i % items.length], address: 'Jl. Pantai Cibaduyut Indah', status: (i % 3 === 0 ? 'undelivered' : 'delivered') as Status, -})) +})); export const Route = createFileRoute('/_authenticated/prizes')({ component: PrizesPage, -}) +}); function PrizesPage() { const [showModalProcessDelivery, setShowModalProcessDelivery] = - useState(false) - + React.useState(false); const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 9, - }) - - const [rowSelection, setRowSelection] = React.useState({}) - const [showFilter, setShowFilter] = useState(false) + pageSize: 10, + }); + const [rowSelection, setRowSelection] = + React.useState({}); + const [showFilter, setShowFilter] = React.useState(false); const deliveryOptions = [ - { id: 'option1', value: 'undelivered', label: 'Undelivered' }, - { id: 'option1', value: 'delivered', label: 'Delivered' }, - ] + { id: 'undelivered', value: 'undelivered', label: 'Undelivered' }, + { id: 'delivered', value: 'delivered', label: 'Delivered' }, + ]; + + const orderValidVariants: Record< + OrderValid, + 'success' | 'destructive' | 'warning' + > = { + valid: 'success', + invalid: 'destructive', + unchecked: 'warning', + }; + + const orderValidText: Record = { + valid: 'Valid', + invalid: 'Invalid', + unchecked: 'Unchecked', + }; + + const statusVariants: Record = { + delivered: 'success', + undelivered: 'destructive', + }; + + const statusText: Record = { + delivered: 'Delivered', + undelivered: 'Undelivered', + }; const columns: ColumnDef[] = [ { id: 'select', - header: ({ table }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - header: 'No', - accessorKey: 'id', - }, - { - header: 'Nama Lengkap', - accessorKey: 'name', + header: ({ table }) => , + cell: ({ row }) => , }, + { header: 'No', accessorKey: 'id' }, + { header: 'Nama Lengkap', accessorKey: 'name' }, { header: 'Order Valid?', accessorKey: 'orderValid', - cell: ({ row }) => { - const status = row.original.orderValid - const statusColors: Record = { - valid: 'bg-success-200 text-success-500', - invalid: 'bg-danger-200 text-danger-500', - unchecked: 'bg-warning-200 text-warning-900', - } - const statusText: Record = { - valid: 'Valid', - invalid: 'Invalid', - unchecked: 'Unchecked', - } - return ( -
- {statusText[status]} -
- ) - }, - }, - { - header: 'Items', - accessorKey: 'items', - }, - { - header: 'Alamat Pengiriman', - accessorKey: 'address', + cell: ({ row }) => ( + + {orderValidText[row.original.orderValid]} + + ), }, + { header: 'Items', accessorKey: 'items' }, + { header: 'Alamat Pengiriman', accessorKey: 'address' }, { header: 'Status', accessorKey: 'status', - cell: ({ row }) => { - const status = row.original.status - const statusColors: Record = { - delivered: 'bg-success-200 text-success-500', - undelivered: 'bg-danger-200 text-danger-500', - } - const statusText: Record = { - delivered: 'Delivered', - undelivered: 'Undelivered', - } - return ( -
- {statusText[status]} -
- ) - }, + cell: ({ row }) => ( + + {statusText[row.original.status]} + + ), }, { header: 'Action', - cell: ({ row }) => ( + cell: () => ( ), }, - ] + ]; const table = useReactTable({ data: mockData, columns, - state: { - pagination, - rowSelection, - }, + state: { pagination, rowSelection }, enableRowSelection: true, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), @@ -187,59 +167,55 @@ function PrizesPage() { onPaginationChange: setPagination, pageCount: Math.ceil(mockData.length / pagination.pageSize), manualPagination: false, - }) + }); return ( - -
-
-

Data Pengiriman Hadiah

-
-
-
-
+ + + +
+
+ -
- -
-
-
- - {showFilter && ( -
- setShowFilter(false)} - onFilterChange={(value) => { - console.log('Selected filter:', value) - }} - /> -
- )}
+ + + + + + setShowFilter(false)} + onFilterChange={(value) => { + console.log('Selected filter:', value); + }} + /> + +
+
+ -
-
+ + setShowModalProcessDelivery(false)} handleProcessDelivery={() => { - console.log('Action ketika user menekan tombol Proses Pengiriman') + console.log('Action proses pengiriman'); }} /> -
- ) + + ); } diff --git a/apps/backoffice/src/routes/_authenticated/roadmap-dimentorin.tsx b/apps/backoffice/src/routes/_authenticated/roadmap-dimentorin.tsx index b5b0dbb..f5f8530 100644 --- a/apps/backoffice/src/routes/_authenticated/roadmap-dimentorin.tsx +++ b/apps/backoffice/src/routes/_authenticated/roadmap-dimentorin.tsx @@ -1,182 +1,162 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' -import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons' -import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms' -import { BackofficeWrapper, DataTable } from '@imphnen-frontend-service/ui/organisms' -import { cn } from '@imphnen-frontend-service/utils' -import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from '@tanstack/react-table' -import { useState } from 'react' -import { toast } from 'sonner' -import { useRoadmapList, useDeleteRoadmap, TRoadmapListItem, TRoadmapStatus } from '@imphnen-frontend-service/service' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { Search, Plus, Pencil, Trash2 } from 'lucide-react'; +import { + Badge, + Button, + Card, + CardContent, + CardHeader, + Input, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@imphnen-frontend-service/ui/atoms'; +import { + BackofficeWrapper, + DataTable, +} from '@imphnen-frontend-service/ui/organisms'; +import { cn } from '@imphnen-frontend-service/utils'; +import { + ColumnDef, + getCoreRowModel, + getPaginationRowModel, + PaginationState, + RowSelectionState, + useReactTable, +} from '@tanstack/react-table'; +import { toast } from 'sonner'; +import { + useRoadmapList, + useDeleteRoadmap, + TRoadmapListItem, + TRoadmapStatus, +} from '@imphnen-frontend-service/service'; +import { + SelectAllCheckbox, + RowSelectCheckbox, + DeleteConfirmDialog, +} from '../../components/list-helpers'; export const Route = createFileRoute('/_authenticated/roadmap-dimentorin')({ component: RoadmapDimentorinPage, -}) +}); -function RoadmapDimentorinPage(): React.ReactElement { - const navigate = useNavigate() - const [search, setSearch] = useState('') - const [statusFilter, setStatusFilter] = useState('') - const [deletingId, setDeletingId] = useState(null) - - const [rowSelection, setRowSelection] = useState({}) - const [pagination, setPagination] = useState({ +function RoadmapDimentorinPage() { + const navigate = useNavigate(); + const [search, setSearch] = React.useState(''); + const [statusFilter, setStatusFilter] = React.useState('all'); + const [deletingId, setDeletingId] = React.useState(null); + const [rowSelection, setRowSelection] = + React.useState({}); + const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 9, - }) + pageSize: 10, + }); - const { data: roadmapData, isLoading } = useRoadmapList() - const deleteRoadmap = useDeleteRoadmap() + const { data: roadmapData, isLoading } = useRoadmapList(); + const deleteRoadmap = useDeleteRoadmap(); - const allItems: TRoadmapListItem[] = roadmapData ?? [] + const allItems: TRoadmapListItem[] = roadmapData ?? []; const filteredItems = allItems.filter((item) => { - const matchSearch = !search || item.title.toLowerCase().includes(search.toLowerCase()) - const matchStatus = !statusFilter || item.status === statusFilter - return matchSearch && matchStatus - }) + const matchSearch = + !search || item.title.toLowerCase().includes(search.toLowerCase()); + const matchStatus = statusFilter === 'all' || item.status === statusFilter; + return matchSearch && matchStatus; + }); const handleDelete = async (id: string) => { try { - await deleteRoadmap.mutateAsync(id) - toast.success('Roadmap berhasil dihapus') - setDeletingId(null) + await deleteRoadmap.mutateAsync(id); + toast.success('Roadmap berhasil dihapus'); + setDeletingId(null); } catch (error) { - console.log(error) - toast.error('Gagal menghapus roadmap') + console.log(error); + toast.error('Gagal menghapus roadmap'); } - } + }; - const statusColors: Record = { - upcoming: 'bg-warning-200 text-warning-700', - in_progress: 'bg-primary-200 text-primary-700', - completed: 'bg-success-200 text-success-500', - } + const statusVariants: Record< + TRoadmapStatus, + 'warning' | 'info' | 'success' + > = { + upcoming: 'warning', + in_progress: 'info', + completed: 'success', + }; const statusText: Record = { upcoming: 'Upcoming', in_progress: 'In Progress', completed: 'Completed', - } + }; const columns: ColumnDef[] = [ { id: 'select', - meta: { cellClassName: cn('w-20') }, - header: ({ table }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - id: 'title', - header: 'Title', - accessorKey: 'title', + meta: { cellClassName: cn('w-10') }, + header: ({ table }) => , + cell: ({ row }) => , }, + { id: 'title', header: 'Title', accessorKey: 'title' }, { id: 'description', header: 'Description', accessorKey: 'description', cell: ({ row }) => ( - {row.original.description} + {row.original.description} ), }, { id: 'status', header: 'Status', accessorKey: 'status', - cell: ({ row }) => { - const status = row.original.status - return ( -
- {statusText[status] ?? status} -
- ) - }, - }, - { - id: 'votes', - header: 'Votes', - accessorKey: 'votes', + cell: ({ row }) => ( + + {statusText[row.original.status] ?? row.original.status} + + ), }, + { id: 'votes', header: 'Votes', accessorKey: 'votes' }, { header: 'Action', - meta: { cellClassName: cn('w-72') }, cell: ({ row }) => ( -
- {deletingId === row.original.id ? ( - <> - - - - ) : ( - <> - - - - )} +
+ +
), }, - ] + ]; const table = useReactTable({ data: filteredItems, columns, - state: { - pagination, - rowSelection, - }, + state: { pagination, rowSelection }, enableRowSelection: true, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), @@ -184,51 +164,64 @@ function RoadmapDimentorinPage(): React.ReactElement { onPaginationChange: setPagination, pageCount: Math.ceil(filteredItems.length / pagination.pageSize), manualPagination: false, - }) + }); return ( - -

Content & Roadmap

- -
-
-

AI Roadmaps

- -
- -
-
- setSearch(e.target.value)} - /> -
- + + + +
+
+
+ + setSearch(e.target.value)} + /> +
+
+
- -
+ + + {isLoading ? ( +
+ Memuat data… +
+ ) : ( + + )} +
+ - {isLoading ? ( -
Loading...
- ) : ( - - )} -
+ !o && setDeletingId(null)} + onConfirm={() => deletingId && handleDelete(deletingId)} + title="Hapus roadmap ini?" + />
- ) + ); } diff --git a/apps/backoffice/src/routes/_authenticated/roles.tsx b/apps/backoffice/src/routes/_authenticated/roles.tsx index ee2011e..5bd263c 100644 --- a/apps/backoffice/src/routes/_authenticated/roles.tsx +++ b/apps/backoffice/src/routes/_authenticated/roles.tsx @@ -1,13 +1,17 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' -import { Fragment, useState } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { Search, Pencil, Trash2, Plus } from 'lucide-react'; import { - SearchOutlined, - EditOutlined, - DeleteOutlined, - PlusOutlined, -} from '@ant-design/icons' -import { Button, Input } from '@imphnen-frontend-service/ui/atoms' -import { DataTable } from '@imphnen-frontend-service/ui/organisms' + Button, + Card, + CardContent, + CardHeader, + Input, +} from '@imphnen-frontend-service/ui/atoms'; +import { + DataTable, + BackofficeWrapper, +} from '@imphnen-frontend-service/ui/organisms'; import { ColumnDef, getCoreRowModel, @@ -15,144 +19,98 @@ import { PaginationState, RowSelectionState, useReactTable, -} from '@tanstack/react-table' +} from '@tanstack/react-table'; import { useRoleList, useDeleteRole, TRolesListItem, -} from '@imphnen-frontend-service/service' -import React from 'react' -import { toast } from 'sonner' +} from '@imphnen-frontend-service/service'; +import { toast } from 'sonner'; +import { + SelectAllCheckbox, + RowSelectCheckbox, + DeleteConfirmDialog, +} from '../../components/list-helpers'; export const Route = createFileRoute('/_authenticated/roles')({ component: RolesPage, -}) +}); function RolesPage() { - const navigate = useNavigate() - const [search, setSearch] = useState('') - const [deleteId, setDeleteId] = useState(null) - + const navigate = useNavigate(); + const [search, setSearch] = React.useState(''); + const [deleteId, setDeleteId] = React.useState(null); const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 9, - }) - - const [rowSelection, setRowSelection] = React.useState({}) + pageSize: 10, + }); + const [rowSelection, setRowSelection] = + React.useState({}); const { data: rolesData, isLoading } = useRoleList({ search, page: pagination.pageIndex + 1, per_page: pagination.pageSize, - }) - const deleteRole = useDeleteRole() + }); + const deleteRole = useDeleteRole(); - const roles: TRolesListItem[] = rolesData?.data ?? [] - const totalItems = rolesData?.meta?.total ?? roles.length + const roles: TRolesListItem[] = rolesData?.data ?? []; + const totalItems = rolesData?.meta?.total ?? roles.length; const handleDelete = async (id: string) => { try { - await deleteRole.mutateAsync(id) - toast.success('Data role berhasil dihapus') - setDeleteId(null) + await deleteRole.mutateAsync(id); + toast.success('Data role berhasil dihapus'); + setDeleteId(null); } catch (error) { - console.log(error) - toast.error('Data role gagal dihapus') + console.log(error); + toast.error('Data role gagal dihapus'); } - } + }; const columns: ColumnDef[] = [ { id: 'select', - header: ({ table }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - header: 'ID', - accessorKey: 'id', - }, - { - header: 'Roles Name', - accessorKey: 'name', + header: ({ table }) => , + cell: ({ row }) => , }, + { header: 'ID', accessorKey: 'id' }, + { header: 'Roles Name', accessorKey: 'name' }, { header: 'Action', cell: ({ row }) => ( -
+
+ - {deleteId === row.original.id ? ( -
- Yakin? - - -
- ) : ( - - )}
), }, - ] + ]; const table = useReactTable({ data: roles, columns, - state: { - pagination, - rowSelection, - }, + state: { pagination, rowSelection }, enableRowSelection: true, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), @@ -160,53 +118,58 @@ function RolesPage() { onPaginationChange: setPagination, pageCount: Math.ceil(totalItems / pagination.pageSize), manualPagination: true, - }) + }); return ( - -
-
-

Roles

-
- -
-
-
+ + + +
+
+ setSearch(e.target.value)} /> -
- -
-
-
-
+
- +
+ {isLoading ? ( -
Loading...
+
+ Memuat data… +
) : ( + setPagination((prev) => ({ ...prev, pageIndex: p - 1 })) + } /> )} -
-
-
- ) + + + + !o && setDeleteId(null)} + onConfirm={() => deleteId && handleDelete(deleteId)} + title="Hapus role ini?" + /> + + ); } diff --git a/apps/backoffice/src/routes/_authenticated/session-dimentorin.tsx b/apps/backoffice/src/routes/_authenticated/session-dimentorin.tsx index d847495..c55754c 100644 --- a/apps/backoffice/src/routes/_authenticated/session-dimentorin.tsx +++ b/apps/backoffice/src/routes/_authenticated/session-dimentorin.tsx @@ -1,64 +1,82 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' -import { SearchOutlined } from '@ant-design/icons' -import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms' -import { BackofficeWrapper, DataTable } from '@imphnen-frontend-service/ui/organisms' -import { cn } from '@imphnen-frontend-service/utils' -import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from '@tanstack/react-table' -import { ReactElement, useState } from 'react' -import { useMySessions, TSessionListItem } from '@imphnen-frontend-service/service' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { Search, Eye } from 'lucide-react'; +import { + Badge, + Button, + Card, + CardContent, + CardHeader, + Input, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@imphnen-frontend-service/ui/atoms'; +import { + BackofficeWrapper, + DataTable, +} from '@imphnen-frontend-service/ui/organisms'; +import { cn } from '@imphnen-frontend-service/utils'; +import { + ColumnDef, + getCoreRowModel, + getPaginationRowModel, + PaginationState, + RowSelectionState, + useReactTable, +} from '@tanstack/react-table'; +import { + useMySessions, + TSessionListItem, +} from '@imphnen-frontend-service/service'; +import { + SelectAllCheckbox, + RowSelectCheckbox, +} from '../../components/list-helpers'; export const Route = createFileRoute('/_authenticated/session-dimentorin')({ component: SessionDimentorinPage, -}) +}); -function SessionDimentorinPage(): ReactElement { - const navigate = useNavigate() - const [statusFilter, setStatusFilter] = useState('') - - const [rowSelection, setRowSelection] = useState({}) - const [pagination, setPagination] = useState({ +function SessionDimentorinPage() { + const navigate = useNavigate(); + const [statusFilter, setStatusFilter] = React.useState('all'); + const [rowSelection, setRowSelection] = + React.useState({}); + const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 9, - }) + pageSize: 10, + }); const { data: sessionsData, isLoading } = useMySessions( - statusFilter ? { status: statusFilter } : undefined - ) + statusFilter !== 'all' ? { status: statusFilter } : undefined + ); - const sessions: TSessionListItem[] = sessionsData?.sessions ?? [] - const totalItems = sessionsData?.total ?? sessions.length + const sessions: TSessionListItem[] = sessionsData?.sessions ?? []; + const totalItems = sessionsData?.total ?? sessions.length; + + const statusVariants: Record< + string, + 'warning' | 'info' | 'success' | 'destructive' | 'secondary' + > = { + pending: 'warning', + confirmed: 'info', + ongoing: 'warning', + completed: 'success', + cancelled: 'destructive', + }; const columns: ColumnDef[] = [ { id: 'select', - meta: { cellClassName: cn('w-20') }, - header: ({ table }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - id: 'id', - header: 'ID Sesi', - accessorKey: 'id', - }, - { - id: 'mentorId', - header: 'Nama Mentor', - accessorKey: 'mentor_id', + meta: { cellClassName: cn('w-10') }, + header: ({ table }) => , + cell: ({ row }) => , }, + { id: 'id', header: 'ID Sesi', accessorKey: 'id' }, + { id: 'mentorId', header: 'Nama Mentor', accessorKey: 'mentor_id' }, { id: 'menteeName', header: 'Nama Mentee', @@ -69,55 +87,49 @@ function SessionDimentorinPage(): ReactElement { header: 'Waktu', accessorKey: 'scheduled_at', cell: ({ row }) => ( - {new Date(row.original.scheduled_at).toLocaleString('id-ID')} + + {new Date(row.original.scheduled_at).toLocaleString('id-ID')} + ), }, { id: 'status', header: 'Status', accessorKey: 'status', - cell: ({ row }) => { - const status = row.original.status - const statusColors: Record = { - pending: 'bg-warning-200 text-warning-700', - confirmed: 'bg-primary-200 text-primary-700', - ongoing: 'bg-warning-200 text-warning-700', - completed: 'bg-success-200 text-success-500', - cancelled: 'bg-danger-200 text-danger-500', - } - return ( -
- {status} -
- ) - }, + cell: ({ row }) => ( + + {row.original.status} + + ), }, { header: 'Action', - meta: { cellClassName: cn('w-52') }, cell: ({ row }) => ( ), }, - ] + ]; const table = useReactTable({ data: sessions, columns, - state: { - pagination, - rowSelection, - }, + state: { pagination, rowSelection }, enableRowSelection: true, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), @@ -125,39 +137,55 @@ function SessionDimentorinPage(): ReactElement { onPaginationChange: setPagination, pageCount: Math.ceil(totalItems / pagination.pageSize), manualPagination: true, - }) + }); return ( - -

Session Management

- -
-
-
- -
- + + + +
+
+ +
+
- -
- - {isLoading ? ( -
Loading...
- ) : ( - - )} -
+ + + {isLoading ? ( +
+ Memuat data… +
+ ) : ( + + setPagination((prev) => ({ ...prev, pageIndex: p - 1 })) + } + /> + )} +
+
- ) + ); } diff --git a/apps/backoffice/src/routes/_authenticated/session-dimentorin_/$id.tsx b/apps/backoffice/src/routes/_authenticated/session-dimentorin_/$id.tsx index c895a4b..ac6ad20 100644 --- a/apps/backoffice/src/routes/_authenticated/session-dimentorin_/$id.tsx +++ b/apps/backoffice/src/routes/_authenticated/session-dimentorin_/$id.tsx @@ -1,6 +1,6 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router' import { ArrowLeftOutlined } from '@ant-design/icons' -import { Input, Select, Textarea } from '@imphnen-frontend-service/ui/atoms' +import { Input, NativeSelect as Select, Textarea } from '@imphnen-frontend-service/ui/atoms' import { cn, For } from '@imphnen-frontend-service/utils' import { useMySessions, TSessionListItem } from '@imphnen-frontend-service/service' diff --git a/apps/backoffice/src/routes/_authenticated/settings-dimentorin.tsx b/apps/backoffice/src/routes/_authenticated/settings-dimentorin.tsx index af027c3..7139f6a 100644 --- a/apps/backoffice/src/routes/_authenticated/settings-dimentorin.tsx +++ b/apps/backoffice/src/routes/_authenticated/settings-dimentorin.tsx @@ -1,58 +1,74 @@ -import { createFileRoute } from '@tanstack/react-router' -import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms' -import { cn, For } from '@imphnen-frontend-service/utils' -import { useState } from 'react' -import { GeneralSettings } from './_components/settings-dimentorin/general' -import { UserRolesPermission } from './_components/settings-dimentorin/user-roles-permission' -import { NotificationSettings } from './_components/settings-dimentorin/notification' -import { SecuritySettings } from './_components/settings-dimentorin/security' -import { PaymentSettings } from './_components/settings-dimentorin/payment' - -const TABS = { - general: 'General Settings', - userRolePermissions: 'User Roles & Permissions', - notification: 'Notification Settings', - security: 'Security', - payment: 'Payment', -} as const -type Tabs = typeof TABS[keyof typeof TABS] +import { createFileRoute } from '@tanstack/react-router'; +import * as React from 'react'; +import { + Card, + CardContent, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@imphnen-frontend-service/ui/atoms'; +import { BackofficeWrapper } from '@imphnen-frontend-service/ui/organisms'; +import { GeneralSettings } from './_components/settings-dimentorin/general'; +import { UserRolesPermission } from './_components/settings-dimentorin/user-roles-permission'; +import { NotificationSettings } from './_components/settings-dimentorin/notification'; +import { SecuritySettings } from './_components/settings-dimentorin/security'; +import { PaymentSettings } from './_components/settings-dimentorin/payment'; export const Route = createFileRoute('/_authenticated/settings-dimentorin')({ component: SettingsDimentorinPage, -}) +}); function SettingsDimentorinPage(): React.ReactElement { - const [activeTab, setActiveTab] = useState(TABS.general) - return ( - -

Settings

- -
-
- - {(tab) => ( - - )} - -
-
- {activeTab === TABS.general && } - {activeTab === TABS.userRolePermissions && } - {activeTab === TABS.notification && } - {activeTab === TABS.security && } - {activeTab === TABS.payment && } -
-
+ + + + General + Roles & Permissions + Notification + Security + Payment + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - ) + ); } diff --git a/apps/backoffice/src/routes/_authenticated/transactions.tsx b/apps/backoffice/src/routes/_authenticated/transactions.tsx index d943aee..61ab2c5 100644 --- a/apps/backoffice/src/routes/_authenticated/transactions.tsx +++ b/apps/backoffice/src/routes/_authenticated/transactions.tsx @@ -1,13 +1,22 @@ -import { createFileRoute } from '@tanstack/react-router' -import * as React from 'react' -import { FC, Fragment, ReactElement, useState } from 'react' +import { createFileRoute } from '@tanstack/react-router'; +import * as React from 'react'; +import { Filter as FilterIcon, Search, ClipboardCheck } from 'lucide-react'; import { - FilterOutlined, - SearchOutlined, - AuditOutlined, -} from '@ant-design/icons' -import { Button, Input } from '@imphnen-frontend-service/ui/atoms' -import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms' + Badge, + Button, + Card, + CardContent, + CardHeader, + Input, + Popover, + PopoverContent, + PopoverTrigger, +} from '@imphnen-frontend-service/ui/atoms'; +import { + DataTable, + Filter, + BackofficeWrapper, +} from '@imphnen-frontend-service/ui/organisms'; import { ColumnDef, getCoreRowModel, @@ -15,131 +24,111 @@ import { PaginationState, useReactTable, RowSelectionState, -} from '@tanstack/react-table' -import ModalValidate from './_components/transactions/modal-validate' +} from '@tanstack/react-table'; +import ModalValidate from './_components/transactions/modal-validate'; +import { + SelectAllCheckbox, + RowSelectCheckbox, +} from '../../components/list-helpers'; -type TransactionStatus = 'valid' | 'invalid' | 'unchecked' +type TransactionStatus = 'valid' | 'invalid' | 'unchecked'; interface Transaction { - id: number - name: string - transactionNumber: string - status: TransactionStatus + id: number; + name: string; + transactionNumber: string; + status: TransactionStatus; } -const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({ - id: i + 1, - name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap', - transactionNumber: '25D2133Y9AFYBD', - status: (i % 3 === 0 - ? 'invalid' - : i % 5 === 0 - ? 'unchecked' - : 'valid') as TransactionStatus, -})) +const mockTransactions: Transaction[] = Array.from( + { length: 20 }, + (_, i) => ({ + id: i + 1, + name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap', + transactionNumber: '25D2133Y9AFYBD', + status: (i % 3 === 0 + ? 'invalid' + : i % 5 === 0 + ? 'unchecked' + : 'valid') as TransactionStatus, + }) +); export const Route = createFileRoute('/_authenticated/transactions')({ component: TransactionsPage, -}) +}); function TransactionsPage() { - const [showModalValidate, setShowModalValidate] = useState(false) - + const [showModalValidate, setShowModalValidate] = React.useState(false); const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 9, - }) - - const [rowSelection, setRowSelection] = React.useState({}) - const [showFilter, setShowFilter] = useState(false) + pageSize: 10, + }); + const [rowSelection, setRowSelection] = + React.useState({}); + const [showFilter, setShowFilter] = React.useState(false); const validationOptions = [ - { id: 'option1', value: 'unchecked', label: 'Unchecked' }, - { id: 'option2', value: 'valid', label: 'Valid' }, - { id: 'option3', value: 'invalid', label: 'Invalid' }, - ] + { id: 'unchecked', value: 'unchecked', label: 'Unchecked' }, + { id: 'valid', value: 'valid', label: 'Valid' }, + { id: 'invalid', value: 'invalid', label: 'Invalid' }, + ]; + + const statusVariants: Record< + TransactionStatus, + 'success' | 'destructive' | 'warning' + > = { + valid: 'success', + invalid: 'destructive', + unchecked: 'warning', + }; + + const statusText: Record = { + valid: 'Valid', + invalid: 'Invalid', + unchecked: 'Unchecked', + }; const columns: ColumnDef[] = [ { id: 'select', - header: ({ table }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - header: 'No', - accessorKey: 'id', - }, - { - header: 'Nama Lengkap', - accessorKey: 'name', - }, - { - header: 'Nomor Transaksi', - accessorKey: 'transactionNumber', + header: ({ table }) => , + cell: ({ row }) => , }, + { header: 'No', accessorKey: 'id' }, + { header: 'Nama Lengkap', accessorKey: 'name' }, + { header: 'Nomor Transaksi', accessorKey: 'transactionNumber' }, { header: 'Order Valid?', accessorKey: 'status', - cell: ({ row }) => { - const status = row.original.status - const statusColors: Record = { - valid: 'bg-success-200 text-success-500', - invalid: 'bg-danger-200 text-danger-500', - unchecked: 'bg-warning-200 text-warning-900', - } - const statusText: Record = { - valid: 'Valid', - invalid: 'Invalid', - unchecked: 'Unchecked', - } - return ( -
- {statusText[status]} -
- ) - }, + cell: ({ row }) => ( + + {statusText[row.original.status]} + + ), }, { header: 'Action', - cell: ({ row }) => ( + cell: () => ( ), }, - ] + ]; const table = useReactTable({ data: mockTransactions, columns, - state: { - pagination, - rowSelection, - }, + state: { pagination, rowSelection }, enableRowSelection: true, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), @@ -147,64 +136,58 @@ function TransactionsPage() { onPaginationChange: setPagination, pageCount: Math.ceil(mockTransactions.length / pagination.pageSize), manualPagination: false, - }) + }); return ( - -
-
-

Validasi Transaksi

-
- -
-
-
+ + + +
+
+ -
- -
-
- -
- - {showFilter && ( -
- setShowFilter(false)} - onFilterChange={(value) => { - console.log('Selected filter:', value) - }} - /> -
- )}
+ + + + + + setShowFilter(false)} + onFilterChange={(value) => { + console.log('Selected filter:', value); + }} + /> + +
- +
+ -
-
+ + + setShowModalValidate(false)} handleValid={() => { - console.log('Action ketika user klik Valid') + console.log('Action ketika user klik Valid'); }} handleInvalid={() => { - console.log('Action ketika user klik Tidak Valid') + console.log('Action ketika user klik Tidak Valid'); }} /> -
- ) +
+ ); } diff --git a/apps/backoffice/src/routes/_authenticated/users-dimentorin.tsx b/apps/backoffice/src/routes/_authenticated/users-dimentorin.tsx index 7c4c2be..e4f16f0 100644 --- a/apps/backoffice/src/routes/_authenticated/users-dimentorin.tsx +++ b/apps/backoffice/src/routes/_authenticated/users-dimentorin.tsx @@ -1,11 +1,23 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' -import { DeleteOutlined, SearchOutlined } from '@ant-design/icons' -import { Button, Input } from '@imphnen-frontend-service/ui/atoms' +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import * as React from 'react'; +import { Eye, Search, Trash2 } from 'lucide-react'; +import { + Badge, + Button, + Card, + CardContent, + CardHeader, + Input, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@imphnen-frontend-service/ui/atoms'; import { BackofficeWrapper, DataTable, -} from '@imphnen-frontend-service/ui/organisms' -import { cn, For } from '@imphnen-frontend-service/utils' +} from '@imphnen-frontend-service/ui/organisms'; +import { cn } from '@imphnen-frontend-service/utils'; import { ColumnDef, getCoreRowModel, @@ -13,98 +25,85 @@ import { PaginationState, RowSelectionState, useReactTable, -} from '@tanstack/react-table' -import { ReactElement, useState } from 'react' -import { toast } from 'sonner' +} from '@tanstack/react-table'; +import { toast } from 'sonner'; import { useMentorList, useUserList, useDeleteMentor, MentorDetailResponseDto, TUsersListItem, -} from '@imphnen-frontend-service/service' +} from '@imphnen-frontend-service/service'; +import { + SelectAllCheckbox, + RowSelectCheckbox, + DeleteConfirmDialog, +} from '../../components/list-helpers'; export const Route = createFileRoute('/_authenticated/users-dimentorin')({ component: UsersDimentorinPage, -}) +}); -function UsersDimentorinPage(): ReactElement { - const TABS = ['mentor', 'mentee'] as const - const navigate = useNavigate() - const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor') - const [search, setSearch] = useState('') - const [deletingId, setDeletingId] = useState(null) - - const [rowSelection, setRowSelection] = useState({}) - const [pagination, setPagination] = useState({ +function UsersDimentorinPage() { + const navigate = useNavigate(); + const [activeTab, setActiveTab] = React.useState<'mentor' | 'mentee'>( + 'mentor' + ); + const [search, setSearch] = React.useState(''); + const [deletingId, setDeletingId] = React.useState(null); + const [rowSelection, setRowSelection] = + React.useState({}); + const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 9, - }) + pageSize: 10, + }); const { data: mentorData, isLoading: mentorLoading } = useMentorList({ search, page: pagination.pageIndex + 1, per_page: pagination.pageSize, - }) - + }); const { data: menteeData, isLoading: menteeLoading } = useUserList({ search, page: pagination.pageIndex + 1, per_page: pagination.pageSize, - }) + }); + const deleteMentor = useDeleteMentor(); - const deleteMentor = useDeleteMentor() - - const mentors: MentorDetailResponseDto[] = mentorData?.data ?? [] - const mentees: TUsersListItem[] = menteeData?.data ?? [] - const mentorTotal = mentorData?.meta?.total ?? mentors.length - const menteeTotal = menteeData?.meta?.total ?? mentees.length - - const isLoading = activeTab === 'mentor' ? mentorLoading : menteeLoading - const totalItems = activeTab === 'mentor' ? mentorTotal : menteeTotal + const mentors: MentorDetailResponseDto[] = mentorData?.data ?? []; + const mentees: TUsersListItem[] = menteeData?.data ?? []; + const mentorTotal = mentorData?.meta?.total ?? mentors.length; + const menteeTotal = menteeData?.meta?.total ?? mentees.length; const handleDelete = async (id: string) => { try { - await deleteMentor.mutateAsync(id) - toast.success('Akun berhasil dihapus') - setDeletingId(null) + await deleteMentor.mutateAsync(id); + toast.success('Akun berhasil dihapus'); + setDeletingId(null); } catch (error) { - console.log(error) - toast.error('Gagal menghapus akun') + console.log(error); + toast.error('Gagal menghapus akun'); } - } + }; + + const statusVariantMap: Record< + string, + 'success' | 'warning' | 'destructive' | 'secondary' + > = { + active: 'success', + pending: 'warning', + inactive: 'destructive', + }; const mentorColumns: ColumnDef[] = [ { id: 'select', - meta: { cellClassName: cn('w-20') }, - header: ({ table }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - id: 'name', - header: 'Name', - accessorKey: 'fullname', - }, - { - id: 'email', - header: 'Email', - accessorKey: 'email', + meta: { cellClassName: cn('w-10') }, + header: ({ table }) => , + cell: ({ row }) => , }, + { id: 'name', header: 'Name', accessorKey: 'fullname' }, + { id: 'email', header: 'Email', accessorKey: 'email' }, { id: 'rating', header: 'Rating', @@ -116,139 +115,86 @@ function UsersDimentorinPage(): ReactElement { header: 'Status', accessorKey: 'status', cell: ({ row }) => { - const status = row.original.status - const statusColors: Record = { - active: 'bg-success-200 text-success-500', - pending: 'bg-warning-200 text-warning-700', - inactive: 'bg-danger-200 text-danger-500', - } + const status = row.original.status ?? ''; return ( -
- {status} -
- ) + + {status || 'unknown'} + + ); }, }, { header: 'Action', - meta: { cellClassName: cn('w-72') }, cell: ({ row }) => ( -
- {deletingId === row.original.id ? ( - <> - - - - ) : ( - <> - - - - )} +
+ +
), }, - ] + ]; const menteeColumns: ColumnDef[] = [ { id: 'select', - meta: { cellClassName: cn('w-20') }, - header: ({ table }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - id: 'name', - header: 'Name', - accessorKey: 'fullname', - }, - { - id: 'email', - header: 'Email', - accessorKey: 'email', + meta: { cellClassName: cn('w-10') }, + header: ({ table }) => , + cell: ({ row }) => , }, + { id: 'name', header: 'Name', accessorKey: 'fullname' }, + { id: 'email', header: 'Email', accessorKey: 'email' }, { id: 'status', header: 'Status', accessorKey: 'is_active', cell: ({ row }) => ( -
+ {row.original.is_active ? 'Active' : 'Inactive'} -
+ ), }, { header: 'Action', - meta: { cellClassName: cn('w-72') }, cell: ({ row }) => ( ), }, - ] + ]; const mentorTable = useReactTable({ data: mentors, @@ -261,7 +207,7 @@ function UsersDimentorinPage(): ReactElement { onPaginationChange: setPagination, pageCount: Math.ceil(mentorTotal / pagination.pageSize), manualPagination: true, - }) + }); const menteeTable = useReactTable({ data: mentees, @@ -274,59 +220,91 @@ function UsersDimentorinPage(): ReactElement { onPaginationChange: setPagination, pageCount: Math.ceil(menteeTotal / pagination.pageSize), manualPagination: true, - }) + }); return ( - -
-

- User Management -

-
- - {(tab) => ( - - )} - -
-
- -
-
-
- setSearch(e.target.value)} - /> -
- + + + +
+
+ + setSearch(e.target.value)} + />
-
+ + + { + setActiveTab(v as 'mentor' | 'mentee'); + setPagination((p) => ({ ...p, pageIndex: 0 })); + }} + > + + + Mentor ({mentorTotal}) + + + Mentee ({menteeTotal}) + + + + {mentorLoading ? ( +
+ Memuat data… +
+ ) : ( + + setPagination((prev) => ({ ...prev, pageIndex: p - 1 })) + } + /> + )} +
+ + {menteeLoading ? ( +
+ Memuat data… +
+ ) : ( + + setPagination((prev) => ({ ...prev, pageIndex: p - 1 })) + } + /> + )} +
+
+
+ - {isLoading ? ( -
Loading...
- ) : activeTab === 'mentor' ? ( - - ) : ( - - )} -
+ !o && setDeletingId(null)} + onConfirm={() => deletingId && handleDelete(deletingId)} + title="Hapus akun mentor ini?" + />
- ) + ); } diff --git a/apps/dimentorin/src/routes/_public/auth/register-mentor.tsx b/apps/dimentorin/src/routes/_public/auth/register-mentor.tsx index d1966cd..12e6965 100644 --- a/apps/dimentorin/src/routes/_public/auth/register-mentor.tsx +++ b/apps/dimentorin/src/routes/_public/auth/register-mentor.tsx @@ -1,7 +1,7 @@ import { createFileRoute } from '@tanstack/react-router' import { FC, ReactElement, useState } from 'react' import { ControlledInputField, RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms' -import { Button, Select } from '@imphnen-frontend-service/ui/atoms' +import { Button } from '@imphnen-frontend-service/ui/atoms' import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons' import { InputField, RegisterMentorStep, SelectField } from '@imphnen-frontend-service/ui/molecules' diff --git a/apps/dimentorin/src/routes/_site/mentoring_/$id/_components/modals/appointment/steps/schedule.tsx b/apps/dimentorin/src/routes/_site/mentoring_/$id/_components/modals/appointment/steps/schedule.tsx index 918c2dc..41d73a1 100644 --- a/apps/dimentorin/src/routes/_site/mentoring_/$id/_components/modals/appointment/steps/schedule.tsx +++ b/apps/dimentorin/src/routes/_site/mentoring_/$id/_components/modals/appointment/steps/schedule.tsx @@ -1,4 +1,4 @@ -import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms" +import { Input, NativeSelect as Select, Textarea } from "@imphnen-frontend-service/ui/atoms" import { cn } from "@imphnen-frontend-service/utils" import { motion } from "framer-motion" diff --git a/apps/dimentorin/src/routes/_site/profile_/_components/profile/profile-sidebar.tsx b/apps/dimentorin/src/routes/_site/profile_/_components/profile/profile-sidebar.tsx index d956fa1..df8caf9 100644 --- a/apps/dimentorin/src/routes/_site/profile_/_components/profile/profile-sidebar.tsx +++ b/apps/dimentorin/src/routes/_site/profile_/_components/profile/profile-sidebar.tsx @@ -1,5 +1,5 @@ import { FC, useState, useEffect, useCallback } from 'react'; -import { Select } from '@imphnen-frontend-service/ui/atoms'; +import { NativeSelect as Select } from '@imphnen-frontend-service/ui/atoms'; import { SectionWrapper } from '../shared/section-wrapper'; import { NotificationType } from '../modals/notification-modal'; import { PersonalInfoSection } from '../sections/personal-info-section'; diff --git a/components.json b/components.json new file mode 100644 index 0000000..d7cce25 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "libs/ui/src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@imphnen-frontend-service/ui", + "utils": "@imphnen-frontend-service/utils", + "ui": "@imphnen-frontend-service/ui/atoms", + "lib": "@imphnen-frontend-service/utils", + "hooks": "@imphnen-frontend-service/utils" + }, + "iconLibrary": "lucide" +} diff --git a/libs/ui/src/atoms/alert-dialog/alert-dialog.tsx b/libs/ui/src/atoms/alert-dialog/alert-dialog.tsx new file mode 100644 index 0000000..098c9e1 --- /dev/null +++ b/libs/ui/src/atoms/alert-dialog/alert-dialog.tsx @@ -0,0 +1,158 @@ +'use client'; + +import * as React from 'react'; +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; +import { cn } from '@imphnen-frontend-service/utils'; +import { buttonVariants } from '../button/button'; + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ); +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/libs/ui/src/atoms/alert-dialog/index.ts b/libs/ui/src/atoms/alert-dialog/index.ts new file mode 100644 index 0000000..a576cca --- /dev/null +++ b/libs/ui/src/atoms/alert-dialog/index.ts @@ -0,0 +1 @@ +export * from './alert-dialog'; diff --git a/libs/ui/src/atoms/avatar/avatar.tsx b/libs/ui/src/atoms/avatar/avatar.tsx new file mode 100644 index 0000000..8d3411a --- /dev/null +++ b/libs/ui/src/atoms/avatar/avatar.tsx @@ -0,0 +1,52 @@ +'use client'; + +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; +import { cn } from '@imphnen-frontend-service/utils'; + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/libs/ui/src/atoms/avatar/index.ts b/libs/ui/src/atoms/avatar/index.ts new file mode 100644 index 0000000..a7424ca --- /dev/null +++ b/libs/ui/src/atoms/avatar/index.ts @@ -0,0 +1 @@ +export * from './avatar'; diff --git a/libs/ui/src/atoms/badge/badge.tsx b/libs/ui/src/atoms/badge/badge.tsx new file mode 100644 index 0000000..ef0d3fe --- /dev/null +++ b/libs/ui/src/atoms/badge/badge.tsx @@ -0,0 +1,52 @@ +'use client'; + +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@imphnen-frontend-service/utils'; + +const badgeVariants = cva( + 'inline-flex items-center justify-center gap-1 rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap transition-colors overflow-hidden [&>svg]:size-3 [&>svg]:pointer-events-none', + { + variants: { + variant: { + default: + 'border-transparent bg-primary-500 text-white', + secondary: + 'border-transparent bg-neutral-100 text-neutral-700', + success: + 'border-transparent bg-success-100 text-success-700', + warning: + 'border-transparent bg-warning-100 text-warning-800', + info: + 'border-transparent bg-info-100 text-info-700', + destructive: + 'border-transparent bg-danger-100 text-danger-700', + outline: + 'border-neutral-200 bg-background text-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span'; + return ( + + ); +} + +export { Badge, badgeVariants }; diff --git a/libs/ui/src/atoms/badge/index.ts b/libs/ui/src/atoms/badge/index.ts new file mode 100644 index 0000000..1566eee --- /dev/null +++ b/libs/ui/src/atoms/badge/index.ts @@ -0,0 +1 @@ +export * from './badge'; diff --git a/libs/ui/src/atoms/breadcrumb/breadcrumb.tsx b/libs/ui/src/atoms/breadcrumb/breadcrumb.tsx new file mode 100644 index 0000000..7823855 --- /dev/null +++ b/libs/ui/src/atoms/breadcrumb/breadcrumb.tsx @@ -0,0 +1,107 @@ +'use client'; + +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { ChevronRight, MoreHorizontal } from 'lucide-react'; +import { cn } from '@imphnen-frontend-service/utils'; + +function Breadcrumb(props: React.ComponentProps<'nav'>) { + return
+ ); + } +); +Input.displayName = 'Input'; diff --git a/libs/ui/src/atoms/popover/index.ts b/libs/ui/src/atoms/popover/index.ts new file mode 100644 index 0000000..1f4904c --- /dev/null +++ b/libs/ui/src/atoms/popover/index.ts @@ -0,0 +1 @@ +export * from './popover'; diff --git a/libs/ui/src/atoms/popover/popover.tsx b/libs/ui/src/atoms/popover/popover.tsx new file mode 100644 index 0000000..60b4f4b --- /dev/null +++ b/libs/ui/src/atoms/popover/popover.tsx @@ -0,0 +1,49 @@ +'use client'; + +import * as React from 'react'; +import * as PopoverPrimitive from '@radix-ui/react-popover'; +import { cn } from '@imphnen-frontend-service/utils'; + +function Popover({ + ...props +}: React.ComponentProps) { + return ; +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function PopoverContent({ + className, + align = 'center', + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return ; +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; diff --git a/libs/ui/src/atoms/radio-group/index.ts b/libs/ui/src/atoms/radio-group/index.ts new file mode 100644 index 0000000..ed40543 --- /dev/null +++ b/libs/ui/src/atoms/radio-group/index.ts @@ -0,0 +1 @@ +export * from './radio-group'; diff --git a/libs/ui/src/atoms/radio-group/radio-group.tsx b/libs/ui/src/atoms/radio-group/radio-group.tsx new file mode 100644 index 0000000..ea2bb6a --- /dev/null +++ b/libs/ui/src/atoms/radio-group/radio-group.tsx @@ -0,0 +1,47 @@ +'use client'; + +import * as React from 'react'; +import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; +import { Circle } from 'lucide-react'; +import { cn } from '@imphnen-frontend-service/utils'; + +function RadioGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ); +} + +export { RadioGroup, RadioGroupItem }; diff --git a/libs/ui/src/atoms/scroll-area/index.ts b/libs/ui/src/atoms/scroll-area/index.ts new file mode 100644 index 0000000..cb8dbd1 --- /dev/null +++ b/libs/ui/src/atoms/scroll-area/index.ts @@ -0,0 +1 @@ +export * from './scroll-area'; diff --git a/libs/ui/src/atoms/scroll-area/scroll-area.tsx b/libs/ui/src/atoms/scroll-area/scroll-area.tsx new file mode 100644 index 0000000..235265d --- /dev/null +++ b/libs/ui/src/atoms/scroll-area/scroll-area.tsx @@ -0,0 +1,57 @@ +'use client'; + +import * as React from 'react'; +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; +import { cn } from '@imphnen-frontend-service/utils'; + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ); +} + +function ScrollBar({ + className, + orientation = 'vertical', + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { ScrollArea, ScrollBar }; diff --git a/libs/ui/src/atoms/select/select.tsx b/libs/ui/src/atoms/select/select.tsx index 6099cd6..26f2b93 100644 --- a/libs/ui/src/atoms/select/select.tsx +++ b/libs/ui/src/atoms/select/select.tsx @@ -1,50 +1,248 @@ -import { - FC, - ReactElement, - SelectHTMLAttributes, -} from 'react'; +'use client'; + +import * as React from 'react'; +import * as SelectPrimitive from '@radix-ui/react-select'; +import { Check, ChevronDown, ChevronUp } from 'lucide-react'; import { cn } from '@imphnen-frontend-service/utils'; -type TSelectSize = 'sm' | 'md' | 'lg'; -type Width = 'standard' | 'custom'; +/* ------------------------------------------------------------------ */ +/* Radix-backed shadcn Select */ +/* ------------------------------------------------------------------ */ -type TSelectProps = Omit< - SelectHTMLAttributes, - 'size' -> & { - size?: TSelectSize; - widthform?: Width; - disabled?: boolean; -}; +function Select({ + ...props +}: React.ComponentProps) { + return ; +} -const sizeClasses: Record = { - sm: 'text-[10px] max-h-[34px]', - md: 'text-[12px] max-h-[36px]', - lg: 'text-[15px] max-h-[38px]', -}; +function SelectGroup({ + ...props +}: React.ComponentProps) { + return ; +} -const disabledClass = - 'opacity-50 hover:border-neutral-200 cursor-not-allowed'; +function SelectValue({ + ...props +}: React.ComponentProps) { + return ; +} -export const Select: FC = ({ +function SelectTrigger({ + className, size = 'md', - widthform = 'standard', - disabled, + children, + ...props +}: React.ComponentProps & { + size?: 'sm' | 'md' | 'lg'; +}) { + const sizeCls = + size === 'sm' ? 'h-8 text-xs' : size === 'lg' ? 'h-11 text-base' : 'h-9 text-sm'; + return ( + + {children} + + + + + ); +} + +function SelectContent({ className, children, - ...rest -}): ReactElement => { - const mergedClassName = cn( - `appearance-none px-[12px] py-[8px] text-neutral-800 bg-white placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md`, - sizeClasses[size], - widthform === 'standard' && 'min-w-70', - disabled && disabledClass, - className - ); - + position = 'popper', + ...props +}: React.ComponentProps) { return ( - for callers */ +/* that still pass