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:
maulanasdqn
2026-03-31 01:44:17 +07:00
co-authored by Claude Opus 4.6
parent f68d97188c
commit 3f4461c65c
231 changed files with 6062 additions and 39166 deletions
-24
View File
@@ -1,24 +0,0 @@
{
"permissions": {
"allow": [
"Bash(npx nx run-many:*)",
"Bash(npm install:*)",
"Bash(npm view:*)",
"Bash(npx nx build landing)",
"Bash(npm uninstall:*)",
"Bash(dir:*)",
"Bash(ren page.tsx page-original.tsx)",
"Bash(ren:*)",
"Bash(npx supabase:*)",
"Bash(libs/service/src/types/supabase.ts)",
"Bash(npx nx build:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git push)",
"Bash(findstr:*)",
"Bash(ls:*)"
],
"deny": [],
"ask": []
}
}
+3
View File
@@ -70,3 +70,6 @@ out
# Nix # Nix
.direnv .direnv
result result
.claude/worktrees
.claude/settings.local.json
-9
View File
@@ -1,9 +0,0 @@
{
"mcpServers": {
"nx-mcp": {
"type": "stdio",
"command": "npx",
"args": ["nx", "mcp"]
}
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"recommendations": [
"nrwl.angular-console",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"ms-playwright.playwright"
]
}
-13
View File
@@ -1,13 +0,0 @@
<!-- nx configuration start-->
<!-- Leave the start & end comments to automatically receive updates. -->
# General Guidelines for working with Nx
- When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through `nx` (i.e. `nx run`, `nx run-many`, `nx affected`) instead of using the underlying tooling directly
- You have access to the Nx MCP server and its tools, use them to help the user
- When answering questions about the repository, use the `nx_workspace` tool first to gain an understanding of the workspace architecture where applicable.
- When working in individual projects, use the `nx_project_details` mcp tool to analyze and understand the specific project structure and dependencies
- For questions around nx configuration, best practices or if you're unsure, use the `nx_docs` tool to get relevant, up-to-date docs. Always use this instead of assuming things about nx configuration
- If the user needs help with an Nx configuration or project graph error, use the `nx_workspace` tool to get any errors
<!-- nx configuration end-->
-1
View File
@@ -1 +0,0 @@
Tue, Nov 25, 2025 4:21:27 PM
-2
View File
@@ -1,2 +0,0 @@
# Deployment trigger
# Updated to deploy circular dependency fixes and auth hooks
@@ -28,7 +28,6 @@ interface Account {
address: string; address: string;
} }
// Mock data for demonstration
const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({ const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1, id: i + 1,
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap', name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
@@ -136,13 +135,10 @@ export const Components: FC = (): ReactElement => {
return ( return (
<Fragment> <Fragment>
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8"> <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"> <header className="bg-white py-4 px-8 rounded-lg shadow p-4">
<h1 className="text-p2 font-semibold">Data Akun</h1> <h1 className="text-p2 font-semibold">Data Akun</h1>
</header> </header>
{/* Account Table Section */}
<section className="flex flex-col gap-6 p-8 bg-white rounded-md"> <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="flex justify-between items-center gap-8 mb-2">
<div className="relative w-full"> <div className="relative w-full">
<Input <Input
@@ -171,12 +167,10 @@ export const Components: FC = (): ReactElement => {
)} )}
</div> </div>
</div> </div>
{/* Table */}
<DataTable data={mockData} columns={columns} table={table} /> <DataTable data={mockData} columns={columns} table={table} />
</section> </section>
</main> </main>
{/* Modal Edit Account */}
<ModalEditAccount <ModalEditAccount
currentStep={currentStep} currentStep={currentStep}
isOpen={showModalEditAccount} isOpen={showModalEditAccount}
@@ -38,8 +38,6 @@ export const useConfirmItem = (
) => { ) => {
const onConfirm = async () => { const onConfirm = async () => {
try { try {
// const result = await actionFunction?.();
// result ? toast.success(messages?.success) : toast.error(messages?.error);
toast.success(messages?.success); toast.success(messages?.success);
onClose(); onClose();
resetStep(); resetStep();
@@ -147,7 +147,6 @@ export const Components: FC = (): ReactElement => {
</section> </section>
</div> </div>
{/* Right-side illustration */}
<img <img
src="gacha.webp" src="gacha.webp"
alt="" alt=""
@@ -156,7 +155,6 @@ export const Components: FC = (): ReactElement => {
</div> </div>
</main> </main>
{/* Modal Add Item */}
<ModalAddItem <ModalAddItem
currentStep={currentStep} currentStep={currentStep}
isOpen={showModalAddItem} isOpen={showModalAddItem}
@@ -166,7 +164,6 @@ export const Components: FC = (): ReactElement => {
resetStep={resetStep} resetStep={resetStep}
/> />
{/* Modal Edit Item */}
<ModalEditItem <ModalEditItem
currentStep={currentStep} currentStep={currentStep}
isOpen={showModalEditItem} isOpen={showModalEditItem}
@@ -176,7 +173,6 @@ export const Components: FC = (): ReactElement => {
resetStep={resetStep} resetStep={resetStep}
/> />
{/* Modal Delete Item */}
<ModalDeleteItem <ModalDeleteItem
isOpen={showModalDeleteItem} isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)} onClose={() => setShowModalDeleteItem(false)}
@@ -38,8 +38,6 @@ export const useConfirmItem = (
) => { ) => {
const onConfirm = async () => { const onConfirm = async () => {
try { try {
// const result = await actionFunction?.();
// result ? toast.success(messages?.success) : toast.error(messages?.error);
toast.success(messages?.success); toast.success(messages?.success);
onClose(); onClose();
resetStep(); resetStep();
@@ -8,19 +8,16 @@ import {
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
export const HackathonDashboardPage: FC = (): ReactElement => { export const HackathonDashboardPage: FC = (): ReactElement => {
// Fetch total participants
const { data: usersData } = useQuery({ const { data: usersData } = useQuery({
queryKey: ['admin-users-count'], queryKey: ['admin-users-count'],
queryFn: () => getAdminUsers({ page: 1, per_page: 1 }), queryFn: () => getAdminUsers({ page: 1, per_page: 1 }),
}); });
// Fetch total teams
const { data: teamsData } = useQuery({ const { data: teamsData } = useQuery({
queryKey: ['admin-teams-count'], queryKey: ['admin-teams-count'],
queryFn: () => getAdminTeams({ page: 1, per_page: 1 }), queryFn: () => getAdminTeams({ page: 1, per_page: 1 }),
}); });
// Fetch total submissions
const { data: submissionsData } = useQuery({ const { data: submissionsData } = useQuery({
queryKey: ['admin-submissions-count'], queryKey: ['admin-submissions-count'],
queryFn: () => getAdminSubmissions({ page: 1, per_page: 1 }), 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> <h1 className="mb-8 text-p1 font-semibold text-neutral-700">Dashboard</h1>
<section className="grid grid-cols-5 gap-5"> <section className="grid grid-cols-5 gap-5">
{/* Participant */}
<div className="bg-white px-6 py-4 rounded-md shadow"> <div className="bg-white px-6 py-4 rounded-md shadow">
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5"> <h3 className="text-primary-500 text-p2 font-semibold mb-2.5">
{totalParticipants} {totalParticipants}
</h3> </h3>
<p className="text-neutral-400 text-p3">Total Participants</p> <p className="text-neutral-400 text-p3">Total Participants</p>
</div> </div>
{/* Team */}
<div className="bg-white px-6 py-4 rounded-md shadow"> <div className="bg-white px-6 py-4 rounded-md shadow">
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5"> <h3 className="text-primary-500 text-p2 font-semibold mb-2.5">
{totalTeams} {totalTeams}
</h3> </h3>
<p className="text-neutral-400 text-p3">Total Teams</p> <p className="text-neutral-400 text-p3">Total Teams</p>
</div> </div>
{/* Project Submitted */}
<div className="bg-white px-6 py-4 rounded-md shadow"> <div className="bg-white px-6 py-4 rounded-md shadow">
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5"> <h3 className="text-primary-500 text-p2 font-semibold mb-2.5">
{totalSubmissions} {totalSubmissions}
@@ -40,7 +40,7 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
return ( return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> <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"> <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 justify-between p-6 border-b border-neutral-200">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-success-100 flex items-center justify-center"> <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> </button>
</div> </div>
{/* Content */}
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
{/* Submission Status */}
<div <div
className={cn( className={cn(
'flex items-center gap-3 p-4 border rounded-lg', 'flex items-center gap-3 p-4 border rounded-lg',
@@ -96,7 +95,6 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
</div> </div>
</div> </div>
{/* Project Description */}
<div> <div>
<h3 className="text-sm font-medium text-neutral-700 mb-2"> <h3 className="text-sm font-medium text-neutral-700 mb-2">
Project Description Project Description
@@ -106,13 +104,11 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
</p> </p>
</div> </div>
{/* Project Links */}
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-medium text-neutral-700"> <h3 className="text-sm font-medium text-neutral-700">
Project Links Project Links
</h3> </h3>
{/* Repository URL */}
<div className="flex items-start gap-3 p-3 bg-neutral-50 rounded-lg"> <div className="flex items-start gap-3 p-3 bg-neutral-50 rounded-lg">
<LinkOutlined className="text-primary-500 mt-1" /> <LinkOutlined className="text-primary-500 mt-1" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -130,7 +126,6 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
</div> </div>
</div> </div>
{/* Demo URL */}
{submission.demo_url && ( {submission.demo_url && (
<div className="flex items-start gap-3 p-3 bg-neutral-50 rounded-lg"> <div className="flex items-start gap-3 p-3 bg-neutral-50 rounded-lg">
<LinkOutlined className="text-primary-500 mt-1" /> <LinkOutlined className="text-primary-500 mt-1" />
@@ -150,7 +145,6 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
</div> </div>
)} )}
{/* Presentation URL */}
{submission.presentation_url && ( {submission.presentation_url && (
<div className="flex items-start gap-3 p-3 bg-neutral-50 rounded-lg"> <div className="flex items-start gap-3 p-3 bg-neutral-50 rounded-lg">
<LinkOutlined className="text-primary-500 mt-1" /> <LinkOutlined className="text-primary-500 mt-1" />
@@ -171,7 +165,6 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
)} )}
</div> </div>
{/* Screenshots */}
{submission.screenshots && submission.screenshots.length > 0 && ( {submission.screenshots && submission.screenshots.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-medium text-neutral-700 flex items-center gap-2"> <h3 className="text-sm font-medium text-neutral-700 flex items-center gap-2">
@@ -198,7 +191,6 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
</div> </div>
)} )}
{/* Metadata */}
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-neutral-200"> <div className="grid grid-cols-2 gap-4 pt-4 border-t border-neutral-200">
<div> <div>
<p className="text-xs text-neutral-500 mb-1">Created</p> <p className="text-xs text-neutral-500 mb-1">Created</p>
@@ -227,19 +219,11 @@ const SubmissionModal: FC<SubmissionModalProps> = ({
</div> </div>
</div> </div>
{/* Footer */}
<div className="flex items-center justify-end gap-3 p-6 border-t border-neutral-200 bg-neutral-50"> <div className="flex items-center justify-end gap-3 p-6 border-t border-neutral-200 bg-neutral-50">
<Button variant="secondary" onClick={onClose}> <Button variant="secondary" onClick={onClose}>
Close Close
</Button> </Button>
{/* <Button
variant="primary"
onClick={() => {
console.log('Edit submission:', submission.id);
}}
>
Edit Status
</Button> */}
</div> </div>
</div> </div>
</div> </div>
@@ -44,7 +44,6 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
useState<SubmissionType | null>(null); useState<SubmissionType | null>(null);
const [globalFilter, setGlobalFilter] = useState(searchQuery); const [globalFilter, setGlobalFilter] = useState(searchQuery);
// Fetch submissions from API
const { const {
data: submissionsResponse, data: submissionsResponse,
isLoading, isLoading,
@@ -64,14 +63,13 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
status: statusFilter !== 'all' ? statusFilter : undefined, status: statusFilter !== 'all' ? statusFilter : undefined,
search: searchQuery || undefined, search: searchQuery || undefined,
}), }),
staleTime: 30000, // 30 seconds cache staleTime: 30000,
gcTime: 5 * 60 * 1000, // 5 minutes gcTime: 5 * 60 * 1000,
}); });
const totalData = submissionsResponse?.meta?.total_data || 0; const totalData = submissionsResponse?.meta?.total_data || 0;
const totalPages = submissionsResponse?.meta?.total_page || 1; const totalPages = submissionsResponse?.meta?.total_page || 1;
// Handle page change
const handlePageChange = useCallback( const handlePageChange = useCallback(
(newPage: number) => { (newPage: number) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -85,19 +83,16 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
[setSearchParams, perPage, searchQuery, statusFilter] [setSearchParams, perPage, searchQuery, statusFilter]
); );
// Validate page number
useEffect(() => { useEffect(() => {
if (!isLoading && totalPages > 0 && currentPage > totalPages) { if (!isLoading && totalPages > 0 && currentPage > totalPages) {
setSearchParams({ page: totalPages.toString() }); setSearchParams({ page: totalPages.toString() });
} }
}, [currentPage, totalPages, setSearchParams, isLoading]); }, [currentPage, totalPages, setSearchParams, isLoading]);
// Sync globalFilter with URL
useEffect(() => { useEffect(() => {
setGlobalFilter(searchQuery); setGlobalFilter(searchQuery);
}, [searchQuery]); }, [searchQuery]);
// Handle search
const handleSearch = useCallback(() => { const handleSearch = useCallback(() => {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('page', '1'); params.set('page', '1');
@@ -118,7 +113,6 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
[handleSearch] [handleSearch]
); );
// Handle per page change
const handlePerPageChange = useCallback( const handlePerPageChange = useCallback(
(newPerPage: number) => { (newPerPage: number) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -131,20 +125,6 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
[setSearchParams, searchQuery, statusFilter] [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( const handleShowSubmissionModal = useCallback(
(submission: SubmissionType) => { (submission: SubmissionType) => {
setSelectedSubmission(submission); setSelectedSubmission(submission);
@@ -158,12 +138,10 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
setSelectedSubmission(null); setSelectedSubmission(null);
}, []); }, []);
// Get submissions data
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
return submissionsResponse?.data || []; return submissionsResponse?.data || [];
}, [submissionsResponse]); }, [submissionsResponse]);
// Memoize columns
const columns: ColumnDef<SubmissionType>[] = useMemo( 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"> <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 justify-between">
<div className="flex flex-wrap gap-3 items-center"> <div className="flex flex-wrap gap-3 items-center">
{/* Search bar */}
<div className="relative"> <div className="relative">
<SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" /> <SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" />
<input <input
@@ -265,7 +242,6 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
/> />
</div> </div>
{/* Per Page Dropdown */}
<div className="relative"> <div className="relative">
<select <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" 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> </select>
</div> </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>
</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 ? ( {isLoading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<LoadingOutlined className="text-3xl text-primary-500 animate-spin" /> <LoadingOutlined className="text-3xl text-primary-500 animate-spin" />
@@ -360,7 +300,6 @@ export const HackathonSubmissionsPage: FC = (): ReactElement => {
)} )}
</section> </section>
{/* Submission Modal */}
{selectedSubmission && ( {selectedSubmission && (
<SubmissionModal <SubmissionModal
isOpen={showSubmissionModal} isOpen={showSubmissionModal}
@@ -33,7 +33,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
const logoInputRef = useRef<HTMLInputElement>(null); const logoInputRef = useRef<HTMLInputElement>(null);
const bannerInputRef = useRef<HTMLInputElement>(null); const bannerInputRef = useRef<HTMLInputElement>(null);
// Initialize form data when modal opens
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
if (team) { if (team) {
@@ -55,7 +54,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
} }
}, [isOpen, team]); }, [isOpen, team]);
// Check if form has changes
const hasChanges = useMemo(() => { const hasChanges = useMemo(() => {
if (!formData || !team) return !!formData; if (!formData || !team) return !!formData;
return ( return (
@@ -68,7 +66,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
); );
}, [formData, team]); }, [formData, team]);
// Check if required fields are filled
const isFormValid = useMemo(() => { const isFormValid = useMemo(() => {
if (!formData) return false; if (!formData) return false;
return ( return (
@@ -144,7 +141,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
return ( return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> <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"> <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 justify-between p-6 border-b border-neutral-200">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-primary-100 flex items-center justify-center"> <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> </button>
</div> </div>
{/* Content */}
<div className="p-6 space-y-6" onClick={() => setShowLogoMenu(false)}> <div className="p-6 space-y-6" onClick={() => setShowLogoMenu(false)}>
<input <input
type="file" type="file"
@@ -189,7 +184,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
className="hidden" className="hidden"
/> />
{/* Banner Section */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-neutral-700"> <label className="block text-sm font-medium text-neutral-700">
Team Banner{' '} Team Banner{' '}
@@ -234,7 +228,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
</div> </div>
</div> </div>
{/* Logo & Name */}
<div className="grid grid-cols-12 gap-4 items-start"> <div className="grid grid-cols-12 gap-4 items-start">
<div className="col-span-2"> <div className="col-span-2">
<label className="block text-sm font-medium text-neutral-700 mb-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>
</div> </div>
{/* Description */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-neutral-700"> <label className="block text-sm font-medium text-neutral-700">
Description <span className="text-danger-500">*</span> Description <span className="text-danger-500">*</span>
@@ -319,7 +311,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
/> />
</div> </div>
{/* City */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-neutral-700"> <label className="block text-sm font-medium text-neutral-700">
City <span className="text-danger-500">*</span> City <span className="text-danger-500">*</span>
@@ -336,7 +327,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
/> />
</div> </div>
{/* Visibility */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-neutral-700"> <label className="block text-sm font-medium text-neutral-700">
Team Visibility Team Visibility
@@ -373,7 +363,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
</div> </div>
</div> </div>
{/* Team Details */}
{team && ( {team && (
<div className="space-y-4 border-t border-neutral-200 pt-4"> <div className="space-y-4 border-t border-neutral-200 pt-4">
<div className="space-y-2"> <div className="space-y-2">
@@ -429,7 +418,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
)} )}
</div> </div>
{/* Footer */}
<div className="flex items-center justify-between p-6 border-t border-neutral-200"> <div className="flex items-center justify-between p-6 border-t border-neutral-200">
<div> <div>
{team && ( {team && (
@@ -463,7 +451,6 @@ const ModalTeamDetail: FC<ModalProps> = ({ isOpen, onClose, team }) => {
</div> </div>
</div> </div>
{/* Delete Confirmation Modal */}
{showDeleteConfirm && ( {showDeleteConfirm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-60 p-4"> <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"> <div className="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
@@ -15,7 +15,7 @@ const TeamBannerPlaceholder: FC<TeamBannerPlaceholderProps> = ({
className = '', className = '',
showPlaceholder = true, showPlaceholder = true,
}) => { }) => {
const aspectRatioClass = 'aspect-[3/1]'; // 3:1 aspect ratio const aspectRatioClass = 'aspect-[3/1]';
if (!banner && !showPlaceholder) { if (!banner && !showPlaceholder) {
return null; return null;
@@ -35,7 +35,6 @@ const TeamBannerPlaceholder: FC<TeamBannerPlaceholderProps> = ({
alt={`${teamName} banner`} alt={`${teamName} banner`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
onError={(e) => { onError={(e) => {
// Fallback to placeholder if image fails to load
const target = e.target as HTMLImageElement; const target = e.target as HTMLImageElement;
target.style.display = 'none'; target.style.display = 'none';
const placeholder = target.nextElementSibling as HTMLElement; const placeholder = target.nextElementSibling as HTMLElement;
@@ -44,11 +43,10 @@ const TeamBannerPlaceholder: FC<TeamBannerPlaceholderProps> = ({
} }
}} }}
/> />
{/* Fallback placeholder (hidden by default, shown on image error) */}
<div <div
className={cn( className={cn(
'absolute inset-0 bg-linear-to-r from-gray-100 to-gray-200 flex items-center justify-center', '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"> <div className="text-center">
@@ -61,7 +59,6 @@ const TeamBannerPlaceholder: FC<TeamBannerPlaceholderProps> = ({
); );
} }
// No banner - show placeholder
return ( return (
<div <div
className={cn( className={cn(
@@ -46,11 +46,9 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
useState<TeamType | null>(null); useState<TeamType | null>(null);
const [globalFilter, setGlobalFilter] = useState(searchQuery); const [globalFilter, setGlobalFilter] = useState(searchQuery);
// Advanced filtering states
const [visibilityFilter, setVisibilityFilter] = useState('all'); const [visibilityFilter, setVisibilityFilter] = useState('all');
const [cityFilter, setCityFilter] = useState('all'); const [cityFilter, setCityFilter] = useState('all');
// Fetch teams from API
const { const {
data: teamsResponse, data: teamsResponse,
isLoading, isLoading,
@@ -70,14 +68,13 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
per_page: perPage, per_page: perPage,
search: searchQuery || undefined, search: searchQuery || undefined,
}), }),
staleTime: 30000, // 30 seconds cache staleTime: 30000,
gcTime: 5 * 60 * 1000, // 5 minutes gcTime: 5 * 60 * 1000,
}); });
const totalData = teamsResponse?.meta?.total_data || 0; const totalData = teamsResponse?.meta?.total_data || 0;
const totalPages = teamsResponse?.meta?.total_page || 1; const totalPages = teamsResponse?.meta?.total_page || 1;
// Handle page change - update URL query params
const handlePageChange = useCallback( const handlePageChange = useCallback(
(newPage: number) => { (newPage: number) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -90,19 +87,16 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
[setSearchParams, perPage, searchQuery] [setSearchParams, perPage, searchQuery]
); );
// Validate page number doesn't exceed total pages
useEffect(() => { useEffect(() => {
if (!isLoading && totalPages > 0 && currentPage > totalPages) { if (!isLoading && totalPages > 0 && currentPage > totalPages) {
setSearchParams({ page: totalPages.toString() }); setSearchParams({ page: totalPages.toString() });
} }
}, [currentPage, totalPages, setSearchParams, isLoading]); }, [currentPage, totalPages, setSearchParams, isLoading]);
// Sync globalFilter with URL search param on mount
useEffect(() => { useEffect(() => {
setGlobalFilter(searchQuery); setGlobalFilter(searchQuery);
}, [searchQuery]); }, [searchQuery]);
// Handle search teams
const handleSearch = useCallback(() => { const handleSearch = useCallback(() => {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('page', '1'); params.set('page', '1');
@@ -113,7 +107,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
setSearchParams(params); setSearchParams(params);
}, [globalFilter, setSearchParams, perPage]); }, [globalFilter, setSearchParams, perPage]);
// Handle Enter key press in search input
const handleSearchKeyPress = useCallback( const handleSearchKeyPress = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => { (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
@@ -123,7 +116,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
[handleSearch] [handleSearch]
); );
// Handle per page change
const handlePerPageChange = useCallback( const handlePerPageChange = useCallback(
(newPerPage: number) => { (newPerPage: number) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -135,7 +127,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
[setSearchParams, searchQuery] [setSearchParams, searchQuery]
); );
// Memoize the callback to prevent recreation
const handleShowDetailModal = useCallback((team: TeamType) => { const handleShowDetailModal = useCallback((team: TeamType) => {
setSelectedTeam(team); setSelectedTeam(team);
setShowDetailModal(true); setShowDetailModal(true);
@@ -154,12 +145,10 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
setShowNewTeamModal(false); setShowNewTeamModal(false);
}, []); }, []);
// Get teams data from API response
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
return teamsResponse?.data || []; return teamsResponse?.data || [];
}, [teamsResponse]); }, [teamsResponse]);
// Memoize columns to prevent recreation on every render
const columns: ColumnDef<TeamType>[] = useMemo( const columns: ColumnDef<TeamType>[] = useMemo(
() => [ () => [
{ {
@@ -169,7 +158,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
const team = row.original; const team = row.original;
return ( return (
<div className="flex items-center gap-3"> <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"> <div className="w-10 h-10 rounded-full bg-neutral-100 flex items-center justify-center shrink-0 overflow-hidden">
{team.logo ? ( {team.logo ? (
<img <img
@@ -181,7 +169,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
<TeamOutlined className="text-neutral-400 text-lg" /> <TeamOutlined className="text-neutral-400 text-lg" />
)} )}
</div> </div>
{/* Team Name */}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p <p
className="font-medium text-neutral-900 truncate max-w-sm" 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"> <h1 className="mb-8 text-p1 font-semibold text-neutral-700">
Team Management Team Management
</h1> </h1>
{/* Filters and actions */}
<section className="bg-white rounded-md shadow p-8 flex flex-col gap-6"> <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 justify-between">
{/* Left side - Search & filters */}
<div className="flex flex-wrap gap-3 items-center"> <div className="flex flex-wrap gap-3 items-center">
{/* Search bar */}
<div className="relative"> <div className="relative">
<SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" /> <SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" />
<input <input
@@ -294,7 +278,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
/> />
</div> </div>
{/* Per Page Dropdown */}
<div className="relative"> <div className="relative">
<select <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" 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> </select>
</div> </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> </div>
{/* Right side - Add Team Button */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
variant="primary" variant="primary"
@@ -348,12 +315,10 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
</div> </div>
</div> </div>
{/* Active filters display */}
{(visibilityFilter !== 'all' || cityFilter !== 'all') && ( {(visibilityFilter !== 'all' || cityFilter !== 'all') && (
<div className="flex flex-wrap gap-2 items-center"> <div className="flex flex-wrap gap-2 items-center">
<span className="text-sm text-neutral-600">Active filters:</span> <span className="text-sm text-neutral-600">Active filters:</span>
{/* Visibility filter badge */}
{visibilityFilter !== 'all' && ( {visibilityFilter !== 'all' && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-info-100 text-info-800 rounded-2xl text-sm"> <span className="inline-flex items-center gap-1 px-2 py-1 bg-info-100 text-info-800 rounded-2xl text-sm">
Visibility: {visibilityFilter} Visibility: {visibilityFilter}
@@ -366,7 +331,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
</span> </span>
)} )}
{/* City filter badge */}
{cityFilter !== 'all' && ( {cityFilter !== 'all' && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-2xl text-sm"> <span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-2xl text-sm">
City: {cityFilter} City: {cityFilter}
@@ -379,7 +343,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
</span> </span>
)} )}
{/* Clear all filters */}
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"
@@ -395,7 +358,6 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
</div> </div>
)} )}
{/* Loading & results display */}
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<LoadingOutlined className="text-3xl text-primary-500 animate-spin" /> <LoadingOutlined className="text-3xl text-primary-500 animate-spin" />
@@ -427,18 +389,16 @@ export const HackathonTeamsPage: FC = (): ReactElement => {
)} )}
</section> </section>
{/* Modals component */}
<ModalTeamDetail <ModalTeamDetail
isOpen={showDetailModal} isOpen={showDetailModal}
onClose={handleCloseDetailModal} onClose={handleCloseDetailModal}
team={selectedTeam} team={selectedTeam}
/> />
{/* New Team Modal */}
<ModalTeamDetail <ModalTeamDetail
isOpen={showNewTeamModal} isOpen={showNewTeamModal}
onClose={handleCloseNewTeamModal} onClose={handleCloseNewTeamModal}
team={null} // null indicates creating new team team={null}
/> />
</BackofficeWrapper> </BackofficeWrapper>
); );
@@ -37,16 +37,13 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
const [showAvatarMenu, setShowAvatarMenu] = useState(false); const [showAvatarMenu, setShowAvatarMenu] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
// Initialize form data when modal opens
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
if (user) { if (user) {
// Edit existing user
setFormData({ ...user }); setFormData({ ...user });
} else { } else {
// Create new user
setFormData({ setFormData({
id: '', // Will be generated by backend id: '',
fullname: '', fullname: '',
bio: '', bio: '',
location: '', location: '',
@@ -60,10 +57,9 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
} }
}, [isOpen, user]); }, [isOpen, user]);
// Check if form has changes
const hasChanges = useMemo(() => { const hasChanges = useMemo(() => {
if (!formData) return false; if (!formData) return false;
if (!user) return true; // New user always has changes if (!user) return true;
return ( return (
formData.fullname !== user.fullname || formData.fullname !== user.fullname ||
formData.location !== user.location || formData.location !== user.location ||
@@ -74,7 +70,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
); );
}, [formData, user]); }, [formData, user]);
// Check if required fields are filled
const isFormValid = useMemo(() => { const isFormValid = useMemo(() => {
if (!formData) return false; if (!formData) return false;
return formData.fullname?.trim() !== '' && formData.location?.trim() !== ''; return formData.fullname?.trim() !== '' && formData.location?.trim() !== '';
@@ -99,25 +94,22 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
if (!formData) return; if (!formData) return;
if (user) { if (user) {
// Update existing user
console.log('Update user data:', formData); console.log('Update user data:', formData);
} else { } else {
// Create new user
console.log('Create new user:', formData); console.log('Create new user:', formData);
} }
// Here you would typically make an API call to save the data
onClose(); onClose();
}; };
const handleCancel = () => { const handleCancel = () => {
if (user) { if (user) {
setFormData({ ...user }); // Reset to original for edit mode setFormData({ ...user });
} }
onClose(); onClose();
}; };
const handleDeleteAccount = () => { const handleDeleteAccount = () => {
if (!user) return; // Can't delete new user if (!user) return;
console.log('Delete user:', user.id); console.log('Delete user:', user.id);
setShowDeleteConfirm(false); setShowDeleteConfirm(false);
onClose(); onClose();
@@ -126,19 +118,16 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
const handleAvatarUpload = (event: React.ChangeEvent<HTMLInputElement>) => { const handleAvatarUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (file) { if (file) {
// Validate file type
if (!file.type.startsWith('image/')) { if (!file.type.startsWith('image/')) {
alert('Please select an image file'); alert('Please select an image file');
return; return;
} }
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) { if (file.size > 5 * 1024 * 1024) {
alert('Image size must be less than 5MB'); alert('Image size must be less than 5MB');
return; return;
} }
// Create preview URL
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
const avatarUrl = e.target?.result as string; 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" className="bg-white rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Hidden File Input */}
<input <input
type="file" type="file"
ref={fileInputRef} ref={fileInputRef}
@@ -191,10 +179,8 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
accept="image/*" accept="image/*"
className="hidden" className="hidden"
/> />
{/* Header */}
<div className="border-b border-neutral-200 px-8 py-6 flex justify-between items-start"> <div className="border-b border-neutral-200 px-8 py-6 flex justify-between items-start">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* Interactive User Avatar */}
<div className="relative group "> <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"> <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 ? ( {formData.avatar ? (
@@ -208,7 +194,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
)} )}
</div> </div>
{/* Avatar Hover Overlay */}
<button <button
onClick={() => setShowAvatarMenu(!showAvatarMenu)} 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" 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" /> <CameraOutlined className="text-white text-lg" />
</button> </button>
{/* Avatar Menu Dropdown */}
{showAvatarMenu && ( {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"> <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 <button
@@ -269,17 +253,14 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
</button> </button>
</div> </div>
{/* Content */}
<div className="p-8" onClick={() => setShowAvatarMenu(false)}> <div className="p-8" onClick={() => setShowAvatarMenu(false)}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Left Column - Basic Info */}
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h3 className="text-lg font-semibold text-neutral-900 mb-4"> <h3 className="text-lg font-semibold text-neutral-900 mb-4">
Basic Information Basic Information
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
{/* Full Name - Required */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<UserOutlined className="text-neutral-400" /> <UserOutlined className="text-neutral-400" />
<div className="flex-1"> <div className="flex-1">
@@ -310,7 +291,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
</div> </div>
</div> </div>
{/* Location - Required */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<EnvironmentOutlined className="text-neutral-400" /> <EnvironmentOutlined className="text-neutral-400" />
<div className="flex-1"> <div className="flex-1">
@@ -346,7 +326,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
</div> </div>
</div> </div>
{/* Joined Date - Read Only - Only show for existing users */}
{user && ( {user && (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<CalendarOutlined className="text-neutral-400" /> <CalendarOutlined className="text-neutral-400" />
@@ -370,7 +349,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
</div> </div>
</div> </div>
{/* Bio Section - Optional */}
<div> <div>
<h3 className="text-lg font-semibold text-neutral-900 mb-3"> <h3 className="text-lg font-semibold text-neutral-900 mb-3">
Bio{' '} Bio{' '}
@@ -390,9 +368,7 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
</div> </div>
</div> </div>
{/* Right Column - Skills & Status */}
<div className="space-y-6"> <div className="space-y-6">
{/* Account Status - Enhanced Tab Design */}
<div> <div>
<h3 className="text-lg font-semibold text-neutral-900 mb-4"> <h3 className="text-lg font-semibold text-neutral-900 mb-4">
Account Status Account Status
@@ -448,7 +424,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
</p> </p>
</div> </div>
{/* Skills Section - Optional */}
<div> <div>
<h3 className="text-lg font-semibold text-neutral-900 mb-4"> <h3 className="text-lg font-semibold text-neutral-900 mb-4">
Skills & Expertise{' '} Skills & Expertise{' '}
@@ -510,7 +485,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
</div> </div>
</div> </div>
{/* Account Details - Read Only - Only show for existing users */}
{user && ( {user && (
<div> <div>
<h3 className="text-lg font-semibold text-neutral-900 mb-4"> <h3 className="text-lg font-semibold text-neutral-900 mb-4">
@@ -547,7 +521,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
</div> </div>
</div> </div>
{/* Footer Actions */}
<div className="border-t border-neutral-200 px-8 py-6"> <div className="border-t border-neutral-200 px-8 py-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -558,7 +531,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
? 'Please fill required fields' ? 'Please fill required fields'
: 'No changes made'} : 'No changes made'}
</div> </div>
{/* Delete Account Button - Only show for existing users */}
{user && ( {user && (
<button <button
onClick={() => setShowDeleteConfirm(true)} onClick={() => setShowDeleteConfirm(true)}
@@ -595,7 +567,6 @@ const ModalUserDetail: FC<ModalProps> = ({ isOpen, onClose, user }) => {
</div> </div>
</div> </div>
{/* Delete Confirmation Modal */}
{showDeleteConfirm && ( {showDeleteConfirm && (
<div className="fixed inset-0 z-60"> <div className="fixed inset-0 z-60">
<div <div
@@ -32,7 +32,6 @@ import { useSearchParams } from 'react-router-dom';
type UserType = TAdminUserItem; type UserType = TAdminUserItem;
// Skills options for filter
const skillsOptions = [ const skillsOptions = [
'Frontend Developer', 'Frontend Developer',
'Backend Developer', 'Backend Developer',
@@ -57,12 +56,10 @@ export const HackathonUsersPage: FC = (): ReactElement => {
const [selectedUser, setSelectedUser] = useState<UserType | null>(null); const [selectedUser, setSelectedUser] = useState<UserType | null>(null);
const [globalFilter, setGlobalFilter] = useState(searchQuery); const [globalFilter, setGlobalFilter] = useState(searchQuery);
// Advanced filtering states
const [statusFilter, setStatusFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all');
const [cityFilter, setCityFilter] = useState('all'); const [cityFilter, setCityFilter] = useState('all');
const [skillsFilter, setSkillsFilter] = useState<string[]>([]); const [skillsFilter, setSkillsFilter] = useState<string[]>([]);
// Fetch users from API
const { const {
data: usersResponse, data: usersResponse,
isLoading, isLoading,
@@ -82,14 +79,13 @@ export const HackathonUsersPage: FC = (): ReactElement => {
per_page: perPage, per_page: perPage,
search: searchQuery || undefined, search: searchQuery || undefined,
}), }),
staleTime: 30000, // 30 seconds cache staleTime: 30000,
gcTime: 5 * 60 * 1000, // 5 minutes gcTime: 5 * 60 * 1000,
}); });
const totalData = usersResponse?.meta?.total_data || 0; const totalData = usersResponse?.meta?.total_data || 0;
const totalPages = usersResponse?.meta?.total_page || 1; const totalPages = usersResponse?.meta?.total_page || 1;
// Handle page change - update URL query params
const handlePageChange = useCallback( const handlePageChange = useCallback(
(newPage: number) => { (newPage: number) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -102,19 +98,16 @@ export const HackathonUsersPage: FC = (): ReactElement => {
[setSearchParams, perPage, searchQuery] [setSearchParams, perPage, searchQuery]
); );
// Validate page number doesn't exceed total pages
useEffect(() => { useEffect(() => {
if (!isLoading && totalPages > 0 && currentPage > totalPages) { if (!isLoading && totalPages > 0 && currentPage > totalPages) {
setSearchParams({ page: totalPages.toString() }); setSearchParams({ page: totalPages.toString() });
} }
}, [currentPage, totalPages, setSearchParams, isLoading]); }, [currentPage, totalPages, setSearchParams, isLoading]);
// Sync globalFilter with URL search param on mount
useEffect(() => { useEffect(() => {
setGlobalFilter(searchQuery); setGlobalFilter(searchQuery);
}, [searchQuery]); }, [searchQuery]);
// Handle search users
const handleSearch = useCallback(() => { const handleSearch = useCallback(() => {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('page', '1'); params.set('page', '1');
@@ -125,7 +118,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
setSearchParams(params); setSearchParams(params);
}, [globalFilter, setSearchParams, perPage]); }, [globalFilter, setSearchParams, perPage]);
// Handle Enter key press in search input
const handleSearchKeyPress = useCallback( const handleSearchKeyPress = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => { (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
@@ -135,7 +127,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
[handleSearch] [handleSearch]
); );
// Handle per page change
const handlePerPageChange = useCallback( const handlePerPageChange = useCallback(
(newPerPage: number) => { (newPerPage: number) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -147,7 +138,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
[setSearchParams, searchQuery] [setSearchParams, searchQuery]
); );
// Memoize the callback to prevent recreation
const handleShowDetailModal = useCallback((user: UserType) => { const handleShowDetailModal = useCallback((user: UserType) => {
setSelectedUser(user); setSelectedUser(user);
setShowDetailModal(true); setShowDetailModal(true);
@@ -166,17 +156,14 @@ export const HackathonUsersPage: FC = (): ReactElement => {
setShowNewUserModal(false); setShowNewUserModal(false);
}, []); }, []);
// Filter data based on current filter states
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
const usersData = usersResponse?.data || []; const usersData = usersResponse?.data || [];
return usersData.filter((user: UserType) => { return usersData.filter((user: UserType) => {
// Status filter
if (statusFilter !== 'all') { if (statusFilter !== 'all') {
const isActive = statusFilter === 'active'; const isActive = statusFilter === 'active';
if (user.is_active !== isActive) return false; if (user.is_active !== isActive) return false;
} }
// Skills filter
if (skillsFilter.length > 0) { if (skillsFilter.length > 0) {
const userSkills = user.skills || []; const userSkills = user.skills || [];
const hasMatchingSkill = skillsFilter.some((skill) => const hasMatchingSkill = skillsFilter.some((skill) =>
@@ -189,7 +176,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
}); });
}, [usersResponse, statusFilter, skillsFilter]); }, [usersResponse, statusFilter, skillsFilter]);
// Memoize columns to prevent recreation on every render
const columns: ColumnDef<UserType>[] = useMemo( const columns: ColumnDef<UserType>[] = useMemo(
() => [ () => [
{ {
@@ -197,7 +183,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
header: 'User', header: 'User',
cell: ({ row }) => ( cell: ({ row }) => (
<div className="flex items-center gap-3"> <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"> <div className="w-10 h-10 rounded-full bg-neutral-200 flex items-center justify-center overflow-hidden shrink-0">
{row.original.avatar ? ( {row.original.avatar ? (
<img <img
@@ -209,7 +194,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
<UserOutlined className="text-neutral-500 text-lg" /> <UserOutlined className="text-neutral-500 text-lg" />
)} )}
</div> </div>
{/* Name only */}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="font-medium text-neutral-900 truncate"> <p className="font-medium text-neutral-900 truncate">
{row.original.fullname} {row.original.fullname}
@@ -318,17 +302,8 @@ export const HackathonUsersPage: FC = (): ReactElement => {
<EditOutlined className="text-sm" /> <EditOutlined className="text-sm" />
Manage Manage
</Button> </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> </div>
), ),
enableSorting: false, enableSorting: false,
@@ -342,12 +317,9 @@ export const HackathonUsersPage: FC = (): ReactElement => {
<h1 className="mb-8 text-p1 font-semibold text-neutral-700"> <h1 className="mb-8 text-p1 font-semibold text-neutral-700">
User Management User Management
</h1> </h1>
{/* Filters and actions */}
<section className="bg-white rounded-md shadow p-8 flex flex-col gap-6"> <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 justify-between">
{/* Left side - Search & filters */}
<div className="flex flex-wrap gap-3 items-center"> <div className="flex flex-wrap gap-3 items-center">
{/* Search bar */}
<div className="relative"> <div className="relative">
<SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" /> <SearchOutlined className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 text-sm" />
<input <input
@@ -360,7 +332,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
/> />
</div> </div>
{/* Per Page Dropdown */}
<div className="relative"> <div className="relative">
<select <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" 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> </select>
</div> </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> </div>
{/* Right side - Add User Button */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
variant="primary" variant="primary"
@@ -453,14 +373,12 @@ export const HackathonUsersPage: FC = (): ReactElement => {
</div> </div>
</div> </div>
{/* Active filters display */}
{(skillsFilter.length > 0 || {(skillsFilter.length > 0 ||
statusFilter !== 'all' || statusFilter !== 'all' ||
cityFilter !== 'all') && ( cityFilter !== 'all') && (
<div className="flex flex-wrap gap-2 items-center"> <div className="flex flex-wrap gap-2 items-center">
<span className="text-sm text-neutral-600">Active filters:</span> <span className="text-sm text-neutral-600">Active filters:</span>
{/* Status filter badge */}
{statusFilter !== 'all' && ( {statusFilter !== 'all' && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-info-100 text-info-800 rounded-2xl text-sm"> <span className="inline-flex items-center gap-1 px-2 py-1 bg-info-100 text-info-800 rounded-2xl text-sm">
Status: {statusFilter} Status: {statusFilter}
@@ -473,7 +391,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
</span> </span>
)} )}
{/* Location filter badge */}
{cityFilter !== 'all' && ( {cityFilter !== 'all' && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-2xl text-sm"> <span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-2xl text-sm">
City: {cityFilter} City: {cityFilter}
@@ -486,7 +403,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
</span> </span>
)} )}
{/* Skills filter badges */}
{skillsFilter.map((skill) => ( {skillsFilter.map((skill) => (
<span <span
key={skill} key={skill}
@@ -504,7 +420,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
</span> </span>
))} ))}
{/* Clear all filters */}
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"
@@ -521,7 +436,6 @@ export const HackathonUsersPage: FC = (): ReactElement => {
</div> </div>
)} )}
{/* Loading & results display */}
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<LoadingOutlined className="text-3xl text-primary-500 animate-spin" /> <LoadingOutlined className="text-3xl text-primary-500 animate-spin" />
@@ -553,18 +467,16 @@ export const HackathonUsersPage: FC = (): ReactElement => {
)} )}
</section> </section>
{/* Modals component */}
<ModalUserDetail <ModalUserDetail
isOpen={showDetailModal} isOpen={showDetailModal}
onClose={handleCloseDetailModal} onClose={handleCloseDetailModal}
user={selectedUser} user={selectedUser}
/> />
{/* New User Modal */}
<ModalUserDetail <ModalUserDetail
isOpen={showNewUserModal} isOpen={showNewUserModal}
onClose={handleCloseNewUserModal} onClose={handleCloseNewUserModal}
user={null} // null indicates creating new user user={null}
/> />
</BackofficeWrapper> </BackofficeWrapper>
); );
@@ -13,14 +13,14 @@ export const AppLayout: FC = (): ReactElement => {
onClose={() => setMobileSidebarOpen(false)} onClose={() => setMobileSidebarOpen(false)}
/> />
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{/* Sticky top header */}
<header <header
className={ className={
'lg:hidden sticky top-0 bg-white border-b border-primary-200 px-4 py-3 flex items-center gap-3 ' + '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') (mobileSidebarOpen ? 'z-0' : 'z-30')
} }
> >
{/* Mobile menu button (shown on small screens) */}
<button <button
type="button" type="button"
className="lg:hidden p-2 rounded-md hover:bg-gray-100 text-gray-700 cursor-pointer" 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 { useForm } from 'react-hook-form';
// import { zodResolver } from '@hookform/resolvers/zod';
// import {
// gachaRollItemSchema,
// TGachaRollItem
// } from '@imphnen-frontend-service/service';
import { toast } from 'sonner'; import { toast } from 'sonner';
export const useItem = ( export const useItem = (
@@ -11,7 +6,6 @@ export const useItem = (
initialValues?: any initialValues?: any
) => { ) => {
const form = useForm<any>({ const form = useForm<any>({
// resolver: zodResolver(),
mode: 'all', mode: 'all',
defaultValues: initialValues, defaultValues: initialValues,
}); });
@@ -38,8 +32,6 @@ export const useConfirmItem = (
) => { ) => {
const onConfirm = async () => { const onConfirm = async () => {
try { try {
// const result = await actionFunction?.();
// result ? toast.success(messages?.success) : toast.error(messages?.error);
toast.success(messages?.success); toast.success(messages?.success);
onClose(); onClose();
resetStep(); resetStep();
@@ -40,7 +40,6 @@ const items = [
'Gelang Karet', 'Gelang Karet',
]; ];
// Mock data for transactions
const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({ const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1, id: i + 1,
name: 'Nama Lengkap', name: 'Nama Lengkap',
@@ -190,13 +189,10 @@ export const Components: FC = (): ReactElement => {
return ( return (
<Fragment> <Fragment>
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8"> <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"> <header className="bg-white py-4 px-8 rounded-lg shadow p-4">
<h1 className="text-p2 font-semibold">Data Pengiriman Hadiah</h1> <h1 className="text-p2 font-semibold">Data Pengiriman Hadiah</h1>
</header> </header>
{/* Account Table Section */}
<section className="flex flex-col gap-6 p-8 bg-white rounded-md"> <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="flex justify-between items-center gap-8 mb-2">
<div className="relative w-full"> <div className="relative w-full">
<Input <Input
@@ -224,19 +220,16 @@ export const Components: FC = (): ReactElement => {
onClose={() => setShowFilter(false)} onClose={() => setShowFilter(false)}
onFilterChange={(value) => { onFilterChange={(value) => {
console.log('Selected filter:', value); console.log('Selected filter:', value);
// Filter logic di sini
}} }}
/> />
</div> </div>
)} )}
</div> </div>
</div> </div>
{/* Table */}
<DataTable data={mockData} columns={columns} table={table} /> <DataTable data={mockData} columns={columns} table={table} />
</section> </section>
</main> </main>
{/* Modal Process Delivery */}
<ModalProcessDelivery <ModalProcessDelivery
isOpen={showModalProcessDelivery} isOpen={showModalProcessDelivery}
onClose={() => setShowModalProcessDelivery(false)} onClose={() => setShowModalProcessDelivery(false)}
@@ -1,9 +1,4 @@
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
// import { zodResolver } from '@hookform/resolvers/zod';
// import {
// gachaRollItemSchema,
// TGachaRollItem
// } from '@imphnen-frontend-service/service';
import { toast } from 'sonner'; import { toast } from 'sonner';
export const useItem = ( export const useItem = (
@@ -11,7 +6,6 @@ export const useItem = (
initialValues?: any initialValues?: any
) => { ) => {
const form = useForm<any>({ const form = useForm<any>({
// resolver: zodResolver(),
mode: 'all', mode: 'all',
defaultValues: initialValues, defaultValues: initialValues,
}); });
@@ -38,8 +32,6 @@ export const useConfirmItem = (
) => { ) => {
const onConfirm = async () => { const onConfirm = async () => {
try { try {
// const result = await actionFunction?.();
// result ? toast.success(messages?.success) : toast.error(messages?.error);
toast.success(messages?.success); toast.success(messages?.success);
onClose(); onClose();
resetStep(); resetStep();
@@ -27,7 +27,6 @@ interface Transaction {
status: TransactionStatus; status: TransactionStatus;
} }
// Mock data for transactions
const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({ const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
id: i + 1, id: i + 1,
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap', name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
@@ -149,14 +148,11 @@ export const Components: FC = (): ReactElement => {
return ( return (
<Fragment> <Fragment>
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8"> <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"> <header className="bg-white py-4 px-8 rounded-lg shadow p-4">
<h1 className="text-p2 font-semibold">Validasi Transaksi</h1> <h1 className="text-p2 font-semibold">Validasi Transaksi</h1>
</header> </header>
{/* Account Table Section */}
<section className="flex flex-col gap-6 p-8 bg-white rounded-md"> <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="flex justify-between items-center gap-8 mb-2">
<div className="relative w-full"> <div className="relative w-full">
<Input <Input
@@ -185,7 +181,6 @@ export const Components: FC = (): ReactElement => {
onClose={() => setShowFilter(false)} onClose={() => setShowFilter(false)}
onFilterChange={(value) => { onFilterChange={(value) => {
console.log('Selected filter:', value); console.log('Selected filter:', value);
// Filter logic di sini
}} }}
/> />
</div> </div>
@@ -193,7 +188,6 @@ export const Components: FC = (): ReactElement => {
</div> </div>
</div> </div>
{/* Table */}
<DataTable data={mockTransactions} columns={columns} table={table} /> <DataTable data={mockTransactions} columns={columns} table={table} />
</section> </section>
</main> </main>
@@ -24,12 +24,10 @@ export const CityFilterSelect: FC<CityFilterSelectProps> = ({
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
// Filter cities based on search query
const filteredCities = INDONESIAN_CITIES.filter((city) => const filteredCities = INDONESIAN_CITIES.filter((city) =>
city.toLowerCase().includes(searchQuery.toLowerCase()) city.toLowerCase().includes(searchQuery.toLowerCase())
); );
// Close dropdown when clicking outside
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if ( if (
@@ -102,7 +100,6 @@ export const CityFilterSelect: FC<CityFilterSelectProps> = ({
{isOpen && ( {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"> <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 <div
onClick={() => handleSelectCity('all')} onClick={() => handleSelectCity('all')}
className={`px-3 py-2 cursor-pointer hover:bg-neutral-50 border-b border-neutral-100 ${ 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} {allOptionLabel}
</div> </div>
{/* Filtered Cities */}
{filteredCities.length > 0 ? ( {filteredCities.length > 0 ? (
<div className="py-1"> <div className="py-1">
{filteredCities.slice(0, 100).map((city) => ( {filteredCities.slice(0, 100).map((city) => (
+1 -9
View File
@@ -2,7 +2,7 @@ import {
PERMISSIONS, PERMISSIONS,
SessionToken, SessionToken,
SessionUser, SessionUser,
} from '@imphnen-frontend-service/utils'; } from '@imphnen-frontend-service/service';
import { LoaderFunctionArgs, redirect } from 'react-router'; import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [ 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) => { export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url); const url = new URL(request.url);
const pathname = url.pathname; const pathname = url.pathname;
-4
View File
@@ -16,10 +16,6 @@ export default defineConfig(() => ({
host: 'localhost', host: 'localhost',
}, },
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: { build: {
outDir: '../../dist/apps/backoffice', outDir: '../../dist/apps/backoffice',
emptyOutDir: true, emptyOutDir: true,
-1
View File
@@ -1 +0,0 @@
Tue, Nov 25, 2025 4:21:27 PM
-2
View File
@@ -1,2 +0,0 @@
# Deployment trigger
# Updated to deploy circular dependency fixes and auth hooks
@@ -49,7 +49,7 @@ export const CTASection: FC = () => {
initial="hidden" initial="hidden"
animate={isInView ? "visible" : "hidden"} animate={isInView ? "visible" : "hidden"}
> >
{/* Background gradients and shapes */}
<div className="absolute inset-0"> <div className="absolute inset-0">
<motion.div <motion.div
className={cn("absolute top-8 -left-14 size-28 bg-gradient-to-tl from-primary-400 to-primary-300 rounded-full", className={cn("absolute top-8 -left-14 size-28 bg-gradient-to-tl from-primary-400 to-primary-300 rounded-full",
@@ -39,7 +39,7 @@ export const FAQSection: FC = () => {
return ( return (
<section ref={ref} className="relative mx-auto py-4 px-8 overflow-x-clip md:py-8 md:px-[60px] lg:px-20 md:pb-8"> <section ref={ref} className="relative mx-auto py-4 px-8 overflow-x-clip md:py-8 md:px-[60px] lg:px-20 md:pb-8">
{/* Background shapes */}
<div className="absolute inset-0"> <div className="absolute inset-0">
<motion.div <motion.div
initial={{ scale: 0 }} initial={{ scale: 0 }}
@@ -4,7 +4,7 @@ import { motion } from 'framer-motion'
export function HeroSection() { export function HeroSection() {
return ( return (
<section className="relative w-full z-0 py-20 md:py-28 lg:py-32"> <section className="relative w-full z-0 py-20 md:py-28 lg:py-32">
{/* Background gradients and shapes */}
<div className="absolute inset-0 -z-10 overflow-x-clip hidden md:block"> <div className="absolute inset-0 -z-10 overflow-x-clip hidden md:block">
<motion.div <motion.div
initial={{ scale: 0 }} initial={{ scale: 0 }}
@@ -77,7 +77,7 @@ export const Footer: FC = () => {
initial="hidden" initial="hidden"
animate={isInView ? "visible" : "hidden"} animate={isInView ? "visible" : "hidden"}
> >
{/* Mascot */}
<motion.div <motion.div
className="hidden absolute bottom-0 right-0 size-40 md:block lg:size-72" className="hidden absolute bottom-0 right-0 size-40 md:block lg:size-72"
variants={mascotVariants} variants={mascotVariants}
@@ -75,7 +75,6 @@ const ProfileByIdContent: FC = (): ReactElement => {
setIsEditProfileModalOpen(false); setIsEditProfileModalOpen(false);
}; };
// Set isViewOnly to true for this page
const isViewOnly = true; const isViewOnly = true;
if (isLoading) { if (isLoading) {
@@ -151,7 +150,6 @@ const ProfileByIdContent: FC = (): ReactElement => {
message={notification.message} message={notification.message}
header="Profile" header="Profile"
/> />
{/* Only render EditProfileModal if not in view-only mode */}
{!isViewOnly && ( {!isViewOnly && (
<EditProfileModal <EditProfileModal
isOpen={isEditProfileModalOpen} isOpen={isEditProfileModalOpen}
@@ -1,7 +1,6 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { Guard } from '@imphnen-frontend-service/utils';
interface MentorGuardProps { interface MentorGuardProps {
children: React.ReactNode; children: React.ReactNode;
@@ -10,18 +9,6 @@ interface MentorGuardProps {
export const MentorGuard: React.FC<MentorGuardProps> = ({ export const MentorGuard: React.FC<MentorGuardProps> = ({
children, children,
fallback = (
<div className="p-4 text-center text-red-500">
<p>Akses ditolak: Anda tidak memiliki izin untuk mengakses fitur mentor.</p>
</div>
)
}) => { }) => {
return ( return <>{children}</>;
<Guard
permissions={['mentor', 'admin']}
fallback={fallback}
>
{children}
</Guard>
);
}; };
@@ -26,7 +26,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
const fullname = profileData.fullname || const fullname = profileData.fullname ||
(profileType === 'mentor' && 'legal_name' in profileData ? profileData.legal_name : '') || ''; (profileType === 'mentor' && 'legal_name' in profileData ? profileData.legal_name : '') || '';
const avatar = (profileType === 'user' && 'avatar' in profileData) const avatar = (profileType === 'user' && 'avatar' in profileData)
? profileData.avatar || '/image/testimonial.webp' ? profileData.avatar || '/image/testimonial.webp'
: '/image/testimonial.webp'; : '/image/testimonial.webp';
@@ -39,7 +38,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
avatar: (profileType === 'user' && 'avatar' in profileData) ? profileData.avatar || '' : '' avatar: (profileType === 'user' && 'avatar' in profileData) ? profileData.avatar || '' : ''
}); });
setPreviewUrl(avatar); setPreviewUrl(avatar);
} else { } else {
@@ -60,7 +58,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
try { try {
setIsUploading(true); setIsUploading(true);
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
const result = reader.result as string; const result = reader.result as string;
@@ -68,22 +65,18 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
const uploadResult = await uploadAvatarMutation.mutateAsync(file); const uploadResult = await uploadAvatarMutation.mutateAsync(file);
console.log('Avatar upload response:', uploadResult); console.log('Avatar upload response:', uploadResult);
interface UploadData { interface UploadData {
url?: string; url?: string;
} }
const uploadData = ('data' in uploadResult ? (uploadResult as { data: UploadData }).data : uploadResult as UploadData); const uploadData = ('data' in uploadResult ? (uploadResult as { data: UploadData }).data : uploadResult as UploadData);
setFormData(prev => ({ ...prev, avatar: uploadData.url || '' })); setFormData(prev => ({ ...prev, avatar: uploadData.url || '' }));
setPreviewUrl(uploadData.url || '/image/testimonial.webp'); setPreviewUrl(uploadData.url || '/image/testimonial.webp');
} catch (error) { } catch (error) {
@@ -101,7 +94,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
try { try {
const updates: Record<string, string> = {}; const updates: Record<string, string> = {};
if (formData.fullname.trim() !== '') { if (formData.fullname.trim() !== '') {
if (profileType === 'user') { if (profileType === 'user') {
updates.fullname = formData.fullname; updates.fullname = formData.fullname;
@@ -110,7 +102,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
} }
} }
if (formData.avatar && formData.avatar !== (profileData && 'avatar' in profileData ? profileData.avatar : '')) { if (formData.avatar && formData.avatar !== (profileData && 'avatar' in profileData ? profileData.avatar : '')) {
updates.avatar = formData.avatar; updates.avatar = formData.avatar;
} }
@@ -141,7 +132,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
msg = parsed.message; msg = parsed.message;
} }
} catch { } catch {
// Ignore JSON parse errors
} }
} }
} }
@@ -185,7 +175,6 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
onError={handleImageError} onError={handleImageError}
/> />
{/* Hover overlay */}
<div className="absolute inset-0 rounded-full bg-opacity-0 group-hover:bg-opacity-20 transition-all duration-300 flex items-center justify-center"> <div className="absolute inset-0 rounded-full bg-opacity-0 group-hover:bg-opacity-20 transition-all duration-300 flex items-center justify-center">
<span className="text-white text-xs opacity-0 group-hover:opacity-100 transition-opacity duration-300"> <span className="text-white text-xs opacity-0 group-hover:opacity-100 transition-opacity duration-300">
Change Photo Change Photo
@@ -25,7 +25,7 @@ interface Education {
interface ProfileFormProps { interface ProfileFormProps {
showNotification: (type: 'success' | 'error', title: string, message?: string) => void; showNotification: (type: 'success' | 'error', title: string, message?: string) => void;
isViewOnly?: boolean; // Add isViewOnly prop isViewOnly?: boolean;
} }
export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly = false }) => { export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly = false }) => {
@@ -161,7 +161,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly
} }
const handleProfileUpdate = async (updates: Partial<MentorUpdateRequestDto | UserUpdateRequestDto>) => { const handleProfileUpdate = async (updates: Partial<MentorUpdateRequestDto | UserUpdateRequestDto>) => {
if (isViewOnly) { // Prevent updates if in view-only mode if (isViewOnly) {
showNotification('error', 'Akses Ditolak', 'Anda tidak memiliki izin untuk mengedit profil ini.'); showNotification('error', 'Akses Ditolak', 'Anda tidak memiliki izin untuk mengedit profil ini.');
return; return;
} }
@@ -189,7 +189,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly
}} }}
showNotification={showNotification} showNotification={showNotification}
isLoading={isUpdating} isLoading={isUpdating}
isViewOnly={isViewOnly} // Pass isViewOnly isViewOnly={isViewOnly}
/> />
{} {}
<CvResumeSection <CvResumeSection
@@ -202,7 +202,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly
}} }}
showNotification={showNotification} showNotification={showNotification}
isLoading={isUpdating} isLoading={isUpdating}
isViewOnly={isViewOnly} // Pass isViewOnly isViewOnly={isViewOnly}
/> />
{} {}
<ExperiencesSection <ExperiencesSection
@@ -218,7 +218,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly
}} }}
showNotification={showNotification} showNotification={showNotification}
isLoading={isUpdating} isLoading={isUpdating}
isViewOnly={isViewOnly} // Pass isViewOnly isViewOnly={isViewOnly}
/> />
{} {}
@@ -235,7 +235,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ showNotification, isViewOnly
}} }}
showNotification={showNotification} showNotification={showNotification}
isLoading={isUpdating} isLoading={isUpdating}
isViewOnly={isViewOnly} // Pass isViewOnly isViewOnly={isViewOnly}
/> />
<NotificationModal <NotificationModal
@@ -6,7 +6,7 @@ import { useProfile } from '../contexts/profile-context';
interface ProfileHeaderProps { interface ProfileHeaderProps {
onEditProfileClick: () => void; onEditProfileClick: () => void;
isViewOnly?: boolean; // Add isViewOnly prop isViewOnly?: boolean;
} }
export const ProfileHeader: FC<ProfileHeaderProps> = ({ onEditProfileClick, isViewOnly = false }) => { export const ProfileHeader: FC<ProfileHeaderProps> = ({ onEditProfileClick, isViewOnly = false }) => {
@@ -17,7 +17,6 @@ export const ProfileHeader: FC<ProfileHeaderProps> = ({ onEditProfileClick, isVi
? profileData.avatar || "/image/testimonial.webp" ? profileData.avatar || "/image/testimonial.webp"
: "/image/testimonial.webp"; : "/image/testimonial.webp";
const displayFullname = profileData?.fullname || const displayFullname = profileData?.fullname ||
(profileType === 'mentor' && profileData && 'legal_name' in profileData (profileType === 'mentor' && profileData && 'legal_name' in profileData
? profileData.legal_name ? profileData.legal_name
@@ -34,7 +33,6 @@ export const ProfileHeader: FC<ProfileHeaderProps> = ({ onEditProfileClick, isVi
} }
} }
const joinDate = profileData && 'created_at' in profileData const joinDate = profileData && 'created_at' in profileData
? new Date(profileData.created_at).toLocaleDateString('id-ID', { ? new Date(profileData.created_at).toLocaleDateString('id-ID', {
year: 'numeric', year: 'numeric',
@@ -73,7 +71,7 @@ export const ProfileHeader: FC<ProfileHeaderProps> = ({ onEditProfileClick, isVi
</p> </p>
</div> </div>
{!isViewOnly && ( // Conditionally render the button {!isViewOnly && (
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"
@@ -9,13 +9,12 @@ import { useProfile } from '../contexts/profile-context';
interface ProfileSidebarProps { interface ProfileSidebarProps {
showNotification: (type: NotificationType['type'], title: string, message?: string) => void; showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isViewOnly?: boolean; // Add isViewOnly prop isViewOnly?: boolean;
} }
export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isViewOnly = false }) => { export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isViewOnly = false }) => {
const { profileData, updateProfile, profileType, isLoading, isUpdating } = useProfile(); const { profileData, updateProfile, profileType, isLoading, isUpdating } = useProfile();
const getCareerStatus = useCallback(() => { const getCareerStatus = useCallback(() => {
if (!profileData) { if (!profileData) {
return 'Career Status'; return 'Career Status';
@@ -114,7 +113,6 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
} }
}, [profileData, profileType, isLoading, isInitialized, getCareerStatus, getEmail, getPhone, getLocation, getSkills, isUpdatingCareerStatus]); }, [profileData, profileType, isLoading, isInitialized, getCareerStatus, getEmail, getPhone, getLocation, getSkills, isUpdatingCareerStatus]);
const tryParseJsonMessage = (msg: string): string => { const tryParseJsonMessage = (msg: string): string => {
if (msg.trim().startsWith('{') && msg.trim().endsWith('}')) { if (msg.trim().startsWith('{') && msg.trim().endsWith('}')) {
try { try {
@@ -129,7 +127,6 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
return msg; return msg;
}; };
const extractApiMessage = (err: unknown): string => { const extractApiMessage = (err: unknown): string => {
if (typeof err !== 'object' || err === null) return ''; if (typeof err !== 'object' || err === null) return '';
@@ -147,7 +144,7 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
}; };
const handleProfileUpdate = async (updates: Partial<MentorUpdateRequestDto | UserUpdateRequestDto>) => { const handleProfileUpdate = async (updates: Partial<MentorUpdateRequestDto | UserUpdateRequestDto>) => {
if (isViewOnly) { // Prevent updates if in view-only mode if (isViewOnly) {
showNotification('error', 'Akses Ditolak', 'Anda tidak memiliki izin untuk mengedit profil ini.'); showNotification('error', 'Akses Ditolak', 'Anda tidak memiliki izin untuk mengedit profil ini.');
return; return;
} }
@@ -167,7 +164,7 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
<Select <Select
value={careerStatus} value={careerStatus}
onChange={async (e) => { onChange={async (e) => {
if (isUpdatingCareerStatus) return; // Prevent multiple clicks if (isUpdatingCareerStatus) return;
const newStatus = e.target.value; const newStatus = e.target.value;
console.log('ProfileSidebar: User selected career status:', newStatus); console.log('ProfileSidebar: User selected career status:', newStatus);
setCareerStatus(newStatus); setCareerStatus(newStatus);
@@ -190,7 +187,7 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
} }
}} }}
className="w-full min-w-[200px]" className="w-full min-w-[200px]"
disabled={isUpdatingCareerStatus || isViewOnly} // Disable if in view-only mode disabled={isUpdatingCareerStatus || isViewOnly}
> >
<option value="Career Status">Career Status</option> <option value="Career Status">Career Status</option>
<option value="Student">Student</option> <option value="Student">Student</option>
@@ -214,7 +211,7 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
}} }}
showNotification={showNotification} showNotification={showNotification}
isLoading={isUpdating} isLoading={isUpdating}
isViewOnly={isViewOnly} // Pass isViewOnly isViewOnly={isViewOnly}
/> />
<SkillsSection <SkillsSection
@@ -232,7 +229,7 @@ export const ProfileSidebar: FC<ProfileSidebarProps> = ({ showNotification, isVi
}} }}
showNotification={showNotification} showNotification={showNotification}
isLoading={isUpdating} isLoading={isUpdating}
isViewOnly={isViewOnly} // Pass isViewOnly isViewOnly={isViewOnly}
/> />
</div> </div>
); );
@@ -6,14 +6,13 @@ import { SectionWrapper } from '../shared/section-wrapper';
import { NotificationType } from '../modals/notification-modal'; import { NotificationType } from '../modals/notification-modal';
import { EditSectionButton } from '../buttons/edit-section-button'; import { EditSectionButton } from '../buttons/edit-section-button';
interface CvResumeSectionProps { interface CvResumeSectionProps {
initialFileName: string; initialFileName: string;
fullname: string; fullname: string;
onSave: (cvData: { fileName: string; fileUrl?: string }) => Promise<void>; onSave: (cvData: { fileName: string; fileUrl?: string }) => Promise<void>;
showNotification: (type: NotificationType['type'], title: string, message?: string) => void; showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isLoading?: boolean; isLoading?: boolean;
isViewOnly?: boolean; // Add isViewOnly prop isViewOnly?: boolean;
} }
export const CvResumeSection: FC<CvResumeSectionProps> = ({ export const CvResumeSection: FC<CvResumeSectionProps> = ({
@@ -22,7 +21,7 @@ export const CvResumeSection: FC<CvResumeSectionProps> = ({
onSave, onSave,
showNotification, showNotification,
isLoading = false, isLoading = false,
isViewOnly = false, // Default to false isViewOnly = false,
}) => { }) => {
const [isCVModalOpen, setIsCVModalOpen] = useState(false); const [isCVModalOpen, setIsCVModalOpen] = useState(false);
const [fileName, setFileName] = useState(initialFileName); const [fileName, setFileName] = useState(initialFileName);
@@ -33,11 +32,10 @@ export const CvResumeSection: FC<CvResumeSectionProps> = ({
}, [initialFileName]); }, [initialFileName]);
const handleSave = async (cvData: { fileName: string; fileUrl?: string }) => { const handleSave = async (cvData: { fileName: string; fileUrl?: string }) => {
if (isViewOnly) return; // Prevent save if in view-only mode if (isViewOnly) return;
await onSave(cvData); await onSave(cvData);
}; };
let displayFileName = 'Belum ada CV'; let displayFileName = 'Belum ada CV';
let fileUrl = ''; let fileUrl = '';
if (fileName) { if (fileName) {
@@ -65,7 +63,7 @@ export const CvResumeSection: FC<CvResumeSectionProps> = ({
<SectionWrapper <SectionWrapper
title="CV/Resume" title="CV/Resume"
editButton={ editButton={
!isViewOnly ? ( // Conditionally render the edit button !isViewOnly ? (
<EditSectionButton <EditSectionButton
onClick={() => setIsCVModalOpen(true)} onClick={() => setIsCVModalOpen(true)}
disabled={isLoading} disabled={isLoading}
@@ -94,7 +92,7 @@ export const CvResumeSection: FC<CvResumeSectionProps> = ({
</div> </div>
<CVModal <CVModal
isOpen={isCVModalOpen && !isViewOnly} // Only open if not in view-only mode isOpen={isCVModalOpen && !isViewOnly}
onClose={() => setIsCVModalOpen(false)} onClose={() => setIsCVModalOpen(false)}
initialValue={{ fileName, fileUrl: fileName }} initialValue={{ fileName, fileUrl: fileName }}
onSave={handleSave} onSave={handleSave}
@@ -9,7 +9,7 @@ interface DescriptionSectionProps {
onSave: (newDescription: string) => Promise<void>; onSave: (newDescription: string) => Promise<void>;
showNotification: (type: NotificationType['type'], title: string, message?: string) => void; showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isLoading?: boolean; isLoading?: boolean;
isViewOnly?: boolean; // Add isViewOnly prop isViewOnly?: boolean;
} }
export const DescriptionSection: FC<DescriptionSectionProps> = ({ export const DescriptionSection: FC<DescriptionSectionProps> = ({
@@ -17,7 +17,7 @@ export const DescriptionSection: FC<DescriptionSectionProps> = ({
onSave, onSave,
showNotification, showNotification,
isLoading = false, isLoading = false,
isViewOnly = false, // Default to false isViewOnly = false,
}) => { }) => {
const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState(false); const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState(false);
const [description, setDescription] = useState(initialDescription); const [description, setDescription] = useState(initialDescription);
@@ -28,7 +28,7 @@ export const DescriptionSection: FC<DescriptionSectionProps> = ({
}, [initialDescription]); }, [initialDescription]);
const handleSave = async (newDescription: string) => { const handleSave = async (newDescription: string) => {
if (isViewOnly) return; // Prevent save if in view-only mode if (isViewOnly) return;
await onSave(newDescription); await onSave(newDescription);
}; };
@@ -36,7 +36,7 @@ export const DescriptionSection: FC<DescriptionSectionProps> = ({
<SectionWrapper <SectionWrapper
title="Description" title="Description"
editButton={ editButton={
!isViewOnly ? ( // Conditionally render the edit button !isViewOnly ? (
<EditSectionButton <EditSectionButton
onClick={() => setIsDescriptionModalOpen(true)} onClick={() => setIsDescriptionModalOpen(true)}
disabled={isLoading} disabled={isLoading}
@@ -50,7 +50,7 @@ export const DescriptionSection: FC<DescriptionSectionProps> = ({
</div> </div>
<DescriptionModal <DescriptionModal
isOpen={isDescriptionModalOpen && !isViewOnly} // Only open if not in view-only mode isOpen={isDescriptionModalOpen && !isViewOnly}
onClose={() => setIsDescriptionModalOpen(false)} onClose={() => setIsDescriptionModalOpen(false)}
initialValue={description} initialValue={description}
onSave={handleSave} onSave={handleSave}
@@ -17,7 +17,7 @@ interface EducationSectionProps {
onSave: (newEducation: Education[]) => Promise<void>; onSave: (newEducation: Education[]) => Promise<void>;
showNotification: (type: NotificationType['type'], title: string, message?: string) => void; showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isLoading?: boolean; isLoading?: boolean;
isViewOnly?: boolean; // Add isViewOnly prop isViewOnly?: boolean;
} }
export const EducationSection: FC<EducationSectionProps> = ({ export const EducationSection: FC<EducationSectionProps> = ({
@@ -25,7 +25,7 @@ export const EducationSection: FC<EducationSectionProps> = ({
onSave, onSave,
showNotification, showNotification,
isLoading = false, isLoading = false,
isViewOnly = false, // Default to false isViewOnly = false,
}) => { }) => {
const [isEducationModalOpen, setIsEducationModalOpen] = useState(false); const [isEducationModalOpen, setIsEducationModalOpen] = useState(false);
const [education, setEducation] = useState<Education[]>(initialEducation); const [education, setEducation] = useState<Education[]>(initialEducation);
@@ -36,7 +36,7 @@ export const EducationSection: FC<EducationSectionProps> = ({
}, [initialEducation]); }, [initialEducation]);
const handleSave = async (newEducation: Education[]) => { const handleSave = async (newEducation: Education[]) => {
if (isViewOnly) return; // Prevent save if in view-only mode if (isViewOnly) return;
await onSave(newEducation); await onSave(newEducation);
}; };
@@ -44,7 +44,7 @@ export const EducationSection: FC<EducationSectionProps> = ({
<SectionWrapper <SectionWrapper
title="Education" title="Education"
editButton={ editButton={
!isViewOnly ? ( // Conditionally render the edit button !isViewOnly ? (
<div className="flex gap-2"> <div className="flex gap-2">
<EditSectionButton <EditSectionButton
onClick={() => setIsEducationModalOpen(true)} onClick={() => setIsEducationModalOpen(true)}
@@ -73,7 +73,7 @@ export const EducationSection: FC<EducationSectionProps> = ({
</div> </div>
<EducationModal <EducationModal
isOpen={isEducationModalOpen && !isViewOnly} // Only open if not in view-only mode isOpen={isEducationModalOpen && !isViewOnly}
onClose={() => setIsEducationModalOpen(false)} onClose={() => setIsEducationModalOpen(false)}
initialValue={education} initialValue={education}
onSave={handleSave} onSave={handleSave}
@@ -17,7 +17,7 @@ interface ExperiencesSectionProps {
onSave: (newExperiences: Experience[]) => Promise<void>; onSave: (newExperiences: Experience[]) => Promise<void>;
showNotification: (type: NotificationType['type'], title: string, message?: string) => void; showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isLoading?: boolean; isLoading?: boolean;
isViewOnly?: boolean; // Add isViewOnly prop isViewOnly?: boolean;
} }
export const ExperiencesSection: FC<ExperiencesSectionProps> = ({ export const ExperiencesSection: FC<ExperiencesSectionProps> = ({
@@ -25,7 +25,7 @@ export const ExperiencesSection: FC<ExperiencesSectionProps> = ({
onSave, onSave,
showNotification, showNotification,
isLoading = false, isLoading = false,
isViewOnly = false, // Default to false isViewOnly = false,
}) => { }) => {
const [isExperienceModalOpen, setIsExperienceModalOpen] = useState(false); const [isExperienceModalOpen, setIsExperienceModalOpen] = useState(false);
const [experiences, setExperiences] = useState<Experience[]>(initialExperiences); const [experiences, setExperiences] = useState<Experience[]>(initialExperiences);
@@ -36,7 +36,7 @@ export const ExperiencesSection: FC<ExperiencesSectionProps> = ({
}, [initialExperiences]); }, [initialExperiences]);
const handleSave = async (newExperiences: Experience[]) => { const handleSave = async (newExperiences: Experience[]) => {
if (isViewOnly) return; // Prevent save if in view-only mode if (isViewOnly) return;
await onSave(newExperiences); await onSave(newExperiences);
}; };
@@ -44,7 +44,7 @@ export const ExperiencesSection: FC<ExperiencesSectionProps> = ({
<SectionWrapper <SectionWrapper
title="Experiences" title="Experiences"
editButton={ editButton={
!isViewOnly ? ( // Conditionally render the edit button !isViewOnly ? (
<div className="flex gap-2"> <div className="flex gap-2">
<EditSectionButton <EditSectionButton
onClick={() => setIsExperienceModalOpen(true)} onClick={() => setIsExperienceModalOpen(true)}
@@ -73,7 +73,7 @@ export const ExperiencesSection: FC<ExperiencesSectionProps> = ({
</div> </div>
<ExperienceModal <ExperienceModal
isOpen={isExperienceModalOpen && !isViewOnly} // Only open if not in view-only mode isOpen={isExperienceModalOpen && !isViewOnly}
onClose={() => setIsExperienceModalOpen(false)} onClose={() => setIsExperienceModalOpen(false)}
initialValue={experiences} initialValue={experiences}
onSave={handleSave} onSave={handleSave}
@@ -16,7 +16,7 @@ interface PersonalInfoSectionProps {
onSave: (newContactInfo: PersonalInfo) => Promise<void>; onSave: (newContactInfo: PersonalInfo) => Promise<void>;
showNotification: (type: NotificationType['type'], title: string, message?: string) => void; showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isLoading?: boolean; isLoading?: boolean;
isViewOnly?: boolean; // Add isViewOnly prop isViewOnly?: boolean;
} }
export const PersonalInfoSection: FC<PersonalInfoSectionProps> = ({ export const PersonalInfoSection: FC<PersonalInfoSectionProps> = ({
@@ -24,18 +24,17 @@ export const PersonalInfoSection: FC<PersonalInfoSectionProps> = ({
onSave, onSave,
showNotification, showNotification,
isLoading = false, isLoading = false,
isViewOnly = false, // Default to false isViewOnly = false,
}) => { }) => {
const [isPersonalInfoModalOpen, setIsPersonalInfoModalOpen] = useState(false); const [isPersonalInfoModalOpen, setIsPersonalInfoModalOpen] = useState(false);
const [personalInfo, setPersonalInfo] = useState<PersonalInfo>(initialContactInfo); const [personalInfo, setPersonalInfo] = useState<PersonalInfo>(initialContactInfo);
useEffect(() => { useEffect(() => {
setPersonalInfo(initialContactInfo); setPersonalInfo(initialContactInfo);
}, [initialContactInfo]); }, [initialContactInfo]);
const handleSave = async (newInfo: { email: string; phone: string; location: string }) => { const handleSave = async (newInfo: { email: string; phone: string; location: string }) => {
if (isViewOnly) return; // Prevent save if in view-only mode if (isViewOnly) return;
const newPersonalInfo = { email: newInfo.email, phone: newInfo.phone, location: newInfo.location }; const newPersonalInfo = { email: newInfo.email, phone: newInfo.phone, location: newInfo.location };
await onSave(newPersonalInfo); await onSave(newPersonalInfo);
@@ -45,7 +44,7 @@ export const PersonalInfoSection: FC<PersonalInfoSectionProps> = ({
<SectionWrapper <SectionWrapper
title="Personal Informations" title="Personal Informations"
editButton={ editButton={
!isViewOnly ? ( // Conditionally render the edit button !isViewOnly ? (
<EditSectionButton <EditSectionButton
onClick={() => setIsPersonalInfoModalOpen(true)} onClick={() => setIsPersonalInfoModalOpen(true)}
disabled={isLoading} disabled={isLoading}
@@ -87,7 +86,7 @@ export const PersonalInfoSection: FC<PersonalInfoSectionProps> = ({
</div> </div>
<PersonalInfoModal <PersonalInfoModal
isOpen={isPersonalInfoModalOpen && !isViewOnly} // Only open if not in view-only mode isOpen={isPersonalInfoModalOpen && !isViewOnly}
onClose={() => setIsPersonalInfoModalOpen(false)} onClose={() => setIsPersonalInfoModalOpen(false)}
initialValue={personalInfo} initialValue={personalInfo}
onSave={handleSave} onSave={handleSave}
@@ -14,7 +14,7 @@ interface SkillsSectionProps {
onSave: (newSkills: string[]) => Promise<void>; onSave: (newSkills: string[]) => Promise<void>;
showNotification: (type: NotificationType['type'], title: string, message?: string) => void; showNotification: (type: NotificationType['type'], title: string, message?: string) => void;
isLoading?: boolean; isLoading?: boolean;
isViewOnly?: boolean; // Add isViewOnly prop isViewOnly?: boolean;
} }
export const SkillsSection: FC<SkillsSectionProps> = ({ export const SkillsSection: FC<SkillsSectionProps> = ({
@@ -22,18 +22,17 @@ export const SkillsSection: FC<SkillsSectionProps> = ({
onSave, onSave,
showNotification, showNotification,
isLoading = false, isLoading = false,
isViewOnly = false, // Default to false isViewOnly = false,
}) => { }) => {
const [isSkillsModalOpen, setIsSkillsModalOpen] = useState(false); const [isSkillsModalOpen, setIsSkillsModalOpen] = useState(false);
const [skills, setSkills] = useState<string[]>(initialSkills); const [skills, setSkills] = useState<string[]>(initialSkills);
useEffect(() => { useEffect(() => {
setSkills(initialSkills); setSkills(initialSkills);
}, [initialSkills]); }, [initialSkills]);
const handleSave = async (newSkills: Skill[]) => { const handleSave = async (newSkills: Skill[]) => {
if (isViewOnly) return; // Prevent save if in view-only mode if (isViewOnly) return;
const stringSkills = newSkills.map(skill => skill.name); const stringSkills = newSkills.map(skill => skill.name);
await onSave(stringSkills); await onSave(stringSkills);
@@ -43,7 +42,7 @@ export const SkillsSection: FC<SkillsSectionProps> = ({
<SectionWrapper <SectionWrapper
title="Skills" title="Skills"
editButton={ editButton={
!isViewOnly ? ( // Conditionally render the edit button !isViewOnly ? (
<EditSectionButton <EditSectionButton
onClick={() => setIsSkillsModalOpen(true)} onClick={() => setIsSkillsModalOpen(true)}
disabled={isLoading} disabled={isLoading}
@@ -64,7 +63,7 @@ export const SkillsSection: FC<SkillsSectionProps> = ({
</div> </div>
<SkillsModal <SkillsModal
isOpen={isSkillsModalOpen && !isViewOnly} // Only open if not in view-only mode isOpen={isSkillsModalOpen && !isViewOnly}
onClose={() => setIsSkillsModalOpen(false)} onClose={() => setIsSkillsModalOpen(false)}
initialValue={skills.map(skill => ({ id: skill, name: skill }))} initialValue={skills.map(skill => ({ id: skill, name: skill }))}
onSave={handleSave} onSave={handleSave}
@@ -78,7 +78,6 @@ export const useGoogleLogin = () => {
const user = payload.user; const user = payload.user;
if (accessToken && refreshToken && user && typeof accessToken === 'string' && typeof refreshToken === 'string') { if (accessToken && refreshToken && user && typeof accessToken === 'string' && typeof refreshToken === 'string') {
// Convert Google user data to match TUserItem structure
const convertedUser = { const convertedUser = {
id: user.id, id: user.id,
avatar: user.avatar || '', avatar: user.avatar || '',
@@ -97,7 +96,6 @@ export const useGoogleLogin = () => {
} }
}; };
// Use setSession like credential login does
setSession({ setSession({
token: { token: {
access_token: accessToken, access_token: accessToken,
@@ -107,7 +105,7 @@ export const useGoogleLogin = () => {
}); });
toast.success('Login berhasil!'); toast.success('Login berhasil!');
navigate(0); // Same as credential login navigate(0);
} else { } else {
toast.error('Data login tidak lengkap'); toast.error('Data login tidak lengkap');
} }
+4 -4
View File
@@ -2,9 +2,9 @@ import { useForm } from 'react-hook-form';
import { import {
authLoginSchema, authLoginSchema,
TLoginRequest, TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useSession } from '@imphnen-frontend-service/utils';
export const useLogin = () => { export const useLogin = () => {
const form = useForm<TLoginRequest>({ const form = useForm<TLoginRequest>({
@@ -12,13 +12,13 @@ export const useLogin = () => {
mode: 'all', mode: 'all',
}); });
const { signIn, isLoading } = useSession(); const loginMutation = usePostLogin();
const onSubmit = form.handleSubmit((data) => signIn(data)); const onSubmit = form.handleSubmit((data) => loginMutation.mutate(data));
return { return {
form, form,
onSubmit, onSubmit,
isLoading isLoading: loginMutation.isPending,
}; };
}; };
+3 -3
View File
@@ -2,9 +2,9 @@ import { useForm } from 'react-hook-form';
import { import {
TVerifyOtpRequest, TVerifyOtpRequest,
verifyEmailSchema, verifyEmailSchema,
usePostVerifyEmail,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useOtp } from '@imphnen-frontend-service/utils';
export const useOtpHook = () => { export const useOtpHook = () => {
const form = useForm<TVerifyOtpRequest>({ const form = useForm<TVerifyOtpRequest>({
@@ -12,9 +12,9 @@ export const useOtpHook = () => {
mode: 'all', mode: 'all',
}); });
const { otp, isLoading } = useOtp(); const { mutate, isPending: isLoading } = usePostVerifyEmail();
const onSubmit = form.handleSubmit((data) => otp(data)); const onSubmit = form.handleSubmit(() => mutate());
return { return {
form, form,
@@ -2,9 +2,9 @@ import { useForm } from 'react-hook-form';
import { import {
authRegisterSchema, authRegisterSchema,
TRegisterRequest, TRegisterRequest,
usePostRegister,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useRegister } from '@imphnen-frontend-service/utils';
export const useRegisterHook = () => { export const useRegisterHook = () => {
const form = useForm<TRegisterRequest>({ const form = useForm<TRegisterRequest>({
@@ -12,9 +12,15 @@ export const useRegisterHook = () => {
mode: 'all', mode: 'all',
}); });
const { register, isLoading } = useRegister(); const { mutate: register, isPending: isLoading } = usePostRegister();
const onSubmit = form.handleSubmit((data) => register(data)); const onSubmit = form.handleSubmit((data) =>
register({
email: data.email,
password: data.password,
fullname: data.fullname,
})
);
return { return {
form, form,
@@ -1,10 +1,15 @@
import { useSendOTP } from '@imphnen-frontend-service/utils'; import { usePostSendOtp } from '@imphnen-frontend-service/service';
export const useResendOtpHook = () => { export const useResendOtpHook = () => {
const { resendOTP, isLoading } = useSendOTP(); const { mutate, isPending: isLoading } = usePostSendOtp();
const resendOTP = (_data?: { email: string }) => {
mutate();
};
return { return {
resendOTP resendOTP,
isLoading
}; };
}; };

Some files were not shown because too many files have changed in this diff Show More