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:
+1
-1
@@ -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} />}
|
||||
|
||||
+7
-1
@@ -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]">
|
||||
|
||||
+15
-4
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user