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>
|
||||
|
||||
@@ -5,23 +5,28 @@ import type {
|
||||
TAdminSubmissionsResponse,
|
||||
} from '../../types/admin';
|
||||
|
||||
const ADMIN_BASE_URL = '/admin';
|
||||
|
||||
export const getAdminUsers = async (params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
is_admin?: boolean;
|
||||
}) => {
|
||||
const response = await api.get<TAdminUsersResponse>(
|
||||
`${ADMIN_BASE_URL}/users`,
|
||||
{
|
||||
params: {
|
||||
...params,
|
||||
is_admin: params?.is_admin ?? false,
|
||||
},
|
||||
}
|
||||
);
|
||||
const response = await api.get<TAdminUsersResponse>('/v1/hackathon/admin/users', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getAdminUserById = async (userId: string) => {
|
||||
const response = await api.get(`/v1/hackathon/admin/users/${userId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteAdminUser = async (userId: string) => {
|
||||
const response = await api.delete(`/v1/hackathon/admin/users/${userId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const setAdminUser = async (userId: string, is_admin: boolean) => {
|
||||
const response = await api.post(`/v1/hackathon/admin/users/${userId}/set-admin`, { is_admin });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -30,10 +35,12 @@ export const getAdminTeams = async (params?: {
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
}) => {
|
||||
const response = await api.get<TAdminTeamsResponse>(
|
||||
`${ADMIN_BASE_URL}/teams`,
|
||||
{ params }
|
||||
);
|
||||
const response = await api.get<TAdminTeamsResponse>('/v1/hackathon/admin/teams', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteAdminTeam = async (teamId: string) => {
|
||||
const response = await api.delete(`/v1/hackathon/admin/teams/${teamId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -43,9 +50,21 @@ export const getAdminSubmissions = async (params?: {
|
||||
search?: string;
|
||||
status?: string;
|
||||
}) => {
|
||||
const response = await api.get<TAdminSubmissionsResponse>(
|
||||
`${ADMIN_BASE_URL}/submissions`,
|
||||
{ params }
|
||||
);
|
||||
const response = await api.get<TAdminSubmissionsResponse>('/v1/hackathon/admin/submissions', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getAdminWinners = async () => {
|
||||
const response = await api.get('/v1/hackathon/admin/winners');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const setWinner = async (data: { team_id: string; rank: number; prize?: string }) => {
|
||||
const response = await api.post('/v1/hackathon/admin/winners', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const removeWinner = async (teamId: string) => {
|
||||
const response = await api.delete(`/v1/hackathon/admin/winners/${teamId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -1,80 +1,65 @@
|
||||
import { api, getBaseURL } from '../';
|
||||
import {
|
||||
TLoginRequest,
|
||||
TLoginResponse,
|
||||
TRegisterRequest,
|
||||
TSendOTPRequest,
|
||||
TVerifyEmailRequest,
|
||||
TGoogleCallbackResponse,
|
||||
} from '../../types/auth';
|
||||
import { TResponseMessage } from '../../types/common';
|
||||
import { api } from '../index';
|
||||
import type { TLoginResponse, TRegisterRequest, TSendOTPRequest, TVerifyEmailRequest } from '../../types/auth';
|
||||
import type { TResponseMessage } from '../../types/common';
|
||||
|
||||
export const postLogin = async (
|
||||
payload: TLoginRequest
|
||||
): Promise<TLoginResponse> => {
|
||||
export type TLoginRequest = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type TForgotPasswordRequest = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type TNewPasswordRequest = {
|
||||
token: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type TRefreshTokenRequest = {
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
export const postLogin = async (payload: TLoginRequest): Promise<TLoginResponse> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/login', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postLoginMentor = async (payload: TLoginRequest): Promise<TLoginResponse> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/login-mentor', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postRegister = async (payload: TRegisterRequest): Promise<TResponseMessage> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/register', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postVerifyEmail = async (payload: TVerifyEmailRequest): Promise<TResponseMessage> => {
|
||||
const { data } = await api({
|
||||
method: 'POST',
|
||||
url: '/auth/login',
|
||||
data: payload,
|
||||
url: '/v1/iam/auth/verify-email',
|
||||
data: { otp: parseInt(payload.otp), email: payload.email },
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postRegister = async (
|
||||
payload: TRegisterRequest
|
||||
): Promise<TResponseMessage> => {
|
||||
const { data } = await api({
|
||||
method: 'POST',
|
||||
url: '/auth/register',
|
||||
data: payload,
|
||||
});
|
||||
export const postSendOtp = async (payload: TSendOTPRequest): Promise<TResponseMessage> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/send-otp', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postVerifyEmail = async (
|
||||
payload: TVerifyEmailRequest
|
||||
): Promise<TResponseMessage> => {
|
||||
const { data } = await api({
|
||||
method: 'POST',
|
||||
url: '/auth/verify-email',
|
||||
data: {otp: parseInt(payload.otp), email: payload.email},
|
||||
});
|
||||
export const postForgotPassword = async (payload: TForgotPasswordRequest): Promise<TResponseMessage> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/forgot', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postSendOtp = async (
|
||||
payload: TSendOTPRequest
|
||||
): Promise<TResponseMessage> => {
|
||||
const { data } = await api({
|
||||
method: 'POST',
|
||||
url: '/auth/send-otp',
|
||||
data: payload,
|
||||
});
|
||||
export const postNewPassword = async (payload: TNewPasswordRequest): Promise<TResponseMessage> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/new-password', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getGoogleAuthUrl = async (): Promise<string> => {
|
||||
const baseUrl = getBaseURL() || 'http://localhost:8080';
|
||||
return `${baseUrl}/auth/google/login`;
|
||||
};
|
||||
|
||||
export const postGoogleCallback = async (code: string, state: string): Promise<TGoogleCallbackResponse> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: `/auth/google/callback?code=${code}&state=${state}`,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getGitHubAuthUrl = async (): Promise<string> => {
|
||||
const baseUrl = getBaseURL() || 'http://localhost:8080';
|
||||
return `${baseUrl}/auth/github/login`;
|
||||
};
|
||||
|
||||
export const postGitHubCallback = async (code: string, state: string): Promise<TGoogleCallbackResponse> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: `/auth/github/callback?code=${code}&state=${state}`,
|
||||
});
|
||||
export const postRefreshToken = async (payload: TRefreshTokenRequest): Promise<{ access_token: string; refresh_token: string }> => {
|
||||
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/refresh', data: payload });
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -1,52 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import { useAuthStore } from '../hooks/auth';
|
||||
|
||||
const BACKOFFICE_API_URL = 'https://api.hackathon.imphnen.dev/api/v1';
|
||||
|
||||
export const backofficeApi = axios.create({
|
||||
baseURL: BACKOFFICE_API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
backofficeApi.interceptors.request.use(
|
||||
(config) => {
|
||||
const { session } = useAuthStore.getState();
|
||||
if (session?.token?.access_token) {
|
||||
config.headers.Authorization = `Bearer ${session.token.access_token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(new Error(error.message || 'Request failed'));
|
||||
}
|
||||
);
|
||||
|
||||
backofficeApi.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
const isAuthPage =
|
||||
globalThis.window !== undefined &&
|
||||
globalThis.location.pathname.startsWith('/auth');
|
||||
|
||||
if (!isAuthPage) {
|
||||
useAuthStore.getState().clearSession();
|
||||
if (globalThis.window !== undefined) {
|
||||
globalThis.location.href = '/auth/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const backendMsg = error?.response?.data?.message;
|
||||
if (backendMsg && typeof backendMsg === 'string') {
|
||||
return Promise.reject(new Error(backendMsg));
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(error.message || 'An error occurred'));
|
||||
}
|
||||
);
|
||||
// All API calls now go through the main `api` instance (api.imphnen.dev).
|
||||
// This file re-exports `api` as `backofficeApi` for backward compatibility.
|
||||
export { api as backofficeApi } from './index';
|
||||
|
||||
export interface BackofficeApiResponse<T> {
|
||||
data: T;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type { TEventsListItem, TEventsDetailItem, TEventCreateRequest, TEventUpdateRequest } from '../../types/events';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
export const getEventList = async (params?: TPaginationParams): Promise<TApiPaginated<TEventsListItem>> => {
|
||||
const response = await api.get<TApiPaginated<TEventsListItem>>('/v1/landing/cms/events', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getEventById = async (id: string): Promise<TEventsDetailItem> => {
|
||||
const response = await api.get<ApiResponse<TEventsDetailItem>>(`/v1/landing/cms/events/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createEvent = async (data: TEventCreateRequest): Promise<TEventsDetailItem> => {
|
||||
const response = await api.post<ApiResponse<TEventsDetailItem>>('/v1/landing/cms/events/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateEvent = async (id: string, data: TEventUpdateRequest): Promise<TEventsDetailItem> => {
|
||||
const response = await api.patch<ApiResponse<TEventsDetailItem>>(`/v1/landing/cms/events/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deleteEvent = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/landing/cms/events/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
@@ -1 +1,87 @@
|
||||
export {};
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type {
|
||||
TGachaItemDto,
|
||||
TGachaItemCreateRequest,
|
||||
TGachaItemUpdateRequest,
|
||||
TGachaRollItemDto,
|
||||
TGachaRollCreateRequest,
|
||||
TGachaCreditDto,
|
||||
TGachaCreditAddRequest,
|
||||
TGachaClaimDetailDto,
|
||||
TGachaClaimCreateRequest,
|
||||
} from '../../types/gacha';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
// ----- Credits -----
|
||||
export const getUserCredits = async (): Promise<TGachaCreditDto> => {
|
||||
const response = await api.get<ApiResponse<TGachaCreditDto>>('/v1/gacha/credits/');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const addCredits = async (data: TGachaCreditAddRequest): Promise<TGachaCreditDto> => {
|
||||
const response = await api.post<ApiResponse<TGachaCreditDto>>('/v1/gacha/credits/add', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const consumeCredit = async (): Promise<{ message: string }> => {
|
||||
const response = await api.post<{ message: string }>('/v1/gacha/credits/consume');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ----- Items -----
|
||||
export const getGachaItemList = async (params?: TPaginationParams): Promise<TApiPaginated<TGachaItemDto>> => {
|
||||
const response = await api.get<TApiPaginated<TGachaItemDto>>('/v1/gacha/items/', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getGachaItemById = async (id: string): Promise<TGachaItemDto> => {
|
||||
const response = await api.get<ApiResponse<TGachaItemDto>>(`/v1/gacha/items/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createGachaItem = async (data: TGachaItemCreateRequest): Promise<TGachaItemDto> => {
|
||||
const response = await api.post<ApiResponse<TGachaItemDto>>('/v1/gacha/items/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateGachaItem = async (id: string, data: TGachaItemUpdateRequest): Promise<TGachaItemDto> => {
|
||||
const response = await api.put<ApiResponse<TGachaItemDto>>(`/v1/gacha/items/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deleteGachaItem = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/gacha/items/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ----- Rolls -----
|
||||
export const getGachaRollById = async (id: string): Promise<TGachaRollItemDto> => {
|
||||
const response = await api.get<ApiResponse<TGachaRollItemDto>>(`/v1/gacha/rolls/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createGachaRoll = async (data: TGachaRollCreateRequest): Promise<TGachaRollItemDto> => {
|
||||
const response = await api.post<ApiResponse<TGachaRollItemDto>>('/v1/gacha/rolls/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const executeGachaRoll = async (): Promise<TGachaRollItemDto> => {
|
||||
const response = await api.post<ApiResponse<TGachaRollItemDto>>('/v1/gacha/rolls/execute');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deleteGachaRoll = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/gacha/rolls/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ----- Claims -----
|
||||
export const getGachaClaimById = async (id: string): Promise<TGachaClaimDetailDto> => {
|
||||
const response = await api.get<ApiResponse<TGachaClaimDetailDto>>(`/v1/gacha/claims/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createGachaClaim = async (data: TGachaClaimCreateRequest): Promise<TGachaClaimDetailDto> => {
|
||||
const response = await api.post<ApiResponse<TGachaClaimDetailDto>>('/v1/gacha/claims/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
@@ -1,51 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import { useAuthStore } from '../hooks/auth';
|
||||
|
||||
const HACKATHON_API_URL = 'https://api.hackathon.imphnen.dev/api/v1';
|
||||
|
||||
export const hackathonApi = axios.create({
|
||||
baseURL: HACKATHON_API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
hackathonApi.interceptors.request.use(
|
||||
(config) => {
|
||||
const { session } = useAuthStore.getState();
|
||||
if (session?.token?.access_token) {
|
||||
config.headers.Authorization = `Bearer ${session.token.access_token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(new Error(error.message || 'Request failed'));
|
||||
}
|
||||
);
|
||||
|
||||
hackathonApi.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
const isAuthPage = globalThis.window !== undefined && globalThis.location.pathname.startsWith('/auth');
|
||||
const isCertificatePage = globalThis.window !== undefined && globalThis.location.pathname.startsWith('/certificate/');
|
||||
|
||||
if (!isAuthPage && !isCertificatePage) {
|
||||
useAuthStore.getState().clearSession();
|
||||
if (globalThis.window !== undefined) {
|
||||
globalThis.location.href = '/auth/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const backendMsg = error?.response?.data?.message;
|
||||
if (backendMsg && typeof backendMsg === 'string') {
|
||||
return Promise.reject(new Error(backendMsg));
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(error.message || 'Request failed'));
|
||||
}
|
||||
);
|
||||
// All API calls now go through the main `api` instance (api.imphnen.dev).
|
||||
// This file re-exports `api` as `hackathonApi` for backward compatibility.
|
||||
export { api as hackathonApi } from './index';
|
||||
|
||||
export interface HackathonApiResponse<T> {
|
||||
data: T;
|
||||
|
||||
@@ -7,6 +7,11 @@ export * from './mentors';
|
||||
export * from './upload';
|
||||
export * from './hackathon';
|
||||
export * from './admin';
|
||||
export * from './roles';
|
||||
export * from './permissions';
|
||||
export * from './events';
|
||||
export * from './testimonials';
|
||||
export * from './sessions';
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
@@ -140,7 +145,7 @@ async function handleTokenRefresh(originalRequest: AxiosRequestConfig) {
|
||||
}
|
||||
|
||||
async function refreshAccessToken(refreshToken: string) {
|
||||
return axios.post(`${getBaseURL()}/auth/refresh`, {
|
||||
return axios.post(`${getBaseURL()}/v1/iam/auth/refresh`, {
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,34 +1,61 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type {
|
||||
MentorDetailResponseDto,
|
||||
MentorUpdateRequestDto
|
||||
} from '../../types/mentors';
|
||||
import type { MentorDetailResponseDto, MentorUpdateRequestDto } from '../../types/mentors';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
export interface MentorService {
|
||||
getMentorMe(): Promise<MentorDetailResponseDto>;
|
||||
getMentorById(id: string): Promise<MentorDetailResponseDto>;
|
||||
updateMentorMe(data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto>;
|
||||
updateMentorById(id: string, data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto>;
|
||||
}
|
||||
|
||||
export const mentorService: MentorService = {
|
||||
async getMentorMe() {
|
||||
const response = await api.get<ApiResponse<MentorDetailResponseDto>>('/mentors/me');
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async getMentorById(id: string) {
|
||||
const response = await api.get<ApiResponse<MentorDetailResponseDto>>(`/mentors/detail/${id}`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async updateMentorMe(data: MentorUpdateRequestDto) {
|
||||
const response = await api.put<ApiResponse<MentorDetailResponseDto>>('/mentors/update/me', data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async updateMentorById(id: string, data: MentorUpdateRequestDto) {
|
||||
const response = await api.put<ApiResponse<MentorDetailResponseDto>>(`/mentors/update/${id}`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
export type MentorRegisterRequest = MentorUpdateRequestDto & {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export const registerMentor = async (data: MentorRegisterRequest): Promise<MentorDetailResponseDto> => {
|
||||
const response = await api.post<ApiResponse<MentorDetailResponseDto>>('/v1/dimentorin/mentors/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getMentorList = async (params?: TPaginationParams): Promise<TApiPaginated<MentorDetailResponseDto>> => {
|
||||
const response = await api.get<TApiPaginated<MentorDetailResponseDto>>('/v1/dimentorin/mentors', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getMentorMe = async (): Promise<MentorDetailResponseDto> => {
|
||||
const response = await api.get<ApiResponse<MentorDetailResponseDto>>('/v1/dimentorin/mentors/me');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getMentorStatus = async (): Promise<{ status: string }> => {
|
||||
const response = await api.get<ApiResponse<{ status: string }>>('/v1/dimentorin/mentors/me/status');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getMentorById = async (id: string): Promise<MentorDetailResponseDto> => {
|
||||
const response = await api.get<ApiResponse<MentorDetailResponseDto>>(`/v1/dimentorin/mentors/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateMentorMe = async (data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto> => {
|
||||
const response = await api.put<ApiResponse<MentorDetailResponseDto>>('/v1/dimentorin/mentors/me/update', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateMentorById = async (id: string, data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto> => {
|
||||
const response = await api.put<ApiResponse<MentorDetailResponseDto>>(`/v1/dimentorin/mentors/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deleteMentor = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/dimentorin/mentors/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const verifyMentor = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.put<{ message: string }>(`/v1/dimentorin/mentors/verify/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Legacy service object for backward compatibility
|
||||
export const mentorService = {
|
||||
getMentorMe,
|
||||
getMentorById,
|
||||
updateMentorMe,
|
||||
updateMentorById: (id: string, data: MentorUpdateRequestDto) => updateMentorById(id, data),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type { TPermissionItem, TPermissionCreateRequest, TPermissionUpdateRequest } from '../../types/permissions';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
export const getPermissionList = async (params?: TPaginationParams): Promise<TApiPaginated<TPermissionItem>> => {
|
||||
const response = await api.get<TApiPaginated<TPermissionItem>>('/v1/iam/permissions', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getPermissionById = async (id: string): Promise<TPermissionItem> => {
|
||||
const response = await api.get<ApiResponse<TPermissionItem>>(`/v1/iam/permissions/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createPermission = async (data: TPermissionCreateRequest): Promise<TPermissionItem> => {
|
||||
const response = await api.post<ApiResponse<TPermissionItem>>('/v1/iam/permissions/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updatePermission = async (id: string, data: TPermissionUpdateRequest): Promise<TPermissionItem> => {
|
||||
const response = await api.put<ApiResponse<TPermissionItem>>(`/v1/iam/permissions/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deletePermission = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/iam/permissions/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type { TRolesListItem, TRoleDetailItem, TRoleCreateRequest, TRoleUpdateRequest } from '../../types/roles';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
export const getRoleList = async (params?: TPaginationParams): Promise<TApiPaginated<TRolesListItem>> => {
|
||||
const response = await api.get<TApiPaginated<TRolesListItem>>('/v1/iam/roles', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getRoleById = async (id: string): Promise<TRoleDetailItem> => {
|
||||
const response = await api.get<ApiResponse<TRoleDetailItem>>(`/v1/iam/roles/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createRole = async (data: TRoleCreateRequest): Promise<TRoleDetailItem> => {
|
||||
const response = await api.post<ApiResponse<TRoleDetailItem>>('/v1/iam/roles/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateRole = async (id: string, data: TRoleUpdateRequest): Promise<TRoleDetailItem> => {
|
||||
const response = await api.put<ApiResponse<TRoleDetailItem>>(`/v1/iam/roles/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deleteRole = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/iam/roles/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type {
|
||||
TBookSessionRequest,
|
||||
TBookSessionResponse,
|
||||
TUpdateSessionStatusRequest,
|
||||
TUpdateSessionStatusResponse,
|
||||
TSessionFeedbackRequest,
|
||||
TSessionFeedbackResponse,
|
||||
TSessionListResponse,
|
||||
TMentorAvailability,
|
||||
} from '../../types/sessions';
|
||||
|
||||
export const getMentorAvailability = async (mentorId: string): Promise<TMentorAvailability> => {
|
||||
const response = await api.get<ApiResponse<TMentorAvailability>>(
|
||||
`/v1/dimentorin/mentors/${mentorId}/availability`
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const bookSession = async (mentorId: string, data: TBookSessionRequest): Promise<TBookSessionResponse> => {
|
||||
const response = await api.post<ApiResponse<TBookSessionResponse>>(
|
||||
`/v1/dimentorin/mentors/${mentorId}/sessions/create`,
|
||||
data
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getMentorSessions = async (mentorId: string, params?: { status?: string }): Promise<TSessionListResponse> => {
|
||||
const response = await api.get<ApiResponse<TSessionListResponse>>(
|
||||
`/v1/dimentorin/mentors/${mentorId}/sessions`,
|
||||
{ params }
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getMySessions = async (params?: { status?: string }): Promise<TSessionListResponse> => {
|
||||
const response = await api.get<ApiResponse<TSessionListResponse>>(
|
||||
'/v1/dimentorin/sessions/me',
|
||||
{ params }
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateSessionStatus = async (
|
||||
id: string,
|
||||
data: TUpdateSessionStatusRequest
|
||||
): Promise<TUpdateSessionStatusResponse> => {
|
||||
const response = await api.put<ApiResponse<TUpdateSessionStatusResponse>>(
|
||||
`/v1/dimentorin/sessions/update/${id}/status`,
|
||||
data
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const submitFeedback = async (
|
||||
id: string,
|
||||
data: TSessionFeedbackRequest
|
||||
): Promise<TSessionFeedbackResponse> => {
|
||||
const response = await api.post<ApiResponse<TSessionFeedbackResponse>>(
|
||||
`/v1/dimentorin/sessions/${id}/feedback/create`,
|
||||
data
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
TUpdateTeamRequest,
|
||||
TInviteMemberRequest,
|
||||
TJoinTeamRequest,
|
||||
TManageMemberRequest,
|
||||
TSubmitProjectRequest,
|
||||
TTeamListResponse,
|
||||
TTeamDetailResponse,
|
||||
@@ -14,8 +13,6 @@ import type {
|
||||
TProjectSubmissionResponse,
|
||||
} from '../../types/teams';
|
||||
|
||||
const TEAMS_BASE_URL = '/teams';
|
||||
|
||||
export const getTeams = async (params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
@@ -23,86 +20,93 @@ export const getTeams = async (params?: {
|
||||
visibility?: string;
|
||||
search?: string;
|
||||
}) => {
|
||||
const response = await api.get<TTeamListResponse>(TEAMS_BASE_URL, { params });
|
||||
const response = await api.get<TTeamListResponse>('/v1/hackathon/teams/browse', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTeamById = async (teamId: string) => {
|
||||
const response = await api.get<TTeamDetailResponse>(`${TEAMS_BASE_URL}/${teamId}`);
|
||||
const response = await api.get<TTeamDetailResponse>(`/v1/hackathon/teams/${teamId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const createTeam = async (data: TCreateTeamRequest) => {
|
||||
const response = await api.post<TTeamDetailResponse>(TEAMS_BASE_URL, data);
|
||||
const response = await api.post<TTeamDetailResponse>('/v1/hackathon/teams', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateTeam = async (teamId: string, data: TUpdateTeamRequest) => {
|
||||
const response = await api.put<TTeamDetailResponse>(`${TEAMS_BASE_URL}/${teamId}`, data);
|
||||
const response = await api.put<TTeamDetailResponse>(`/v1/hackathon/teams/${teamId}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteTeam = async (teamId: string) => {
|
||||
const response = await api.delete(`/v1/hackathon/teams/${teamId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTeamMembers = async (teamId: string) => {
|
||||
const response = await api.get<TTeamMembersResponse>(`${TEAMS_BASE_URL}/${teamId}/members`);
|
||||
const response = await api.get<TTeamMembersResponse>(`/v1/hackathon/teams/${teamId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const inviteMember = async (teamId: string, data: TInviteMemberRequest) => {
|
||||
const response = await api.post(`${TEAMS_BASE_URL}/${teamId}/invite`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const manageMember = async (teamId: string, userId: string, data: TManageMemberRequest) => {
|
||||
const response = await api.put(`${TEAMS_BASE_URL}/${teamId}/members/${userId}`, data);
|
||||
const response = await api.post(`/v1/hackathon/invitations/teams/${teamId}/invite`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const removeMember = async (teamId: string, userId: string) => {
|
||||
const response = await api.delete(`${TEAMS_BASE_URL}/${teamId}/members/${userId}`);
|
||||
const response = await api.delete(`/v1/hackathon/teams/${teamId}/members/${userId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const joinTeam = async (teamId: string, data: TJoinTeamRequest) => {
|
||||
const response = await api.post(`${TEAMS_BASE_URL}/${teamId}/join-request`, data);
|
||||
const response = await api.post(`/v1/hackathon/join-requests/teams/${teamId}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTeamJoinRequests = async (teamId: string) => {
|
||||
const response = await api.get<TTeamJoinRequestsResponse>(`${TEAMS_BASE_URL}/${teamId}/join-requests`);
|
||||
const response = await api.get<TTeamJoinRequestsResponse>(
|
||||
`/v1/hackathon/join-requests/teams/${teamId}/pending`
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const respondToJoinRequest = async (teamId: string, requestId: string, action: 'approve' | 'reject') => {
|
||||
const response = await api.put(`${TEAMS_BASE_URL}/${teamId}/join-requests/${requestId}`, { action });
|
||||
export const respondToJoinRequest = async (requestId: string, action: 'accept' | 'reject') => {
|
||||
const response = await api.post(`/v1/hackathon/join-requests/${requestId}/respond`, { action });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getMyInvitations = async () => {
|
||||
const response = await api.get<TTeamInvitationsResponse>(`${TEAMS_BASE_URL}/invitations/me`);
|
||||
const response = await api.get<TTeamInvitationsResponse>('/v1/hackathon/invitations/my');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const respondToInvitation = async (invitationId: string, action: 'accept' | 'reject') => {
|
||||
const response = await api.put(`${TEAMS_BASE_URL}/invitations/${invitationId}`, { action });
|
||||
const response = await api.post(`/v1/hackathon/invitations/${invitationId}/respond`, { action });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getMyTeams = async () => {
|
||||
const response = await api.get<TTeamListResponse>(`${TEAMS_BASE_URL}/me`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const submitProject = async (teamId: string, data: TSubmitProjectRequest) => {
|
||||
const response = await api.post<TProjectSubmissionResponse>(`${TEAMS_BASE_URL}/${teamId}/submission`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTeamSubmission = async (teamId: string) => {
|
||||
const response = await api.get<TProjectSubmissionResponse>(`${TEAMS_BASE_URL}/${teamId}/submission`);
|
||||
const response = await api.get('/v1/hackathon/teams/my');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const leaveTeam = async (teamId: string) => {
|
||||
const response = await api.post(`${TEAMS_BASE_URL}/${teamId}/leave`);
|
||||
const response = await api.post(`/v1/hackathon/teams/${teamId}/leave`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const submitProject = async (teamId: string, data: TSubmitProjectRequest) => {
|
||||
const response = await api.post<TProjectSubmissionResponse>(
|
||||
`/v1/hackathon/submissions/teams/${teamId}`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTeamSubmission = async (teamId: string) => {
|
||||
const response = await api.get<TProjectSubmissionResponse>(
|
||||
`/v1/hackathon/submissions/teams/${teamId}`
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type {
|
||||
TTestimonialsListItem,
|
||||
TTestimonialsDetailItem,
|
||||
TTestimonialCreateRequest,
|
||||
TTestimonialUpdateRequest,
|
||||
} from '../../types/testimonials';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
export const getTestimonialList = async (params?: TPaginationParams): Promise<TApiPaginated<TTestimonialsListItem>> => {
|
||||
const response = await api.get<TApiPaginated<TTestimonialsListItem>>('/v1/landing/cms/testimonials', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTestimonialById = async (id: string): Promise<TTestimonialsDetailItem> => {
|
||||
const response = await api.get<ApiResponse<TTestimonialsDetailItem>>(`/v1/landing/cms/testimonials/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createTestimonial = async (data: TTestimonialCreateRequest): Promise<TTestimonialsDetailItem> => {
|
||||
const response = await api.post<ApiResponse<TTestimonialsDetailItem>>('/v1/landing/cms/testimonials/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateTestimonial = async (id: string, data: TTestimonialUpdateRequest): Promise<TTestimonialsDetailItem> => {
|
||||
const response = await api.patch<ApiResponse<TTestimonialsDetailItem>>(`/v1/landing/cms/testimonials/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const deleteTestimonial = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/landing/cms/testimonials/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
@@ -1,59 +1,39 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
|
||||
export interface UploadResponse {
|
||||
filename: string;
|
||||
original_filename: string;
|
||||
uploaded_path: string;
|
||||
filename?: string;
|
||||
original_filename?: string;
|
||||
uploaded_path?: string;
|
||||
url: string;
|
||||
size: number;
|
||||
content_type: string;
|
||||
file_type: string;
|
||||
user_id: string;
|
||||
email: string;
|
||||
size?: number;
|
||||
content_type?: string;
|
||||
file_type?: string;
|
||||
user_id?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface UploadService {
|
||||
uploadFile(file: File): Promise<UploadResponse>;
|
||||
uploadAvatar(file: File): Promise<UploadResponse>;
|
||||
uploadCV(file: File): Promise<UploadResponse>;
|
||||
}
|
||||
const multipartPost = async (url: string, file: File, fieldName = 'file'): Promise<UploadResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append(fieldName, file);
|
||||
const response = await api.post<ApiResponse<UploadResponse>>(url, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const uploadService: UploadService = {
|
||||
async uploadFile(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
export const uploadUserFile = (file: File) => multipartPost('/v1/iam/users/upload', file);
|
||||
|
||||
const response = await api.post<ApiResponse<UploadResponse>>('/users/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
return response.data.data;
|
||||
},
|
||||
export const uploadHackathonFile = (file: File) => multipartPost('/v1/hackathon/upload', file);
|
||||
|
||||
async uploadAvatar(file: File) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
throw new Error('File harus berupa gambar');
|
||||
}
|
||||
export const uploadHackathonAvatar = (file: File) => multipartPost('/v1/hackathon/upload/avatar', file);
|
||||
|
||||
const maxSize = 5 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('Ukuran file maksimal 5MB');
|
||||
}
|
||||
export const uploadHackathonTeamFile = (file: File) => multipartPost('/v1/hackathon/upload/team', file);
|
||||
|
||||
return this.uploadFile(file);
|
||||
},
|
||||
export const uploadHackathonSubmission = (file: File) => multipartPost('/v1/hackathon/upload/submission', file);
|
||||
|
||||
async uploadCV(file: File) {
|
||||
if (file.type !== 'application/pdf') {
|
||||
throw new Error('CV harus berupa file PDF');
|
||||
}
|
||||
|
||||
const maxSize = 10 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('Ukuran file maksimal 10MB');
|
||||
}
|
||||
|
||||
return this.uploadFile(file);
|
||||
},
|
||||
// Legacy service object for backward compatibility
|
||||
export const uploadService = {
|
||||
uploadFile: uploadUserFile,
|
||||
uploadAvatar: uploadUserFile,
|
||||
uploadCV: uploadUserFile,
|
||||
};
|
||||
|
||||
@@ -1,89 +1,69 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import { TUserItem } from '../../types/users';
|
||||
import type {
|
||||
TUsersListItem,
|
||||
TUsersDetailItem,
|
||||
TUserCreateRequest,
|
||||
TUserUpdateRequest,
|
||||
} from '../../types/users';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
|
||||
export interface UserDetailResponseDto extends TUserItem {
|
||||
bio?: string;
|
||||
location?: string;
|
||||
website_url?: string;
|
||||
linkedin_url?: string;
|
||||
github_url?: string;
|
||||
twitter_url?: string;
|
||||
skills?: string[];
|
||||
career_status?: string;
|
||||
experience?: Array<{
|
||||
id: string;
|
||||
company: string;
|
||||
position: string;
|
||||
duration: string;
|
||||
period: string;
|
||||
}>;
|
||||
education?: Array<{
|
||||
id: string;
|
||||
institution: string;
|
||||
degree: string;
|
||||
field: string;
|
||||
period: string;
|
||||
}>;
|
||||
}
|
||||
// Re-export for backward compatibility
|
||||
export type UserDetailResponseDto = TUsersDetailItem;
|
||||
export type UserUpdateRequestDto = TUserUpdateRequest;
|
||||
|
||||
export interface UserUpdateRequestDto {
|
||||
fullname?: string;
|
||||
bio?: string;
|
||||
location?: string;
|
||||
website_url?: string;
|
||||
linkedin_url?: string;
|
||||
github_url?: string;
|
||||
twitter_url?: string;
|
||||
skills?: string[];
|
||||
phone_number?: string;
|
||||
birthdate?: string;
|
||||
gender?: string;
|
||||
career_status?: string;
|
||||
avatar?: string;
|
||||
cv_url?: string;
|
||||
phone_for_verification?: string;
|
||||
domicile?: string;
|
||||
experience?: Array<{
|
||||
id: string;
|
||||
company: string;
|
||||
position: string;
|
||||
duration: string;
|
||||
period: string;
|
||||
}>;
|
||||
education?: Array<{
|
||||
id: string;
|
||||
institution: string;
|
||||
degree: string;
|
||||
field: string;
|
||||
period: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface UserService {
|
||||
getUserMe(): Promise<UserDetailResponseDto>;
|
||||
getUserById(id: string): Promise<UserDetailResponseDto>;
|
||||
updateUserMe(data: UserUpdateRequestDto): Promise<UserDetailResponseDto>;
|
||||
updateUserById(id: string, data: UserUpdateRequestDto): Promise<UserDetailResponseDto>;
|
||||
}
|
||||
|
||||
export const userService: UserService = {
|
||||
async getUserMe() {
|
||||
const response = await api.get<ApiResponse<UserDetailResponseDto>>('/users/me');
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async getUserById(id: string) {
|
||||
const response = await api.get<ApiResponse<UserDetailResponseDto>>(`/users/detail/${id}`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async updateUserMe(data: UserUpdateRequestDto) {
|
||||
const response = await api.put<ApiResponse<UserDetailResponseDto>>('/users/update/me', data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async updateUserById(id: string, data: UserUpdateRequestDto) {
|
||||
const response = await api.put<ApiResponse<UserDetailResponseDto>>(`/users/${id}`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
export const getUserList = async (params?: TPaginationParams): Promise<TApiPaginated<TUsersListItem>> => {
|
||||
const response = await api.get<TApiPaginated<TUsersListItem>>('/v1/iam/users', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getUserMe = async (): Promise<TUsersDetailItem> => {
|
||||
const response = await api.get<ApiResponse<TUsersDetailItem>>('/v1/iam/users/me');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getUserById = async (id: string): Promise<TUsersDetailItem> => {
|
||||
const response = await api.get<ApiResponse<TUsersDetailItem>>(`/v1/iam/users/detail/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createUser = async (data: TUserCreateRequest): Promise<TUsersDetailItem> => {
|
||||
const response = await api.post<ApiResponse<TUsersDetailItem>>('/v1/iam/users/create', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateUserMe = async (data: TUserUpdateRequest): Promise<TUsersDetailItem> => {
|
||||
const response = await api.put<ApiResponse<TUsersDetailItem>>('/v1/iam/users/update/me', data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateUserById = async (id: string, data: TUserUpdateRequest): Promise<TUsersDetailItem> => {
|
||||
const response = await api.put<ApiResponse<TUsersDetailItem>>(`/v1/iam/users/update/${id}`, data);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const activateUser = async (id: string, is_active: boolean): Promise<{ message: string }> => {
|
||||
const response = await api.put<{ message: string }>(`/v1/iam/users/activate/${id}`, { is_active });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteUser = async (id: string): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>(`/v1/iam/users/delete/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const uploadUserFile = async (file: File): Promise<{ url: string }> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const response = await api.post<ApiResponse<{ url: string }>>('/v1/iam/users/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
// Legacy service object for backward compatibility
|
||||
export const userService = {
|
||||
getUserMe,
|
||||
getUserById,
|
||||
updateUserMe,
|
||||
updateUserById: (id: string, data: TUserUpdateRequest) => updateUserById(id, data),
|
||||
};
|
||||
|
||||
@@ -1,242 +1,140 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
|
||||
import { backofficeApi, BackofficeApiResponse } from '../../api/backoffice';
|
||||
import { useAuthStore } from './use-auth-store';
|
||||
import {
|
||||
postLogin,
|
||||
postRegister,
|
||||
postVerifyEmail,
|
||||
postSendOtp,
|
||||
postForgotPassword,
|
||||
postNewPassword,
|
||||
} from '../../api/auth';
|
||||
import { getUserMe } from '../../api/users';
|
||||
|
||||
export * from './use-auth-store';
|
||||
|
||||
interface TokenInfo {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
fullname: string;
|
||||
phone_number?: string;
|
||||
avatar?: string;
|
||||
birthdate?: string;
|
||||
gender?: string;
|
||||
is_active: boolean;
|
||||
location?: string;
|
||||
bio?: string;
|
||||
skills?: string[];
|
||||
role_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
interface AuthResponse {
|
||||
token: TokenInfo;
|
||||
user: User;
|
||||
}
|
||||
|
||||
interface SessionResponse {
|
||||
user: User;
|
||||
}
|
||||
|
||||
interface MessageResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface SignupRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
fullname: string;
|
||||
}
|
||||
|
||||
interface GitHubAuthRequest {
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface ForgotPasswordRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface ResetPasswordRequest {
|
||||
access_token: string;
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
export const useLogin = () => {
|
||||
const { setSession } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: LoginRequest) => {
|
||||
const response = await hackathonApi.post<
|
||||
HackathonApiResponse<AuthResponse>
|
||||
>('/auth/login', data);
|
||||
return response.data.data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
mutationFn: postLogin,
|
||||
onSuccess: (res) => {
|
||||
const u = res.data?.user;
|
||||
const t = res.data?.token;
|
||||
if (!u || !t) return;
|
||||
setSession({
|
||||
token: data.token,
|
||||
token: t,
|
||||
user: {
|
||||
id: data.user.id,
|
||||
email: data.user.email,
|
||||
fullname: data.user.fullname,
|
||||
phone_number: data.user.phone_number || '',
|
||||
avatar: data.user.avatar || '',
|
||||
birthdate: data.user.birthdate || '',
|
||||
gender: data.user.gender || '',
|
||||
is_active: data.user.is_active,
|
||||
location: data.user.location,
|
||||
bio: data.user.bio,
|
||||
skills: data.user.skills,
|
||||
role: {
|
||||
id: '',
|
||||
name: 'user',
|
||||
permissions: [],
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
},
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
fullname: u.fullname,
|
||||
phone_number: u.phone_number || '',
|
||||
avatar: u.avatar || '',
|
||||
birthdate: u.birthdate || '',
|
||||
gender: u.gender || '',
|
||||
is_active: u.is_active,
|
||||
location: u.location,
|
||||
bio: u.bio,
|
||||
skills: u.skills,
|
||||
role: u.role ?? { id: '', name: 'user', permissions: [], created_at: '', updated_at: '' },
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useSignup = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (data: SignupRequest) => {
|
||||
const response = await hackathonApi.post<
|
||||
HackathonApiResponse<MessageResponse>
|
||||
>('/auth/signup', data);
|
||||
return response.data.data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useGitHubCallback = () => {
|
||||
const { setSession } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: GitHubAuthRequest) => {
|
||||
const response = await hackathonApi.post<
|
||||
HackathonApiResponse<AuthResponse>
|
||||
>('/auth/github', data);
|
||||
return response.data.data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setSession({
|
||||
token: data.token,
|
||||
user: {
|
||||
id: data.user.id,
|
||||
email: data.user.email,
|
||||
fullname: data.user.fullname,
|
||||
phone_number: data.user.phone_number || '',
|
||||
avatar: data.user.avatar || '',
|
||||
birthdate: data.user.birthdate || '',
|
||||
gender: data.user.gender || '',
|
||||
is_active: data.user.is_active,
|
||||
location: data.user.location,
|
||||
bio: data.user.bio,
|
||||
skills: data.user.skills,
|
||||
role: {
|
||||
id: '',
|
||||
name: 'user',
|
||||
permissions: [],
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useSessionQuery = () => {
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['auth-session'],
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<
|
||||
HackathonApiResponse<SessionResponse>
|
||||
>('/auth/session');
|
||||
return response.data.data;
|
||||
},
|
||||
enabled: !!session?.token,
|
||||
});
|
||||
};
|
||||
|
||||
export const useForgotPassword = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (data: ForgotPasswordRequest) => {
|
||||
const response = await hackathonApi.post<
|
||||
HackathonApiResponse<MessageResponse>
|
||||
>('/auth/forgot-password', data);
|
||||
return response.data.data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useResetPassword = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (data: ResetPasswordRequest) => {
|
||||
const response = await hackathonApi.post<
|
||||
HackathonApiResponse<MessageResponse>
|
||||
>('/auth/reset-password', data);
|
||||
return response.data.data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useSignOut = () => {
|
||||
const { clearSession } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
clearSession();
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useBackofficeLogin = () => {
|
||||
const { setSession } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: LoginRequest) => {
|
||||
const response = await backofficeApi.post<
|
||||
BackofficeApiResponse<AuthResponse>
|
||||
>('/auth/login', data);
|
||||
return response.data.data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
mutationFn: postLogin,
|
||||
onSuccess: (res) => {
|
||||
const u = res.data?.user;
|
||||
const t = res.data?.token;
|
||||
if (!u || !t) return;
|
||||
setSession({
|
||||
token: data.token,
|
||||
token: t,
|
||||
user: {
|
||||
id: data.user.id,
|
||||
email: data.user.email,
|
||||
fullname: data.user.fullname,
|
||||
phone_number: data.user.phone_number || '',
|
||||
avatar: data.user.avatar || '',
|
||||
birthdate: data.user.birthdate || '',
|
||||
gender: data.user.gender || '',
|
||||
is_active: data.user.is_active,
|
||||
location: data.user.location,
|
||||
bio: data.user.bio,
|
||||
skills: data.user.skills,
|
||||
role: {
|
||||
id: data.user.role_id || '',
|
||||
name: 'admin',
|
||||
permissions: [],
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
},
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
fullname: u.fullname,
|
||||
phone_number: u.phone_number || '',
|
||||
avatar: u.avatar || '',
|
||||
birthdate: u.birthdate || '',
|
||||
gender: u.gender || '',
|
||||
is_active: u.is_active,
|
||||
location: u.location,
|
||||
bio: u.bio,
|
||||
skills: u.skills,
|
||||
role: u.role ?? { id: '', name: 'admin', permissions: [], created_at: '', updated_at: '' },
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useSignup = () => {
|
||||
return useMutation({ mutationFn: postRegister });
|
||||
};
|
||||
|
||||
export const usePostLogin = () => {
|
||||
return useMutation({
|
||||
mutationFn: postLogin,
|
||||
onSuccess: (res) => res,
|
||||
});
|
||||
};
|
||||
|
||||
export const usePostRegister = () => {
|
||||
return useMutation({ mutationFn: postRegister });
|
||||
};
|
||||
|
||||
export const usePostVerifyEmail = () => {
|
||||
return useMutation({ mutationFn: postVerifyEmail });
|
||||
};
|
||||
|
||||
export const usePostSendOtp = () => {
|
||||
return useMutation({ mutationFn: postSendOtp });
|
||||
};
|
||||
|
||||
export const useForgotPassword = () => {
|
||||
return useMutation({ mutationFn: postForgotPassword });
|
||||
};
|
||||
|
||||
export const useResetPassword = () => {
|
||||
return useMutation({
|
||||
mutationFn: (data: { token: string; password: string }) => postNewPassword(data),
|
||||
});
|
||||
};
|
||||
|
||||
export const useSignOut = () => {
|
||||
const { clearSession } = useAuthStore();
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
clearSession();
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useSessionQuery = () => {
|
||||
const { session } = useAuthStore();
|
||||
return useQuery({
|
||||
queryKey: ['auth-session'],
|
||||
queryFn: async () => {
|
||||
const user = await getUserMe();
|
||||
return { user };
|
||||
},
|
||||
enabled: !!session?.token,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGoogleCallback = () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
throw new Error('Google OAuth not supported. Use GitHub OAuth instead.');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getGitHubOAuthUrl = (clientId: string, redirectUri: string) => {
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId,
|
||||
@@ -257,21 +155,23 @@ export const useGitHubAuth = () => {
|
||||
if (meta.env?.VITE_GITHUB_CLIENT_ID) clientId = meta.env.VITE_GITHUB_CLIENT_ID;
|
||||
} catch { /* not in Vite context */ }
|
||||
}
|
||||
if (!clientId) {
|
||||
throw new Error(
|
||||
'GitHub Client ID not configured.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!clientId) throw new Error('GitHub Client ID not configured.');
|
||||
const redirectUri = `${globalThis.location.origin}/auth/callback`;
|
||||
const url = getGitHubOAuthUrl(clientId, redirectUri);
|
||||
|
||||
return { url };
|
||||
return { url: getGitHubOAuthUrl(clientId, redirectUri) };
|
||||
};
|
||||
return { signInWithGitHub };
|
||||
};
|
||||
|
||||
return {
|
||||
signInWithGitHub,
|
||||
};
|
||||
export const useGitHubCallback = () => {
|
||||
const { setSession } = useAuthStore();
|
||||
return useMutation({
|
||||
mutationFn: async (data: { code: string }) => {
|
||||
// GitHub OAuth callback is handled by the backend redirect.
|
||||
// This hook exists for compatibility; in practice the backend
|
||||
// redirects to the frontend with a token in the URL params.
|
||||
throw new Error(`GitHub callback must be handled via backend redirect. Code: ${data.code}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useEmailAuth = () => {
|
||||
@@ -282,82 +182,20 @@ export const useEmailAuth = () => {
|
||||
const signInWithEmail = async (email: string, password: string) => {
|
||||
const result = await loginMutation.mutateAsync({ email, password });
|
||||
return {
|
||||
user: result.user,
|
||||
user: result.data?.user,
|
||||
session: {
|
||||
access_token: result.token.access_token,
|
||||
refresh_token: result.token.refresh_token,
|
||||
access_token: result.data?.token?.access_token,
|
||||
refresh_token: result.data?.token?.refresh_token,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const signUpWithEmail = async (
|
||||
email: string,
|
||||
password: string,
|
||||
fullname: string
|
||||
) => {
|
||||
const result = await signupMutation.mutateAsync({
|
||||
email,
|
||||
password,
|
||||
fullname,
|
||||
});
|
||||
return {
|
||||
message: result.message,
|
||||
};
|
||||
const signUpWithEmail = async (email: string, password: string, fullname: string) => {
|
||||
const result = await signupMutation.mutateAsync({ email, password, fullname });
|
||||
return { message: result.message };
|
||||
};
|
||||
|
||||
const signOut = async () => {
|
||||
clearSession();
|
||||
};
|
||||
const signOut = async () => { clearSession(); };
|
||||
|
||||
return {
|
||||
signInWithEmail,
|
||||
signUpWithEmail,
|
||||
signOut,
|
||||
};
|
||||
};
|
||||
|
||||
export const usePostLogin = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (data: LoginRequest) => {
|
||||
const response = await hackathonApi.post<
|
||||
HackathonApiResponse<AuthResponse>
|
||||
>('/auth/login', data);
|
||||
return { data: response.data.data };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const usePostRegister = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (data: SignupRequest) => {
|
||||
const response = await hackathonApi.post<
|
||||
HackathonApiResponse<AuthResponse>
|
||||
>('/auth/signup', data);
|
||||
return { data: response.data.data };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const usePostVerifyEmail = () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
throw new Error('Email verification not required with new backend');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const usePostSendOtp = () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
throw new Error('OTP not required with new backend');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useGoogleCallback = () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
throw new Error('Google OAuth not supported. Use GitHub OAuth instead.');
|
||||
},
|
||||
});
|
||||
return { signInWithEmail, signUpWithEmail, signOut };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
getEventList,
|
||||
getEventById,
|
||||
createEvent,
|
||||
updateEvent,
|
||||
deleteEvent,
|
||||
} from '../../api/events';
|
||||
import type { TEventCreateRequest, TEventUpdateRequest } from '../../types/events';
|
||||
import type { TPaginationParams } from '../../types/common';
|
||||
|
||||
export const eventKeys = {
|
||||
all: ['events'] as const,
|
||||
lists: () => [...eventKeys.all, 'list'] as const,
|
||||
list: (params?: TPaginationParams) => [...eventKeys.lists(), params] as const,
|
||||
detail: (id: string) => [...eventKeys.all, 'detail', id] as const,
|
||||
};
|
||||
|
||||
export const useEventList = (params?: TPaginationParams) => {
|
||||
return useQuery({
|
||||
queryKey: eventKeys.list(params),
|
||||
queryFn: () => getEventList(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useEventById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: eventKeys.detail(id),
|
||||
queryFn: () => getEventById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateEvent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: TEventCreateRequest) => createEvent(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: eventKeys.lists() }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateEvent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: TEventUpdateRequest }) => updateEvent(id, data),
|
||||
onSuccess: (_, vars) => {
|
||||
queryClient.invalidateQueries({ queryKey: eventKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: eventKeys.detail(vars.id) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteEvent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => deleteEvent(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: eventKeys.lists() }),
|
||||
});
|
||||
};
|
||||
@@ -1 +1,109 @@
|
||||
export {};
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
getUserCredits,
|
||||
addCredits,
|
||||
consumeCredit,
|
||||
getGachaItemList,
|
||||
getGachaItemById,
|
||||
createGachaItem,
|
||||
updateGachaItem,
|
||||
deleteGachaItem,
|
||||
executeGachaRoll,
|
||||
createGachaClaim,
|
||||
} from '../../api/gacha';
|
||||
import type {
|
||||
TGachaCreditAddRequest,
|
||||
TGachaItemCreateRequest,
|
||||
TGachaItemUpdateRequest,
|
||||
TGachaClaimCreateRequest,
|
||||
} from '../../types/gacha';
|
||||
import type { TPaginationParams } from '../../types/common';
|
||||
|
||||
export const gachaKeys = {
|
||||
credits: ['gacha-credits'] as const,
|
||||
items: (params?: TPaginationParams) => ['gacha-items', params] as const,
|
||||
item: (id: string) => ['gacha-item', id] as const,
|
||||
};
|
||||
|
||||
// ----- Credits -----
|
||||
export const useUserCredits = () => {
|
||||
return useQuery({
|
||||
queryKey: gachaKeys.credits,
|
||||
queryFn: getUserCredits,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddCredits = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: TGachaCreditAddRequest) => addCredits(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: gachaKeys.credits }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useConsumeCredit = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: consumeCredit,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: gachaKeys.credits }),
|
||||
});
|
||||
};
|
||||
|
||||
// ----- Items -----
|
||||
export const useGachaItemList = (params?: TPaginationParams) => {
|
||||
return useQuery({
|
||||
queryKey: gachaKeys.items(params),
|
||||
queryFn: () => getGachaItemList(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGachaItemById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: gachaKeys.item(id),
|
||||
queryFn: () => getGachaItemById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateGachaItem = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: TGachaItemCreateRequest) => createGachaItem(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['gacha-items'] }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateGachaItem = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: TGachaItemUpdateRequest }) => updateGachaItem(id, data),
|
||||
onSuccess: (_, vars) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['gacha-items'] });
|
||||
queryClient.invalidateQueries({ queryKey: gachaKeys.item(vars.id) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteGachaItem = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => deleteGachaItem(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['gacha-items'] }),
|
||||
});
|
||||
};
|
||||
|
||||
// ----- Roll -----
|
||||
export const useExecuteGachaRoll = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: executeGachaRoll,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: gachaKeys.credits }),
|
||||
});
|
||||
};
|
||||
|
||||
// ----- Claims -----
|
||||
export const useCreateGachaClaim = () => {
|
||||
return useMutation({
|
||||
mutationFn: (data: TGachaClaimCreateRequest) => createGachaClaim(data),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,3 +7,8 @@ export * from './teams';
|
||||
export * from './messages';
|
||||
export * from './winners';
|
||||
export * from './use-session';
|
||||
export * from './roles';
|
||||
export * from './permissions';
|
||||
export * from './events';
|
||||
export * from './testimonials';
|
||||
export * from './sessions';
|
||||
|
||||
@@ -1,45 +1,72 @@
|
||||
import { useQuery, useMutation, UseQueryResult, UseMutationResult, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { mentorService } from '../../api/mentors';
|
||||
import { MentorDetailResponseDto, MentorUpdateRequestDto } from '../../types/mentors';
|
||||
import { TResponseError } from '../../types/common';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
getMentorMe,
|
||||
getMentorById,
|
||||
getMentorList,
|
||||
updateMentorMe,
|
||||
updateMentorById,
|
||||
verifyMentor,
|
||||
deleteMentor,
|
||||
} from '../../api/mentors';
|
||||
import type { MentorDetailResponseDto, MentorUpdateRequestDto } from '../../types/mentors';
|
||||
import type { TPaginationParams } from '../../types/common';
|
||||
|
||||
export const useMentorMe = (options?: UseQueryOptions<MentorDetailResponseDto, TResponseError>): UseQueryResult<MentorDetailResponseDto, TResponseError> => {
|
||||
export const useMentorMe = () => {
|
||||
return useQuery({
|
||||
queryKey: ['mentor-me'],
|
||||
queryFn: () => mentorService.getMentorMe(),
|
||||
...options,
|
||||
queryFn: getMentorMe,
|
||||
});
|
||||
};
|
||||
|
||||
export const useMentorById = (id: string, options?: UseQueryOptions<MentorDetailResponseDto, TResponseError>): UseQueryResult<MentorDetailResponseDto, TResponseError> => {
|
||||
export const useMentorById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['mentor-by-id', id],
|
||||
queryFn: () => mentorService.getMentorById(id),
|
||||
queryFn: () => getMentorById(id),
|
||||
enabled: !!id,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateMentorMe = (): UseMutationResult<
|
||||
MentorDetailResponseDto,
|
||||
TResponseError,
|
||||
MentorUpdateRequestDto,
|
||||
unknown
|
||||
> => {
|
||||
export const useMentorList = (params?: TPaginationParams) => {
|
||||
return useQuery({
|
||||
queryKey: ['mentor-list', params],
|
||||
queryFn: () => getMentorList(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateMentorMe = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationKey: ['update-mentor-me'],
|
||||
mutationFn: (data) => mentorService.updateMentorMe(data),
|
||||
mutationFn: (data: MentorUpdateRequestDto) => updateMentorMe(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['mentor-me'] }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateMentorById = (): UseMutationResult<
|
||||
MentorDetailResponseDto,
|
||||
TResponseError,
|
||||
{ id: string; data: MentorUpdateRequestDto },
|
||||
unknown
|
||||
> => {
|
||||
export const useUpdateMentorById = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationKey: ['update-mentor-by-id'],
|
||||
mutationFn: ({ id, data }) => mentorService.updateMentorById(id, data),
|
||||
mutationFn: ({ id, data }: { id: string; data: MentorUpdateRequestDto }) =>
|
||||
updateMentorById(id, data),
|
||||
onSuccess: (_, vars) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['mentor-by-id', vars.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['mentor-list'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useVerifyMentor = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => verifyMentor(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['mentor-list'] }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteMentor = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => deleteMentor(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['mentor-list'] }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
|
||||
import { api } from '../../api/index';
|
||||
import { useAuthStore } from '../auth';
|
||||
|
||||
export type Message = {
|
||||
@@ -9,12 +9,7 @@ export type Message = {
|
||||
message: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
user?: {
|
||||
id: string;
|
||||
fullname: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
};
|
||||
user?: { id: string; fullname: string; avatar: string; email: string };
|
||||
};
|
||||
|
||||
export const messageKeys = {
|
||||
@@ -22,13 +17,13 @@ export const messageKeys = {
|
||||
team: (teamId: string) => [...messageKeys.all, 'team', teamId] as const,
|
||||
};
|
||||
|
||||
interface ApiResp<T> { data: T; message?: string; }
|
||||
|
||||
export const useTeamMessages = (teamId: string) => {
|
||||
return useQuery({
|
||||
queryKey: messageKeys.team(teamId),
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<HackathonApiResponse<Message[]>>(
|
||||
`/chat/teams/${teamId}`
|
||||
);
|
||||
const response = await api.get<ApiResp<Message[]>>(`/v1/hackathon/chat/teams/${teamId}`);
|
||||
return response.data.data || [];
|
||||
},
|
||||
enabled: !!teamId,
|
||||
@@ -43,20 +38,11 @@ export const useSendMessage = (teamId: string) => {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (message: string) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to send messages');
|
||||
}
|
||||
|
||||
const response = await hackathonApi.post<HackathonApiResponse<Message>>(
|
||||
`/chat/teams/${teamId}`,
|
||||
{ message }
|
||||
);
|
||||
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to send messages');
|
||||
const response = await api.post<ApiResp<Message>>(`/v1/hackathon/chat/teams/${teamId}`, { message });
|
||||
return response.data.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) });
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -65,10 +51,8 @@ export const useDeleteMessage = (teamId: string) => {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (messageId: string) => {
|
||||
await hackathonApi.delete(`/chat/messages/${messageId}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) });
|
||||
await api.delete(`/v1/hackathon/chat/messages/${messageId}`);
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
getPermissionList,
|
||||
getPermissionById,
|
||||
createPermission,
|
||||
updatePermission,
|
||||
deletePermission,
|
||||
} from '../../api/permissions';
|
||||
import type { TPermissionCreateRequest, TPermissionUpdateRequest } from '../../types/permissions';
|
||||
import type { TPaginationParams } from '../../types/common';
|
||||
|
||||
export const permissionKeys = {
|
||||
all: ['permissions'] as const,
|
||||
lists: () => [...permissionKeys.all, 'list'] as const,
|
||||
list: (params?: TPaginationParams) => [...permissionKeys.lists(), params] as const,
|
||||
detail: (id: string) => [...permissionKeys.all, 'detail', id] as const,
|
||||
};
|
||||
|
||||
export const usePermissionList = (params?: TPaginationParams) => {
|
||||
return useQuery({
|
||||
queryKey: permissionKeys.list(params),
|
||||
queryFn: () => getPermissionList(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const usePermissionById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: permissionKeys.detail(id),
|
||||
queryFn: () => getPermissionById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreatePermission = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: TPermissionCreateRequest) => createPermission(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: permissionKeys.lists() }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdatePermission = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: TPermissionUpdateRequest }) => updatePermission(id, data),
|
||||
onSuccess: (_, vars) => {
|
||||
queryClient.invalidateQueries({ queryKey: permissionKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: permissionKeys.detail(vars.id) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeletePermission = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => deletePermission(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: permissionKeys.lists() }),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getRoleList, getRoleById, createRole, updateRole, deleteRole } from '../../api/roles';
|
||||
import type { TRoleCreateRequest, TRoleUpdateRequest } from '../../types/roles';
|
||||
import type { TPaginationParams } from '../../types/common';
|
||||
|
||||
export const roleKeys = {
|
||||
all: ['roles'] as const,
|
||||
lists: () => [...roleKeys.all, 'list'] as const,
|
||||
list: (params?: TPaginationParams) => [...roleKeys.lists(), params] as const,
|
||||
detail: (id: string) => [...roleKeys.all, 'detail', id] as const,
|
||||
};
|
||||
|
||||
export const useRoleList = (params?: TPaginationParams) => {
|
||||
return useQuery({
|
||||
queryKey: roleKeys.list(params),
|
||||
queryFn: () => getRoleList(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useRoleById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: roleKeys.detail(id),
|
||||
queryFn: () => getRoleById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: TRoleCreateRequest) => createRole(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: roleKeys.lists() }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: TRoleUpdateRequest }) => updateRole(id, data),
|
||||
onSuccess: (_, vars) => {
|
||||
queryClient.invalidateQueries({ queryKey: roleKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: roleKeys.detail(vars.id) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => deleteRole(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: roleKeys.lists() }),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
getMentorAvailability,
|
||||
bookSession,
|
||||
getMentorSessions,
|
||||
getMySessions,
|
||||
updateSessionStatus,
|
||||
submitFeedback,
|
||||
} from '../../api/sessions';
|
||||
import type {
|
||||
TBookSessionRequest,
|
||||
TUpdateSessionStatusRequest,
|
||||
TSessionFeedbackRequest,
|
||||
} from '../../types/sessions';
|
||||
|
||||
export const sessionKeys = {
|
||||
all: ['sessions'] as const,
|
||||
mine: (params?: Record<string, unknown>) => [...sessionKeys.all, 'mine', params] as const,
|
||||
mentor: (id: string) => [...sessionKeys.all, 'mentor', id] as const,
|
||||
availability: (id: string) => [...sessionKeys.all, 'availability', id] as const,
|
||||
};
|
||||
|
||||
export const useMentorAvailability = (mentorId: string) => {
|
||||
return useQuery({
|
||||
queryKey: sessionKeys.availability(mentorId),
|
||||
queryFn: () => getMentorAvailability(mentorId),
|
||||
enabled: !!mentorId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useBookSession = (mentorId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: TBookSessionRequest) => bookSession(mentorId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sessionKeys.mentor(mentorId) });
|
||||
queryClient.invalidateQueries({ queryKey: sessionKeys.all });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useMentorSessions = (mentorId: string, params?: { status?: string }) => {
|
||||
return useQuery({
|
||||
queryKey: sessionKeys.mentor(mentorId),
|
||||
queryFn: () => getMentorSessions(mentorId, params),
|
||||
enabled: !!mentorId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useMySessions = (params?: { status?: string }) => {
|
||||
return useQuery({
|
||||
queryKey: sessionKeys.mine(params as Record<string, unknown>),
|
||||
queryFn: () => getMySessions(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSessionStatus = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: TUpdateSessionStatusRequest }) =>
|
||||
updateSessionStatus(id, data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: sessionKeys.all }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useSubmitFeedback = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: TSessionFeedbackRequest }) =>
|
||||
submitFeedback(id, data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: sessionKeys.all }),
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation, useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
|
||||
import { api } from '../../api/index';
|
||||
import { useAuthStore } from '../auth';
|
||||
import type {
|
||||
TCreateTeamRequest,
|
||||
@@ -23,110 +23,41 @@ export const teamKeys = {
|
||||
};
|
||||
|
||||
interface TeamMember {
|
||||
id: string;
|
||||
team_id: string;
|
||||
user_id: string;
|
||||
role: string;
|
||||
status: string;
|
||||
joined_at: string;
|
||||
user?: {
|
||||
id: string;
|
||||
email: string;
|
||||
fullname: string;
|
||||
avatar: string;
|
||||
};
|
||||
id: string; team_id: string; user_id: string; role: string; status: string;
|
||||
joined_at: string; user?: { id: string; email: string; fullname: string; avatar: string };
|
||||
}
|
||||
|
||||
interface Team {
|
||||
id: string;
|
||||
name: string;
|
||||
logo?: string;
|
||||
banner?: string;
|
||||
description?: string;
|
||||
city?: string;
|
||||
visibility: string;
|
||||
leader_id: string;
|
||||
created_at: string;
|
||||
leader?: {
|
||||
id: string;
|
||||
email: string;
|
||||
fullname: string;
|
||||
avatar: string;
|
||||
};
|
||||
members?: TeamMember[];
|
||||
member_count?: number;
|
||||
has_submission?: boolean;
|
||||
id: string; name: string; logo?: string; banner?: string; description?: string;
|
||||
city?: string; visibility: string; leader_id: string; created_at: string;
|
||||
leader?: { id: string; email: string; fullname: string; avatar: string };
|
||||
members?: TeamMember[]; member_count?: number; has_submission?: boolean;
|
||||
}
|
||||
|
||||
interface JoinRequest {
|
||||
id: string;
|
||||
team_id: string;
|
||||
user_id: string;
|
||||
message?: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
user?: {
|
||||
id: string;
|
||||
email: string;
|
||||
fullname: string;
|
||||
avatar: string;
|
||||
};
|
||||
id: string; team_id: string; user_id: string; message?: string; status: string;
|
||||
created_at: string; user?: { id: string; email: string; fullname: string; avatar: string };
|
||||
}
|
||||
|
||||
interface Invitation {
|
||||
id: string;
|
||||
team_id: string;
|
||||
inviter_id: string;
|
||||
invitee_email: string;
|
||||
invitee_id?: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
team?: Team;
|
||||
inviter?: {
|
||||
id: string;
|
||||
fullname: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
};
|
||||
id: string; team_id: string; inviter_id: string; invitee_email: string;
|
||||
invitee_id?: string; status: string; created_at: string; team?: Team;
|
||||
inviter?: { id: string; fullname: string; email: string; avatar: string };
|
||||
}
|
||||
|
||||
interface Submission {
|
||||
id: string;
|
||||
team_id: string;
|
||||
project_name: string;
|
||||
description?: string;
|
||||
repository_url?: string;
|
||||
demo_url?: string;
|
||||
video_url?: string;
|
||||
presentation_url?: string;
|
||||
status: string;
|
||||
submitted_at?: string;
|
||||
created_at: string;
|
||||
id: string; team_id: string; project_name: string; description?: string;
|
||||
repository_url?: string; demo_url?: string; video_url?: string;
|
||||
presentation_url?: string; status: string; submitted_at?: string; created_at: string;
|
||||
}
|
||||
interface ListMeta { page: number; per_page: number; total_page: number; total_data: number; }
|
||||
interface ListResponse<T> { message: string; data: T[]; meta: ListMeta; }
|
||||
interface ApiResp<T> { data: T; message?: string; }
|
||||
|
||||
interface ListResponseWithMeta<T> {
|
||||
message: string;
|
||||
data: T[];
|
||||
meta: {
|
||||
page: number;
|
||||
per_page: number;
|
||||
total_page: number;
|
||||
total_data: number;
|
||||
};
|
||||
}
|
||||
const TEAMS_PAGE_SIZE = 12;
|
||||
|
||||
export const useTeams = (params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
city?: string;
|
||||
visibility?: string;
|
||||
search?: string;
|
||||
minMembers?: number;
|
||||
maxMembers?: number;
|
||||
hasSubmission?: boolean;
|
||||
page?: number; limit?: number; city?: string; visibility?: string; search?: string;
|
||||
minMembers?: number; maxMembers?: number; hasSubmission?: boolean;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: teamKeys.list(params),
|
||||
queryKey: teamKeys.list(params as Record<string, unknown>),
|
||||
queryFn: async () => {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.page) queryParams.append('page', String(params.page));
|
||||
@@ -137,11 +68,10 @@ export const useTeams = (params?: {
|
||||
if (params?.minMembers) queryParams.append('min_members', String(params.minMembers));
|
||||
if (params?.maxMembers) queryParams.append('max_members', String(params.maxMembers));
|
||||
if (params?.hasSubmission !== undefined) queryParams.append('has_submission', String(params.hasSubmission));
|
||||
|
||||
const response = await hackathonApi.get<ListResponseWithMeta<Team>>(
|
||||
`/teams/browse${queryParams.toString() ? `?${queryParams.toString()}` : ''}`
|
||||
const qs = queryParams.toString();
|
||||
const response = await api.get<ListResponse<Team>>(
|
||||
`/v1/hackathon/teams/browse${qs ? `?${qs}` : ''}`
|
||||
);
|
||||
|
||||
const { data, meta } = response.data;
|
||||
return {
|
||||
teams: data || [],
|
||||
@@ -154,13 +84,7 @@ export const useTeams = (params?: {
|
||||
});
|
||||
};
|
||||
|
||||
const TEAMS_PAGE_SIZE = 12;
|
||||
|
||||
export const useInfiniteTeams = (params?: {
|
||||
city?: string;
|
||||
visibility?: string;
|
||||
search?: string;
|
||||
}) => {
|
||||
export const useInfiniteTeams = (params?: { city?: string; visibility?: string; search?: string }) => {
|
||||
return useInfiniteQuery({
|
||||
queryKey: [...teamKeys.lists(), 'infinite', params],
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
@@ -170,19 +94,12 @@ export const useInfiniteTeams = (params?: {
|
||||
if (params?.search) queryParams.append('search', params.search);
|
||||
if (params?.city) queryParams.append('city', params.city);
|
||||
if (params?.visibility) queryParams.append('visibility', params.visibility);
|
||||
|
||||
const response = await hackathonApi.get<HackathonApiResponse<Team[]>>(
|
||||
`/teams/browse?${queryParams.toString()}`
|
||||
);
|
||||
|
||||
const response = await api.get<ApiResp<Team[]>>(`/v1/hackathon/teams/browse?${queryParams}`);
|
||||
const teams = response.data.data || [];
|
||||
return {
|
||||
data: teams,
|
||||
nextPage: teams.length === TEAMS_PAGE_SIZE ? pageParam + 1 : undefined,
|
||||
};
|
||||
return { data: teams, nextPage: teams.length === TEAMS_PAGE_SIZE ? pageParam + 1 : undefined };
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage,
|
||||
getNextPageParam: (lastPage: { nextPage?: number }) => lastPage.nextPage,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -190,7 +107,7 @@ export const useTeamById = (teamId: string, enabled = true) => {
|
||||
return useQuery({
|
||||
queryKey: teamKeys.detail(teamId),
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<HackathonApiResponse<Team>>(`/teams/${teamId}`);
|
||||
const response = await api.get<ApiResp<Team>>(`/v1/hackathon/teams/${teamId}`);
|
||||
return { data: response.data.data };
|
||||
},
|
||||
enabled: enabled && !!teamId,
|
||||
@@ -200,22 +117,10 @@ export const useTeamById = (teamId: string, enabled = true) => {
|
||||
export const useCreateTeam = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: TCreateTeamRequest) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to create a team');
|
||||
}
|
||||
|
||||
const response = await hackathonApi.post<HackathonApiResponse<Team>>('/teams', {
|
||||
name: data.name,
|
||||
logo: data.logo,
|
||||
banner: data.banner,
|
||||
description: data.description,
|
||||
city: data.city,
|
||||
visibility: data.visibility,
|
||||
});
|
||||
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to create a team');
|
||||
const response = await api.post<ApiResp<Team>>('/v1/hackathon/teams', data);
|
||||
return { data: response.data.data };
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -228,22 +133,10 @@ export const useCreateTeam = () => {
|
||||
export const useUpdateTeam = (teamId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: TUpdateTeamRequest) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to update a team');
|
||||
}
|
||||
|
||||
const response = await hackathonApi.put<HackathonApiResponse<Team>>(`/teams/${teamId}`, {
|
||||
name: data.name,
|
||||
logo: data.logo,
|
||||
banner: data.banner,
|
||||
description: data.description,
|
||||
city: data.city,
|
||||
visibility: data.visibility,
|
||||
});
|
||||
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to update a team');
|
||||
const response = await api.put<ApiResp<Team>>(`/v1/hackathon/teams/${teamId}`, data);
|
||||
return { data: response.data.data };
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -257,7 +150,7 @@ export const useTeamMembers = (teamId: string, enabled = true) => {
|
||||
return useQuery({
|
||||
queryKey: teamKeys.members(teamId),
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<HackathonApiResponse<Team>>(`/teams/${teamId}`);
|
||||
const response = await api.get<ApiResp<Team>>(`/v1/hackathon/teams/${teamId}`);
|
||||
return { data: response.data.data?.members || [] };
|
||||
},
|
||||
enabled: enabled && !!teamId,
|
||||
@@ -267,32 +160,26 @@ export const useTeamMembers = (teamId: string, enabled = true) => {
|
||||
export const useInviteMember = (teamId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: TInviteMemberRequest) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to invite a member');
|
||||
}
|
||||
|
||||
const response = await hackathonApi.post<HackathonApiResponse<Invitation>>(
|
||||
`/teams/${teamId}/invite`,
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to invite a member');
|
||||
const response = await api.post<ApiResp<Invitation>>(
|
||||
`/v1/hackathon/invitations/teams/${teamId}/invite`,
|
||||
{ invitee_email: data.email }
|
||||
);
|
||||
|
||||
return { data: response.data.data };
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useManageMember = (teamId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ userId, data }: { userId: string; data: { role?: string; status?: string } }) => {
|
||||
throw new Error('Manage member functionality not yet implemented in backend');
|
||||
// Remove member is the available operation; role management not exposed by backend
|
||||
await api.delete(`/v1/hackathon/teams/${teamId}/members/${userId}`);
|
||||
return { success: true };
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
|
||||
@@ -304,14 +191,10 @@ export const useManageMember = (teamId: string) => {
|
||||
export const useRemoveMember = (teamId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (userId: string) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to remove a member');
|
||||
}
|
||||
|
||||
await hackathonApi.delete(`/teams/${teamId}/members/${userId}`);
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to remove a member');
|
||||
await api.delete(`/v1/hackathon/teams/${teamId}/members/${userId}`);
|
||||
return { success: true };
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -324,23 +207,16 @@ export const useRemoveMember = (teamId: string) => {
|
||||
export const useJoinTeam = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ teamId, data }: { teamId: string; data: TJoinTeamRequest }) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to join a team');
|
||||
}
|
||||
|
||||
const response = await hackathonApi.post<HackathonApiResponse<JoinRequest>>(
|
||||
`/join-requests/teams/${teamId}`,
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to join a team');
|
||||
const response = await api.post<ApiResp<JoinRequest>>(
|
||||
`/v1/hackathon/join-requests/teams/${teamId}`,
|
||||
{ message: data.message }
|
||||
);
|
||||
|
||||
return { data: response.data.data };
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: teamKeys.lists() }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -348,8 +224,8 @@ export const useTeamJoinRequests = (teamId: string, enabled = true) => {
|
||||
return useQuery({
|
||||
queryKey: teamKeys.joinRequests(teamId),
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<HackathonApiResponse<JoinRequest[]>>(
|
||||
`/join-requests/teams/${teamId}/pending`
|
||||
const response = await api.get<ApiResp<JoinRequest[]>>(
|
||||
`/v1/hackathon/join-requests/teams/${teamId}/pending`
|
||||
);
|
||||
return { data: response.data.data || [] };
|
||||
},
|
||||
@@ -360,19 +236,11 @@ export const useTeamJoinRequests = (teamId: string, enabled = true) => {
|
||||
export const useRespondToJoinRequest = (teamId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ requestId, action }: { requestId: string; action: 'approve' | 'reject' }) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to respond to join requests');
|
||||
}
|
||||
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to respond to join requests');
|
||||
const backendAction = action === 'approve' ? 'accept' : 'reject';
|
||||
|
||||
await hackathonApi.post(`/join-requests/${requestId}/respond`, {
|
||||
action: backendAction,
|
||||
});
|
||||
|
||||
await api.post(`/v1/hackathon/join-requests/${requestId}/respond`, { action: backendAction });
|
||||
return { success: true, action };
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -385,11 +253,10 @@ export const useRespondToJoinRequest = (teamId: string) => {
|
||||
|
||||
export const useMyInvitations = () => {
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useQuery({
|
||||
queryKey: teamKeys.myInvitations(),
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<HackathonApiResponse<Invitation[]>>('/invitations/my');
|
||||
const response = await api.get<ApiResp<Invitation[]>>('/v1/hackathon/invitations/my');
|
||||
return { data: response.data.data || [] };
|
||||
},
|
||||
enabled: !!session?.user?.id,
|
||||
@@ -399,15 +266,10 @@ export const useMyInvitations = () => {
|
||||
export const useRespondToInvitation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ invitationId, action }: { invitationId: string; action: 'accept' | 'reject' }) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
await hackathonApi.post(`/invitations/${invitationId}/respond`, { action });
|
||||
|
||||
if (!session?.user?.id) throw new Error('User not authenticated');
|
||||
await api.post(`/v1/hackathon/invitations/${invitationId}/respond`, { action });
|
||||
return { success: true, action };
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -420,13 +282,12 @@ export const useRespondToInvitation = () => {
|
||||
|
||||
export const useMyTeams = () => {
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useQuery({
|
||||
queryKey: teamKeys.myTeams(),
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<HackathonApiResponse<any[]>>('/teams/my');
|
||||
const response = await api.get<ApiResp<unknown[]>>('/v1/hackathon/teams/my');
|
||||
const rawData = response.data.data || [];
|
||||
const teams = rawData.map((item: any) => item.team || item);
|
||||
const teams = rawData.map((item: unknown) => (item as Record<string, unknown>).team ?? item);
|
||||
return { data: teams };
|
||||
},
|
||||
enabled: !!session?.user?.id,
|
||||
@@ -435,58 +296,22 @@ export const useMyTeams = () => {
|
||||
|
||||
export const useSubmitProject = (teamId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: TSubmitProjectRequest) => {
|
||||
let submissionId: string;
|
||||
|
||||
try {
|
||||
const existingResponse = await hackathonApi.get<HackathonApiResponse<Submission | null>>(
|
||||
`/submissions/teams/${teamId}`
|
||||
);
|
||||
|
||||
if (existingResponse.data.data?.id) {
|
||||
const response = await hackathonApi.put<HackathonApiResponse<Submission>>(
|
||||
`/submissions/${existingResponse.data.data.id}`,
|
||||
{
|
||||
project_name: data.project_name,
|
||||
description: data.description,
|
||||
repository_url: data.repository_url,
|
||||
demo_url: data.demo_url,
|
||||
video_url: data.video_url,
|
||||
presentation_url: data.presentation_url,
|
||||
screenshots: data.screenshots,
|
||||
}
|
||||
);
|
||||
submissionId = response.data.data.id;
|
||||
} else {
|
||||
throw new Error('No existing submission');
|
||||
}
|
||||
const existing = await api.get<ApiResp<Submission | null>>(`/v1/hackathon/submissions/teams/${teamId}`);
|
||||
if (existing.data.data?.id) {
|
||||
const res = await api.put<ApiResp<Submission>>(`/v1/hackathon/submissions/${existing.data.data.id}`, data);
|
||||
submissionId = res.data.data.id;
|
||||
} else throw new Error('No existing submission');
|
||||
} catch {
|
||||
const response = await hackathonApi.post<HackathonApiResponse<Submission>>(
|
||||
`/submissions/teams/${teamId}`,
|
||||
{
|
||||
project_name: data.project_name,
|
||||
description: data.description,
|
||||
repository_url: data.repository_url,
|
||||
demo_url: data.demo_url,
|
||||
video_url: data.video_url,
|
||||
presentation_url: data.presentation_url,
|
||||
screenshots: data.screenshots,
|
||||
}
|
||||
);
|
||||
submissionId = response.data.data.id;
|
||||
const res = await api.post<ApiResp<Submission>>(`/v1/hackathon/submissions/teams/${teamId}`, data);
|
||||
submissionId = res.data.data.id;
|
||||
}
|
||||
|
||||
await hackathonApi.post<HackathonApiResponse<Submission>>(
|
||||
`/submissions/${submissionId}/submit`
|
||||
);
|
||||
|
||||
const finalResponse = await hackathonApi.post<HackathonApiResponse<Submission>>(
|
||||
`/submissions/${submissionId}/confirm`
|
||||
);
|
||||
|
||||
return { data: finalResponse.data.data };
|
||||
await api.post<ApiResp<Submission>>(`/v1/hackathon/submissions/${submissionId}/submit`);
|
||||
const final = await api.post<ApiResp<Submission>>(`/v1/hackathon/submissions/${submissionId}/confirm`);
|
||||
return { data: final.data.data };
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: teamKeys.submission(teamId) });
|
||||
@@ -499,9 +324,7 @@ export const useTeamSubmission = (teamId: string, enabled = true) => {
|
||||
return useQuery({
|
||||
queryKey: teamKeys.submission(teamId),
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<HackathonApiResponse<Submission | null>>(
|
||||
`/submissions/teams/${teamId}`
|
||||
);
|
||||
const response = await api.get<ApiResp<Submission | null>>(`/v1/hackathon/submissions/teams/${teamId}`);
|
||||
return { data: response.data.data };
|
||||
},
|
||||
enabled: enabled && !!teamId,
|
||||
@@ -511,14 +334,10 @@ export const useTeamSubmission = (teamId: string, enabled = true) => {
|
||||
export const useLeaveTeam = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (teamId: string) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to leave a team');
|
||||
}
|
||||
|
||||
await hackathonApi.post(`/teams/${teamId}/leave`);
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to leave a team');
|
||||
await api.post(`/v1/hackathon/teams/${teamId}/leave`);
|
||||
return { success: true };
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -531,14 +350,10 @@ export const useLeaveTeam = () => {
|
||||
export const useDeleteTeam = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (teamId: string) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to delete a team');
|
||||
}
|
||||
|
||||
await hackathonApi.delete(`/teams/${teamId}`);
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to delete a team');
|
||||
await api.delete(`/v1/hackathon/teams/${teamId}`);
|
||||
return { success: true };
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -552,9 +367,9 @@ export const useTeamsByUserId = (userId: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['teams-by-user', userId],
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<HackathonApiResponse<any[]>>(`/users/${userId}/teams`);
|
||||
const response = await api.get<ApiResp<unknown[]>>(`/v1/hackathon/users/${userId}/teams`);
|
||||
const rawData = response.data.data || [];
|
||||
const teams = rawData.map((item: any) => item.team || item);
|
||||
const teams = rawData.map((item: unknown) => (item as Record<string, unknown>).team ?? item);
|
||||
return { data: teams };
|
||||
},
|
||||
enabled: !!userId,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
getTestimonialList,
|
||||
getTestimonialById,
|
||||
createTestimonial,
|
||||
updateTestimonial,
|
||||
deleteTestimonial,
|
||||
} from '../../api/testimonials';
|
||||
import type { TTestimonialCreateRequest, TTestimonialUpdateRequest } from '../../types/testimonials';
|
||||
import type { TPaginationParams } from '../../types/common';
|
||||
|
||||
export const testimonialKeys = {
|
||||
all: ['testimonials'] as const,
|
||||
lists: () => [...testimonialKeys.all, 'list'] as const,
|
||||
list: (params?: TPaginationParams) => [...testimonialKeys.lists(), params] as const,
|
||||
detail: (id: string) => [...testimonialKeys.all, 'detail', id] as const,
|
||||
};
|
||||
|
||||
export const useTestimonialList = (params?: TPaginationParams) => {
|
||||
return useQuery({
|
||||
queryKey: testimonialKeys.list(params),
|
||||
queryFn: () => getTestimonialList(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useTestimonialById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: testimonialKeys.detail(id),
|
||||
queryFn: () => getTestimonialById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateTestimonial = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: TTestimonialCreateRequest) => createTestimonial(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: testimonialKeys.lists() }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateTestimonial = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: TTestimonialUpdateRequest }) => updateTestimonial(id, data),
|
||||
onSuccess: (_, vars) => {
|
||||
queryClient.invalidateQueries({ queryKey: testimonialKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: testimonialKeys.detail(vars.id) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteTestimonial = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => deleteTestimonial(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: testimonialKeys.lists() }),
|
||||
});
|
||||
};
|
||||
@@ -1,208 +1,84 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
|
||||
import { useAuthStore } from '../auth';
|
||||
|
||||
interface UploadResponse {
|
||||
url: string;
|
||||
}
|
||||
|
||||
const fileToBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
const base64 = (reader.result as string).split(',')[1];
|
||||
resolve(base64);
|
||||
};
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
};
|
||||
import {
|
||||
uploadHackathonAvatar,
|
||||
uploadHackathonTeamFile,
|
||||
uploadHackathonSubmission,
|
||||
uploadHackathonFile,
|
||||
uploadUserFile,
|
||||
} from '../../api/upload';
|
||||
|
||||
export const useUploadFile = () => {
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['upload-file'],
|
||||
mutationFn: async (file: File) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to upload files');
|
||||
}
|
||||
|
||||
const base64Data = await fileToBase64(file);
|
||||
|
||||
const response = await hackathonApi.post<HackathonApiResponse<UploadResponse>>(
|
||||
'/upload/team',
|
||||
{
|
||||
filename: file.name,
|
||||
content_type: file.type,
|
||||
data: base64Data,
|
||||
}
|
||||
);
|
||||
|
||||
return { data: { url: response.data.data.url } };
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to upload files');
|
||||
const data = await uploadHackathonTeamFile(file);
|
||||
return { data };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUploadAvatar = () => {
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['upload-avatar'],
|
||||
mutationFn: async (file: File) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to upload avatar');
|
||||
}
|
||||
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to upload avatar');
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
throw new Error('Invalid file type. Allowed types: JPEG, PNG, WebP, GIF');
|
||||
}
|
||||
|
||||
const maxSize = 5 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('File too large. Maximum size: 5MB');
|
||||
}
|
||||
|
||||
const base64Data = await fileToBase64(file);
|
||||
|
||||
const response = await hackathonApi.post<HackathonApiResponse<UploadResponse>>(
|
||||
'/upload/avatar',
|
||||
{
|
||||
filename: file.name,
|
||||
content_type: file.type,
|
||||
data: base64Data,
|
||||
}
|
||||
);
|
||||
|
||||
return { data: { url: response.data.data.url } };
|
||||
if (!allowedTypes.includes(file.type)) throw new Error('Invalid file type. Allowed: JPEG, PNG, WebP, GIF');
|
||||
if (file.size > 5 * 1024 * 1024) throw new Error('File too large. Maximum size: 5MB');
|
||||
const data = await uploadHackathonAvatar(file);
|
||||
return { data };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUploadTeamFile = () => {
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['upload-team-file'],
|
||||
mutationFn: async (file: File) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to upload files');
|
||||
}
|
||||
|
||||
const allowedTypes = [
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/gif',
|
||||
'application/pdf',
|
||||
];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
throw new Error('Invalid file type. Allowed types: JPEG, PNG, WebP, GIF, PDF');
|
||||
}
|
||||
|
||||
const maxSize = 20 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('File too large. Maximum size: 20MB');
|
||||
}
|
||||
|
||||
const base64Data = await fileToBase64(file);
|
||||
|
||||
const response = await hackathonApi.post<HackathonApiResponse<UploadResponse>>(
|
||||
'/upload/team',
|
||||
{
|
||||
filename: file.name,
|
||||
content_type: file.type,
|
||||
data: base64Data,
|
||||
}
|
||||
);
|
||||
|
||||
return { data: { url: response.data.data.url } };
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to upload files');
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif', 'application/pdf'];
|
||||
if (!allowedTypes.includes(file.type)) throw new Error('Invalid file type. Allowed: JPEG, PNG, WebP, GIF, PDF');
|
||||
if (file.size > 20 * 1024 * 1024) throw new Error('File too large. Maximum size: 20MB');
|
||||
const data = await uploadHackathonTeamFile(file);
|
||||
return { data };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUploadSubmission = () => {
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['upload-submission'],
|
||||
mutationFn: async (file: File) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to upload submissions');
|
||||
}
|
||||
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to upload submissions');
|
||||
const allowedTypes = [
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/gif',
|
||||
'application/pdf',
|
||||
'application/zip',
|
||||
'application/x-zip-compressed',
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif',
|
||||
'application/pdf', 'application/zip', 'application/x-zip-compressed',
|
||||
'video/mp4', 'video/webm',
|
||||
];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
throw new Error(
|
||||
'Invalid file type. Allowed types: Images, PDF, ZIP, MP4, WebM'
|
||||
);
|
||||
}
|
||||
|
||||
const maxSize = 50 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('File too large. Maximum size: 50MB');
|
||||
}
|
||||
|
||||
const base64Data = await fileToBase64(file);
|
||||
|
||||
const response = await hackathonApi.post<HackathonApiResponse<UploadResponse>>(
|
||||
'/upload/submission',
|
||||
{
|
||||
filename: file.name,
|
||||
content_type: file.type,
|
||||
data: base64Data,
|
||||
}
|
||||
);
|
||||
|
||||
return { data: { url: response.data.data.url } };
|
||||
if (!allowedTypes.includes(file.type)) throw new Error('Invalid file type. Allowed: Images, PDF, ZIP, MP4, WebM');
|
||||
if (file.size > 50 * 1024 * 1024) throw new Error('File too large. Maximum size: 50MB');
|
||||
const data = await uploadHackathonSubmission(file);
|
||||
return { data };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUploadCV = () => {
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['upload-cv'],
|
||||
mutationFn: async (file: File) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to upload CV');
|
||||
}
|
||||
|
||||
if (file.type !== 'application/pdf') {
|
||||
throw new Error('CV must be a PDF file');
|
||||
}
|
||||
|
||||
const maxSize = 20 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('File too large. Maximum size: 20MB');
|
||||
}
|
||||
|
||||
const base64Data = await fileToBase64(file);
|
||||
|
||||
const response = await hackathonApi.post<HackathonApiResponse<UploadResponse>>(
|
||||
'/upload/team',
|
||||
{
|
||||
filename: file.name,
|
||||
content_type: file.type,
|
||||
data: base64Data,
|
||||
}
|
||||
);
|
||||
|
||||
return { data: { url: response.data.data.url } };
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to upload CV');
|
||||
if (file.type !== 'application/pdf') throw new Error('CV must be a PDF file');
|
||||
if (file.size > 20 * 1024 * 1024) throw new Error('File too large. Maximum size: 20MB');
|
||||
const data = await uploadUserFile(file);
|
||||
return { data };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,69 +1,30 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
|
||||
import { useAuthStore } from '../auth';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
fullname: string;
|
||||
bio?: string;
|
||||
location?: string;
|
||||
avatar?: string;
|
||||
skills?: string[];
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
interface CertificateUserData {
|
||||
id: string;
|
||||
fullname: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
interface CertificateTeamData {
|
||||
id: string;
|
||||
name: string;
|
||||
logo?: string;
|
||||
is_leader: boolean;
|
||||
}
|
||||
|
||||
interface CertificateSubmissionData {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
repository_url?: string;
|
||||
demo_url?: string;
|
||||
}
|
||||
|
||||
interface CertificateWinnerData {
|
||||
rank: number;
|
||||
prize?: string;
|
||||
}
|
||||
import {
|
||||
getUserMe,
|
||||
getUserById,
|
||||
getUserList,
|
||||
updateUserMe,
|
||||
updateUserById,
|
||||
} from '../../api/users';
|
||||
import { api } from '../../api/index';
|
||||
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||
import type { TUsersDetailItem, TUserUpdateRequest } from '../../types/users';
|
||||
|
||||
export interface CertificatePublicData {
|
||||
user: CertificateUserData;
|
||||
team?: CertificateTeamData;
|
||||
submission?: CertificateSubmissionData;
|
||||
winner?: CertificateWinnerData;
|
||||
}
|
||||
|
||||
interface UpdateUserRequest {
|
||||
fullname?: string;
|
||||
bio?: string;
|
||||
location?: string;
|
||||
avatar?: string;
|
||||
skills?: string[];
|
||||
user: { id: string; fullname: string; email: string; avatar?: string };
|
||||
team?: { id: string; name: string; logo?: string; is_leader: boolean };
|
||||
submission?: { id: string; title: string; description: string; repository_url?: string; demo_url?: string };
|
||||
winner?: { rank: number; prize?: string };
|
||||
}
|
||||
|
||||
export const useUserMe = () => {
|
||||
const { session } = useAuthStore();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['user-me'],
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<HackathonApiResponse<User>>('/users/me');
|
||||
return { data: response.data.data };
|
||||
const data = await getUserMe();
|
||||
return { data };
|
||||
},
|
||||
enabled: !!session?.user?.id,
|
||||
});
|
||||
@@ -73,45 +34,52 @@ export const useUserById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['user-by-id', id],
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<HackathonApiResponse<User>>(`/users/${id}`);
|
||||
return { data: response.data.data };
|
||||
const data = await getUserById(id);
|
||||
return { data };
|
||||
},
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUserDetailsById = (userId: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['user-details', userId],
|
||||
queryFn: async () => {
|
||||
const data = await getUserById(userId);
|
||||
return { data };
|
||||
},
|
||||
enabled: !!userId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUserList = (params?: TPaginationParams) => {
|
||||
return useQuery({
|
||||
queryKey: ['user-list', params],
|
||||
queryFn: () => getUserList(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateUserMe = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { session, setSession } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['update-user-me'],
|
||||
mutationFn: async (data: UpdateUserRequest) => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('You must be logged in to update profile');
|
||||
}
|
||||
|
||||
const response = await hackathonApi.put<HackathonApiResponse<User>>('/users/me', {
|
||||
fullname: data.fullname,
|
||||
bio: data.bio,
|
||||
location: data.location,
|
||||
avatar: data.avatar,
|
||||
skills: data.skills,
|
||||
});
|
||||
|
||||
return { data: response.data.data };
|
||||
mutationFn: (data: TUserUpdateRequest) => {
|
||||
if (!session?.user?.id) throw new Error('You must be logged in to update profile');
|
||||
return updateUserMe(data);
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
if (session?.user && result.data) {
|
||||
if (session?.user && result) {
|
||||
setSession({
|
||||
token: session.token,
|
||||
user: {
|
||||
...session.user,
|
||||
fullname: result.data.fullname || session.user.fullname,
|
||||
bio: result.data.bio || '',
|
||||
location: result.data.location || '',
|
||||
avatar: result.data.avatar || session.user.avatar,
|
||||
skills: result.data.skills || [],
|
||||
fullname: result.fullname || session.user.fullname,
|
||||
bio: result.profile_extension?.bio || '',
|
||||
location: result.profile_extension?.location || '',
|
||||
avatar: result.avatar || session.user.avatar,
|
||||
skills: result.profile_extension?.skills || [],
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -125,33 +93,20 @@ export const useUpdateUserById = () => {
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['update-user-by-id'],
|
||||
mutationFn: async ({ id, data }: { id: string; data: UpdateUserRequest }) => {
|
||||
const response = await hackathonApi.put<HackathonApiResponse<User>>(`/users/${id}`, data);
|
||||
return { data: response.data.data };
|
||||
},
|
||||
mutationFn: ({ id, data }: { id: string; data: TUserUpdateRequest }) => updateUserById(id, data),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['user-by-id', variables.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['user-list'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUserDetailsById = (userId: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['user-details', userId],
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<HackathonApiResponse<User>>(`/users/${userId}`);
|
||||
return { data: response.data.data };
|
||||
},
|
||||
enabled: !!userId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCertificatePublicData = (userId: string, enabled = true) => {
|
||||
return useQuery({
|
||||
queryKey: ['certificate-public-data', userId],
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get<HackathonApiResponse<CertificatePublicData>>(
|
||||
`/certificates/${userId}`
|
||||
const response = await api.get<{ data: CertificatePublicData }>(
|
||||
`/v1/hackathon/certificates/${userId}`
|
||||
);
|
||||
return { data: response.data.data };
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
|
||||
import { api } from '../../api/index';
|
||||
|
||||
export const winnerKeys = {
|
||||
all: ['winners'] as const,
|
||||
@@ -7,34 +7,19 @@ export const winnerKeys = {
|
||||
};
|
||||
|
||||
interface Team {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
city: string;
|
||||
visibility: string;
|
||||
logo: string;
|
||||
banner: string;
|
||||
leader_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
id: string; name: string; description: string; city: string; visibility: string;
|
||||
logo: string; banner: string; leader_id: string; created_at: string; updated_at: string;
|
||||
}
|
||||
|
||||
interface Winner {
|
||||
id: string;
|
||||
team_id: string;
|
||||
team: Team;
|
||||
rank: number;
|
||||
prize: string;
|
||||
announced_at: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
id: string; team_id: string; team: Team; rank: number; prize: string;
|
||||
announced_at: string; created_at: string; updated_at: string;
|
||||
}
|
||||
|
||||
export const useWinners = () => {
|
||||
return useQuery<HackathonApiResponse<Winner[]>>({
|
||||
return useQuery({
|
||||
queryKey: winnerKeys.lists(),
|
||||
queryFn: async () => {
|
||||
const response = await hackathonApi.get('/winners');
|
||||
const response = await api.get<{ data: Winner[] }>('/v1/hackathon/winners');
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
export type TPaginationMeta = {
|
||||
page: number;
|
||||
per_page: number;
|
||||
total?: number;
|
||||
total_pages?: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
next_cursor?: string;
|
||||
prev_cursor?: string;
|
||||
};
|
||||
|
||||
export type TPaginationParams = {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
sort_by?: string;
|
||||
order?: string;
|
||||
filter?: string;
|
||||
filter_by?: string;
|
||||
};
|
||||
|
||||
export type TApiSuccess<T> = {
|
||||
data: T;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type TApiPaginated<T> = {
|
||||
data: T[];
|
||||
meta: TPaginationMeta;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type TApiMessage = {
|
||||
message: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
// Legacy aliases kept for backward compatibility
|
||||
export type TMetaResponse = {
|
||||
page: number;
|
||||
per_page: number;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
export type TEventsListItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
detail_link: string;
|
||||
price: number;
|
||||
is_online: boolean;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
created_at: string;
|
||||
location?: string;
|
||||
is_deleted: boolean;
|
||||
};
|
||||
|
||||
export type TEventsDetailItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
detail_link: string;
|
||||
price: number;
|
||||
is_online: boolean;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
location?: string;
|
||||
};
|
||||
|
||||
export type TEventCreateRequest = {
|
||||
name: string;
|
||||
description: string;
|
||||
detail_link: string;
|
||||
price: number;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
location?: string;
|
||||
is_online: boolean;
|
||||
};
|
||||
|
||||
export type TEventUpdateRequest = TEventCreateRequest;
|
||||
@@ -1,11 +1,89 @@
|
||||
export type TGachaItem = {
|
||||
itemName: string;
|
||||
quantity: number;
|
||||
foto?: File;
|
||||
export type TGachaItemDto = {
|
||||
id: string;
|
||||
name: string;
|
||||
is_deleted: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type TGachaRollItem = {
|
||||
itemName: string;
|
||||
quantity: number;
|
||||
chanceRate: number;
|
||||
export type TGachaItemCreateRequest = {
|
||||
item_code: string;
|
||||
name: string;
|
||||
description: string;
|
||||
rarity: string;
|
||||
type_: string;
|
||||
category: string;
|
||||
value: number;
|
||||
weight: number;
|
||||
stock: number;
|
||||
is_limited: boolean;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type TGachaItemUpdateRequest = {
|
||||
item_code?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
rarity?: string;
|
||||
type_?: string;
|
||||
category?: string;
|
||||
value?: number;
|
||||
weight?: number;
|
||||
stock?: number;
|
||||
is_limited?: boolean;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type TGachaRollItemDto = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
gacha_id: string;
|
||||
item_id: string;
|
||||
weight: number;
|
||||
quantity: number;
|
||||
is_deleted: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type TGachaRollCreateRequest = {
|
||||
item_id: string;
|
||||
weight: number;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
export type TGachaCreditDto = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
available_rolls: number;
|
||||
is_deleted: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type TGachaCreditAddRequest = {
|
||||
amount: number;
|
||||
};
|
||||
|
||||
export type TGachaClaimDetailDto = {
|
||||
id: string;
|
||||
user: {
|
||||
id: string;
|
||||
fullname: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
};
|
||||
item: TGachaItemDto;
|
||||
is_deleted: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type TGachaClaimCreateRequest = {
|
||||
user_id: string;
|
||||
item_id: string;
|
||||
};
|
||||
|
||||
// Legacy aliases kept for backward compatibility
|
||||
export type TGachaItem = TGachaItemDto;
|
||||
export type TGachaRollItem = TGachaRollItemDto;
|
||||
|
||||
@@ -6,3 +6,6 @@ export * from './permissions';
|
||||
export * from './mentors';
|
||||
export * from './teams';
|
||||
export * from './admin';
|
||||
export * from './events';
|
||||
export * from './testimonials';
|
||||
export * from './sessions';
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
export type TPermissionItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type TPermissionCreateRequest = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TPermissionUpdateRequest = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
import { TPermissionItem } from '../permissions';
|
||||
|
||||
export type TRoleDetailItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: TPermissionItem[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type TRolesListItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
permissions_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type TRoleDetailItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
is_system_role?: boolean;
|
||||
is_default?: boolean;
|
||||
is_deleted?: boolean;
|
||||
permissions: TPermissionItem[];
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type TRoleCreateRequest = {
|
||||
name: string;
|
||||
permissions: string[];
|
||||
};
|
||||
|
||||
export type TRoleUpdateRequest = {
|
||||
name?: string;
|
||||
permissions?: string[];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
export type TSessionListItem = {
|
||||
id: string;
|
||||
mentor_id: string;
|
||||
mentee_id: string;
|
||||
mentee_fullname?: string;
|
||||
mentee_email?: string;
|
||||
topic: string;
|
||||
scheduled_at: string;
|
||||
duration_minutes: number;
|
||||
session_type: string;
|
||||
status: string;
|
||||
rating?: number;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type TSessionListResponse = {
|
||||
sessions: TSessionListItem[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type TBookSessionRequest = {
|
||||
topic: string;
|
||||
description?: string;
|
||||
scheduled_at: string;
|
||||
duration_minutes?: number;
|
||||
session_type?: string;
|
||||
};
|
||||
|
||||
export type TBookSessionResponse = {
|
||||
id: string;
|
||||
mentor_id: string;
|
||||
mentee_id: string;
|
||||
topic: string;
|
||||
description?: string;
|
||||
scheduled_at: string;
|
||||
duration_minutes: number;
|
||||
session_type: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type TUpdateSessionStatusRequest = {
|
||||
status: string;
|
||||
meeting_link?: string;
|
||||
};
|
||||
|
||||
export type TUpdateSessionStatusResponse = {
|
||||
id: string;
|
||||
status: string;
|
||||
meeting_link?: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type TSessionFeedbackRequest = {
|
||||
feedback: string;
|
||||
rating: number;
|
||||
};
|
||||
|
||||
export type TSessionFeedbackResponse = {
|
||||
id: string;
|
||||
feedback: string;
|
||||
rating: number;
|
||||
submitted_at: string;
|
||||
};
|
||||
|
||||
export type TAvailabilitySlot = {
|
||||
date: string;
|
||||
time: string;
|
||||
available: boolean;
|
||||
};
|
||||
|
||||
export type TMentorAvailability = {
|
||||
mentor_id: string;
|
||||
availability_commitment: string;
|
||||
preferred_formats: string[];
|
||||
slots: TAvailabilitySlot[];
|
||||
booked_dates: string[];
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
export type TTestimonialsListItem = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
user_fullname: string;
|
||||
role: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
is_deleted: boolean;
|
||||
};
|
||||
|
||||
export type TTestimonialsDetailItem = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
user_fullname: string;
|
||||
role: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type TTestimonialCreateRequest = {
|
||||
role: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type TTestimonialUpdateRequest = TTestimonialCreateRequest;
|
||||
@@ -1,18 +1,101 @@
|
||||
import { TRoleDetailItem } from '../roles';
|
||||
|
||||
export type TExperienceDto = {
|
||||
id: string;
|
||||
company: string;
|
||||
position: string;
|
||||
duration: string;
|
||||
period: string;
|
||||
};
|
||||
|
||||
export type TEducationDto = {
|
||||
id: string;
|
||||
institution: string;
|
||||
degree: string;
|
||||
field: string;
|
||||
period: string;
|
||||
};
|
||||
|
||||
export type TUserProfileExtension = {
|
||||
phone_number?: string;
|
||||
phone_for_verification?: string;
|
||||
gender?: string;
|
||||
birthdate?: string;
|
||||
domicile?: string;
|
||||
bio?: string;
|
||||
last_education?: string;
|
||||
linkedin_url?: string;
|
||||
github_url?: string;
|
||||
cv_url?: string;
|
||||
portfolio_url?: string;
|
||||
website_url?: string;
|
||||
twitter_url?: string;
|
||||
location?: string;
|
||||
skills?: string[];
|
||||
experience?: TExperienceDto[];
|
||||
education?: TEducationDto[];
|
||||
career_status?: string;
|
||||
};
|
||||
|
||||
export type TUserItem = {
|
||||
id: string;
|
||||
avatar: string;
|
||||
birthdate: string;
|
||||
avatar?: string;
|
||||
birthdate?: string;
|
||||
email: string;
|
||||
fullname: string;
|
||||
gender: string;
|
||||
gender?: string;
|
||||
is_active: boolean;
|
||||
phone_number: string;
|
||||
phone_number?: string;
|
||||
role: TRoleDetailItem;
|
||||
location?: string;
|
||||
bio?: string;
|
||||
skills?: string[];
|
||||
};
|
||||
|
||||
export type { UserDetailResponseDto, UserUpdateRequestDto } from '../../api/users';
|
||||
export type TUsersListItem = {
|
||||
id: string;
|
||||
role: string;
|
||||
fullname: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type TUsersDetailItem = {
|
||||
id: string;
|
||||
role: TRoleDetailItem;
|
||||
fullname: string;
|
||||
legal_name?: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
is_active: boolean;
|
||||
profile_extension?: TUserProfileExtension;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type TUserCreateRequest = {
|
||||
email: string;
|
||||
password: string;
|
||||
fullname: string;
|
||||
is_active: boolean;
|
||||
role_id: string;
|
||||
avatar?: string;
|
||||
};
|
||||
|
||||
export type TUserUpdateRequest = {
|
||||
email?: string;
|
||||
password?: string;
|
||||
fullname?: string;
|
||||
legal_name?: string;
|
||||
is_active?: boolean;
|
||||
avatar?: string;
|
||||
role_id?: string;
|
||||
profile_extension?: TUserProfileExtension;
|
||||
};
|
||||
|
||||
// Legacy alias
|
||||
export type { TUsersDetailItem as UserDetailResponseDto };
|
||||
export type { TUserUpdateRequest as UserUpdateRequestDto };
|
||||
|
||||
Reference in New Issue
Block a user