feat: integrate all backend APIs into frontend apps

- Fix API service paths to use /v1/iam/, /v1/dimentorin/, /v1/gacha/, /v1/hackathon/ prefixes
- Add roles, permissions, events, testimonials, sessions API services and hooks
- Rewrite gacha service with full CRUD + roll/claim endpoints
- Replace all mock data in backoffice pages with real API calls (accounts, roles, permissions, gacha-roll, dashboard, sessions, users-dimentorin, feedback-review, settings)
- Wire dimentorin mentoring list to useMentorList with search + pagination
- Wire dimentorin mentor detail page to useMentorById, pass real data to all sections
- Wire appointment modal to useBookSession with controlled schedule inputs
- Wire gacha app Spin Now to useExecuteGachaRoll, show credits and real items

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-04-05 17:08:58 +07:00
co-authored by Claude Sonnet 4.6
parent 8d2aaf5188
commit 53ad40f3fd
74 changed files with 3028 additions and 1942 deletions
@@ -1,15 +1,17 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
import { useState } from 'react';
import { useEffect, useState } from 'react';
interface IModalEditAccount {
isOpen: boolean;
onClose: () => void;
handleEditAccount?: () => void;
handleEditAccount?: () => Promise<void>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { fullname?: string; email?: string };
onDataCapture?: (data: any) => void;
}
const ModalEditAccount = ({
@@ -17,9 +19,10 @@ const ModalEditAccount = ({
onClose,
currentStep,
nextStep,
prevStep,
resetStep,
handleEditAccount,
initialValues,
onDataCapture,
}: IModalEditAccount) => {
return (
<Modal
@@ -31,7 +34,14 @@ const ModalEditAccount = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && (
<StepOne
nextStep={nextStep}
onClose={onClose}
initialValues={initialValues}
onDataCapture={onDataCapture}
/>
)}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,13 +56,18 @@ const ModalEditAccount = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
initialValues?: { fullname?: string; email?: string };
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const [fullName, setFullName] = useState('Ahmad Wiyana');
const [email, setEmail] = useState('fullname23@gmail.com');
const [phoneNumber, setPhoneNumber] = useState('081904423804');
const [address, setAddress] = useState('Jl. Pantai Cibaduyut Indah');
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
const [fullName, setFullName] = useState(initialValues?.fullname ?? '');
const [email, setEmail] = useState(initialValues?.email ?? '');
useEffect(() => {
setFullName(initialValues?.fullname ?? '');
setEmail(initialValues?.email ?? '');
}, [initialValues]);
return (
<>
@@ -81,31 +96,16 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
size="lg"
className="w-full"
/>
<InputField
label="Nomor Telepon"
type="text"
placeholder="Masukkan Nomor Telepon"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
size="lg"
className="w-full"
/>
<InputField
label="Alamat"
type="text"
placeholder="Masukkan Alamat"
value={address}
onChange={(e) => setAddress(e.target.value)}
size="lg"
className="w-full"
/>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={nextStep}
onClick={() => {
onDataCapture?.({ fullname: fullName, email });
nextStep();
}}
>
Perbarui Data
</Button>
@@ -116,7 +116,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
interface IStepTwoProps {
onClose: () => void;
handleEditAccount?: () => void;
handleEditAccount?: () => Promise<void>;
resetStep: () => void;
}
@@ -147,8 +147,8 @@ const StepTwo = ({ onClose, handleEditAccount, resetStep }: IStepTwoProps) => (
variant="primary"
size="lg"
className="w-full"
onClick={() => {
handleEditAccount && handleEditAccount();
onClick={async () => {
if (handleEditAccount) await handleEditAccount();
onClose();
resetStep();
}}
@@ -1,6 +1,6 @@
import * as React from 'react';
import { FC, Fragment, ReactElement, useState } from 'react';
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
import {
FilterOutlined,
SearchOutlined,
@@ -19,25 +19,17 @@ import {
} from '@tanstack/react-table';
import ModalEditAccount from './_components/modal-edit-account';
import { useQueryState } from '@imphnen-frontend-service/utils';
interface Account {
id: number;
name: string;
email: string;
phone: string;
address: string;
}
const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
email: 'fullname23@gmail.com',
phone: '081904423804',
address: 'Jl. Pantai Cibaduyut Indah',
}));
import {
useUserList,
useUpdateUserById,
TUsersListItem,
} from '@imphnen-frontend-service/service';
export const Components: FC = (): ReactElement => {
const [showModalEditAccount, setShowModalEditAccount] = useState(false);
const [selectedUser, setSelectedUser] = useState<TUsersListItem | null>(null);
const [search, setSearch] = useState('');
const pendingFormData = useRef<any>(null);
const {
step: currentStep,
@@ -58,7 +50,23 @@ export const Components: FC = (): ReactElement => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
const columns: ColumnDef<Account>[] = [
const { data: usersData, isLoading } = useUserList({
search,
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const updateUser = useUpdateUserById();
const users: TUsersListItem[] = usersData?.data ?? [];
const totalItems = usersData?.meta?.total ?? users.length;
const handleEditAccount = async () => {
if (selectedUser && pendingFormData.current) {
await updateUser.mutateAsync({ id: selectedUser.id, data: pendingFormData.current });
}
};
const columns: ColumnDef<TUsersListItem>[] = [
{
id: 'select',
header: ({ table }) => (
@@ -84,19 +92,24 @@ export const Components: FC = (): ReactElement => {
},
{
header: 'Nama Lengkap',
accessorKey: 'name',
accessorKey: 'fullname',
},
{
header: 'Email',
accessorKey: 'email',
},
{
header: 'Nomor Telp',
accessorKey: 'phone',
header: 'Role',
accessorKey: 'role',
},
{
header: 'Alamat Pengiriman',
accessorKey: 'address',
header: 'Status',
accessorKey: 'is_active',
cell: ({ row }) => (
<span className={row.original.is_active ? 'text-success-500' : 'text-danger-500'}>
{row.original.is_active ? 'Aktif' : 'Tidak Aktif'}
</span>
),
},
{
header: 'Action',
@@ -106,6 +119,7 @@ export const Components: FC = (): ReactElement => {
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedUser(row.original);
setShowModalEditAccount(true);
}}
className="flex items-center gap-2"
@@ -117,7 +131,7 @@ export const Components: FC = (): ReactElement => {
];
const table = useReactTable({
data: mockData,
data: users,
columns,
state: {
pagination,
@@ -128,8 +142,8 @@ export const Components: FC = (): ReactElement => {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -144,6 +158,8 @@ export const Components: FC = (): ReactElement => {
<Input
placeholder="Cari berdasarkan nama lengkap, email"
className="pl-12 w-full max-h-full"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
@@ -167,7 +183,11 @@ export const Components: FC = (): ReactElement => {
)}
</div>
</div>
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable data={users} columns={columns} table={table} />
)}
</section>
</main>
@@ -175,12 +195,12 @@ export const Components: FC = (): ReactElement => {
currentStep={currentStep}
isOpen={showModalEditAccount}
onClose={() => setShowModalEditAccount(false)}
handleEditAccount={() => {
console.log('Account updated');
}}
handleEditAccount={handleEditAccount}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
initialValues={selectedUser ? { fullname: selectedUser.fullname, email: selectedUser.email } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
</Fragment>
);
@@ -4,17 +4,26 @@ import { For } from "@imphnen-frontend-service/utils";
import { ReactElement } from "react";
import { UserGrowthChart } from "./_components/chart/user-growth";
import { SessionStatusChart } from "./_components/chart/session-status";
const Overview = () => {
return (
<div className="bg-white px-6 py-4 rounded-md shadow">
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">0</h3>
<p className="text-neutral-400 text-p3">Total Users</p>
</div>
)
}
import { useMentorList, useUserList, useMySessions } from "@imphnen-frontend-service/service";
export default function Components(): ReactElement {
const { data: mentorData } = useMentorList({ per_page: 5, sort_by: 'rating', order: 'desc' });
const { data: userData } = useUserList({ per_page: 1 });
const { data: sessionsData } = useMySessions();
const totalMentors = mentorData?.meta?.total ?? 0;
const totalUsers = userData?.meta?.total ?? 0;
const totalSessions = sessionsData?.total ?? 0;
const topMentors = mentorData?.data ?? [];
const overviewStats = [
{ label: 'Total Users', value: totalUsers },
{ label: 'Total Mentors', value: totalMentors },
{ label: 'Total Sessions', value: totalSessions },
{ label: 'Active Mentors', value: topMentors.filter((m) => m.status === 'active').length },
{ label: 'Completed Sessions', value: sessionsData?.sessions?.filter((s) => s.status === 'completed').length ?? 0 },
];
return (
<BackofficeWrapper title="Dimentorin.dev">
<h1 className="text-p1 font-semibold text-neutral-700 mb-5">Overview</h1>
@@ -26,8 +35,13 @@ export default function Components(): ReactElement {
</Button>
<div className="grid grid-cols-5 gap-5">
<For data={Array.from({ length: 5 })}>
{(_, index) => <Overview key={index} />}
<For data={overviewStats}>
{(stat, index) => (
<div key={index} className="bg-white px-6 py-4 rounded-md shadow">
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">{stat.value}</h3>
<p className="text-neutral-400 text-p3">{stat.label}</p>
</div>
)}
</For>
</div>
</div>
@@ -67,15 +81,18 @@ export default function Components(): ReactElement {
</thead>
<tbody>
<For data={Array.from({ length: 5 })}>
{(_, index) => (
<tr key={index} className="shadow rounded-lg">
<td className="py-4 px-5">{index + 1}</td>
<td className="py-4 px-5">Mursid Al-Catraz</td>
<td className="py-4 px-5">4.9</td>
</tr>
)}
</For>
{topMentors.slice(0, 5).map((mentor, index) => (
<tr key={mentor.id} className="shadow rounded-lg">
<td className="py-4 px-5">{index + 1}</td>
<td className="py-4 px-5">{mentor.fullname ?? '-'}</td>
<td className="py-4 px-5">{mentor.rating?.toFixed(1) ?? '-'}</td>
</tr>
))}
{topMentors.length === 0 && (
<tr>
<td colSpan={3} className="py-4 px-5 text-center text-neutral-400">Belum ada data</td>
</tr>
)}
</tbody>
</table>
</div>
@@ -89,21 +106,36 @@ export default function Components(): ReactElement {
<thead>
<tr className="text-label1 bg-primary-50 text-left font-medium">
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
<th className="font-medium py-4 px-5 w-3/5">Topik</th>
<th className="font-medium py-4 px-5 rounded-r-lg">Total Sesi</th>
</tr>
</thead>
<tbody>
<For data={Array.from({ length: 5 })}>
{(_, index) => (
<tr key={index} className="shadow rounded-lg">
{(() => {
const sessions = sessionsData?.sessions ?? [];
const topicCount: Record<string, number> = {};
sessions.forEach((s) => {
topicCount[s.topic] = (topicCount[s.topic] ?? 0) + 1;
});
const topTopics = Object.entries(topicCount)
.sort(([, a], [, b]) => b - a)
.slice(0, 5);
if (topTopics.length === 0) {
return (
<tr>
<td colSpan={3} className="py-4 px-5 text-center text-neutral-400">Belum ada data</td>
</tr>
);
}
return topTopics.map(([topic, count], index) => (
<tr key={topic} className="shadow rounded-lg">
<td className="py-4 px-5">{index + 1}</td>
<td className="py-4 px-5">Mursid Al-Catraz</td>
<td className="py-4 px-5">1000</td>
<td className="py-4 px-5">{topic}</td>
<td className="py-4 px-5">{count}</td>
</tr>
)}
</For>
));
})()}
</tbody>
</table>
</div>
@@ -11,6 +11,7 @@ interface IModalAddItem {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddItem = ({
@@ -20,6 +21,7 @@ const ModalAddItem = ({
nextStep,
resetStep,
handleAddItem,
onDataCapture,
}: IModalAddItem) => {
return (
<Modal
@@ -31,7 +33,7 @@ const ModalAddItem = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,10 +48,11 @@ const ModalAddItem = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep);
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
@@ -98,7 +101,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
variant="primary"
size="lg"
className="w-full"
onClick={nextStep}
type="submit"
>
Tambahkan Item
</Button>
@@ -4,7 +4,7 @@ import { Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalDeleteItem {
isOpen: boolean;
onClose: () => void;
handleDeleteItem?: () => void;
handleDeleteItem?: () => Promise<boolean>;
}
const ModalDeleteItem = ({
@@ -48,8 +48,9 @@ const ModalDeleteItem = ({
variant="danger"
size="lg"
className="w-full"
onClick={() => {
handleDeleteItem && handleDeleteItem();
onClick={async () => {
if (handleDeleteItem) await handleDeleteItem();
onClose();
}}
>
Hapus Item
@@ -11,6 +11,8 @@ interface IModalEditItem {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { itemName?: string; quantity?: number };
onDataCapture?: (data: any) => void;
}
const ModalEditItem = ({
@@ -20,6 +22,8 @@ const ModalEditItem = ({
nextStep,
resetStep,
handleEditItem,
initialValues,
onDataCapture,
}: IModalEditItem) => {
return (
<Modal
@@ -31,7 +35,7 @@ const ModalEditItem = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} initialValues={initialValues} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,15 +50,12 @@ const ModalEditItem = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
initialValues?: { itemName?: string; quantity?: number };
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const initialValues = {
itemName: 'Hoodie IMPHNEN Official 2025',
quantity: 10,
};
const { form, onSubmit } = useItem(nextStep, initialValues);
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, initialValues as any, onDataCapture);
return (
<>
@@ -102,7 +103,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
variant="primary"
size="lg"
className="w-full"
onClick={nextStep}
type="submit"
>
Perbarui Item
</Button>
@@ -8,16 +8,17 @@ import { toast } from 'sonner';
export const useItem = (
nextStep: () => void,
initialValues?: TGachaItem
initialValues?: Partial<TGachaItem>,
onDataCapture?: (data: any) => void,
) => {
const form = useForm<TGachaItem>({
const form = useForm<any>({
resolver: zodResolver(gachaItemSchema),
mode: 'all',
defaultValues: initialValues,
});
const onSubmit = form.handleSubmit((data) => {
console.log('Form data:', data);
onDataCapture?.(data);
nextStep();
});
@@ -38,6 +39,9 @@ export const useConfirmItem = (
) => {
const onConfirm = async () => {
try {
if (actionFunction) {
await actionFunction();
}
toast.success(messages?.success);
onClose();
resetStep();
@@ -6,16 +6,26 @@ import {
UserSwitchOutlined,
} from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { FC, Fragment, ReactElement, useState } from 'react';
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
import ModalAddItem from './_components/modal-add-item';
import ModalEditItem from './_components/modal-edit-item';
import ModalDeleteItem from './_components/modal-delete-item';
import { useQueryState } from '@imphnen-frontend-service/utils';
import {
useUserList,
useGachaItemList,
useCreateGachaItem,
useUpdateGachaItem,
useDeleteGachaItem,
TGachaItemDto,
} from '@imphnen-frontend-service/service';
export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false);
const [showModalEditItem, setShowModalEditItem] = useState(false);
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null);
const pendingFormData = useRef<any>(null);
const {
step: currentStep,
@@ -28,6 +38,52 @@ export const Components: FC = (): ReactElement => {
minValue: 1,
});
const { data: usersData } = useUserList({ per_page: 1 });
const { data: gachaItemsData } = useGachaItemList({ per_page: 9 });
const createItem = useCreateGachaItem();
const updateItem = useUpdateGachaItem();
const deleteItem = useDeleteGachaItem();
const totalUsers = usersData?.meta?.total ?? 0;
const gachaItems: TGachaItemDto[] = gachaItemsData?.data ?? [];
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
const { itemName, quantity } = pendingFormData.current;
await createItem.mutateAsync({
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
name: itemName,
description: '',
rarity: 'common',
type_: 'physical',
category: 'merchandise',
value: 0,
weight: 1,
stock: quantity ?? 1,
is_limited: false,
});
}
return true;
};
const handleEdit = async (): Promise<boolean> => {
if (selectedItem && pendingFormData.current) {
const { itemName, quantity } = pendingFormData.current;
await updateItem.mutateAsync({
id: selectedItem.id,
data: { name: itemName, stock: quantity },
});
}
return true;
};
const handleDelete = async (): Promise<boolean> => {
if (selectedItem) {
await deleteItem.mutateAsync(selectedItem.id);
}
return true;
};
return (
<Fragment>
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
@@ -47,7 +103,7 @@ export const Components: FC = (): ReactElement => {
<UsergroupAddOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<h3 className="text-p1 font-semibold">{totalUsers}</h3>
<p className="text-label1 text-neutral-500">Participants</p>
</div>
</div>
@@ -57,9 +113,9 @@ export const Components: FC = (): ReactElement => {
<ReloadOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<h3 className="text-p1 font-semibold">{gachaItemsData?.meta?.total ?? 0}</h3>
<p className="text-label1 text-neutral-500">
Roll and Reroll
Gacha Items
</p>
</div>
</div>
@@ -69,7 +125,7 @@ export const Components: FC = (): ReactElement => {
<UserSwitchOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<h3 className="text-p1 font-semibold">-</h3>
<p className="text-label1 text-neutral-500">Redeem</p>
</div>
</div>
@@ -79,7 +135,7 @@ export const Components: FC = (): ReactElement => {
<UsergroupDeleteOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<h3 className="text-p1 font-semibold">-</h3>
<p className="text-label1 text-neutral-500">
Inactive Users
</p>
@@ -105,19 +161,18 @@ export const Components: FC = (): ReactElement => {
</div>
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
{[1, 2, 3, 4, 5, 6].map((item) => (
{gachaItems.map((item) => (
<div
key={item}
key={item.id}
className="bg-white overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
>
<div className="flex flex-col py-4 px-6 gap-[8px]">
<div>
<h3 className="text-p3 text-primary-500 font-medium">
Lanyard IMPHNEN
{item.name}
</h3>
<div className="flex items-center justify-start gap-10 text-label2 text-gray-500 mt-1">
<span>Prize {item}</span>
<span>Quantity: 10</span>
<span>{item.id}</span>
</div>
</div>
<div className="flex justify-start gap-2">
@@ -125,7 +180,10 @@ export const Components: FC = (): ReactElement => {
variant="text"
size="sm"
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
onClick={() => setShowModalEditItem(true)}
onClick={() => {
setSelectedItem(item);
setShowModalEditItem(true);
}}
>
Edit
</Button>
@@ -133,14 +191,17 @@ export const Components: FC = (): ReactElement => {
variant="text"
size="sm"
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
onClick={() => setShowModalDeleteItem(true)}
onClick={() => {
setSelectedItem(item);
setShowModalDeleteItem(true);
}}
>
Delete
</Button>
</div>
</div>
<img src="gacha-clip.webp" alt="Lanyard IMPHNEN" />
<img src="gacha-clip.webp" alt={item.name} />
</div>
))}
</div>
@@ -162,6 +223,8 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAddItem={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalEditItem
@@ -171,11 +234,15 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleEditItem={handleEdit}
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalDeleteItem
isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)}
handleDeleteItem={async () => { await handleDelete(); return true; }}
/>
</Fragment>
);
@@ -4,6 +4,7 @@ import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organ
import { cn, For } from "@imphnen-frontend-service/utils";
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
import { ReactElement, useState } from "react"
import { useMySessions, TSessionListItem } from "@imphnen-frontend-service/service";
const TABS = {
MENTORING: 'Mentoring',
@@ -11,24 +12,6 @@ const TABS = {
} as const
type Tabs = typeof TABS[keyof typeof TABS]
type FeedbackStatus = 'done' | 'todo';
interface FeedbackType {
id: number
name: string
email: string
rating: number
status: FeedbackStatus
}
const mockData: FeedbackType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
email: 'fullname23@gmail.com',
rating: 4.5,
status: i % 2 === 0 ? 'done' : 'todo',
}))
export default function Components(): ReactElement {
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
@@ -38,7 +21,18 @@ export default function Components(): ReactElement {
pageSize: 9,
});
const columns: ColumnDef<FeedbackType>[] = [
const { data: sessionsData, isLoading } = useMySessions(
activeTab === TABS.MENTORING ? { status: 'completed' } : undefined
);
const sessions: TSessionListItem[] = activeTab === TABS.MENTORING
? (sessionsData?.sessions ?? [])
: [];
const totalItems = activeTab === TABS.MENTORING
? (sessionsData?.total ?? sessions.length)
: 0;
const columns: ColumnDef<TSessionListItem>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-20") },
@@ -62,37 +56,30 @@ export default function Components(): ReactElement {
{
id: 'name',
header: 'Name',
accessorKey: 'name',
accessorKey: 'mentee_fullname',
cell: ({ row }) => <span>{row.original.mentee_fullname ?? '-'}</span>,
},
{
id: 'email',
header: 'Email',
accessorKey: 'email',
accessorKey: 'mentee_email',
cell: ({ row }) => <span>{row.original.mentee_email ?? '-'}</span>,
},
{
id: 'rating',
header: 'Rating',
accessorKey: 'rating',
cell: ({ row }) => <span>{row.original.rating ?? '-'}</span>,
},
{
id: 'status',
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<FeedbackStatus, string> = {
done: 'bg-success-200 text-success-500',
todo: 'bg-primary-200 text-primary-500',
};
const statusText: Record<FeedbackStatus, string> = {
done: 'Done',
todo: 'To Do',
};
const hasRating = !!row.original.rating;
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
<div className={`py-2 px-4 rounded-md text-center ${hasRating ? 'bg-success-200 text-success-500' : 'bg-primary-200 text-primary-500'}`}>
{hasRating ? 'Done' : 'To Do'}
</div>
);
},
@@ -100,7 +87,7 @@ export default function Components(): ReactElement {
{
header: 'Action',
meta: { cellClassName: cn("w-72") },
cell: ({ row }) => (
cell: () => (
<Button
variant="primary"
size="sm"
@@ -116,7 +103,7 @@ export default function Components(): ReactElement {
]
const table = useReactTable({
data: mockData,
data: sessions,
columns,
state: {
pagination,
@@ -127,8 +114,8 @@ export default function Components(): ReactElement {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -142,7 +129,10 @@ export default function Components(): ReactElement {
key={tab}
variant="text"
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
onClick={() => setActiveTab(tab)}
onClick={() => {
setActiveTab(tab);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
>
{tab}
</Button>
@@ -163,18 +153,26 @@ export default function Components(): ReactElement {
</div>
</div>
<Select>
<option selected disabled>Rating</option>
<option disabled>Rating</option>
<option value="4.5">4.5</option>
<option value="5">5</option>
</Select>
<Select>
<option selected disabled>Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option disabled>Status</option>
<option value="done">Done</option>
<option value="todo">To Do</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : activeTab === TABS.PLATFORM ? (
<div className="text-center py-8 text-neutral-400">
Platform feedback tidak tersedia
</div>
) : (
<DataTable data={sessions} columns={columns} table={table} />
)}
</section>
</BackofficeWrapper>
);
@@ -11,6 +11,7 @@ interface IModalAddItem {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddItem = ({
@@ -20,6 +21,7 @@ const ModalAddItem = ({
nextStep,
resetStep,
handleAddItem,
onDataCapture,
}: IModalAddItem) => {
return (
<Modal
@@ -31,7 +33,7 @@ const ModalAddItem = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,10 +48,11 @@ const ModalAddItem = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep);
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
@@ -4,7 +4,7 @@ import { Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalDeleteItem {
isOpen: boolean;
onClose: () => void;
handleDeleteItem?: () => void;
handleDeleteItem?: () => Promise<void>;
}
const ModalDeleteItem = ({
@@ -48,8 +48,9 @@ const ModalDeleteItem = ({
variant="danger"
size="lg"
className="w-full"
onClick={() => {
handleDeleteItem && handleDeleteItem();
onClick={async () => {
if (handleDeleteItem) await handleDeleteItem();
onClose();
}}
>
Hapus Item
@@ -11,6 +11,8 @@ interface IModalUpdateItem {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { itemName?: string; quantity?: number; chanceRate?: number };
onDataCapture?: (data: any) => void;
}
const ModalUpdateItem = ({
@@ -20,6 +22,8 @@ const ModalUpdateItem = ({
nextStep,
resetStep,
handleUpdateItem,
initialValues,
onDataCapture,
}: IModalUpdateItem) => {
return (
<Modal
@@ -31,7 +35,7 @@ const ModalUpdateItem = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} initialValues={initialValues} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,16 +50,12 @@ const ModalUpdateItem = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
initialValues?: { itemName?: string; quantity?: number; chanceRate?: number };
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const initialValues = {
itemName: 'Hoodie IMPHNEN Official 2025',
quantity: 10,
chanceRate: 0.1,
};
const { form, onSubmit } = useItem(nextStep, initialValues);
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, initialValues as any, onDataCapture);
return (
<>
@@ -105,7 +105,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
variant="primary"
size="lg"
className="w-full"
onClick={nextStep}
type="submit"
>
Perbarui Item
</Button>
@@ -8,16 +8,17 @@ import { toast } from 'sonner';
export const useItem = (
nextStep: () => void,
initialValues?: TGachaRollItem
initialValues?: Partial<TGachaRollItem>,
onDataCapture?: (data: any) => void,
) => {
const form = useForm<TGachaRollItem>({
const form = useForm<any>({
resolver: zodResolver(gachaRollItemSchema),
mode: 'all',
defaultValues: initialValues,
});
const onSubmit = form.handleSubmit((data) => {
console.log('Form data:', data);
onDataCapture?.(data);
nextStep();
});
@@ -38,6 +39,9 @@ export const useConfirmItem = (
) => {
const onConfirm = async () => {
try {
if (actionFunction) {
await actionFunction();
}
toast.success(messages?.success);
onClose();
resetStep();
@@ -1,6 +1,6 @@
import * as React from 'react';
import { FC, Fragment, ReactElement, useState } from 'react';
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
import {
SearchOutlined,
EditOutlined,
@@ -22,25 +22,21 @@ import ModalAddItem from './_components/modal-add-item';
import ModalUpdateItem from './_components/modal-update-item';
import ModalDeleteItem from './_components/modal-delete-item';
import { useQueryState } from '@imphnen-frontend-service/utils';
interface GachaItem {
id: number;
name: string;
chanceRate: number;
quantity: number;
}
const mockData: GachaItem[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: 'Hoodie IMPHNEN Official 2025',
chanceRate: 0.1,
quantity: 10,
}));
import {
useGachaItemList,
useCreateGachaItem,
useUpdateGachaItem,
useDeleteGachaItem,
TGachaItemDto,
} from '@imphnen-frontend-service/service';
export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false);
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null);
const [search, setSearch] = useState('');
const pendingFormData = useRef<any>(null);
const {
step: currentStep,
@@ -60,7 +56,60 @@ export const Components: FC = (): ReactElement => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const columns: ColumnDef<GachaItem>[] = [
const { data: itemsData, isLoading } = useGachaItemList({
search,
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const createItem = useCreateGachaItem();
const updateItem = useUpdateGachaItem();
const deleteItem = useDeleteGachaItem();
const items: TGachaItemDto[] = itemsData?.data ?? [];
const totalItems = itemsData?.meta?.total ?? items.length;
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
const { itemName, quantity, chanceRate } = pendingFormData.current;
await createItem.mutateAsync({
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
name: itemName,
description: '',
rarity: 'common',
type_: 'physical',
category: 'merchandise',
value: 0,
weight: chanceRate ?? 1,
stock: quantity ?? 1,
is_limited: false,
});
}
return true;
};
const handleUpdate = async (): Promise<boolean> => {
if (selectedItem && pendingFormData.current) {
const { itemName, quantity, chanceRate } = pendingFormData.current;
await updateItem.mutateAsync({
id: selectedItem.id,
data: {
name: itemName,
weight: chanceRate,
stock: quantity,
},
});
}
return true;
};
const handleDelete = async () => {
if (selectedItem) {
await deleteItem.mutateAsync(selectedItem.id);
}
setShowModalDeleteItem(false);
};
const columns: ColumnDef<TGachaItemDto>[] = [
{
id: 'select',
header: ({ table }) => (
@@ -88,23 +137,16 @@ export const Components: FC = (): ReactElement => {
header: 'Nama Item',
accessorKey: 'name',
},
{
header: 'Chance Rate',
accessorKey: 'chanceRate',
},
{
header: 'Quantity',
accessorKey: 'quantity',
},
{
header: 'Action',
cell: () => (
cell: ({ row }) => (
<div className="flex gap-[8px]">
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedItem(row.original);
setShowModalUpdateItem(true);
}}
className="flex items-center gap-2"
@@ -116,6 +158,7 @@ export const Components: FC = (): ReactElement => {
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedItem(row.original);
setShowModalDeleteItem(true);
}}
className="flex items-center gap-2"
@@ -128,7 +171,7 @@ export const Components: FC = (): ReactElement => {
];
const table = useReactTable({
data: mockData,
data: items,
columns,
state: {
pagination,
@@ -139,8 +182,8 @@ export const Components: FC = (): ReactElement => {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -156,6 +199,8 @@ export const Components: FC = (): ReactElement => {
<Input
placeholder="Cari berdasarkan nama item"
className="pl-12 w-full max-h-full"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
@@ -166,9 +211,7 @@ export const Components: FC = (): ReactElement => {
variant="primary"
size="md"
className="flex gap-3 text-nowrap"
onClick={() => {
setShowModalAddItem(true);
}}
onClick={() => setShowModalAddItem(true)}
>
<PlusOutlined />
Tambah Item
@@ -176,7 +219,11 @@ export const Components: FC = (): ReactElement => {
</div>
</div>
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable data={items} columns={columns} table={table} />
)}
</section>
</main>
@@ -187,6 +234,8 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAddItem={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalUpdateItem
currentStep={currentStep}
@@ -195,13 +244,14 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleUpdateItem={handleUpdate}
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalDeleteItem
isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)}
handleDeleteItem={() => {
console.log('Item deleted');
}}
handleDeleteItem={handleDelete}
/>
</Fragment>
);
@@ -11,6 +11,7 @@ interface IModalAddPermission {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddPermission = ({
@@ -20,6 +21,7 @@ const ModalAddPermission = ({
nextStep,
resetStep,
handleAddItem,
onDataCapture,
}: IModalAddPermission) => {
return (
<Modal
@@ -31,7 +33,7 @@ const ModalAddPermission = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,10 +48,11 @@ const ModalAddPermission = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep);
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
@@ -1,6 +1,9 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
import { useConfirmItem } from '../_hook/use-item';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
interface IModalUpdatePermission {
isOpen: boolean;
@@ -10,6 +13,8 @@ interface IModalUpdatePermission {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { name?: string };
onDataCapture?: (data: any) => void;
}
const ModalUpdatePermission = ({
@@ -17,10 +22,31 @@ const ModalUpdatePermission = ({
onClose,
resetStep,
handleUpdate,
initialValues,
onDataCapture,
}: IModalUpdatePermission) => {
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
success: 'Perubahan permissions berhasil dilakukan',
error: 'Perubahan permissions gagal dilakukan',
const form = useForm<{ name: string }>({
mode: 'all',
defaultValues: initialValues,
});
useEffect(() => {
if (isOpen) {
form.reset(initialValues);
}
}, [isOpen, initialValues]);
const onSubmit = form.handleSubmit(async (data) => {
try {
onDataCapture?.(data);
if (handleUpdate) await handleUpdate();
toast.success('Perubahan permissions berhasil dilakukan');
onClose();
resetStep();
} catch (error) {
console.log(error);
toast.error('Perubahan permissions gagal dilakukan');
}
});
return (
@@ -36,24 +62,26 @@ const ModalUpdatePermission = ({
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<InputField
label="Name"
name="name"
type="text"
placeholder="Nama Permission"
size="lg"
className="w-full"
/>
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<ControlledInputField
control={form.control}
label="Name"
name="name"
type="text"
placeholder="Nama Permission"
size="lg"
className="w-full"
/>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
onClick={onConfirm}
>
Update Permission
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Update Permission
</Button>
</form>
</Modal.Content>
</Modal>
);
@@ -3,7 +3,8 @@ import { toast } from 'sonner';
export const useItem = (
nextStep: () => void,
initialValues?: any
initialValues?: any,
onDataCapture?: (data: any) => void,
) => {
const form = useForm<any>({
mode: 'all',
@@ -11,7 +12,7 @@ export const useItem = (
});
const onSubmit = form.handleSubmit((data) => {
console.log('Form data:', data);
onDataCapture?.(data);
nextStep();
});
@@ -32,6 +33,9 @@ export const useConfirmItem = (
) => {
const onConfirm = async () => {
try {
if (actionFunction) {
await actionFunction();
}
toast.success(messages?.success);
onClose();
resetStep();
@@ -1,4 +1,4 @@
import { FC, Fragment, ReactElement, useState } from 'react';
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
import {
SearchOutlined,
EditOutlined,
@@ -19,24 +19,22 @@ import ModalAddPermission from './_components/modal-add-permission';
import ModalUpdatePermission from './_components/modal-update-permission';
import ModalDeletePermission from './_components/modal-delete-permission';
import { useQueryState } from '@imphnen-frontend-service/utils';
import {
usePermissionList,
useCreatePermission,
useUpdatePermission,
useDeletePermission,
TPermissionItem,
} from '@imphnen-frontend-service/service';
import React from 'react';
interface Permission {
id: number;
name: string;
}
const mockData: Permission[] = [
{ id: 1, name: 'Read' },
{ id: 2, name: 'Create' },
{ id: 3, name: 'Update' },
{ id: 4, name: 'Delete' },
];
export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false);
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedItem, setSelectedItem] = useState<TPermissionItem | null>(null);
const [search, setSearch] = useState('');
const pendingFormData = useRef<any>(null);
const {
step: currentStep,
@@ -56,7 +54,40 @@ export const Components: FC = (): ReactElement => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const columns: ColumnDef<Permission>[] = [
const { data: permissionsData, isLoading } = usePermissionList({
search,
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const createPermission = useCreatePermission();
const updatePermission = useUpdatePermission();
const deletePermission = useDeletePermission();
const permissions: TPermissionItem[] = permissionsData?.data ?? [];
const totalItems = permissionsData?.meta?.total ?? permissions.length;
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
await createPermission.mutateAsync(pendingFormData.current);
}
return true;
};
const handleUpdate = async (): Promise<boolean> => {
if (selectedItem && pendingFormData.current) {
await updatePermission.mutateAsync({ id: selectedItem.id, data: pendingFormData.current });
}
return true;
};
const handleDelete = async (): Promise<boolean> => {
if (selectedItem) {
await deletePermission.mutateAsync(selectedItem.id);
}
return true;
};
const columns: ColumnDef<TPermissionItem>[] = [
{
id: 'select',
header: ({ table }) => (
@@ -86,13 +117,14 @@ export const Components: FC = (): ReactElement => {
},
{
header: 'Action',
cell: () => (
cell: ({ row }) => (
<div className="flex gap-[8px]">
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedItem(row.original);
setShowModalUpdateItem(true);
}}
className="flex items-center gap-2"
@@ -104,6 +136,7 @@ export const Components: FC = (): ReactElement => {
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedItem(row.original);
setShowModalDeleteItem(true);
}}
className="flex items-center gap-2"
@@ -116,7 +149,7 @@ export const Components: FC = (): ReactElement => {
];
const table = useReactTable({
data: mockData,
data: permissions,
columns,
state: {
pagination,
@@ -127,8 +160,8 @@ export const Components: FC = (): ReactElement => {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -144,6 +177,8 @@ export const Components: FC = (): ReactElement => {
<Input
placeholder="Cari berdasarkan nama permissions"
className="pl-12 w-full max-h-full"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
@@ -154,22 +189,24 @@ export const Components: FC = (): ReactElement => {
variant="primary"
size="md"
className="flex gap-3 text-nowrap"
onClick={() => {
setShowModalAddItem(true);
}}
onClick={() => setShowModalAddItem(true)}
>
<PlusOutlined />
Tambah Permissionss
Tambah Permissions
</Button>
</div>
</div>
<DataTable
data={mockData}
columns={columns}
pageSize={9}
table={table}
/>
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable
data={permissions}
columns={columns}
pageSize={9}
table={table}
/>
)}
</section>
</main>
@@ -180,6 +217,8 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAddItem={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalUpdatePermission
isOpen={showModalUpdateItem}
@@ -187,6 +226,9 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleUpdate={handleUpdate}
initialValues={selectedItem ? { name: selectedItem.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalDeletePermission
isOpen={showModalDeleteItem}
@@ -194,6 +236,7 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleDelete={handleDelete}
/>
</Fragment>
);
@@ -11,6 +11,7 @@ interface IModalAddRole {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddRole = ({
@@ -20,6 +21,7 @@ const ModalAddRole = ({
nextStep,
resetStep,
handleAdd,
onDataCapture,
}: IModalAddRole) => {
return (
<Modal
@@ -31,7 +33,7 @@ const ModalAddRole = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,10 +48,11 @@ const ModalAddRole = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep);
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
@@ -1,8 +1,11 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
import { useConfirmItem } from '../_hook/use-item';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
interface IModalUpdatePermission {
interface IModalUpdateRole {
isOpen: boolean;
onClose: () => void;
handleUpdate?: () => Promise<boolean>;
@@ -10,17 +13,40 @@ interface IModalUpdatePermission {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { name?: string };
onDataCapture?: (data: any) => void;
}
const ModalUpdatePermission = ({
const ModalUpdateRole = ({
isOpen,
onClose,
resetStep,
handleUpdate,
}: IModalUpdatePermission) => {
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
success: 'Perubahan roles berhasil dilakukan',
error: 'Perubahan roles gagal dilakukan',
initialValues,
onDataCapture,
}: IModalUpdateRole) => {
const form = useForm<{ name: string }>({
mode: 'all',
defaultValues: initialValues,
});
useEffect(() => {
if (isOpen) {
form.reset(initialValues);
}
}, [isOpen, initialValues]);
const onSubmit = form.handleSubmit(async (data) => {
try {
onDataCapture?.(data);
if (handleUpdate) await handleUpdate();
toast.success('Perubahan roles berhasil dilakukan');
onClose();
resetStep();
} catch (error) {
console.log(error);
toast.error('Perubahan roles gagal dilakukan');
}
});
return (
@@ -36,76 +62,78 @@ const ModalUpdatePermission = ({
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<InputField
label="Nama Role"
name="name"
type="text"
placeholder="Masukkan Nama Role"
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ControlledInputField
control={form.control}
label="Nama Role"
name="name"
type="text"
placeholder="Masukkan Nama Role"
size="lg"
className="w-full"
/>
</div>
<div className="flex flex-col gap-4 items-start overflow-auto">
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
Permissions
</span>
<div className="flex gap-x-6 overflow-x-scroll">
{['Gacha Items', 'Gacha Roll', 'Roll', 'Users', 'Gacha Claim'].map(
(title) => (
<div
key={title}
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
>
<span className="text-nowrap text-label1">{title}</span>
<div className="flex gap-[8px] items-center">
<input
type="checkbox"
id={`${title}-all`}
className="rounded"
/>
<label htmlFor={`${title}-all`} className="text-nowrap">
Check All
</label>
</div>
<hr className="border-blue-200" />
<div className="flex flex-col items-start gap-4 mb-4">
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-read`} />
<label htmlFor={`${title}-read`}>Read</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-create`} />
<label htmlFor={`${title}-create`}>Create</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-update`} />
<label htmlFor={`${title}-update`}>Update</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-delete`} />
<label htmlFor={`${title}-delete`}>Delete</label>
</div>
</div>
</div>
)
)}
</div>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
/>
</div>
<div className="flex flex-col gap-4 items-start overflow-auto">
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
Permissions
</span>
<div className="flex gap-x-6 overflow-x-scroll">
{['Gacha Items', 'Gacha Roll', 'Roll', 'Users', 'Gacha Claim'].map(
(title) => (
<div
key={title}
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
>
<span className="text-nowrap text-label1">{title}</span>
<div className="flex gap-[8px] items-center">
<input
type="checkbox"
id={`${title}-all`}
className="rounded"
/>
<label htmlFor={`${title}-all`} className="text-nowrap">
Check All
</label>
</div>
<hr className="border-blue-200" />
<div className="flex flex-col items-start gap-4 mb-4">
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-read`} />
<label htmlFor={`${title}-read`}>Read</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-create`} />
<label htmlFor={`${title}-create`}>Create</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-update`} />
<label htmlFor={`${title}-update`}>Update</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-delete`} />
<label htmlFor={`${title}-delete`}>Delete</label>
</div>
</div>
</div>
)
)}
</div>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
onClick={onConfirm}
>
Update Role
</Button>
type="submit"
>
Update Role
</Button>
</form>
</Modal.Content>
</Modal>
);
};
export default ModalUpdatePermission;
export default ModalUpdateRole;
@@ -3,7 +3,8 @@ import { toast } from 'sonner';
export const useItem = (
nextStep: () => void,
initialValues?: any
initialValues?: any,
onDataCapture?: (data: any) => void,
) => {
const form = useForm<any>({
mode: 'all',
@@ -11,7 +12,7 @@ export const useItem = (
});
const onSubmit = form.handleSubmit((data) => {
console.log('Form data:', data);
onDataCapture?.(data);
nextStep();
});
@@ -32,6 +33,9 @@ export const useConfirmItem = (
) => {
const onConfirm = async () => {
try {
if (actionFunction) {
await actionFunction();
}
toast.success(messages?.success);
onClose();
resetStep();
@@ -1,4 +1,4 @@
import { FC, Fragment, ReactElement, useState } from 'react';
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
import {
SearchOutlined,
EditOutlined,
@@ -19,25 +19,22 @@ import ModalAddRole from './_components/modal-add-role';
import ModalUpdateRole from './_components/modal-update-role';
import ModalDeleteRole from './_components/modal-delete-role';
import { useQueryState } from '@imphnen-frontend-service/utils';
import {
useRoleList,
useCreateRole,
useUpdateRole,
useDeleteRole,
TRolesListItem,
} from '@imphnen-frontend-service/service';
import React from 'react';
interface Role {
id: number;
name: string;
}
const mockData: Role[] = [
{ id: 1, name: 'Admin' },
{ id: 2, name: 'Admin Pembayaran' },
{ id: 3, name: 'Staff' },
{ id: 4, name: 'Staff Aktivasi User' },
{ id: 5, name: 'User' },
];
export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false);
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedRole, setSelectedRole] = useState<TRolesListItem | null>(null);
const [search, setSearch] = useState('');
const pendingFormData = useRef<any>(null);
const {
step: currentStep,
@@ -57,7 +54,40 @@ export const Components: FC = (): ReactElement => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const columns: ColumnDef<Role>[] = [
const { data: rolesData, isLoading } = useRoleList({
search,
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const createRole = useCreateRole();
const updateRole = useUpdateRole();
const deleteRole = useDeleteRole();
const roles: TRolesListItem[] = rolesData?.data ?? [];
const totalItems = rolesData?.meta?.total ?? roles.length;
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
await createRole.mutateAsync(pendingFormData.current);
}
return true;
};
const handleUpdate = async (): Promise<boolean> => {
if (selectedRole && pendingFormData.current) {
await updateRole.mutateAsync({ id: selectedRole.id, data: pendingFormData.current });
}
return true;
};
const handleDelete = async (): Promise<boolean> => {
if (selectedRole) {
await deleteRole.mutateAsync(selectedRole.id);
}
return true;
};
const columns: ColumnDef<TRolesListItem>[] = [
{
id: 'select',
header: ({ table }) => (
@@ -87,13 +117,14 @@ export const Components: FC = (): ReactElement => {
},
{
header: 'Action',
cell: () => (
cell: ({ row }) => (
<div className="flex gap-[8px]">
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedRole(row.original);
setShowModalUpdateItem(true);
}}
className="flex items-center gap-2"
@@ -105,6 +136,7 @@ export const Components: FC = (): ReactElement => {
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedRole(row.original);
setShowModalDeleteItem(true);
}}
className="flex items-center gap-2"
@@ -117,7 +149,7 @@ export const Components: FC = (): ReactElement => {
];
const table = useReactTable({
data: mockData,
data: roles,
columns,
state: {
pagination,
@@ -128,8 +160,8 @@ export const Components: FC = (): ReactElement => {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -145,6 +177,8 @@ export const Components: FC = (): ReactElement => {
<Input
placeholder="Cari berdasarkan nama roles"
className="pl-12 w-full max-h-full"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
@@ -155,9 +189,7 @@ export const Components: FC = (): ReactElement => {
variant="primary"
size="md"
className="flex gap-3 text-nowrap"
onClick={() => {
setShowModalAddItem(true);
}}
onClick={() => setShowModalAddItem(true)}
>
<PlusOutlined />
Tambah Role
@@ -165,12 +197,16 @@ export const Components: FC = (): ReactElement => {
</div>
</div>
<DataTable
data={mockData}
columns={columns}
pageSize={9}
table={table}
/>
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable
data={roles}
columns={columns}
pageSize={9}
table={table}
/>
)}
</section>
</main>
@@ -181,6 +217,8 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAdd={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalUpdateRole
isOpen={showModalUpdateItem}
@@ -188,6 +226,9 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleUpdate={handleUpdate}
initialValues={selectedRole ? { name: selectedRole.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalDeleteRole
isOpen={showModalDeleteItem}
@@ -195,6 +236,7 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleDelete={handleDelete}
/>
</Fragment>
);
@@ -5,27 +5,11 @@ import { cn } from "@imphnen-frontend-service/utils";
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
import { ReactElement, useState } from "react";
import { ModalDetailSession } from "./_components/modal/detail";
type SessionStatus = 'ongoing' | 'finished';
interface SessionType {
id: string
mentorName: string
menteeName: string
datetime: number
status: SessionStatus
}
const mockData: SessionType[] = Array.from({ length: 90 }, (_, i) => ({
id: `DS-${i + 1}`,
mentorName: 'Ahmad Wijuana',
menteeName: 'Sofia Wijuana',
datetime: new Date().getTime(),
status: i % 2 === 0 ? 'ongoing' : 'finished',
}))
import { useMySessions, TSessionListItem } from "@imphnen-frontend-service/service";
export default function Components(): ReactElement {
const [openDetail, setOpenDetail] = useState(false);
const [statusFilter, setStatusFilter] = useState('');
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({
@@ -33,7 +17,14 @@ export default function Components(): ReactElement {
pageSize: 9,
});
const columns: ColumnDef<SessionType>[] = [
const { data: sessionsData, isLoading } = useMySessions(
statusFilter ? { status: statusFilter } : undefined
);
const sessions: TSessionListItem[] = sessionsData?.sessions ?? [];
const totalItems = sessionsData?.total ?? sessions.length;
const columns: ColumnDef<TSessionListItem>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-20") },
@@ -60,19 +51,22 @@ export default function Components(): ReactElement {
accessorKey: 'id',
},
{
id: 'mentorName',
id: 'mentorId',
header: 'Nama Mentor',
accessorKey: 'name',
accessorKey: 'mentor_id',
},
{
id: 'menteeName',
header: 'Nama Mentee',
accessorKey: 'name',
accessorKey: 'mentee_fullname',
},
{
id: 'datetime',
header: 'Waktu',
accessorKey: 'datetime',
accessorKey: 'scheduled_at',
cell: ({ row }) => (
<span>{new Date(row.original.scheduled_at).toLocaleString('id-ID')}</span>
),
},
{
id: 'status',
@@ -80,19 +74,16 @@ export default function Components(): ReactElement {
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<SessionStatus, string> = {
const statusColors: Record<string, string> = {
pending: 'bg-warning-200 text-warning-700',
confirmed: 'bg-primary-200 text-primary-700',
ongoing: 'bg-warning-200 text-warning-700',
finished: 'bg-success-200 text-success-500',
};
const statusText: Record<SessionStatus, string> = {
ongoing: 'On Going',
finished: 'Finished',
completed: 'bg-success-200 text-success-500',
cancelled: 'bg-danger-200 text-danger-500',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
<div className={`py-2 px-4 rounded-md text-center capitalize ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}>
{status}
</div>
);
},
@@ -100,7 +91,7 @@ export default function Components(): ReactElement {
{
header: 'Action',
meta: { cellClassName: cn("w-52") },
cell: ({ row }) => (
cell: () => (
<Button
variant="primary"
size="sm"
@@ -117,7 +108,7 @@ export default function Components(): ReactElement {
]
const table = useReactTable({
data: mockData,
data: sessions,
columns,
state: {
pagination,
@@ -128,8 +119,8 @@ export default function Components(): ReactElement {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -147,19 +138,21 @@ export default function Components(): ReactElement {
<SearchOutlined />
</div>
</div>
<Select>
<option selected disabled>Rating</option>
<option value="4.5">4.5</option>
<option value="5">5</option>
</Select>
<Select>
<option selected disabled>Status</option>
<option value="finished">Finished</option>
<Select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
<option value="">Semua Status</option>
<option value="pending">Pending</option>
<option value="confirmed">Confirmed</option>
<option value="ongoing">On Going</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable data={sessions} columns={columns} table={table} />
)}
</section>
<ModalDetailSession open={openDetail} setOpen={setOpenDetail} />
@@ -4,18 +4,8 @@ import { DataTable } from "@imphnen-frontend-service/ui/organisms";
import { cn } from "@imphnen-frontend-service/utils";
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
import { FC, useState } from "react";
type UserRolesPermissionType = {
id: number
role: string
totalUser: number
}
const mockData: UserRolesPermissionType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
role: ['Admin', 'Super Admin', 'Mentee', 'Mentor'][Math.floor(Math.random() * 4)],
totalUser: 10
}))
import { useRoleList, useDeleteRole, TRolesListItem } from "@imphnen-frontend-service/service";
import { toast } from "sonner";
export const UserRolesPermission: FC = () => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
@@ -24,7 +14,25 @@ export const UserRolesPermission: FC = () => {
pageSize: 9,
});
const columns: ColumnDef<UserRolesPermissionType>[] = [
const { data: rolesData, isLoading } = useRoleList({
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const deleteRole = useDeleteRole();
const roles: TRolesListItem[] = rolesData?.data ?? [];
const totalItems = rolesData?.meta?.total ?? roles.length;
const handleDelete = async (id: string) => {
try {
await deleteRole.mutateAsync(id);
toast.success('Role berhasil dihapus');
} catch {
toast.error('Role gagal dihapus');
}
};
const columns: ColumnDef<TRolesListItem>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-20") },
@@ -48,12 +56,12 @@ export const UserRolesPermission: FC = () => {
{
id: 'role',
header: 'Role',
accessorKey: 'role',
accessorKey: 'name',
},
{
id: 'totalUser',
header: 'Total User',
accessorKey: 'totalUser',
header: 'Total Permissions',
accessorKey: 'permissions_count',
},
{
header: 'Action',
@@ -75,6 +83,7 @@ export const UserRolesPermission: FC = () => {
size="sm"
onClick={(e) => {
e.stopPropagation();
handleDelete(row.original.id);
}}
className="flex items-center gap-2 w-max"
>
@@ -86,7 +95,7 @@ export const UserRolesPermission: FC = () => {
]
const table = useReactTable({
data: mockData,
data: roles,
columns,
state: {
pagination,
@@ -97,8 +106,8 @@ export const UserRolesPermission: FC = () => {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -106,12 +115,16 @@ export const UserRolesPermission: FC = () => {
<div className="mb-8 flex items-center justify-between">
<h1 className="text-p2 font-semibold text-neutral-700">User Roles & Permissions</h1>
<Button type="button">
Add Rols
Add Role
</Button>
</div>
<div className="bg-white shadow p-8 rounded-lg">
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable data={roles} columns={columns} table={table} />
)}
</div>
</div>
)
@@ -15,30 +15,19 @@ import {
} from '@tanstack/react-table';
import { ReactElement, useState } from 'react';
import { ModalDetailUser } from './_components/modal/detail';
type UserStatus = 'active' | 'inactive';
interface UserType {
id: number;
name: string;
email: string;
rating: number;
status: UserStatus;
}
const mockData: UserType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
email: 'fullname23@gmail.com',
rating: 4.5,
status: i % 2 === 0 ? 'active' : 'inactive',
}));
import {
useMentorList,
useUserList,
MentorDetailResponseDto,
TUsersListItem,
} from '@imphnen-frontend-service/service';
export default function Components(): ReactElement {
const TABS = ['mentor', 'mentee'] as const;
const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor');
const [showDetail, setShowDetail] = useState(false);
const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [pagination, setPagination] = useState<PaginationState>({
@@ -46,7 +35,27 @@ export default function Components(): ReactElement {
pageSize: 9,
});
const columns: ColumnDef<UserType>[] = [
const { data: mentorData, isLoading: mentorLoading } = useMentorList({
search,
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const { data: menteeData, isLoading: menteeLoading } = useUserList({
search,
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const mentors: MentorDetailResponseDto[] = mentorData?.data ?? [];
const mentees: TUsersListItem[] = menteeData?.data ?? [];
const mentorTotal = mentorData?.meta?.total ?? mentors.length;
const menteeTotal = menteeData?.meta?.total ?? mentees.length;
const isLoading = activeTab === 'mentor' ? mentorLoading : menteeLoading;
const totalItems = activeTab === 'mentor' ? mentorTotal : menteeTotal;
const mentorColumns: ColumnDef<MentorDetailResponseDto>[] = [
{
id: 'select',
meta: { cellClassName: cn('w-20') },
@@ -70,7 +79,7 @@ export default function Components(): ReactElement {
{
id: 'name',
header: 'Name',
accessorKey: 'name',
accessorKey: 'fullname',
},
{
id: 'email',
@@ -81,6 +90,7 @@ export default function Components(): ReactElement {
id: 'rating',
header: 'Rating',
accessorKey: 'rating',
cell: ({ row }) => <span>{row.original.rating ?? '-'}</span>,
},
{
id: 'status',
@@ -88,19 +98,14 @@ export default function Components(): ReactElement {
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<UserStatus, string> = {
const statusColors: Record<string, string> = {
active: 'bg-success-200 text-success-500',
pending: 'bg-warning-200 text-warning-700',
inactive: 'bg-danger-200 text-danger-500',
};
const statusText: Record<UserStatus, string> = {
active: 'Active',
inactive: 'Inactive',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
<div className={`py-2 px-4 rounded-md text-center capitalize ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}>
{status}
</div>
);
},
@@ -125,20 +130,91 @@ export default function Components(): ReactElement {
},
];
const table = useReactTable({
data: mockData,
columns,
state: {
pagination,
rowSelection,
const menteeColumns: ColumnDef<TUsersListItem>[] = [
{
id: 'select',
meta: { cellClassName: cn('w-20') },
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
id: 'name',
header: 'Name',
accessorKey: 'fullname',
},
{
id: 'email',
header: 'Email',
accessorKey: 'email',
},
{
id: 'status',
header: 'Status',
accessorKey: 'is_active',
cell: ({ row }) => (
<div className={`py-2 px-4 rounded-md text-center ${row.original.is_active ? 'bg-success-200 text-success-500' : 'bg-danger-200 text-danger-500'}`}>
{row.original.is_active ? 'Active' : 'Inactive'}
</div>
),
},
{
header: 'Action',
meta: { cellClassName: cn('w-72') },
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedUserId(row.original.id);
setShowDetail(true);
}}
className="flex items-center gap-2 w-max"
>
<SearchOutlined className="text-[16px]" /> Lihat Detail & Action
</Button>
),
},
];
const mentorTable = useReactTable({
data: mentors,
columns: mentorColumns,
state: { pagination, rowSelection },
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(mentorTotal / pagination.pageSize),
manualPagination: true,
});
const menteeTable = useReactTable({
data: mentees,
columns: menteeColumns,
state: { pagination, rowSelection },
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(menteeTotal / pagination.pageSize),
manualPagination: true,
});
return (
@@ -157,7 +233,10 @@ export default function Components(): ReactElement {
'px-3 py-2 capitalize',
activeTab === tab && 'bg-white'
)}
onClick={() => setActiveTab(tab)}
onClick={() => {
setActiveTab(tab);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
>
{tab}
</Button>
@@ -172,28 +251,22 @@ export default function Components(): ReactElement {
<Input
placeholder="Cari berdasarkan nama lengkap"
className="pl-12 w-full max-h-full"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
</div>
</div>
<Select>
<option selected disabled>
Rating
</option>
<option value="4.5">4.5</option>
<option value="5">5</option>
</Select>
<Select>
<option selected disabled>
Status
</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : activeTab === 'mentor' ? (
<DataTable data={mentors} columns={mentorColumns} table={mentorTable} />
) : (
<DataTable data={mentees} columns={menteeColumns} table={menteeTable} />
)}
</section>
<ModalDetailUser
@@ -10,6 +10,9 @@ import { QrisPaymentStep } from "./steps/qris-payement"
import { VAPaymentStep } from "./steps/va-payment"
import { SuccessStep } from "./steps/success"
import { PaymentStep } from "./steps/payment"
import { useBookSession } from "@imphnen-frontend-service/service"
import { TOPICS } from "../../sections/topics"
import { toast } from "sonner"
const STEPS = ['topic', 'schedule', 'profile', 'payment', 'qr-payment', 'va-payment', 'success'] as const
type Step = typeof STEPS[number]
@@ -17,15 +20,48 @@ type Step = typeof STEPS[number]
type Props = {
open: boolean
setOpen: (open: boolean) => void
mentorId?: string
}
export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
export const AppointmentModal: FC<Props> = ({ open, setOpen, mentorId }) => {
const [step, setStep] = useState<Step>('topic')
const [selectedTopics, setSelectedTopics] = useState<number[]>([])
const [scheduledDate, setScheduledDate] = useState('')
const [scheduledTime, setScheduledTime] = useState('')
const [description, setDescription] = useState('')
const [sessionType, setSessionType] = useState('online')
const [isBooking, setIsBooking] = useState(false)
const handleStep = (action: 'next' | 'prev') => {
const bookSession = useBookSession(mentorId ?? '')
const handleStep = async (action: 'next' | 'prev') => {
if (action === 'next' && step === 'success') {
setOpen(false)
} else if (action === 'next' && step === 'payment' && mentorId) {
const topicNames = selectedTopics
.map((id) => TOPICS.find((t) => t.id === id)?.name)
.filter(Boolean)
.join(', ')
const scheduledAt =
scheduledDate && scheduledTime
? new Date(`${scheduledDate}T${scheduledTime}`).toISOString()
: new Date().toISOString()
setIsBooking(true)
try {
await bookSession.mutateAsync({
topic: topicNames || 'General Mentoring',
description: description || undefined,
scheduled_at: scheduledAt,
session_type: sessionType,
})
setStep('qr-payment')
} catch {
toast.error('Gagal membuat sesi. Silakan coba lagi.')
} finally {
setIsBooking(false)
}
} else if (action === 'next') {
setStep(STEPS[STEPS.indexOf(step) + 1])
} else if (action === 'prev' && step !== 'topic') {
@@ -46,6 +82,12 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
window.addEventListener("keydown", handleEscapeKey)
} else {
document.body.style.overflow = ""
setStep('topic')
setSelectedTopics([])
setScheduledDate('')
setScheduledTime('')
setDescription('')
setSessionType('online')
}
return () => {
@@ -77,6 +119,7 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
size="sm"
variant="text"
className="absolute top-3 right-3 bg-primary-200 p-1 shadow md:bg-white md:p-2 md:top-8 md:right-10"
onClick={() => setOpen(false)}
>
<CloseOutlined className="md:text-lg" />
</Button>
@@ -113,7 +156,18 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
<AnimatePresence>
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} />}
{step === 'schedule' && <ScheduleStep />}
{step === 'schedule' && (
<ScheduleStep
scheduledDate={scheduledDate}
scheduledTime={scheduledTime}
description={description}
sessionType={sessionType}
onDateChange={setScheduledDate}
onTimeChange={setScheduledTime}
onDescriptionChange={setDescription}
onSessionTypeChange={setSessionType}
/>
)}
{step === 'profile' && <ProfileStep />}
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} />}
{step === 'qr-payment' && <QrisPaymentStep />}
@@ -141,14 +195,14 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
size="sm"
variant="primary"
className={cn((step === 'topic' || step === 'success') && 'w-full')}
disabled={selectedTopics.length === 0 && step === 'topic'}
disabled={(selectedTopics.length === 0 && step === 'topic') || isBooking}
onClick={() => handleStep('next')}
>
<Show
condition={step !== 'success'}
fallback="Halman Booking"
>
<Show condition={step !== 'payment'} fallback="Bayar Sekarang">
<Show condition={step !== 'payment'} fallback={isBooking ? 'Memproses...' : 'Bayar Sekarang'}>
Selanjutnya
</Show>
</Show>
@@ -13,7 +13,27 @@ Saya ingin tahu secara khusus tentang ___.
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
export const ScheduleStep = () => {
type Props = {
scheduledDate: string
scheduledTime: string
description: string
sessionType: string
onDateChange: (v: string) => void
onTimeChange: (v: string) => void
onDescriptionChange: (v: string) => void
onSessionTypeChange: (v: string) => void
}
export const ScheduleStep = ({
scheduledDate,
scheduledTime,
description,
sessionType,
onDateChange,
onTimeChange,
onDescriptionChange,
onSessionTypeChange,
}: Props) => {
return (
<motion.div
className="bg-white px-6 py-5 rounded-md md:px-6"
@@ -31,15 +51,29 @@ export const ScheduleStep = () => {
<div className="grid gap-2.5 md:grid-cols-2 md:gap-5">
<div>
<label className={labelClass}>Tanggal</label>
<Input type="date" className="min-w-full w-full" />
<Input
type="date"
className="min-w-full w-full"
value={scheduledDate}
onChange={(e) => onDateChange(e.target.value)}
/>
</div>
<div>
<label className={labelClass}>Waktu</label>
<Input type="time" className="min-w-full w-full" />
<Input
type="time"
className="min-w-full w-full"
value={scheduledTime}
onChange={(e) => onTimeChange(e.target.value)}
/>
</div>
<div className="relative md:col-span-full">
<label className={labelClass}>Lokasi</label>
<Select className="min-w-full w-full">
<Select
className="min-w-full w-full"
value={sessionType}
onChange={(e) => onSessionTypeChange(e.target.value)}
>
<option value="online">Online</option>
<option value="offline">Offline</option>
</Select>
@@ -48,7 +82,10 @@ export const ScheduleStep = () => {
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
<Textarea
className="min-w-full w-full h-40"
placeholder={placeholder} />
placeholder={placeholder}
value={description}
onChange={(e) => onDescriptionChange(e.target.value)}
/>
</div>
</div>
</motion.div>
@@ -1,27 +1,30 @@
import { cn, For } from "@imphnen-frontend-service/utils"
import { FC } from "react"
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
const EDUCATION = [
{ name: 'Universitas Widyabakti', major: 'Intern Front End', duration: '2 Months', range: 'Oct 2024 - Present' },
{ name: 'SMKN 99 Banjaran', major: 'Rekayasa Perangkat Lunak', duration: '6 Months', range: 'May 2024 - Oct 2024' },
]
type Props = {
mentor?: MentorDetailResponseDto
}
export const EducationSection: FC<Props> = ({ mentor }) => {
const education = mentor?.education ?? []
if (education.length === 0) return null
export const EducationSection = () => {
return (
<div className="px-7 py-8 rounded-md shadow-md">
<h2 className="text-xs font-semibold mb-5 md:text-[15px] xl:text-[19px]">Education</h2>
<div className="space-y-4 divide-y">
<For data={EDUCATION}>
<For data={education}>
{(item, index) => (
<div key={index} className={cn("flex items-center gap-x-4", index !== EDUCATION.length - 1 && "pb-4")}>
<div key={item.id ?? index} className={cn("flex items-center gap-x-4", index !== education.length - 1 && "pb-4")}>
<div className="rounded-full size-6 bg-neutral-200 md:size-7 xl:size-8"></div>
<div className='flex-1 text-[10px] font-medium'>
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.name}</p>
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.institution}</p>
<p>
<span className="text-neutral-600 xl:text-xs">{item.major}</span>
<span className="text-neutral-600 xl:text-xs">{item.degree} - {item.field}</span>
<span className="text-neutral-300"> · </span>
<span className="text-neutral-400 font-normal">{item.duration}</span>
<span className="text-neutral-300"> · </span>
<span className="text-neutral-400 font-normal xl:font-medium">{item.range}</span>
<span className="text-neutral-400 font-normal xl:font-medium">{item.period}</span>
</p>
</div>
</div>
@@ -1,28 +1,32 @@
import { cn, For } from "@imphnen-frontend-service/utils"
import { FC } from "react"
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
const EXPERIENCE = [
{ name: 'Sunday.com', position: 'Intern Front End', duration: '2 Months', range: 'Oct 2024 - Present' },
{ name: 'CodeX Digital', position: 'Intern Front End', duration: '6 Months', range: 'May 2024 - Oct 2024' },
]
type Props = {
mentor?: MentorDetailResponseDto
}
export const ExperienceSection: FC<Props> = ({ mentor }) => {
const experience = mentor?.experience ?? []
if (experience.length === 0) return null
export const ExperienceSection: FC = () => {
return (
<div className="px-7 py-8 rounded-md shadow-md">
<h2 className="text-xs font-semibold mb-5 md:text-[15px] xl:text-[19px]">Experience</h2>
<div className="space-y-4 divide-y">
<For data={EXPERIENCE}>
<For data={experience}>
{(item, index) => (
<div key={index} className={cn("flex items-center gap-x-4", index !== EXPERIENCE.length - 1 && "pb-4")}>
<div key={item.id ?? index} className={cn("flex items-center gap-x-4", index !== experience.length - 1 && "pb-4")}>
<div className="rounded-full size-6 bg-neutral-200 md:size-7 xl:size-8"></div>
<div className='flex-1 text-[10px] font-medium'>
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.name}</p>
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.company}</p>
<p>
<span className="text-neutral-600 xl:text-xs">{item.position}</span>
<span className="text-neutral-300"> · </span>
<span className="text-neutral-400 font-normal">{item.duration}</span>
<span className="text-neutral-300"> · </span>
<span className="text-neutral-400 font-normal xl:font-medium">{item.range}</span>
<span className="text-neutral-400 font-normal xl:font-medium">{item.period}</span>
</p>
</div>
</div>
@@ -1,12 +1,19 @@
import { StarFilled } from "@ant-design/icons"
import { Button } from "@imphnen-frontend-service/ui/atoms"
import { cn, For } from "@imphnen-frontend-service/utils"
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
type Props = {
onBook: () => void
mentor?: MentorDetailResponseDto
}
export const ProfileSection: React.FC<Props> = ({ onBook }) => {
export const ProfileSection: React.FC<Props> = ({ onBook, mentor }) => {
const expertise = mentor?.expertise ?? []
const softSkills = mentor?.topics_of_interest ?? []
const rating = mentor?.rating ?? 0
const ratingLabel = rating >= 4.5 ? 'Excelent Sensei' : rating >= 3.5 ? 'Good Sensei' : 'Rising Sensei'
return (
<div className="bg-white px-4 py-5 space-y-6 md:px-8 md:pt-6 md:pb-0 xl:space-y-0 xl:py-[30px] xl:flex xl:gap-x-9 xl:justify-between">
<div
@@ -33,56 +40,61 @@ export const ProfileSection: React.FC<Props> = ({ onBook }) => {
"md:text-[19px] md:mb-2 md:text-start xl:text-[23px]",
)}
>
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
{mentor?.fullname || 'Loading...'}
</h1>
<p className="text-xs mb-4 text-neutral-600 md:mb-5 md:text-[15px] xl:text-[19px] xl:mb-5">
UI Designer at Oray orayan Studios
{mentor ? `${mentor.current_role} at ${mentor.current_company}` : ''}
</p>
<div className="flex items-center gap-x-2.5 w-full max-w-max border border-primary-50 p-1.5 rounded-md mx-auto md:ms-0">
<div
className="bg-gradient-to-tr from-primary-500 to-primary-200 rounded-sm text-white size-5 flex justify-center items-center xl:size-[29.4px]"
>
<StarFilled className="text-xs xl:text-sm" />
{rating > 0 && (
<div className="flex items-center gap-x-2.5 w-full max-w-max border border-primary-50 p-1.5 rounded-md mx-auto md:ms-0">
<div
className="bg-gradient-to-tr from-primary-500 to-primary-200 rounded-sm text-white size-5 flex justify-center items-center xl:size-[29.4px]"
>
<StarFilled className="text-xs xl:text-sm" />
</div>
<div>
<p className="text-[10px] font-medium mb-1 text-neutral-800 xl:text-xs">{ratingLabel}</p>
<p className="text-[8px] text-neutral-600 xl:text-[10px]">{rating.toFixed(1)}/5.0</p>
</div>
</div>
<div>
<p className="text-[10px] font-medium mb-1 text-neutral-800 xl:text-xs">Excelent Sensei</p>
<p className="text-[8px] text-neutral-600 xl:text-[10px]">4.8/5.0</p>
</div>
</div>
)}
</div>
</div>
<div className="flex flex-col gap-y-3 xl:gap-y-4 xl:max-w-[402px]">
<div>
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Expertise</h2>
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
<For data={['UI Design', 'UX Reseacrh']}>
{(item) => (
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
{item}
</div>
)}
</For>
{expertise.length > 0 && (
<div>
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Expertise</h2>
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
<For data={expertise}>
{(item) => (
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
{item}
</div>
)}
</For>
</div>
</div>
</div>
<div>
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Soft Skills</h2>
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
<For data={['Design Thinking', 'Communication', 'Problem Solving', '19:00 WIB']}>
{(item) => (
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
{item}
</div>
)}
</For>
)}
{softSkills.length > 0 && (
<div>
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Topics of Interest</h2>
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
<For data={softSkills}>
{(item) => (
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
{item}
</div>
)}
</For>
</div>
</div>
</div>
)}
</div>
<div className="hidden md:flex xl:hidden justify-between">
<div>
<p className="text-primary-500 text-[15px] font-semibold">Jum, 4 April 2025</p>
<p className="text-neutral-500 text-xs font-medium">Jum, 4 April 2025</p>
<p className="text-primary-500 text-[15px] font-semibold">{mentor?.availability_commitment || ''}</p>
</div>
<Button type="button" size="sm" onClick={onBook}>
Book Your Senpai!
@@ -1,19 +1,24 @@
import { StarFilled } from "@ant-design/icons";
import { For } from "@imphnen-frontend-service/utils";
import { FC } from "react";
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service";
const SENPAI_STATISTIC = [
{ name: 'Total Sessions', count: 8 },
{ name: 'Mentee Impact', count: 1000 },
{ name: 'Response Time', count: '30 Minute' }
]
type Props = {
mentor?: MentorDetailResponseDto
}
export const StatisticsSection: FC<Props> = ({ mentor }) => {
const stats = [
{ name: 'Total Sessions', count: mentor?.mentoring_sessions ?? 0 },
{ name: 'Rating', count: mentor?.rating != null ? `${mentor.rating.toFixed(1)}/5.0` : 'N/A' },
{ name: 'Experience', count: mentor?.years_of_experience != null ? `${mentor.years_of_experience} Yrs` : 'N/A' },
]
export const StatisticsSection: FC = () => {
return (
<div className="md:mb-10">
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Statistics</h2>
<div className="grid gap-4 xl:flex">
<For data={SENPAI_STATISTIC}>
<For data={stats}>
{(item, index) => (
<div key={index} className="px-2.5 py-2 border border-primary-50 rounded-md shadow flex items-center gap-x-2.5">
<div
@@ -1,5 +1,6 @@
import { For } from "@imphnen-frontend-service/utils"
import { FC } from "react"
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
export const TOPICS = [
{ id: 1, icon: '💼', name: 'Career & Self Development' },
@@ -11,22 +12,41 @@ export const TOPICS = [
{ id: 7, icon: <span className="font-bold text-primary-500">AI</span>, name: 'AI Tips' }
]
export const TopicsSection: FC = () => {
type Props = {
mentor?: MentorDetailResponseDto
}
export const TopicsSection: FC<Props> = ({ mentor }) => {
const mentorTopics = mentor?.topics_of_interest ?? []
return (
<div>
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Topics</h2>
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5">
<For data={TOPICS}>
{(item, index) => (
<div
key={index}
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
>
<span>{item.icon} </span>
<span>{item.name}</span>
</div>
)}
</For>
{mentorTopics.length > 0 ? (
<For data={mentorTopics}>
{(topic, index) => (
<div
key={index}
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
>
{topic}
</div>
)}
</For>
) : (
<For data={TOPICS}>
{(item, index) => (
<div
key={index}
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
>
<span>{item.icon} </span>
<span>{item.name}</span>
</div>
)}
</For>
)}
</div>
</div>
)
File diff suppressed because one or more lines are too long
@@ -1,8 +1,20 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { FC } from 'react';
import { Link } from 'react-router-dom';
import type { MentorDetailResponseDto } from '@imphnen-frontend-service/service';
interface MentorCardProps {
mentor: MentorDetailResponseDto;
}
export const MentorCard: FC<MentorCardProps> = ({ mentor }) => {
const expertise = mentor.expertise ?? [];
const firstSkill = expertise[0];
const secondSkill = expertise[1];
const extraCount = expertise.length - 2;
const yearsExp = mentor.years_of_experience ?? 0;
const expLabel = `${yearsExp}+ Years Experience`;
export const MentorCard: FC = () => {
return (
<div className="p-2.5 rounded-md bg-white shadow flex gap-x-4 items-start md:p-4 md:flex-col md:rounded-lg md:gap-y-4">
<div className="size-[60px] rounded-md overflow-hidden md:w-full md:h-auto md:aspect-square">
@@ -10,32 +22,38 @@ export const MentorCard: FC = () => {
</div>
<div>
<Button variant="text" size="sm" className="bg-primary-100 h-auto px-1.5 py-1 text-[8px] font-normal mb-2 hover:bg-primary-100 md:text-[10px] md:font-medium md:mb-4">
3-5 Years Experience
{expLabel}
</Button>
<h2 className="mb-1">
<Link to="/mentoring/detail" className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
Fullname
<Link to={`/mentoring/${mentor.id}`} className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
{mentor.fullname || 'Unknown Mentor'}
</Link>
</h2>
<p className="text-[8px] text-neutral-500 mb-3 md:text-[10px] md:font-medium md:mb-4 lg:text-xs">
Full Stack Enjoyer at name company
{mentor.current_role} at {mentor.current_company}
</p>
<div>
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Soft Skills :</p>
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Expertise :</p>
<div className="space-x-2">
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]">
Communication
</Button>
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hidden hover:bg-primary-200 md:inline-block lg:text-[10px]">
Communication
</Button>
<Button variant="text" size="sm" className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]">
+2
</Button>
{firstSkill && (
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]">
{firstSkill}
</Button>
)}
{secondSkill && (
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hidden hover:bg-primary-200 md:inline-block lg:text-[10px]">
{secondSkill}
</Button>
)}
{extraCount > 0 && (
<Button variant="text" size="sm" className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]">
+{extraCount}
</Button>
)}
</div>
</div>
</div>
</div>
)
}
);
};
@@ -8,27 +8,35 @@ import { MentorCard } from './_components/mentor-card';
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
import { motion, useInView, Variants } from 'framer-motion';
const TEMP_DATA = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'John Doe' },
{ id: 3, name: 'John Doe' },
{ id: 4, name: 'John Doe' },
]
import { useMentorList } from '@imphnen-frontend-service/service';
export const Components: FC = (): ReactElement => {
const [search, setSearch] = useState('');
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 3,
pageSize: 8,
});
const { data: mentorData, isLoading } = useMentorList({
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
search: search || undefined,
});
const mentors = mentorData?.data ?? [];
const totalItems = mentorData?.meta?.total ?? 0;
const table = useReactTable({
data: TEMP_DATA,
data: mentors,
columns: [],
state: { pagination },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
onPaginationChange: (updater) => {
setPagination(updater);
},
pageCount: Math.ceil(totalItems / pagination.pageSize) || 1,
manualPagination: true,
});
const ref = useRef(null)
@@ -91,24 +99,33 @@ export const Components: FC = (): ReactElement => {
<Input
placeholder="Cari berdasarkan nama, posisi/peran"
className="relative min-w-full w-full"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
/>
<SearchOutlined className="absolute right-2.5 top-1/2 -translate-y-1/2 text-primary-500 size-2.5 cursor-text md:me-12 lg:me-0" />
</motion.div>
<motion.div
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
<For data={Array.from({ length: 8 })}>
{(_, index) => (
<motion.div key={index} variants={childVariants}>
<MentorCard />
</motion.div>
)}
</For>
</motion.div>
{isLoading ? (
<div className="text-center py-12 text-neutral-400">Loading mentors...</div>
) : (
<motion.div
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
<For data={mentors}>
{(mentor, index) => (
<motion.div key={mentor.id ?? index} variants={childVariants}>
<MentorCard mentor={mentor} />
</motion.div>
)}
</For>
</motion.div>
)}
<Pagination table={table} />
</motion.div>
+62 -19
View File
@@ -6,11 +6,22 @@ import ModalFormLogin from './_components/form/modal-form-login';
import ModalFormRegister from './_components/form/modal-form-register';
import { GachaItem } from './_components/item/gacha-item';
import { useModalLogin } from '@imphnen-frontend-service/utils';
import { useGachaItemList, useUserCredits, useExecuteGachaRoll } from '@imphnen-frontend-service/service';
import { toast } from 'sonner';
import type { TGachaRollItemDto } from '@imphnen-frontend-service/service';
export const Components: FC = (): ReactElement => {
const { showModalLogin, setShowModalLogin } = useModalLogin();
const [showModalForgotPassword, setShowModalForgotPassword] = useState(false);
const [showModalRegister, setShowModalRegister] = useState(false);
const [spinResult, setSpinResult] = useState<TGachaRollItemDto | null>(null);
const { data: creditsData } = useUserCredits();
const { data: itemsData } = useGachaItemList({ per_page: 10 });
const executeRoll = useExecuteGachaRoll();
const gachaItems = itemsData?.data ?? [];
const availableRolls = creditsData?.available_rolls ?? 0;
const scrollToRoulette = () => {
const rouletteSection = document.getElementById('roulette');
@@ -24,6 +35,21 @@ export const Components: FC = (): ReactElement => {
setShowModalForgotPassword(true);
};
const handleSpin = async () => {
if (availableRolls <= 0) {
toast.error('Kamu tidak punya gacha roll. Beli dulu ya!');
return;
}
try {
const result = await executeRoll.mutateAsync();
setSpinResult(result);
const wonItem = gachaItems.find((item) => item.id === result.item_id);
toast.success(`Selamat! Kamu mendapatkan: ${wonItem?.name ?? 'item'}`);
} catch {
toast.error('Gagal spin gacha. Coba lagi ya!');
}
};
return (
<Fragment>
<section
@@ -51,6 +77,14 @@ export const Components: FC = (): ReactElement => {
<p>Let's Go Checkout Our Merch &</p>
<p>Gacha Your Prize Here</p>
</div>
{creditsData && (
<div className="text-center text-p3 md:text-p2 bg-primary-100 rounded-md px-4 py-2 border border-primary-300">
<span className="font-semibold">Roll tersisa: </span>
<span className="text-primary-600 font-bold">{availableRolls}</span>
</div>
)}
<Button
size="sm"
variant="bordered"
@@ -152,31 +186,40 @@ export const Components: FC = (): ReactElement => {
<div className="col-span-4 md:col-span-8 lg:col-span-6 flex flex-col items-center gap-4 md:gap-8 overflow-x-hidden">
<div className="bg-white text-primary-500 font-medium text-p3 md:text-h3 shadow py-2 px-4 md:py-4 md:px-8 max-w-fit rounded-md md:rounded-lg">
Here Take Your Prize
{spinResult
? `Hadiahmu: ${gachaItems.find((i) => i.id === spinResult.item_id)?.name ?? 'Item'}`
: 'Here Take Your Prize'}
</div>
<section
id="gacha-play"
className="flex flex-nowrap overflow-auto w-full gap-x-8 snap-x snap-mandatory"
>
<GachaItem
src="/gacha/certificate.png"
label="Sertifikat + Laminating"
/>
<GachaItem
src="/gacha/lanyard-id-card.png"
label="Lanyard + ID Card"
/>
<GachaItem src="/gacha/pin.png" label="Pin" />
<GachaItem src="/gacha/sticker.png" label="Sticker Isi 3" />
<GachaItem src="/gacha/sticker.png" label="Sticker Isi 5" />
<GachaItem
src="/gacha/gelang-karet.png"
label="Gelang Karet"
className="h-[86px] md:h-[160px]"
/>
{gachaItems.length > 0 ? (
gachaItems.map((item) => (
<GachaItem
key={item.id}
src="/gacha/certificate.png"
label={item.name}
/>
))
) : (
<>
<GachaItem src="/gacha/certificate.png" label="Sertifikat + Laminating" />
<GachaItem src="/gacha/lanyard-id-card.png" label="Lanyard + ID Card" />
<GachaItem src="/gacha/pin.png" label="Pin" />
<GachaItem src="/gacha/sticker.png" label="Sticker Isi 3" />
<GachaItem src="/gacha/sticker.png" label="Sticker Isi 5" />
<GachaItem src="/gacha/gelang-karet.png" label="Gelang Karet" className="h-[86px] md:h-[160px]" />
</>
)}
</section>
<Button variant="secondary" size="md">
Spin Now
<Button
variant="secondary"
size="md"
onClick={handleSpin}
disabled={executeRoll.isPending}
>
{executeRoll.isPending ? 'Spinning...' : 'Spin Now'}
</Button>
</div>
</section>
+38 -19
View File
@@ -5,23 +5,28 @@ import type {
TAdminSubmissionsResponse,
} from '../../types/admin';
const ADMIN_BASE_URL = '/admin';
export const getAdminUsers = async (params?: {
page?: number;
per_page?: number;
search?: string;
is_admin?: boolean;
}) => {
const response = await api.get<TAdminUsersResponse>(
`${ADMIN_BASE_URL}/users`,
{
params: {
...params,
is_admin: params?.is_admin ?? false,
},
}
);
const response = await api.get<TAdminUsersResponse>('/v1/hackathon/admin/users', { params });
return response.data;
};
export const getAdminUserById = async (userId: string) => {
const response = await api.get(`/v1/hackathon/admin/users/${userId}`);
return response.data;
};
export const deleteAdminUser = async (userId: string) => {
const response = await api.delete(`/v1/hackathon/admin/users/${userId}`);
return response.data;
};
export const setAdminUser = async (userId: string, is_admin: boolean) => {
const response = await api.post(`/v1/hackathon/admin/users/${userId}/set-admin`, { is_admin });
return response.data;
};
@@ -30,10 +35,12 @@ export const getAdminTeams = async (params?: {
per_page?: number;
search?: string;
}) => {
const response = await api.get<TAdminTeamsResponse>(
`${ADMIN_BASE_URL}/teams`,
{ params }
);
const response = await api.get<TAdminTeamsResponse>('/v1/hackathon/admin/teams', { params });
return response.data;
};
export const deleteAdminTeam = async (teamId: string) => {
const response = await api.delete(`/v1/hackathon/admin/teams/${teamId}`);
return response.data;
};
@@ -43,9 +50,21 @@ export const getAdminSubmissions = async (params?: {
search?: string;
status?: string;
}) => {
const response = await api.get<TAdminSubmissionsResponse>(
`${ADMIN_BASE_URL}/submissions`,
{ params }
);
const response = await api.get<TAdminSubmissionsResponse>('/v1/hackathon/admin/submissions', { params });
return response.data;
};
export const getAdminWinners = async () => {
const response = await api.get('/v1/hackathon/admin/winners');
return response.data;
};
export const setWinner = async (data: { team_id: string; rank: number; prize?: string }) => {
const response = await api.post('/v1/hackathon/admin/winners', data);
return response.data;
};
export const removeWinner = async (teamId: string) => {
const response = await api.delete(`/v1/hackathon/admin/winners/${teamId}`);
return response.data;
};
+47 -62
View File
@@ -1,80 +1,65 @@
import { api, getBaseURL } from '../';
import {
TLoginRequest,
TLoginResponse,
TRegisterRequest,
TSendOTPRequest,
TVerifyEmailRequest,
TGoogleCallbackResponse,
} from '../../types/auth';
import { TResponseMessage } from '../../types/common';
import { api } from '../index';
import type { TLoginResponse, TRegisterRequest, TSendOTPRequest, TVerifyEmailRequest } from '../../types/auth';
import type { TResponseMessage } from '../../types/common';
export const postLogin = async (
payload: TLoginRequest
): Promise<TLoginResponse> => {
export type TLoginRequest = {
email: string;
password: string;
};
export type TForgotPasswordRequest = {
email: string;
};
export type TNewPasswordRequest = {
token: string;
password: string;
};
export type TRefreshTokenRequest = {
refresh_token: string;
};
export const postLogin = async (payload: TLoginRequest): Promise<TLoginResponse> => {
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/login', data: payload });
return data;
};
export const postLoginMentor = async (payload: TLoginRequest): Promise<TLoginResponse> => {
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/login-mentor', data: payload });
return data;
};
export const postRegister = async (payload: TRegisterRequest): Promise<TResponseMessage> => {
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/register', data: payload });
return data;
};
export const postVerifyEmail = async (payload: TVerifyEmailRequest): Promise<TResponseMessage> => {
const { data } = await api({
method: 'POST',
url: '/auth/login',
data: payload,
url: '/v1/iam/auth/verify-email',
data: { otp: parseInt(payload.otp), email: payload.email },
});
return data;
};
export const postRegister = async (
payload: TRegisterRequest
): Promise<TResponseMessage> => {
const { data } = await api({
method: 'POST',
url: '/auth/register',
data: payload,
});
export const postSendOtp = async (payload: TSendOTPRequest): Promise<TResponseMessage> => {
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/send-otp', data: payload });
return data;
};
export const postVerifyEmail = async (
payload: TVerifyEmailRequest
): Promise<TResponseMessage> => {
const { data } = await api({
method: 'POST',
url: '/auth/verify-email',
data: {otp: parseInt(payload.otp), email: payload.email},
});
export const postForgotPassword = async (payload: TForgotPasswordRequest): Promise<TResponseMessage> => {
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/forgot', data: payload });
return data;
};
export const postSendOtp = async (
payload: TSendOTPRequest
): Promise<TResponseMessage> => {
const { data } = await api({
method: 'POST',
url: '/auth/send-otp',
data: payload,
});
export const postNewPassword = async (payload: TNewPasswordRequest): Promise<TResponseMessage> => {
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/new-password', data: payload });
return data;
};
export const getGoogleAuthUrl = async (): Promise<string> => {
const baseUrl = getBaseURL() || 'http://localhost:8080';
return `${baseUrl}/auth/google/login`;
};
export const postGoogleCallback = async (code: string, state: string): Promise<TGoogleCallbackResponse> => {
const { data } = await api({
method: 'GET',
url: `/auth/google/callback?code=${code}&state=${state}`,
});
return data;
};
export const getGitHubAuthUrl = async (): Promise<string> => {
const baseUrl = getBaseURL() || 'http://localhost:8080';
return `${baseUrl}/auth/github/login`;
};
export const postGitHubCallback = async (code: string, state: string): Promise<TGoogleCallbackResponse> => {
const { data } = await api({
method: 'GET',
url: `/auth/github/callback?code=${code}&state=${state}`,
});
export const postRefreshToken = async (payload: TRefreshTokenRequest): Promise<{ access_token: string; refresh_token: string }> => {
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/refresh', data: payload });
return data;
};
+3 -49
View File
@@ -1,52 +1,6 @@
import axios from 'axios';
import { useAuthStore } from '../hooks/auth';
const BACKOFFICE_API_URL = 'https://api.hackathon.imphnen.dev/api/v1';
export const backofficeApi = axios.create({
baseURL: BACKOFFICE_API_URL,
headers: {
'Content-Type': 'application/json',
},
});
backofficeApi.interceptors.request.use(
(config) => {
const { session } = useAuthStore.getState();
if (session?.token?.access_token) {
config.headers.Authorization = `Bearer ${session.token.access_token}`;
}
return config;
},
(error) => {
return Promise.reject(new Error(error.message || 'Request failed'));
}
);
backofficeApi.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
const isAuthPage =
globalThis.window !== undefined &&
globalThis.location.pathname.startsWith('/auth');
if (!isAuthPage) {
useAuthStore.getState().clearSession();
if (globalThis.window !== undefined) {
globalThis.location.href = '/auth/login';
}
}
}
const backendMsg = error?.response?.data?.message;
if (backendMsg && typeof backendMsg === 'string') {
return Promise.reject(new Error(backendMsg));
}
return Promise.reject(new Error(error.message || 'An error occurred'));
}
);
// All API calls now go through the main `api` instance (api.imphnen.dev).
// This file re-exports `api` as `backofficeApi` for backward compatibility.
export { api as backofficeApi } from './index';
export interface BackofficeApiResponse<T> {
data: T;
+28
View File
@@ -0,0 +1,28 @@
import { api, ApiResponse } from '../index';
import type { TEventsListItem, TEventsDetailItem, TEventCreateRequest, TEventUpdateRequest } from '../../types/events';
import type { TApiPaginated, TPaginationParams } from '../../types/common';
export const getEventList = async (params?: TPaginationParams): Promise<TApiPaginated<TEventsListItem>> => {
const response = await api.get<TApiPaginated<TEventsListItem>>('/v1/landing/cms/events', { params });
return response.data;
};
export const getEventById = async (id: string): Promise<TEventsDetailItem> => {
const response = await api.get<ApiResponse<TEventsDetailItem>>(`/v1/landing/cms/events/detail/${id}`);
return response.data.data;
};
export const createEvent = async (data: TEventCreateRequest): Promise<TEventsDetailItem> => {
const response = await api.post<ApiResponse<TEventsDetailItem>>('/v1/landing/cms/events/create', data);
return response.data.data;
};
export const updateEvent = async (id: string, data: TEventUpdateRequest): Promise<TEventsDetailItem> => {
const response = await api.patch<ApiResponse<TEventsDetailItem>>(`/v1/landing/cms/events/update/${id}`, data);
return response.data.data;
};
export const deleteEvent = async (id: string): Promise<{ message: string }> => {
const response = await api.delete<{ message: string }>(`/v1/landing/cms/events/delete/${id}`);
return response.data;
};
+87 -1
View File
@@ -1 +1,87 @@
export {};
import { api, ApiResponse } from '../index';
import type {
TGachaItemDto,
TGachaItemCreateRequest,
TGachaItemUpdateRequest,
TGachaRollItemDto,
TGachaRollCreateRequest,
TGachaCreditDto,
TGachaCreditAddRequest,
TGachaClaimDetailDto,
TGachaClaimCreateRequest,
} from '../../types/gacha';
import type { TApiPaginated, TPaginationParams } from '../../types/common';
// ----- Credits -----
export const getUserCredits = async (): Promise<TGachaCreditDto> => {
const response = await api.get<ApiResponse<TGachaCreditDto>>('/v1/gacha/credits/');
return response.data.data;
};
export const addCredits = async (data: TGachaCreditAddRequest): Promise<TGachaCreditDto> => {
const response = await api.post<ApiResponse<TGachaCreditDto>>('/v1/gacha/credits/add', data);
return response.data.data;
};
export const consumeCredit = async (): Promise<{ message: string }> => {
const response = await api.post<{ message: string }>('/v1/gacha/credits/consume');
return response.data;
};
// ----- Items -----
export const getGachaItemList = async (params?: TPaginationParams): Promise<TApiPaginated<TGachaItemDto>> => {
const response = await api.get<TApiPaginated<TGachaItemDto>>('/v1/gacha/items/', { params });
return response.data;
};
export const getGachaItemById = async (id: string): Promise<TGachaItemDto> => {
const response = await api.get<ApiResponse<TGachaItemDto>>(`/v1/gacha/items/detail/${id}`);
return response.data.data;
};
export const createGachaItem = async (data: TGachaItemCreateRequest): Promise<TGachaItemDto> => {
const response = await api.post<ApiResponse<TGachaItemDto>>('/v1/gacha/items/create', data);
return response.data.data;
};
export const updateGachaItem = async (id: string, data: TGachaItemUpdateRequest): Promise<TGachaItemDto> => {
const response = await api.put<ApiResponse<TGachaItemDto>>(`/v1/gacha/items/update/${id}`, data);
return response.data.data;
};
export const deleteGachaItem = async (id: string): Promise<{ message: string }> => {
const response = await api.delete<{ message: string }>(`/v1/gacha/items/delete/${id}`);
return response.data;
};
// ----- Rolls -----
export const getGachaRollById = async (id: string): Promise<TGachaRollItemDto> => {
const response = await api.get<ApiResponse<TGachaRollItemDto>>(`/v1/gacha/rolls/detail/${id}`);
return response.data.data;
};
export const createGachaRoll = async (data: TGachaRollCreateRequest): Promise<TGachaRollItemDto> => {
const response = await api.post<ApiResponse<TGachaRollItemDto>>('/v1/gacha/rolls/create', data);
return response.data.data;
};
export const executeGachaRoll = async (): Promise<TGachaRollItemDto> => {
const response = await api.post<ApiResponse<TGachaRollItemDto>>('/v1/gacha/rolls/execute');
return response.data.data;
};
export const deleteGachaRoll = async (id: string): Promise<{ message: string }> => {
const response = await api.delete<{ message: string }>(`/v1/gacha/rolls/delete/${id}`);
return response.data;
};
// ----- Claims -----
export const getGachaClaimById = async (id: string): Promise<TGachaClaimDetailDto> => {
const response = await api.get<ApiResponse<TGachaClaimDetailDto>>(`/v1/gacha/claims/detail/${id}`);
return response.data.data;
};
export const createGachaClaim = async (data: TGachaClaimCreateRequest): Promise<TGachaClaimDetailDto> => {
const response = await api.post<ApiResponse<TGachaClaimDetailDto>>('/v1/gacha/claims/create', data);
return response.data.data;
};
+3 -48
View File
@@ -1,51 +1,6 @@
import axios from 'axios';
import { useAuthStore } from '../hooks/auth';
const HACKATHON_API_URL = 'https://api.hackathon.imphnen.dev/api/v1';
export const hackathonApi = axios.create({
baseURL: HACKATHON_API_URL,
headers: {
'Content-Type': 'application/json',
},
});
hackathonApi.interceptors.request.use(
(config) => {
const { session } = useAuthStore.getState();
if (session?.token?.access_token) {
config.headers.Authorization = `Bearer ${session.token.access_token}`;
}
return config;
},
(error) => {
return Promise.reject(new Error(error.message || 'Request failed'));
}
);
hackathonApi.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
const isAuthPage = globalThis.window !== undefined && globalThis.location.pathname.startsWith('/auth');
const isCertificatePage = globalThis.window !== undefined && globalThis.location.pathname.startsWith('/certificate/');
if (!isAuthPage && !isCertificatePage) {
useAuthStore.getState().clearSession();
if (globalThis.window !== undefined) {
globalThis.location.href = '/auth/login';
}
}
}
const backendMsg = error?.response?.data?.message;
if (backendMsg && typeof backendMsg === 'string') {
return Promise.reject(new Error(backendMsg));
}
return Promise.reject(new Error(error.message || 'Request failed'));
}
);
// All API calls now go through the main `api` instance (api.imphnen.dev).
// This file re-exports `api` as `hackathonApi` for backward compatibility.
export { api as hackathonApi } from './index';
export interface HackathonApiResponse<T> {
data: T;
+6 -1
View File
@@ -7,6 +7,11 @@ export * from './mentors';
export * from './upload';
export * from './hackathon';
export * from './admin';
export * from './roles';
export * from './permissions';
export * from './events';
export * from './testimonials';
export * from './sessions';
export interface ApiResponse<T> {
data: T;
@@ -140,7 +145,7 @@ async function handleTokenRefresh(originalRequest: AxiosRequestConfig) {
}
async function refreshAccessToken(refreshToken: string) {
return axios.post(`${getBaseURL()}/auth/refresh`, {
return axios.post(`${getBaseURL()}/v1/iam/auth/refresh`, {
refresh_token: refreshToken,
});
}
+58 -31
View File
@@ -1,34 +1,61 @@
import { api, ApiResponse } from '../index';
import type {
MentorDetailResponseDto,
MentorUpdateRequestDto
} from '../../types/mentors';
import type { MentorDetailResponseDto, MentorUpdateRequestDto } from '../../types/mentors';
import type { TApiPaginated, TPaginationParams } from '../../types/common';
export interface MentorService {
getMentorMe(): Promise<MentorDetailResponseDto>;
getMentorById(id: string): Promise<MentorDetailResponseDto>;
updateMentorMe(data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto>;
updateMentorById(id: string, data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto>;
}
export const mentorService: MentorService = {
async getMentorMe() {
const response = await api.get<ApiResponse<MentorDetailResponseDto>>('/mentors/me');
return response.data.data;
},
async getMentorById(id: string) {
const response = await api.get<ApiResponse<MentorDetailResponseDto>>(`/mentors/detail/${id}`);
return response.data.data;
},
async updateMentorMe(data: MentorUpdateRequestDto) {
const response = await api.put<ApiResponse<MentorDetailResponseDto>>('/mentors/update/me', data);
return response.data.data;
},
async updateMentorById(id: string, data: MentorUpdateRequestDto) {
const response = await api.put<ApiResponse<MentorDetailResponseDto>>(`/mentors/update/${id}`, data);
return response.data.data;
},
export type MentorRegisterRequest = MentorUpdateRequestDto & {
email: string;
password: string;
};
export const registerMentor = async (data: MentorRegisterRequest): Promise<MentorDetailResponseDto> => {
const response = await api.post<ApiResponse<MentorDetailResponseDto>>('/v1/dimentorin/mentors/create', data);
return response.data.data;
};
export const getMentorList = async (params?: TPaginationParams): Promise<TApiPaginated<MentorDetailResponseDto>> => {
const response = await api.get<TApiPaginated<MentorDetailResponseDto>>('/v1/dimentorin/mentors', { params });
return response.data;
};
export const getMentorMe = async (): Promise<MentorDetailResponseDto> => {
const response = await api.get<ApiResponse<MentorDetailResponseDto>>('/v1/dimentorin/mentors/me');
return response.data.data;
};
export const getMentorStatus = async (): Promise<{ status: string }> => {
const response = await api.get<ApiResponse<{ status: string }>>('/v1/dimentorin/mentors/me/status');
return response.data.data;
};
export const getMentorById = async (id: string): Promise<MentorDetailResponseDto> => {
const response = await api.get<ApiResponse<MentorDetailResponseDto>>(`/v1/dimentorin/mentors/detail/${id}`);
return response.data.data;
};
export const updateMentorMe = async (data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto> => {
const response = await api.put<ApiResponse<MentorDetailResponseDto>>('/v1/dimentorin/mentors/me/update', data);
return response.data.data;
};
export const updateMentorById = async (id: string, data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto> => {
const response = await api.put<ApiResponse<MentorDetailResponseDto>>(`/v1/dimentorin/mentors/update/${id}`, data);
return response.data.data;
};
export const deleteMentor = async (id: string): Promise<{ message: string }> => {
const response = await api.delete<{ message: string }>(`/v1/dimentorin/mentors/delete/${id}`);
return response.data;
};
export const verifyMentor = async (id: string): Promise<{ message: string }> => {
const response = await api.put<{ message: string }>(`/v1/dimentorin/mentors/verify/${id}`);
return response.data;
};
// Legacy service object for backward compatibility
export const mentorService = {
getMentorMe,
getMentorById,
updateMentorMe,
updateMentorById: (id: string, data: MentorUpdateRequestDto) => updateMentorById(id, data),
};
+28
View File
@@ -0,0 +1,28 @@
import { api, ApiResponse } from '../index';
import type { TPermissionItem, TPermissionCreateRequest, TPermissionUpdateRequest } from '../../types/permissions';
import type { TApiPaginated, TPaginationParams } from '../../types/common';
export const getPermissionList = async (params?: TPaginationParams): Promise<TApiPaginated<TPermissionItem>> => {
const response = await api.get<TApiPaginated<TPermissionItem>>('/v1/iam/permissions', { params });
return response.data;
};
export const getPermissionById = async (id: string): Promise<TPermissionItem> => {
const response = await api.get<ApiResponse<TPermissionItem>>(`/v1/iam/permissions/detail/${id}`);
return response.data.data;
};
export const createPermission = async (data: TPermissionCreateRequest): Promise<TPermissionItem> => {
const response = await api.post<ApiResponse<TPermissionItem>>('/v1/iam/permissions/create', data);
return response.data.data;
};
export const updatePermission = async (id: string, data: TPermissionUpdateRequest): Promise<TPermissionItem> => {
const response = await api.put<ApiResponse<TPermissionItem>>(`/v1/iam/permissions/update/${id}`, data);
return response.data.data;
};
export const deletePermission = async (id: string): Promise<{ message: string }> => {
const response = await api.delete<{ message: string }>(`/v1/iam/permissions/delete/${id}`);
return response.data;
};
+28
View File
@@ -0,0 +1,28 @@
import { api, ApiResponse } from '../index';
import type { TRolesListItem, TRoleDetailItem, TRoleCreateRequest, TRoleUpdateRequest } from '../../types/roles';
import type { TApiPaginated, TPaginationParams } from '../../types/common';
export const getRoleList = async (params?: TPaginationParams): Promise<TApiPaginated<TRolesListItem>> => {
const response = await api.get<TApiPaginated<TRolesListItem>>('/v1/iam/roles', { params });
return response.data;
};
export const getRoleById = async (id: string): Promise<TRoleDetailItem> => {
const response = await api.get<ApiResponse<TRoleDetailItem>>(`/v1/iam/roles/detail/${id}`);
return response.data.data;
};
export const createRole = async (data: TRoleCreateRequest): Promise<TRoleDetailItem> => {
const response = await api.post<ApiResponse<TRoleDetailItem>>('/v1/iam/roles/create', data);
return response.data.data;
};
export const updateRole = async (id: string, data: TRoleUpdateRequest): Promise<TRoleDetailItem> => {
const response = await api.put<ApiResponse<TRoleDetailItem>>(`/v1/iam/roles/update/${id}`, data);
return response.data.data;
};
export const deleteRole = async (id: string): Promise<{ message: string }> => {
const response = await api.delete<{ message: string }>(`/v1/iam/roles/delete/${id}`);
return response.data;
};
+64
View File
@@ -0,0 +1,64 @@
import { api, ApiResponse } from '../index';
import type {
TBookSessionRequest,
TBookSessionResponse,
TUpdateSessionStatusRequest,
TUpdateSessionStatusResponse,
TSessionFeedbackRequest,
TSessionFeedbackResponse,
TSessionListResponse,
TMentorAvailability,
} from '../../types/sessions';
export const getMentorAvailability = async (mentorId: string): Promise<TMentorAvailability> => {
const response = await api.get<ApiResponse<TMentorAvailability>>(
`/v1/dimentorin/mentors/${mentorId}/availability`
);
return response.data.data;
};
export const bookSession = async (mentorId: string, data: TBookSessionRequest): Promise<TBookSessionResponse> => {
const response = await api.post<ApiResponse<TBookSessionResponse>>(
`/v1/dimentorin/mentors/${mentorId}/sessions/create`,
data
);
return response.data.data;
};
export const getMentorSessions = async (mentorId: string, params?: { status?: string }): Promise<TSessionListResponse> => {
const response = await api.get<ApiResponse<TSessionListResponse>>(
`/v1/dimentorin/mentors/${mentorId}/sessions`,
{ params }
);
return response.data.data;
};
export const getMySessions = async (params?: { status?: string }): Promise<TSessionListResponse> => {
const response = await api.get<ApiResponse<TSessionListResponse>>(
'/v1/dimentorin/sessions/me',
{ params }
);
return response.data.data;
};
export const updateSessionStatus = async (
id: string,
data: TUpdateSessionStatusRequest
): Promise<TUpdateSessionStatusResponse> => {
const response = await api.put<ApiResponse<TUpdateSessionStatusResponse>>(
`/v1/dimentorin/sessions/update/${id}/status`,
data
);
return response.data.data;
};
export const submitFeedback = async (
id: string,
data: TSessionFeedbackRequest
): Promise<TSessionFeedbackResponse> => {
const response = await api.post<ApiResponse<TSessionFeedbackResponse>>(
`/v1/dimentorin/sessions/${id}/feedback/create`,
data
);
return response.data.data;
};
+37 -33
View File
@@ -4,7 +4,6 @@ import type {
TUpdateTeamRequest,
TInviteMemberRequest,
TJoinTeamRequest,
TManageMemberRequest,
TSubmitProjectRequest,
TTeamListResponse,
TTeamDetailResponse,
@@ -14,8 +13,6 @@ import type {
TProjectSubmissionResponse,
} from '../../types/teams';
const TEAMS_BASE_URL = '/teams';
export const getTeams = async (params?: {
page?: number;
limit?: number;
@@ -23,86 +20,93 @@ export const getTeams = async (params?: {
visibility?: string;
search?: string;
}) => {
const response = await api.get<TTeamListResponse>(TEAMS_BASE_URL, { params });
const response = await api.get<TTeamListResponse>('/v1/hackathon/teams/browse', { params });
return response.data;
};
export const getTeamById = async (teamId: string) => {
const response = await api.get<TTeamDetailResponse>(`${TEAMS_BASE_URL}/${teamId}`);
const response = await api.get<TTeamDetailResponse>(`/v1/hackathon/teams/${teamId}`);
return response.data;
};
export const createTeam = async (data: TCreateTeamRequest) => {
const response = await api.post<TTeamDetailResponse>(TEAMS_BASE_URL, data);
const response = await api.post<TTeamDetailResponse>('/v1/hackathon/teams', data);
return response.data;
};
export const updateTeam = async (teamId: string, data: TUpdateTeamRequest) => {
const response = await api.put<TTeamDetailResponse>(`${TEAMS_BASE_URL}/${teamId}`, data);
const response = await api.put<TTeamDetailResponse>(`/v1/hackathon/teams/${teamId}`, data);
return response.data;
};
export const deleteTeam = async (teamId: string) => {
const response = await api.delete(`/v1/hackathon/teams/${teamId}`);
return response.data;
};
export const getTeamMembers = async (teamId: string) => {
const response = await api.get<TTeamMembersResponse>(`${TEAMS_BASE_URL}/${teamId}/members`);
const response = await api.get<TTeamMembersResponse>(`/v1/hackathon/teams/${teamId}`);
return response.data;
};
export const inviteMember = async (teamId: string, data: TInviteMemberRequest) => {
const response = await api.post(`${TEAMS_BASE_URL}/${teamId}/invite`, data);
return response.data;
};
export const manageMember = async (teamId: string, userId: string, data: TManageMemberRequest) => {
const response = await api.put(`${TEAMS_BASE_URL}/${teamId}/members/${userId}`, data);
const response = await api.post(`/v1/hackathon/invitations/teams/${teamId}/invite`, data);
return response.data;
};
export const removeMember = async (teamId: string, userId: string) => {
const response = await api.delete(`${TEAMS_BASE_URL}/${teamId}/members/${userId}`);
const response = await api.delete(`/v1/hackathon/teams/${teamId}/members/${userId}`);
return response.data;
};
export const joinTeam = async (teamId: string, data: TJoinTeamRequest) => {
const response = await api.post(`${TEAMS_BASE_URL}/${teamId}/join-request`, data);
const response = await api.post(`/v1/hackathon/join-requests/teams/${teamId}`, data);
return response.data;
};
export const getTeamJoinRequests = async (teamId: string) => {
const response = await api.get<TTeamJoinRequestsResponse>(`${TEAMS_BASE_URL}/${teamId}/join-requests`);
const response = await api.get<TTeamJoinRequestsResponse>(
`/v1/hackathon/join-requests/teams/${teamId}/pending`
);
return response.data;
};
export const respondToJoinRequest = async (teamId: string, requestId: string, action: 'approve' | 'reject') => {
const response = await api.put(`${TEAMS_BASE_URL}/${teamId}/join-requests/${requestId}`, { action });
export const respondToJoinRequest = async (requestId: string, action: 'accept' | 'reject') => {
const response = await api.post(`/v1/hackathon/join-requests/${requestId}/respond`, { action });
return response.data;
};
export const getMyInvitations = async () => {
const response = await api.get<TTeamInvitationsResponse>(`${TEAMS_BASE_URL}/invitations/me`);
const response = await api.get<TTeamInvitationsResponse>('/v1/hackathon/invitations/my');
return response.data;
};
export const respondToInvitation = async (invitationId: string, action: 'accept' | 'reject') => {
const response = await api.put(`${TEAMS_BASE_URL}/invitations/${invitationId}`, { action });
const response = await api.post(`/v1/hackathon/invitations/${invitationId}/respond`, { action });
return response.data;
};
export const getMyTeams = async () => {
const response = await api.get<TTeamListResponse>(`${TEAMS_BASE_URL}/me`);
return response.data;
};
export const submitProject = async (teamId: string, data: TSubmitProjectRequest) => {
const response = await api.post<TProjectSubmissionResponse>(`${TEAMS_BASE_URL}/${teamId}/submission`, data);
return response.data;
};
export const getTeamSubmission = async (teamId: string) => {
const response = await api.get<TProjectSubmissionResponse>(`${TEAMS_BASE_URL}/${teamId}/submission`);
const response = await api.get('/v1/hackathon/teams/my');
return response.data;
};
export const leaveTeam = async (teamId: string) => {
const response = await api.post(`${TEAMS_BASE_URL}/${teamId}/leave`);
const response = await api.post(`/v1/hackathon/teams/${teamId}/leave`);
return response.data;
};
export const submitProject = async (teamId: string, data: TSubmitProjectRequest) => {
const response = await api.post<TProjectSubmissionResponse>(
`/v1/hackathon/submissions/teams/${teamId}`,
data
);
return response.data;
};
export const getTeamSubmission = async (teamId: string) => {
const response = await api.get<TProjectSubmissionResponse>(
`/v1/hackathon/submissions/teams/${teamId}`
);
return response.data;
};
@@ -0,0 +1,33 @@
import { api, ApiResponse } from '../index';
import type {
TTestimonialsListItem,
TTestimonialsDetailItem,
TTestimonialCreateRequest,
TTestimonialUpdateRequest,
} from '../../types/testimonials';
import type { TApiPaginated, TPaginationParams } from '../../types/common';
export const getTestimonialList = async (params?: TPaginationParams): Promise<TApiPaginated<TTestimonialsListItem>> => {
const response = await api.get<TApiPaginated<TTestimonialsListItem>>('/v1/landing/cms/testimonials', { params });
return response.data;
};
export const getTestimonialById = async (id: string): Promise<TTestimonialsDetailItem> => {
const response = await api.get<ApiResponse<TTestimonialsDetailItem>>(`/v1/landing/cms/testimonials/detail/${id}`);
return response.data.data;
};
export const createTestimonial = async (data: TTestimonialCreateRequest): Promise<TTestimonialsDetailItem> => {
const response = await api.post<ApiResponse<TTestimonialsDetailItem>>('/v1/landing/cms/testimonials/create', data);
return response.data.data;
};
export const updateTestimonial = async (id: string, data: TTestimonialUpdateRequest): Promise<TTestimonialsDetailItem> => {
const response = await api.patch<ApiResponse<TTestimonialsDetailItem>>(`/v1/landing/cms/testimonials/update/${id}`, data);
return response.data.data;
};
export const deleteTestimonial = async (id: string): Promise<{ message: string }> => {
const response = await api.delete<{ message: string }>(`/v1/landing/cms/testimonials/delete/${id}`);
return response.data;
};
+26 -46
View File
@@ -1,59 +1,39 @@
import { api, ApiResponse } from '../index';
export interface UploadResponse {
filename: string;
original_filename: string;
uploaded_path: string;
filename?: string;
original_filename?: string;
uploaded_path?: string;
url: string;
size: number;
content_type: string;
file_type: string;
user_id: string;
email: string;
size?: number;
content_type?: string;
file_type?: string;
user_id?: string;
email?: string;
}
export interface UploadService {
uploadFile(file: File): Promise<UploadResponse>;
uploadAvatar(file: File): Promise<UploadResponse>;
uploadCV(file: File): Promise<UploadResponse>;
}
const multipartPost = async (url: string, file: File, fieldName = 'file'): Promise<UploadResponse> => {
const formData = new FormData();
formData.append(fieldName, file);
const response = await api.post<ApiResponse<UploadResponse>>(url, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data.data;
};
export const uploadService: UploadService = {
async uploadFile(file: File) {
const formData = new FormData();
formData.append('file', file);
export const uploadUserFile = (file: File) => multipartPost('/v1/iam/users/upload', file);
const response = await api.post<ApiResponse<UploadResponse>>('/users/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data.data;
},
export const uploadHackathonFile = (file: File) => multipartPost('/v1/hackathon/upload', file);
async uploadAvatar(file: File) {
if (!file.type.startsWith('image/')) {
throw new Error('File harus berupa gambar');
}
export const uploadHackathonAvatar = (file: File) => multipartPost('/v1/hackathon/upload/avatar', file);
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('Ukuran file maksimal 5MB');
}
export const uploadHackathonTeamFile = (file: File) => multipartPost('/v1/hackathon/upload/team', file);
return this.uploadFile(file);
},
export const uploadHackathonSubmission = (file: File) => multipartPost('/v1/hackathon/upload/submission', file);
async uploadCV(file: File) {
if (file.type !== 'application/pdf') {
throw new Error('CV harus berupa file PDF');
}
const maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('Ukuran file maksimal 10MB');
}
return this.uploadFile(file);
},
// Legacy service object for backward compatibility
export const uploadService = {
uploadFile: uploadUserFile,
uploadAvatar: uploadUserFile,
uploadCV: uploadUserFile,
};

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