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 (
-
-
-
-
- Back
-
-
- Read Next
-
-
-
+
+ {isError ? "Artikel tidak ditemukan." : "Memuat artikel..."}
+
+ }
+ >
+
+
+ window.history.back()}
+ >
+
+ Back
+
+
+ Read Next
+
+
+
-
-
- 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}
+
-
-
-
-
+
+
+
+
+
+
+
{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.
-
-
-
-
-
- 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' }) => {
)}
>
- 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}
-
- Baca Artikel
-
+
+
+ Baca Artikel
+
+
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 (
{
diff --git a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/sections/senpai-schedule.tsx b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/sections/senpai-schedule.tsx
index b47c709..7d8cc37 100644
--- a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/sections/senpai-schedule.tsx
+++ b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/sections/senpai-schedule.tsx
@@ -1,68 +1,77 @@
import { Button } from "@imphnen-frontend-service/ui/atoms"
-import { cn, For } from "@imphnen-frontend-service/utils"
+import { For, Show } from "@imphnen-frontend-service/utils"
+import { useGetMentorAvailability } from "@imphnen-frontend-service/service"
import { FC } from "react"
-const SCHEDULES = [
- {
- date: "Kamis, 20 Maret 2025",
- availableCount: 10,
- schedule: [
- { time: "19:00 WIB", available: true },
- { time: "20:00 WIB", available: true },
- { time: "21:00 WIB", available: true },
- { time: "22:00 WIB", available: true },
- ]
- },
- {
- date: "Jumat, 21 Maret 2025",
- availableCount: 1,
- schedule: [
- { time: "19:00 WIB", available: true },
- { time: "20:00 WIB", available: true },
- { time: "21:00 WIB", available: false },
- { time: "22:00 WIB", available: true },
- ]
- }
-]
-
type Props = {
onBook: () => void
+ mentorId?: string
}
-export const SenpaiScheduleSection: FC = ({ onBook }) => {
+const DAY_NAMES = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"]
+
+function formatDate(dateStr: string) {
+ const d = new Date(dateStr + "T00:00:00")
+ return `${DAY_NAMES[d.getDay()]}, ${d.getDate()} ${d.toLocaleString("id-ID", { month: "long" })} ${d.getFullYear()}`
+}
+
+function formatTime(timeStr: string) {
+ const [h] = timeStr.split(":").map(Number)
+ return `${String(h).padStart(2, "0")}:00 WIB`
+}
+
+export const SenpaiScheduleSection: FC = ({ onBook, mentorId }) => {
+ const { data: availability, isLoading } = useGetMentorAvailability(mentorId ?? "")
+
+ const slots = availability?.slots ?? []
+ const byDate = new Map()
+ for (const s of slots) {
+ if (!byDate.has(s.date)) byDate.set(s.date, [])
+ byDate.get(s.date)!.push(s)
+ }
+ const days = [...byDate.entries()]
+
return (
Senpai Schedule
-
- {(item) => (
-
-
-
{item.date}
-
5 ? "bg-primary-200 text-primary-500" : "bg-danger-100 text-danger-600"
- )}
- >
- {item.availableCount} slot tersisa
-
+
0}
+ fallback={
+
+ {isLoading ? "Memuat jadwal senpai..." : "Belum ada jadwal tersedia."}
+
+ }
+ >
+
+ {([date, daySlots]) => (
+
+
+
{formatDate(date)}
+
+ {daySlots.length} slot tersisa
+
+
+
+
+ {(schedule) => (
+
+ {formatTime(schedule.time)}
+
+ )}
+
+
-
-
- {(schedule, index) => (
-
- {schedule.time}
-
- )}
-
-
-
- )}
-
+ )}
+
+
@@ -72,4 +81,4 @@ export const SenpaiScheduleSection: FC
= ({ onBook }) => {
)
-}
\ No newline at end of file
+}
diff --git a/apps/dimentorin/src/app/(public)/mentoring/[id]/page.tsx b/apps/dimentorin/src/app/(public)/mentoring/[id]/page.tsx
index 55ef39f..bbb3e4d 100644
--- a/apps/dimentorin/src/app/(public)/mentoring/[id]/page.tsx
+++ b/apps/dimentorin/src/app/(public)/mentoring/[id]/page.tsx
@@ -55,13 +55,13 @@ export const Components: FC = () => {
- setOpen(true)} />
+ setOpen(true)} mentorId={mentor?.id} />
-
+
)
diff --git a/libs/service/src/api/dimentorin/index.ts b/libs/service/src/api/dimentorin/index.ts
index 01cecfe..182f13f 100644
--- a/libs/service/src/api/dimentorin/index.ts
+++ b/libs/service/src/api/dimentorin/index.ts
@@ -1,5 +1,8 @@
import { api } from '../';
import {
+ TArticleDetail,
+ TArticleListItem,
+ TArticleListParams,
TMentorAvailability,
TMentorDetail,
TMentorListParams,
@@ -95,3 +98,38 @@ export const postSessionFeedback = async (
});
return data;
};
+
+export const getArticles = async (
+ params?: TArticleListParams
+): Promise => {
+ const { data } = await api({
+ method: 'GET',
+ url: '/dimentorin/articles',
+ params,
+ });
+ return data.data;
+};
+
+export const getArticleCategories = async (): Promise => {
+ const { data } = await api({
+ method: 'GET',
+ url: '/dimentorin/articles/categories',
+ });
+ return data.data;
+};
+
+export const getArticleBySlug = async (slug: string): Promise => {
+ const { data } = await api({
+ method: 'GET',
+ url: `/dimentorin/articles/slug/${slug}`,
+ });
+ return data.data;
+};
+
+export const getArticleById = async (id: string): Promise => {
+ const { data } = await api({
+ method: 'GET',
+ url: `/dimentorin/articles/${id}`,
+ });
+ return data.data;
+};
diff --git a/libs/service/src/hooks/dimentorin/index.ts b/libs/service/src/hooks/dimentorin/index.ts
index 097914f..9468a09 100644
--- a/libs/service/src/hooks/dimentorin/index.ts
+++ b/libs/service/src/hooks/dimentorin/index.ts
@@ -1,5 +1,9 @@
import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query';
import {
+ getArticleById,
+ getArticleBySlug,
+ getArticleCategories,
+ getArticles,
getMentorAvailability,
getMentorDetail,
getMentorMe,
@@ -11,6 +15,9 @@ import {
postSessionFeedback,
} from '../../api/dimentorin';
import {
+ TArticleDetail,
+ TArticleListItem,
+ TArticleListParams,
TBookSessionRequest,
TMentorAvailability,
TMentorDetail,
@@ -105,4 +112,43 @@ export const usePostSessionFeedback = (sessionId: string) => {
});
};
+export const useGetArticles = (
+ params?: TArticleListParams
+): UseQueryResult => {
+ return useQuery({
+ queryKey: ['get-articles', params],
+ queryFn: async () => await getArticles(params),
+ });
+};
+
+export const useGetArticleCategories = (): UseQueryResult<
+ string[],
+ TResponseError
+> => {
+ return useQuery({
+ queryKey: ['get-article-categories'],
+ queryFn: async () => await getArticleCategories(),
+ });
+};
+
+export const useGetArticleBySlug = (
+ slug: string
+): UseQueryResult => {
+ return useQuery({
+ queryKey: ['get-article-by-slug', slug],
+ queryFn: async () => await getArticleBySlug(slug),
+ enabled: !!slug,
+ });
+};
+
+export const useGetArticleById = (
+ id: string
+): UseQueryResult => {
+ return useQuery({
+ queryKey: ['get-article-by-id', id],
+ queryFn: async () => await getArticleById(id),
+ enabled: !!id,
+ });
+};
+
export type { TResponseMessage };
diff --git a/libs/service/src/types/dimentorin/index.ts b/libs/service/src/types/dimentorin/index.ts
index 2fb8824..1000191 100644
--- a/libs/service/src/types/dimentorin/index.ts
+++ b/libs/service/src/types/dimentorin/index.ts
@@ -97,3 +97,26 @@ export type TSessionFeedbackRequest = {
feedback: string;
rating: number;
};
+
+export type TArticleListItem = {
+ id: string;
+ title: string;
+ slug: string;
+ category: string;
+ excerpt: string;
+ cover_url: string | null;
+ author_name: string | null;
+ created_at: string;
+};
+
+export type TArticleDetail = TArticleListItem & {
+ content: string;
+ is_published: boolean;
+ updated_at: string;
+};
+
+export type TArticleListParams = {
+ page?: number;
+ per_page?: number;
+ category?: string;
+};