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>
|
||||
@@ -112,4 +144,4 @@ export default function Components(): ReactElement {
|
||||
</div>
|
||||
</BackofficeWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ interface IModalAddItem {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const ModalAddItem = ({
|
||||
@@ -20,6 +21,7 @@ const ModalAddItem = ({
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
onDataCapture,
|
||||
}: IModalAddItem) => {
|
||||
return (
|
||||
<Modal
|
||||
@@ -31,7 +33,7 @@ const ModalAddItem = ({
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
@@ -46,10 +48,11 @@ const ModalAddItem = ({
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep);
|
||||
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -98,7 +101,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
type="submit"
|
||||
>
|
||||
Tambahkan Item
|
||||
</Button>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
interface IModalDeleteItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleDeleteItem?: () => void;
|
||||
handleDeleteItem?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const ModalDeleteItem = ({
|
||||
@@ -48,8 +48,9 @@ const ModalDeleteItem = ({
|
||||
variant="danger"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
handleDeleteItem && handleDeleteItem();
|
||||
onClick={async () => {
|
||||
if (handleDeleteItem) await handleDeleteItem();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Hapus Item
|
||||
|
||||
@@ -11,6 +11,8 @@ interface IModalEditItem {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
initialValues?: { itemName?: string; quantity?: number };
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const ModalEditItem = ({
|
||||
@@ -20,6 +22,8 @@ const ModalEditItem = ({
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleEditItem,
|
||||
initialValues,
|
||||
onDataCapture,
|
||||
}: IModalEditItem) => {
|
||||
return (
|
||||
<Modal
|
||||
@@ -31,7 +35,7 @@ const ModalEditItem = ({
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} initialValues={initialValues} onDataCapture={onDataCapture} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
@@ -46,15 +50,12 @@ const ModalEditItem = ({
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
initialValues?: { itemName?: string; quantity?: number };
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const initialValues = {
|
||||
itemName: 'Hoodie IMPHNEN Official 2025',
|
||||
quantity: 10,
|
||||
};
|
||||
|
||||
const { form, onSubmit } = useItem(nextStep, initialValues);
|
||||
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep, initialValues as any, onDataCapture);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -102,7 +103,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
type="submit"
|
||||
>
|
||||
Perbarui Item
|
||||
</Button>
|
||||
|
||||
@@ -8,16 +8,17 @@ import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
nextStep: () => void,
|
||||
initialValues?: TGachaItem
|
||||
initialValues?: Partial<TGachaItem>,
|
||||
onDataCapture?: (data: any) => void,
|
||||
) => {
|
||||
const form = useForm<TGachaItem>({
|
||||
const form = useForm<any>({
|
||||
resolver: zodResolver(gachaItemSchema),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log('Form data:', data);
|
||||
onDataCapture?.(data);
|
||||
nextStep();
|
||||
});
|
||||
|
||||
@@ -38,6 +39,9 @@ export const useConfirmItem = (
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
if (actionFunction) {
|
||||
await actionFunction();
|
||||
}
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
|
||||
@@ -6,16 +6,26 @@ import {
|
||||
UserSwitchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
|
||||
import ModalAddItem from './_components/modal-add-item';
|
||||
import ModalEditItem from './_components/modal-edit-item';
|
||||
import ModalDeleteItem from './_components/modal-delete-item';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
import {
|
||||
useUserList,
|
||||
useGachaItemList,
|
||||
useCreateGachaItem,
|
||||
useUpdateGachaItem,
|
||||
useDeleteGachaItem,
|
||||
TGachaItemDto,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
const [showModalEditItem, setShowModalEditItem] = useState(false);
|
||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null);
|
||||
const pendingFormData = useRef<any>(null);
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
@@ -28,6 +38,52 @@ export const Components: FC = (): ReactElement => {
|
||||
minValue: 1,
|
||||
});
|
||||
|
||||
const { data: usersData } = useUserList({ per_page: 1 });
|
||||
const { data: gachaItemsData } = useGachaItemList({ per_page: 9 });
|
||||
const createItem = useCreateGachaItem();
|
||||
const updateItem = useUpdateGachaItem();
|
||||
const deleteItem = useDeleteGachaItem();
|
||||
|
||||
const totalUsers = usersData?.meta?.total ?? 0;
|
||||
const gachaItems: TGachaItemDto[] = gachaItemsData?.data ?? [];
|
||||
|
||||
const handleAdd = async (): Promise<boolean> => {
|
||||
if (pendingFormData.current) {
|
||||
const { itemName, quantity } = pendingFormData.current;
|
||||
await createItem.mutateAsync({
|
||||
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
|
||||
name: itemName,
|
||||
description: '',
|
||||
rarity: 'common',
|
||||
type_: 'physical',
|
||||
category: 'merchandise',
|
||||
value: 0,
|
||||
weight: 1,
|
||||
stock: quantity ?? 1,
|
||||
is_limited: false,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleEdit = async (): Promise<boolean> => {
|
||||
if (selectedItem && pendingFormData.current) {
|
||||
const { itemName, quantity } = pendingFormData.current;
|
||||
await updateItem.mutateAsync({
|
||||
id: selectedItem.id,
|
||||
data: { name: itemName, stock: quantity },
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleDelete = async (): Promise<boolean> => {
|
||||
if (selectedItem) {
|
||||
await deleteItem.mutateAsync(selectedItem.id);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
@@ -47,7 +103,7 @@ export const Components: FC = (): ReactElement => {
|
||||
<UsergroupAddOutlined className="text-[20px]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-p1 font-semibold">1000</h3>
|
||||
<h3 className="text-p1 font-semibold">{totalUsers}</h3>
|
||||
<p className="text-label1 text-neutral-500">Participants</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,9 +113,9 @@ export const Components: FC = (): ReactElement => {
|
||||
<ReloadOutlined className="text-[20px]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-p1 font-semibold">1000</h3>
|
||||
<h3 className="text-p1 font-semibold">{gachaItemsData?.meta?.total ?? 0}</h3>
|
||||
<p className="text-label1 text-neutral-500">
|
||||
Roll and Reroll
|
||||
Gacha Items
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +125,7 @@ export const Components: FC = (): ReactElement => {
|
||||
<UserSwitchOutlined className="text-[20px]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-p1 font-semibold">1000</h3>
|
||||
<h3 className="text-p1 font-semibold">-</h3>
|
||||
<p className="text-label1 text-neutral-500">Redeem</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,7 +135,7 @@ export const Components: FC = (): ReactElement => {
|
||||
<UsergroupDeleteOutlined className="text-[20px]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-p1 font-semibold">1000</h3>
|
||||
<h3 className="text-p1 font-semibold">-</h3>
|
||||
<p className="text-label1 text-neutral-500">
|
||||
Inactive Users
|
||||
</p>
|
||||
@@ -105,19 +161,18 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
||||
{gachaItems.map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
key={item.id}
|
||||
className="bg-white overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
||||
>
|
||||
<div className="flex flex-col py-4 px-6 gap-[8px]">
|
||||
<div>
|
||||
<h3 className="text-p3 text-primary-500 font-medium">
|
||||
Lanyard IMPHNEN
|
||||
{item.name}
|
||||
</h3>
|
||||
<div className="flex items-center justify-start gap-10 text-label2 text-gray-500 mt-1">
|
||||
<span>Prize {item}</span>
|
||||
<span>Quantity: 10</span>
|
||||
<span>{item.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-start gap-2">
|
||||
@@ -125,7 +180,10 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="text"
|
||||
size="sm"
|
||||
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
|
||||
onClick={() => setShowModalEditItem(true)}
|
||||
onClick={() => {
|
||||
setSelectedItem(item);
|
||||
setShowModalEditItem(true);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
@@ -133,14 +191,17 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="text"
|
||||
size="sm"
|
||||
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
|
||||
onClick={() => setShowModalDeleteItem(true)}
|
||||
onClick={() => {
|
||||
setSelectedItem(item);
|
||||
setShowModalDeleteItem(true);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img src="gacha-clip.webp" alt="Lanyard IMPHNEN" />
|
||||
<img src="gacha-clip.webp" alt={item.name} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -162,6 +223,8 @@ export const Components: FC = (): ReactElement => {
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleAddItem={handleAdd}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
/>
|
||||
|
||||
<ModalEditItem
|
||||
@@ -171,11 +234,15 @@ export const Components: FC = (): ReactElement => {
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleEditItem={handleEdit}
|
||||
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
/>
|
||||
|
||||
<ModalDeleteItem
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
handleDeleteItem={async () => { await handleDelete(); return true; }}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organ
|
||||
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||
import { ReactElement, useState } from "react"
|
||||
import { useMySessions, TSessionListItem } from "@imphnen-frontend-service/service";
|
||||
|
||||
const TABS = {
|
||||
MENTORING: 'Mentoring',
|
||||
@@ -11,24 +12,6 @@ const TABS = {
|
||||
} as const
|
||||
type Tabs = typeof TABS[keyof typeof TABS]
|
||||
|
||||
type FeedbackStatus = 'done' | 'todo';
|
||||
|
||||
interface FeedbackType {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
rating: number
|
||||
status: FeedbackStatus
|
||||
}
|
||||
|
||||
const mockData: FeedbackType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
||||
email: 'fullname23@gmail.com',
|
||||
rating: 4.5,
|
||||
status: i % 2 === 0 ? 'done' : 'todo',
|
||||
}))
|
||||
|
||||
export default function Components(): ReactElement {
|
||||
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
|
||||
|
||||
@@ -38,7 +21,18 @@ export default function Components(): ReactElement {
|
||||
pageSize: 9,
|
||||
});
|
||||
|
||||
const columns: ColumnDef<FeedbackType>[] = [
|
||||
const { data: sessionsData, isLoading } = useMySessions(
|
||||
activeTab === TABS.MENTORING ? { status: 'completed' } : undefined
|
||||
);
|
||||
|
||||
const sessions: TSessionListItem[] = activeTab === TABS.MENTORING
|
||||
? (sessionsData?.sessions ?? [])
|
||||
: [];
|
||||
const totalItems = activeTab === TABS.MENTORING
|
||||
? (sessionsData?.total ?? sessions.length)
|
||||
: 0;
|
||||
|
||||
const columns: ColumnDef<TSessionListItem>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
meta: { cellClassName: cn("w-20") },
|
||||
@@ -62,37 +56,30 @@ export default function Components(): ReactElement {
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
accessorKey: 'mentee_fullname',
|
||||
cell: ({ row }) => <span>{row.original.mentee_fullname ?? '-'}</span>,
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
header: 'Email',
|
||||
accessorKey: 'email',
|
||||
accessorKey: 'mentee_email',
|
||||
cell: ({ row }) => <span>{row.original.mentee_email ?? '-'}</span>,
|
||||
},
|
||||
{
|
||||
id: 'rating',
|
||||
header: 'Rating',
|
||||
accessorKey: 'rating',
|
||||
cell: ({ row }) => <span>{row.original.rating ?? '-'}</span>,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const statusColors: Record<FeedbackStatus, string> = {
|
||||
done: 'bg-success-200 text-success-500',
|
||||
todo: 'bg-primary-200 text-primary-500',
|
||||
};
|
||||
const statusText: Record<FeedbackStatus, string> = {
|
||||
done: 'Done',
|
||||
todo: 'To Do',
|
||||
};
|
||||
const hasRating = !!row.original.rating;
|
||||
return (
|
||||
<div
|
||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||
>
|
||||
{statusText[status]}
|
||||
<div className={`py-2 px-4 rounded-md text-center ${hasRating ? 'bg-success-200 text-success-500' : 'bg-primary-200 text-primary-500'}`}>
|
||||
{hasRating ? 'Done' : 'To Do'}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -100,7 +87,7 @@ export default function Components(): ReactElement {
|
||||
{
|
||||
header: 'Action',
|
||||
meta: { cellClassName: cn("w-72") },
|
||||
cell: ({ row }) => (
|
||||
cell: () => (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@@ -116,7 +103,7 @@ export default function Components(): ReactElement {
|
||||
]
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockData,
|
||||
data: sessions,
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
@@ -127,8 +114,8 @@ export default function Components(): ReactElement {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -142,7 +129,10 @@ export default function Components(): ReactElement {
|
||||
key={tab}
|
||||
variant="text"
|
||||
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
onClick={() => {
|
||||
setActiveTab(tab);
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
}}
|
||||
>
|
||||
{tab}
|
||||
</Button>
|
||||
@@ -163,18 +153,26 @@ export default function Components(): ReactElement {
|
||||
</div>
|
||||
</div>
|
||||
<Select>
|
||||
<option selected disabled>Rating</option>
|
||||
<option disabled>Rating</option>
|
||||
<option value="4.5">4.5</option>
|
||||
<option value="5">5</option>
|
||||
</Select>
|
||||
<Select>
|
||||
<option selected disabled>Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option disabled>Status</option>
|
||||
<option value="done">Done</option>
|
||||
<option value="todo">To Do</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<DataTable data={mockData} columns={columns} table={table} />
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-neutral-400">Loading...</div>
|
||||
) : activeTab === TABS.PLATFORM ? (
|
||||
<div className="text-center py-8 text-neutral-400">
|
||||
Platform feedback tidak tersedia
|
||||
</div>
|
||||
) : (
|
||||
<DataTable data={sessions} columns={columns} table={table} />
|
||||
)}
|
||||
</section>
|
||||
</BackofficeWrapper>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ interface IModalAddItem {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const ModalAddItem = ({
|
||||
@@ -20,6 +21,7 @@ const ModalAddItem = ({
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
onDataCapture,
|
||||
}: IModalAddItem) => {
|
||||
return (
|
||||
<Modal
|
||||
@@ -31,7 +33,7 @@ const ModalAddItem = ({
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
@@ -46,10 +48,11 @@ const ModalAddItem = ({
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep);
|
||||
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
interface IModalDeleteItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleDeleteItem?: () => void;
|
||||
handleDeleteItem?: () => Promise<void>;
|
||||
}
|
||||
|
||||
const ModalDeleteItem = ({
|
||||
@@ -48,8 +48,9 @@ const ModalDeleteItem = ({
|
||||
variant="danger"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
handleDeleteItem && handleDeleteItem();
|
||||
onClick={async () => {
|
||||
if (handleDeleteItem) await handleDeleteItem();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Hapus Item
|
||||
|
||||
@@ -11,6 +11,8 @@ interface IModalUpdateItem {
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
initialValues?: { itemName?: string; quantity?: number; chanceRate?: number };
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const ModalUpdateItem = ({
|
||||
@@ -20,6 +22,8 @@ const ModalUpdateItem = ({
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleUpdateItem,
|
||||
initialValues,
|
||||
onDataCapture,
|
||||
}: IModalUpdateItem) => {
|
||||
return (
|
||||
<Modal
|
||||
@@ -31,7 +35,7 @@ const ModalUpdateItem = ({
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} initialValues={initialValues} onDataCapture={onDataCapture} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
@@ -46,16 +50,12 @@ const ModalUpdateItem = ({
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
initialValues?: { itemName?: string; quantity?: number; chanceRate?: number };
|
||||
onDataCapture?: (data: any) => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const initialValues = {
|
||||
itemName: 'Hoodie IMPHNEN Official 2025',
|
||||
quantity: 10,
|
||||
chanceRate: 0.1,
|
||||
};
|
||||
|
||||
const { form, onSubmit } = useItem(nextStep, initialValues);
|
||||
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep, initialValues as any, onDataCapture);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -105,7 +105,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
type="submit"
|
||||
>
|
||||
Perbarui Item
|
||||
</Button>
|
||||
|
||||
@@ -8,16 +8,17 @@ import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
nextStep: () => void,
|
||||
initialValues?: TGachaRollItem
|
||||
initialValues?: Partial<TGachaRollItem>,
|
||||
onDataCapture?: (data: any) => void,
|
||||
) => {
|
||||
const form = useForm<TGachaRollItem>({
|
||||
const form = useForm<any>({
|
||||
resolver: zodResolver(gachaRollItemSchema),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log('Form data:', data);
|
||||
onDataCapture?.(data);
|
||||
nextStep();
|
||||
});
|
||||
|
||||
@@ -38,6 +39,9 @@ export const useConfirmItem = (
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
if (actionFunction) {
|
||||
await actionFunction();
|
||||
}
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
|
||||
import {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
@@ -22,25 +22,21 @@ import ModalAddItem from './_components/modal-add-item';
|
||||
import ModalUpdateItem from './_components/modal-update-item';
|
||||
import ModalDeleteItem from './_components/modal-delete-item';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
|
||||
interface GachaItem {
|
||||
id: number;
|
||||
name: string;
|
||||
chanceRate: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
const mockData: GachaItem[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: 'Hoodie IMPHNEN Official 2025',
|
||||
chanceRate: 0.1,
|
||||
quantity: 10,
|
||||
}));
|
||||
import {
|
||||
useGachaItemList,
|
||||
useCreateGachaItem,
|
||||
useUpdateGachaItem,
|
||||
useDeleteGachaItem,
|
||||
TGachaItemDto,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const pendingFormData = useRef<any>(null);
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
@@ -60,7 +56,60 @@ export const Components: FC = (): ReactElement => {
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
|
||||
const columns: ColumnDef<GachaItem>[] = [
|
||||
const { data: itemsData, isLoading } = useGachaItemList({
|
||||
search,
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
});
|
||||
const createItem = useCreateGachaItem();
|
||||
const updateItem = useUpdateGachaItem();
|
||||
const deleteItem = useDeleteGachaItem();
|
||||
|
||||
const items: TGachaItemDto[] = itemsData?.data ?? [];
|
||||
const totalItems = itemsData?.meta?.total ?? items.length;
|
||||
|
||||
const handleAdd = async (): Promise<boolean> => {
|
||||
if (pendingFormData.current) {
|
||||
const { itemName, quantity, chanceRate } = pendingFormData.current;
|
||||
await createItem.mutateAsync({
|
||||
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
|
||||
name: itemName,
|
||||
description: '',
|
||||
rarity: 'common',
|
||||
type_: 'physical',
|
||||
category: 'merchandise',
|
||||
value: 0,
|
||||
weight: chanceRate ?? 1,
|
||||
stock: quantity ?? 1,
|
||||
is_limited: false,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleUpdate = async (): Promise<boolean> => {
|
||||
if (selectedItem && pendingFormData.current) {
|
||||
const { itemName, quantity, chanceRate } = pendingFormData.current;
|
||||
await updateItem.mutateAsync({
|
||||
id: selectedItem.id,
|
||||
data: {
|
||||
name: itemName,
|
||||
weight: chanceRate,
|
||||
stock: quantity,
|
||||
},
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedItem) {
|
||||
await deleteItem.mutateAsync(selectedItem.id);
|
||||
}
|
||||
setShowModalDeleteItem(false);
|
||||
};
|
||||
|
||||
const columns: ColumnDef<TGachaItemDto>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
@@ -88,23 +137,16 @@ export const Components: FC = (): ReactElement => {
|
||||
header: 'Nama Item',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'Chance Rate',
|
||||
accessorKey: 'chanceRate',
|
||||
},
|
||||
{
|
||||
header: 'Quantity',
|
||||
accessorKey: 'quantity',
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
cell: () => (
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-[8px]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedItem(row.original);
|
||||
setShowModalUpdateItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
@@ -116,6 +158,7 @@ export const Components: FC = (): ReactElement => {
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedItem(row.original);
|
||||
setShowModalDeleteItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
@@ -128,7 +171,7 @@ export const Components: FC = (): ReactElement => {
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockData,
|
||||
data: items,
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
@@ -139,8 +182,8 @@ export const Components: FC = (): ReactElement => {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -156,6 +199,8 @@ export const Components: FC = (): ReactElement => {
|
||||
<Input
|
||||
placeholder="Cari berdasarkan nama item"
|
||||
className="pl-12 w-full max-h-full"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||
<SearchOutlined />
|
||||
@@ -166,9 +211,7 @@ export const Components: FC = (): ReactElement => {
|
||||
variant="primary"
|
||||
size="md"
|
||||
className="flex gap-3 text-nowrap"
|
||||
onClick={() => {
|
||||
setShowModalAddItem(true);
|
||||
}}
|
||||
onClick={() => setShowModalAddItem(true)}
|
||||
>
|
||||
<PlusOutlined />
|
||||
Tambah Item
|
||||
@@ -176,7 +219,11 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable data={mockData} columns={columns} table={table} />
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-neutral-400">Loading...</div>
|
||||
) : (
|
||||
<DataTable data={items} columns={columns} table={table} />
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -187,6 +234,8 @@ export const Components: FC = (): ReactElement => {
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleAddItem={handleAdd}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
/>
|
||||
<ModalUpdateItem
|
||||
currentStep={currentStep}
|
||||
@@ -195,13 +244,14 @@ export const Components: FC = (): ReactElement => {
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
handleUpdateItem={handleUpdate}
|
||||
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
|
||||
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||
/>
|
||||
<ModalDeleteItem
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
handleDeleteItem={() => {
|
||||
console.log('Item deleted');
|
||||
}}
|
||||
handleDeleteItem={handleDelete}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
+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
|
||||
|
||||
+60
-6
@@ -10,6 +10,9 @@ import { QrisPaymentStep } from "./steps/qris-payement"
|
||||
import { VAPaymentStep } from "./steps/va-payment"
|
||||
import { SuccessStep } from "./steps/success"
|
||||
import { PaymentStep } from "./steps/payment"
|
||||
import { useBookSession } from "@imphnen-frontend-service/service"
|
||||
import { TOPICS } from "../../sections/topics"
|
||||
import { toast } from "sonner"
|
||||
|
||||
const STEPS = ['topic', 'schedule', 'profile', 'payment', 'qr-payment', 'va-payment', 'success'] as const
|
||||
type Step = typeof STEPS[number]
|
||||
@@ -17,15 +20,48 @@ type Step = typeof STEPS[number]
|
||||
type Props = {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
mentorId?: string
|
||||
}
|
||||
|
||||
export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
||||
export const AppointmentModal: FC<Props> = ({ open, setOpen, mentorId }) => {
|
||||
const [step, setStep] = useState<Step>('topic')
|
||||
const [selectedTopics, setSelectedTopics] = useState<number[]>([])
|
||||
const [scheduledDate, setScheduledDate] = useState('')
|
||||
const [scheduledTime, setScheduledTime] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [sessionType, setSessionType] = useState('online')
|
||||
const [isBooking, setIsBooking] = useState(false)
|
||||
|
||||
const handleStep = (action: 'next' | 'prev') => {
|
||||
const bookSession = useBookSession(mentorId ?? '')
|
||||
|
||||
const handleStep = async (action: 'next' | 'prev') => {
|
||||
if (action === 'next' && step === 'success') {
|
||||
setOpen(false)
|
||||
} else if (action === 'next' && step === 'payment' && mentorId) {
|
||||
const topicNames = selectedTopics
|
||||
.map((id) => TOPICS.find((t) => t.id === id)?.name)
|
||||
.filter(Boolean)
|
||||
.join(', ')
|
||||
|
||||
const scheduledAt =
|
||||
scheduledDate && scheduledTime
|
||||
? new Date(`${scheduledDate}T${scheduledTime}`).toISOString()
|
||||
: new Date().toISOString()
|
||||
|
||||
setIsBooking(true)
|
||||
try {
|
||||
await bookSession.mutateAsync({
|
||||
topic: topicNames || 'General Mentoring',
|
||||
description: description || undefined,
|
||||
scheduled_at: scheduledAt,
|
||||
session_type: sessionType,
|
||||
})
|
||||
setStep('qr-payment')
|
||||
} catch {
|
||||
toast.error('Gagal membuat sesi. Silakan coba lagi.')
|
||||
} finally {
|
||||
setIsBooking(false)
|
||||
}
|
||||
} else if (action === 'next') {
|
||||
setStep(STEPS[STEPS.indexOf(step) + 1])
|
||||
} else if (action === 'prev' && step !== 'topic') {
|
||||
@@ -46,6 +82,12 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
||||
window.addEventListener("keydown", handleEscapeKey)
|
||||
} else {
|
||||
document.body.style.overflow = ""
|
||||
setStep('topic')
|
||||
setSelectedTopics([])
|
||||
setScheduledDate('')
|
||||
setScheduledTime('')
|
||||
setDescription('')
|
||||
setSessionType('online')
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -77,6 +119,7 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
||||
size="sm"
|
||||
variant="text"
|
||||
className="absolute top-3 right-3 bg-primary-200 p-1 shadow md:bg-white md:p-2 md:top-8 md:right-10"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<CloseOutlined className="md:text-lg" />
|
||||
</Button>
|
||||
@@ -113,7 +156,18 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
||||
|
||||
<AnimatePresence>
|
||||
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} />}
|
||||
{step === 'schedule' && <ScheduleStep />}
|
||||
{step === 'schedule' && (
|
||||
<ScheduleStep
|
||||
scheduledDate={scheduledDate}
|
||||
scheduledTime={scheduledTime}
|
||||
description={description}
|
||||
sessionType={sessionType}
|
||||
onDateChange={setScheduledDate}
|
||||
onTimeChange={setScheduledTime}
|
||||
onDescriptionChange={setDescription}
|
||||
onSessionTypeChange={setSessionType}
|
||||
/>
|
||||
)}
|
||||
{step === 'profile' && <ProfileStep />}
|
||||
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} />}
|
||||
{step === 'qr-payment' && <QrisPaymentStep />}
|
||||
@@ -141,14 +195,14 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
||||
size="sm"
|
||||
variant="primary"
|
||||
className={cn((step === 'topic' || step === 'success') && 'w-full')}
|
||||
disabled={selectedTopics.length === 0 && step === 'topic'}
|
||||
disabled={(selectedTopics.length === 0 && step === 'topic') || isBooking}
|
||||
onClick={() => handleStep('next')}
|
||||
>
|
||||
<Show
|
||||
condition={step !== 'success'}
|
||||
fallback="Halman Booking"
|
||||
>
|
||||
<Show condition={step !== 'payment'} fallback="Bayar Sekarang">
|
||||
<Show condition={step !== 'payment'} fallback={isBooking ? 'Memproses...' : 'Bayar Sekarang'}>
|
||||
Selanjutnya
|
||||
</Show>
|
||||
</Show>
|
||||
@@ -160,4 +214,4 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+43
-6
@@ -3,7 +3,7 @@ import { cn } from "@imphnen-frontend-service/utils"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
|
||||
|
||||
|
||||
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
|
||||
|
||||
Saya ingin tahu secara khusus tentang ___.
|
||||
@@ -13,7 +13,27 @@ Saya ingin tahu secara khusus tentang ___.
|
||||
|
||||
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
||||
|
||||
export const ScheduleStep = () => {
|
||||
type Props = {
|
||||
scheduledDate: string
|
||||
scheduledTime: string
|
||||
description: string
|
||||
sessionType: string
|
||||
onDateChange: (v: string) => void
|
||||
onTimeChange: (v: string) => void
|
||||
onDescriptionChange: (v: string) => void
|
||||
onSessionTypeChange: (v: string) => void
|
||||
}
|
||||
|
||||
export const ScheduleStep = ({
|
||||
scheduledDate,
|
||||
scheduledTime,
|
||||
description,
|
||||
sessionType,
|
||||
onDateChange,
|
||||
onTimeChange,
|
||||
onDescriptionChange,
|
||||
onSessionTypeChange,
|
||||
}: Props) => {
|
||||
return (
|
||||
<motion.div
|
||||
className="bg-white px-6 py-5 rounded-md md:px-6"
|
||||
@@ -31,15 +51,29 @@ export const ScheduleStep = () => {
|
||||
<div className="grid gap-2.5 md:grid-cols-2 md:gap-5">
|
||||
<div>
|
||||
<label className={labelClass}>Tanggal</label>
|
||||
<Input type="date" className="min-w-full w-full" />
|
||||
<Input
|
||||
type="date"
|
||||
className="min-w-full w-full"
|
||||
value={scheduledDate}
|
||||
onChange={(e) => onDateChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Waktu</label>
|
||||
<Input type="time" className="min-w-full w-full" />
|
||||
<Input
|
||||
type="time"
|
||||
className="min-w-full w-full"
|
||||
value={scheduledTime}
|
||||
onChange={(e) => onTimeChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative md:col-span-full">
|
||||
<label className={labelClass}>Lokasi</label>
|
||||
<Select className="min-w-full w-full">
|
||||
<Select
|
||||
className="min-w-full w-full"
|
||||
value={sessionType}
|
||||
onChange={(e) => onSessionTypeChange(e.target.value)}
|
||||
>
|
||||
<option value="online">Online</option>
|
||||
<option value="offline">Offline</option>
|
||||
</Select>
|
||||
@@ -48,7 +82,10 @@ export const ScheduleStep = () => {
|
||||
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
|
||||
<Textarea
|
||||
className="min-w-full w-full h-40"
|
||||
placeholder={placeholder} />
|
||||
placeholder={placeholder}
|
||||
value={description}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
+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!
|
||||
|
||||
+13
-8
@@ -1,19 +1,24 @@
|
||||
import { StarFilled } from "@ant-design/icons";
|
||||
import { For } from "@imphnen-frontend-service/utils";
|
||||
import { FC } from "react";
|
||||
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service";
|
||||
|
||||
const SENPAI_STATISTIC = [
|
||||
{ name: 'Total Sessions', count: 8 },
|
||||
{ name: 'Mentee Impact', count: 1000 },
|
||||
{ name: 'Response Time', count: '30 Minute' }
|
||||
]
|
||||
type Props = {
|
||||
mentor?: MentorDetailResponseDto
|
||||
}
|
||||
|
||||
export const StatisticsSection: FC<Props> = ({ mentor }) => {
|
||||
const stats = [
|
||||
{ name: 'Total Sessions', count: mentor?.mentoring_sessions ?? 0 },
|
||||
{ name: 'Rating', count: mentor?.rating != null ? `${mentor.rating.toFixed(1)}/5.0` : 'N/A' },
|
||||
{ name: 'Experience', count: mentor?.years_of_experience != null ? `${mentor.years_of_experience} Yrs` : 'N/A' },
|
||||
]
|
||||
|
||||
export const StatisticsSection: FC = () => {
|
||||
return (
|
||||
<div className="md:mb-10">
|
||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Statistics</h2>
|
||||
<div className="grid gap-4 xl:flex">
|
||||
<For data={SENPAI_STATISTIC}>
|
||||
<For data={stats}>
|
||||
{(item, index) => (
|
||||
<div key={index} className="px-2.5 py-2 border border-primary-50 rounded-md shadow flex items-center gap-x-2.5">
|
||||
<div
|
||||
@@ -31,4 +36,4 @@ export const StatisticsSection: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { For } from "@imphnen-frontend-service/utils"
|
||||
import { FC } from "react"
|
||||
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
|
||||
|
||||
export const TOPICS = [
|
||||
{ id: 1, icon: '💼', name: 'Career & Self Development' },
|
||||
@@ -11,22 +12,41 @@ export const TOPICS = [
|
||||
{ id: 7, icon: <span className="font-bold text-primary-500">AI</span>, name: 'AI Tips' }
|
||||
]
|
||||
|
||||
export const TopicsSection: FC = () => {
|
||||
type Props = {
|
||||
mentor?: MentorDetailResponseDto
|
||||
}
|
||||
|
||||
export const TopicsSection: FC<Props> = ({ mentor }) => {
|
||||
const mentorTopics = mentor?.topics_of_interest ?? []
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Topics</h2>
|
||||
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5">
|
||||
<For data={TOPICS}>
|
||||
{(item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
|
||||
>
|
||||
<span>{item.icon} </span>
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
{mentorTopics.length > 0 ? (
|
||||
<For data={mentorTopics}>
|
||||
{(topic, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
|
||||
>
|
||||
{topic}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
) : (
|
||||
<For data={TOPICS}>
|
||||
{(item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
|
||||
>
|
||||
<span>{item.icon} </span>
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { ProfileSection } from './_components/sections/profile'
|
||||
import { StatisticsSection } from './_components/sections/senpai-statistics'
|
||||
import { TopicsSection } from './_components/sections/topics'
|
||||
@@ -7,15 +8,27 @@ import { EducationSection } from './_components/sections/education'
|
||||
import { SenpaiScheduleSection } from './_components/sections/senpai-schedule'
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { AppointmentModal } from './_components/modals/appointment'
|
||||
import { useMentorById } from '@imphnen-frontend-service/service'
|
||||
|
||||
export const Components: FC = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const params = useParams()
|
||||
const mentorId = params?.id ?? ''
|
||||
const { data: mentor, isLoading } = useMentorById(mentorId)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<main className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center text-neutral-400">Loading mentor profile...</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20">
|
||||
<div className="max-w-7xl mx-auto space-y-8 md:bg-white xl:bg-transparent">
|
||||
<ProfileSection onBook={() => setOpen(true)} />
|
||||
<ProfileSection mentor={mentor} onBook={() => setOpen(true)} />
|
||||
|
||||
<Button type="button" size="sm" className="w-full md:hidden" onClick={() => setOpen(true)}>
|
||||
Book Your Senpai!
|
||||
@@ -23,18 +36,20 @@ export const Components: FC = () => {
|
||||
|
||||
<div className="bg-white px-4 py-5 md:px-8 md:pb-6 md:pt-0 xl:py-7 xl:flex xl:gap-x-10">
|
||||
<div className="space-y-10 md:space-y-7 xl:flex-1">
|
||||
<StatisticsSection />
|
||||
<TopicsSection />
|
||||
<StatisticsSection mentor={mentor} />
|
||||
<TopicsSection mentor={mentor} />
|
||||
|
||||
<div className="px-6 py-8 rounded-md shadow-md">
|
||||
<h2 className="text-xs text-neutral-800 font-semibold mb-5 md:text-[15px] xl:text-[19px]">Senpai Resume</h2>
|
||||
<p className="text-[10px] font-medium text-neutral-600 text-pretty md:text-[15px]">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
|
||||
</p>
|
||||
</div>
|
||||
{mentor?.bio && (
|
||||
<div className="px-6 py-8 rounded-md shadow-md">
|
||||
<h2 className="text-xs text-neutral-800 font-semibold mb-5 md:text-[15px] xl:text-[19px]">Senpai Resume</h2>
|
||||
<p className="text-[10px] font-medium text-neutral-600 text-pretty md:text-[15px]">
|
||||
{mentor.bio}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ExperienceSection />
|
||||
<EducationSection />
|
||||
<ExperienceSection mentor={mentor} />
|
||||
<EducationSection mentor={mentor} />
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block xl:w-[400px]">
|
||||
@@ -44,7 +59,7 @@ export const Components: FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<AppointmentModal open={open} setOpen={setOpen} />
|
||||
<AppointmentModal open={open} setOpen={setOpen} mentorId={mentorId} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { MentorDetailResponseDto } from '@imphnen-frontend-service/service';
|
||||
|
||||
interface MentorCardProps {
|
||||
mentor: MentorDetailResponseDto;
|
||||
}
|
||||
|
||||
export const MentorCard: FC<MentorCardProps> = ({ mentor }) => {
|
||||
const expertise = mentor.expertise ?? [];
|
||||
const firstSkill = expertise[0];
|
||||
const secondSkill = expertise[1];
|
||||
const extraCount = expertise.length - 2;
|
||||
const yearsExp = mentor.years_of_experience ?? 0;
|
||||
const expLabel = `${yearsExp}+ Years Experience`;
|
||||
|
||||
export const MentorCard: FC = () => {
|
||||
return (
|
||||
<div className="p-2.5 rounded-md bg-white shadow flex gap-x-4 items-start md:p-4 md:flex-col md:rounded-lg md:gap-y-4">
|
||||
<div className="size-[60px] rounded-md overflow-hidden md:w-full md:h-auto md:aspect-square">
|
||||
@@ -10,32 +22,38 @@ export const MentorCard: FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="text" size="sm" className="bg-primary-100 h-auto px-1.5 py-1 text-[8px] font-normal mb-2 hover:bg-primary-100 md:text-[10px] md:font-medium md:mb-4">
|
||||
3-5 Years Experience
|
||||
{expLabel}
|
||||
</Button>
|
||||
<h2 className="mb-1">
|
||||
<Link to="/mentoring/detail" className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
|
||||
Fullname
|
||||
<Link to={`/mentoring/${mentor.id}`} className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
|
||||
{mentor.fullname || 'Unknown Mentor'}
|
||||
</Link>
|
||||
</h2>
|
||||
<p className="text-[8px] text-neutral-500 mb-3 md:text-[10px] md:font-medium md:mb-4 lg:text-xs">
|
||||
Full Stack Enjoyer at name company
|
||||
{mentor.current_role} at {mentor.current_company}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Soft Skills :</p>
|
||||
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Expertise :</p>
|
||||
<div className="space-x-2">
|
||||
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]">
|
||||
Communication
|
||||
</Button>
|
||||
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hidden hover:bg-primary-200 md:inline-block lg:text-[10px]">
|
||||
Communication
|
||||
</Button>
|
||||
<Button variant="text" size="sm" className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]">
|
||||
+2
|
||||
</Button>
|
||||
{firstSkill && (
|
||||
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]">
|
||||
{firstSkill}
|
||||
</Button>
|
||||
)}
|
||||
{secondSkill && (
|
||||
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hidden hover:bg-primary-200 md:inline-block lg:text-[10px]">
|
||||
{secondSkill}
|
||||
</Button>
|
||||
)}
|
||||
{extraCount > 0 && (
|
||||
<Button variant="text" size="sm" className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]">
|
||||
+{extraCount}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,27 +8,35 @@ import { MentorCard } from './_components/mentor-card';
|
||||
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
|
||||
import { motion, useInView, Variants } from 'framer-motion';
|
||||
|
||||
const TEMP_DATA = [
|
||||
{ id: 1, name: 'John Doe' },
|
||||
{ id: 2, name: 'John Doe' },
|
||||
{ id: 3, name: 'John Doe' },
|
||||
{ id: 4, name: 'John Doe' },
|
||||
]
|
||||
import { useMentorList } from '@imphnen-frontend-service/service';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 3,
|
||||
pageSize: 8,
|
||||
});
|
||||
|
||||
const { data: mentorData, isLoading } = useMentorList({
|
||||
page: pagination.pageIndex + 1,
|
||||
per_page: pagination.pageSize,
|
||||
search: search || undefined,
|
||||
});
|
||||
|
||||
const mentors = mentorData?.data ?? [];
|
||||
const totalItems = mentorData?.meta?.total ?? 0;
|
||||
|
||||
const table = useReactTable({
|
||||
data: TEMP_DATA,
|
||||
data: mentors,
|
||||
columns: [],
|
||||
state: { pagination },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: (updater) => {
|
||||
setPagination(updater);
|
||||
},
|
||||
pageCount: Math.ceil(totalItems / pagination.pageSize) || 1,
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
const ref = useRef(null)
|
||||
@@ -91,24 +99,33 @@ export const Components: FC = (): ReactElement => {
|
||||
<Input
|
||||
placeholder="Cari berdasarkan nama, posisi/peran"
|
||||
className="relative min-w-full w-full"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
}}
|
||||
/>
|
||||
<SearchOutlined className="absolute right-2.5 top-1/2 -translate-y-1/2 text-primary-500 size-2.5 cursor-text md:me-12 lg:me-0" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
<For data={Array.from({ length: 8 })}>
|
||||
{(_, index) => (
|
||||
<motion.div key={index} variants={childVariants}>
|
||||
<MentorCard />
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</motion.div>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12 text-neutral-400">Loading mentors...</div>
|
||||
) : (
|
||||
<motion.div
|
||||
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
<For data={mentors}>
|
||||
{(mentor, index) => (
|
||||
<motion.div key={mentor.id ?? index} variants={childVariants}>
|
||||
<MentorCard mentor={mentor} />
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<Pagination table={table} />
|
||||
</motion.div>
|
||||
|
||||
+62
-19
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user