feat(dimentorin): implementasi avatar inisial mentor + resume dinamis + fix dummy data

- Komponen baru MentorAvatar: gradient unik per nama (hash), inisial nama, reusable (sm/md/lg + textClassName)
- Kartu mentor: ganti img testimonial.webp statis -> avatar inisial gradient (MU, dll)
- Detail mentor profile: avatar besar + fix hardcoded tanggal 'Jum, 4 April 2025' -> tanggal dinamis id-ID
- Senpai Resume: fallback dinamis dari data mentor (role/experience/expertise/industries/languages) saat bio kosong, bukan 'Belum ada deskripsi'
- Booking modal topic step: ganti nama mentor hardcoded 'Muhammad Firdaus Oi Oi Oi' -> dari prop mentor + avatar
- Booking modal payment step: avatar mentor di summary
This commit is contained in:
asepharyana
2026-08-05 20:59:56 +07:00
parent 8b77f27f58
commit 22bb4b63ce
7 changed files with 145 additions and 14 deletions
@@ -159,7 +159,7 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen, mentorId, mentor })
)}
<AnimatePresence>
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} />}
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} mentor={mentor} />}
{step === 'schedule' && <ScheduleStep onChange={(data) => setBooking(data)} />}
{step === 'profile' && <ProfileStep />}
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} mentor={mentor} booking={booking} onMethodChange={setPaymentMethod} />}
@@ -3,6 +3,7 @@ import { FC, useState } from "react"
import { motion } from "framer-motion"
import { TOPICS } from "../../../sections/topics"
import { TBookSessionRequest } from "@imphnen-frontend-service/service"
import { MentorAvatar } from "../../../../../_components/mentor-avatar"
type Props = {
selectedTopics: number[]
@@ -42,7 +43,12 @@ export const PaymentStep: FC<Props> = ({ selectedTopics, mentor, booking, onMeth
</p>
<div>
<div className="aspect-square mb-2 size-10 mx-auto rounded-full overflow-hidden md:size-20 md:ms-0">
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
<MentorAvatar
name={mentor?.fullname || "Mentor"}
size="sm"
className="w-full h-full rounded-full"
textClassName="text-sm md:text-xl"
/>
</div>
<div className="w-44 mx-auto text-center md:ms-0 md:text-left md:mb-2 md:text-[15px] md:w-auto">
<h3 className="text-xs font-semibold mb-1 xl:text-[15px]">
@@ -3,13 +3,19 @@ import { cn, For, Show } from "@imphnen-frontend-service/utils"
import { FC } from "react"
import { motion } from "framer-motion"
import { TOPICS } from "../../../sections/topics"
import { MentorAvatar } from "../../../../../_components/mentor-avatar"
type Props = {
selectedTopics: number[]
setSelectedTopics: (topics: number[]) => void
mentor?: {
fullname?: string | null
current_role?: string
current_company?: string
}
}
export const TopicStep: FC<Props> = ({ selectedTopics, setSelectedTopics }) => {
export const TopicStep: FC<Props> = ({ selectedTopics, setSelectedTopics, mentor }) => {
const handleSelectTopic = (topicId: number) => {
if (selectedTopics.includes(topicId)) {
setSelectedTopics(selectedTopics.filter((id) => id !== topicId))
@@ -27,14 +33,19 @@ export const TopicStep: FC<Props> = ({ selectedTopics, setSelectedTopics }) => {
exit={{ opacity: 0, y: 20 }}
>
<div className="w-16 aspect-4/5 overflow-hidden md:w-[134px] xl:w-[140px]">
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
<MentorAvatar
name={mentor?.fullname || "Mentor"}
size="md"
className="w-full h-full rounded-none"
textClassName="text-lg md:text-3xl"
/>
</div>
<div className="flex-1 pe-4">
<h3 className="text-xs font-semibold mb-1 line-clamp-2 md:text-[19px] md:mb-2 xl:text-[23px]">
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
{mentor?.fullname || "Mentor"}
</h3>
<p className="text-[10px] line-clamp-2 text-neutral-600 md:text-[15px] xl:text-[19px]">
UI Designer at Oray orayan Studios
{mentor?.current_role || "Mentor"} at {mentor?.current_company || "IMPHNEN"}
</p>
</div>
</motion.div>
@@ -2,6 +2,7 @@ import { StarFilled } from "@ant-design/icons"
import { Button } from "@imphnen-frontend-service/ui/atoms"
import { cn, For } from "@imphnen-frontend-service/utils"
import { TMentorDetail } from "@imphnen-frontend-service/service"
import { MentorAvatar } from "../../../_components/mentor-avatar"
type Props = {
onBook: () => void
@@ -31,7 +32,12 @@ export const ProfileSection: React.FC<Props> = ({ onBook, mentor }) => {
"xl:aspect-5/6 md:h-60 md:rounded-lg"
)}
>
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
<MentorAvatar
name={name}
size="lg"
className="w-full h-full rounded-none"
textClassName="text-xl md:text-3xl lg:text-5xl"
/>
</div>
<div className="md:max-w-[242px] xl:max-w-[306px]">
@@ -89,8 +95,12 @@ export const ProfileSection: React.FC<Props> = ({ onBook, mentor }) => {
<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">
{new Date().toLocaleDateString('id-ID', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' })}
</p>
<p className="text-neutral-500 text-xs font-medium">
{new Date().toLocaleDateString('id-ID', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })}
</p>
</div>
<Button type="button" size="sm" onClick={onBook}>
Book Your Senpai!
@@ -16,7 +16,39 @@ export const Components: FC = () => {
const { id } = useParams()
const { data: mentor, isLoading, isError } = useGetMentorDetail(id ?? '')
const resume = mentor?.bio ?? ''
const buildResume = (): string => {
if (mentor?.bio) return mentor.bio
if (!mentor) return ''
const parts: string[] = []
const role = mentor.current_role
const company = mentor.current_company
const years = mentor.years_of_experience
const expertise = mentor.expertise
const industries = mentor.industries
const languages = mentor.languages
if (role || company) {
parts.push(`${role || 'Mentor'}${company && role ? ' at ' : company ? ' di ' : ''}${company || ''}.`)
}
if (years > 0) {
parts.push(`Memiliki pengalaman ${years} tahun di industri IT.`)
}
if (expertise.length > 0) {
parts.push(`Keahlian utama: ${expertise.join(', ')}.`)
}
if (industries.length > 0) {
parts.push(`Bidang yang digeluti: ${industries.join(', ')}.`)
}
if (languages.length > 0) {
parts.push(`Bahasa yang dikuasai: ${languages.join(', ')}.`)
}
if (parts.length === 0) {
return 'Profil senpai ini sedang dilengkapi. Segera cek kembali!'
}
return parts.join(' ')
}
const resume = buildResume()
return (
<main>
@@ -46,7 +78,7 @@ export const Components: FC = () => {
<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]">
{resume || 'Belum ada deskripsi dari senpai ini.'}
{resume || 'Profil senpai ini sedang dilengkapi. Segera cek kembali!'}
</p>
</div>
@@ -0,0 +1,69 @@
import { FC } from 'react';
import { cn } from '@imphnen-frontend-service/utils';
type TMentorAvatarProps = {
name: string;
size?: 'sm' | 'md' | 'lg';
className?: string;
textClassName?: string;
};
// Warna gradient unik per nama — hash nama → pasangan warna dari palet brand
const GRADIENTS: [string, string][] = [
['#23a1eb', '#0b5b8c'],
['#5b8def', '#3a3f8f'],
['#14b8a6', '#0f766e'],
['#8b5cf6', '#5b21b6'],
['#f59e0b', '#b45309'],
['#ef4444', '#991b1b'],
['#ec4899', '#9d174d'],
['#06b6d4', '#0e7490'],
];
const SIZES: Record<'sm' | 'md' | 'lg', string> = {
sm: 'size-[60px] text-lg md:size-10 lg:size-12',
md: 'size-16 text-xl md:size-20',
lg: 'size-24 text-3xl md:size-32 lg:size-40 md:text-4xl lg:text-5xl',
};
const getInitials = (name: string): string => {
const parts = name.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return '?';
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
};
const getGradient = (name: string): string => {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = (hash * 31 + name.charCodeAt(i)) >>> 0;
}
const [from, to] = GRADIENTS[hash % GRADIENTS.length];
return `linear-gradient(135deg, ${from} 0%, ${to} 100%)`;
};
export const MentorAvatar: FC<TMentorAvatarProps> = ({
name,
size = 'md',
className,
textClassName,
}) => {
const displayName = name || 'Mentor';
return (
<div
className={cn(
'flex items-center justify-center rounded-md overflow-hidden shrink-0 select-none',
SIZES[size],
className
)}
style={{ background: getGradient(displayName) }}
aria-label={`Avatar ${displayName}`}
>
<span
className={cn('font-semibold text-white drop-shadow-sm', textClassName)}
>
{getInitials(displayName)}
</span>
</div>
);
};
@@ -3,6 +3,7 @@ import { For } from '@imphnen-frontend-service/utils';
import { TMentorDetail } from '@imphnen-frontend-service/service';
import { FC } from 'react';
import { Link } from 'react-router-dom';
import { MentorAvatar } from './mentor-avatar';
type TMentorCardProps = {
mentor: TMentorDetail;
@@ -21,9 +22,11 @@ export const MentorCard: FC<TMentorCardProps> = ({ mentor }) => {
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">
<img src="/image/testimonial.webp" alt={mentor.fullname ?? 'Mentor'} className="w-full object-cover" />
</div>
<MentorAvatar
name={mentor.fullname ?? mentor.legal_name ?? 'Mentor'}
size="sm"
className="md:w-full md:h-auto md:aspect-square"
/>
<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">
{experience}