From 583b6b224ec3a51ba69d54b62512d91a3994d8df Mon Sep 17 00:00:00 2001 From: asepharyana Date: Tue, 4 Aug 2026 19:31:29 +0700 Subject: [PATCH] feat(dimentorin): articles page integrated with API (list, filter, detail) --- .../src/app/(public)/articles/[slug]/page.tsx | 149 ++++++++++-------- .../articles/_components/card/article.tsx | 40 +++-- .../src/app/(public)/articles/page.tsx | 88 +++++++---- .../_components/modals/appointment/index.tsx | 34 +++- .../modals/appointment/steps/payment.tsx | 23 ++- .../modals/appointment/steps/schedule.tsx | 36 ++++- .../_components/sections/senpai-schedule.tsx | 117 +++++++------- .../src/app/(public)/mentoring/[id]/page.tsx | 4 +- libs/service/src/api/dimentorin/index.ts | 38 +++++ libs/service/src/hooks/dimentorin/index.ts | 46 ++++++ libs/service/src/types/dimentorin/index.ts | 23 +++ 11 files changed, 412 insertions(+), 186 deletions(-) diff --git a/apps/dimentorin/src/app/(public)/articles/[slug]/page.tsx b/apps/dimentorin/src/app/(public)/articles/[slug]/page.tsx index 6dda909..078fb94 100644 --- a/apps/dimentorin/src/app/(public)/articles/[slug]/page.tsx +++ b/apps/dimentorin/src/app/(public)/articles/[slug]/page.tsx @@ -1,9 +1,19 @@ import { Icon } from "@iconify/react"; import { Button } from "@imphnen-frontend-service/ui/atoms"; -import { cn } from "@imphnen-frontend-service/utils"; +import { cn, For, Show } from "@imphnen-frontend-service/utils"; import { motion } from "framer-motion"; +import { useParams } from "react-router-dom"; +import { useGetArticleBySlug } from "@imphnen-frontend-service/service"; export default function DetailArticle() { + const { slug } = useParams() + const { data: article, isLoading, isError } = useGetArticleBySlug(slug ?? "") + + const paragraphs = article?.content.split("\n").filter((p) => p.trim()) ?? [] + const date = article?.created_at + ? new Date(article.created_at).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }) + : "" + return (
-
-
- - -
+ + {isError ? "Artikel tidak ditemukan." : "Memuat artikel..."} +
+ } + > +
+
+ + +
-
-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. -

-

- Writer’s Name -

-

- 14 March 2025 -

-
- 5 min read +
+

+ {article?.title} +

+

+ {article?.author_name || "IMPHNEN Editorial"} +

+

+ {date} +

+
+ {article?.category} +
-
-
-
- Cover Article +
+
+ Cover Article +
+ +
+

{article?.excerpt}

+ + {(paragraph) => ( +

+ {paragraph} +

+ )} +
+
- -
-

- 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. -

-

- 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. -

- - Code - -

- 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. -

-

- 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. -

-
-
+
) diff --git a/apps/dimentorin/src/app/(public)/articles/_components/card/article.tsx b/apps/dimentorin/src/app/(public)/articles/_components/card/article.tsx index 6df135e..e9bf661 100644 --- a/apps/dimentorin/src/app/(public)/articles/_components/card/article.tsx +++ b/apps/dimentorin/src/app/(public)/articles/_components/card/article.tsx @@ -2,12 +2,26 @@ import { Button } from "@imphnen-frontend-service/ui/atoms" import { cn, Show } from "@imphnen-frontend-service/utils" import { FC } from "react" import { Link } from "react-router-dom" +import { TArticleListItem } from "@imphnen-frontend-service/service" export type ArticleCardProps = { variant?: "default" | "recent" | "featured" + article?: TArticleListItem } -export const ArticleCard: FC = ({ variant = 'default' }) => { +function formatDate(iso?: string) { + if (!iso) return "" + return new Date(iso).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }) +} + +export const ArticleCard: FC = ({ variant = 'default', article }) => { + const title = article?.title ?? "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + const excerpt = article?.excerpt ?? "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + const author = article?.author_name ?? "IMPHNEN Editorial" + const date = formatDate(article?.created_at) + const slug = article?.slug ?? "" + const cover = article?.cover_url + return (
= ({ variant = 'default' }) => { )} > Cover's Article
- 5 min read + {article?.category ?? "Artikel"}
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. + {title}

= ({ variant = 'default' }) => { variant === "featured" && "text-[15px] md:text-[19px] md:mb-6", )} > - 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. + {excerpt}

