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:
maulanasdqn
2026-04-05 17:08:58 +07:00
co-authored by Claude Sonnet 4.6
parent 8d2aaf5188
commit 53ad40f3fd
74 changed files with 3028 additions and 1942 deletions
@@ -10,6 +10,9 @@ import { QrisPaymentStep } from "./steps/qris-payement"
import { VAPaymentStep } from "./steps/va-payment"
import { SuccessStep } from "./steps/success"
import { PaymentStep } from "./steps/payment"
import { useBookSession } from "@imphnen-frontend-service/service"
import { TOPICS } from "../../sections/topics"
import { toast } from "sonner"
const STEPS = ['topic', 'schedule', 'profile', 'payment', 'qr-payment', 'va-payment', 'success'] as const
type Step = typeof STEPS[number]
@@ -17,15 +20,48 @@ type Step = typeof STEPS[number]
type Props = {
open: boolean
setOpen: (open: boolean) => void
mentorId?: string
}
export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
export const AppointmentModal: FC<Props> = ({ open, setOpen, mentorId }) => {
const [step, setStep] = useState<Step>('topic')
const [selectedTopics, setSelectedTopics] = useState<number[]>([])
const [scheduledDate, setScheduledDate] = useState('')
const [scheduledTime, setScheduledTime] = useState('')
const [description, setDescription] = useState('')
const [sessionType, setSessionType] = useState('online')
const [isBooking, setIsBooking] = useState(false)
const handleStep = (action: 'next' | 'prev') => {
const bookSession = useBookSession(mentorId ?? '')
const handleStep = async (action: 'next' | 'prev') => {
if (action === 'next' && step === 'success') {
setOpen(false)
} else if (action === 'next' && step === 'payment' && mentorId) {
const topicNames = selectedTopics
.map((id) => TOPICS.find((t) => t.id === id)?.name)
.filter(Boolean)
.join(', ')
const scheduledAt =
scheduledDate && scheduledTime
? new Date(`${scheduledDate}T${scheduledTime}`).toISOString()
: new Date().toISOString()
setIsBooking(true)
try {
await bookSession.mutateAsync({
topic: topicNames || 'General Mentoring',
description: description || undefined,
scheduled_at: scheduledAt,
session_type: sessionType,
})
setStep('qr-payment')
} catch {
toast.error('Gagal membuat sesi. Silakan coba lagi.')
} finally {
setIsBooking(false)
}
} else if (action === 'next') {
setStep(STEPS[STEPS.indexOf(step) + 1])
} else if (action === 'prev' && step !== 'topic') {
@@ -46,6 +82,12 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
window.addEventListener("keydown", handleEscapeKey)
} else {
document.body.style.overflow = ""
setStep('topic')
setSelectedTopics([])
setScheduledDate('')
setScheduledTime('')
setDescription('')
setSessionType('online')
}
return () => {
@@ -77,6 +119,7 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
size="sm"
variant="text"
className="absolute top-3 right-3 bg-primary-200 p-1 shadow md:bg-white md:p-2 md:top-8 md:right-10"
onClick={() => setOpen(false)}
>
<CloseOutlined className="md:text-lg" />
</Button>
@@ -113,7 +156,18 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
<AnimatePresence>
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} />}
{step === 'schedule' && <ScheduleStep />}
{step === 'schedule' && (
<ScheduleStep
scheduledDate={scheduledDate}
scheduledTime={scheduledTime}
description={description}
sessionType={sessionType}
onDateChange={setScheduledDate}
onTimeChange={setScheduledTime}
onDescriptionChange={setDescription}
onSessionTypeChange={setSessionType}
/>
)}
{step === 'profile' && <ProfileStep />}
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} />}
{step === 'qr-payment' && <QrisPaymentStep />}
@@ -141,14 +195,14 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
size="sm"
variant="primary"
className={cn((step === 'topic' || step === 'success') && 'w-full')}
disabled={selectedTopics.length === 0 && step === 'topic'}
disabled={(selectedTopics.length === 0 && step === 'topic') || isBooking}
onClick={() => handleStep('next')}
>
<Show
condition={step !== 'success'}
fallback="Halman Booking"
>
<Show condition={step !== 'payment'} fallback="Bayar Sekarang">
<Show condition={step !== 'payment'} fallback={isBooking ? 'Memproses...' : 'Bayar Sekarang'}>
Selanjutnya
</Show>
</Show>
@@ -160,4 +214,4 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
)}
</AnimatePresence>
)
}
}
@@ -3,7 +3,7 @@ import { cn } from "@imphnen-frontend-service/utils"
import { motion } from "framer-motion"
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
Saya ingin tahu secara khusus tentang ___.
@@ -13,7 +13,27 @@ Saya ingin tahu secara khusus tentang ___.
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
export const ScheduleStep = () => {
type Props = {
scheduledDate: string
scheduledTime: string
description: string
sessionType: string
onDateChange: (v: string) => void
onTimeChange: (v: string) => void
onDescriptionChange: (v: string) => void
onSessionTypeChange: (v: string) => void
}
export const ScheduleStep = ({
scheduledDate,
scheduledTime,
description,
sessionType,
onDateChange,
onTimeChange,
onDescriptionChange,
onSessionTypeChange,
}: Props) => {
return (
<motion.div
className="bg-white px-6 py-5 rounded-md md:px-6"
@@ -31,15 +51,29 @@ export const ScheduleStep = () => {
<div className="grid gap-2.5 md:grid-cols-2 md:gap-5">
<div>
<label className={labelClass}>Tanggal</label>
<Input type="date" className="min-w-full w-full" />
<Input
type="date"
className="min-w-full w-full"
value={scheduledDate}
onChange={(e) => onDateChange(e.target.value)}
/>
</div>
<div>
<label className={labelClass}>Waktu</label>
<Input type="time" className="min-w-full w-full" />
<Input
type="time"
className="min-w-full w-full"
value={scheduledTime}
onChange={(e) => onTimeChange(e.target.value)}
/>
</div>
<div className="relative md:col-span-full">
<label className={labelClass}>Lokasi</label>
<Select className="min-w-full w-full">
<Select
className="min-w-full w-full"
value={sessionType}
onChange={(e) => onSessionTypeChange(e.target.value)}
>
<option value="online">Online</option>
<option value="offline">Offline</option>
</Select>
@@ -48,7 +82,10 @@ export const ScheduleStep = () => {
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
<Textarea
className="min-w-full w-full h-40"
placeholder={placeholder} />
placeholder={placeholder}
value={description}
onChange={(e) => onDescriptionChange(e.target.value)}
/>
</div>
</div>
</motion.div>
@@ -1,27 +1,30 @@
import { cn, For } from "@imphnen-frontend-service/utils"
import { FC } from "react"
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
const EDUCATION = [
{ name: 'Universitas Widyabakti', major: 'Intern Front End', duration: '2 Months', range: 'Oct 2024 - Present' },
{ name: 'SMKN 99 Banjaran', major: 'Rekayasa Perangkat Lunak', duration: '6 Months', range: 'May 2024 - Oct 2024' },
]
type Props = {
mentor?: MentorDetailResponseDto
}
export const EducationSection: FC<Props> = ({ mentor }) => {
const education = mentor?.education ?? []
if (education.length === 0) return null
export const EducationSection = () => {
return (
<div className="px-7 py-8 rounded-md shadow-md">
<h2 className="text-xs font-semibold mb-5 md:text-[15px] xl:text-[19px]">Education</h2>
<div className="space-y-4 divide-y">
<For data={EDUCATION}>
<For data={education}>
{(item, index) => (
<div key={index} className={cn("flex items-center gap-x-4", index !== EDUCATION.length - 1 && "pb-4")}>
<div key={item.id ?? index} className={cn("flex items-center gap-x-4", index !== education.length - 1 && "pb-4")}>
<div className="rounded-full size-6 bg-neutral-200 md:size-7 xl:size-8"></div>
<div className='flex-1 text-[10px] font-medium'>
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.name}</p>
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.institution}</p>
<p>
<span className="text-neutral-600 xl:text-xs">{item.major}</span>
<span className="text-neutral-600 xl:text-xs">{item.degree} - {item.field}</span>
<span className="text-neutral-300"> · </span>
<span className="text-neutral-400 font-normal">{item.duration}</span>
<span className="text-neutral-300"> · </span>
<span className="text-neutral-400 font-normal xl:font-medium">{item.range}</span>
<span className="text-neutral-400 font-normal xl:font-medium">{item.period}</span>
</p>
</div>
</div>
@@ -1,28 +1,32 @@
import { cn, For } from "@imphnen-frontend-service/utils"
import { FC } from "react"
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
const EXPERIENCE = [
{ name: 'Sunday.com', position: 'Intern Front End', duration: '2 Months', range: 'Oct 2024 - Present' },
{ name: 'CodeX Digital', position: 'Intern Front End', duration: '6 Months', range: 'May 2024 - Oct 2024' },
]
type Props = {
mentor?: MentorDetailResponseDto
}
export const ExperienceSection: FC<Props> = ({ mentor }) => {
const experience = mentor?.experience ?? []
if (experience.length === 0) return null
export const ExperienceSection: FC = () => {
return (
<div className="px-7 py-8 rounded-md shadow-md">
<h2 className="text-xs font-semibold mb-5 md:text-[15px] xl:text-[19px]">Experience</h2>
<div className="space-y-4 divide-y">
<For data={EXPERIENCE}>
<For data={experience}>
{(item, index) => (
<div key={index} className={cn("flex items-center gap-x-4", index !== EXPERIENCE.length - 1 && "pb-4")}>
<div key={item.id ?? index} className={cn("flex items-center gap-x-4", index !== experience.length - 1 && "pb-4")}>
<div className="rounded-full size-6 bg-neutral-200 md:size-7 xl:size-8"></div>
<div className='flex-1 text-[10px] font-medium'>
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.name}</p>
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.company}</p>
<p>
<span className="text-neutral-600 xl:text-xs">{item.position}</span>
<span className="text-neutral-300"> · </span>
<span className="text-neutral-400 font-normal">{item.duration}</span>
<span className="text-neutral-300"> · </span>
<span className="text-neutral-400 font-normal xl:font-medium">{item.range}</span>
<span className="text-neutral-400 font-normal xl:font-medium">{item.period}</span>
</p>
</div>
</div>
@@ -1,12 +1,19 @@
import { StarFilled } from "@ant-design/icons"
import { Button } from "@imphnen-frontend-service/ui/atoms"
import { cn, For } from "@imphnen-frontend-service/utils"
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
type Props = {
onBook: () => void
mentor?: MentorDetailResponseDto
}
export const ProfileSection: React.FC<Props> = ({ onBook }) => {
export const ProfileSection: React.FC<Props> = ({ onBook, mentor }) => {
const expertise = mentor?.expertise ?? []
const softSkills = mentor?.topics_of_interest ?? []
const rating = mentor?.rating ?? 0
const ratingLabel = rating >= 4.5 ? 'Excelent Sensei' : rating >= 3.5 ? 'Good Sensei' : 'Rising Sensei'
return (
<div className="bg-white px-4 py-5 space-y-6 md:px-8 md:pt-6 md:pb-0 xl:space-y-0 xl:py-[30px] xl:flex xl:gap-x-9 xl:justify-between">
<div
@@ -33,56 +40,61 @@ export const ProfileSection: React.FC<Props> = ({ onBook }) => {
"md:text-[19px] md:mb-2 md:text-start xl:text-[23px]",
)}
>
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
{mentor?.fullname || 'Loading...'}
</h1>
<p className="text-xs mb-4 text-neutral-600 md:mb-5 md:text-[15px] xl:text-[19px] xl:mb-5">
UI Designer at Oray orayan Studios
{mentor ? `${mentor.current_role} at ${mentor.current_company}` : ''}
</p>
<div className="flex items-center gap-x-2.5 w-full max-w-max border border-primary-50 p-1.5 rounded-md mx-auto md:ms-0">
<div
className="bg-gradient-to-tr from-primary-500 to-primary-200 rounded-sm text-white size-5 flex justify-center items-center xl:size-[29.4px]"
>
<StarFilled className="text-xs xl:text-sm" />
{rating > 0 && (
<div className="flex items-center gap-x-2.5 w-full max-w-max border border-primary-50 p-1.5 rounded-md mx-auto md:ms-0">
<div
className="bg-gradient-to-tr from-primary-500 to-primary-200 rounded-sm text-white size-5 flex justify-center items-center xl:size-[29.4px]"
>
<StarFilled className="text-xs xl:text-sm" />
</div>
<div>
<p className="text-[10px] font-medium mb-1 text-neutral-800 xl:text-xs">{ratingLabel}</p>
<p className="text-[8px] text-neutral-600 xl:text-[10px]">{rating.toFixed(1)}/5.0</p>
</div>
</div>
<div>
<p className="text-[10px] font-medium mb-1 text-neutral-800 xl:text-xs">Excelent Sensei</p>
<p className="text-[8px] text-neutral-600 xl:text-[10px]">4.8/5.0</p>
</div>
</div>
)}
</div>
</div>
<div className="flex flex-col gap-y-3 xl:gap-y-4 xl:max-w-[402px]">
<div>
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Expertise</h2>
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
<For data={['UI Design', 'UX Reseacrh']}>
{(item) => (
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
{item}
</div>
)}
</For>
{expertise.length > 0 && (
<div>
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Expertise</h2>
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
<For data={expertise}>
{(item) => (
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
{item}
</div>
)}
</For>
</div>
</div>
</div>
<div>
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Soft Skills</h2>
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
<For data={['Design Thinking', 'Communication', 'Problem Solving', '19:00 WIB']}>
{(item) => (
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
{item}
</div>
)}
</For>
)}
{softSkills.length > 0 && (
<div>
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Topics of Interest</h2>
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
<For data={softSkills}>
{(item) => (
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
{item}
</div>
)}
</For>
</div>
</div>
</div>
)}
</div>
<div className="hidden md:flex xl:hidden justify-between">
<div>
<p className="text-primary-500 text-[15px] font-semibold">Jum, 4 April 2025</p>
<p className="text-neutral-500 text-xs font-medium">Jum, 4 April 2025</p>
<p className="text-primary-500 text-[15px] font-semibold">{mentor?.availability_commitment || ''}</p>
</div>
<Button type="button" size="sm" onClick={onBook}>
Book Your Senpai!
@@ -1,19 +1,24 @@
import { StarFilled } from "@ant-design/icons";
import { For } from "@imphnen-frontend-service/utils";
import { FC } from "react";
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service";
const SENPAI_STATISTIC = [
{ name: 'Total Sessions', count: 8 },
{ name: 'Mentee Impact', count: 1000 },
{ name: 'Response Time', count: '30 Minute' }
]
type Props = {
mentor?: MentorDetailResponseDto
}
export const StatisticsSection: FC<Props> = ({ mentor }) => {
const stats = [
{ name: 'Total Sessions', count: mentor?.mentoring_sessions ?? 0 },
{ name: 'Rating', count: mentor?.rating != null ? `${mentor.rating.toFixed(1)}/5.0` : 'N/A' },
{ name: 'Experience', count: mentor?.years_of_experience != null ? `${mentor.years_of_experience} Yrs` : 'N/A' },
]
export const StatisticsSection: FC = () => {
return (
<div className="md:mb-10">
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Statistics</h2>
<div className="grid gap-4 xl:flex">
<For data={SENPAI_STATISTIC}>
<For data={stats}>
{(item, index) => (
<div key={index} className="px-2.5 py-2 border border-primary-50 rounded-md shadow flex items-center gap-x-2.5">
<div
@@ -31,4 +36,4 @@ export const StatisticsSection: FC = () => {
</div>
</div>
)
};
};
@@ -1,5 +1,6 @@
import { For } from "@imphnen-frontend-service/utils"
import { FC } from "react"
import type { MentorDetailResponseDto } from "@imphnen-frontend-service/service"
export const TOPICS = [
{ id: 1, icon: '💼', name: 'Career & Self Development' },
@@ -11,22 +12,41 @@ export const TOPICS = [
{ id: 7, icon: <span className="font-bold text-primary-500">AI</span>, name: 'AI Tips' }
]
export const TopicsSection: FC = () => {
type Props = {
mentor?: MentorDetailResponseDto
}
export const TopicsSection: FC<Props> = ({ mentor }) => {
const mentorTopics = mentor?.topics_of_interest ?? []
return (
<div>
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Topics</h2>
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5">
<For data={TOPICS}>
{(item, index) => (
<div
key={index}
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
>
<span>{item.icon} </span>
<span>{item.name}</span>
</div>
)}
</For>
{mentorTopics.length > 0 ? (
<For data={mentorTopics}>
{(topic, index) => (
<div
key={index}
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
>
{topic}
</div>
)}
</For>
) : (
<For data={TOPICS}>
{(item, index) => (
<div
key={index}
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
>
<span>{item.icon} </span>
<span>{item.name}</span>
</div>
)}
</For>
)}
</div>
</div>
)
@@ -1,4 +1,5 @@
import { FC, useState } from 'react'
import { useParams } from 'react-router-dom'
import { ProfileSection } from './_components/sections/profile'
import { StatisticsSection } from './_components/sections/senpai-statistics'
import { TopicsSection } from './_components/sections/topics'
@@ -7,15 +8,27 @@ import { EducationSection } from './_components/sections/education'
import { SenpaiScheduleSection } from './_components/sections/senpai-schedule'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { AppointmentModal } from './_components/modals/appointment'
import { useMentorById } from '@imphnen-frontend-service/service'
export const Components: FC = () => {
const [open, setOpen] = useState(false)
const params = useParams()
const mentorId = params?.id ?? ''
const { data: mentor, isLoading } = useMentorById(mentorId)
if (isLoading) {
return (
<main className="min-h-screen flex items-center justify-center">
<div className="text-center text-neutral-400">Loading mentor profile...</div>
</main>
)
}
return (
<main>
<section className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20">
<div className="max-w-7xl mx-auto space-y-8 md:bg-white xl:bg-transparent">
<ProfileSection onBook={() => setOpen(true)} />
<ProfileSection mentor={mentor} onBook={() => setOpen(true)} />
<Button type="button" size="sm" className="w-full md:hidden" onClick={() => setOpen(true)}>
Book Your Senpai!
@@ -23,18 +36,20 @@ export const Components: FC = () => {
<div className="bg-white px-4 py-5 md:px-8 md:pb-6 md:pt-0 xl:py-7 xl:flex xl:gap-x-10">
<div className="space-y-10 md:space-y-7 xl:flex-1">
<StatisticsSection />
<TopicsSection />
<StatisticsSection mentor={mentor} />
<TopicsSection mentor={mentor} />
<div className="px-6 py-8 rounded-md shadow-md">
<h2 className="text-xs text-neutral-800 font-semibold mb-5 md:text-[15px] xl:text-[19px]">Senpai Resume</h2>
<p className="text-[10px] font-medium text-neutral-600 text-pretty md:text-[15px]">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
</p>
</div>
{mentor?.bio && (
<div className="px-6 py-8 rounded-md shadow-md">
<h2 className="text-xs text-neutral-800 font-semibold mb-5 md:text-[15px] xl:text-[19px]">Senpai Resume</h2>
<p className="text-[10px] font-medium text-neutral-600 text-pretty md:text-[15px]">
{mentor.bio}
</p>
</div>
)}
<ExperienceSection />
<EducationSection />
<ExperienceSection mentor={mentor} />
<EducationSection mentor={mentor} />
</div>
<div className="hidden xl:block xl:w-[400px]">
@@ -44,7 +59,7 @@ export const Components: FC = () => {
</div>
</section>
<AppointmentModal open={open} setOpen={setOpen} />
<AppointmentModal open={open} setOpen={setOpen} mentorId={mentorId} />
</main>
)
}
@@ -1,8 +1,20 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { FC } from 'react';
import { Link } from 'react-router-dom';
import type { MentorDetailResponseDto } from '@imphnen-frontend-service/service';
interface MentorCardProps {
mentor: MentorDetailResponseDto;
}
export const MentorCard: FC<MentorCardProps> = ({ mentor }) => {
const expertise = mentor.expertise ?? [];
const firstSkill = expertise[0];
const secondSkill = expertise[1];
const extraCount = expertise.length - 2;
const yearsExp = mentor.years_of_experience ?? 0;
const expLabel = `${yearsExp}+ Years Experience`;
export const MentorCard: FC = () => {
return (
<div className="p-2.5 rounded-md bg-white shadow flex gap-x-4 items-start md:p-4 md:flex-col md:rounded-lg md:gap-y-4">
<div className="size-[60px] rounded-md overflow-hidden md:w-full md:h-auto md:aspect-square">
@@ -10,32 +22,38 @@ export const MentorCard: FC = () => {
</div>
<div>
<Button variant="text" size="sm" className="bg-primary-100 h-auto px-1.5 py-1 text-[8px] font-normal mb-2 hover:bg-primary-100 md:text-[10px] md:font-medium md:mb-4">
3-5 Years Experience
{expLabel}
</Button>
<h2 className="mb-1">
<Link to="/mentoring/detail" className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
Fullname
<Link to={`/mentoring/${mentor.id}`} className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
{mentor.fullname || 'Unknown Mentor'}
</Link>
</h2>
<p className="text-[8px] text-neutral-500 mb-3 md:text-[10px] md:font-medium md:mb-4 lg:text-xs">
Full Stack Enjoyer at name company
{mentor.current_role} at {mentor.current_company}
</p>
<div>
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Soft Skills :</p>
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Expertise :</p>
<div className="space-x-2">
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]">
Communication
</Button>
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hidden hover:bg-primary-200 md:inline-block lg:text-[10px]">
Communication
</Button>
<Button variant="text" size="sm" className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]">
+2
</Button>
{firstSkill && (
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]">
{firstSkill}
</Button>
)}
{secondSkill && (
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hidden hover:bg-primary-200 md:inline-block lg:text-[10px]">
{secondSkill}
</Button>
)}
{extraCount > 0 && (
<Button variant="text" size="sm" className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]">
+{extraCount}
</Button>
)}
</div>
</div>
</div>
</div>
)
}
);
};
@@ -8,27 +8,35 @@ import { MentorCard } from './_components/mentor-card';
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
import { motion, useInView, Variants } from 'framer-motion';
const TEMP_DATA = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'John Doe' },
{ id: 3, name: 'John Doe' },
{ id: 4, name: 'John Doe' },
]
import { useMentorList } from '@imphnen-frontend-service/service';
export const Components: FC = (): ReactElement => {
const [search, setSearch] = useState('');
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 3,
pageSize: 8,
});
const { data: mentorData, isLoading } = useMentorList({
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
search: search || undefined,
});
const mentors = mentorData?.data ?? [];
const totalItems = mentorData?.meta?.total ?? 0;
const table = useReactTable({
data: TEMP_DATA,
data: mentors,
columns: [],
state: { pagination },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
onPaginationChange: (updater) => {
setPagination(updater);
},
pageCount: Math.ceil(totalItems / pagination.pageSize) || 1,
manualPagination: true,
});
const ref = useRef(null)
@@ -91,24 +99,33 @@ export const Components: FC = (): ReactElement => {
<Input
placeholder="Cari berdasarkan nama, posisi/peran"
className="relative min-w-full w-full"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
/>
<SearchOutlined className="absolute right-2.5 top-1/2 -translate-y-1/2 text-primary-500 size-2.5 cursor-text md:me-12 lg:me-0" />
</motion.div>
<motion.div
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
<For data={Array.from({ length: 8 })}>
{(_, index) => (
<motion.div key={index} variants={childVariants}>
<MentorCard />
</motion.div>
)}
</For>
</motion.div>
{isLoading ? (
<div className="text-center py-12 text-neutral-400">Loading mentors...</div>
) : (
<motion.div
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
<For data={mentors}>
{(mentor, index) => (
<motion.div key={mentor.id ?? index} variants={childVariants}>
<MentorCard mentor={mentor} />
</motion.div>
)}
</For>
</motion.div>
)}
<Pagination table={table} />
</motion.div>