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:
co-authored by
Claude Sonnet 4.6
parent
8d2aaf5188
commit
53ad40f3fd
@@ -1,15 +1,17 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface IModalEditAccount {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleEditAccount?: () => void;
|
||||
handleEditAccount?: () => Promise<void>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
initialValues?: { fullname?: string; email?: string };
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const ModalEditAccount = ({
|
||||
@@ -17,9 +19,10 @@ const ModalEditAccount = ({
|
||||
onClose,
|
||||
currentStep,
|
||||
nextStep,
|
||||
prevStep,
|
||||
resetStep,
|
||||
handleEditAccount,
|
||||
initialValues,
|
||||
onDataCapture,
|
||||
}: IModalEditAccount) => {
|
||||
return (
|
||||
<Modal
|
||||
@@ -31,7 +34,14 @@ const ModalEditAccount = ({
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 1 && (
|
||||
<StepOne
|
||||
nextStep={nextStep}
|
||||
onClose={onClose}
|
||||
initialValues={initialValues}
|
||||
onDataCapture={onDataCapture}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
@@ -46,13 +56,18 @@ const ModalEditAccount = ({
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
initialValues?: { fullname?: string; email?: string };
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const [fullName, setFullName] = useState('Ahmad Wiyana');
|
||||
const [email, setEmail] = useState('fullname23@gmail.com');
|
||||
const [phoneNumber, setPhoneNumber] = useState('081904423804');
|
||||
const [address, setAddress] = useState('Jl. Pantai Cibaduyut Indah');
|
||||
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
|
||||
const [fullName, setFullName] = useState(initialValues?.fullname ?? '');
|
||||
const [email, setEmail] = useState(initialValues?.email ?? '');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(initialValues?.fullname ?? '');
|
||||
setEmail(initialValues?.email ?? '');
|
||||
}, [initialValues]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -81,31 +96,16 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Nomor Telepon"
|
||||
type="text"
|
||||
placeholder="Masukkan Nomor Telepon"
|
||||
value={phoneNumber}
|
||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Alamat"
|
||||
type="text"
|
||||
placeholder="Masukkan Alamat"
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
onClick={() => {
|
||||
onDataCapture?.({ fullname: fullName, email });
|
||||
nextStep();
|
||||
}}
|
||||
>
|
||||
Perbarui Data
|
||||
</Button>
|
||||
@@ -116,7 +116,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleEditAccount?: () => void;
|
||||
handleEditAccount?: () => Promise<void>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
@@ -147,8 +147,8 @@ const StepTwo = ({ onClose, handleEditAccount, resetStep }: IStepTwoProps) => (
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
handleEditAccount && handleEditAccount();
|
||||
onClick={async () => {
|
||||
if (handleEditAccount) await handleEditAccount();
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
|
||||
import {
|
||||
FilterOutlined,
|
||||
SearchOutlined,
|
||||
@@ -19,25 +19,17 @@ import {
|
||||
} from '@tanstack/react-table';
|
||||
import ModalEditAccount from './_components/modal-edit-account';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
|
||||
interface Account {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
|
||||
email: 'fullname23@gmail.com',
|
||||
phone: '081904423804',
|
||||
address: 'Jl. Pantai Cibaduyut Indah',
|
||||
}));
|
||||
import {
|
||||
useUserList,
|
||||
useUpdateUserById,
|
||||
TUsersListItem,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalEditAccount, setShowModalEditAccount] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState<TUsersListItem | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const pendingFormData = useRef<any>(null);
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
@@ -58,7 +50,23 @@ export const Components: FC = (): ReactElement => {
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
|
||||
const columns: ColumnDef<Account>[] = [
|
||||
const { data: usersData, isLoading } = useUserList({
|
||||
search,
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
});
|
||||
const updateUser = useUpdateUserById();
|
||||
|
||||
const users: TUsersListItem[] = usersData?.data ?? [];
|
||||
const totalItems = usersData?.meta?.total ?? users.length;
|
||||
|
||||
const handleEditAccount = async () => {
|
||||
if (selectedUser && pendingFormData.current) {
|
||||
await updateUser.mutateAsync({ id: selectedUser.id, data: pendingFormData.current });
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnDef<TUsersListItem>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
@@ -84,19 +92,24 @@ export const Components: FC = (): ReactElement => {
|
||||
},
|
||||
{
|
||||
header: 'Nama Lengkap',
|
||||
accessorKey: 'name',
|
||||
accessorKey: 'fullname',
|
||||
},
|
||||
{
|
||||
header: 'Email',
|
||||
accessorKey: 'email',
|
||||
},
|
||||
{
|
||||
header: 'Nomor Telp',
|
||||
accessorKey: 'phone',
|
||||
header: 'Role',
|
||||
accessorKey: 'role',
|
||||
},
|
||||
{
|
||||
header: 'Alamat Pengiriman',
|
||||
accessorKey: 'address',
|
||||
header: 'Status',
|
||||
accessorKey: 'is_active',
|
||||
cell: ({ row }) => (
|
||||
<span className={row.original.is_active ? 'text-success-500' : 'text-danger-500'}>
|
||||
{row.original.is_active ? 'Aktif' : 'Tidak Aktif'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
@@ -106,6 +119,7 @@ export const Components: FC = (): ReactElement => {
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedUser(row.original);
|
||||
setShowModalEditAccount(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
@@ -117,7 +131,7 @@ export const Components: FC = (): ReactElement => {
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockData,
|
||||
data: users,
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
@@ -128,8 +142,8 @@ export const Components: FC = (): ReactElement => {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -144,6 +158,8 @@ export const Components: FC = (): ReactElement => {
|
||||
<Input
|
||||
placeholder="Cari berdasarkan nama lengkap, email"
|
||||
className="pl-12 w-full max-h-full"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||
<SearchOutlined />
|
||||
@@ -167,7 +183,11 @@ export const Components: FC = (): ReactElement => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DataTable data={mockData} columns={columns} table={table} />
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-neutral-400">Loading...</div>
|
||||
) : (
|
||||
<DataTable data={users} columns={columns} table={table} />
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -175,12 +195,12 @@ export const Components: FC = (): ReactElement => {
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalEditAccount}
|
||||
onClose={() => setShowModalEditAccount(false)}
|
||||
handleEditAccount={() => {
|
||||
console.log('Account updated');
|
||||
}}
|
||||
handleEditAccount={handleEditAccount}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
initialValues={selectedUser ? { fullname: selectedUser.fullname, email: selectedUser.email } : undefined}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@@ -4,17 +4,26 @@ import { For } from "@imphnen-frontend-service/utils";
|
||||
import { ReactElement } from "react";
|
||||
import { UserGrowthChart } from "./_components/chart/user-growth";
|
||||
import { SessionStatusChart } from "./_components/chart/session-status";
|
||||
|
||||
const Overview = () => {
|
||||
return (
|
||||
<div className="bg-white px-6 py-4 rounded-md shadow">
|
||||
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">0</h3>
|
||||
<p className="text-neutral-400 text-p3">Total Users</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { useMentorList, useUserList, useMySessions } from "@imphnen-frontend-service/service";
|
||||
|
||||
export default function Components(): ReactElement {
|
||||
const { data: mentorData } = useMentorList({ per_page: 5, sort_by: 'rating', order: 'desc' });
|
||||
const { data: userData } = useUserList({ per_page: 1 });
|
||||
const { data: sessionsData } = useMySessions();
|
||||
|
||||
const totalMentors = mentorData?.meta?.total ?? 0;
|
||||
const totalUsers = userData?.meta?.total ?? 0;
|
||||
const totalSessions = sessionsData?.total ?? 0;
|
||||
const topMentors = mentorData?.data ?? [];
|
||||
|
||||
const overviewStats = [
|
||||
{ label: 'Total Users', value: totalUsers },
|
||||
{ label: 'Total Mentors', value: totalMentors },
|
||||
{ label: 'Total Sessions', value: totalSessions },
|
||||
{ label: 'Active Mentors', value: topMentors.filter((m) => m.status === 'active').length },
|
||||
{ label: 'Completed Sessions', value: sessionsData?.sessions?.filter((s) => s.status === 'completed').length ?? 0 },
|
||||
];
|
||||
|
||||
return (
|
||||
<BackofficeWrapper title="Dimentorin.dev">
|
||||
<h1 className="text-p1 font-semibold text-neutral-700 mb-5">Overview</h1>
|
||||
@@ -26,8 +35,13 @@ export default function Components(): ReactElement {
|
||||
</Button>
|
||||
|
||||
<div className="grid grid-cols-5 gap-5">
|
||||
<For data={Array.from({ length: 5 })}>
|
||||
{(_, index) => <Overview key={index} />}
|
||||
<For data={overviewStats}>
|
||||
{(stat, index) => (
|
||||
<div key={index} className="bg-white px-6 py-4 rounded-md shadow">
|
||||
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">{stat.value}</h3>
|
||||
<p className="text-neutral-400 text-p3">{stat.label}</p>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,15 +81,18 @@ export default function Components(): ReactElement {
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<For data={Array.from({ length: 5 })}>
|
||||
{(_, index) => (
|
||||
<tr key={index} className="shadow rounded-lg">
|
||||
<td className="py-4 px-5">{index + 1}</td>
|
||||
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
||||
<td className="py-4 px-5">4.9</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
{topMentors.slice(0, 5).map((mentor, index) => (
|
||||
<tr key={mentor.id} className="shadow rounded-lg">
|
||||
<td className="py-4 px-5">{index + 1}</td>
|
||||
<td className="py-4 px-5">{mentor.fullname ?? '-'}</td>
|
||||
<td className="py-4 px-5">{mentor.rating?.toFixed(1) ?? '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
{topMentors.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="py-4 px-5 text-center text-neutral-400">Belum ada data</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -89,21 +106,36 @@ export default function Components(): ReactElement {
|
||||
<thead>
|
||||
<tr className="text-label1 bg-primary-50 text-left font-medium">
|
||||
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
|
||||
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
|
||||
<th className="font-medium py-4 px-5 w-3/5">Topik</th>
|
||||
<th className="font-medium py-4 px-5 rounded-r-lg">Total Sesi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<For data={Array.from({ length: 5 })}>
|
||||
{(_, index) => (
|
||||
<tr key={index} className="shadow rounded-lg">
|
||||
{(() => {
|
||||
const sessions = sessionsData?.sessions ?? [];
|
||||
const topicCount: Record<string, number> = {};
|
||||
sessions.forEach((s) => {
|
||||
topicCount[s.topic] = (topicCount[s.topic] ?? 0) + 1;
|
||||
});
|
||||
const topTopics = Object.entries(topicCount)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 5);
|
||||
if (topTopics.length === 0) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={3} className="py-4 px-5 text-center text-neutral-400">Belum ada data</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return topTopics.map(([topic, count], index) => (
|
||||
<tr key={topic} className="shadow rounded-lg">
|
||||
<td className="py-4 px-5">{index + 1}</td>
|
||||
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
||||
<td className="py-4 px-5">1000</td>
|
||||
<td className="py-4 px-5">{topic}</td>
|
||||
<td className="py-4 px-5">{count}</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
));
|
||||
})()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ interface IModalAddItem {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const ModalAddItem = ({
|
||||
@@ -20,6 +21,7 @@ const ModalAddItem = ({
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
onDataCapture,
|
||||
}: IModalAddItem) => {
|
||||
return (
|
||||
<Modal
|
||||
@@ -31,7 +33,7 @@ const ModalAddItem = ({
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
@@ -46,10 +48,11 @@ const ModalAddItem = ({
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep);
|
||||
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -98,7 +101,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
type="submit"
|
||||
>
|
||||
Tambahkan Item
|
||||
</Button>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
interface IModalDeleteItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleDeleteItem?: () => void;
|
||||
handleDeleteItem?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const ModalDeleteItem = ({
|
||||
@@ -48,8 +48,9 @@ const ModalDeleteItem = ({
|
||||
variant="danger"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
handleDeleteItem && handleDeleteItem();
|
||||
onClick={async () => {
|
||||
if (handleDeleteItem) await handleDeleteItem();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Hapus Item
|
||||
|
||||
@@ -11,6 +11,8 @@ interface IModalEditItem {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
initialValues?: { itemName?: string; quantity?: number };
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const ModalEditItem = ({
|
||||
@@ -20,6 +22,8 @@ const ModalEditItem = ({
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleEditItem,
|
||||
initialValues,
|
||||
onDataCapture,
|
||||
}: IModalEditItem) => {
|
||||
return (
|
||||
<Modal
|
||||
@@ -31,7 +35,7 @@ const ModalEditItem = ({
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} initialValues={initialValues} onDataCapture={onDataCapture} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
@@ -46,15 +50,12 @@ const ModalEditItem = ({
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
initialValues?: { itemName?: string; quantity?: number };
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const initialValues = {
|
||||
itemName: 'Hoodie IMPHNEN Official 2025',
|
||||
quantity: 10,
|
||||
};
|
||||
|
||||
const { form, onSubmit } = useItem(nextStep, initialValues);
|
||||
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep, initialValues as any, onDataCapture);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -102,7 +103,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
type="submit"
|
||||
>
|
||||
Perbarui Item
|
||||
</Button>
|
||||
|
||||
@@ -8,16 +8,17 @@ import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
nextStep: () => void,
|
||||
initialValues?: TGachaItem
|
||||
initialValues?: Partial<TGachaItem>,
|
||||
onDataCapture?: (data: any) => void,
|
||||
) => {
|
||||
const form = useForm<TGachaItem>({
|
||||
const form = useForm<any>({
|
||||
resolver: zodResolver(gachaItemSchema),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log('Form data:', data);
|
||||
onDataCapture?.(data);
|
||||
nextStep();
|
||||
});
|
||||
|
||||
@@ -38,6 +39,9 @@ export const useConfirmItem = (
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
if (actionFunction) {
|
||||
await actionFunction();
|
||||
}
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
|
||||
@@ -6,16 +6,26 @@ import {
|
||||
UserSwitchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
|
||||
import ModalAddItem from './_components/modal-add-item';
|
||||
import ModalEditItem from './_components/modal-edit-item';
|
||||
import ModalDeleteItem from './_components/modal-delete-item';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
import {
|
||||
useUserList,
|
||||
useGachaItemList,
|
||||
useCreateGachaItem,
|
||||
useUpdateGachaItem,
|
||||
useDeleteGachaItem,
|
||||
TGachaItemDto,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
const [showModalEditItem, setShowModalEditItem] = useState(false);
|
||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null);
|
||||
const pendingFormData = useRef<any>(null);
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
@@ -28,6 +38,52 @@ export const Components: FC = (): ReactElement => {
|
||||
minValue: 1,
|
||||
});
|
||||
|
||||
const { data: usersData } = useUserList({ per_page: 1 });
|
||||
const { data: gachaItemsData } = useGachaItemList({ per_page: 9 });
|
||||
const createItem = useCreateGachaItem();
|
||||
const updateItem = useUpdateGachaItem();
|
||||
const deleteItem = useDeleteGachaItem();
|
||||
|
||||
const totalUsers = usersData?.meta?.total ?? 0;
|
||||
const gachaItems: TGachaItemDto[] = gachaItemsData?.data ?? [];
|
||||
|
||||
const handleAdd = async (): Promise<boolean> => {
|
||||
if (pendingFormData.current) {
|
||||
const { itemName, quantity } = pendingFormData.current;
|
||||
await createItem.mutateAsync({
|
||||
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
|
||||
name: itemName,
|
||||
description: '',
|
||||
rarity: 'common',
|
||||
type_: 'physical',
|
||||
category: 'merchandise',
|
||||
value: 0,
|
||||
weight: 1,
|
||||
stock: quantity ?? 1,
|
||||
is_limited: false,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleEdit = async (): Promise<boolean> => {
|
||||
if (selectedItem && pendingFormData.current) {
|
||||
const { itemName, quantity } = pendingFormData.current;
|
||||
await updateItem.mutateAsync({
|
||||
id: selectedItem.id,
|
||||
data: { name: itemName, stock: quantity },
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleDelete = async (): Promise<boolean> => {
|
||||
if (selectedItem) {
|
||||
await deleteItem.mutateAsync(selectedItem.id);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
@@ -47,7 +103,7 @@ export const Components: FC = (): ReactElement => {
|
||||
<UsergroupAddOutlined className="text-[20px]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-p1 font-semibold">1000</h3>
|
||||
<h3 className="text-p1 font-semibold">{totalUsers}</h3>
|
||||
<p className="text-label1 text-neutral-500">Participants</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,9 +113,9 @@ export const Components: FC = (): ReactElement => {
|
||||
<ReloadOutlined className="text-[20px]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-p1 font-semibold">1000</h3>
|
||||
<h3 className="text-p1 font-semibold">{gachaItemsData?.meta?.total ?? 0}</h3>
|
||||
<p className="text-label1 text-neutral-500">
|
||||
Roll and Reroll
|
||||
Gacha Items
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +125,7 @@ export const Components: FC = (): ReactElement => {
|
||||
<UserSwitchOutlined className="text-[20px]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-p1 font-semibold">1000</h3>
|
||||
<h3 className="text-p1 font-semibold">-</h3>
|
||||
<p className="text-label1 text-neutral-500">Redeem</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,7 +135,7 @@ export const Components: FC = (): ReactElement => {
|
||||
<UsergroupDeleteOutlined className="text-[20px]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-p1 font-semibold">1000</h3>
|
||||
<h3 className="text-p1 font-semibold">-</h3>
|
||||
<p className="text-label1 text-neutral-500">
|
||||
Inactive Users
|
||||
</p>
|
||||
@@ -105,19 +161,18 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
||||
{gachaItems.map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
key={item.id}
|
||||
className="bg-white overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
||||
>
|
||||
<div className="flex flex-col py-4 px-6 gap-[8px]">
|
||||
<div>
|
||||
<h3 className="text-p3 text-primary-500 font-medium">
|
||||
Lanyard IMPHNEN
|
||||
{item.name}
|
||||
</h3>
|
||||
<div className="flex items-center justify-start gap-10 text-label2 text-gray-500 mt-1">
|
||||
<span>Prize {item}</span>
|
||||
<span>Quantity: 10</span>
|
||||
<span>{item.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-start gap-2">
|
||||
@@ -125,7 +180,10 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="text"
|
||||
size="sm"
|
||||
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
|
||||
onClick={() => setShowModalEditItem(true)}
|
||||
onClick={() => {
|
||||
setSelectedItem(item);
|
||||
setShowModalEditItem(true);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
@@ -133,14 +191,17 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="text"
|
||||
size="sm"
|
||||
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
|
||||
onClick={() => setShowModalDeleteItem(true)}
|
||||
onClick={() => {
|
||||
setSelectedItem(item);
|
||||
setShowModalDeleteItem(true);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img src="gacha-clip.webp" alt="Lanyard IMPHNEN" />
|
||||
<img src="gacha-clip.webp" alt={item.name} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -162,6 +223,8 @@ export const Components: FC = (): ReactElement => {
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleAddItem={handleAdd}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
/>
|
||||
|
||||
<ModalEditItem
|
||||
@@ -171,11 +234,15 @@ export const Components: FC = (): ReactElement => {
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleEditItem={handleEdit}
|
||||
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
/>
|
||||
|
||||
<ModalDeleteItem
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
handleDeleteItem={async () => { await handleDelete(); return true; }}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organ
|
||||
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||
import { ReactElement, useState } from "react"
|
||||
import { useMySessions, TSessionListItem } from "@imphnen-frontend-service/service";
|
||||
|
||||
const TABS = {
|
||||
MENTORING: 'Mentoring',
|
||||
@@ -11,24 +12,6 @@ const TABS = {
|
||||
} as const
|
||||
type Tabs = typeof TABS[keyof typeof TABS]
|
||||
|
||||
type FeedbackStatus = 'done' | 'todo';
|
||||
|
||||
interface FeedbackType {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
rating: number
|
||||
status: FeedbackStatus
|
||||
}
|
||||
|
||||
const mockData: FeedbackType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
||||
email: 'fullname23@gmail.com',
|
||||
rating: 4.5,
|
||||
status: i % 2 === 0 ? 'done' : 'todo',
|
||||
}))
|
||||
|
||||
export default function Components(): ReactElement {
|
||||
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
|
||||
|
||||
@@ -38,7 +21,18 @@ export default function Components(): ReactElement {
|
||||
pageSize: 9,
|
||||
});
|
||||
|
||||
const columns: ColumnDef<FeedbackType>[] = [
|
||||
const { data: sessionsData, isLoading } = useMySessions(
|
||||
activeTab === TABS.MENTORING ? { status: 'completed' } : undefined
|
||||
);
|
||||
|
||||
const sessions: TSessionListItem[] = activeTab === TABS.MENTORING
|
||||
? (sessionsData?.sessions ?? [])
|
||||
: [];
|
||||
const totalItems = activeTab === TABS.MENTORING
|
||||
? (sessionsData?.total ?? sessions.length)
|
||||
: 0;
|
||||
|
||||
const columns: ColumnDef<TSessionListItem>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
meta: { cellClassName: cn("w-20") },
|
||||
@@ -62,37 +56,30 @@ export default function Components(): ReactElement {
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
accessorKey: 'mentee_fullname',
|
||||
cell: ({ row }) => <span>{row.original.mentee_fullname ?? '-'}</span>,
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
header: 'Email',
|
||||
accessorKey: 'email',
|
||||
accessorKey: 'mentee_email',
|
||||
cell: ({ row }) => <span>{row.original.mentee_email ?? '-'}</span>,
|
||||
},
|
||||
{
|
||||
id: 'rating',
|
||||
header: 'Rating',
|
||||
accessorKey: 'rating',
|
||||
cell: ({ row }) => <span>{row.original.rating ?? '-'}</span>,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const statusColors: Record<FeedbackStatus, string> = {
|
||||
done: 'bg-success-200 text-success-500',
|
||||
todo: 'bg-primary-200 text-primary-500',
|
||||
};
|
||||
const statusText: Record<FeedbackStatus, string> = {
|
||||
done: 'Done',
|
||||
todo: 'To Do',
|
||||
};
|
||||
const hasRating = !!row.original.rating;
|
||||
return (
|
||||
<div
|
||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||
>
|
||||
{statusText[status]}
|
||||
<div className={`py-2 px-4 rounded-md text-center ${hasRating ? 'bg-success-200 text-success-500' : 'bg-primary-200 text-primary-500'}`}>
|
||||
{hasRating ? 'Done' : 'To Do'}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -100,7 +87,7 @@ export default function Components(): ReactElement {
|
||||
{
|
||||
header: 'Action',
|
||||
meta: { cellClassName: cn("w-72") },
|
||||
cell: ({ row }) => (
|
||||
cell: () => (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@@ -116,7 +103,7 @@ export default function Components(): ReactElement {
|
||||
]
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockData,
|
||||
data: sessions,
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
@@ -127,8 +114,8 @@ export default function Components(): ReactElement {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -142,7 +129,10 @@ export default function Components(): ReactElement {
|
||||
key={tab}
|
||||
variant="text"
|
||||
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
onClick={() => {
|
||||
setActiveTab(tab);
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
}}
|
||||
>
|
||||
{tab}
|
||||
</Button>
|
||||
@@ -163,18 +153,26 @@ export default function Components(): ReactElement {
|
||||
</div>
|
||||
</div>
|
||||
<Select>
|
||||
<option selected disabled>Rating</option>
|
||||
<option disabled>Rating</option>
|
||||
<option value="4.5">4.5</option>
|
||||
<option value="5">5</option>
|
||||
</Select>
|
||||
<Select>
|
||||
<option selected disabled>Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option disabled>Status</option>
|
||||
<option value="done">Done</option>
|
||||
<option value="todo">To Do</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<DataTable data={mockData} columns={columns} table={table} />
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-neutral-400">Loading...</div>
|
||||
) : activeTab === TABS.PLATFORM ? (
|
||||
<div className="text-center py-8 text-neutral-400">
|
||||
Platform feedback tidak tersedia
|
||||
</div>
|
||||
) : (
|
||||
<DataTable data={sessions} columns={columns} table={table} />
|
||||
)}
|
||||
</section>
|
||||
</BackofficeWrapper>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ interface IModalAddItem {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const ModalAddItem = ({
|
||||
@@ -20,6 +21,7 @@ const ModalAddItem = ({
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
onDataCapture,
|
||||
}: IModalAddItem) => {
|
||||
return (
|
||||
<Modal
|
||||
@@ -31,7 +33,7 @@ const ModalAddItem = ({
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
@@ -46,10 +48,11 @@ const ModalAddItem = ({
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep);
|
||||
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
interface IModalDeleteItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleDeleteItem?: () => void;
|
||||
handleDeleteItem?: () => Promise<void>;
|
||||
}
|
||||
|
||||
const ModalDeleteItem = ({
|
||||
@@ -48,8 +48,9 @@ const ModalDeleteItem = ({
|
||||
variant="danger"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
handleDeleteItem && handleDeleteItem();
|
||||
onClick={async () => {
|
||||
if (handleDeleteItem) await handleDeleteItem();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Hapus Item
|
||||
|
||||
@@ -11,6 +11,8 @@ interface IModalUpdateItem {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
initialValues?: { itemName?: string; quantity?: number; chanceRate?: number };
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const ModalUpdateItem = ({
|
||||
@@ -20,6 +22,8 @@ const ModalUpdateItem = ({
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleUpdateItem,
|
||||
initialValues,
|
||||
onDataCapture,
|
||||
}: IModalUpdateItem) => {
|
||||
return (
|
||||
<Modal
|
||||
@@ -31,7 +35,7 @@ const ModalUpdateItem = ({
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} initialValues={initialValues} onDataCapture={onDataCapture} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
@@ -46,16 +50,12 @@ const ModalUpdateItem = ({
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
initialValues?: { itemName?: string; quantity?: number; chanceRate?: number };
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const initialValues = {
|
||||
itemName: 'Hoodie IMPHNEN Official 2025',
|
||||
quantity: 10,
|
||||
chanceRate: 0.1,
|
||||
};
|
||||
|
||||
const { form, onSubmit } = useItem(nextStep, initialValues);
|
||||
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep, initialValues as any, onDataCapture);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -105,7 +105,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
type="submit"
|
||||
>
|
||||
Perbarui Item
|
||||
</Button>
|
||||
|
||||
@@ -8,16 +8,17 @@ import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
nextStep: () => void,
|
||||
initialValues?: TGachaRollItem
|
||||
initialValues?: Partial<TGachaRollItem>,
|
||||
onDataCapture?: (data: any) => void,
|
||||
) => {
|
||||
const form = useForm<TGachaRollItem>({
|
||||
const form = useForm<any>({
|
||||
resolver: zodResolver(gachaRollItemSchema),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log('Form data:', data);
|
||||
onDataCapture?.(data);
|
||||
nextStep();
|
||||
});
|
||||
|
||||
@@ -38,6 +39,9 @@ export const useConfirmItem = (
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
if (actionFunction) {
|
||||
await actionFunction();
|
||||
}
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
|
||||
import {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
@@ -22,25 +22,21 @@ import ModalAddItem from './_components/modal-add-item';
|
||||
import ModalUpdateItem from './_components/modal-update-item';
|
||||
import ModalDeleteItem from './_components/modal-delete-item';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
|
||||
interface GachaItem {
|
||||
id: number;
|
||||
name: string;
|
||||
chanceRate: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
const mockData: GachaItem[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: 'Hoodie IMPHNEN Official 2025',
|
||||
chanceRate: 0.1,
|
||||
quantity: 10,
|
||||
}));
|
||||
import {
|
||||
useGachaItemList,
|
||||
useCreateGachaItem,
|
||||
useUpdateGachaItem,
|
||||
useDeleteGachaItem,
|
||||
TGachaItemDto,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const pendingFormData = useRef<any>(null);
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
@@ -60,7 +56,60 @@ export const Components: FC = (): ReactElement => {
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
|
||||
const columns: ColumnDef<GachaItem>[] = [
|
||||
const { data: itemsData, isLoading } = useGachaItemList({
|
||||
search,
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
});
|
||||
const createItem = useCreateGachaItem();
|
||||
const updateItem = useUpdateGachaItem();
|
||||
const deleteItem = useDeleteGachaItem();
|
||||
|
||||
const items: TGachaItemDto[] = itemsData?.data ?? [];
|
||||
const totalItems = itemsData?.meta?.total ?? items.length;
|
||||
|
||||
const handleAdd = async (): Promise<boolean> => {
|
||||
if (pendingFormData.current) {
|
||||
const { itemName, quantity, chanceRate } = pendingFormData.current;
|
||||
await createItem.mutateAsync({
|
||||
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
|
||||
name: itemName,
|
||||
description: '',
|
||||
rarity: 'common',
|
||||
type_: 'physical',
|
||||
category: 'merchandise',
|
||||
value: 0,
|
||||
weight: chanceRate ?? 1,
|
||||
stock: quantity ?? 1,
|
||||
is_limited: false,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleUpdate = async (): Promise<boolean> => {
|
||||
if (selectedItem && pendingFormData.current) {
|
||||
const { itemName, quantity, chanceRate } = pendingFormData.current;
|
||||
await updateItem.mutateAsync({
|
||||
id: selectedItem.id,
|
||||
data: {
|
||||
name: itemName,
|
||||
weight: chanceRate,
|
||||
stock: quantity,
|
||||
},
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedItem) {
|
||||
await deleteItem.mutateAsync(selectedItem.id);
|
||||
}
|
||||
setShowModalDeleteItem(false);
|
||||
};
|
||||
|
||||
const columns: ColumnDef<TGachaItemDto>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
@@ -88,23 +137,16 @@ export const Components: FC = (): ReactElement => {
|
||||
header: 'Nama Item',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'Chance Rate',
|
||||
accessorKey: 'chanceRate',
|
||||
},
|
||||
{
|
||||
header: 'Quantity',
|
||||
accessorKey: 'quantity',
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
cell: () => (
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-[8px]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedItem(row.original);
|
||||
setShowModalUpdateItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
@@ -116,6 +158,7 @@ export const Components: FC = (): ReactElement => {
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedItem(row.original);
|
||||
setShowModalDeleteItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
@@ -128,7 +171,7 @@ export const Components: FC = (): ReactElement => {
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockData,
|
||||
data: items,
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
@@ -139,8 +182,8 @@ export const Components: FC = (): ReactElement => {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -156,6 +199,8 @@ export const Components: FC = (): ReactElement => {
|
||||
<Input
|
||||
placeholder="Cari berdasarkan nama item"
|
||||
className="pl-12 w-full max-h-full"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||
<SearchOutlined />
|
||||
@@ -166,9 +211,7 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="primary"
|
||||
size="md"
|
||||
className="flex gap-3 text-nowrap"
|
||||
onClick={() => {
|
||||
setShowModalAddItem(true);
|
||||
}}
|
||||
onClick={() => setShowModalAddItem(true)}
|
||||
>
|
||||
<PlusOutlined />
|
||||
Tambah Item
|
||||
@@ -176,7 +219,11 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable data={mockData} columns={columns} table={table} />
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-neutral-400">Loading...</div>
|
||||
) : (
|
||||
<DataTable data={items} columns={columns} table={table} />
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -187,6 +234,8 @@ export const Components: FC = (): ReactElement => {
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleAddItem={handleAdd}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
/>
|
||||
<ModalUpdateItem
|
||||
currentStep={currentStep}
|
||||
@@ -195,13 +244,14 @@ export const Components: FC = (): ReactElement => {
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleUpdateItem={handleUpdate}
|
||||
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
/>
|
||||
<ModalDeleteItem
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
handleDeleteItem={() => {
|
||||
console.log('Item deleted');
|
||||
}}
|
||||
handleDeleteItem={handleDelete}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
+6
-3
@@ -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 (
|
||||
<>
|
||||
|
||||
+50
-22
@@ -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} />
|
||||
|
||||
+34
-21
@@ -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
|
||||
|
||||
+59
-5
@@ -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>
|
||||
|
||||
+42
-5
@@ -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>
|
||||
|
||||
+15
-12
@@ -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>
|
||||
|
||||
+13
-9
@@ -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!
|
||||
|
||||
+12
-7
@@ -1,19 +1,24 @@
|
||||
import { StarFilled } from "@ant-design/icons";
|
||||
import { For } from "@imphnen-frontend-service/utils";
|
||||
import { FC } from "react";
|
||||
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service";
|
||||
|
||||
const SENPAI_STATISTIC = [
|
||||
{ name: 'Total Sessions', count: 8 },
|
||||
{ name: 'Mentee Impact', count: 1000 },
|
||||
{ name: 'Response Time', count: '30 Minute' }
|
||||
]
|
||||
type Props = {
|
||||
mentor?: MentorDetailResponseDto
|
||||
}
|
||||
|
||||
export const StatisticsSection: FC<Props> = ({ mentor }) => {
|
||||
const stats = [
|
||||
{ name: 'Total Sessions', count: mentor?.mentoring_sessions ?? 0 },
|
||||
{ name: 'Rating', count: mentor?.rating != null ? `${mentor.rating.toFixed(1)}/5.0` : 'N/A' },
|
||||
{ name: 'Experience', count: mentor?.years_of_experience != null ? `${mentor.years_of_experience} Yrs` : 'N/A' },
|
||||
]
|
||||
|
||||
export const StatisticsSection: FC = () => {
|
||||
return (
|
||||
<div className="md:mb-10">
|
||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Statistics</h2>
|
||||
<div className="grid gap-4 xl:flex">
|
||||
<For data={SENPAI_STATISTIC}>
|
||||
<For data={stats}>
|
||||
{(item, index) => (
|
||||
<div key={index} className="px-2.5 py-2 border border-primary-50 rounded-md shadow flex items-center gap-x-2.5">
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { For } from "@imphnen-frontend-service/utils"
|
||||
import { FC } from "react"
|
||||
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
|
||||
|
||||
export const TOPICS = [
|
||||
{ id: 1, icon: '💼', name: 'Career & Self Development' },
|
||||
@@ -11,22 +12,41 @@ export const TOPICS = [
|
||||
{ id: 7, icon: <span className="font-bold text-primary-500">AI</span>, name: 'AI Tips' }
|
||||
]
|
||||
|
||||
export const TopicsSection: FC = () => {
|
||||
type Props = {
|
||||
mentor?: MentorDetailResponseDto
|
||||
}
|
||||
|
||||
export const TopicsSection: FC<Props> = ({ mentor }) => {
|
||||
const mentorTopics = mentor?.topics_of_interest ?? []
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Topics</h2>
|
||||
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5">
|
||||
<For data={TOPICS}>
|
||||
{(item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
|
||||
>
|
||||
<span>{item.icon} </span>
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
{mentorTopics.length > 0 ? (
|
||||
<For data={mentorTopics}>
|
||||
{(topic, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
|
||||
>
|
||||
{topic}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
) : (
|
||||
<For data={TOPICS}>
|
||||
{(item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
|
||||
>
|
||||
<span>{item.icon} </span>
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,20 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { MentorDetailResponseDto } from '@imphnen-frontend-service/service';
|
||||
|
||||
interface MentorCardProps {
|
||||
mentor: MentorDetailResponseDto;
|
||||
}
|
||||
|
||||
export const MentorCard: FC<MentorCardProps> = ({ mentor }) => {
|
||||
const expertise = mentor.expertise ?? [];
|
||||
const firstSkill = expertise[0];
|
||||
const secondSkill = expertise[1];
|
||||
const extraCount = expertise.length - 2;
|
||||
const yearsExp = mentor.years_of_experience ?? 0;
|
||||
const expLabel = `${yearsExp}+ Years Experience`;
|
||||
|
||||
export const MentorCard: FC = () => {
|
||||
return (
|
||||
<div className="p-2.5 rounded-md bg-white shadow flex gap-x-4 items-start md:p-4 md:flex-col md:rounded-lg md:gap-y-4">
|
||||
<div className="size-[60px] rounded-md overflow-hidden md:w-full md:h-auto md:aspect-square">
|
||||
@@ -10,32 +22,38 @@ export const MentorCard: FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="text" size="sm" className="bg-primary-100 h-auto px-1.5 py-1 text-[8px] font-normal mb-2 hover:bg-primary-100 md:text-[10px] md:font-medium md:mb-4">
|
||||
3-5 Years Experience
|
||||
{expLabel}
|
||||
</Button>
|
||||
<h2 className="mb-1">
|
||||
<Link to="/mentoring/detail" className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
|
||||
Fullname
|
||||
<Link to={`/mentoring/${mentor.id}`} className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
|
||||
{mentor.fullname || 'Unknown Mentor'}
|
||||
</Link>
|
||||
</h2>
|
||||
<p className="text-[8px] text-neutral-500 mb-3 md:text-[10px] md:font-medium md:mb-4 lg:text-xs">
|
||||
Full Stack Enjoyer at name company
|
||||
{mentor.current_role} at {mentor.current_company}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Soft Skills :</p>
|
||||
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Expertise :</p>
|
||||
<div className="space-x-2">
|
||||
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]">
|
||||
Communication
|
||||
</Button>
|
||||
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hidden hover:bg-primary-200 md:inline-block lg:text-[10px]">
|
||||
Communication
|
||||
</Button>
|
||||
<Button variant="text" size="sm" className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]">
|
||||
+2
|
||||
</Button>
|
||||
{firstSkill && (
|
||||
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]">
|
||||
{firstSkill}
|
||||
</Button>
|
||||
)}
|
||||
{secondSkill && (
|
||||
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hidden hover:bg-primary-200 md:inline-block lg:text-[10px]">
|
||||
{secondSkill}
|
||||
</Button>
|
||||
)}
|
||||
{extraCount > 0 && (
|
||||
<Button variant="text" size="sm" className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]">
|
||||
+{extraCount}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,27 +8,35 @@ import { MentorCard } from './_components/mentor-card';
|
||||
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
|
||||
import { motion, useInView, Variants } from 'framer-motion';
|
||||
|
||||
const TEMP_DATA = [
|
||||
{ id: 1, name: 'John Doe' },
|
||||
{ id: 2, name: 'John Doe' },
|
||||
{ id: 3, name: 'John Doe' },
|
||||
{ id: 4, name: 'John Doe' },
|
||||
]
|
||||
import { useMentorList } from '@imphnen-frontend-service/service';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 3,
|
||||
pageSize: 8,
|
||||
});
|
||||
|
||||
const { data: mentorData, isLoading } = useMentorList({
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
search: search || undefined,
|
||||
});
|
||||
|
||||
const mentors = mentorData?.data ?? [];
|
||||
const totalItems = mentorData?.meta?.total ?? 0;
|
||||
|
||||
const table = useReactTable({
|
||||
data: TEMP_DATA,
|
||||
data: mentors,
|
||||
columns: [],
|
||||
state: { pagination },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: (updater) => {
|
||||
setPagination(updater);
|
||||
},
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize) || 1,
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
const ref = useRef(null)
|
||||
@@ -91,24 +99,33 @@ export const Components: FC = (): ReactElement => {
|
||||
<Input
|
||||
placeholder="Cari berdasarkan nama, posisi/peran"
|
||||
className="relative min-w-full w-full"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
}}
|
||||
/>
|
||||
<SearchOutlined className="absolute right-2.5 top-1/2 -translate-y-1/2 text-primary-500 size-2.5 cursor-text md:me-12 lg:me-0" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
<For data={Array.from({ length: 8 })}>
|
||||
{(_, index) => (
|
||||
<motion.div key={index} variants={childVariants}>
|
||||
<MentorCard />
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</motion.div>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12 text-neutral-400">Loading mentors...</div>
|
||||
) : (
|
||||
<motion.div
|
||||
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
<For data={mentors}>
|
||||
{(mentor, index) => (
|
||||
<motion.div key={mentor.id ?? index} variants={childVariants}>
|
||||
<MentorCard mentor={mentor} />
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<Pagination table={table} />
|
||||
</motion.div>
|
||||
|
||||
+62
-19
@@ -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>
|
||||
|
||||
@@ -5,23 +5,28 @@ import type {
|
||||
TAdminSubmissionsResponse,
|
||||
} from '../../types/admin';
|
||||
|
||||
const ADMIN_BASE_URL = '/admin';
|
||||
|
||||
export const getAdminUsers = async (params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
is_admin?: boolean;
|
||||
}) => {
|
||||
const response = await api.get<TAdminUsersResponse>(
|
||||
`${ADMIN_BASE_URL}/users`,
|
||||
{
|
||||
params: {
|
||||
...params,
|
||||
is_admin: params?.is_admin ?? false,
|
||||
},
|
||||
}
|
||||
);
|
||||
const response = await api.get<TAdminUsersResponse>('/v1/hackathon/admin/users', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getAdminUserById = async (userId: string) => {
|
||||
const response = await api.get(`/v1/hackathon/admin/users/${userId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteAdminUser = async (userId: string) => {
|
||||
const response = await api.delete(`/v1/hackathon/admin/users/${userId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const setAdminUser = async (userId: string, is_admin: boolean) => {
|
||||
const response = await api.post(`/v1/hackathon/admin/users/${userId}/set-admin`, { is_admin });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -30,10 +35,12 @@ export const getAdminTeams = async (params?: {
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
}) => {
|
||||
const response = await api.get<TAdminTeamsResponse>(
|
||||
`${ADMIN_BASE_URL}/teams`,
|
||||
{ params }
|
||||
);
|
||||
const response = await api.get<TAdminTeamsResponse>('/v1/hackathon/admin/teams', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteAdminTeam = async (teamId: string) => {
|
||||
const response = await api.delete(`/v1/hackathon/admin/teams/${teamId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -43,9 +50,21 @@ export const getAdminSubmissions = async (params?: {
|
||||
search?: string;
|
||||
status?: string;
|
||||
}) => {
|
||||
const response = await api.get<TAdminSubmissionsResponse>(
|
||||
`${ADMIN_BASE_URL}/submissions`,
|
||||
{ params }
|
||||
);
|
||||
const response = await api.get<TAdminSubmissionsResponse>('/v1/hackathon/admin/submissions', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getAdminWinners = async () => {
|
||||
const response = await api.get('/v1/hackathon/admin/winners');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const setWinner = async (data: { team_id: string; rank: number; prize?: string }) => {
|
||||
const response = await api.post('/v1/hackathon/admin/winners', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const removeWinner = async (teamId: string) => {
|
||||
const response = await api.delete(`/v1/hackathon/admin/winners/${teamId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -1,80 +1,65 @@
|
||||
import { api, getBaseURL } from '../';
|
||||
import {
|
||||
TLoginRequest,
|
||||
TLoginResponse,
|
||||
TRegisterRequest,
|
||||
TSendOTPRequest,
|
||||
TVerifyEmailRequest,
|
||||
TGoogleCallbackResponse,
|
||||
} from '../../types/auth';
|
||||
import { TResponseMessage } from '../../types/common';
|
||||
import { api } from '../index';
|
||||
import type { TLoginResponse, TRegisterRequest, TSendOTPRequest, TVerifyEmailRequest } from '../../types/auth';
|
||||
import type { TResponseMessage } from '../../types/common';
|
||||
|
||||
export const postLogin = async (
|
||||
payload: TLoginRequest
|
||||
): Promise<TLoginResponse> => {
|
||||
export type TLoginRequest = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type TForgotPasswordRequest = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type TNewPasswordRequest = {
|
||||
token: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type TRefreshTokenRequest = {
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
export const postLogin = async (payload: TLoginRequest): Promise<TLoginResponse> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/login', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postLoginMentor = async (payload: TLoginRequest): Promise<TLoginResponse> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/login-mentor', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postRegister = async (payload: TRegisterRequest): Promise<TResponseMessage> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/register', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postVerifyEmail = async (payload: TVerifyEmailRequest): Promise<TResponseMessage> => {
|
||||
const { data } = await api({
|
||||
method: 'POST',
|
||||
url: '/auth/login',
|
||||
data: payload,
|
||||
url: '/v1/iam/auth/verify-email',
|
||||
data: { otp: parseInt(payload.otp), email: payload.email },
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postRegister = async (
|
||||
payload: TRegisterRequest
|
||||
): Promise<TResponseMessage> => {
|
||||
const { data } = await api({
|
||||
method: 'POST',
|
||||
url: '/auth/register',
|
||||
data: payload,
|
||||
});
|
||||
export const postSendOtp = async (payload: TSendOTPRequest): Promise<TResponseMessage> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/send-otp', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postVerifyEmail = async (
|
||||
payload: TVerifyEmailRequest
|
||||
): Promise<TResponseMessage> => {
|
||||
const { data } = await api({
|
||||
method: 'POST',
|
||||
url: '/auth/verify-email',
|
||||
data: {otp: parseInt(payload.otp), email: payload.email},
|
||||
});
|
||||
export const postForgotPassword = async (payload: TForgotPasswordRequest): Promise<TResponseMessage> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/forgot', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postSendOtp = async (
|
||||
payload: TSendOTPRequest
|
||||
): Promise<TResponseMessage> => {
|
||||
const { data } = await api({
|
||||
method: 'POST',
|
||||
url: '/auth/send-otp',
|
||||
data: payload,
|
||||
});
|
||||
export const postNewPassword = async (payload: TNewPasswordRequest): Promise<TResponseMessage> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/new-password', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getGoogleAuthUrl = async (): Promise<string> => {
|
||||
const baseUrl = getBaseURL() || 'http://localhost:8080';
|
||||
return `${baseUrl}/auth/google/login`;
|
||||
};
|
||||
|
||||
export const postGoogleCallback = async (code: string, state: string): Promise<TGoogleCallbackResponse> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: `/auth/google/callback?code=${code}&state=${state}`,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getGitHubAuthUrl = async (): Promise<string> => {
|
||||
const baseUrl = getBaseURL() || 'http://localhost:8080';
|
||||
return `${baseUrl}/auth/github/login`;
|
||||
};
|
||||
|
||||
export const postGitHubCallback = async (code: string, state: string): Promise<TGoogleCallbackResponse> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: `/auth/github/callback?code=${code}&state=${state}`,
|
||||
});
|
||||
export const postRefreshToken = async (payload: TRefreshTokenRequest): Promise<{ access_token: string; refresh_token: string }> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/refresh', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -1,52 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import { useAuthStore } from '../hooks/auth';
|
||||
|
||||
const BACKOFFICE_API_URL = 'https://api.hackathon.imphnen.dev/api/v1';
|
||||
|
||||
export const backofficeApi = axios.create({
|
||||
baseURL: BACKOFFICE_API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
backofficeApi.interceptors.request.use(
|
||||
(config) => {
|
||||
const { session } = useAuthStore.getState();
|
||||
if (session?.token?.access_token) {
|
||||
config.headers.Authorization = `Bearer ${session.token.access_token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(new Error(error.message || 'Request failed'));
|
||||
}
|
||||
);
|
||||
|
||||
backofficeApi.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
const isAuthPage =
|
||||
globalThis.window !== undefined &&
|
||||
globalThis.location.pathname.startsWith('/auth');
|
||||
|
||||
if (!isAuthPage) {
|
||||
useAuthStore.getState().clearSession();
|
||||
if (globalThis.window !== undefined) {
|
||||
globalThis.location.href = '/auth/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const backendMsg = error?.response?.data?.message;
|
||||
if (backendMsg && typeof backendMsg === 'string') {
|
||||
return Promise.reject(new Error(backendMsg));
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(error.message || 'An error occurred'));
|
||||
}
|
||||
);
|
||||
// All API calls now go through the main `api` instance (api.imphnen.dev).
|
||||
// This file re-exports `api` as `backofficeApi` for backward compatibility.
|
||||
export { api as backofficeApi } from './index';
|
||||
|
||||
export interface BackofficeApiResponse<T> {
|
||||
data: T;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type { TEventsListItem, TEventsDetailItem, TEventCreateRequest, TEventUpdateRequest } from '../../types/events';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
export const getEventList = async (params?: TPaginationParams): Promise<TApiPaginated<TEventsListItem>> => {
|
||||
const response = await api.get<TApiPaginated<TEventsListItem>>('/v1/landing/cms/events', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getEventById = async (id: string): Promise<TEventsDetailItem> => {
|
||||
const response = await api.get<ApiResponse<TEventsDetailItem>>(`/v1/landing/cms/events/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createEvent = async (data: TEventCreateRequest): Promise<TEventsDetailItem> => {
|
||||
const response = await api.post<ApiResponse<TEventsDetailItem>>('/v1/landing/cms/events/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateEvent = async (id: string, data: TEventUpdateRequest): Promise<TEventsDetailItem> => {
|
||||
const response = await api.patch<ApiResponse<TEventsDetailItem>>(`/v1/landing/cms/events/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deleteEvent = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/landing/cms/events/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
@@ -1 +1,87 @@
|
||||
export {};
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type {
|
||||
TGachaItemDto,
|
||||
TGachaItemCreateRequest,
|
||||
TGachaItemUpdateRequest,
|
||||
TGachaRollItemDto,
|
||||
TGachaRollCreateRequest,
|
||||
TGachaCreditDto,
|
||||
TGachaCreditAddRequest,
|
||||
TGachaClaimDetailDto,
|
||||
TGachaClaimCreateRequest,
|
||||
} from '../../types/gacha';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
// ----- Credits -----
|
||||
export const getUserCredits = async (): Promise<TGachaCreditDto> => {
|
||||
const response = await api.get<ApiResponse<TGachaCreditDto>>('/v1/gacha/credits/');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const addCredits = async (data: TGachaCreditAddRequest): Promise<TGachaCreditDto> => {
|
||||
const response = await api.post<ApiResponse<TGachaCreditDto>>('/v1/gacha/credits/add', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const consumeCredit = async (): Promise<{ message: string }> => {
|
||||
const response = await api.post<{ message: string }>('/v1/gacha/credits/consume');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ----- Items -----
|
||||
export const getGachaItemList = async (params?: TPaginationParams): Promise<TApiPaginated<TGachaItemDto>> => {
|
||||
const response = await api.get<TApiPaginated<TGachaItemDto>>('/v1/gacha/items/', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getGachaItemById = async (id: string): Promise<TGachaItemDto> => {
|
||||
const response = await api.get<ApiResponse<TGachaItemDto>>(`/v1/gacha/items/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createGachaItem = async (data: TGachaItemCreateRequest): Promise<TGachaItemDto> => {
|
||||
const response = await api.post<ApiResponse<TGachaItemDto>>('/v1/gacha/items/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateGachaItem = async (id: string, data: TGachaItemUpdateRequest): Promise<TGachaItemDto> => {
|
||||
const response = await api.put<ApiResponse<TGachaItemDto>>(`/v1/gacha/items/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deleteGachaItem = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/gacha/items/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ----- Rolls -----
|
||||
export const getGachaRollById = async (id: string): Promise<TGachaRollItemDto> => {
|
||||
const response = await api.get<ApiResponse<TGachaRollItemDto>>(`/v1/gacha/rolls/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createGachaRoll = async (data: TGachaRollCreateRequest): Promise<TGachaRollItemDto> => {
|
||||
const response = await api.post<ApiResponse<TGachaRollItemDto>>('/v1/gacha/rolls/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const executeGachaRoll = async (): Promise<TGachaRollItemDto> => {
|
||||
const response = await api.post<ApiResponse<TGachaRollItemDto>>('/v1/gacha/rolls/execute');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deleteGachaRoll = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/gacha/rolls/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ----- Claims -----
|
||||
export const getGachaClaimById = async (id: string): Promise<TGachaClaimDetailDto> => {
|
||||
const response = await api.get<ApiResponse<TGachaClaimDetailDto>>(`/v1/gacha/claims/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createGachaClaim = async (data: TGachaClaimCreateRequest): Promise<TGachaClaimDetailDto> => {
|
||||
const response = await api.post<ApiResponse<TGachaClaimDetailDto>>('/v1/gacha/claims/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
@@ -1,51 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import { useAuthStore } from '../hooks/auth';
|
||||
|
||||
const HACKATHON_API_URL = 'https://api.hackathon.imphnen.dev/api/v1';
|
||||
|
||||
export const hackathonApi = axios.create({
|
||||
baseURL: HACKATHON_API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
hackathonApi.interceptors.request.use(
|
||||
(config) => {
|
||||
const { session } = useAuthStore.getState();
|
||||
if (session?.token?.access_token) {
|
||||
config.headers.Authorization = `Bearer ${session.token.access_token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(new Error(error.message || 'Request failed'));
|
||||
}
|
||||
);
|
||||
|
||||
hackathonApi.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
const isAuthPage = globalThis.window !== undefined && globalThis.location.pathname.startsWith('/auth');
|
||||
const isCertificatePage = globalThis.window !== undefined && globalThis.location.pathname.startsWith('/certificate/');
|
||||
|
||||
if (!isAuthPage && !isCertificatePage) {
|
||||
useAuthStore.getState().clearSession();
|
||||
if (globalThis.window !== undefined) {
|
||||
globalThis.location.href = '/auth/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const backendMsg = error?.response?.data?.message;
|
||||
if (backendMsg && typeof backendMsg === 'string') {
|
||||
return Promise.reject(new Error(backendMsg));
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(error.message || 'Request failed'));
|
||||
}
|
||||
);
|
||||
// All API calls now go through the main `api` instance (api.imphnen.dev).
|
||||
// This file re-exports `api` as `hackathonApi` for backward compatibility.
|
||||
export { api as hackathonApi } from './index';
|
||||
|
||||
export interface HackathonApiResponse<T> {
|
||||
data: T;
|
||||
|
||||
@@ -7,6 +7,11 @@ export * from './mentors';
|
||||
export * from './upload';
|
||||
export * from './hackathon';
|
||||
export * from './admin';
|
||||
export * from './roles';
|
||||
export * from './permissions';
|
||||
export * from './events';
|
||||
export * from './testimonials';
|
||||
export * from './sessions';
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
@@ -140,7 +145,7 @@ async function handleTokenRefresh(originalRequest: AxiosRequestConfig) {
|
||||
}
|
||||
|
||||
async function refreshAccessToken(refreshToken: string) {
|
||||
return axios.post(`${getBaseURL()}/auth/refresh`, {
|
||||
return axios.post(`${getBaseURL()}/v1/iam/auth/refresh`, {
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,34 +1,61 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type {
|
||||
MentorDetailResponseDto,
|
||||
MentorUpdateRequestDto
|
||||
} from '../../types/mentors';
|
||||
import type { MentorDetailResponseDto, MentorUpdateRequestDto } from '../../types/mentors';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
export interface MentorService {
|
||||
getMentorMe(): Promise<MentorDetailResponseDto>;
|
||||
getMentorById(id: string): Promise<MentorDetailResponseDto>;
|
||||
updateMentorMe(data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto>;
|
||||
updateMentorById(id: string, data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto>;
|
||||
}
|
||||
|
||||
export const mentorService: MentorService = {
|
||||
async getMentorMe() {
|
||||
const response = await api.get<ApiResponse<MentorDetailResponseDto>>('/mentors/me');
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async getMentorById(id: string) {
|
||||
const response = await api.get<ApiResponse<MentorDetailResponseDto>>(`/mentors/detail/${id}`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async updateMentorMe(data: MentorUpdateRequestDto) {
|
||||
const response = await api.put<ApiResponse<MentorDetailResponseDto>>('/mentors/update/me', data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async updateMentorById(id: string, data: MentorUpdateRequestDto) {
|
||||
const response = await api.put<ApiResponse<MentorDetailResponseDto>>(`/mentors/update/${id}`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
export type MentorRegisterRequest = MentorUpdateRequestDto & {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export const registerMentor = async (data: MentorRegisterRequest): Promise<MentorDetailResponseDto> => {
|
||||
const response = await api.post<ApiResponse<MentorDetailResponseDto>>('/v1/dimentorin/mentors/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getMentorList = async (params?: TPaginationParams): Promise<TApiPaginated<MentorDetailResponseDto>> => {
|
||||
const response = await api.get<TApiPaginated<MentorDetailResponseDto>>('/v1/dimentorin/mentors', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getMentorMe = async (): Promise<MentorDetailResponseDto> => {
|
||||
const response = await api.get<ApiResponse<MentorDetailResponseDto>>('/v1/dimentorin/mentors/me');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getMentorStatus = async (): Promise<{ status: string }> => {
|
||||
const response = await api.get<ApiResponse<{ status: string }>>('/v1/dimentorin/mentors/me/status');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getMentorById = async (id: string): Promise<MentorDetailResponseDto> => {
|
||||
const response = await api.get<ApiResponse<MentorDetailResponseDto>>(`/v1/dimentorin/mentors/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateMentorMe = async (data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto> => {
|
||||
const response = await api.put<ApiResponse<MentorDetailResponseDto>>('/v1/dimentorin/mentors/me/update', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateMentorById = async (id: string, data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto> => {
|
||||
const response = await api.put<ApiResponse<MentorDetailResponseDto>>(`/v1/dimentorin/mentors/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deleteMentor = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/dimentorin/mentors/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const verifyMentor = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.put<{ message: string }>(`/v1/dimentorin/mentors/verify/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Legacy service object for backward compatibility
|
||||
export const mentorService = {
|
||||
getMentorMe,
|
||||
getMentorById,
|
||||
updateMentorMe,
|
||||
updateMentorById: (id: string, data: MentorUpdateRequestDto) => updateMentorById(id, data),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type { TPermissionItem, TPermissionCreateRequest, TPermissionUpdateRequest } from '../../types/permissions';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
export const getPermissionList = async (params?: TPaginationParams): Promise<TApiPaginated<TPermissionItem>> => {
|
||||
const response = await api.get<TApiPaginated<TPermissionItem>>('/v1/iam/permissions', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getPermissionById = async (id: string): Promise<TPermissionItem> => {
|
||||
const response = await api.get<ApiResponse<TPermissionItem>>(`/v1/iam/permissions/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createPermission = async (data: TPermissionCreateRequest): Promise<TPermissionItem> => {
|
||||
const response = await api.post<ApiResponse<TPermissionItem>>('/v1/iam/permissions/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updatePermission = async (id: string, data: TPermissionUpdateRequest): Promise<TPermissionItem> => {
|
||||
const response = await api.put<ApiResponse<TPermissionItem>>(`/v1/iam/permissions/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deletePermission = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/iam/permissions/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type { TRolesListItem, TRoleDetailItem, TRoleCreateRequest, TRoleUpdateRequest } from '../../types/roles';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
export const getRoleList = async (params?: TPaginationParams): Promise<TApiPaginated<TRolesListItem>> => {
|
||||
const response = await api.get<TApiPaginated<TRolesListItem>>('/v1/iam/roles', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getRoleById = async (id: string): Promise<TRoleDetailItem> => {
|
||||
const response = await api.get<ApiResponse<TRoleDetailItem>>(`/v1/iam/roles/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createRole = async (data: TRoleCreateRequest): Promise<TRoleDetailItem> => {
|
||||
const response = await api.post<ApiResponse<TRoleDetailItem>>('/v1/iam/roles/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateRole = async (id: string, data: TRoleUpdateRequest): Promise<TRoleDetailItem> => {
|
||||
const response = await api.put<ApiResponse<TRoleDetailItem>>(`/v1/iam/roles/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deleteRole = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/iam/roles/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type {
|
||||
TBookSessionRequest,
|
||||
TBookSessionResponse,
|
||||
TUpdateSessionStatusRequest,
|
||||
TUpdateSessionStatusResponse,
|
||||
TSessionFeedbackRequest,
|
||||
TSessionFeedbackResponse,
|
||||
TSessionListResponse,
|
||||
TMentorAvailability,
|
||||
} from '../../types/sessions';
|
||||
|
||||
export const getMentorAvailability = async (mentorId: string): Promise<TMentorAvailability> => {
|
||||
const response = await api.get<ApiResponse<TMentorAvailability>>(
|
||||
`/v1/dimentorin/mentors/${mentorId}/availability`
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const bookSession = async (mentorId: string, data: TBookSessionRequest): Promise<TBookSessionResponse> => {
|
||||
const response = await api.post<ApiResponse<TBookSessionResponse>>(
|
||||
`/v1/dimentorin/mentors/${mentorId}/sessions/create`,
|
||||
data
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getMentorSessions = async (mentorId: string, params?: { status?: string }): Promise<TSessionListResponse> => {
|
||||
const response = await api.get<ApiResponse<TSessionListResponse>>(
|
||||
`/v1/dimentorin/mentors/${mentorId}/sessions`,
|
||||
{ params }
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getMySessions = async (params?: { status?: string }): Promise<TSessionListResponse> => {
|
||||
const response = await api.get<ApiResponse<TSessionListResponse>>(
|
||||
'/v1/dimentorin/sessions/me',
|
||||
{ params }
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateSessionStatus = async (
|
||||
id: string,
|
||||
data: TUpdateSessionStatusRequest
|
||||
): Promise<TUpdateSessionStatusResponse> => {
|
||||
const response = await api.put<ApiResponse<TUpdateSessionStatusResponse>>(
|
||||
`/v1/dimentorin/sessions/update/${id}/status`,
|
||||
data
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const submitFeedback = async (
|
||||
id: string,
|
||||
data: TSessionFeedbackRequest
|
||||
): Promise<TSessionFeedbackResponse> => {
|
||||
const response = await api.post<ApiResponse<TSessionFeedbackResponse>>(
|
||||
`/v1/dimentorin/sessions/${id}/feedback/create`,
|
||||
data
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
TUpdateTeamRequest,
|
||||
TInviteMemberRequest,
|
||||
TJoinTeamRequest,
|
||||
TManageMemberRequest,
|
||||
TSubmitProjectRequest,
|
||||
TTeamListResponse,
|
||||
TTeamDetailResponse,
|
||||
@@ -14,8 +13,6 @@ import type {
|
||||
TProjectSubmissionResponse,
|
||||
} from '../../types/teams';
|
||||
|
||||
const TEAMS_BASE_URL = '/teams';
|
||||
|
||||
export const getTeams = async (params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
@@ -23,86 +20,93 @@ export const getTeams = async (params?: {
|
||||
visibility?: string;
|
||||
search?: string;
|
||||
}) => {
|
||||
const response = await api.get<TTeamListResponse>(TEAMS_BASE_URL, { params });
|
||||
const response = await api.get<TTeamListResponse>('/v1/hackathon/teams/browse', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTeamById = async (teamId: string) => {
|
||||
const response = await api.get<TTeamDetailResponse>(`${TEAMS_BASE_URL}/${teamId}`);
|
||||
const response = await api.get<TTeamDetailResponse>(`/v1/hackathon/teams/${teamId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const createTeam = async (data: TCreateTeamRequest) => {
|
||||
const response = await api.post<TTeamDetailResponse>(TEAMS_BASE_URL, data);
|
||||
const response = await api.post<TTeamDetailResponse>('/v1/hackathon/teams', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateTeam = async (teamId: string, data: TUpdateTeamRequest) => {
|
||||
const response = await api.put<TTeamDetailResponse>(`${TEAMS_BASE_URL}/${teamId}`, data);
|
||||
const response = await api.put<TTeamDetailResponse>(`/v1/hackathon/teams/${teamId}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteTeam = async (teamId: string) => {
|
||||
const response = await api.delete(`/v1/hackathon/teams/${teamId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTeamMembers = async (teamId: string) => {
|
||||
const response = await api.get<TTeamMembersResponse>(`${TEAMS_BASE_URL}/${teamId}/members`);
|
||||
const response = await api.get<TTeamMembersResponse>(`/v1/hackathon/teams/${teamId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const inviteMember = async (teamId: string, data: TInviteMemberRequest) => {
|
||||
const response = await api.post(`${TEAMS_BASE_URL}/${teamId}/invite`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const manageMember = async (teamId: string, userId: string, data: TManageMemberRequest) => {
|
||||
const response = await api.put(`${TEAMS_BASE_URL}/${teamId}/members/${userId}`, data);
|
||||
const response = await api.post(`/v1/hackathon/invitations/teams/${teamId}/invite`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const removeMember = async (teamId: string, userId: string) => {
|
||||
const response = await api.delete(`${TEAMS_BASE_URL}/${teamId}/members/${userId}`);
|
||||
const response = await api.delete(`/v1/hackathon/teams/${teamId}/members/${userId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const joinTeam = async (teamId: string, data: TJoinTeamRequest) => {
|
||||
const response = await api.post(`${TEAMS_BASE_URL}/${teamId}/join-request`, data);
|
||||
const response = await api.post(`/v1/hackathon/join-requests/teams/${teamId}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTeamJoinRequests = async (teamId: string) => {
|
||||
const response = await api.get<TTeamJoinRequestsResponse>(`${TEAMS_BASE_URL}/${teamId}/join-requests`);
|
||||
const response = await api.get<TTeamJoinRequestsResponse>(
|
||||
`/v1/hackathon/join-requests/teams/${teamId}/pending`
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const respondToJoinRequest = async (teamId: string, requestId: string, action: 'approve' | 'reject') => {
|
||||
const response = await api.put(`${TEAMS_BASE_URL}/${teamId}/join-requests/${requestId}`, { action });
|
||||
export const respondToJoinRequest = async (requestId: string, action: 'accept' | 'reject') => {
|
||||
const response = await api.post(`/v1/hackathon/join-requests/${requestId}/respond`, { action });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getMyInvitations = async () => {
|
||||
const response = await api.get<TTeamInvitationsResponse>(`${TEAMS_BASE_URL}/invitations/me`);
|
||||
const response = await api.get<TTeamInvitationsResponse>('/v1/hackathon/invitations/my');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const respondToInvitation = async (invitationId: string, action: 'accept' | 'reject') => {
|
||||
const response = await api.put(`${TEAMS_BASE_URL}/invitations/${invitationId}`, { action });
|
||||
const response = await api.post(`/v1/hackathon/invitations/${invitationId}/respond`, { action });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getMyTeams = async () => {
|
||||
const response = await api.get<TTeamListResponse>(`${TEAMS_BASE_URL}/me`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const submitProject = async (teamId: string, data: TSubmitProjectRequest) => {
|
||||
const response = await api.post<TProjectSubmissionResponse>(`${TEAMS_BASE_URL}/${teamId}/submission`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTeamSubmission = async (teamId: string) => {
|
||||
const response = await api.get<TProjectSubmissionResponse>(`${TEAMS_BASE_URL}/${teamId}/submission`);
|
||||
const response = await api.get('/v1/hackathon/teams/my');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const leaveTeam = async (teamId: string) => {
|
||||
const response = await api.post(`${TEAMS_BASE_URL}/${teamId}/leave`);
|
||||
const response = await api.post(`/v1/hackathon/teams/${teamId}/leave`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const submitProject = async (teamId: string, data: TSubmitProjectRequest) => {
|
||||
const response = await api.post<TProjectSubmissionResponse>(
|
||||
`/v1/hackathon/submissions/teams/${teamId}`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTeamSubmission = async (teamId: string) => {
|
||||
const response = await api.get<TProjectSubmissionResponse>(
|
||||
`/v1/hackathon/submissions/teams/${teamId}`
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type {
|
||||
TTestimonialsListItem,
|
||||
TTestimonialsDetailItem,
|
||||
TTestimonialCreateRequest,
|
||||
TTestimonialUpdateRequest,
|
||||
} from '../../types/testimonials';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
export const getTestimonialList = async (params?: TPaginationParams): Promise<TApiPaginated<TTestimonialsListItem>> => {
|
||||
const response = await api.get<TApiPaginated<TTestimonialsListItem>>('/v1/landing/cms/testimonials', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTestimonialById = async (id: string): Promise<TTestimonialsDetailItem> => {
|
||||
const response = await api.get<ApiResponse<TTestimonialsDetailItem>>(`/v1/landing/cms/testimonials/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createTestimonial = async (data: TTestimonialCreateRequest): Promise<TTestimonialsDetailItem> => {
|
||||
const response = await api.post<ApiResponse<TTestimonialsDetailItem>>('/v1/landing/cms/testimonials/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateTestimonial = async (id: string, data: TTestimonialUpdateRequest): Promise<TTestimonialsDetailItem> => {
|
||||
const response = await api.patch<ApiResponse<TTestimonialsDetailItem>>(`/v1/landing/cms/testimonials/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deleteTestimonial = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/landing/cms/testimonials/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
@@ -1,59 +1,39 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
|
||||
export interface UploadResponse {
|
||||
filename: string;
|
||||
original_filename: string;
|
||||
uploaded_path: string;
|
||||
filename?: string;
|
||||
original_filename?: string;
|
||||
uploaded_path?: string;
|
||||
url: string;
|
||||
size: number;
|
||||
content_type: string;
|
||||
file_type: string;
|
||||
user_id: string;
|
||||
email: string;
|
||||
size?: number;
|
||||
content_type?: string;
|
||||
file_type?: string;
|
||||
user_id?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface UploadService {
|
||||
uploadFile(file: File): Promise<UploadResponse>;
|
||||
uploadAvatar(file: File): Promise<UploadResponse>;
|
||||
uploadCV(file: File): Promise<UploadResponse>;
|
||||
}
|
||||
const multipartPost = async (url: string, file: File, fieldName = 'file'): Promise<UploadResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append(fieldName, file);
|
||||
const response = await api.post<ApiResponse<UploadResponse>>(url, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const uploadService: UploadService = {
|
||||
async uploadFile(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
export const uploadUserFile = (file: File) => multipartPost('/v1/iam/users/upload', file);
|
||||
|
||||
const response = await api.post<ApiResponse<UploadResponse>>('/users/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
return response.data.data;
|
||||
},
|
||||
export const uploadHackathonFile = (file: File) => multipartPost('/v1/hackathon/upload', file);
|
||||
|
||||
async uploadAvatar(file: File) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
throw new Error('File harus berupa gambar');
|
||||
}
|
||||
export const uploadHackathonAvatar = (file: File) => multipartPost('/v1/hackathon/upload/avatar', file);
|
||||
|
||||
const maxSize = 5 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('Ukuran file maksimal 5MB');
|
||||
}
|
||||
export const uploadHackathonTeamFile = (file: File) => multipartPost('/v1/hackathon/upload/team', file);
|
||||
|
||||
return this.uploadFile(file);
|
||||
},
|
||||
export const uploadHackathonSubmission = (file: File) => multipartPost('/v1/hackathon/upload/submission', file);
|
||||
|
||||
async uploadCV(file: File) {
|
||||
if (file.type !== 'application/pdf') {
|
||||
throw new Error('CV harus berupa file PDF');
|
||||
}
|
||||
|
||||
const maxSize = 10 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('Ukuran file maksimal 10MB');
|
||||
}
|
||||
|
||||
return this.uploadFile(file);
|
||||
},
|
||||
// Legacy service object for backward compatibility
|
||||
export const uploadService = {
|
||||
uploadFile: uploadUserFile,
|
||||
uploadAvatar: uploadUserFile,
|
||||
uploadCV: uploadUserFile,
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user