chore: upgrade dependencies, restructure shared libs, and fix UI
- Upgrade Nx 22.1.1 → 22.6.3 and all patch/minor dependencies - Restructure shared libs: move business logic from utils to service - Consolidate shadcn-ui into ui lib with atomic design pattern - Fix container centering for landing app (Tailwind v4 compatibility) - Fix button styling by updating @source directive in globals.css - Fix SiCss3 → SiCss rename in react-icons 5.6 - Fix duplicate useSession export conflict - Remove dead code, comments, and unused files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f68d97188c
commit
3f4461c65c
@@ -28,7 +28,6 @@ interface Account {
|
||||
address: string;
|
||||
}
|
||||
|
||||
// Mock data for demonstration
|
||||
const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
|
||||
@@ -136,13 +135,10 @@ export const Components: FC = (): ReactElement => {
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
{/* Header */}
|
||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||
<h1 className="text-p2 font-semibold">Data Akun</h1>
|
||||
</header>
|
||||
{/* Account Table Section */}
|
||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||
{/* Search and Filter */}
|
||||
<div className="flex justify-between items-center gap-8 mb-2">
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
@@ -171,12 +167,10 @@ export const Components: FC = (): ReactElement => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Table */}
|
||||
<DataTable data={mockData} columns={columns} table={table} />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Modal Edit Account */}
|
||||
<ModalEditAccount
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalEditAccount}
|
||||
|
||||
@@ -38,8 +38,6 @@ export const useConfirmItem = (
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
// const result = await actionFunction?.();
|
||||
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
|
||||
@@ -147,7 +147,6 @@ export const Components: FC = (): ReactElement => {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Right-side illustration */}
|
||||
<img
|
||||
src="gacha.webp"
|
||||
alt=""
|
||||
@@ -156,7 +155,6 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Modal Add Item */}
|
||||
<ModalAddItem
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalAddItem}
|
||||
@@ -166,7 +164,6 @@ export const Components: FC = (): ReactElement => {
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
|
||||
{/* Modal Edit Item */}
|
||||
<ModalEditItem
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalEditItem}
|
||||
@@ -176,7 +173,6 @@ export const Components: FC = (): ReactElement => {
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
|
||||
{/* Modal Delete Item */}
|
||||
<ModalDeleteItem
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
|
||||
@@ -38,8 +38,6 @@ export const useConfirmItem = (
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
// const result = await actionFunction?.();
|
||||
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
|
||||
@@ -8,19 +8,16 @@ import {
|
||||
} from '@imphnen-frontend-service/service';
|
||||
|
||||
export const HackathonDashboardPage: FC = (): ReactElement => {
|
||||
// Fetch total participants
|
||||
const { data: usersData } = useQuery({
|
||||
queryKey: ['admin-users-count'],
|
||||
queryFn: () => getAdminUsers({ page: 1, per_page: 1 }),
|
||||
});
|
||||
|
||||
// Fetch total teams
|
||||
const { data: teamsData } = useQuery({
|
||||
queryKey: ['admin-teams-count'],
|
||||
queryFn: () => getAdminTeams({ page: 1, per_page: 1 }),
|
||||
});
|
||||
|
||||
// Fetch total submissions
|
||||
const { data: submissionsData } = useQuery({
|
||||
queryKey: ['admin-submissions-count'],
|
||||
queryFn: () => getAdminSubmissions({ page: 1, per_page: 1 }),
|
||||
@@ -35,21 +32,18 @@ export const HackathonDashboardPage: FC = (): ReactElement => {
|
||||
<h1 className="mb-8 text-p1 font-semibold text-neutral-700">Dashboard</h1>
|
||||
|
||||
<section className="grid grid-cols-5 gap-5">
|
||||
{/* Participant */}
|
||||
<div className="bg-white px-6 py-4 rounded-md shadow">
|
||||
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">
|
||||
{totalParticipants}
|
||||
</h3>
|
||||
<p className="text-neutral-400 text-p3">Total Participants</p>
|
||||
</div>
|
||||
{/* Team */}
|
||||
<div className="bg-white px-6 py-4 rounded-md shadow">
|
||||
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">
|
||||
{totalTeams}
|
||||
</h3>
|
||||
<p className="text-neutral-400 text-p3">Total Teams</p>
|
||||
</div>
|
||||
{/* Project Submitted */}
|
||||
<div className="bg-white px-6 py-4 rounded-md shadow">
|
||||
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">
|
||||
{totalSubmissions}
|
||||
|
||||
+3
-19
@@ -40,7 +40,7 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
|
||||
<div className="flex items-center justify-between p-6 border-b border-neutral-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-success-100 flex items-center justify-center">
|
||||
@@ -68,9 +68,8 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Submission Status */}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 p-4 border rounded-lg',
|
||||
@@ -96,7 +95,6 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Description */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-neutral-700 mb-2">
|
||||
Project Description
|
||||
@@ -106,13 +104,11 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Project Links */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-neutral-700">
|
||||
Project Links
|
||||
</h3>
|
||||
|
||||
{/* Repository URL */}
|
||||
<div className="flex items-start gap-3 p-3 bg-neutral-50 rounded-lg">
|
||||
<LinkOutlined className="text-primary-500 mt-1" />
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -130,7 +126,6 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Demo URL */}
|
||||
{submission.demo_url && (
|
||||
<div className="flex items-start gap-3 p-3 bg-neutral-50 rounded-lg">
|
||||
<LinkOutlined className="text-primary-500 mt-1" />
|
||||
@@ -150,7 +145,6 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Presentation URL */}
|
||||
{submission.presentation_url && (
|
||||
<div className="flex items-start gap-3 p-3 bg-neutral-50 rounded-lg">
|
||||
<LinkOutlined className="text-primary-500 mt-1" />
|
||||
@@ -171,7 +165,6 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Screenshots */}
|
||||
{submission.screenshots && submission.screenshots.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-neutral-700 flex items-center gap-2">
|
||||
@@ -198,7 +191,6 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-neutral-200">
|
||||
<div>
|
||||
<p className="text-xs text-neutral-500 mb-1">Created</p>
|
||||
@@ -227,19 +219,11 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-3 p-6 border-t border-neutral-200 bg-neutral-50">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
{/* <Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
console.log('Edit submission:', submission.id);
|
||||
}}
|
||||
>
|
||||
Edit Status
|
||||
</Button> */}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,6 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
useState<SubmissionType | null>(null);
|
||||
const [globalFilter, setGlobalFilter] = useState(searchQuery);
|
||||
|
||||
// Fetch submissions from API
|
||||
const {
|
||||
data: submissionsResponse,
|
||||
isLoading,
|
||||
@@ -64,14 +63,13 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
status: statusFilter !== 'all' ? statusFilter : undefined,
|
||||
search: searchQuery || undefined,
|
||||
}),
|
||||
staleTime: 30000, // 30 seconds cache
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes
|
||||
staleTime: 30000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const totalData = submissionsResponse?.meta?.total_data || 0;
|
||||
const totalPages = submissionsResponse?.meta?.total_page || 1;
|
||||
|
||||
// Handle page change
|
||||
const handlePageChange = useCallback(
|
||||
(newPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -85,19 +83,16 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
[setSearchParams, perPage, searchQuery, statusFilter]
|
||||
);
|
||||
|
||||
// Validate page number
|
||||
useEffect(() => {
|
||||
if (!isLoading && totalPages > 0 && currentPage > totalPages) {
|
||||
setSearchParams({ page: totalPages.toString() });
|
||||
}
|
||||
}, [currentPage, totalPages, setSearchParams, isLoading]);
|
||||
|
||||
// Sync globalFilter with URL
|
||||
useEffect(() => {
|
||||
setGlobalFilter(searchQuery);
|
||||
}, [searchQuery]);
|
||||
|
||||
// Handle search
|
||||
const handleSearch = useCallback(() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', '1');
|
||||
@@ -118,7 +113,6 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
[handleSearch]
|
||||
);
|
||||
|
||||
// Handle per page change
|
||||
const handlePerPageChange = useCallback(
|
||||
(newPerPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -131,20 +125,6 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
[setSearchParams, searchQuery, statusFilter]
|
||||
);
|
||||
|
||||
// Handle status filter change
|
||||
// const handleStatusFilterChange = useCallback(
|
||||
// (newStatus: string) => {
|
||||
// const params = new URLSearchParams();
|
||||
// params.set('page', '1');
|
||||
// if (perPage !== 10) params.set('per_page', perPage.toString());
|
||||
// if (searchQuery) params.set('search', searchQuery);
|
||||
// if (newStatus !== 'all') params.set('status', newStatus);
|
||||
// setSearchParams(params);
|
||||
// },
|
||||
// [setSearchParams, perPage, searchQuery]
|
||||
// );
|
||||
|
||||
// Handle modal
|
||||
const handleShowSubmissionModal = useCallback(
|
||||
(submission: SubmissionType) => {
|
||||
setSelectedSubmission(submission);
|
||||
@@ -158,12 +138,10 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
setSelectedSubmission(null);
|
||||
}, []);
|
||||
|
||||
// Get submissions data
|
||||
const filteredData = useMemo(() => {
|
||||
return submissionsResponse?.data || [];
|
||||
}, [submissionsResponse]);
|
||||
|
||||
// Memoize columns
|
||||
const columns: ColumnDef<SubmissionType>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -252,7 +230,6 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
<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">
|
||||
{/* Search bar */}
|
||||
<div className="relative">
|
||||
<SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" />
|
||||
<input
|
||||
@@ -265,7 +242,6 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Per Page Dropdown */}
|
||||
<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"
|
||||
@@ -281,52 +257,16 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Status Filter */}
|
||||
{/* <div className="relative">
|
||||
<FilterOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm pointer-events-none z-10" />
|
||||
<select
|
||||
className="border border-neutral-200 rounded-lg pl-10 pr-10 py-2.5 text-sm w-full sm:w-40 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
|
||||
value={statusFilter}
|
||||
onChange={(e) => handleStatusFilterChange(e.target.value)}
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="submitted">Submitted</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="approved">Approved</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
</select>
|
||||
</div> */}
|
||||
{}
|
||||
{
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active filters */}
|
||||
{/* {statusFilter !== 'all' && (
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm text-neutral-600">Active filters:</span>
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-info-100 text-info-800 rounded-2xl text-sm">
|
||||
Status: {statusFilter}
|
||||
<button
|
||||
onClick={() => handleStatusFilterChange('all')}
|
||||
className="text-info-600 hover:text-info-800 cursor-pointer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
handleStatusFilterChange('all');
|
||||
setGlobalFilter('');
|
||||
}}
|
||||
className="text-sm text-neutral-600"
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
)} */}
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
{/* Loading & results */}
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<LoadingOutlined className="text-3xl text-primary-500 animate-spin" />
|
||||
@@ -360,7 +300,6 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Submission Modal */}
|
||||
{selectedSubmission && (
|
||||
<SubmissionModal
|
||||
isOpen={showSubmissionModal}
|
||||
|
||||
-13
@@ -33,7 +33,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
const logoInputRef = useRef<HTMLInputElement>(null);
|
||||
const bannerInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Initialize form data when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (team) {
|
||||
@@ -55,7 +54,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
}
|
||||
}, [isOpen, team]);
|
||||
|
||||
// Check if form has changes
|
||||
const hasChanges = useMemo(() => {
|
||||
if (!formData || !team) return !!formData;
|
||||
return (
|
||||
@@ -68,7 +66,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
);
|
||||
}, [formData, team]);
|
||||
|
||||
// Check if required fields are filled
|
||||
const isFormValid = useMemo(() => {
|
||||
if (!formData) return false;
|
||||
return (
|
||||
@@ -144,7 +141,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-neutral-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-100 flex items-center justify-center">
|
||||
@@ -172,7 +168,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6" onClick={() => setShowLogoMenu(false)}>
|
||||
<input
|
||||
type="file"
|
||||
@@ -189,7 +184,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{/* Banner Section */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-neutral-700">
|
||||
Team Banner{' '}
|
||||
@@ -234,7 +228,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logo & Name */}
|
||||
<div className="grid grid-cols-12 gap-4 items-start">
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-neutral-700 mb-2">
|
||||
@@ -305,7 +298,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-neutral-700">
|
||||
Description <span className="text-danger-500">*</span>
|
||||
@@ -319,7 +311,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* City */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-neutral-700">
|
||||
City <span className="text-danger-500">*</span>
|
||||
@@ -336,7 +327,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Visibility */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-neutral-700">
|
||||
Team Visibility
|
||||
@@ -373,7 +363,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Team Details */}
|
||||
{team && (
|
||||
<div className="space-y-4 border-t border-neutral-200 pt-4">
|
||||
<div className="space-y-2">
|
||||
@@ -429,7 +418,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-6 border-t border-neutral-200">
|
||||
<div>
|
||||
{team && (
|
||||
@@ -463,7 +451,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{showDeleteConfirm && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-60 p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
|
||||
|
||||
+2
-5
@@ -15,7 +15,7 @@ const TeamBannerPlaceholder: FC<TeamBannerPlaceholderProps> = ({
|
||||
className = '',
|
||||
showPlaceholder = true,
|
||||
}) => {
|
||||
const aspectRatioClass = 'aspect-[3/1]'; // 3:1 aspect ratio
|
||||
const aspectRatioClass = 'aspect-[3/1]';
|
||||
|
||||
if (!banner && !showPlaceholder) {
|
||||
return null;
|
||||
@@ -35,7 +35,6 @@ const TeamBannerPlaceholder: FC<TeamBannerPlaceholderProps> = ({
|
||||
alt={`${teamName} banner`}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
// Fallback to placeholder if image fails to load
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const placeholder = target.nextElementSibling as HTMLElement;
|
||||
@@ -44,11 +43,10 @@ const TeamBannerPlaceholder: FC<TeamBannerPlaceholderProps> = ({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* Fallback placeholder (hidden by default, shown on image error) */}
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 bg-linear-to-r from-gray-100 to-gray-200 flex items-center justify-center',
|
||||
'hidden' // Hidden by default
|
||||
'hidden'
|
||||
)}
|
||||
>
|
||||
<div className="text-center">
|
||||
@@ -61,7 +59,6 @@ const TeamBannerPlaceholder: FC<TeamBannerPlaceholderProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// No banner - show placeholder
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -46,11 +46,9 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
useState<TeamType | null>(null);
|
||||
const [globalFilter, setGlobalFilter] = useState(searchQuery);
|
||||
|
||||
// Advanced filtering states
|
||||
const [visibilityFilter, setVisibilityFilter] = useState('all');
|
||||
const [cityFilter, setCityFilter] = useState('all');
|
||||
|
||||
// Fetch teams from API
|
||||
const {
|
||||
data: teamsResponse,
|
||||
isLoading,
|
||||
@@ -70,14 +68,13 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
per_page: perPage,
|
||||
search: searchQuery || undefined,
|
||||
}),
|
||||
staleTime: 30000, // 30 seconds cache
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes
|
||||
staleTime: 30000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const totalData = teamsResponse?.meta?.total_data || 0;
|
||||
const totalPages = teamsResponse?.meta?.total_page || 1;
|
||||
|
||||
// Handle page change - update URL query params
|
||||
const handlePageChange = useCallback(
|
||||
(newPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -90,19 +87,16 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
[setSearchParams, perPage, searchQuery]
|
||||
);
|
||||
|
||||
// Validate page number doesn't exceed total pages
|
||||
useEffect(() => {
|
||||
if (!isLoading && totalPages > 0 && currentPage > totalPages) {
|
||||
setSearchParams({ page: totalPages.toString() });
|
||||
}
|
||||
}, [currentPage, totalPages, setSearchParams, isLoading]);
|
||||
|
||||
// Sync globalFilter with URL search param on mount
|
||||
useEffect(() => {
|
||||
setGlobalFilter(searchQuery);
|
||||
}, [searchQuery]);
|
||||
|
||||
// Handle search teams
|
||||
const handleSearch = useCallback(() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', '1');
|
||||
@@ -113,7 +107,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
setSearchParams(params);
|
||||
}, [globalFilter, setSearchParams, perPage]);
|
||||
|
||||
// Handle Enter key press in search input
|
||||
const handleSearchKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -123,7 +116,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
[handleSearch]
|
||||
);
|
||||
|
||||
// Handle per page change
|
||||
const handlePerPageChange = useCallback(
|
||||
(newPerPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -135,7 +127,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
[setSearchParams, searchQuery]
|
||||
);
|
||||
|
||||
// Memoize the callback to prevent recreation
|
||||
const handleShowDetailModal = useCallback((team: TeamType) => {
|
||||
setSelectedTeam(team);
|
||||
setShowDetailModal(true);
|
||||
@@ -154,12 +145,10 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
setShowNewTeamModal(false);
|
||||
}, []);
|
||||
|
||||
// Get teams data from API response
|
||||
const filteredData = useMemo(() => {
|
||||
return teamsResponse?.data || [];
|
||||
}, [teamsResponse]);
|
||||
|
||||
// Memoize columns to prevent recreation on every render
|
||||
const columns: ColumnDef<TeamType>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -169,7 +158,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
const team = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Team Logo */}
|
||||
<div className="w-10 h-10 rounded-full bg-neutral-100 flex items-center justify-center shrink-0 overflow-hidden">
|
||||
{team.logo ? (
|
||||
<img
|
||||
@@ -181,7 +169,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
<TeamOutlined className="text-neutral-400 text-lg" />
|
||||
)}
|
||||
</div>
|
||||
{/* Team Name */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p
|
||||
className="font-medium text-neutral-900 truncate max-w-sm"
|
||||
@@ -276,12 +263,9 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
<h1 className="mb-8 text-p1 font-semibold text-neutral-700">
|
||||
Team Management
|
||||
</h1>
|
||||
{/* Filters and actions */}
|
||||
<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">
|
||||
{/* Left side - Search & filters */}
|
||||
<div className="flex flex-wrap gap-3 items-center">
|
||||
{/* Search bar */}
|
||||
<div className="relative">
|
||||
<SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" />
|
||||
<input
|
||||
@@ -294,7 +278,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Per Page Dropdown */}
|
||||
<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"
|
||||
@@ -310,31 +293,15 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Visibility Filter */}
|
||||
{/* <div className="relative">
|
||||
<FilterOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm pointer-events-none z-10" />
|
||||
<select
|
||||
className="border border-neutral-200 rounded-lg pl-10 pr-10 py-2.5 text-sm w-full sm:w-40 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
|
||||
value={visibilityFilter}
|
||||
onChange={(e) => setVisibilityFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">All Visibility</option>
|
||||
<option value="public">Public</option>
|
||||
<option value="private">Private</option>
|
||||
</select>
|
||||
</div> */}
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
{/* City Filter */}
|
||||
{/* <CityFilterSelect
|
||||
value={cityFilter}
|
||||
onChange={setCityFilter}
|
||||
className="w-full sm:w-44"
|
||||
placeholder="Search cities..."
|
||||
allOptionLabel="All Cities"
|
||||
/> */}
|
||||
{}
|
||||
{
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Right side - Add Team Button */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -348,12 +315,10 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active filters display */}
|
||||
{(visibilityFilter !== 'all' || cityFilter !== 'all') && (
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm text-neutral-600">Active filters:</span>
|
||||
|
||||
{/* Visibility filter badge */}
|
||||
{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}
|
||||
@@ -366,7 +331,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* City filter badge */}
|
||||
{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}
|
||||
@@ -379,7 +343,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Clear all filters */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -395,7 +358,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading & results display */}
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<LoadingOutlined className="text-3xl text-primary-500 animate-spin" />
|
||||
@@ -427,18 +389,16 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Modals component */}
|
||||
<ModalTeamDetail
|
||||
isOpen={showDetailModal}
|
||||
onClose={handleCloseDetailModal}
|
||||
team={selectedTeam}
|
||||
/>
|
||||
|
||||
{/* New Team Modal */}
|
||||
<ModalTeamDetail
|
||||
isOpen={showNewTeamModal}
|
||||
onClose={handleCloseNewTeamModal}
|
||||
team={null} // null indicates creating new team
|
||||
team={null}
|
||||
/>
|
||||
</BackofficeWrapper>
|
||||
);
|
||||
|
||||
+4
-33
@@ -37,16 +37,13 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
const [showAvatarMenu, setShowAvatarMenu] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Initialize form data when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (user) {
|
||||
// Edit existing user
|
||||
setFormData({ ...user });
|
||||
} else {
|
||||
// Create new user
|
||||
setFormData({
|
||||
id: '', // Will be generated by backend
|
||||
id: '',
|
||||
fullname: '',
|
||||
bio: '',
|
||||
location: '',
|
||||
@@ -60,10 +57,9 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
}
|
||||
}, [isOpen, user]);
|
||||
|
||||
// Check if form has changes
|
||||
const hasChanges = useMemo(() => {
|
||||
if (!formData) return false;
|
||||
if (!user) return true; // New user always has changes
|
||||
if (!user) return true;
|
||||
return (
|
||||
formData.fullname !== user.fullname ||
|
||||
formData.location !== user.location ||
|
||||
@@ -74,7 +70,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
);
|
||||
}, [formData, user]);
|
||||
|
||||
// Check if required fields are filled
|
||||
const isFormValid = useMemo(() => {
|
||||
if (!formData) return false;
|
||||
return formData.fullname?.trim() !== '' && formData.location?.trim() !== '';
|
||||
@@ -99,25 +94,22 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
if (!formData) return;
|
||||
|
||||
if (user) {
|
||||
// Update existing user
|
||||
console.log('Update user data:', formData);
|
||||
} else {
|
||||
// Create new user
|
||||
console.log('Create new user:', formData);
|
||||
}
|
||||
// Here you would typically make an API call to save the data
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (user) {
|
||||
setFormData({ ...user }); // Reset to original for edit mode
|
||||
setFormData({ ...user });
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDeleteAccount = () => {
|
||||
if (!user) return; // Can't delete new user
|
||||
if (!user) return;
|
||||
console.log('Delete user:', user.id);
|
||||
setShowDeleteConfirm(false);
|
||||
onClose();
|
||||
@@ -126,19 +118,16 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
const handleAvatarUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
// Validate file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('Please select an image file');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (max 5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert('Image size must be less than 5MB');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create preview URL
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const avatarUrl = e.target?.result as string;
|
||||
@@ -183,7 +172,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
className="bg-white rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Hidden File Input */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
@@ -191,10 +179,8 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
/>
|
||||
{/* Header */}
|
||||
<div className="border-b border-neutral-200 px-8 py-6 flex justify-between items-start">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Interactive User Avatar */}
|
||||
<div className="relative group ">
|
||||
<div className="w-16 h-16 rounded-full bg-neutral-200 flex items-center justify-center overflow-hidden border-2 border-transparent group-hover:border-primary-300 transition-colors">
|
||||
{formData.avatar ? (
|
||||
@@ -208,7 +194,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Avatar Hover Overlay */}
|
||||
<button
|
||||
onClick={() => setShowAvatarMenu(!showAvatarMenu)}
|
||||
className="absolute inset-0 bg-neutral-400 cursor-pointer rounded-full opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
|
||||
@@ -216,7 +201,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
<CameraOutlined className="text-white text-lg" />
|
||||
</button>
|
||||
|
||||
{/* Avatar Menu Dropdown */}
|
||||
{showAvatarMenu && (
|
||||
<div className="absolute top-full left-0 mt-2 bg-white rounded-lg shadow-lg border border-neutral-200 py-2 min-w-[140px] z-10">
|
||||
<button
|
||||
@@ -269,17 +253,14 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-8" onClick={() => setShowAvatarMenu(false)}>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Left Column - Basic Info */}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-neutral-900 mb-4">
|
||||
Basic Information
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{/* Full Name - Required */}
|
||||
<div className="flex items-center gap-3">
|
||||
<UserOutlined className="text-neutral-400" />
|
||||
<div className="flex-1">
|
||||
@@ -310,7 +291,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location - Required */}
|
||||
<div className="flex items-center gap-3">
|
||||
<EnvironmentOutlined className="text-neutral-400" />
|
||||
<div className="flex-1">
|
||||
@@ -346,7 +326,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Joined Date - Read Only - Only show for existing users */}
|
||||
{user && (
|
||||
<div className="flex items-center gap-3">
|
||||
<CalendarOutlined className="text-neutral-400" />
|
||||
@@ -370,7 +349,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bio Section - Optional */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-neutral-900 mb-3">
|
||||
Bio{' '}
|
||||
@@ -390,9 +368,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Skills & Status */}
|
||||
<div className="space-y-6">
|
||||
{/* Account Status - Enhanced Tab Design */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-neutral-900 mb-4">
|
||||
Account Status
|
||||
@@ -448,7 +424,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Skills Section - Optional */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-neutral-900 mb-4">
|
||||
Skills & Expertise{' '}
|
||||
@@ -510,7 +485,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Account Details - Read Only - Only show for existing users */}
|
||||
{user && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-neutral-900 mb-4">
|
||||
@@ -547,7 +521,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="border-t border-neutral-200 px-8 py-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -558,7 +531,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
? 'Please fill required fields'
|
||||
: 'No changes made'}
|
||||
</div>
|
||||
{/* Delete Account Button - Only show for existing users */}
|
||||
{user && (
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
@@ -595,7 +567,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{showDeleteConfirm && (
|
||||
<div className="fixed inset-0 z-60">
|
||||
<div
|
||||
|
||||
@@ -32,7 +32,6 @@ import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
type UserType = TAdminUserItem;
|
||||
|
||||
// Skills options for filter
|
||||
const skillsOptions = [
|
||||
'Frontend Developer',
|
||||
'Backend Developer',
|
||||
@@ -57,12 +56,10 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
const [selectedUser, setSelectedUser] = useState<UserType | null>(null);
|
||||
const [globalFilter, setGlobalFilter] = useState(searchQuery);
|
||||
|
||||
// Advanced filtering states
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [cityFilter, setCityFilter] = useState('all');
|
||||
const [skillsFilter, setSkillsFilter] = useState<string[]>([]);
|
||||
|
||||
// Fetch users from API
|
||||
const {
|
||||
data: usersResponse,
|
||||
isLoading,
|
||||
@@ -82,14 +79,13 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
per_page: perPage,
|
||||
search: searchQuery || undefined,
|
||||
}),
|
||||
staleTime: 30000, // 30 seconds cache
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes
|
||||
staleTime: 30000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const totalData = usersResponse?.meta?.total_data || 0;
|
||||
const totalPages = usersResponse?.meta?.total_page || 1;
|
||||
|
||||
// Handle page change - update URL query params
|
||||
const handlePageChange = useCallback(
|
||||
(newPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -102,19 +98,16 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
[setSearchParams, perPage, searchQuery]
|
||||
);
|
||||
|
||||
// Validate page number doesn't exceed total pages
|
||||
useEffect(() => {
|
||||
if (!isLoading && totalPages > 0 && currentPage > totalPages) {
|
||||
setSearchParams({ page: totalPages.toString() });
|
||||
}
|
||||
}, [currentPage, totalPages, setSearchParams, isLoading]);
|
||||
|
||||
// Sync globalFilter with URL search param on mount
|
||||
useEffect(() => {
|
||||
setGlobalFilter(searchQuery);
|
||||
}, [searchQuery]);
|
||||
|
||||
// Handle search users
|
||||
const handleSearch = useCallback(() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', '1');
|
||||
@@ -125,7 +118,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
setSearchParams(params);
|
||||
}, [globalFilter, setSearchParams, perPage]);
|
||||
|
||||
// Handle Enter key press in search input
|
||||
const handleSearchKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -135,7 +127,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
[handleSearch]
|
||||
);
|
||||
|
||||
// Handle per page change
|
||||
const handlePerPageChange = useCallback(
|
||||
(newPerPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -147,7 +138,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
[setSearchParams, searchQuery]
|
||||
);
|
||||
|
||||
// Memoize the callback to prevent recreation
|
||||
const handleShowDetailModal = useCallback((user: UserType) => {
|
||||
setSelectedUser(user);
|
||||
setShowDetailModal(true);
|
||||
@@ -166,17 +156,14 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
setShowNewUserModal(false);
|
||||
}, []);
|
||||
|
||||
// Filter data based on current filter states
|
||||
const filteredData = useMemo(() => {
|
||||
const usersData = usersResponse?.data || [];
|
||||
return usersData.filter((user: UserType) => {
|
||||
// Status filter
|
||||
if (statusFilter !== 'all') {
|
||||
const isActive = statusFilter === 'active';
|
||||
if (user.is_active !== isActive) return false;
|
||||
}
|
||||
|
||||
// Skills filter
|
||||
if (skillsFilter.length > 0) {
|
||||
const userSkills = user.skills || [];
|
||||
const hasMatchingSkill = skillsFilter.some((skill) =>
|
||||
@@ -189,7 +176,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
});
|
||||
}, [usersResponse, statusFilter, skillsFilter]);
|
||||
|
||||
// Memoize columns to prevent recreation on every render
|
||||
const columns: ColumnDef<UserType>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -197,7 +183,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
header: 'User',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Avatar */}
|
||||
<div className="w-10 h-10 rounded-full bg-neutral-200 flex items-center justify-center overflow-hidden shrink-0">
|
||||
{row.original.avatar ? (
|
||||
<img
|
||||
@@ -209,7 +194,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
<UserOutlined className="text-neutral-500 text-lg" />
|
||||
)}
|
||||
</div>
|
||||
{/* Name only */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium text-neutral-900 truncate">
|
||||
{row.original.fullname}
|
||||
@@ -318,17 +302,8 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
<EditOutlined className="text-sm" />
|
||||
Manage
|
||||
</Button>
|
||||
{/* <Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="text-sm px-4 py-2"
|
||||
onClick={() => {
|
||||
// Toggle user status - implement later
|
||||
console.log(`Toggle status for ${row.original.fullname}`);
|
||||
}}
|
||||
>
|
||||
{row.original.is_active ? 'Deactivate' : 'Activate'}
|
||||
</Button> */}
|
||||
{
|
||||
}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
@@ -342,12 +317,9 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
<h1 className="mb-8 text-p1 font-semibold text-neutral-700">
|
||||
User Management
|
||||
</h1>
|
||||
{/* Filters and actions */}
|
||||
<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">
|
||||
{/* Left side - Search & filters */}
|
||||
<div className="flex flex-wrap gap-3 items-center">
|
||||
{/* Search bar */}
|
||||
<div className="relative">
|
||||
<SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" />
|
||||
<input
|
||||
@@ -360,7 +332,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Per Page Dropdown */}
|
||||
<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"
|
||||
@@ -376,70 +347,19 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Status Filter */}
|
||||
{/* <div className="relative">
|
||||
<FilterOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm pointer-events-none z-10" />
|
||||
<select
|
||||
className="border border-neutral-200 rounded-lg pl-10 pr-10 py-2.5 text-sm w-full sm:w-36 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
</select>
|
||||
</div> */}
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
{/* City Filter */}
|
||||
{/* <CityFilterSelect
|
||||
value={cityFilter}
|
||||
onChange={setCityFilter}
|
||||
className="w-full sm:w-44"
|
||||
placeholder="Search cities..."
|
||||
allOptionLabel="All Cities"
|
||||
/>
|
||||
{cityFilter !== 'all' && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-2xl text-sm">
|
||||
Location: {cityFilter}
|
||||
<button
|
||||
onClick={() => setCityFilter('all')}
|
||||
className="text-green-600 hover:text-green-800 cursor-pointer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</span>
|
||||
)} */}
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
{/* Skills Filter with Icon */}
|
||||
{/* <div className="relative">
|
||||
<FilterOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm pointer-events-none z-10" />
|
||||
<select
|
||||
className="border border-neutral-200 rounded-lg pl-10 pr-10 py-2.5 text-sm w-full sm:w-44 focus:border-primary-500 focus:outline-none appearance-none bg-white cursor-pointer"
|
||||
value=""
|
||||
onChange={(e) => {
|
||||
if (
|
||||
e.target.value &&
|
||||
!skillsFilter.includes(e.target.value)
|
||||
) {
|
||||
setSkillsFilter((prev) => [...prev, e.target.value]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">Add Skill Filter</option>
|
||||
{skillsOptions.map((skill) => (
|
||||
<option
|
||||
key={skill}
|
||||
value={skill}
|
||||
disabled={skillsFilter.includes(skill)}
|
||||
>
|
||||
{skill}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div> */}
|
||||
{}
|
||||
{
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Right side - Add User Button */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -453,14 +373,12 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active filters display */}
|
||||
{(skillsFilter.length > 0 ||
|
||||
statusFilter !== 'all' ||
|
||||
cityFilter !== 'all') && (
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm text-neutral-600">Active filters:</span>
|
||||
|
||||
{/* Status filter badge */}
|
||||
{statusFilter !== 'all' && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-info-100 text-info-800 rounded-2xl text-sm">
|
||||
Status: {statusFilter}
|
||||
@@ -473,7 +391,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Location filter badge */}
|
||||
{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}
|
||||
@@ -486,7 +403,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Skills filter badges */}
|
||||
{skillsFilter.map((skill) => (
|
||||
<span
|
||||
key={skill}
|
||||
@@ -504,7 +420,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
</span>
|
||||
))}
|
||||
|
||||
{/* Clear all filters */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -521,7 +436,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading & results display */}
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<LoadingOutlined className="text-3xl text-primary-500 animate-spin" />
|
||||
@@ -553,18 +467,16 @@ export const HackathonUsersPage: FC = (): ReactElement => {
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Modals component */}
|
||||
<ModalUserDetail
|
||||
isOpen={showDetailModal}
|
||||
onClose={handleCloseDetailModal}
|
||||
user={selectedUser}
|
||||
/>
|
||||
|
||||
{/* New User Modal */}
|
||||
<ModalUserDetail
|
||||
isOpen={showNewUserModal}
|
||||
onClose={handleCloseNewUserModal}
|
||||
user={null} // null indicates creating new user
|
||||
user={null}
|
||||
/>
|
||||
</BackofficeWrapper>
|
||||
);
|
||||
|
||||
@@ -13,14 +13,14 @@ export const AppLayout: FC = (): ReactElement => {
|
||||
onClose={() => setMobileSidebarOpen(false)}
|
||||
/>
|
||||
<div className="flex-1 overflow-auto">
|
||||
{/* Sticky top header */}
|
||||
|
||||
<header
|
||||
className={
|
||||
'lg:hidden sticky top-0 bg-white border-b border-primary-200 px-4 py-3 flex items-center gap-3 ' +
|
||||
(mobileSidebarOpen ? 'z-0' : 'z-30')
|
||||
}
|
||||
>
|
||||
{/* Mobile menu button (shown on small screens) */}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="lg:hidden p-2 rounded-md hover:bg-gray-100 text-gray-700 cursor-pointer"
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
// import { zodResolver } from '@hookform/resolvers/zod';
|
||||
// import {
|
||||
// gachaRollItemSchema,
|
||||
// TGachaRollItem
|
||||
// } from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
@@ -11,7 +6,6 @@ export const useItem = (
|
||||
initialValues?: any
|
||||
) => {
|
||||
const form = useForm<any>({
|
||||
// resolver: zodResolver(),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
@@ -38,8 +32,6 @@ export const useConfirmItem = (
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
// const result = await actionFunction?.();
|
||||
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
|
||||
@@ -40,7 +40,6 @@ const items = [
|
||||
'Gelang Karet',
|
||||
];
|
||||
|
||||
// Mock data for transactions
|
||||
const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: 'Nama Lengkap',
|
||||
@@ -190,13 +189,10 @@ export const Components: FC = (): ReactElement => {
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
{/* Header */}
|
||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||
<h1 className="text-p2 font-semibold">Data Pengiriman Hadiah</h1>
|
||||
</header>
|
||||
{/* Account Table Section */}
|
||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||
{/* Search and Filter */}
|
||||
<div className="flex justify-between items-center gap-8 mb-2">
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
@@ -224,19 +220,16 @@ export const Components: FC = (): ReactElement => {
|
||||
onClose={() => setShowFilter(false)}
|
||||
onFilterChange={(value) => {
|
||||
console.log('Selected filter:', value);
|
||||
// Filter logic di sini
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Table */}
|
||||
<DataTable data={mockData} columns={columns} table={table} />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Modal Process Delivery */}
|
||||
<ModalProcessDelivery
|
||||
isOpen={showModalProcessDelivery}
|
||||
onClose={() => setShowModalProcessDelivery(false)}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
// import { zodResolver } from '@hookform/resolvers/zod';
|
||||
// import {
|
||||
// gachaRollItemSchema,
|
||||
// TGachaRollItem
|
||||
// } from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
@@ -11,7 +6,6 @@ export const useItem = (
|
||||
initialValues?: any
|
||||
) => {
|
||||
const form = useForm<any>({
|
||||
// resolver: zodResolver(),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
@@ -38,8 +32,6 @@ export const useConfirmItem = (
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
// const result = await actionFunction?.();
|
||||
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
|
||||
@@ -27,7 +27,6 @@ interface Transaction {
|
||||
status: TransactionStatus;
|
||||
}
|
||||
|
||||
// Mock data for transactions
|
||||
const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
|
||||
@@ -149,14 +148,11 @@ export const Components: FC = (): ReactElement => {
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
{/* Header */}
|
||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||
<h1 className="text-p2 font-semibold">Validasi Transaksi</h1>
|
||||
</header>
|
||||
|
||||
{/* Account Table Section */}
|
||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||
{/* Search and Filter */}
|
||||
<div className="flex justify-between items-center gap-8 mb-2">
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
@@ -185,7 +181,6 @@ export const Components: FC = (): ReactElement => {
|
||||
onClose={() => setShowFilter(false)}
|
||||
onFilterChange={(value) => {
|
||||
console.log('Selected filter:', value);
|
||||
// Filter logic di sini
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -193,7 +188,6 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<DataTable data={mockTransactions} columns={columns} table={table} />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -24,12 +24,10 @@ export const CityFilterSelect: FC<CityFilterSelectProps> = ({
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Filter cities based on search query
|
||||
const filteredCities = INDONESIAN_CITIES.filter((city) =>
|
||||
city.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
@@ -102,7 +100,6 @@ export const CityFilterSelect: FC<CityFilterSelectProps> = ({
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute z-50 w-full mt-1 bg-white border border-neutral-200 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
||||
{/* All Cities Option */}
|
||||
<div
|
||||
onClick={() => handleSelectCity('all')}
|
||||
className={`px-3 py-2 cursor-pointer hover:bg-neutral-50 border-b border-neutral-100 ${
|
||||
@@ -114,7 +111,6 @@ export const CityFilterSelect: FC<CityFilterSelectProps> = ({
|
||||
{allOptionLabel}
|
||||
</div>
|
||||
|
||||
{/* Filtered Cities */}
|
||||
{filteredCities.length > 0 ? (
|
||||
<div className="py-1">
|
||||
{filteredCities.slice(0, 100).map((city) => (
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
PERMISSIONS,
|
||||
SessionToken,
|
||||
SessionUser,
|
||||
} from '@imphnen-frontend-service/utils';
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { LoaderFunctionArgs, redirect } from 'react-router';
|
||||
|
||||
const mappingPublicRoutes = [
|
||||
@@ -66,14 +66,6 @@ const mappingRoutePermissions = [
|
||||
},
|
||||
];
|
||||
|
||||
//TODO : Fix this later
|
||||
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
|
||||
// const fallback = mappingRoutePermissions.find((route) =>
|
||||
// route.permissions.some((perm) => userPermissions.includes(perm))
|
||||
// );
|
||||
// return redirect(fallback?.path ?? '/auth/login');
|
||||
// };
|
||||
|
||||
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
Reference in New Issue
Block a user