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>
|
<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 === 'schedule' && <ScheduleStep onChange={(data) => setBooking(data)} />}
|
||||||
{step === 'profile' && <ProfileStep />}
|
{step === 'profile' && <ProfileStep />}
|
||||||
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} mentor={mentor} booking={booking} onMethodChange={setPaymentMethod} />}
|
{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 { motion } from "framer-motion"
|
||||||
import { TOPICS } from "../../../sections/topics"
|
import { TOPICS } from "../../../sections/topics"
|
||||||
import { TBookSessionRequest } from "@imphnen-frontend-service/service"
|
import { TBookSessionRequest } from "@imphnen-frontend-service/service"
|
||||||
|
import { MentorAvatar } from "../../../../../_components/mentor-avatar"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedTopics: number[]
|
selectedTopics: number[]
|
||||||
@@ -42,7 +43,12 @@ export const PaymentStep: FC<Props> = ({ selectedTopics, mentor, booking, onMeth
|
|||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<div className="aspect-square mb-2 size-10 mx-auto rounded-full overflow-hidden md:size-20 md:ms-0">
|
<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>
|
||||||
<div className="w-44 mx-auto text-center md:ms-0 md:text-left md:mb-2 md:text-[15px] md:w-auto">
|
<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]">
|
<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 { FC } from "react"
|
||||||
import { motion } from "framer-motion"
|
import { motion } from "framer-motion"
|
||||||
import { TOPICS } from "../../../sections/topics"
|
import { TOPICS } from "../../../sections/topics"
|
||||||
|
import { MentorAvatar } from "../../../../../_components/mentor-avatar"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedTopics: number[]
|
selectedTopics: number[]
|
||||||
setSelectedTopics: (topics: number[]) => void
|
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) => {
|
const handleSelectTopic = (topicId: number) => {
|
||||||
if (selectedTopics.includes(topicId)) {
|
if (selectedTopics.includes(topicId)) {
|
||||||
setSelectedTopics(selectedTopics.filter((id) => id !== topicId))
|
setSelectedTopics(selectedTopics.filter((id) => id !== topicId))
|
||||||
@@ -27,14 +33,19 @@ export const TopicStep: FC<Props> = ({ selectedTopics, setSelectedTopics }) => {
|
|||||||
exit={{ opacity: 0, y: 20 }}
|
exit={{ opacity: 0, y: 20 }}
|
||||||
>
|
>
|
||||||
<div className="w-16 aspect-4/5 overflow-hidden md:w-[134px] xl:w-[140px]">
|
<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>
|
||||||
<div className="flex-1 pe-4">
|
<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]">
|
<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>
|
</h3>
|
||||||
<p className="text-[10px] line-clamp-2 text-neutral-600 md:text-[15px] xl:text-[19px]">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { StarFilled } from "@ant-design/icons"
|
|||||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
import { TMentorDetail } from "@imphnen-frontend-service/service"
|
import { TMentorDetail } from "@imphnen-frontend-service/service"
|
||||||
|
import { MentorAvatar } from "../../../_components/mentor-avatar"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onBook: () => void
|
onBook: () => void
|
||||||
@@ -31,7 +32,12 @@ export const ProfileSection: React.FC<Props> = ({ onBook, mentor }) => {
|
|||||||
"xl:aspect-5/6 md:h-60 md:rounded-lg"
|
"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>
|
||||||
|
|
||||||
<div className="md:max-w-[242px] xl:max-w-[306px]">
|
<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 className="hidden md:flex xl:hidden justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-primary-500 text-[15px] font-semibold">Jum, 4 April 2025</p>
|
<p className="text-primary-500 text-[15px] font-semibold">
|
||||||
<p className="text-neutral-500 text-xs font-medium">Jum, 4 April 2025</p>
|
{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>
|
</div>
|
||||||
<Button type="button" size="sm" onClick={onBook}>
|
<Button type="button" size="sm" onClick={onBook}>
|
||||||
Book Your Senpai!
|
Book Your Senpai!
|
||||||
|
|||||||
@@ -16,7 +16,39 @@ export const Components: FC = () => {
|
|||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { data: mentor, isLoading, isError } = useGetMentorDetail(id ?? '')
|
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 (
|
return (
|
||||||
<main>
|
<main>
|
||||||
@@ -46,7 +78,7 @@ export const Components: FC = () => {
|
|||||||
<div className="px-6 py-8 rounded-md shadow-md">
|
<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>
|
<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]">
|
<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>
|
</p>
|
||||||
</div>
|
</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 { TMentorDetail } from '@imphnen-frontend-service/service';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { MentorAvatar } from './mentor-avatar';
|
||||||
|
|
||||||
type TMentorCardProps = {
|
type TMentorCardProps = {
|
||||||
mentor: TMentorDetail;
|
mentor: TMentorDetail;
|
||||||
@@ -21,9 +22,11 @@ export const MentorCard: FC<TMentorCardProps> = ({ mentor }) => {
|
|||||||
|
|
||||||
return (
|
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="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">
|
<MentorAvatar
|
||||||
<img src="/image/testimonial.webp" alt={mentor.fullname ?? 'Mentor'} className="w-full object-cover" />
|
name={mentor.fullname ?? mentor.legal_name ?? 'Mentor'}
|
||||||
</div>
|
size="sm"
|
||||||
|
className="md:w-full md:h-auto md:aspect-square"
|
||||||
|
/>
|
||||||
<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">
|
<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}
|
{experience}
|
||||||
|
|||||||
Reference in New Issue
Block a user