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 { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface IModalEditAccount {
|
interface IModalEditAccount {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
handleEditAccount?: () => void;
|
handleEditAccount?: () => Promise<void>;
|
||||||
currentStep?: number;
|
currentStep?: number;
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
prevStep: () => void;
|
prevStep: () => void;
|
||||||
resetStep: () => void;
|
resetStep: () => void;
|
||||||
|
initialValues?: { fullname?: string; email?: string };
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalEditAccount = ({
|
const ModalEditAccount = ({
|
||||||
@@ -17,9 +19,10 @@ const ModalEditAccount = ({
|
|||||||
onClose,
|
onClose,
|
||||||
currentStep,
|
currentStep,
|
||||||
nextStep,
|
nextStep,
|
||||||
prevStep,
|
|
||||||
resetStep,
|
resetStep,
|
||||||
handleEditAccount,
|
handleEditAccount,
|
||||||
|
initialValues,
|
||||||
|
onDataCapture,
|
||||||
}: IModalEditAccount) => {
|
}: IModalEditAccount) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -31,7 +34,14 @@ const ModalEditAccount = ({
|
|||||||
}}
|
}}
|
||||||
disableEscapeKeyDown={true}
|
disableEscapeKeyDown={true}
|
||||||
>
|
>
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
{currentStep === 1 && (
|
||||||
|
<StepOne
|
||||||
|
nextStep={nextStep}
|
||||||
|
onClose={onClose}
|
||||||
|
initialValues={initialValues}
|
||||||
|
onDataCapture={onDataCapture}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<StepTwo
|
<StepTwo
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
@@ -46,13 +56,18 @@ const ModalEditAccount = ({
|
|||||||
interface IStepOneProps {
|
interface IStepOneProps {
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
initialValues?: { fullname?: string; email?: string };
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
|
||||||
const [fullName, setFullName] = useState('Ahmad Wiyana');
|
const [fullName, setFullName] = useState(initialValues?.fullname ?? '');
|
||||||
const [email, setEmail] = useState('fullname23@gmail.com');
|
const [email, setEmail] = useState(initialValues?.email ?? '');
|
||||||
const [phoneNumber, setPhoneNumber] = useState('081904423804');
|
|
||||||
const [address, setAddress] = useState('Jl. Pantai Cibaduyut Indah');
|
useEffect(() => {
|
||||||
|
setFullName(initialValues?.fullname ?? '');
|
||||||
|
setEmail(initialValues?.email ?? '');
|
||||||
|
}, [initialValues]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -81,31 +96,16 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
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>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={nextStep}
|
onClick={() => {
|
||||||
|
onDataCapture?.({ fullname: fullName, email });
|
||||||
|
nextStep();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Perbarui Data
|
Perbarui Data
|
||||||
</Button>
|
</Button>
|
||||||
@@ -116,7 +116,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
|||||||
|
|
||||||
interface IStepTwoProps {
|
interface IStepTwoProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
handleEditAccount?: () => void;
|
handleEditAccount?: () => Promise<void>;
|
||||||
resetStep: () => void;
|
resetStep: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,8 +147,8 @@ const StepTwo = ({ onClose, handleEditAccount, resetStep }: IStepTwoProps) => (
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
handleEditAccount && handleEditAccount();
|
if (handleEditAccount) await handleEditAccount();
|
||||||
onClose();
|
onClose();
|
||||||
resetStep();
|
resetStep();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
FilterOutlined,
|
FilterOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
@@ -19,25 +19,17 @@ import {
|
|||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import ModalEditAccount from './_components/modal-edit-account';
|
import ModalEditAccount from './_components/modal-edit-account';
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
import {
|
||||||
interface Account {
|
useUserList,
|
||||||
id: number;
|
useUpdateUserById,
|
||||||
name: string;
|
TUsersListItem,
|
||||||
email: string;
|
} from '@imphnen-frontend-service/service';
|
||||||
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',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [showModalEditAccount, setShowModalEditAccount] = useState(false);
|
const [showModalEditAccount, setShowModalEditAccount] = useState(false);
|
||||||
|
const [selectedUser, setSelectedUser] = useState<TUsersListItem | null>(null);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const pendingFormData = useRef<any>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
step: currentStep,
|
step: currentStep,
|
||||||
@@ -58,7 +50,23 @@ export const Components: FC = (): ReactElement => {
|
|||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
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',
|
id: 'select',
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
@@ -84,19 +92,24 @@ export const Components: FC = (): ReactElement => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Nama Lengkap',
|
header: 'Nama Lengkap',
|
||||||
accessorKey: 'name',
|
accessorKey: 'fullname',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Email',
|
header: 'Email',
|
||||||
accessorKey: 'email',
|
accessorKey: 'email',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Nomor Telp',
|
header: 'Role',
|
||||||
accessorKey: 'phone',
|
accessorKey: 'role',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Alamat Pengiriman',
|
header: 'Status',
|
||||||
accessorKey: 'address',
|
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',
|
header: 'Action',
|
||||||
@@ -106,6 +119,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setSelectedUser(row.original);
|
||||||
setShowModalEditAccount(true);
|
setShowModalEditAccount(true);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@@ -117,7 +131,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: mockData,
|
data: users,
|
||||||
columns,
|
columns,
|
||||||
state: {
|
state: {
|
||||||
pagination,
|
pagination,
|
||||||
@@ -128,8 +142,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
onPaginationChange: setPagination,
|
onPaginationChange: setPagination,
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||||
manualPagination: false,
|
manualPagination: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -144,6 +158,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Cari berdasarkan nama lengkap, email"
|
placeholder="Cari berdasarkan nama lengkap, email"
|
||||||
className="pl-12 w-full max-h-full"
|
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]">
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
<SearchOutlined />
|
<SearchOutlined />
|
||||||
@@ -167,7 +183,11 @@ export const Components: FC = (): ReactElement => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -175,12 +195,12 @@ export const Components: FC = (): ReactElement => {
|
|||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
isOpen={showModalEditAccount}
|
isOpen={showModalEditAccount}
|
||||||
onClose={() => setShowModalEditAccount(false)}
|
onClose={() => setShowModalEditAccount(false)}
|
||||||
handleEditAccount={() => {
|
handleEditAccount={handleEditAccount}
|
||||||
console.log('Account updated');
|
|
||||||
}}
|
|
||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
|
initialValues={selectedUser ? { fullname: selectedUser.fullname, email: selectedUser.email } : undefined}
|
||||||
|
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,17 +4,26 @@ import { For } from "@imphnen-frontend-service/utils";
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { UserGrowthChart } from "./_components/chart/user-growth";
|
import { UserGrowthChart } from "./_components/chart/user-growth";
|
||||||
import { SessionStatusChart } from "./_components/chart/session-status";
|
import { SessionStatusChart } from "./_components/chart/session-status";
|
||||||
|
import { useMentorList, useUserList, useMySessions } from "@imphnen-frontend-service/service";
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
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 (
|
return (
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700 mb-5">Overview</h1>
|
<h1 className="text-p1 font-semibold text-neutral-700 mb-5">Overview</h1>
|
||||||
@@ -26,8 +35,13 @@ export default function Components(): ReactElement {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="grid grid-cols-5 gap-5">
|
<div className="grid grid-cols-5 gap-5">
|
||||||
<For data={Array.from({ length: 5 })}>
|
<For data={overviewStats}>
|
||||||
{(_, index) => <Overview key={index} />}
|
{(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>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,15 +81,18 @@ export default function Components(): ReactElement {
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<For data={Array.from({ length: 5 })}>
|
{topMentors.slice(0, 5).map((mentor, index) => (
|
||||||
{(_, index) => (
|
<tr key={mentor.id} className="shadow rounded-lg">
|
||||||
<tr key={index} className="shadow rounded-lg">
|
<td className="py-4 px-5">{index + 1}</td>
|
||||||
<td className="py-4 px-5">{index + 1}</td>
|
<td className="py-4 px-5">{mentor.fullname ?? '-'}</td>
|
||||||
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
<td className="py-4 px-5">{mentor.rating?.toFixed(1) ?? '-'}</td>
|
||||||
<td className="py-4 px-5">4.9</td>
|
</tr>
|
||||||
</tr>
|
))}
|
||||||
)}
|
{topMentors.length === 0 && (
|
||||||
</For>
|
<tr>
|
||||||
|
<td colSpan={3} className="py-4 px-5 text-center text-neutral-400">Belum ada data</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,21 +106,36 @@ export default function Components(): ReactElement {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr className="text-label1 bg-primary-50 text-left font-medium">
|
<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-[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>
|
<th className="font-medium py-4 px-5 rounded-r-lg">Total Sesi</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<For data={Array.from({ length: 5 })}>
|
{(() => {
|
||||||
{(_, index) => (
|
const sessions = sessionsData?.sessions ?? [];
|
||||||
<tr key={index} className="shadow rounded-lg">
|
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">{index + 1}</td>
|
||||||
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
<td className="py-4 px-5">{topic}</td>
|
||||||
<td className="py-4 px-5">1000</td>
|
<td className="py-4 px-5">{count}</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
));
|
||||||
</For>
|
})()}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,4 +144,4 @@ export default function Components(): ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
</BackofficeWrapper>
|
</BackofficeWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface IModalAddItem {
|
|||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
prevStep: () => void;
|
prevStep: () => void;
|
||||||
resetStep: () => void;
|
resetStep: () => void;
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalAddItem = ({
|
const ModalAddItem = ({
|
||||||
@@ -20,6 +21,7 @@ const ModalAddItem = ({
|
|||||||
nextStep,
|
nextStep,
|
||||||
resetStep,
|
resetStep,
|
||||||
handleAddItem,
|
handleAddItem,
|
||||||
|
onDataCapture,
|
||||||
}: IModalAddItem) => {
|
}: IModalAddItem) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -31,7 +33,7 @@ const ModalAddItem = ({
|
|||||||
}}
|
}}
|
||||||
disableEscapeKeyDown={true}
|
disableEscapeKeyDown={true}
|
||||||
>
|
>
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<StepTwo
|
<StepTwo
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
@@ -46,10 +48,11 @@ const ModalAddItem = ({
|
|||||||
interface IStepOneProps {
|
interface IStepOneProps {
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
|
||||||
const { form, onSubmit } = useItem(nextStep);
|
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -98,7 +101,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={nextStep}
|
type="submit"
|
||||||
>
|
>
|
||||||
Tambahkan Item
|
Tambahkan Item
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|||||||
interface IModalDeleteItem {
|
interface IModalDeleteItem {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
handleDeleteItem?: () => void;
|
handleDeleteItem?: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalDeleteItem = ({
|
const ModalDeleteItem = ({
|
||||||
@@ -48,8 +48,9 @@ const ModalDeleteItem = ({
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
handleDeleteItem && handleDeleteItem();
|
if (handleDeleteItem) await handleDeleteItem();
|
||||||
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Hapus Item
|
Hapus Item
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ interface IModalEditItem {
|
|||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
prevStep: () => void;
|
prevStep: () => void;
|
||||||
resetStep: () => void;
|
resetStep: () => void;
|
||||||
|
initialValues?: { itemName?: string; quantity?: number };
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalEditItem = ({
|
const ModalEditItem = ({
|
||||||
@@ -20,6 +22,8 @@ const ModalEditItem = ({
|
|||||||
nextStep,
|
nextStep,
|
||||||
resetStep,
|
resetStep,
|
||||||
handleEditItem,
|
handleEditItem,
|
||||||
|
initialValues,
|
||||||
|
onDataCapture,
|
||||||
}: IModalEditItem) => {
|
}: IModalEditItem) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -31,7 +35,7 @@ const ModalEditItem = ({
|
|||||||
}}
|
}}
|
||||||
disableEscapeKeyDown={true}
|
disableEscapeKeyDown={true}
|
||||||
>
|
>
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} initialValues={initialValues} onDataCapture={onDataCapture} />}
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<StepTwo
|
<StepTwo
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
@@ -46,15 +50,12 @@ const ModalEditItem = ({
|
|||||||
interface IStepOneProps {
|
interface IStepOneProps {
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
initialValues?: { itemName?: string; quantity?: number };
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
|
||||||
const initialValues = {
|
const { form, onSubmit } = useItem(nextStep, initialValues as any, onDataCapture);
|
||||||
itemName: 'Hoodie IMPHNEN Official 2025',
|
|
||||||
quantity: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { form, onSubmit } = useItem(nextStep, initialValues);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -102,7 +103,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={nextStep}
|
type="submit"
|
||||||
>
|
>
|
||||||
Perbarui Item
|
Perbarui Item
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ import { toast } from 'sonner';
|
|||||||
|
|
||||||
export const useItem = (
|
export const useItem = (
|
||||||
nextStep: () => void,
|
nextStep: () => void,
|
||||||
initialValues?: TGachaItem
|
initialValues?: Partial<TGachaItem>,
|
||||||
|
onDataCapture?: (data: any) => void,
|
||||||
) => {
|
) => {
|
||||||
const form = useForm<TGachaItem>({
|
const form = useForm<any>({
|
||||||
resolver: zodResolver(gachaItemSchema),
|
resolver: zodResolver(gachaItemSchema),
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
defaultValues: initialValues,
|
defaultValues: initialValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit((data) => {
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
console.log('Form data:', data);
|
onDataCapture?.(data);
|
||||||
nextStep();
|
nextStep();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,6 +39,9 @@ export const useConfirmItem = (
|
|||||||
) => {
|
) => {
|
||||||
const onConfirm = async () => {
|
const onConfirm = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (actionFunction) {
|
||||||
|
await actionFunction();
|
||||||
|
}
|
||||||
toast.success(messages?.success);
|
toast.success(messages?.success);
|
||||||
onClose();
|
onClose();
|
||||||
resetStep();
|
resetStep();
|
||||||
|
|||||||
@@ -6,16 +6,26 @@ import {
|
|||||||
UserSwitchOutlined,
|
UserSwitchOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
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 ModalAddItem from './_components/modal-add-item';
|
||||||
import ModalEditItem from './_components/modal-edit-item';
|
import ModalEditItem from './_components/modal-edit-item';
|
||||||
import ModalDeleteItem from './_components/modal-delete-item';
|
import ModalDeleteItem from './_components/modal-delete-item';
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
import {
|
||||||
|
useUserList,
|
||||||
|
useGachaItemList,
|
||||||
|
useCreateGachaItem,
|
||||||
|
useUpdateGachaItem,
|
||||||
|
useDeleteGachaItem,
|
||||||
|
TGachaItemDto,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||||
const [showModalEditItem, setShowModalEditItem] = useState(false);
|
const [showModalEditItem, setShowModalEditItem] = useState(false);
|
||||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null);
|
||||||
|
const pendingFormData = useRef<any>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
step: currentStep,
|
step: currentStep,
|
||||||
@@ -28,6 +38,52 @@ export const Components: FC = (): ReactElement => {
|
|||||||
minValue: 1,
|
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 (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
<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]" />
|
<UsergroupAddOutlined className="text-[20px]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<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>
|
<p className="text-label1 text-neutral-500">Participants</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,9 +113,9 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<ReloadOutlined className="text-[20px]" />
|
<ReloadOutlined className="text-[20px]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<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">
|
<p className="text-label1 text-neutral-500">
|
||||||
Roll and Reroll
|
Gacha Items
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,7 +125,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<UserSwitchOutlined className="text-[20px]" />
|
<UserSwitchOutlined className="text-[20px]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<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>
|
<p className="text-label1 text-neutral-500">Redeem</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +135,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<UsergroupDeleteOutlined className="text-[20px]" />
|
<UsergroupDeleteOutlined className="text-[20px]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<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">
|
<p className="text-label1 text-neutral-500">
|
||||||
Inactive Users
|
Inactive Users
|
||||||
</p>
|
</p>
|
||||||
@@ -105,19 +161,18 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
||||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
{gachaItems.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item}
|
key={item.id}
|
||||||
className="bg-white overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
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 className="flex flex-col py-4 px-6 gap-[8px]">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-p3 text-primary-500 font-medium">
|
<h3 className="text-p3 text-primary-500 font-medium">
|
||||||
Lanyard IMPHNEN
|
{item.name}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center justify-start gap-10 text-label2 text-gray-500 mt-1">
|
<div className="flex items-center justify-start gap-10 text-label2 text-gray-500 mt-1">
|
||||||
<span>Prize {item}</span>
|
<span>{item.id}</span>
|
||||||
<span>Quantity: 10</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-start gap-2">
|
<div className="flex justify-start gap-2">
|
||||||
@@ -125,7 +180,10 @@ export const Components: FC = (): ReactElement => {
|
|||||||
variant="text"
|
variant="text"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
|
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
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -133,14 +191,17 @@ export const Components: FC = (): ReactElement => {
|
|||||||
variant="text"
|
variant="text"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
|
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
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img src="gacha-clip.webp" alt="Lanyard IMPHNEN" />
|
<img src="gacha-clip.webp" alt={item.name} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -162,6 +223,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
|
handleAddItem={handleAdd}
|
||||||
|
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModalEditItem
|
<ModalEditItem
|
||||||
@@ -171,11 +234,15 @@ export const Components: FC = (): ReactElement => {
|
|||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
|
handleEditItem={handleEdit}
|
||||||
|
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
|
||||||
|
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModalDeleteItem
|
<ModalDeleteItem
|
||||||
isOpen={showModalDeleteItem}
|
isOpen={showModalDeleteItem}
|
||||||
onClose={() => setShowModalDeleteItem(false)}
|
onClose={() => setShowModalDeleteItem(false)}
|
||||||
|
handleDeleteItem={async () => { await handleDelete(); return true; }}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organ
|
|||||||
import { cn, For } from "@imphnen-frontend-service/utils";
|
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
import { ReactElement, useState } from "react"
|
import { ReactElement, useState } from "react"
|
||||||
|
import { useMySessions, TSessionListItem } from "@imphnen-frontend-service/service";
|
||||||
|
|
||||||
const TABS = {
|
const TABS = {
|
||||||
MENTORING: 'Mentoring',
|
MENTORING: 'Mentoring',
|
||||||
@@ -11,24 +12,6 @@ const TABS = {
|
|||||||
} as const
|
} as const
|
||||||
type Tabs = typeof TABS[keyof typeof TABS]
|
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 {
|
export default function Components(): ReactElement {
|
||||||
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
|
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
|
||||||
|
|
||||||
@@ -38,7 +21,18 @@ export default function Components(): ReactElement {
|
|||||||
pageSize: 9,
|
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',
|
id: 'select',
|
||||||
meta: { cellClassName: cn("w-20") },
|
meta: { cellClassName: cn("w-20") },
|
||||||
@@ -62,37 +56,30 @@ export default function Components(): ReactElement {
|
|||||||
{
|
{
|
||||||
id: 'name',
|
id: 'name',
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
accessorKey: 'name',
|
accessorKey: 'mentee_fullname',
|
||||||
|
cell: ({ row }) => <span>{row.original.mentee_fullname ?? '-'}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'email',
|
id: 'email',
|
||||||
header: 'Email',
|
header: 'Email',
|
||||||
accessorKey: 'email',
|
accessorKey: 'mentee_email',
|
||||||
|
cell: ({ row }) => <span>{row.original.mentee_email ?? '-'}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'rating',
|
id: 'rating',
|
||||||
header: 'Rating',
|
header: 'Rating',
|
||||||
accessorKey: 'rating',
|
accessorKey: 'rating',
|
||||||
|
cell: ({ row }) => <span>{row.original.rating ?? '-'}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'status',
|
id: 'status',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const status = row.original.status;
|
const hasRating = !!row.original.rating;
|
||||||
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',
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`py-2 px-4 rounded-md text-center ${hasRating ? 'bg-success-200 text-success-500' : 'bg-primary-200 text-primary-500'}`}>
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
{hasRating ? 'Done' : 'To Do'}
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -100,7 +87,7 @@ export default function Components(): ReactElement {
|
|||||||
{
|
{
|
||||||
header: 'Action',
|
header: 'Action',
|
||||||
meta: { cellClassName: cn("w-72") },
|
meta: { cellClassName: cn("w-72") },
|
||||||
cell: ({ row }) => (
|
cell: () => (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -116,7 +103,7 @@ export default function Components(): ReactElement {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: mockData,
|
data: sessions,
|
||||||
columns,
|
columns,
|
||||||
state: {
|
state: {
|
||||||
pagination,
|
pagination,
|
||||||
@@ -127,8 +114,8 @@ export default function Components(): ReactElement {
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
onPaginationChange: setPagination,
|
onPaginationChange: setPagination,
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||||
manualPagination: false,
|
manualPagination: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -142,7 +129,10 @@ export default function Components(): ReactElement {
|
|||||||
key={tab}
|
key={tab}
|
||||||
variant="text"
|
variant="text"
|
||||||
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => {
|
||||||
|
setActiveTab(tab);
|
||||||
|
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -163,18 +153,26 @@ export default function Components(): ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Select>
|
<Select>
|
||||||
<option selected disabled>Rating</option>
|
<option disabled>Rating</option>
|
||||||
<option value="4.5">4.5</option>
|
<option value="4.5">4.5</option>
|
||||||
<option value="5">5</option>
|
<option value="5">5</option>
|
||||||
</Select>
|
</Select>
|
||||||
<Select>
|
<Select>
|
||||||
<option selected disabled>Status</option>
|
<option disabled>Status</option>
|
||||||
<option value="active">Active</option>
|
<option value="done">Done</option>
|
||||||
<option value="inactive">Inactive</option>
|
<option value="todo">To Do</option>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
</BackofficeWrapper>
|
</BackofficeWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface IModalAddItem {
|
|||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
prevStep: () => void;
|
prevStep: () => void;
|
||||||
resetStep: () => void;
|
resetStep: () => void;
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalAddItem = ({
|
const ModalAddItem = ({
|
||||||
@@ -20,6 +21,7 @@ const ModalAddItem = ({
|
|||||||
nextStep,
|
nextStep,
|
||||||
resetStep,
|
resetStep,
|
||||||
handleAddItem,
|
handleAddItem,
|
||||||
|
onDataCapture,
|
||||||
}: IModalAddItem) => {
|
}: IModalAddItem) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -31,7 +33,7 @@ const ModalAddItem = ({
|
|||||||
}}
|
}}
|
||||||
disableEscapeKeyDown={true}
|
disableEscapeKeyDown={true}
|
||||||
>
|
>
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<StepTwo
|
<StepTwo
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
@@ -46,10 +48,11 @@ const ModalAddItem = ({
|
|||||||
interface IStepOneProps {
|
interface IStepOneProps {
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
|
||||||
const { form, onSubmit } = useItem(nextStep);
|
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
|||||||
interface IModalDeleteItem {
|
interface IModalDeleteItem {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
handleDeleteItem?: () => void;
|
handleDeleteItem?: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalDeleteItem = ({
|
const ModalDeleteItem = ({
|
||||||
@@ -48,8 +48,9 @@ const ModalDeleteItem = ({
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
handleDeleteItem && handleDeleteItem();
|
if (handleDeleteItem) await handleDeleteItem();
|
||||||
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Hapus Item
|
Hapus Item
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ interface IModalUpdateItem {
|
|||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
prevStep: () => void;
|
prevStep: () => void;
|
||||||
resetStep: () => void;
|
resetStep: () => void;
|
||||||
|
initialValues?: { itemName?: string; quantity?: number; chanceRate?: number };
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalUpdateItem = ({
|
const ModalUpdateItem = ({
|
||||||
@@ -20,6 +22,8 @@ const ModalUpdateItem = ({
|
|||||||
nextStep,
|
nextStep,
|
||||||
resetStep,
|
resetStep,
|
||||||
handleUpdateItem,
|
handleUpdateItem,
|
||||||
|
initialValues,
|
||||||
|
onDataCapture,
|
||||||
}: IModalUpdateItem) => {
|
}: IModalUpdateItem) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -31,7 +35,7 @@ const ModalUpdateItem = ({
|
|||||||
}}
|
}}
|
||||||
disableEscapeKeyDown={true}
|
disableEscapeKeyDown={true}
|
||||||
>
|
>
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} initialValues={initialValues} onDataCapture={onDataCapture} />}
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<StepTwo
|
<StepTwo
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
@@ -46,16 +50,12 @@ const ModalUpdateItem = ({
|
|||||||
interface IStepOneProps {
|
interface IStepOneProps {
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
initialValues?: { itemName?: string; quantity?: number; chanceRate?: number };
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
|
||||||
const initialValues = {
|
const { form, onSubmit } = useItem(nextStep, initialValues as any, onDataCapture);
|
||||||
itemName: 'Hoodie IMPHNEN Official 2025',
|
|
||||||
quantity: 10,
|
|
||||||
chanceRate: 0.1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { form, onSubmit } = useItem(nextStep, initialValues);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -105,7 +105,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={nextStep}
|
type="submit"
|
||||||
>
|
>
|
||||||
Perbarui Item
|
Perbarui Item
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ import { toast } from 'sonner';
|
|||||||
|
|
||||||
export const useItem = (
|
export const useItem = (
|
||||||
nextStep: () => void,
|
nextStep: () => void,
|
||||||
initialValues?: TGachaRollItem
|
initialValues?: Partial<TGachaRollItem>,
|
||||||
|
onDataCapture?: (data: any) => void,
|
||||||
) => {
|
) => {
|
||||||
const form = useForm<TGachaRollItem>({
|
const form = useForm<any>({
|
||||||
resolver: zodResolver(gachaRollItemSchema),
|
resolver: zodResolver(gachaRollItemSchema),
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
defaultValues: initialValues,
|
defaultValues: initialValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit((data) => {
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
console.log('Form data:', data);
|
onDataCapture?.(data);
|
||||||
nextStep();
|
nextStep();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,6 +39,9 @@ export const useConfirmItem = (
|
|||||||
) => {
|
) => {
|
||||||
const onConfirm = async () => {
|
const onConfirm = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (actionFunction) {
|
||||||
|
await actionFunction();
|
||||||
|
}
|
||||||
toast.success(messages?.success);
|
toast.success(messages?.success);
|
||||||
onClose();
|
onClose();
|
||||||
resetStep();
|
resetStep();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@@ -22,25 +22,21 @@ import ModalAddItem from './_components/modal-add-item';
|
|||||||
import ModalUpdateItem from './_components/modal-update-item';
|
import ModalUpdateItem from './_components/modal-update-item';
|
||||||
import ModalDeleteItem from './_components/modal-delete-item';
|
import ModalDeleteItem from './_components/modal-delete-item';
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
import {
|
||||||
interface GachaItem {
|
useGachaItemList,
|
||||||
id: number;
|
useCreateGachaItem,
|
||||||
name: string;
|
useUpdateGachaItem,
|
||||||
chanceRate: number;
|
useDeleteGachaItem,
|
||||||
quantity: number;
|
TGachaItemDto,
|
||||||
}
|
} from '@imphnen-frontend-service/service';
|
||||||
|
|
||||||
const mockData: GachaItem[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: 'Hoodie IMPHNEN Official 2025',
|
|
||||||
chanceRate: 0.1,
|
|
||||||
quantity: 10,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const pendingFormData = useRef<any>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
step: currentStep,
|
step: currentStep,
|
||||||
@@ -60,7 +56,60 @@ export const Components: FC = (): ReactElement => {
|
|||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
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',
|
id: 'select',
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
@@ -88,23 +137,16 @@ export const Components: FC = (): ReactElement => {
|
|||||||
header: 'Nama Item',
|
header: 'Nama Item',
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
header: 'Chance Rate',
|
|
||||||
accessorKey: 'chanceRate',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Quantity',
|
|
||||||
accessorKey: 'quantity',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
header: 'Action',
|
header: 'Action',
|
||||||
cell: () => (
|
cell: ({ row }) => (
|
||||||
<div className="flex gap-[8px]">
|
<div className="flex gap-[8px]">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setSelectedItem(row.original);
|
||||||
setShowModalUpdateItem(true);
|
setShowModalUpdateItem(true);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@@ -116,6 +158,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setSelectedItem(row.original);
|
||||||
setShowModalDeleteItem(true);
|
setShowModalDeleteItem(true);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@@ -128,7 +171,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: mockData,
|
data: items,
|
||||||
columns,
|
columns,
|
||||||
state: {
|
state: {
|
||||||
pagination,
|
pagination,
|
||||||
@@ -139,8 +182,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
onPaginationChange: setPagination,
|
onPaginationChange: setPagination,
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||||
manualPagination: false,
|
manualPagination: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -156,6 +199,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Cari berdasarkan nama item"
|
placeholder="Cari berdasarkan nama item"
|
||||||
className="pl-12 w-full max-h-full"
|
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]">
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
<SearchOutlined />
|
<SearchOutlined />
|
||||||
@@ -166,9 +211,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
size="md"
|
size="md"
|
||||||
className="flex gap-3 text-nowrap"
|
className="flex gap-3 text-nowrap"
|
||||||
onClick={() => {
|
onClick={() => setShowModalAddItem(true)}
|
||||||
setShowModalAddItem(true);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
Tambah Item
|
Tambah Item
|
||||||
@@ -176,7 +219,11 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -187,6 +234,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
|
handleAddItem={handleAdd}
|
||||||
|
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||||
/>
|
/>
|
||||||
<ModalUpdateItem
|
<ModalUpdateItem
|
||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
@@ -195,13 +244,14 @@ export const Components: FC = (): ReactElement => {
|
|||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
|
handleUpdateItem={handleUpdate}
|
||||||
|
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
|
||||||
|
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||||
/>
|
/>
|
||||||
<ModalDeleteItem
|
<ModalDeleteItem
|
||||||
isOpen={showModalDeleteItem}
|
isOpen={showModalDeleteItem}
|
||||||
onClose={() => setShowModalDeleteItem(false)}
|
onClose={() => setShowModalDeleteItem(false)}
|
||||||
handleDeleteItem={() => {
|
handleDeleteItem={handleDelete}
|
||||||
console.log('Item deleted');
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
+6
-3
@@ -11,6 +11,7 @@ interface IModalAddPermission {
|
|||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
prevStep: () => void;
|
prevStep: () => void;
|
||||||
resetStep: () => void;
|
resetStep: () => void;
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalAddPermission = ({
|
const ModalAddPermission = ({
|
||||||
@@ -20,6 +21,7 @@ const ModalAddPermission = ({
|
|||||||
nextStep,
|
nextStep,
|
||||||
resetStep,
|
resetStep,
|
||||||
handleAddItem,
|
handleAddItem,
|
||||||
|
onDataCapture,
|
||||||
}: IModalAddPermission) => {
|
}: IModalAddPermission) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -31,7 +33,7 @@ const ModalAddPermission = ({
|
|||||||
}}
|
}}
|
||||||
disableEscapeKeyDown={true}
|
disableEscapeKeyDown={true}
|
||||||
>
|
>
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<StepTwo
|
<StepTwo
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
@@ -46,10 +48,11 @@ const ModalAddPermission = ({
|
|||||||
interface IStepOneProps {
|
interface IStepOneProps {
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
|
||||||
const { form, onSubmit } = useItem(nextStep);
|
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
|
||||||
|
|
||||||
return (
|
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 { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
import { useConfirmItem } from '../_hook/use-item';
|
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
interface IModalUpdatePermission {
|
interface IModalUpdatePermission {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -10,6 +13,8 @@ interface IModalUpdatePermission {
|
|||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
prevStep: () => void;
|
prevStep: () => void;
|
||||||
resetStep: () => void;
|
resetStep: () => void;
|
||||||
|
initialValues?: { name?: string };
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalUpdatePermission = ({
|
const ModalUpdatePermission = ({
|
||||||
@@ -17,10 +22,31 @@ const ModalUpdatePermission = ({
|
|||||||
onClose,
|
onClose,
|
||||||
resetStep,
|
resetStep,
|
||||||
handleUpdate,
|
handleUpdate,
|
||||||
|
initialValues,
|
||||||
|
onDataCapture,
|
||||||
}: IModalUpdatePermission) => {
|
}: IModalUpdatePermission) => {
|
||||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
|
const form = useForm<{ name: string }>({
|
||||||
success: 'Perubahan permissions berhasil dilakukan',
|
mode: 'all',
|
||||||
error: 'Perubahan permissions gagal dilakukan',
|
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 (
|
return (
|
||||||
@@ -36,24 +62,26 @@ const ModalUpdatePermission = ({
|
|||||||
</h2>
|
</h2>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Content className="flex flex-col gap-8">
|
<Modal.Content className="flex flex-col gap-8">
|
||||||
<InputField
|
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||||
label="Name"
|
<ControlledInputField
|
||||||
name="name"
|
control={form.control}
|
||||||
type="text"
|
label="Name"
|
||||||
placeholder="Nama Permission"
|
name="name"
|
||||||
size="lg"
|
type="text"
|
||||||
className="w-full"
|
placeholder="Nama Permission"
|
||||||
/>
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={onConfirm}
|
>
|
||||||
>
|
Update Permission
|
||||||
Update Permission
|
</Button>
|
||||||
</Button>
|
</form>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { toast } from 'sonner';
|
|||||||
|
|
||||||
export const useItem = (
|
export const useItem = (
|
||||||
nextStep: () => void,
|
nextStep: () => void,
|
||||||
initialValues?: any
|
initialValues?: any,
|
||||||
|
onDataCapture?: (data: any) => void,
|
||||||
) => {
|
) => {
|
||||||
const form = useForm<any>({
|
const form = useForm<any>({
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
@@ -11,7 +12,7 @@ export const useItem = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit((data) => {
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
console.log('Form data:', data);
|
onDataCapture?.(data);
|
||||||
nextStep();
|
nextStep();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,6 +33,9 @@ export const useConfirmItem = (
|
|||||||
) => {
|
) => {
|
||||||
const onConfirm = async () => {
|
const onConfirm = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (actionFunction) {
|
||||||
|
await actionFunction();
|
||||||
|
}
|
||||||
toast.success(messages?.success);
|
toast.success(messages?.success);
|
||||||
onClose();
|
onClose();
|
||||||
resetStep();
|
resetStep();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@@ -19,24 +19,22 @@ import ModalAddPermission from './_components/modal-add-permission';
|
|||||||
import ModalUpdatePermission from './_components/modal-update-permission';
|
import ModalUpdatePermission from './_components/modal-update-permission';
|
||||||
import ModalDeletePermission from './_components/modal-delete-permission';
|
import ModalDeletePermission from './_components/modal-delete-permission';
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
import {
|
||||||
|
usePermissionList,
|
||||||
|
useCreatePermission,
|
||||||
|
useUpdatePermission,
|
||||||
|
useDeletePermission,
|
||||||
|
TPermissionItem,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
import React from 'react';
|
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 => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<TPermissionItem | null>(null);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const pendingFormData = useRef<any>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
step: currentStep,
|
step: currentStep,
|
||||||
@@ -56,7 +54,40 @@ export const Components: FC = (): ReactElement => {
|
|||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
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',
|
id: 'select',
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
@@ -86,13 +117,14 @@ export const Components: FC = (): ReactElement => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Action',
|
header: 'Action',
|
||||||
cell: () => (
|
cell: ({ row }) => (
|
||||||
<div className="flex gap-[8px]">
|
<div className="flex gap-[8px]">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setSelectedItem(row.original);
|
||||||
setShowModalUpdateItem(true);
|
setShowModalUpdateItem(true);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@@ -104,6 +136,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setSelectedItem(row.original);
|
||||||
setShowModalDeleteItem(true);
|
setShowModalDeleteItem(true);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@@ -116,7 +149,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: mockData,
|
data: permissions,
|
||||||
columns,
|
columns,
|
||||||
state: {
|
state: {
|
||||||
pagination,
|
pagination,
|
||||||
@@ -127,8 +160,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
onPaginationChange: setPagination,
|
onPaginationChange: setPagination,
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||||
manualPagination: false,
|
manualPagination: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -144,6 +177,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Cari berdasarkan nama permissions"
|
placeholder="Cari berdasarkan nama permissions"
|
||||||
className="pl-12 w-full max-h-full"
|
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]">
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
<SearchOutlined />
|
<SearchOutlined />
|
||||||
@@ -154,22 +189,24 @@ export const Components: FC = (): ReactElement => {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
size="md"
|
size="md"
|
||||||
className="flex gap-3 text-nowrap"
|
className="flex gap-3 text-nowrap"
|
||||||
onClick={() => {
|
onClick={() => setShowModalAddItem(true)}
|
||||||
setShowModalAddItem(true);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
Tambah Permissionss
|
Tambah Permissions
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
{isLoading ? (
|
||||||
data={mockData}
|
<div className="text-center py-8 text-neutral-400">Loading...</div>
|
||||||
columns={columns}
|
) : (
|
||||||
pageSize={9}
|
<DataTable
|
||||||
table={table}
|
data={permissions}
|
||||||
/>
|
columns={columns}
|
||||||
|
pageSize={9}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -180,6 +217,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
|
handleAddItem={handleAdd}
|
||||||
|
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||||
/>
|
/>
|
||||||
<ModalUpdatePermission
|
<ModalUpdatePermission
|
||||||
isOpen={showModalUpdateItem}
|
isOpen={showModalUpdateItem}
|
||||||
@@ -187,6 +226,9 @@ export const Components: FC = (): ReactElement => {
|
|||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
|
handleUpdate={handleUpdate}
|
||||||
|
initialValues={selectedItem ? { name: selectedItem.name } : undefined}
|
||||||
|
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||||
/>
|
/>
|
||||||
<ModalDeletePermission
|
<ModalDeletePermission
|
||||||
isOpen={showModalDeleteItem}
|
isOpen={showModalDeleteItem}
|
||||||
@@ -194,6 +236,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
|
handleDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface IModalAddRole {
|
|||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
prevStep: () => void;
|
prevStep: () => void;
|
||||||
resetStep: () => void;
|
resetStep: () => void;
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalAddRole = ({
|
const ModalAddRole = ({
|
||||||
@@ -20,6 +21,7 @@ const ModalAddRole = ({
|
|||||||
nextStep,
|
nextStep,
|
||||||
resetStep,
|
resetStep,
|
||||||
handleAdd,
|
handleAdd,
|
||||||
|
onDataCapture,
|
||||||
}: IModalAddRole) => {
|
}: IModalAddRole) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -31,7 +33,7 @@ const ModalAddRole = ({
|
|||||||
}}
|
}}
|
||||||
disableEscapeKeyDown={true}
|
disableEscapeKeyDown={true}
|
||||||
>
|
>
|
||||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<StepTwo
|
<StepTwo
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
@@ -46,10 +48,11 @@ const ModalAddRole = ({
|
|||||||
interface IStepOneProps {
|
interface IStepOneProps {
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
|
||||||
const { form, onSubmit } = useItem(nextStep);
|
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
|
||||||
|
|
||||||
return (
|
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 { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
import { useConfirmItem } from '../_hook/use-item';
|
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
interface IModalUpdatePermission {
|
interface IModalUpdateRole {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
handleUpdate?: () => Promise<boolean>;
|
handleUpdate?: () => Promise<boolean>;
|
||||||
@@ -10,17 +13,40 @@ interface IModalUpdatePermission {
|
|||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
prevStep: () => void;
|
prevStep: () => void;
|
||||||
resetStep: () => void;
|
resetStep: () => void;
|
||||||
|
initialValues?: { name?: string };
|
||||||
|
onDataCapture?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalUpdatePermission = ({
|
const ModalUpdateRole = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
resetStep,
|
resetStep,
|
||||||
handleUpdate,
|
handleUpdate,
|
||||||
}: IModalUpdatePermission) => {
|
initialValues,
|
||||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
|
onDataCapture,
|
||||||
success: 'Perubahan roles berhasil dilakukan',
|
}: IModalUpdateRole) => {
|
||||||
error: 'Perubahan roles 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 roles berhasil dilakukan');
|
||||||
|
onClose();
|
||||||
|
resetStep();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error('Perubahan roles gagal dilakukan');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -36,76 +62,78 @@ const ModalUpdatePermission = ({
|
|||||||
</h2>
|
</h2>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Content className="flex flex-col gap-8">
|
<Modal.Content className="flex flex-col gap-8">
|
||||||
<div className="flex flex-col gap-4">
|
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||||
<InputField
|
<div className="flex flex-col gap-4">
|
||||||
label="Nama Role"
|
<ControlledInputField
|
||||||
name="name"
|
control={form.control}
|
||||||
type="text"
|
label="Nama Role"
|
||||||
placeholder="Masukkan 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"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
type="submit"
|
||||||
</div>
|
>
|
||||||
|
Update Role
|
||||||
<div className="flex flex-col gap-4 items-start overflow-auto">
|
</Button>
|
||||||
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
|
</form>
|
||||||
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>
|
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ModalUpdatePermission;
|
export default ModalUpdateRole;
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { toast } from 'sonner';
|
|||||||
|
|
||||||
export const useItem = (
|
export const useItem = (
|
||||||
nextStep: () => void,
|
nextStep: () => void,
|
||||||
initialValues?: any
|
initialValues?: any,
|
||||||
|
onDataCapture?: (data: any) => void,
|
||||||
) => {
|
) => {
|
||||||
const form = useForm<any>({
|
const form = useForm<any>({
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
@@ -11,7 +12,7 @@ export const useItem = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit((data) => {
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
console.log('Form data:', data);
|
onDataCapture?.(data);
|
||||||
nextStep();
|
nextStep();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,6 +33,9 @@ export const useConfirmItem = (
|
|||||||
) => {
|
) => {
|
||||||
const onConfirm = async () => {
|
const onConfirm = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (actionFunction) {
|
||||||
|
await actionFunction();
|
||||||
|
}
|
||||||
toast.success(messages?.success);
|
toast.success(messages?.success);
|
||||||
onClose();
|
onClose();
|
||||||
resetStep();
|
resetStep();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@@ -19,25 +19,22 @@ import ModalAddRole from './_components/modal-add-role';
|
|||||||
import ModalUpdateRole from './_components/modal-update-role';
|
import ModalUpdateRole from './_components/modal-update-role';
|
||||||
import ModalDeleteRole from './_components/modal-delete-role';
|
import ModalDeleteRole from './_components/modal-delete-role';
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
import {
|
||||||
|
useRoleList,
|
||||||
|
useCreateRole,
|
||||||
|
useUpdateRole,
|
||||||
|
useDeleteRole,
|
||||||
|
TRolesListItem,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
import React from 'react';
|
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 => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||||
|
const [selectedRole, setSelectedRole] = useState<TRolesListItem | null>(null);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const pendingFormData = useRef<any>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
step: currentStep,
|
step: currentStep,
|
||||||
@@ -57,7 +54,40 @@ export const Components: FC = (): ReactElement => {
|
|||||||
|
|
||||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
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',
|
id: 'select',
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
@@ -87,13 +117,14 @@ export const Components: FC = (): ReactElement => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Action',
|
header: 'Action',
|
||||||
cell: () => (
|
cell: ({ row }) => (
|
||||||
<div className="flex gap-[8px]">
|
<div className="flex gap-[8px]">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setSelectedRole(row.original);
|
||||||
setShowModalUpdateItem(true);
|
setShowModalUpdateItem(true);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@@ -105,6 +136,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setSelectedRole(row.original);
|
||||||
setShowModalDeleteItem(true);
|
setShowModalDeleteItem(true);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@@ -117,7 +149,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: mockData,
|
data: roles,
|
||||||
columns,
|
columns,
|
||||||
state: {
|
state: {
|
||||||
pagination,
|
pagination,
|
||||||
@@ -128,8 +160,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
onPaginationChange: setPagination,
|
onPaginationChange: setPagination,
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||||
manualPagination: false,
|
manualPagination: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -145,6 +177,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Cari berdasarkan nama roles"
|
placeholder="Cari berdasarkan nama roles"
|
||||||
className="pl-12 w-full max-h-full"
|
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]">
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
<SearchOutlined />
|
<SearchOutlined />
|
||||||
@@ -155,9 +189,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
size="md"
|
size="md"
|
||||||
className="flex gap-3 text-nowrap"
|
className="flex gap-3 text-nowrap"
|
||||||
onClick={() => {
|
onClick={() => setShowModalAddItem(true)}
|
||||||
setShowModalAddItem(true);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
Tambah Role
|
Tambah Role
|
||||||
@@ -165,12 +197,16 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
{isLoading ? (
|
||||||
data={mockData}
|
<div className="text-center py-8 text-neutral-400">Loading...</div>
|
||||||
columns={columns}
|
) : (
|
||||||
pageSize={9}
|
<DataTable
|
||||||
table={table}
|
data={roles}
|
||||||
/>
|
columns={columns}
|
||||||
|
pageSize={9}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -181,6 +217,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
|
handleAdd={handleAdd}
|
||||||
|
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||||
/>
|
/>
|
||||||
<ModalUpdateRole
|
<ModalUpdateRole
|
||||||
isOpen={showModalUpdateItem}
|
isOpen={showModalUpdateItem}
|
||||||
@@ -188,6 +226,9 @@ export const Components: FC = (): ReactElement => {
|
|||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
|
handleUpdate={handleUpdate}
|
||||||
|
initialValues={selectedRole ? { name: selectedRole.name } : undefined}
|
||||||
|
onDataCapture={(data) => { pendingFormData.current = data; }}
|
||||||
/>
|
/>
|
||||||
<ModalDeleteRole
|
<ModalDeleteRole
|
||||||
isOpen={showModalDeleteItem}
|
isOpen={showModalDeleteItem}
|
||||||
@@ -195,6 +236,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
resetStep={resetStep}
|
resetStep={resetStep}
|
||||||
|
handleDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,27 +5,11 @@ import { cn } from "@imphnen-frontend-service/utils";
|
|||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
import { ReactElement, useState } from "react";
|
import { ReactElement, useState } from "react";
|
||||||
import { ModalDetailSession } from "./_components/modal/detail";
|
import { ModalDetailSession } from "./_components/modal/detail";
|
||||||
|
import { useMySessions, TSessionListItem } from "@imphnen-frontend-service/service";
|
||||||
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',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
export default function Components(): ReactElement {
|
||||||
const [openDetail, setOpenDetail] = useState(false);
|
const [openDetail, setOpenDetail] = useState(false);
|
||||||
|
const [statusFilter, setStatusFilter] = useState('');
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
@@ -33,7 +17,14 @@ export default function Components(): ReactElement {
|
|||||||
pageSize: 9,
|
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',
|
id: 'select',
|
||||||
meta: { cellClassName: cn("w-20") },
|
meta: { cellClassName: cn("w-20") },
|
||||||
@@ -60,19 +51,22 @@ export default function Components(): ReactElement {
|
|||||||
accessorKey: 'id',
|
accessorKey: 'id',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'mentorName',
|
id: 'mentorId',
|
||||||
header: 'Nama Mentor',
|
header: 'Nama Mentor',
|
||||||
accessorKey: 'name',
|
accessorKey: 'mentor_id',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'menteeName',
|
id: 'menteeName',
|
||||||
header: 'Nama Mentee',
|
header: 'Nama Mentee',
|
||||||
accessorKey: 'name',
|
accessorKey: 'mentee_fullname',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'datetime',
|
id: 'datetime',
|
||||||
header: 'Waktu',
|
header: 'Waktu',
|
||||||
accessorKey: 'datetime',
|
accessorKey: 'scheduled_at',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span>{new Date(row.original.scheduled_at).toLocaleString('id-ID')}</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'status',
|
id: 'status',
|
||||||
@@ -80,19 +74,16 @@ export default function Components(): ReactElement {
|
|||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const status = row.original.status;
|
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',
|
ongoing: 'bg-warning-200 text-warning-700',
|
||||||
finished: 'bg-success-200 text-success-500',
|
completed: 'bg-success-200 text-success-500',
|
||||||
};
|
cancelled: 'bg-danger-200 text-danger-500',
|
||||||
const statusText: Record<SessionStatus, string> = {
|
|
||||||
ongoing: 'On Going',
|
|
||||||
finished: 'Finished',
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`py-2 px-4 rounded-md text-center capitalize ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}>
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
{status}
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -100,7 +91,7 @@ export default function Components(): ReactElement {
|
|||||||
{
|
{
|
||||||
header: 'Action',
|
header: 'Action',
|
||||||
meta: { cellClassName: cn("w-52") },
|
meta: { cellClassName: cn("w-52") },
|
||||||
cell: ({ row }) => (
|
cell: () => (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -117,7 +108,7 @@ export default function Components(): ReactElement {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: mockData,
|
data: sessions,
|
||||||
columns,
|
columns,
|
||||||
state: {
|
state: {
|
||||||
pagination,
|
pagination,
|
||||||
@@ -128,8 +119,8 @@ export default function Components(): ReactElement {
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
onPaginationChange: setPagination,
|
onPaginationChange: setPagination,
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||||
manualPagination: false,
|
manualPagination: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -147,19 +138,21 @@ export default function Components(): ReactElement {
|
|||||||
<SearchOutlined />
|
<SearchOutlined />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Select>
|
<Select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
|
||||||
<option selected disabled>Rating</option>
|
<option value="">Semua Status</option>
|
||||||
<option value="4.5">4.5</option>
|
<option value="pending">Pending</option>
|
||||||
<option value="5">5</option>
|
<option value="confirmed">Confirmed</option>
|
||||||
</Select>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Status</option>
|
|
||||||
<option value="finished">Finished</option>
|
|
||||||
<option value="ongoing">On Going</option>
|
<option value="ongoing">On Going</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
|
<option value="cancelled">Cancelled</option>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
|
|
||||||
<ModalDetailSession open={openDetail} setOpen={setOpenDetail} />
|
<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 { cn } from "@imphnen-frontend-service/utils";
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
|
import { useRoleList, useDeleteRole, TRolesListItem } from "@imphnen-frontend-service/service";
|
||||||
type UserRolesPermissionType = {
|
import { toast } from "sonner";
|
||||||
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
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const UserRolesPermission: FC = () => {
|
export const UserRolesPermission: FC = () => {
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
@@ -24,7 +14,25 @@ export const UserRolesPermission: FC = () => {
|
|||||||
pageSize: 9,
|
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',
|
id: 'select',
|
||||||
meta: { cellClassName: cn("w-20") },
|
meta: { cellClassName: cn("w-20") },
|
||||||
@@ -48,12 +56,12 @@ export const UserRolesPermission: FC = () => {
|
|||||||
{
|
{
|
||||||
id: 'role',
|
id: 'role',
|
||||||
header: 'Role',
|
header: 'Role',
|
||||||
accessorKey: 'role',
|
accessorKey: 'name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'totalUser',
|
id: 'totalUser',
|
||||||
header: 'Total User',
|
header: 'Total Permissions',
|
||||||
accessorKey: 'totalUser',
|
accessorKey: 'permissions_count',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Action',
|
header: 'Action',
|
||||||
@@ -75,6 +83,7 @@ export const UserRolesPermission: FC = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
handleDelete(row.original.id);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2 w-max"
|
className="flex items-center gap-2 w-max"
|
||||||
>
|
>
|
||||||
@@ -86,7 +95,7 @@ export const UserRolesPermission: FC = () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: mockData,
|
data: roles,
|
||||||
columns,
|
columns,
|
||||||
state: {
|
state: {
|
||||||
pagination,
|
pagination,
|
||||||
@@ -97,8 +106,8 @@ export const UserRolesPermission: FC = () => {
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
onPaginationChange: setPagination,
|
onPaginationChange: setPagination,
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
pageCount: Math.ceil(totalItems / pagination.pageSize),
|
||||||
manualPagination: false,
|
manualPagination: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -106,12 +115,16 @@ export const UserRolesPermission: FC = () => {
|
|||||||
<div className="mb-8 flex items-center justify-between">
|
<div className="mb-8 flex items-center justify-between">
|
||||||
<h1 className="text-p2 font-semibold text-neutral-700">User Roles & Permissions</h1>
|
<h1 className="text-p2 font-semibold text-neutral-700">User Roles & Permissions</h1>
|
||||||
<Button type="button">
|
<Button type="button">
|
||||||
Add Rols
|
Add Role
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white shadow p-8 rounded-lg">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,30 +15,19 @@ import {
|
|||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { ReactElement, useState } from 'react';
|
import { ReactElement, useState } from 'react';
|
||||||
import { ModalDetailUser } from './_components/modal/detail';
|
import { ModalDetailUser } from './_components/modal/detail';
|
||||||
|
import {
|
||||||
type UserStatus = 'active' | 'inactive';
|
useMentorList,
|
||||||
|
useUserList,
|
||||||
interface UserType {
|
MentorDetailResponseDto,
|
||||||
id: number;
|
TUsersListItem,
|
||||||
name: string;
|
} from '@imphnen-frontend-service/service';
|
||||||
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',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
export default function Components(): ReactElement {
|
||||||
const TABS = ['mentor', 'mentee'] as const;
|
const TABS = ['mentor', 'mentee'] as const;
|
||||||
const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor');
|
const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor');
|
||||||
const [showDetail, setShowDetail] = useState(false);
|
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 [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
@@ -46,7 +35,27 @@ export default function Components(): ReactElement {
|
|||||||
pageSize: 9,
|
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',
|
id: 'select',
|
||||||
meta: { cellClassName: cn('w-20') },
|
meta: { cellClassName: cn('w-20') },
|
||||||
@@ -70,7 +79,7 @@ export default function Components(): ReactElement {
|
|||||||
{
|
{
|
||||||
id: 'name',
|
id: 'name',
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
accessorKey: 'name',
|
accessorKey: 'fullname',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'email',
|
id: 'email',
|
||||||
@@ -81,6 +90,7 @@ export default function Components(): ReactElement {
|
|||||||
id: 'rating',
|
id: 'rating',
|
||||||
header: 'Rating',
|
header: 'Rating',
|
||||||
accessorKey: 'rating',
|
accessorKey: 'rating',
|
||||||
|
cell: ({ row }) => <span>{row.original.rating ?? '-'}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'status',
|
id: 'status',
|
||||||
@@ -88,19 +98,14 @@ export default function Components(): ReactElement {
|
|||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const status = row.original.status;
|
const status = row.original.status;
|
||||||
const statusColors: Record<UserStatus, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
active: 'bg-success-200 text-success-500',
|
active: 'bg-success-200 text-success-500',
|
||||||
|
pending: 'bg-warning-200 text-warning-700',
|
||||||
inactive: 'bg-danger-200 text-danger-500',
|
inactive: 'bg-danger-200 text-danger-500',
|
||||||
};
|
};
|
||||||
const statusText: Record<UserStatus, string> = {
|
|
||||||
active: 'Active',
|
|
||||||
inactive: 'Inactive',
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`py-2 px-4 rounded-md text-center capitalize ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}>
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
{status}
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -125,20 +130,91 @@ export default function Components(): ReactElement {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const table = useReactTable({
|
const menteeColumns: ColumnDef<TUsersListItem>[] = [
|
||||||
data: mockData,
|
{
|
||||||
columns,
|
id: 'select',
|
||||||
state: {
|
meta: { cellClassName: cn('w-20') },
|
||||||
pagination,
|
header: ({ table }) => (
|
||||||
rowSelection,
|
<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,
|
enableRowSelection: true,
|
||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
onPaginationChange: setPagination,
|
onPaginationChange: setPagination,
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
pageCount: Math.ceil(mentorTotal / pagination.pageSize),
|
||||||
manualPagination: false,
|
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 (
|
return (
|
||||||
@@ -157,7 +233,10 @@ export default function Components(): ReactElement {
|
|||||||
'px-3 py-2 capitalize',
|
'px-3 py-2 capitalize',
|
||||||
activeTab === tab && 'bg-white'
|
activeTab === tab && 'bg-white'
|
||||||
)}
|
)}
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => {
|
||||||
|
setActiveTab(tab);
|
||||||
|
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -172,28 +251,22 @@ export default function Components(): ReactElement {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Cari berdasarkan nama lengkap"
|
placeholder="Cari berdasarkan nama lengkap"
|
||||||
className="pl-12 w-full max-h-full"
|
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]">
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
<SearchOutlined />
|
<SearchOutlined />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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>
|
</section>
|
||||||
|
|
||||||
<ModalDetailUser
|
<ModalDetailUser
|
||||||
|
|||||||
+60
-6
@@ -10,6 +10,9 @@ import { QrisPaymentStep } from "./steps/qris-payement"
|
|||||||
import { VAPaymentStep } from "./steps/va-payment"
|
import { VAPaymentStep } from "./steps/va-payment"
|
||||||
import { SuccessStep } from "./steps/success"
|
import { SuccessStep } from "./steps/success"
|
||||||
import { PaymentStep } from "./steps/payment"
|
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
|
const STEPS = ['topic', 'schedule', 'profile', 'payment', 'qr-payment', 'va-payment', 'success'] as const
|
||||||
type Step = typeof STEPS[number]
|
type Step = typeof STEPS[number]
|
||||||
@@ -17,15 +20,48 @@ type Step = typeof STEPS[number]
|
|||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
setOpen: (open: boolean) => void
|
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 [step, setStep] = useState<Step>('topic')
|
||||||
const [selectedTopics, setSelectedTopics] = useState<number[]>([])
|
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') {
|
if (action === 'next' && step === 'success') {
|
||||||
setOpen(false)
|
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') {
|
} else if (action === 'next') {
|
||||||
setStep(STEPS[STEPS.indexOf(step) + 1])
|
setStep(STEPS[STEPS.indexOf(step) + 1])
|
||||||
} else if (action === 'prev' && step !== 'topic') {
|
} else if (action === 'prev' && step !== 'topic') {
|
||||||
@@ -46,6 +82,12 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
|||||||
window.addEventListener("keydown", handleEscapeKey)
|
window.addEventListener("keydown", handleEscapeKey)
|
||||||
} else {
|
} else {
|
||||||
document.body.style.overflow = ""
|
document.body.style.overflow = ""
|
||||||
|
setStep('topic')
|
||||||
|
setSelectedTopics([])
|
||||||
|
setScheduledDate('')
|
||||||
|
setScheduledTime('')
|
||||||
|
setDescription('')
|
||||||
|
setSessionType('online')
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -77,6 +119,7 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="text"
|
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"
|
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" />
|
<CloseOutlined className="md:text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -113,7 +156,18 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
|||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} />}
|
{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 === 'profile' && <ProfileStep />}
|
||||||
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} />}
|
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} />}
|
||||||
{step === 'qr-payment' && <QrisPaymentStep />}
|
{step === 'qr-payment' && <QrisPaymentStep />}
|
||||||
@@ -141,14 +195,14 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className={cn((step === 'topic' || step === 'success') && 'w-full')}
|
className={cn((step === 'topic' || step === 'success') && 'w-full')}
|
||||||
disabled={selectedTopics.length === 0 && step === 'topic'}
|
disabled={(selectedTopics.length === 0 && step === 'topic') || isBooking}
|
||||||
onClick={() => handleStep('next')}
|
onClick={() => handleStep('next')}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
condition={step !== 'success'}
|
condition={step !== 'success'}
|
||||||
fallback="Halman Booking"
|
fallback="Halman Booking"
|
||||||
>
|
>
|
||||||
<Show condition={step !== 'payment'} fallback="Bayar Sekarang">
|
<Show condition={step !== 'payment'} fallback={isBooking ? 'Memproses...' : 'Bayar Sekarang'}>
|
||||||
Selanjutnya
|
Selanjutnya
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -160,4 +214,4 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-6
@@ -3,7 +3,7 @@ import { cn } from "@imphnen-frontend-service/utils"
|
|||||||
import { motion } from "framer-motion"
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
|
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 __.
|
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
|
||||||
|
|
||||||
Saya ingin tahu secara khusus tentang ___.
|
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]')
|
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 (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white px-6 py-5 rounded-md md:px-6"
|
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 className="grid gap-2.5 md:grid-cols-2 md:gap-5">
|
||||||
<div>
|
<div>
|
||||||
<label className={labelClass}>Tanggal</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className={labelClass}>Waktu</label>
|
<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>
|
||||||
<div className="relative md:col-span-full">
|
<div className="relative md:col-span-full">
|
||||||
<label className={labelClass}>Lokasi</label>
|
<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="online">Online</option>
|
||||||
<option value="offline">Offline</option>
|
<option value="offline">Offline</option>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -48,7 +82,10 @@ export const ScheduleStep = () => {
|
|||||||
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
|
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="min-w-full w-full h-40"
|
className="min-w-full w-full h-40"
|
||||||
placeholder={placeholder} />
|
placeholder={placeholder}
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
+15
-12
@@ -1,27 +1,30 @@
|
|||||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
|
||||||
|
|
||||||
const EDUCATION = [
|
type Props = {
|
||||||
{ name: 'Universitas Widyabakti', major: 'Intern Front End', duration: '2 Months', range: 'Oct 2024 - Present' },
|
mentor?: MentorDetailResponseDto
|
||||||
{ name: 'SMKN 99 Banjaran', major: 'Rekayasa Perangkat Lunak', duration: '6 Months', range: 'May 2024 - Oct 2024' },
|
}
|
||||||
]
|
|
||||||
|
export const EducationSection: FC<Props> = ({ mentor }) => {
|
||||||
|
const education = mentor?.education ?? []
|
||||||
|
|
||||||
|
if (education.length === 0) return null
|
||||||
|
|
||||||
export const EducationSection = () => {
|
|
||||||
return (
|
return (
|
||||||
<div className="px-7 py-8 rounded-md shadow-md">
|
<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>
|
<h2 className="text-xs font-semibold mb-5 md:text-[15px] xl:text-[19px]">Education</h2>
|
||||||
<div className="space-y-4 divide-y">
|
<div className="space-y-4 divide-y">
|
||||||
<For data={EDUCATION}>
|
<For data={education}>
|
||||||
{(item, index) => (
|
{(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="rounded-full size-6 bg-neutral-200 md:size-7 xl:size-8"></div>
|
||||||
<div className='flex-1 text-[10px] font-medium'>
|
<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>
|
<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-300"> · </span>
|
||||||
<span className="text-neutral-400 font-normal">{item.duration}</span>
|
<span className="text-neutral-400 font-normal xl:font-medium">{item.period}</span>
|
||||||
<span className="text-neutral-300"> · </span>
|
|
||||||
<span className="text-neutral-400 font-normal xl:font-medium">{item.range}</span>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+13
-9
@@ -1,28 +1,32 @@
|
|||||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
|
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
|
||||||
|
|
||||||
const EXPERIENCE = [
|
type Props = {
|
||||||
{ name: 'Sunday.com', position: 'Intern Front End', duration: '2 Months', range: 'Oct 2024 - Present' },
|
mentor?: MentorDetailResponseDto
|
||||||
{ name: 'CodeX Digital', position: 'Intern Front End', duration: '6 Months', range: 'May 2024 - Oct 2024' },
|
}
|
||||||
]
|
|
||||||
|
export const ExperienceSection: FC<Props> = ({ mentor }) => {
|
||||||
|
const experience = mentor?.experience ?? []
|
||||||
|
|
||||||
|
if (experience.length === 0) return null
|
||||||
|
|
||||||
export const ExperienceSection: FC = () => {
|
|
||||||
return (
|
return (
|
||||||
<div className="px-7 py-8 rounded-md shadow-md">
|
<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>
|
<h2 className="text-xs font-semibold mb-5 md:text-[15px] xl:text-[19px]">Experience</h2>
|
||||||
<div className="space-y-4 divide-y">
|
<div className="space-y-4 divide-y">
|
||||||
<For data={EXPERIENCE}>
|
<For data={experience}>
|
||||||
{(item, index) => (
|
{(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="rounded-full size-6 bg-neutral-200 md:size-7 xl:size-8"></div>
|
||||||
<div className='flex-1 text-[10px] font-medium'>
|
<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>
|
<p>
|
||||||
<span className="text-neutral-600 xl:text-xs">{item.position}</span>
|
<span className="text-neutral-600 xl:text-xs">{item.position}</span>
|
||||||
<span className="text-neutral-300"> · </span>
|
<span className="text-neutral-300"> · </span>
|
||||||
<span className="text-neutral-400 font-normal">{item.duration}</span>
|
<span className="text-neutral-400 font-normal">{item.duration}</span>
|
||||||
<span className="text-neutral-300"> · </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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { StarFilled } from "@ant-design/icons"
|
import { StarFilled } from "@ant-design/icons"
|
||||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onBook: () => void
|
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 (
|
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 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
|
<div
|
||||||
@@ -33,56 +40,61 @@ export const ProfileSection: React.FC<Props> = ({ onBook }) => {
|
|||||||
"md:text-[19px] md:mb-2 md:text-start xl:text-[23px]",
|
"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>
|
</h1>
|
||||||
<p className="text-xs mb-4 text-neutral-600 md:mb-5 md:text-[15px] xl:text-[19px] xl:mb-5">
|
<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>
|
</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">
|
{rating > 0 && (
|
||||||
<div
|
<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">
|
||||||
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]"
|
<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" />
|
>
|
||||||
|
<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>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-3 xl:gap-y-4 xl:max-w-[402px]">
|
<div className="flex flex-col gap-y-3 xl:gap-y-4 xl:max-w-[402px]">
|
||||||
<div>
|
{expertise.length > 0 && (
|
||||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Expertise</h2>
|
<div>
|
||||||
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Expertise</h2>
|
||||||
<For data={['UI Design', 'UX Reseacrh']}>
|
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
||||||
{(item) => (
|
<For data={expertise}>
|
||||||
<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) => (
|
||||||
{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">
|
||||||
</div>
|
{item}
|
||||||
)}
|
</div>
|
||||||
</For>
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div>
|
{softSkills.length > 0 && (
|
||||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Soft Skills</h2>
|
<div>
|
||||||
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Topics of Interest</h2>
|
||||||
<For data={['Design Thinking', 'Communication', 'Problem Solving', '19:00 WIB']}>
|
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
||||||
{(item) => (
|
<For data={softSkills}>
|
||||||
<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) => (
|
||||||
{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">
|
||||||
</div>
|
{item}
|
||||||
)}
|
</div>
|
||||||
</For>
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden md:flex xl:hidden justify-between">
|
<div className="hidden md:flex xl:hidden justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-primary-500 text-[15px] font-semibold">Jum, 4 April 2025</p>
|
<p className="text-primary-500 text-[15px] font-semibold">{mentor?.availability_commitment || ''}</p>
|
||||||
<p className="text-neutral-500 text-xs font-medium">Jum, 4 April 2025</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" size="sm" onClick={onBook}>
|
<Button type="button" size="sm" onClick={onBook}>
|
||||||
Book Your Senpai!
|
Book Your Senpai!
|
||||||
|
|||||||
+13
-8
@@ -1,19 +1,24 @@
|
|||||||
import { StarFilled } from "@ant-design/icons";
|
import { StarFilled } from "@ant-design/icons";
|
||||||
import { For } from "@imphnen-frontend-service/utils";
|
import { For } from "@imphnen-frontend-service/utils";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service";
|
||||||
|
|
||||||
const SENPAI_STATISTIC = [
|
type Props = {
|
||||||
{ name: 'Total Sessions', count: 8 },
|
mentor?: MentorDetailResponseDto
|
||||||
{ name: 'Mentee Impact', count: 1000 },
|
}
|
||||||
{ name: 'Response Time', count: '30 Minute' }
|
|
||||||
]
|
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 (
|
return (
|
||||||
<div className="md:mb-10">
|
<div className="md:mb-10">
|
||||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Statistics</h2>
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Statistics</h2>
|
||||||
<div className="grid gap-4 xl:flex">
|
<div className="grid gap-4 xl:flex">
|
||||||
<For data={SENPAI_STATISTIC}>
|
<For data={stats}>
|
||||||
{(item, index) => (
|
{(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 key={index} className="px-2.5 py-2 border border-primary-50 rounded-md shadow flex items-center gap-x-2.5">
|
||||||
<div
|
<div
|
||||||
@@ -31,4 +36,4 @@ export const StatisticsSection: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { For } from "@imphnen-frontend-service/utils"
|
import { For } from "@imphnen-frontend-service/utils"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
|
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
|
||||||
|
|
||||||
export const TOPICS = [
|
export const TOPICS = [
|
||||||
{ id: 1, icon: '💼', name: 'Career & Self Development' },
|
{ 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' }
|
{ 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Topics</h2>
|
<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">
|
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5">
|
||||||
<For data={TOPICS}>
|
{mentorTopics.length > 0 ? (
|
||||||
{(item, index) => (
|
<For data={mentorTopics}>
|
||||||
<div
|
{(topic, index) => (
|
||||||
key={index}
|
<div
|
||||||
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
|
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>
|
{topic}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
import { ProfileSection } from './_components/sections/profile'
|
import { ProfileSection } from './_components/sections/profile'
|
||||||
import { StatisticsSection } from './_components/sections/senpai-statistics'
|
import { StatisticsSection } from './_components/sections/senpai-statistics'
|
||||||
import { TopicsSection } from './_components/sections/topics'
|
import { TopicsSection } from './_components/sections/topics'
|
||||||
@@ -7,15 +8,27 @@ import { EducationSection } from './_components/sections/education'
|
|||||||
import { SenpaiScheduleSection } from './_components/sections/senpai-schedule'
|
import { SenpaiScheduleSection } from './_components/sections/senpai-schedule'
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||||
import { AppointmentModal } from './_components/modals/appointment'
|
import { AppointmentModal } from './_components/modals/appointment'
|
||||||
|
import { useMentorById } from '@imphnen-frontend-service/service'
|
||||||
|
|
||||||
export const Components: FC = () => {
|
export const Components: FC = () => {
|
||||||
const [open, setOpen] = useState(false)
|
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 (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<section className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20">
|
<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">
|
<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)}>
|
<Button type="button" size="sm" className="w-full md:hidden" onClick={() => setOpen(true)}>
|
||||||
Book Your Senpai!
|
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="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">
|
<div className="space-y-10 md:space-y-7 xl:flex-1">
|
||||||
<StatisticsSection />
|
<StatisticsSection mentor={mentor} />
|
||||||
<TopicsSection />
|
<TopicsSection mentor={mentor} />
|
||||||
|
|
||||||
<div className="px-6 py-8 rounded-md shadow-md">
|
{mentor?.bio && (
|
||||||
<h2 className="text-xs text-neutral-800 font-semibold mb-5 md:text-[15px] xl:text-[19px]">Senpai Resume</h2>
|
<div className="px-6 py-8 rounded-md shadow-md">
|
||||||
<p className="text-[10px] font-medium text-neutral-600 text-pretty md:text-[15px]">
|
<h2 className="text-xs text-neutral-800 font-semibold mb-5 md:text-[15px] xl:text-[19px]">Senpai Resume</h2>
|
||||||
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 className="text-[10px] font-medium text-neutral-600 text-pretty md:text-[15px]">
|
||||||
</p>
|
{mentor.bio}
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ExperienceSection />
|
<ExperienceSection mentor={mentor} />
|
||||||
<EducationSection />
|
<EducationSection mentor={mentor} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden xl:block xl:w-[400px]">
|
<div className="hidden xl:block xl:w-[400px]">
|
||||||
@@ -44,7 +59,7 @@ export const Components: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<AppointmentModal open={open} setOpen={setOpen} />
|
<AppointmentModal open={open} setOpen={setOpen} mentorId={mentorId} />
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
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 (
|
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="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">
|
<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>
|
||||||
<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">
|
<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>
|
</Button>
|
||||||
<h2 className="mb-1">
|
<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]">
|
<Link to={`/mentoring/${mentor.id}`} className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
|
||||||
Fullname
|
{mentor.fullname || 'Unknown Mentor'}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[8px] text-neutral-500 mb-3 md:text-[10px] md:font-medium md:mb-4 lg:text-xs">
|
<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>
|
</p>
|
||||||
|
|
||||||
<div>
|
<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">
|
<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]">
|
{firstSkill && (
|
||||||
Communication
|
<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]">
|
||||||
</Button>
|
{firstSkill}
|
||||||
<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]">
|
</Button>
|
||||||
Communication
|
)}
|
||||||
</Button>
|
{secondSkill && (
|
||||||
<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]">
|
<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]">
|
||||||
+2
|
{secondSkill}
|
||||||
</Button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -8,27 +8,35 @@ import { MentorCard } from './_components/mentor-card';
|
|||||||
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
|
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
|
||||||
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
|
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
|
||||||
import { motion, useInView, Variants } from 'framer-motion';
|
import { motion, useInView, Variants } from 'framer-motion';
|
||||||
|
import { useMentorList } from '@imphnen-frontend-service/service';
|
||||||
const TEMP_DATA = [
|
|
||||||
{ id: 1, name: 'John Doe' },
|
|
||||||
{ id: 2, name: 'John Doe' },
|
|
||||||
{ id: 3, name: 'John Doe' },
|
|
||||||
{ id: 4, name: 'John Doe' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: 0,
|
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({
|
const table = useReactTable({
|
||||||
data: TEMP_DATA,
|
data: mentors,
|
||||||
columns: [],
|
columns: [],
|
||||||
state: { pagination },
|
state: { pagination },
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
onPaginationChange: setPagination,
|
onPaginationChange: (updater) => {
|
||||||
|
setPagination(updater);
|
||||||
|
},
|
||||||
|
pageCount: Math.ceil(totalItems / pagination.pageSize) || 1,
|
||||||
|
manualPagination: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ref = useRef(null)
|
const ref = useRef(null)
|
||||||
@@ -91,24 +99,33 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Cari berdasarkan nama, posisi/peran"
|
placeholder="Cari berdasarkan nama, posisi/peran"
|
||||||
className="relative min-w-full w-full"
|
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" />
|
<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>
|
||||||
|
|
||||||
<motion.div
|
{isLoading ? (
|
||||||
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
|
<div className="text-center py-12 text-neutral-400">Loading mentors...</div>
|
||||||
variants={containerVariants}
|
) : (
|
||||||
initial="hidden"
|
<motion.div
|
||||||
animate={isInView ? 'visible' : 'hidden'}
|
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
|
||||||
>
|
variants={containerVariants}
|
||||||
<For data={Array.from({ length: 8 })}>
|
initial="hidden"
|
||||||
{(_, index) => (
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
<motion.div key={index} variants={childVariants}>
|
>
|
||||||
<MentorCard />
|
<For data={mentors}>
|
||||||
</motion.div>
|
{(mentor, index) => (
|
||||||
)}
|
<motion.div key={mentor.id ?? index} variants={childVariants}>
|
||||||
</For>
|
<MentorCard mentor={mentor} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Pagination table={table} />
|
<Pagination table={table} />
|
||||||
</motion.div>
|
</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 ModalFormRegister from './_components/form/modal-form-register';
|
||||||
import { GachaItem } from './_components/item/gacha-item';
|
import { GachaItem } from './_components/item/gacha-item';
|
||||||
import { useModalLogin } from '@imphnen-frontend-service/utils';
|
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 => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const { showModalLogin, setShowModalLogin } = useModalLogin();
|
const { showModalLogin, setShowModalLogin } = useModalLogin();
|
||||||
const [showModalForgotPassword, setShowModalForgotPassword] = useState(false);
|
const [showModalForgotPassword, setShowModalForgotPassword] = useState(false);
|
||||||
const [showModalRegister, setShowModalRegister] = 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 scrollToRoulette = () => {
|
||||||
const rouletteSection = document.getElementById('roulette');
|
const rouletteSection = document.getElementById('roulette');
|
||||||
@@ -24,6 +35,21 @@ export const Components: FC = (): ReactElement => {
|
|||||||
setShowModalForgotPassword(true);
|
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 (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<section
|
<section
|
||||||
@@ -51,6 +77,14 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<p>Let's Go Checkout Our Merch &</p>
|
<p>Let's Go Checkout Our Merch &</p>
|
||||||
<p>Gacha Your Prize Here</p>
|
<p>Gacha Your Prize Here</p>
|
||||||
</div>
|
</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
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="bordered"
|
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="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">
|
<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>
|
</div>
|
||||||
<section
|
<section
|
||||||
id="gacha-play"
|
id="gacha-play"
|
||||||
className="flex flex-nowrap overflow-auto w-full gap-x-8 snap-x snap-mandatory"
|
className="flex flex-nowrap overflow-auto w-full gap-x-8 snap-x snap-mandatory"
|
||||||
>
|
>
|
||||||
<GachaItem
|
{gachaItems.length > 0 ? (
|
||||||
src="/gacha/certificate.png"
|
gachaItems.map((item) => (
|
||||||
label="Sertifikat + Laminating"
|
<GachaItem
|
||||||
/>
|
key={item.id}
|
||||||
<GachaItem
|
src="/gacha/certificate.png"
|
||||||
src="/gacha/lanyard-id-card.png"
|
label={item.name}
|
||||||
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/certificate.png" label="Sertifikat + Laminating" />
|
||||||
<GachaItem
|
<GachaItem src="/gacha/lanyard-id-card.png" label="Lanyard + ID Card" />
|
||||||
src="/gacha/gelang-karet.png"
|
<GachaItem src="/gacha/pin.png" label="Pin" />
|
||||||
label="Gelang Karet"
|
<GachaItem src="/gacha/sticker.png" label="Sticker Isi 3" />
|
||||||
className="h-[86px] md:h-[160px]"
|
<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>
|
</section>
|
||||||
<Button variant="secondary" size="md">
|
<Button
|
||||||
Spin Now
|
variant="secondary"
|
||||||
|
size="md"
|
||||||
|
onClick={handleSpin}
|
||||||
|
disabled={executeRoll.isPending}
|
||||||
|
>
|
||||||
|
{executeRoll.isPending ? 'Spinning...' : 'Spin Now'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -5,23 +5,28 @@ import type {
|
|||||||
TAdminSubmissionsResponse,
|
TAdminSubmissionsResponse,
|
||||||
} from '../../types/admin';
|
} from '../../types/admin';
|
||||||
|
|
||||||
const ADMIN_BASE_URL = '/admin';
|
|
||||||
|
|
||||||
export const getAdminUsers = async (params?: {
|
export const getAdminUsers = async (params?: {
|
||||||
page?: number;
|
page?: number;
|
||||||
per_page?: number;
|
per_page?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
is_admin?: boolean;
|
is_admin?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const response = await api.get<TAdminUsersResponse>(
|
const response = await api.get<TAdminUsersResponse>('/v1/hackathon/admin/users', { params });
|
||||||
`${ADMIN_BASE_URL}/users`,
|
return response.data;
|
||||||
{
|
};
|
||||||
params: {
|
|
||||||
...params,
|
export const getAdminUserById = async (userId: string) => {
|
||||||
is_admin: params?.is_admin ?? false,
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,10 +35,12 @@ export const getAdminTeams = async (params?: {
|
|||||||
per_page?: number;
|
per_page?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const response = await api.get<TAdminTeamsResponse>(
|
const response = await api.get<TAdminTeamsResponse>('/v1/hackathon/admin/teams', { params });
|
||||||
`${ADMIN_BASE_URL}/teams`,
|
return response.data;
|
||||||
{ params }
|
};
|
||||||
);
|
|
||||||
|
export const deleteAdminTeam = async (teamId: string) => {
|
||||||
|
const response = await api.delete(`/v1/hackathon/admin/teams/${teamId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,9 +50,21 @@ export const getAdminSubmissions = async (params?: {
|
|||||||
search?: string;
|
search?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const response = await api.get<TAdminSubmissionsResponse>(
|
const response = await api.get<TAdminSubmissionsResponse>('/v1/hackathon/admin/submissions', { params });
|
||||||
`${ADMIN_BASE_URL}/submissions`,
|
return response.data;
|
||||||
{ params }
|
};
|
||||||
);
|
|
||||||
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,80 +1,65 @@
|
|||||||
import { api, getBaseURL } from '../';
|
import { api } from '../index';
|
||||||
import {
|
import type { TLoginResponse, TRegisterRequest, TSendOTPRequest, TVerifyEmailRequest } from '../../types/auth';
|
||||||
TLoginRequest,
|
import type { TResponseMessage } from '../../types/common';
|
||||||
TLoginResponse,
|
|
||||||
TRegisterRequest,
|
|
||||||
TSendOTPRequest,
|
|
||||||
TVerifyEmailRequest,
|
|
||||||
TGoogleCallbackResponse,
|
|
||||||
} from '../../types/auth';
|
|
||||||
import { TResponseMessage } from '../../types/common';
|
|
||||||
|
|
||||||
export const postLogin = async (
|
export type TLoginRequest = {
|
||||||
payload: TLoginRequest
|
email: string;
|
||||||
): Promise<TLoginResponse> => {
|
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({
|
const { data } = await api({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/auth/login',
|
url: '/v1/iam/auth/verify-email',
|
||||||
data: payload,
|
data: { otp: parseInt(payload.otp), email: payload.email },
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const postRegister = async (
|
export const postSendOtp = async (payload: TSendOTPRequest): Promise<TResponseMessage> => {
|
||||||
payload: TRegisterRequest
|
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/send-otp', data: payload });
|
||||||
): Promise<TResponseMessage> => {
|
|
||||||
const { data } = await api({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/auth/register',
|
|
||||||
data: payload,
|
|
||||||
});
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const postVerifyEmail = async (
|
export const postForgotPassword = async (payload: TForgotPasswordRequest): Promise<TResponseMessage> => {
|
||||||
payload: TVerifyEmailRequest
|
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/forgot', data: payload });
|
||||||
): Promise<TResponseMessage> => {
|
|
||||||
const { data } = await api({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/auth/verify-email',
|
|
||||||
data: {otp: parseInt(payload.otp), email: payload.email},
|
|
||||||
});
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const postSendOtp = async (
|
export const postNewPassword = async (payload: TNewPasswordRequest): Promise<TResponseMessage> => {
|
||||||
payload: TSendOTPRequest
|
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/new-password', data: payload });
|
||||||
): Promise<TResponseMessage> => {
|
|
||||||
const { data } = await api({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/auth/send-otp',
|
|
||||||
data: payload,
|
|
||||||
});
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGoogleAuthUrl = async (): Promise<string> => {
|
export const postRefreshToken = async (payload: TRefreshTokenRequest): Promise<{ access_token: string; refresh_token: string }> => {
|
||||||
const baseUrl = getBaseURL() || 'http://localhost:8080';
|
const { data } = await api({ method: 'POST', url: '/v1/iam/auth/refresh', data: payload });
|
||||||
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}`,
|
|
||||||
});
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,52 +1,6 @@
|
|||||||
import axios from 'axios';
|
// All API calls now go through the main `api` instance (api.imphnen.dev).
|
||||||
import { useAuthStore } from '../hooks/auth';
|
// This file re-exports `api` as `backofficeApi` for backward compatibility.
|
||||||
|
export { api as backofficeApi } from './index';
|
||||||
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'));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface BackofficeApiResponse<T> {
|
export interface BackofficeApiResponse<T> {
|
||||||
data: 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';
|
// All API calls now go through the main `api` instance (api.imphnen.dev).
|
||||||
import { useAuthStore } from '../hooks/auth';
|
// This file re-exports `api` as `hackathonApi` for backward compatibility.
|
||||||
|
export { api as hackathonApi } from './index';
|
||||||
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'));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface HackathonApiResponse<T> {
|
export interface HackathonApiResponse<T> {
|
||||||
data: T;
|
data: T;
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ export * from './mentors';
|
|||||||
export * from './upload';
|
export * from './upload';
|
||||||
export * from './hackathon';
|
export * from './hackathon';
|
||||||
export * from './admin';
|
export * from './admin';
|
||||||
|
export * from './roles';
|
||||||
|
export * from './permissions';
|
||||||
|
export * from './events';
|
||||||
|
export * from './testimonials';
|
||||||
|
export * from './sessions';
|
||||||
|
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
data: T;
|
data: T;
|
||||||
@@ -140,7 +145,7 @@ async function handleTokenRefresh(originalRequest: AxiosRequestConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshAccessToken(refreshToken: string) {
|
async function refreshAccessToken(refreshToken: string) {
|
||||||
return axios.post(`${getBaseURL()}/auth/refresh`, {
|
return axios.post(`${getBaseURL()}/v1/iam/auth/refresh`, {
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,61 @@
|
|||||||
import { api, ApiResponse } from '../index';
|
import { api, ApiResponse } from '../index';
|
||||||
import type {
|
import type { MentorDetailResponseDto, MentorUpdateRequestDto } from '../../types/mentors';
|
||||||
MentorDetailResponseDto,
|
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||||
MentorUpdateRequestDto
|
|
||||||
} from '../../types/mentors';
|
|
||||||
|
|
||||||
export interface MentorService {
|
export type MentorRegisterRequest = MentorUpdateRequestDto & {
|
||||||
getMentorMe(): Promise<MentorDetailResponseDto>;
|
email: string;
|
||||||
getMentorById(id: string): Promise<MentorDetailResponseDto>;
|
password: string;
|
||||||
updateMentorMe(data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto>;
|
};
|
||||||
updateMentorById(id: string, data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto>;
|
|
||||||
}
|
export const registerMentor = async (data: MentorRegisterRequest): Promise<MentorDetailResponseDto> => {
|
||||||
|
const response = await api.post<ApiResponse<MentorDetailResponseDto>>('/v1/dimentorin/mentors/create', data);
|
||||||
export const mentorService: MentorService = {
|
return response.data.data;
|
||||||
async getMentorMe() {
|
};
|
||||||
const response = await api.get<ApiResponse<MentorDetailResponseDto>>('/mentors/me');
|
|
||||||
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;
|
||||||
async getMentorById(id: string) {
|
};
|
||||||
const response = await api.get<ApiResponse<MentorDetailResponseDto>>(`/mentors/detail/${id}`);
|
|
||||||
return response.data.data;
|
export const getMentorMe = async (): Promise<MentorDetailResponseDto> => {
|
||||||
},
|
const response = await api.get<ApiResponse<MentorDetailResponseDto>>('/v1/dimentorin/mentors/me');
|
||||||
|
return response.data.data;
|
||||||
async updateMentorMe(data: MentorUpdateRequestDto) {
|
};
|
||||||
const response = await api.put<ApiResponse<MentorDetailResponseDto>>('/mentors/update/me', data);
|
|
||||||
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;
|
||||||
async updateMentorById(id: string, data: MentorUpdateRequestDto) {
|
};
|
||||||
const response = await api.put<ApiResponse<MentorDetailResponseDto>>(`/mentors/update/${id}`, data);
|
|
||||||
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,
|
TUpdateTeamRequest,
|
||||||
TInviteMemberRequest,
|
TInviteMemberRequest,
|
||||||
TJoinTeamRequest,
|
TJoinTeamRequest,
|
||||||
TManageMemberRequest,
|
|
||||||
TSubmitProjectRequest,
|
TSubmitProjectRequest,
|
||||||
TTeamListResponse,
|
TTeamListResponse,
|
||||||
TTeamDetailResponse,
|
TTeamDetailResponse,
|
||||||
@@ -14,8 +13,6 @@ import type {
|
|||||||
TProjectSubmissionResponse,
|
TProjectSubmissionResponse,
|
||||||
} from '../../types/teams';
|
} from '../../types/teams';
|
||||||
|
|
||||||
const TEAMS_BASE_URL = '/teams';
|
|
||||||
|
|
||||||
export const getTeams = async (params?: {
|
export const getTeams = async (params?: {
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
@@ -23,86 +20,93 @@ export const getTeams = async (params?: {
|
|||||||
visibility?: string;
|
visibility?: string;
|
||||||
search?: 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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTeamById = async (teamId: string) => {
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTeam = async (data: TCreateTeamRequest) => {
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateTeam = async (teamId: string, data: TUpdateTeamRequest) => {
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTeamMembers = async (teamId: string) => {
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const inviteMember = async (teamId: string, data: TInviteMemberRequest) => {
|
export const inviteMember = async (teamId: string, data: TInviteMemberRequest) => {
|
||||||
const response = await api.post(`${TEAMS_BASE_URL}/${teamId}/invite`, data);
|
const response = await api.post(`/v1/hackathon/invitations/teams/${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);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeMember = async (teamId: string, userId: string) => {
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const joinTeam = async (teamId: string, data: TJoinTeamRequest) => {
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTeamJoinRequests = async (teamId: string) => {
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const respondToJoinRequest = async (teamId: string, requestId: string, action: 'approve' | 'reject') => {
|
export const respondToJoinRequest = async (requestId: string, action: 'accept' | 'reject') => {
|
||||||
const response = await api.put(`${TEAMS_BASE_URL}/${teamId}/join-requests/${requestId}`, { action });
|
const response = await api.post(`/v1/hackathon/join-requests/${requestId}/respond`, { action });
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMyInvitations = async () => {
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const respondToInvitation = async (invitationId: string, action: 'accept' | 'reject') => {
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMyTeams = async () => {
|
export const getMyTeams = async () => {
|
||||||
const response = await api.get<TTeamListResponse>(`${TEAMS_BASE_URL}/me`);
|
const response = await api.get('/v1/hackathon/teams/my');
|
||||||
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`);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const leaveTeam = async (teamId: string) => {
|
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;
|
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';
|
import { api, ApiResponse } from '../index';
|
||||||
|
|
||||||
export interface UploadResponse {
|
export interface UploadResponse {
|
||||||
filename: string;
|
filename?: string;
|
||||||
original_filename: string;
|
original_filename?: string;
|
||||||
uploaded_path: string;
|
uploaded_path?: string;
|
||||||
url: string;
|
url: string;
|
||||||
size: number;
|
size?: number;
|
||||||
content_type: string;
|
content_type?: string;
|
||||||
file_type: string;
|
file_type?: string;
|
||||||
user_id: string;
|
user_id?: string;
|
||||||
email: string;
|
email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadService {
|
const multipartPost = async (url: string, file: File, fieldName = 'file'): Promise<UploadResponse> => {
|
||||||
uploadFile(file: File): Promise<UploadResponse>;
|
const formData = new FormData();
|
||||||
uploadAvatar(file: File): Promise<UploadResponse>;
|
formData.append(fieldName, file);
|
||||||
uploadCV(file: File): Promise<UploadResponse>;
|
const response = await api.post<ApiResponse<UploadResponse>>(url, formData, {
|
||||||
}
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
export const uploadService: UploadService = {
|
export const uploadUserFile = (file: File) => multipartPost('/v1/iam/users/upload', file);
|
||||||
async uploadFile(file: File) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
const response = await api.post<ApiResponse<UploadResponse>>('/users/upload', formData, {
|
export const uploadHackathonFile = (file: File) => multipartPost('/v1/hackathon/upload', file);
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async uploadAvatar(file: File) {
|
export const uploadHackathonAvatar = (file: File) => multipartPost('/v1/hackathon/upload/avatar', file);
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
throw new Error('File harus berupa gambar');
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxSize = 5 * 1024 * 1024;
|
export const uploadHackathonTeamFile = (file: File) => multipartPost('/v1/hackathon/upload/team', file);
|
||||||
if (file.size > maxSize) {
|
|
||||||
throw new Error('Ukuran file maksimal 5MB');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.uploadFile(file);
|
export const uploadHackathonSubmission = (file: File) => multipartPost('/v1/hackathon/upload/submission', file);
|
||||||
},
|
|
||||||
|
|
||||||
async uploadCV(file: File) {
|
// Legacy service object for backward compatibility
|
||||||
if (file.type !== 'application/pdf') {
|
export const uploadService = {
|
||||||
throw new Error('CV harus berupa file PDF');
|
uploadFile: uploadUserFile,
|
||||||
}
|
uploadAvatar: uploadUserFile,
|
||||||
|
uploadCV: uploadUserFile,
|
||||||
const maxSize = 10 * 1024 * 1024;
|
|
||||||
if (file.size > maxSize) {
|
|
||||||
throw new Error('Ukuran file maksimal 10MB');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.uploadFile(file);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,89 +1,69 @@
|
|||||||
import { api, ApiResponse } from '../index';
|
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 {
|
// Re-export for backward compatibility
|
||||||
bio?: string;
|
export type UserDetailResponseDto = TUsersDetailItem;
|
||||||
location?: string;
|
export type UserUpdateRequestDto = TUserUpdateRequest;
|
||||||
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;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserUpdateRequestDto {
|
export const getUserList = async (params?: TPaginationParams): Promise<TApiPaginated<TUsersListItem>> => {
|
||||||
fullname?: string;
|
const response = await api.get<TApiPaginated<TUsersListItem>>('/v1/iam/users', { params });
|
||||||
bio?: string;
|
return response.data;
|
||||||
location?: string;
|
};
|
||||||
website_url?: string;
|
|
||||||
linkedin_url?: string;
|
export const getUserMe = async (): Promise<TUsersDetailItem> => {
|
||||||
github_url?: string;
|
const response = await api.get<ApiResponse<TUsersDetailItem>>('/v1/iam/users/me');
|
||||||
twitter_url?: string;
|
return response.data.data;
|
||||||
skills?: string[];
|
};
|
||||||
phone_number?: string;
|
|
||||||
birthdate?: string;
|
export const getUserById = async (id: string): Promise<TUsersDetailItem> => {
|
||||||
gender?: string;
|
const response = await api.get<ApiResponse<TUsersDetailItem>>(`/v1/iam/users/detail/${id}`);
|
||||||
career_status?: string;
|
return response.data.data;
|
||||||
avatar?: string;
|
};
|
||||||
cv_url?: string;
|
|
||||||
phone_for_verification?: string;
|
export const createUser = async (data: TUserCreateRequest): Promise<TUsersDetailItem> => {
|
||||||
domicile?: string;
|
const response = await api.post<ApiResponse<TUsersDetailItem>>('/v1/iam/users/create', data);
|
||||||
experience?: Array<{
|
return response.data.data;
|
||||||
id: string;
|
};
|
||||||
company: string;
|
|
||||||
position: string;
|
export const updateUserMe = async (data: TUserUpdateRequest): Promise<TUsersDetailItem> => {
|
||||||
duration: string;
|
const response = await api.put<ApiResponse<TUsersDetailItem>>('/v1/iam/users/update/me', data);
|
||||||
period: string;
|
return response.data.data;
|
||||||
}>;
|
};
|
||||||
education?: Array<{
|
|
||||||
id: string;
|
export const updateUserById = async (id: string, data: TUserUpdateRequest): Promise<TUsersDetailItem> => {
|
||||||
institution: string;
|
const response = await api.put<ApiResponse<TUsersDetailItem>>(`/v1/iam/users/update/${id}`, data);
|
||||||
degree: string;
|
return response.data.data;
|
||||||
field: string;
|
};
|
||||||
period: string;
|
|
||||||
}>;
|
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 interface UserService {
|
};
|
||||||
getUserMe(): Promise<UserDetailResponseDto>;
|
|
||||||
getUserById(id: string): Promise<UserDetailResponseDto>;
|
export const deleteUser = async (id: string): Promise<{ message: string }> => {
|
||||||
updateUserMe(data: UserUpdateRequestDto): Promise<UserDetailResponseDto>;
|
const response = await api.delete<{ message: string }>(`/v1/iam/users/delete/${id}`);
|
||||||
updateUserById(id: string, data: UserUpdateRequestDto): Promise<UserDetailResponseDto>;
|
return response.data;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const userService: UserService = {
|
export const uploadUserFile = async (file: File): Promise<{ url: string }> => {
|
||||||
async getUserMe() {
|
const formData = new FormData();
|
||||||
const response = await api.get<ApiResponse<UserDetailResponseDto>>('/users/me');
|
formData.append('file', file);
|
||||||
return response.data.data;
|
const response = await api.post<ApiResponse<{ url: string }>>('/v1/iam/users/upload', formData, {
|
||||||
},
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
async getUserById(id: string) {
|
return response.data.data;
|
||||||
const response = await api.get<ApiResponse<UserDetailResponseDto>>(`/users/detail/${id}`);
|
};
|
||||||
return response.data.data;
|
|
||||||
},
|
// Legacy service object for backward compatibility
|
||||||
|
export const userService = {
|
||||||
async updateUserMe(data: UserUpdateRequestDto) {
|
getUserMe,
|
||||||
const response = await api.put<ApiResponse<UserDetailResponseDto>>('/users/update/me', data);
|
getUserById,
|
||||||
return response.data.data;
|
updateUserMe,
|
||||||
},
|
updateUserById: (id: string, data: TUserUpdateRequest) => updateUserById(id, data),
|
||||||
|
|
||||||
async updateUserById(id: string, data: UserUpdateRequestDto) {
|
|
||||||
const response = await api.put<ApiResponse<UserDetailResponseDto>>(`/users/${id}`, data);
|
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,242 +1,140 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
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 { 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';
|
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 = () => {
|
export const useLogin = () => {
|
||||||
const { setSession } = useAuthStore();
|
const { setSession } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (data: LoginRequest) => {
|
mutationFn: postLogin,
|
||||||
const response = await hackathonApi.post<
|
onSuccess: (res) => {
|
||||||
HackathonApiResponse<AuthResponse>
|
const u = res.data?.user;
|
||||||
>('/auth/login', data);
|
const t = res.data?.token;
|
||||||
return response.data.data;
|
if (!u || !t) return;
|
||||||
},
|
|
||||||
onSuccess: (data) => {
|
|
||||||
setSession({
|
setSession({
|
||||||
token: data.token,
|
token: t,
|
||||||
user: {
|
user: {
|
||||||
id: data.user.id,
|
id: u.id,
|
||||||
email: data.user.email,
|
email: u.email,
|
||||||
fullname: data.user.fullname,
|
fullname: u.fullname,
|
||||||
phone_number: data.user.phone_number || '',
|
phone_number: u.phone_number || '',
|
||||||
avatar: data.user.avatar || '',
|
avatar: u.avatar || '',
|
||||||
birthdate: data.user.birthdate || '',
|
birthdate: u.birthdate || '',
|
||||||
gender: data.user.gender || '',
|
gender: u.gender || '',
|
||||||
is_active: data.user.is_active,
|
is_active: u.is_active,
|
||||||
location: data.user.location,
|
location: u.location,
|
||||||
bio: data.user.bio,
|
bio: u.bio,
|
||||||
skills: data.user.skills,
|
skills: u.skills,
|
||||||
role: {
|
role: u.role ?? { id: '', name: 'user', permissions: [], created_at: '', updated_at: '' },
|
||||||
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 = () => {
|
export const useBackofficeLogin = () => {
|
||||||
const { setSession } = useAuthStore();
|
const { setSession } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (data: LoginRequest) => {
|
mutationFn: postLogin,
|
||||||
const response = await backofficeApi.post<
|
onSuccess: (res) => {
|
||||||
BackofficeApiResponse<AuthResponse>
|
const u = res.data?.user;
|
||||||
>('/auth/login', data);
|
const t = res.data?.token;
|
||||||
return response.data.data;
|
if (!u || !t) return;
|
||||||
},
|
|
||||||
onSuccess: (data) => {
|
|
||||||
setSession({
|
setSession({
|
||||||
token: data.token,
|
token: t,
|
||||||
user: {
|
user: {
|
||||||
id: data.user.id,
|
id: u.id,
|
||||||
email: data.user.email,
|
email: u.email,
|
||||||
fullname: data.user.fullname,
|
fullname: u.fullname,
|
||||||
phone_number: data.user.phone_number || '',
|
phone_number: u.phone_number || '',
|
||||||
avatar: data.user.avatar || '',
|
avatar: u.avatar || '',
|
||||||
birthdate: data.user.birthdate || '',
|
birthdate: u.birthdate || '',
|
||||||
gender: data.user.gender || '',
|
gender: u.gender || '',
|
||||||
is_active: data.user.is_active,
|
is_active: u.is_active,
|
||||||
location: data.user.location,
|
location: u.location,
|
||||||
bio: data.user.bio,
|
bio: u.bio,
|
||||||
skills: data.user.skills,
|
skills: u.skills,
|
||||||
role: {
|
role: u.role ?? { id: '', name: 'admin', permissions: [], created_at: '', updated_at: '' },
|
||||||
id: data.user.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) => {
|
export const getGitHubOAuthUrl = (clientId: string, redirectUri: string) => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
@@ -257,21 +155,23 @@ export const useGitHubAuth = () => {
|
|||||||
if (meta.env?.VITE_GITHUB_CLIENT_ID) clientId = meta.env.VITE_GITHUB_CLIENT_ID;
|
if (meta.env?.VITE_GITHUB_CLIENT_ID) clientId = meta.env.VITE_GITHUB_CLIENT_ID;
|
||||||
} catch { /* not in Vite context */ }
|
} catch { /* not in Vite context */ }
|
||||||
}
|
}
|
||||||
if (!clientId) {
|
if (!clientId) throw new Error('GitHub Client ID not configured.');
|
||||||
throw new Error(
|
|
||||||
'GitHub Client ID not configured.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const redirectUri = `${globalThis.location.origin}/auth/callback`;
|
const redirectUri = `${globalThis.location.origin}/auth/callback`;
|
||||||
const url = getGitHubOAuthUrl(clientId, redirectUri);
|
return { url: getGitHubOAuthUrl(clientId, redirectUri) };
|
||||||
|
|
||||||
return { url };
|
|
||||||
};
|
};
|
||||||
|
return { signInWithGitHub };
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
export const useGitHubCallback = () => {
|
||||||
signInWithGitHub,
|
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 = () => {
|
export const useEmailAuth = () => {
|
||||||
@@ -282,82 +182,20 @@ export const useEmailAuth = () => {
|
|||||||
const signInWithEmail = async (email: string, password: string) => {
|
const signInWithEmail = async (email: string, password: string) => {
|
||||||
const result = await loginMutation.mutateAsync({ email, password });
|
const result = await loginMutation.mutateAsync({ email, password });
|
||||||
return {
|
return {
|
||||||
user: result.user,
|
user: result.data?.user,
|
||||||
session: {
|
session: {
|
||||||
access_token: result.token.access_token,
|
access_token: result.data?.token?.access_token,
|
||||||
refresh_token: result.token.refresh_token,
|
refresh_token: result.data?.token?.refresh_token,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const signUpWithEmail = async (
|
const signUpWithEmail = async (email: string, password: string, fullname: string) => {
|
||||||
email: string,
|
const result = await signupMutation.mutateAsync({ email, password, fullname });
|
||||||
password: string,
|
return { message: result.message };
|
||||||
fullname: string
|
|
||||||
) => {
|
|
||||||
const result = await signupMutation.mutateAsync({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
fullname,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
message: result.message,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const signOut = async () => {
|
const signOut = async () => { clearSession(); };
|
||||||
clearSession();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return { signInWithEmail, signUpWithEmail, signOut };
|
||||||
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.');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 './messages';
|
||||||
export * from './winners';
|
export * from './winners';
|
||||||
export * from './use-session';
|
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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { mentorService } from '../../api/mentors';
|
import {
|
||||||
import { MentorDetailResponseDto, MentorUpdateRequestDto } from '../../types/mentors';
|
getMentorMe,
|
||||||
import { TResponseError } from '../../types/common';
|
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({
|
return useQuery({
|
||||||
queryKey: ['mentor-me'],
|
queryKey: ['mentor-me'],
|
||||||
queryFn: () => mentorService.getMentorMe(),
|
queryFn: getMentorMe,
|
||||||
...options,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useMentorById = (id: string, options?: UseQueryOptions<MentorDetailResponseDto, TResponseError>): UseQueryResult<MentorDetailResponseDto, TResponseError> => {
|
export const useMentorById = (id: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['mentor-by-id', id],
|
queryKey: ['mentor-by-id', id],
|
||||||
queryFn: () => mentorService.getMentorById(id),
|
queryFn: () => getMentorById(id),
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
...options,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateMentorMe = (): UseMutationResult<
|
export const useMentorList = (params?: TPaginationParams) => {
|
||||||
MentorDetailResponseDto,
|
return useQuery({
|
||||||
TResponseError,
|
queryKey: ['mentor-list', params],
|
||||||
MentorUpdateRequestDto,
|
queryFn: () => getMentorList(params),
|
||||||
unknown
|
});
|
||||||
> => {
|
};
|
||||||
|
|
||||||
|
export const useUpdateMentorMe = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['update-mentor-me'],
|
mutationKey: ['update-mentor-me'],
|
||||||
mutationFn: (data) => mentorService.updateMentorMe(data),
|
mutationFn: (data: MentorUpdateRequestDto) => updateMentorMe(data),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['mentor-me'] }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateMentorById = (): UseMutationResult<
|
export const useUpdateMentorById = () => {
|
||||||
MentorDetailResponseDto,
|
const queryClient = useQueryClient();
|
||||||
TResponseError,
|
|
||||||
{ id: string; data: MentorUpdateRequestDto },
|
|
||||||
unknown
|
|
||||||
> => {
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['update-mentor-by-id'],
|
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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
|
import { api } from '../../api/index';
|
||||||
import { useAuthStore } from '../auth';
|
import { useAuthStore } from '../auth';
|
||||||
|
|
||||||
export type Message = {
|
export type Message = {
|
||||||
@@ -9,12 +9,7 @@ export type Message = {
|
|||||||
message: string;
|
message: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
user?: {
|
user?: { id: string; fullname: string; avatar: string; email: string };
|
||||||
id: string;
|
|
||||||
fullname: string;
|
|
||||||
avatar: string;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const messageKeys = {
|
export const messageKeys = {
|
||||||
@@ -22,13 +17,13 @@ export const messageKeys = {
|
|||||||
team: (teamId: string) => [...messageKeys.all, 'team', teamId] as const,
|
team: (teamId: string) => [...messageKeys.all, 'team', teamId] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface ApiResp<T> { data: T; message?: string; }
|
||||||
|
|
||||||
export const useTeamMessages = (teamId: string) => {
|
export const useTeamMessages = (teamId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: messageKeys.team(teamId),
|
queryKey: messageKeys.team(teamId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await hackathonApi.get<HackathonApiResponse<Message[]>>(
|
const response = await api.get<ApiResp<Message[]>>(`/v1/hackathon/chat/teams/${teamId}`);
|
||||||
`/chat/teams/${teamId}`
|
|
||||||
);
|
|
||||||
return response.data.data || [];
|
return response.data.data || [];
|
||||||
},
|
},
|
||||||
enabled: !!teamId,
|
enabled: !!teamId,
|
||||||
@@ -43,20 +38,11 @@ export const useSendMessage = (teamId: string) => {
|
|||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (message: string) => {
|
mutationFn: async (message: string) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to send messages');
|
||||||
throw new Error('You must be logged in to send messages');
|
const response = await api.post<ApiResp<Message>>(`/v1/hackathon/chat/teams/${teamId}`, { message });
|
||||||
}
|
|
||||||
|
|
||||||
const response = await hackathonApi.post<HackathonApiResponse<Message>>(
|
|
||||||
`/chat/teams/${teamId}`,
|
|
||||||
{ message }
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) }),
|
||||||
queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) });
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,10 +51,8 @@ export const useDeleteMessage = (teamId: string) => {
|
|||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (messageId: string) => {
|
mutationFn: async (messageId: string) => {
|
||||||
await hackathonApi.delete(`/chat/messages/${messageId}`);
|
await api.delete(`/v1/hackathon/chat/messages/${messageId}`);
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) });
|
|
||||||
},
|
},
|
||||||
|
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 { useMutation, useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
|
||||||
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
|
import { api } from '../../api/index';
|
||||||
import { useAuthStore } from '../auth';
|
import { useAuthStore } from '../auth';
|
||||||
import type {
|
import type {
|
||||||
TCreateTeamRequest,
|
TCreateTeamRequest,
|
||||||
@@ -23,110 +23,41 @@ export const teamKeys = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface TeamMember {
|
interface TeamMember {
|
||||||
id: string;
|
id: string; team_id: string; user_id: string; role: string; status: string;
|
||||||
team_id: string;
|
joined_at: string; user?: { id: string; email: string; fullname: string; avatar: string };
|
||||||
user_id: string;
|
|
||||||
role: string;
|
|
||||||
status: string;
|
|
||||||
joined_at: string;
|
|
||||||
user?: {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
fullname: string;
|
|
||||||
avatar: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Team {
|
interface Team {
|
||||||
id: string;
|
id: string; name: string; logo?: string; banner?: string; description?: string;
|
||||||
name: string;
|
city?: string; visibility: string; leader_id: string; created_at: string;
|
||||||
logo?: string;
|
leader?: { id: string; email: string; fullname: string; avatar: string };
|
||||||
banner?: string;
|
members?: TeamMember[]; member_count?: number; has_submission?: boolean;
|
||||||
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 {
|
interface JoinRequest {
|
||||||
id: string;
|
id: string; team_id: string; user_id: string; message?: string; status: string;
|
||||||
team_id: string;
|
created_at: string; user?: { id: string; email: string; fullname: string; avatar: string };
|
||||||
user_id: string;
|
|
||||||
message?: string;
|
|
||||||
status: string;
|
|
||||||
created_at: string;
|
|
||||||
user?: {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
fullname: string;
|
|
||||||
avatar: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Invitation {
|
interface Invitation {
|
||||||
id: string;
|
id: string; team_id: string; inviter_id: string; invitee_email: string;
|
||||||
team_id: string;
|
invitee_id?: string; status: string; created_at: string; team?: Team;
|
||||||
inviter_id: string;
|
inviter?: { id: string; fullname: string; email: string; avatar: 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 {
|
interface Submission {
|
||||||
id: string;
|
id: string; team_id: string; project_name: string; description?: string;
|
||||||
team_id: string;
|
repository_url?: string; demo_url?: string; video_url?: string;
|
||||||
project_name: string;
|
presentation_url?: string; status: string; submitted_at?: string; created_at: 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> {
|
const TEAMS_PAGE_SIZE = 12;
|
||||||
message: string;
|
|
||||||
data: T[];
|
|
||||||
meta: {
|
|
||||||
page: number;
|
|
||||||
per_page: number;
|
|
||||||
total_page: number;
|
|
||||||
total_data: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useTeams = (params?: {
|
export const useTeams = (params?: {
|
||||||
page?: number;
|
page?: number; limit?: number; city?: string; visibility?: string; search?: string;
|
||||||
limit?: number;
|
minMembers?: number; maxMembers?: number; hasSubmission?: boolean;
|
||||||
city?: string;
|
|
||||||
visibility?: string;
|
|
||||||
search?: string;
|
|
||||||
minMembers?: number;
|
|
||||||
maxMembers?: number;
|
|
||||||
hasSubmission?: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: teamKeys.list(params),
|
queryKey: teamKeys.list(params as Record<string, unknown>),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
if (params?.page) queryParams.append('page', String(params.page));
|
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?.minMembers) queryParams.append('min_members', String(params.minMembers));
|
||||||
if (params?.maxMembers) queryParams.append('max_members', String(params.maxMembers));
|
if (params?.maxMembers) queryParams.append('max_members', String(params.maxMembers));
|
||||||
if (params?.hasSubmission !== undefined) queryParams.append('has_submission', String(params.hasSubmission));
|
if (params?.hasSubmission !== undefined) queryParams.append('has_submission', String(params.hasSubmission));
|
||||||
|
const qs = queryParams.toString();
|
||||||
const response = await hackathonApi.get<ListResponseWithMeta<Team>>(
|
const response = await api.get<ListResponse<Team>>(
|
||||||
`/teams/browse${queryParams.toString() ? `?${queryParams.toString()}` : ''}`
|
`/v1/hackathon/teams/browse${qs ? `?${qs}` : ''}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, meta } = response.data;
|
const { data, meta } = response.data;
|
||||||
return {
|
return {
|
||||||
teams: data || [],
|
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({
|
return useInfiniteQuery({
|
||||||
queryKey: [...teamKeys.lists(), 'infinite', params],
|
queryKey: [...teamKeys.lists(), 'infinite', params],
|
||||||
queryFn: async ({ pageParam = 1 }) => {
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
@@ -170,19 +94,12 @@ export const useInfiniteTeams = (params?: {
|
|||||||
if (params?.search) queryParams.append('search', params.search);
|
if (params?.search) queryParams.append('search', params.search);
|
||||||
if (params?.city) queryParams.append('city', params.city);
|
if (params?.city) queryParams.append('city', params.city);
|
||||||
if (params?.visibility) queryParams.append('visibility', params.visibility);
|
if (params?.visibility) queryParams.append('visibility', params.visibility);
|
||||||
|
const response = await api.get<ApiResp<Team[]>>(`/v1/hackathon/teams/browse?${queryParams}`);
|
||||||
const response = await hackathonApi.get<HackathonApiResponse<Team[]>>(
|
|
||||||
`/teams/browse?${queryParams.toString()}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const teams = response.data.data || [];
|
const teams = response.data.data || [];
|
||||||
return {
|
return { data: teams, nextPage: teams.length === TEAMS_PAGE_SIZE ? pageParam + 1 : undefined };
|
||||||
data: teams,
|
|
||||||
nextPage: teams.length === TEAMS_PAGE_SIZE ? pageParam + 1 : undefined,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
initialPageParam: 1,
|
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({
|
return useQuery({
|
||||||
queryKey: teamKeys.detail(teamId),
|
queryKey: teamKeys.detail(teamId),
|
||||||
queryFn: async () => {
|
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 };
|
return { data: response.data.data };
|
||||||
},
|
},
|
||||||
enabled: enabled && !!teamId,
|
enabled: enabled && !!teamId,
|
||||||
@@ -200,22 +117,10 @@ export const useTeamById = (teamId: string, enabled = true) => {
|
|||||||
export const useCreateTeam = () => {
|
export const useCreateTeam = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (data: TCreateTeamRequest) => {
|
mutationFn: async (data: TCreateTeamRequest) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to create a team');
|
||||||
throw new Error('You must be logged in to create a team');
|
const response = await api.post<ApiResp<Team>>('/v1/hackathon/teams', data);
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { data: response.data.data };
|
return { data: response.data.data };
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -228,22 +133,10 @@ export const useCreateTeam = () => {
|
|||||||
export const useUpdateTeam = (teamId: string) => {
|
export const useUpdateTeam = (teamId: string) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (data: TUpdateTeamRequest) => {
|
mutationFn: async (data: TUpdateTeamRequest) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to update a team');
|
||||||
throw new Error('You must be logged in to update a team');
|
const response = await api.put<ApiResp<Team>>(`/v1/hackathon/teams/${teamId}`, data);
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { data: response.data.data };
|
return { data: response.data.data };
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -257,7 +150,7 @@ export const useTeamMembers = (teamId: string, enabled = true) => {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: teamKeys.members(teamId),
|
queryKey: teamKeys.members(teamId),
|
||||||
queryFn: async () => {
|
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 || [] };
|
return { data: response.data.data?.members || [] };
|
||||||
},
|
},
|
||||||
enabled: enabled && !!teamId,
|
enabled: enabled && !!teamId,
|
||||||
@@ -267,32 +160,26 @@ export const useTeamMembers = (teamId: string, enabled = true) => {
|
|||||||
export const useInviteMember = (teamId: string) => {
|
export const useInviteMember = (teamId: string) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (data: TInviteMemberRequest) => {
|
mutationFn: async (data: TInviteMemberRequest) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to invite a member');
|
||||||
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`,
|
||||||
|
|
||||||
const response = await hackathonApi.post<HackathonApiResponse<Invitation>>(
|
|
||||||
`/teams/${teamId}/invite`,
|
|
||||||
{ invitee_email: data.email }
|
{ invitee_email: data.email }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data: response.data.data };
|
return { data: response.data.data };
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) }),
|
||||||
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useManageMember = (teamId: string) => {
|
export const useManageMember = (teamId: string) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ userId, data }: { userId: string; data: { role?: string; status?: string } }) => {
|
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: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
|
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
|
||||||
@@ -304,14 +191,10 @@ export const useManageMember = (teamId: string) => {
|
|||||||
export const useRemoveMember = (teamId: string) => {
|
export const useRemoveMember = (teamId: string) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (userId: string) => {
|
mutationFn: async (userId: string) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to remove a member');
|
||||||
throw new Error('You must be logged in to remove a member');
|
await api.delete(`/v1/hackathon/teams/${teamId}/members/${userId}`);
|
||||||
}
|
|
||||||
|
|
||||||
await hackathonApi.delete(`/teams/${teamId}/members/${userId}`);
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -324,23 +207,16 @@ export const useRemoveMember = (teamId: string) => {
|
|||||||
export const useJoinTeam = () => {
|
export const useJoinTeam = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ teamId, data }: { teamId: string; data: TJoinTeamRequest }) => {
|
mutationFn: async ({ teamId, data }: { teamId: string; data: TJoinTeamRequest }) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to join a team');
|
||||||
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}`,
|
||||||
|
|
||||||
const response = await hackathonApi.post<HackathonApiResponse<JoinRequest>>(
|
|
||||||
`/join-requests/teams/${teamId}`,
|
|
||||||
{ message: data.message }
|
{ message: data.message }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data: response.data.data };
|
return { data: response.data.data };
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: teamKeys.lists() }),
|
||||||
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -348,8 +224,8 @@ export const useTeamJoinRequests = (teamId: string, enabled = true) => {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: teamKeys.joinRequests(teamId),
|
queryKey: teamKeys.joinRequests(teamId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await hackathonApi.get<HackathonApiResponse<JoinRequest[]>>(
|
const response = await api.get<ApiResp<JoinRequest[]>>(
|
||||||
`/join-requests/teams/${teamId}/pending`
|
`/v1/hackathon/join-requests/teams/${teamId}/pending`
|
||||||
);
|
);
|
||||||
return { data: response.data.data || [] };
|
return { data: response.data.data || [] };
|
||||||
},
|
},
|
||||||
@@ -360,19 +236,11 @@ export const useTeamJoinRequests = (teamId: string, enabled = true) => {
|
|||||||
export const useRespondToJoinRequest = (teamId: string) => {
|
export const useRespondToJoinRequest = (teamId: string) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ requestId, action }: { requestId: string; action: 'approve' | 'reject' }) => {
|
mutationFn: async ({ requestId, action }: { requestId: string; action: 'approve' | 'reject' }) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to respond to join requests');
|
||||||
throw new Error('You must be logged in to respond to join requests');
|
|
||||||
}
|
|
||||||
|
|
||||||
const backendAction = action === 'approve' ? 'accept' : 'reject';
|
const backendAction = action === 'approve' ? 'accept' : 'reject';
|
||||||
|
await api.post(`/v1/hackathon/join-requests/${requestId}/respond`, { action: backendAction });
|
||||||
await hackathonApi.post(`/join-requests/${requestId}/respond`, {
|
|
||||||
action: backendAction,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, action };
|
return { success: true, action };
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -385,11 +253,10 @@ export const useRespondToJoinRequest = (teamId: string) => {
|
|||||||
|
|
||||||
export const useMyInvitations = () => {
|
export const useMyInvitations = () => {
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: teamKeys.myInvitations(),
|
queryKey: teamKeys.myInvitations(),
|
||||||
queryFn: async () => {
|
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 || [] };
|
return { data: response.data.data || [] };
|
||||||
},
|
},
|
||||||
enabled: !!session?.user?.id,
|
enabled: !!session?.user?.id,
|
||||||
@@ -399,15 +266,10 @@ export const useMyInvitations = () => {
|
|||||||
export const useRespondToInvitation = () => {
|
export const useRespondToInvitation = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ invitationId, action }: { invitationId: string; action: 'accept' | 'reject' }) => {
|
mutationFn: async ({ invitationId, action }: { invitationId: string; action: 'accept' | 'reject' }) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('User not authenticated');
|
||||||
throw new Error('User not authenticated');
|
await api.post(`/v1/hackathon/invitations/${invitationId}/respond`, { action });
|
||||||
}
|
|
||||||
|
|
||||||
await hackathonApi.post(`/invitations/${invitationId}/respond`, { action });
|
|
||||||
|
|
||||||
return { success: true, action };
|
return { success: true, action };
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -420,13 +282,12 @@ export const useRespondToInvitation = () => {
|
|||||||
|
|
||||||
export const useMyTeams = () => {
|
export const useMyTeams = () => {
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: teamKeys.myTeams(),
|
queryKey: teamKeys.myTeams(),
|
||||||
queryFn: async () => {
|
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 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 };
|
return { data: teams };
|
||||||
},
|
},
|
||||||
enabled: !!session?.user?.id,
|
enabled: !!session?.user?.id,
|
||||||
@@ -435,58 +296,22 @@ export const useMyTeams = () => {
|
|||||||
|
|
||||||
export const useSubmitProject = (teamId: string) => {
|
export const useSubmitProject = (teamId: string) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (data: TSubmitProjectRequest) => {
|
mutationFn: async (data: TSubmitProjectRequest) => {
|
||||||
let submissionId: string;
|
let submissionId: string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existingResponse = await hackathonApi.get<HackathonApiResponse<Submission | null>>(
|
const existing = await api.get<ApiResp<Submission | null>>(`/v1/hackathon/submissions/teams/${teamId}`);
|
||||||
`/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;
|
||||||
if (existingResponse.data.data?.id) {
|
} else throw new Error('No existing submission');
|
||||||
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');
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
const response = await hackathonApi.post<HackathonApiResponse<Submission>>(
|
const res = await api.post<ApiResp<Submission>>(`/v1/hackathon/submissions/teams/${teamId}`, data);
|
||||||
`/submissions/teams/${teamId}`,
|
submissionId = res.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;
|
|
||||||
}
|
}
|
||||||
|
await api.post<ApiResp<Submission>>(`/v1/hackathon/submissions/${submissionId}/submit`);
|
||||||
await hackathonApi.post<HackathonApiResponse<Submission>>(
|
const final = await api.post<ApiResp<Submission>>(`/v1/hackathon/submissions/${submissionId}/confirm`);
|
||||||
`/submissions/${submissionId}/submit`
|
return { data: final.data.data };
|
||||||
);
|
|
||||||
|
|
||||||
const finalResponse = await hackathonApi.post<HackathonApiResponse<Submission>>(
|
|
||||||
`/submissions/${submissionId}/confirm`
|
|
||||||
);
|
|
||||||
|
|
||||||
return { data: finalResponse.data.data };
|
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: teamKeys.submission(teamId) });
|
queryClient.invalidateQueries({ queryKey: teamKeys.submission(teamId) });
|
||||||
@@ -499,9 +324,7 @@ export const useTeamSubmission = (teamId: string, enabled = true) => {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: teamKeys.submission(teamId),
|
queryKey: teamKeys.submission(teamId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await hackathonApi.get<HackathonApiResponse<Submission | null>>(
|
const response = await api.get<ApiResp<Submission | null>>(`/v1/hackathon/submissions/teams/${teamId}`);
|
||||||
`/submissions/teams/${teamId}`
|
|
||||||
);
|
|
||||||
return { data: response.data.data };
|
return { data: response.data.data };
|
||||||
},
|
},
|
||||||
enabled: enabled && !!teamId,
|
enabled: enabled && !!teamId,
|
||||||
@@ -511,14 +334,10 @@ export const useTeamSubmission = (teamId: string, enabled = true) => {
|
|||||||
export const useLeaveTeam = () => {
|
export const useLeaveTeam = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (teamId: string) => {
|
mutationFn: async (teamId: string) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to leave a team');
|
||||||
throw new Error('You must be logged in to leave a team');
|
await api.post(`/v1/hackathon/teams/${teamId}/leave`);
|
||||||
}
|
|
||||||
|
|
||||||
await hackathonApi.post(`/teams/${teamId}/leave`);
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -531,14 +350,10 @@ export const useLeaveTeam = () => {
|
|||||||
export const useDeleteTeam = () => {
|
export const useDeleteTeam = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (teamId: string) => {
|
mutationFn: async (teamId: string) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to delete a team');
|
||||||
throw new Error('You must be logged in to delete a team');
|
await api.delete(`/v1/hackathon/teams/${teamId}`);
|
||||||
}
|
|
||||||
|
|
||||||
await hackathonApi.delete(`/teams/${teamId}`);
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -552,9 +367,9 @@ export const useTeamsByUserId = (userId: string) => {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['teams-by-user', userId],
|
queryKey: ['teams-by-user', userId],
|
||||||
queryFn: async () => {
|
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 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 };
|
return { data: teams };
|
||||||
},
|
},
|
||||||
enabled: !!userId,
|
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 { useMutation } from '@tanstack/react-query';
|
||||||
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
|
|
||||||
import { useAuthStore } from '../auth';
|
import { useAuthStore } from '../auth';
|
||||||
|
import {
|
||||||
interface UploadResponse {
|
uploadHackathonAvatar,
|
||||||
url: string;
|
uploadHackathonTeamFile,
|
||||||
}
|
uploadHackathonSubmission,
|
||||||
|
uploadHackathonFile,
|
||||||
const fileToBase64 = (file: File): Promise<string> => {
|
uploadUserFile,
|
||||||
return new Promise((resolve, reject) => {
|
} from '../../api/upload';
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
reader.onload = () => {
|
|
||||||
const base64 = (reader.result as string).split(',')[1];
|
|
||||||
resolve(base64);
|
|
||||||
};
|
|
||||||
reader.onerror = (error) => reject(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUploadFile = () => {
|
export const useUploadFile = () => {
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['upload-file'],
|
mutationKey: ['upload-file'],
|
||||||
mutationFn: async (file: File) => {
|
mutationFn: async (file: File) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to upload files');
|
||||||
throw new Error('You must be logged in to upload files');
|
const data = await uploadHackathonTeamFile(file);
|
||||||
}
|
return { data };
|
||||||
|
|
||||||
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 } };
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUploadAvatar = () => {
|
export const useUploadAvatar = () => {
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['upload-avatar'],
|
mutationKey: ['upload-avatar'],
|
||||||
mutationFn: async (file: File) => {
|
mutationFn: async (file: File) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to upload avatar');
|
||||||
throw new Error('You must be logged in to upload avatar');
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
|
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
|
||||||
if (!allowedTypes.includes(file.type)) {
|
if (!allowedTypes.includes(file.type)) throw new Error('Invalid file type. Allowed: JPEG, PNG, WebP, GIF');
|
||||||
throw new Error('Invalid file type. Allowed types: 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 };
|
||||||
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 } };
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUploadTeamFile = () => {
|
export const useUploadTeamFile = () => {
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['upload-team-file'],
|
mutationKey: ['upload-team-file'],
|
||||||
mutationFn: async (file: File) => {
|
mutationFn: async (file: File) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to upload files');
|
||||||
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 allowedTypes = [
|
const data = await uploadHackathonTeamFile(file);
|
||||||
'image/jpeg',
|
return { data };
|
||||||
'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 } };
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUploadSubmission = () => {
|
export const useUploadSubmission = () => {
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['upload-submission'],
|
mutationKey: ['upload-submission'],
|
||||||
mutationFn: async (file: File) => {
|
mutationFn: async (file: File) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to upload submissions');
|
||||||
throw new Error('You must be logged in to upload submissions');
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowedTypes = [
|
const allowedTypes = [
|
||||||
'image/jpeg',
|
'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif',
|
||||||
'image/jpg',
|
'application/pdf', 'application/zip', 'application/x-zip-compressed',
|
||||||
'image/png',
|
'video/mp4', 'video/webm',
|
||||||
'image/webp',
|
|
||||||
'image/gif',
|
|
||||||
'application/pdf',
|
|
||||||
'application/zip',
|
|
||||||
'application/x-zip-compressed',
|
|
||||||
'video/mp4',
|
|
||||||
'video/webm',
|
|
||||||
];
|
];
|
||||||
if (!allowedTypes.includes(file.type)) {
|
if (!allowedTypes.includes(file.type)) throw new Error('Invalid file type. Allowed: Images, PDF, ZIP, MP4, WebM');
|
||||||
throw new Error(
|
if (file.size > 50 * 1024 * 1024) throw new Error('File too large. Maximum size: 50MB');
|
||||||
'Invalid file type. Allowed types: Images, PDF, ZIP, MP4, WebM'
|
const data = await uploadHackathonSubmission(file);
|
||||||
);
|
return { data };
|
||||||
}
|
|
||||||
|
|
||||||
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 } };
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUploadCV = () => {
|
export const useUploadCV = () => {
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['upload-cv'],
|
mutationKey: ['upload-cv'],
|
||||||
mutationFn: async (file: File) => {
|
mutationFn: async (file: File) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to upload CV');
|
||||||
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);
|
||||||
if (file.type !== 'application/pdf') {
|
return { data };
|
||||||
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 } };
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,69 +1,30 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
|
|
||||||
import { useAuthStore } from '../auth';
|
import { useAuthStore } from '../auth';
|
||||||
|
import {
|
||||||
interface User {
|
getUserMe,
|
||||||
id: string;
|
getUserById,
|
||||||
email: string;
|
getUserList,
|
||||||
fullname: string;
|
updateUserMe,
|
||||||
bio?: string;
|
updateUserById,
|
||||||
location?: string;
|
} from '../../api/users';
|
||||||
avatar?: string;
|
import { api } from '../../api/index';
|
||||||
skills?: string[];
|
import type { TApiPaginated, TPaginationParams } from '../../types/common';
|
||||||
created_at: string;
|
import type { TUsersDetailItem, TUserUpdateRequest } from '../../types/users';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CertificatePublicData {
|
export interface CertificatePublicData {
|
||||||
user: CertificateUserData;
|
user: { id: string; fullname: string; email: string; avatar?: string };
|
||||||
team?: CertificateTeamData;
|
team?: { id: string; name: string; logo?: string; is_leader: boolean };
|
||||||
submission?: CertificateSubmissionData;
|
submission?: { id: string; title: string; description: string; repository_url?: string; demo_url?: string };
|
||||||
winner?: CertificateWinnerData;
|
winner?: { rank: number; prize?: string };
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateUserRequest {
|
|
||||||
fullname?: string;
|
|
||||||
bio?: string;
|
|
||||||
location?: string;
|
|
||||||
avatar?: string;
|
|
||||||
skills?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUserMe = () => {
|
export const useUserMe = () => {
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['user-me'],
|
queryKey: ['user-me'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await hackathonApi.get<HackathonApiResponse<User>>('/users/me');
|
const data = await getUserMe();
|
||||||
return { data: response.data.data };
|
return { data };
|
||||||
},
|
},
|
||||||
enabled: !!session?.user?.id,
|
enabled: !!session?.user?.id,
|
||||||
});
|
});
|
||||||
@@ -73,45 +34,52 @@ export const useUserById = (id: string) => {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['user-by-id', id],
|
queryKey: ['user-by-id', id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await hackathonApi.get<HackathonApiResponse<User>>(`/users/${id}`);
|
const data = await getUserById(id);
|
||||||
return { data: response.data.data };
|
return { data };
|
||||||
},
|
},
|
||||||
enabled: !!id,
|
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 = () => {
|
export const useUpdateUserMe = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { session, setSession } = useAuthStore();
|
const { session, setSession } = useAuthStore();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['update-user-me'],
|
mutationKey: ['update-user-me'],
|
||||||
mutationFn: async (data: UpdateUserRequest) => {
|
mutationFn: (data: TUserUpdateRequest) => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) throw new Error('You must be logged in to update profile');
|
||||||
throw new Error('You must be logged in to update profile');
|
return updateUserMe(data);
|
||||||
}
|
|
||||||
|
|
||||||
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 };
|
|
||||||
},
|
},
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
if (session?.user && result.data) {
|
if (session?.user && result) {
|
||||||
setSession({
|
setSession({
|
||||||
token: session.token,
|
token: session.token,
|
||||||
user: {
|
user: {
|
||||||
...session.user,
|
...session.user,
|
||||||
fullname: result.data.fullname || session.user.fullname,
|
fullname: result.fullname || session.user.fullname,
|
||||||
bio: result.data.bio || '',
|
bio: result.profile_extension?.bio || '',
|
||||||
location: result.data.location || '',
|
location: result.profile_extension?.location || '',
|
||||||
avatar: result.data.avatar || session.user.avatar,
|
avatar: result.avatar || session.user.avatar,
|
||||||
skills: result.data.skills || [],
|
skills: result.profile_extension?.skills || [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -125,33 +93,20 @@ export const useUpdateUserById = () => {
|
|||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['update-user-by-id'],
|
mutationKey: ['update-user-by-id'],
|
||||||
mutationFn: async ({ id, data }: { id: string; data: UpdateUserRequest }) => {
|
mutationFn: ({ id, data }: { id: string; data: TUserUpdateRequest }) => updateUserById(id, data),
|
||||||
const response = await hackathonApi.put<HackathonApiResponse<User>>(`/users/${id}`, data);
|
|
||||||
return { data: response.data.data };
|
|
||||||
},
|
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['user-by-id', variables.id] });
|
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) => {
|
export const useCertificatePublicData = (userId: string, enabled = true) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['certificate-public-data', userId],
|
queryKey: ['certificate-public-data', userId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await hackathonApi.get<HackathonApiResponse<CertificatePublicData>>(
|
const response = await api.get<{ data: CertificatePublicData }>(
|
||||||
`/certificates/${userId}`
|
`/v1/hackathon/certificates/${userId}`
|
||||||
);
|
);
|
||||||
return { data: response.data.data };
|
return { data: response.data.data };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
|
import { api } from '../../api/index';
|
||||||
|
|
||||||
export const winnerKeys = {
|
export const winnerKeys = {
|
||||||
all: ['winners'] as const,
|
all: ['winners'] as const,
|
||||||
@@ -7,34 +7,19 @@ export const winnerKeys = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface Team {
|
interface Team {
|
||||||
id: string;
|
id: string; name: string; description: string; city: string; visibility: string;
|
||||||
name: string;
|
logo: string; banner: string; leader_id: string; created_at: string; updated_at: string;
|
||||||
description: string;
|
|
||||||
city: string;
|
|
||||||
visibility: string;
|
|
||||||
logo: string;
|
|
||||||
banner: string;
|
|
||||||
leader_id: string;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Winner {
|
interface Winner {
|
||||||
id: string;
|
id: string; team_id: string; team: Team; rank: number; prize: string;
|
||||||
team_id: string;
|
announced_at: string; created_at: string; updated_at: string;
|
||||||
team: Team;
|
|
||||||
rank: number;
|
|
||||||
prize: string;
|
|
||||||
announced_at: string;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWinners = () => {
|
export const useWinners = () => {
|
||||||
return useQuery<HackathonApiResponse<Winner[]>>({
|
return useQuery({
|
||||||
queryKey: winnerKeys.lists(),
|
queryKey: winnerKeys.lists(),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await hackathonApi.get('/winners');
|
const response = await api.get<{ data: Winner[] }>('/v1/hackathon/winners');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,43 @@
|
|||||||
import { AxiosError } from 'axios';
|
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 = {
|
export type TMetaResponse = {
|
||||||
page: number;
|
page: number;
|
||||||
per_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 = {
|
export type TGachaItemDto = {
|
||||||
itemName: string;
|
id: string;
|
||||||
quantity: number;
|
name: string;
|
||||||
foto?: File;
|
is_deleted: boolean;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGachaRollItem = {
|
export type TGachaItemCreateRequest = {
|
||||||
itemName: string;
|
item_code: string;
|
||||||
quantity: number;
|
name: string;
|
||||||
chanceRate: number;
|
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 './mentors';
|
||||||
export * from './teams';
|
export * from './teams';
|
||||||
export * from './admin';
|
export * from './admin';
|
||||||
|
export * from './events';
|
||||||
|
export * from './testimonials';
|
||||||
|
export * from './sessions';
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
export type TPermissionItem = {
|
export type TPermissionItem = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
created_at: string;
|
created_at?: string;
|
||||||
updated_at: string;
|
updated_at?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPermissionCreateRequest = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPermissionUpdateRequest = {
|
||||||
|
name?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
import { TPermissionItem } from '../permissions';
|
import { TPermissionItem } from '../permissions';
|
||||||
|
|
||||||
export type TRoleDetailItem = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
permissions: TPermissionItem[];
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TRolesListItem = {
|
export type TRolesListItem = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
permissions_count: number;
|
permissions_count: number;
|
||||||
created_at: string;
|
created_at?: string;
|
||||||
updated_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';
|
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 = {
|
export type TUserItem = {
|
||||||
id: string;
|
id: string;
|
||||||
avatar: string;
|
avatar?: string;
|
||||||
birthdate: string;
|
birthdate?: string;
|
||||||
email: string;
|
email: string;
|
||||||
fullname: string;
|
fullname: string;
|
||||||
gender: string;
|
gender?: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
phone_number: string;
|
phone_number?: string;
|
||||||
role: TRoleDetailItem;
|
role: TRoleDetailItem;
|
||||||
location?: string;
|
location?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
skills?: 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