diff --git a/package.json b/package.json index 6c042bf..f967eda 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.85.5", "@tanstack/react-table": "^8.21.3", "@tiptap/extension-image": "^3.2.0", "@tiptap/extension-link": "^3.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3dba909..607887a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: "@radix-ui/react-tooltip": specifier: ^1.2.7 version: 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + "@tanstack/react-query": + specifier: ^5.85.5 + version: 5.85.5(react@19.1.0) "@tanstack/react-table": specifier: ^8.21.3 version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -1668,6 +1671,16 @@ packages: peerDependencies: tailwindcss: ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + "@tanstack/query-core@5.85.5": + resolution: + { integrity: sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w== } + + "@tanstack/react-query@5.85.5": + resolution: + { integrity: sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A== } + peerDependencies: + react: ^18 || ^19 + "@tanstack/react-table@8.21.3": resolution: { integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww== } @@ -6783,6 +6796,13 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.1.11 + "@tanstack/query-core@5.85.5": {} + + "@tanstack/react-query@5.85.5(react@19.1.0)": + dependencies: + "@tanstack/query-core": 5.85.5 + react: 19.1.0 + "@tanstack/react-table@8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)": dependencies: "@tanstack/table-core": 8.21.3 diff --git a/src/app/[locale]/(public)/(user)/layout.tsx b/src/app/[locale]/(public)/(user)/layout.tsx index 12fad47..013a3a2 100644 --- a/src/app/[locale]/(public)/(user)/layout.tsx +++ b/src/app/[locale]/(public)/(user)/layout.tsx @@ -19,7 +19,7 @@ export default function UserLayout({ children }: UserLayoutProps) {
-
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 89744df..3cf94a8 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -9,6 +9,7 @@ import { cookies } from "next/headers"; import { AuthJwtPayload } from "@/types"; import { jwtDecode } from "jwt-decode"; import { ThemeProvider } from "@/components/provider/ThemeProvider"; +import TanstackProvider from "@/components/provider/TanstackProvider"; const sora = Sora({ subsets: ["latin"] }); type Props = { @@ -57,11 +58,13 @@ export default async function LocaleRootLayout(props: Readonly) { - - - {children} - - + + + + {children} + + + diff --git a/src/components/common/Loader/index.tsx b/src/components/common/Loader/index.tsx new file mode 100644 index 0000000..5070489 --- /dev/null +++ b/src/components/common/Loader/index.tsx @@ -0,0 +1,10 @@ +import { LoaderIcon } from "lucide-react"; + +const Loader = () => { + return ( +
+ +
+ ); +}; +export default Loader; diff --git a/src/components/common/NotFoundData/index.tsx b/src/components/common/NotFoundData/index.tsx new file mode 100644 index 0000000..9651335 --- /dev/null +++ b/src/components/common/NotFoundData/index.tsx @@ -0,0 +1,10 @@ +import { FileX } from "lucide-react"; + +export const NotFoundData = ({ message = "No data found" }: { message?: string }) => { + return ( +
+ +

{message}

+
+ ); +}; diff --git a/src/components/provider/TanstackProvider/index.tsx b/src/components/provider/TanstackProvider/index.tsx new file mode 100644 index 0000000..bacb6be --- /dev/null +++ b/src/components/provider/TanstackProvider/index.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient(); + +const TanstackProvider = ({ children }: { children: React.ReactNode }) => { + return {children}; +}; +export default TanstackProvider; diff --git a/src/components/ui/Badge/index.tsx b/src/components/ui/Badge/index.tsx index a244a7b..8e6f2f0 100644 --- a/src/components/ui/Badge/index.tsx +++ b/src/components/ui/Badge/index.tsx @@ -15,6 +15,10 @@ const badgeVariants = cva( soon: "border-transparent bg-green-500 text-white hover:bg-green-600", closed: "border-transparent bg-red-500 text-white hover:bg-red-600", open: "border-transparent bg-blue-500 text-white hover:bg-blue-600", + // for payment status + PENDING: "border-transparent bg-yellow-500 text-white hover:bg-yellow-600", + SUCCESS: "border-transparent bg-green-500 text-white hover:bg-green-600", + FAILED: "border-transparent bg-red-500 text-white hover:bg-red-600", }, }, defaultVariants: { diff --git a/src/components/ui/Toaster/index.tsx b/src/components/ui/Toaster/index.tsx index 1db3bad..89a2aaa 100644 --- a/src/components/ui/Toaster/index.tsx +++ b/src/components/ui/Toaster/index.tsx @@ -19,6 +19,7 @@ const Toaster = ({ ...props }: ToasterProps) => { } toastOptions={{ classNames: { + success: "!bg-green-500 !text-white", error: "!bg-destructive !text-white", description: "!text-muted-foreground", }, diff --git a/src/constants/event.ts b/src/constants/event.ts new file mode 100644 index 0000000..31717ea --- /dev/null +++ b/src/constants/event.ts @@ -0,0 +1 @@ +export const EVENTS_TYPE = ["Conference", "Tech Talk", "Ngobar"]; diff --git a/src/domains/Events.ts b/src/domains/Events.ts index bd49130..3107022 100644 --- a/src/domains/Events.ts +++ b/src/domains/Events.ts @@ -4,6 +4,7 @@ export const eventSchema = z.object({ id: z.number(), title: z.string(), description: z.string(), + slug: z.string().optional(), author: z.string(), image: z.string(), date: z.string().optional(), @@ -11,8 +12,8 @@ export const eventSchema = z.object({ location: z.string(), duration: z.string(), capacity: z.number(), - status: z.enum(["open", "soon", "closed"]), - Tags: z + status: z.enum(["open", "soon", "closed", "comming soon"]), + tags: z .array( z.object({ id: z.number(), @@ -21,7 +22,7 @@ export const eventSchema = z.object({ }) ) .optional(), - Speakers: z + speakers: z .array( z.object({ id: z.number(), @@ -32,14 +33,12 @@ export const eventSchema = z.object({ .optional(), registration_link: z.string(), price: z.number(), - created_by: z.number(), - updated_by: z.number(), - deleted_by: z.number(), reservation_start_date: z.string().optional(), - reseveration_end_date: z.string().optional(), + reservation_end_date: z.string().optional(), created_at: z.string(), updated_at: z.string().optional(), deleted_at: z.string().optional(), + additional_link: z.string().optional(), }); export type EventType = z.infer; @@ -61,3 +60,20 @@ export const registrationSchema = z.object({ }); export type RegistrationForm = z.infer; + +export const userEventSchema = z.object({ + id: z.number(), + order_no: z.string(), + event_id: z.number(), + user_id: z.string(), + name: z.string(), + email: z.string(), + phone_number: z.string(), + image_proof_payment: z.string().url(), + payment_date: z.string(), + status: z.enum(["PENDING", "SUCCESS", "FAILED"]), + created_at: z.string(), + event_detail: eventSchema, +}); + +export type UserEventType = z.infer; diff --git a/src/features/events/components/MyEventCard.tsx b/src/features/events/components/MyEventCard.tsx index e247c6a..d0182e9 100644 --- a/src/features/events/components/MyEventCard.tsx +++ b/src/features/events/components/MyEventCard.tsx @@ -1,56 +1,54 @@ -import { FC } from "react"; import Image from "next/image"; import { Clock, Pin } from "lucide-react"; import Badge from "@/components/ui/Badge"; import { Card, CardContent, CardFooter } from "@/components/ui/Card"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/Tooltip"; -import { EventType } from "@/domains/Events"; -import { useFormatDate } from "@/lib/format"; -import { Link } from "@/lib/navigation"; +import { UserEventType } from "@/domains/Events"; +import { useFormatDateEvent } from "@/lib/format"; -const MyEventCard: FC<{ data: EventType }> = ({ data }) => { - const { id, title, date, image, status, duration, location } = data; +const MyEventCard = ({ data }: { data: UserEventType }) => { + const { order_no, status } = data; return ( - +
-
+
+
{String(id)}
- - {status} - - - - -

{title}

-
- {title} -
-
+
+ #{order_no} + {status} +
+ +

{data.event_detail.title}

-

{useFormatDate(date)}

- -

{duration}

+ +

{useFormatDateEvent(data.event_detail.date as string) || "-"}

- -

{location}

+ +

{data.event_detail.location || "-"}

- +
); }; + export default MyEventCard; diff --git a/src/features/events/hooks/useMyEvent.ts b/src/features/events/hooks/useMyEvent.ts index b81e095..89bcc67 100644 --- a/src/features/events/hooks/useMyEvent.ts +++ b/src/features/events/hooks/useMyEvent.ts @@ -1,27 +1,21 @@ -import { EventType } from "@/domains/Events"; import { eventsService } from "@/services/events"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; +import { useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; -export const useMyEvents = (page: number = 1, limit: number = 10) => { - const [myEvents, setMyEvents] = useState([]); - const [isLoading, setIsLoading] = useState(false); +export const useMyEvents = (page: number = 1, limit: number = 10, type: string) => { + const typeMemo = useMemo(() => { + return type === "all" ? "" : type; + }, [type]); - const getEvents = async () => { - setIsLoading(true); - try { - const res = await eventsService.getMyEvents(page, limit); - setMyEvents(res.data); - } catch (err) { - toast.error(err instanceof Error ? err.message : "Something went wrong."); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - getEvents(); - }, [toast]); + const { data, isLoading, error } = useQuery({ + queryKey: ["getListMyEvents", page, limit, type], + queryFn: async () => eventsService.getMyEvents(page, limit, typeMemo), + }); - return { myEvents, isLoading }; + return { + myEvents: data?.data || [], + paginationMyEvents: data?.pagination, + isLoading, + error, + }; }; diff --git a/src/features/events/pages/UserEventPage.tsx b/src/features/events/pages/UserEventPage.tsx index e7bca35..d2ec334 100644 --- a/src/features/events/pages/UserEventPage.tsx +++ b/src/features/events/pages/UserEventPage.tsx @@ -1,13 +1,16 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; import { useTranslations } from "next-intl"; import { motion } from "motion/react"; -import { LoaderIcon } from "lucide-react"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/Select"; import { PaginationCustom } from "@/components/common/PaginationCustom"; import { useMyEvents } from "../hooks/useMyEvent"; import MyEventCard from "../components/MyEventCard"; +import { EVENTS_TYPE } from "@/constants/event"; +import Loader from "@/components/common/Loader"; +import { UserEventType } from "@/domains/Events"; +import { NotFoundData } from "@/components/common/NotFoundData"; interface UserEventPageProps { page?: number; @@ -16,22 +19,16 @@ interface UserEventPageProps { const UserEventPage = ({ page = 1, perPage = 10 }: UserEventPageProps) => { const t = useTranslations("MyEventPage"); - const { myEvents, isLoading } = useMyEvents(page, perPage); + const [typeActive, setTypeActive] = useState("all"); + const { myEvents, isLoading } = useMyEvents(page, perPage, typeActive); const totalEvents = myEvents.length; const totalPages = Math.ceil(totalEvents / perPage); const startIndex = (page - 1) * perPage; const endIndex = startIndex + perPage; - const events = myEvents.slice(startIndex, endIndex); + const events: UserEventType[] = myEvents.slice(startIndex, endIndex); - if (isLoading) { - return ( -
- -
- ); - } return (
@@ -60,63 +57,71 @@ const UserEventPage = ({ page = 1, perPage = 10 }: UserEventPageProps) => { transition={{ duration: 0.4, delay: 0.7 }} className="w-full sm:w-auto" > - - + - Workshop - Tech Talk - Learning - Ngobar + All Type Event + {EVENTS_TYPE.map((item) => ( + + {item} + + ))}
- + ) : events.length === 0 ? ( + + ) : ( + -
- {events.map((event) => ( - +
+ {events.map((event: UserEventType) => ( + - {event && } - - ))} -
- -
+ }} + whileHover={{ scale: 1.01 }} + > + {event && } + + ))} +
+ +
+ )}
); }; diff --git a/src/lib/format.ts b/src/lib/format.ts index 348273c..d964ad8 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -21,3 +21,15 @@ export function useFormatPrice(price?: number) { minimumFractionDigits: 0, }).format(price as number); } + +export const useFormatDateEvent = (dateString: string): string => { + const date = new Date(dateString); + return new Intl.DateTimeFormat("id-ID", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(date); +}; diff --git a/src/services/events/index.ts b/src/services/events/index.ts index 0127e58..9f6f2aa 100644 --- a/src/services/events/index.ts +++ b/src/services/events/index.ts @@ -1,6 +1,6 @@ import { HttpResponse } from "@/types/http"; import { fetcher } from "../instance"; -import { EventType, RegistrationForm } from "@/domains/Events"; +import { EventType, RegistrationForm, UserEventType } from "@/domains/Events"; export const eventsService = { /** @@ -27,7 +27,13 @@ export const eventsService = { /** * API to retrieve the list of events owned by the current user. */ - getMyEvents(page: number = 1, limit: number = 10): Promise> { - return fetcher.get(`/events/registrations?page=${page}&limit=${limit}`); + async getMyEvents(page: number = 1, limit: number = 10, type?: string): Promise> { + return fetcher.get(`/events/registrations`, { + params: { + page, + limit, + type, + }, + }); }, }; diff --git a/src/types/http.ts b/src/types/http.ts index 75b1b11..1b6dd59 100644 --- a/src/types/http.ts +++ b/src/types/http.ts @@ -5,8 +5,9 @@ interface PaginatedResponse { total_pages?: number; } -export interface HttpResponse extends PaginatedResponse { +export interface HttpResponse { code: number; message: string; data: T; + pagination?: PaginatedResponse; }