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>
@@ -112,4 +144,4 @@ export default function Components(): ReactElement {
</div>
</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 (
<>
@@ -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>
@@ -160,4 +214,4 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
)}
</AnimatePresence>
)
}
}
@@ -3,7 +3,7 @@ import { cn } from "@imphnen-frontend-service/utils"
import { motion } from "framer-motion"
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
Saya ingin tahu secara khusus tentang ___.
@@ -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
@@ -31,4 +36,4 @@ export const StatisticsSection: FC = () => {
</div>
</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>
)
@@ -1,4 +1,5 @@
import { FC, useState } from 'react'
import { useParams } from 'react-router-dom'
import { ProfileSection } from './_components/sections/profile'
import { StatisticsSection } from './_components/sections/senpai-statistics'
import { TopicsSection } from './_components/sections/topics'
@@ -7,15 +8,27 @@ import { EducationSection } from './_components/sections/education'
import { SenpaiScheduleSection } from './_components/sections/senpai-schedule'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { AppointmentModal } from './_components/modals/appointment'
import { useMentorById } from '@imphnen-frontend-service/service'
export const Components: FC = () => {
const [open, setOpen] = useState(false)
const params = useParams()
const mentorId = params?.id ?? ''
const { data: mentor, isLoading } = useMentorById(mentorId)
if (isLoading) {
return (
<main className="min-h-screen flex items-center justify-center">
<div className="text-center text-neutral-400">Loading mentor profile...</div>
</main>
)
}
return (
<main>
<section className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20">
<div className="max-w-7xl mx-auto space-y-8 md:bg-white xl:bg-transparent">
<ProfileSection onBook={() => setOpen(true)} />
<ProfileSection mentor={mentor} onBook={() => setOpen(true)} />
<Button type="button" size="sm" className="w-full md:hidden" onClick={() => setOpen(true)}>
Book Your Senpai!
@@ -23,18 +36,20 @@ export const Components: FC = () => {
<div className="bg-white px-4 py-5 md:px-8 md:pb-6 md:pt-0 xl:py-7 xl:flex xl:gap-x-10">
<div className="space-y-10 md:space-y-7 xl:flex-1">
<StatisticsSection />
<TopicsSection />
<StatisticsSection mentor={mentor} />
<TopicsSection mentor={mentor} />
<div className="px-6 py-8 rounded-md shadow-md">
<h2 className="text-xs text-neutral-800 font-semibold mb-5 md:text-[15px] xl:text-[19px]">Senpai Resume</h2>
<p className="text-[10px] font-medium text-neutral-600 text-pretty md:text-[15px]">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
</p>
</div>
{mentor?.bio && (
<div className="px-6 py-8 rounded-md shadow-md">
<h2 className="text-xs text-neutral-800 font-semibold mb-5 md:text-[15px] xl:text-[19px]">Senpai Resume</h2>
<p className="text-[10px] font-medium text-neutral-600 text-pretty md:text-[15px]">
{mentor.bio}
</p>
</div>
)}
<ExperienceSection />
<EducationSection />
<ExperienceSection mentor={mentor} />
<EducationSection mentor={mentor} />
</div>
<div className="hidden xl:block xl:w-[400px]">
@@ -44,7 +59,7 @@ export const Components: FC = () => {
</div>
</section>
<AppointmentModal open={open} setOpen={setOpen} />
<AppointmentModal open={open} setOpen={setOpen} mentorId={mentorId} />
</main>
)
}
@@ -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>