= ({ variant = 'default' }) => { variant === "featured" && "text-[15px] md:text-[19px] xl:text-base", )} > -

Writer’s Name

-

14 March 2025

+

{author}

+

{date}

- + + +
diff --git a/apps/dimentorin/src/app/(public)/articles/page.tsx b/apps/dimentorin/src/app/(public)/articles/page.tsx index 51c09ec..fd1bbb0 100644 --- a/apps/dimentorin/src/app/(public)/articles/page.tsx +++ b/apps/dimentorin/src/app/(public)/articles/page.tsx @@ -1,14 +1,22 @@ import { ReactElement, useRef, useState } from "react"; import { ArticleCard } from "./_components/card/article"; -import { cn, For } from "@imphnen-frontend-service/utils"; +import { cn, For, Show } from "@imphnen-frontend-service/utils"; import { Button } from "@imphnen-frontend-service/ui/atoms"; import { motion, useInView, Variants } from "framer-motion"; - -const CATEGORIES = ['UI/UX Design', 'Software/Web Dev', 'Data & AI', 'Cloud & DevOps', 'Cybersecurity', 'IT & Network', 'Project Management', 'QA & Testing'] as const -type Category = typeof CATEGORIES[number] +import { useGetArticleCategories, useGetArticles } from "@imphnen-frontend-service/service"; export default function Components(): ReactElement { - const [activeTab, setActiveTab] = useState('UI/UX Design') + const [activeTab, setActiveTab] = useState("Semua") + const { data: articles = [], isLoading } = useGetArticles({ per_page: 20 }) + const { data: categories = [] } = useGetArticleCategories() + + const tabs = ["Semua", ...categories] + + const featured = articles[0] + const recent = articles.slice(1, 4) + const filtered = activeTab === "Semua" + ? articles + : articles.filter((a) => a.category === activeTab) const ref = useRef(null) const isInView = useInView(ref, { once: true, amount: 0.2 }) @@ -52,29 +60,34 @@ export default function Components(): ReactElement { Featuring Articles -
- - - + Memuat artikel...

} + > +
+ + + -
- - Recent Articles - -
- - {(_, index) => ( - - - - )} - +
+ + Recent Articles + +
+ + {(article, index) => ( + + + + )} + +
-
+
@@ -86,7 +99,7 @@ export default function Components(): ReactElement {
- + {(category) => (
-
- - {(_, index) => ( - - - - )} - -
+ 0} + fallback={

Belum ada artikel di kategori ini.

} + > +
+ + {(article, index) => ( + + + + )} + +
+
diff --git a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/index.tsx b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/index.tsx index 16437ef..e2ed330 100644 --- a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/index.tsx +++ b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/index.tsx @@ -10,6 +10,7 @@ import { QrisPaymentStep } from "./steps/qris-payement" import { VAPaymentStep } from "./steps/va-payment" import { SuccessStep } from "./steps/success" import { PaymentStep } from "./steps/payment" +import { TBookSessionRequest, usePostBookSession } from "@imphnen-frontend-service/service" const STEPS = ['topic', 'schedule', 'profile', 'payment', 'qr-payment', 'va-payment', 'success'] as const type Step = typeof STEPS[number] @@ -17,13 +18,32 @@ type Step = typeof STEPS[number] type Props = { open: boolean setOpen: (open: boolean) => void + mentorId?: string + mentor?: { fullname?: string | null; current_role?: string; current_company?: string } } -export const AppointmentModal: FC = ({ open, setOpen }) => { +export const AppointmentModal: FC = ({ open, setOpen, mentorId, mentor }) => { const [step, setStep] = useState('topic') const [selectedTopics, setSelectedTopics] = useState([]) + const [booking, setBooking] = useState(null) + const [error, setError] = useState("") + const bookMutation = usePostBookSession(mentorId ?? "") - const handleStep = (action: 'next' | 'prev') => { + const handleStep = async (action: 'next' | 'prev') => { + setError("") + if (step === 'payment' && action === 'next') { + if (!mentorId || !booking) { + setError("Data booking tidak lengkap.") + return + } + try { + await bookMutation.mutateAsync({ ...booking, topic: booking.topic || `Mentoring #${selectedTopics[0] ?? 1}` }) + setStep('success') + } catch { + setError("Gagal membuat sesi mentoring. Coba lagi.") + } + return + } if (action === 'next' && step === 'success') { setOpen(false) } else if (action === 'next') { @@ -113,13 +133,17 @@ export const AppointmentModal: FC = ({ open, setOpen }) => { {step === 'topic' && } - {step === 'schedule' && } + {step === 'schedule' && setBooking(data)} />} {step === 'profile' && } - {step === 'payment' && } + {step === 'payment' && } {step === 'qr-payment' && } {step === 'va-payment' && } {step === 'success' && } + + {error && ( +

{error}

+ )}
@@ -148,7 +172,7 @@ export const AppointmentModal: FC = ({ open, setOpen }) => { condition={step !== 'success'} fallback="Halman Booking" > - + Selanjutnya diff --git a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/payment.tsx b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/payment.tsx index aaea02c..e0e804b 100644 --- a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/payment.tsx +++ b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/payment.tsx @@ -2,12 +2,18 @@ import { For, Show } from "@imphnen-frontend-service/utils" import { FC } from "react" import { motion } from "framer-motion" import { TOPICS } from "../../../sections/topics" +import { TBookSessionRequest } from "@imphnen-frontend-service/service" type Props = { selectedTopics: number[] + mentor?: { fullname?: string | null; current_role?: string; current_company?: string } | null + booking?: TBookSessionRequest | null } -export const PaymentStep: FC = ({ selectedTopics }) => { +export const PaymentStep: FC = ({ selectedTopics, mentor, booking }) => { + const rate = 50000 + const serviceFee = 2000 + return ( = ({ selectedTopics }) => {

- Muhammad Firdaus Oi Oi Oi, S.H., M.H. + {mentor?.fullname || "Mentor"}

- UI Designer at Oray orayan Studios + {mentor?.current_role || "Mentor"} at {mentor?.current_company || "IMPHNEN"}

+ {booking?.scheduled_at && ( +

+ {new Date(booking.scheduled_at).toLocaleString("id-ID", { dateStyle: "full", timeStyle: "short" })} +

+ )}
@@ -104,14 +115,14 @@ export const PaymentStep: FC = ({ selectedTopics }) => {

Subtotal :

-

Rp. 50.000

+

Rp. {rate.toLocaleString("id-ID")}

Service Fee :

-

Rp. 2.000

+

Rp. {serviceFee.toLocaleString("id-ID")}


Total :

-

Rp. 52.000

+

Rp. {(rate + serviceFee).toLocaleString("id-ID")}

diff --git a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/schedule.tsx b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/schedule.tsx index be7127f..5493ddc 100644 --- a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/schedule.tsx +++ b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/schedule.tsx @@ -1,9 +1,11 @@ import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms" import { cn } from "@imphnen-frontend-service/utils" import { motion } from "framer-motion" +import { useEffect, useState } from "react" +import { TBookSessionRequest } from "@imphnen-frontend-service/service" const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda. - + Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __. Saya ingin tahu secara khusus tentang ___. @@ -13,7 +15,27 @@ Saya ingin tahu secara khusus tentang ___. const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]') -export const ScheduleStep = () => { +type Props = { + onChange: (data: TBookSessionRequest) => void +} + +export const ScheduleStep = ({ onChange }: Props) => { + const [date, setDate] = useState("") + const [time, setTime] = useState("") + const [sessionType, setSessionType] = useState("online") + const [description, setDescription] = useState("") + + useEffect(() => { + if (!date || !time) return + onChange({ + topic: "Mentoring Session", + scheduled_at: `${date}T${time}:00`, + description, + duration_minutes: 60, + session_type: sessionType, + }) + }, [date, time, sessionType, description]) + return ( {
- + setDate(e.target.value)} />
- + setTime(e.target.value)} />
- setSessionType(e.target.value)}> @@ -48,7 +70,9 @@ export const ScheduleStep = () => {