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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
53079fe868
commit
63dfb7299f
@@ -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<TeamType | null>(null)
|
||||
useState<TeamType | null>(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<TeamType | null>(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<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSearch()
|
||||
}
|
||||
},
|
||||
[handleSearch]
|
||||
)
|
||||
const filteredData = React.useMemo<TeamType[]>(() => {
|
||||
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<TeamType>[] = useMemo(
|
||||
const columns: ColumnDef<TeamType>[] = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Team',
|
||||
cell: ({ row }) => {
|
||||
const team = row.original
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-neutral-100 flex items-center justify-center shrink-0 overflow-hidden">
|
||||
{team.logo ? (
|
||||
<img
|
||||
src={team.logo}
|
||||
alt={team.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<TeamOutlined className="text-neutral-400 text-lg" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p
|
||||
className="font-medium text-neutral-900 truncate max-w-sm"
|
||||
title={team.name}
|
||||
>
|
||||
{team.name}
|
||||
</p>
|
||||
</div>
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar>
|
||||
<AvatarImage src={row.original.logo ?? undefined} alt={row.original.name} />
|
||||
<AvatarFallback>
|
||||
<TeamIcon className="size-4 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p
|
||||
className="truncate font-medium text-foreground"
|
||||
title={row.original.name}
|
||||
>
|
||||
{row.original.name}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
</div>
|
||||
),
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'city',
|
||||
header: 'City',
|
||||
cell: ({ row }) => (
|
||||
<span className="text-neutral-700">{row.original.city}</span>
|
||||
<span className="text-foreground">{row.original.city}</span>
|
||||
),
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'visibility',
|
||||
header: 'Visibility',
|
||||
cell: ({ row }) => {
|
||||
const isPublic = row.original.visibility === 'public'
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 px-2 py-1 rounded-2xl text-xs font-medium',
|
||||
isPublic
|
||||
? 'bg-success-100 text-success-800'
|
||||
: 'bg-neutral-100 text-neutral-700'
|
||||
)}
|
||||
>
|
||||
{isPublic ? 'Public' : 'Private'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<Badge
|
||||
variant={
|
||||
row.original.visibility === 'public' ? 'success' : 'secondary'
|
||||
}
|
||||
>
|
||||
{row.original.visibility === 'public' ? 'Public' : 'Private'}
|
||||
</Badge>
|
||||
),
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
id: 'leader',
|
||||
header: 'Leader ID',
|
||||
cell: ({ row }) => (
|
||||
<div className="text-sm text-neutral-700 font-mono">
|
||||
<div className="font-mono text-xs text-muted-foreground">
|
||||
{row.original.leader_id}
|
||||
</div>
|
||||
),
|
||||
@@ -236,7 +183,7 @@ function HackathonTeamsPage() {
|
||||
accessorKey: 'created_at',
|
||||
header: 'Created',
|
||||
cell: ({ row }) => (
|
||||
<span className="text-neutral-900 text-sm">
|
||||
<span className="text-sm text-foreground">
|
||||
{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 }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="flex items-center gap-2 text-sm px-4 py-2"
|
||||
onClick={() => handleShowDetailModal(row.original)}
|
||||
>
|
||||
<EditOutlined className="text-sm" />
|
||||
Manage
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => handleShowDetailModal(row.original)}
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
Manage
|
||||
</Button>
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
],
|
||||
[handleShowDetailModal]
|
||||
)
|
||||
);
|
||||
|
||||
const hasActiveFilters = visibilityFilter !== 'all' || cityFilter !== 'all';
|
||||
|
||||
return (
|
||||
<BackofficeWrapper title="IMPHNEN x Kolosal.ai Hackathon 2025">
|
||||
<h1 className="mb-8 text-p1 font-semibold text-neutral-700">
|
||||
Team Management
|
||||
</h1>
|
||||
<section className="bg-white rounded-md shadow p-8 flex flex-col gap-6">
|
||||
<div className="flex flex-wrap gap-3 items-center justify-between">
|
||||
<div className="flex flex-wrap gap-3 items-center">
|
||||
<div className="relative">
|
||||
<SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" />
|
||||
<input
|
||||
type="text"
|
||||
className="border border-neutral-200 rounded-lg pl-10 pr-4 py-2.5 text-sm w-full sm:w-80 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="Search teams by name or city..."
|
||||
<BackofficeWrapper
|
||||
title="Hackathon Teams"
|
||||
description="IMPHNEN x Kolosal.ai Hackathon 2025"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="relative w-full sm:max-w-sm">
|
||||
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
className="pl-9"
|
||||
placeholder="Cari nama atau kota…"
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
onKeyPress={handleSearchKeyPress}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<select
|
||||
className="border border-neutral-200 rounded-lg px-4 py-2.5 text-sm w-28 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
|
||||
value={perPage}
|
||||
onChange={(e) =>
|
||||
handlePerPageChange(parseInt(e.target.value, 10))
|
||||
}
|
||||
>
|
||||
<option value={10}>10 / page</option>
|
||||
<option value={20}>20 / page</option>
|
||||
<option value={50}>50 / page</option>
|
||||
<option value={100}>100 / page</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
{}
|
||||
{
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
className="flex items-center gap-2 px-4 py-2"
|
||||
onClick={handleShowNewTeamModal}
|
||||
>
|
||||
<PlusOutlined className="text-sm" />
|
||||
<Button onClick={() => setShowNewTeamModal(true)} size="md">
|
||||
<Plus className="size-4" />
|
||||
Add Team
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(visibilityFilter !== 'all' || cityFilter !== 'all') && (
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm text-neutral-600">Active filters:</span>
|
||||
|
||||
{visibilityFilter !== 'all' && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-info-100 text-info-800 rounded-2xl text-sm">
|
||||
Visibility: {visibilityFilter}
|
||||
<button
|
||||
onClick={() => setVisibilityFilter('all')}
|
||||
className="text-info-600 hover:text-info-800 cursor-pointer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{hasActiveFilters && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Active filters:
|
||||
</span>
|
||||
)}
|
||||
|
||||
{cityFilter !== 'all' && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-2xl text-sm">
|
||||
City: {cityFilter}
|
||||
<button
|
||||
onClick={() => setCityFilter('all')}
|
||||
className="text-green-600 hover:text-green-800 cursor-pointer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setVisibilityFilter('all')
|
||||
setCityFilter('all')
|
||||
setGlobalFilter('')
|
||||
}}
|
||||
className="text-sm text-neutral-600"
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<LoadingOutlined className="text-3xl text-primary-500 animate-spin" />
|
||||
<span className="ml-3 text-neutral-600">Loading teams...</span>
|
||||
</div>
|
||||
) : filteredData.length > 0 ? (
|
||||
<>
|
||||
<div className="text-sm text-neutral-600">
|
||||
Showing {filteredData.length} of {totalData} teams (Page{' '}
|
||||
{currentPage} of {totalPages})
|
||||
{isFetching && (
|
||||
<span className="ml-2 text-primary-500">(Updating...)</span>
|
||||
{visibilityFilter !== 'all' && (
|
||||
<Badge variant="info" className="gap-1">
|
||||
Visibility: {visibilityFilter}
|
||||
<button onClick={() => setVisibilityFilter('all')}>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
)}
|
||||
{cityFilter !== 'all' && (
|
||||
<Badge variant="success" className="gap-1">
|
||||
City: {cityFilter}
|
||||
<button onClick={() => setCityFilter('all')}>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
)}
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setVisibilityFilter('all');
|
||||
setCityFilter('all');
|
||||
setGlobalFilter('');
|
||||
}}
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable
|
||||
data={filteredData}
|
||||
columns={columns}
|
||||
pageSize={perPage}
|
||||
manualPagination={true}
|
||||
pageCount={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12 text-neutral-500">
|
||||
No teams found. Try adjusting your filters.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
||||
Memuat data teams…
|
||||
</div>
|
||||
) : filteredData.length > 0 ? (
|
||||
<>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Menampilkan {filteredData.length} dari {totalData} teams (page{' '}
|
||||
{currentPage} / {totalPages})
|
||||
{isFetching && (
|
||||
<span className="ml-2 text-primary-500">Updating…</span>
|
||||
)}
|
||||
</div>
|
||||
<DataTable
|
||||
data={filteredData}
|
||||
columns={columns}
|
||||
pageSize={perPage}
|
||||
manualPagination
|
||||
pageCount={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="py-12 text-center text-sm text-muted-foreground">
|
||||
Tidak ada team. Coba ubah filter pencarian.
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<ModalTeamDetail
|
||||
isOpen={showDetailModal}
|
||||
onClose={handleCloseDetailModal}
|
||||
onClose={() => {
|
||||
setShowDetailModal(false);
|
||||
setSelectedTeam(null);
|
||||
}}
|
||||
team={selectedTeam}
|
||||
/>
|
||||
|
||||
<ModalTeamDetail
|
||||
isOpen={showNewTeamModal}
|
||||
onClose={handleCloseNewTeamModal}
|
||||
onClose={() => setShowNewTeamModal(false)}
|
||||
team={null}
|
||||
/>
|
||||
</BackofficeWrapper>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user