Compare commits

...
2 Commits
12 changed files with 626 additions and 357 deletions
File diff suppressed because one or more lines are too long
@@ -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<ArticleCardProps> = ({ 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<ArticleCardProps> = ({ 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 (
<div
className={cn(
@@ -22,25 +36,25 @@ export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default' }) => {
)}
>
<img
src="/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp"
alt="Cover's Article"
src={cover ?? "/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp"}
alt={title}
className={cn("w-full object-cover", variant === "recent" && "h-full")}
/>
</div>
<div className={cn("leading-tight", variant === "recent" && "xl:flex-1")}>
<div className="w-max bg-primary-200 text-primary-600 px-3 py-1 text-xs font-medium rounded-4xl mb-4 xl:mb-[15px]">
5 min read
{article?.category ?? "Artikel"}
</div>
<Link
to="/articles/detail"
to={slug ? `/articles/${slug}` : "/articles"}
className={cn(
"text-[15px] text-neutral-800 font-semibold mb-4 line-clamp-2 md:text-[19px] xl:mb-[23px]",
variant === "featured" && "text-[19px] md:text-[23px]",
variant === "recent" && "mb-7 xl:text-[27px]"
)}
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
{title}
</Link>
<Show condition={variant !== "recent"}>
<p
@@ -49,7 +63,7 @@ export const ArticleCard: FC<ArticleCardProps> = ({ 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}
</p>
</Show>
<div
@@ -59,14 +73,16 @@ export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default' }) => {
variant === "featured" && "text-[15px] md:text-[19px] xl:text-base",
)}
>
<p>Writers Name</p>
<p>14 March 2025</p>
<p>{author}</p>
<p>{date}</p>
</div>
<Show condition={variant === "default"}>
<Button type="button" size="sm" className="px-2.5 py-2">
Baca Artikel
</Button>
<Link to={slug ? `/articles/${slug}` : "/articles"}>
<Button type="button" size="sm" className="px-2.5 py-2">
Baca Artikel
</Button>
</Link>
</Show>
</div>
</div>
@@ -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<Category>('UI/UX Design')
const [activeTab, setActiveTab] = useState<string>("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
</motion.h1>
<div className="grid gap-12 xl:grid-cols-2">
<motion.div variants={childVariants}>
<ArticleCard variant="featured" />
</motion.div>
<Show
condition={!isLoading}
fallback={<p className="text-neutral-500">Memuat artikel...</p>}
>
<div className="grid gap-12 xl:grid-cols-2">
<motion.div variants={childVariants}>
<ArticleCard variant="featured" article={featured} />
</motion.div>
<div>
<motion.h1
className="text-[19px] text-neutral-800 font-semibold mb-10 md:text-[29px] xl:text-[37px] xl:mb-12"
variants={childVariants}
>
Recent Articles
</motion.h1>
<div className="grid gap-10">
<For data={Array.from({ length: 3 })}>
{(_, index) => (
<motion.div key={index} variants={childVariants}>
<ArticleCard variant="recent" />
</motion.div>
)}
</For>
<div>
<motion.h1
className="text-[19px] text-neutral-800 font-semibold mb-10 md:text-[29px] xl:text-[37px] xl:mb-12"
variants={childVariants}
>
Recent Articles
</motion.h1>
<div className="grid gap-10">
<For data={recent}>
{(article, index) => (
<motion.div key={article.id ?? index} variants={childVariants}>
<ArticleCard variant="recent" article={article} />
</motion.div>
)}
</For>
</div>
</div>
</div>
</div>
</Show>
</div>
<div className="max-w-7xl mx-auto">
@@ -86,7 +99,7 @@ export default function Components(): ReactElement {
</motion.h1>
<div className="scrollbar-hide mb-8 w-full overflow-auto mx-auto">
<motion.div className="min-w-max w-auto flex gap-x-3" variants={childVariants}>
<For data={CATEGORIES}>
<For data={tabs}>
{(category) => (
<Button
key={category}
@@ -106,15 +119,20 @@ export default function Components(): ReactElement {
</motion.div>
</div>
<div className="grid gap-8 xl:grid-cols-3">
<For data={Array.from({ length: 3 })}>
{(_, index) => (
<motion.div key={index} variants={childVariants}>
<ArticleCard />
</motion.div>
)}
</For>
</div>
<Show
condition={filtered.length > 0}
fallback={<p className="text-neutral-500 text-center py-10">Belum ada artikel di kategori ini.</p>}
>
<div className="grid gap-8 xl:grid-cols-3">
<For data={filtered}>
{(article, index) => (
<motion.div key={article.id ?? index} variants={childVariants}>
<ArticleCard article={article} />
</motion.div>
)}
</For>
</div>
</Show>
</div>
</motion.section>
</main>
@@ -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<Props> = ({ open, setOpen }) => {
export const AppointmentModal: FC<Props> = ({ open, setOpen, mentorId, mentor }) => {
const [step, setStep] = useState<Step>('topic')
const [selectedTopics, setSelectedTopics] = useState<number[]>([])
const [booking, setBooking] = useState<TBookSessionRequest | null>(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<Props> = ({ open, setOpen }) => {
<AnimatePresence>
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} />}
{step === 'schedule' && <ScheduleStep />}
{step === 'schedule' && <ScheduleStep onChange={(data) => setBooking(data)} />}
{step === 'profile' && <ProfileStep />}
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} />}
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} mentor={mentor} booking={booking} />}
{step === 'qr-payment' && <QrisPaymentStep />}
{step === 'va-payment' && <VAPaymentStep />}
{step === 'success' && <SuccessStep />}
</AnimatePresence>
{error && (
<p className="text-danger-600 text-xs mt-4 font-medium">{error}</p>
)}
</div>
<Show condition={step !== 'success'}>
@@ -148,7 +172,7 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
condition={step !== 'success'}
fallback="Halman Booking"
>
<Show condition={step !== 'payment'} fallback="Bayar Sekarang">
<Show condition={step !== 'payment'} fallback={bookMutation.isPending ? "Memproses..." : "Bayar Sekarang"}>
Selanjutnya
</Show>
</Show>
@@ -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<Props> = ({ selectedTopics }) => {
export const PaymentStep: FC<Props> = ({ selectedTopics, mentor, booking }) => {
const rate = 50000
const serviceFee = 2000
return (
<motion.div
className="bg-white px-6 py-5 rounded-md grid gap-6 md:grid-cols-2 md:gap-8 xl:grid-cols-9"
@@ -26,11 +32,16 @@ export const PaymentStep: FC<Props> = ({ selectedTopics }) => {
</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]">
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
{mentor?.fullname || "Mentor"}
</h3>
<p className="text-[10px] text-neutral-600 md:text-xs">
UI Designer at Oray orayan Studios
{mentor?.current_role || "Mentor"} at {mentor?.current_company || "IMPHNEN"}
</p>
{booking?.scheduled_at && (
<p className="text-[10px] text-neutral-500 md:text-xs mt-1">
{new Date(booking.scheduled_at).toLocaleString("id-ID", { dateStyle: "full", timeStyle: "short" })}
</p>
)}
</div>
</div>
</div>
@@ -104,14 +115,14 @@ export const PaymentStep: FC<Props> = ({ selectedTopics }) => {
</p>
<div className="grid grid-cols-2 space-y-2.5 text-[10px] text-neutral-400 font-medium md:text-xs md:space-y-3">
<p className="font-normal">Subtotal :</p>
<p className="text-right">Rp. 50.000</p>
<p className="text-right">Rp. {rate.toLocaleString("id-ID")}</p>
<p className="font-normal">Service Fee :</p>
<p className="text-right">Rp. 2.000</p>
<p className="text-right">Rp. {serviceFee.toLocaleString("id-ID")}</p>
<hr className="border-neutral-200 col-span-full mt-1" />
<p className="text-neutral-700">Total :</p>
<p className="text-right text-neutral-700">Rp. 52.000</p>
<p className="text-right text-neutral-700">Rp. {(rate + serviceFee).toLocaleString("id-ID")}</p>
</div>
</div>
</div>
@@ -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 (
<motion.div
className="bg-white px-6 py-5 rounded-md md:px-6"
@@ -31,15 +53,15 @@ 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={date} onChange={(e) => setDate(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={time} onChange={(e) => setTime(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) => setSessionType(e.target.value)}>
<option value="online">Online</option>
<option value="offline">Offline</option>
</Select>
@@ -48,7 +70,9 @@ 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) => setDescription(e.target.value)} />
</div>
</div>
</motion.div>
@@ -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<Props> = ({ 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<Props> = ({ onBook, mentorId }) => {
const { data: availability, isLoading } = useGetMentorAvailability(mentorId ?? "")
const slots = availability?.slots ?? []
const byDate = new Map<string, typeof slots>()
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 (
<div>
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Schedule</h2>
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md space-y-5">
<For data={SCHEDULES}>
{(item) => (
<div key={item.date}>
<div className="flex justify-between items-center mb-4">
<p className="text-[15px] font-medium">{item.date}</p>
<p
className={cn(
"rounded-4xl py-1 px-2.5 text-xs font-medium",
item.availableCount > 5 ? "bg-primary-200 text-primary-500" : "bg-danger-100 text-danger-600"
)}
>
{item.availableCount} slot tersisa
</p>
<Show
condition={!isLoading && days.length > 0}
fallback={
<p className="text-xs text-neutral-500">
{isLoading ? "Memuat jadwal senpai..." : "Belum ada jadwal tersedia."}
</p>
}
>
<For data={days}>
{([date, daySlots]) => (
<div key={date}>
<div className="flex justify-between items-center mb-4">
<p className="text-[15px] font-medium">{formatDate(date)}</p>
<p className="rounded-4xl py-1 px-2.5 text-xs font-medium bg-primary-200 text-primary-500">
{daySlots.length} slot tersisa
</p>
</div>
<div className="flex flex-wrap gap-x-3 gap-y-2">
<For data={daySlots}>
{(schedule) => (
<div
key={`${schedule.date}-${schedule.time}`}
className={`px-3 py-2 text-xs rounded-md font-semibold ${
schedule.available
? "bg-primary-300 text-primary-600"
: "bg-neutral-200 text-neutral-400 line-through"
}`}
>
{formatTime(schedule.time)}
</div>
)}
</For>
</div>
</div>
<div className="flex gap-x-3">
<For data={item.schedule}>
{(schedule, index) => (
<div
key={index}
className="bg-primary-300 text-primary-600 px-3 py-2 text-xs rounded-md font-semibold"
>
{schedule.time}
</div>
)}
</For>
</div>
</div>
)}
</For>
)}
</For>
</Show>
<hr className="border-primary-200" />
@@ -72,4 +81,4 @@ export const SenpaiScheduleSection: FC<Props> = ({ onBook }) => {
</div>
</div>
)
}
}
@@ -55,13 +55,13 @@ export const Components: FC = () => {
</div>
<div className="hidden xl:block xl:w-[400px]">
<SenpaiScheduleSection onBook={() => setOpen(true)} />
<SenpaiScheduleSection onBook={() => setOpen(true)} mentorId={mentor?.id} />
</div>
</div>
</div>
</section>
<AppointmentModal open={open} setOpen={setOpen} />
<AppointmentModal open={open} setOpen={setOpen} mentorId={mentor?.id} mentor={mentor} />
</Show>
</main>
)
@@ -1,11 +1,80 @@
import { FC, ReactElement, useState } from 'react';
import { ControlledInputField, RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
import { Button, Select } from '@imphnen-frontend-service/ui/atoms';
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
import { InputField, RegisterMentorStep, SelectField } from '@imphnen-frontend-service/ui/molecules';
import { usePostRegisterMentor, TMentorRegisterRequest } from '@imphnen-frontend-service/service';
import { useNavigate } from 'react-router-dom';
export const Components: FC = (): ReactElement => {
const [ step, setStep ] = useState(1)
const [step, setStep] = useState(1)
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState("")
const [form, setForm] = useState({
email: "", password: "", confirmPassword: "",
fullname: "", phone_number: "", gender: "", domicile: "",
legal_name: "", phone_for_verification: "", identity_document_url: "",
last_education: "", bio: "", linkedin_url: "", github_url: "",
cv_url: "", portfolio_url: "", industries: "", expertise: "", languages: "",
current_company: "", current_role: "", years_of_experience: "",
topics_of_interest: "", preferred_mentee_level: "", preferred_mentoring_formats: "",
availability_commitment: "", mentoring_rate_amount: "",
})
const setF = (k: string, v: string) => setForm((f) => ({ ...f, [k]: v }))
const registerMutation = usePostRegisterMentor()
const navigate = useNavigate()
const handleSubmit = async () => {
setError("")
if (form.password !== form.confirmPassword) {
setError("Password dan konfirmasi tidak sama.")
return
}
const payload: TMentorRegisterRequest = {
email: form.email,
password: form.password,
fullname: form.fullname,
phone_number: form.phone_number || null,
identity_and_verification: {
legal_name: form.legal_name || form.fullname,
gender: form.gender || null,
domicile: form.domicile || null,
identity_document_url: form.identity_document_url || "https://example.com/identity.jpg",
phone_for_verification: form.phone_for_verification || null,
},
professional_profile: {
bio: form.bio,
last_education: form.last_education || null,
linkedin_url: form.linkedin_url || null,
github_url: form.github_url || null,
cv_url: form.cv_url || null,
portfolio_url: form.portfolio_url || null,
industries: form.industries.split(",").map((s) => s.trim()).filter(Boolean),
expertise: form.expertise.split(",").map((s) => s.trim()).filter(Boolean),
languages: form.languages.split(",").map((s) => s.trim()).filter(Boolean),
current_company: form.current_company,
current_role: form.current_role,
years_of_experience: Number(form.years_of_experience) || 0,
},
mentoring_logistics: {
topics_of_interest: form.topics_of_interest.split(",").map((s) => s.trim()).filter(Boolean),
preferred_mentee_level: [form.preferred_mentee_level].filter(Boolean),
preferred_mentoring_formats: form.preferred_mentoring_formats.split(",").map((s) => s.trim()).filter(Boolean),
availability_commitment: form.availability_commitment,
mentoring_rate_amount: Number(form.mentoring_rate_amount) || 0,
},
}
setSubmitting(true)
try {
await registerMutation.mutateAsync(payload)
navigate('/auth/register-mentor/pending')
} catch (e: unknown) {
const msg = (e as { response?: { data?: { message?: string } } })?.response?.data?.message
setError(msg || "Gagal mendaftar sebagai mentor. Coba lagi.")
} finally {
setSubmitting(false)
}
}
return (
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
@@ -13,13 +82,6 @@ export const Components: FC = (): ReactElement => {
<RegisterResetBanner />
<div className="xl:border-2 xl:border-primary-500/50 xl:w-[726px] rounded-lg py-[32px] px-[48px] flex justify-center">
<div className="xl:w-[714px]">
<Button
className='xl:hidden gap-3'
variant='secondary'
>
<ArrowLeftOutlined />
Login
</Button>
<RegisterMentorStep step={step} />
<h3 className="mt-5 text-3xl font-semibold text-primary-500">
Register
@@ -27,60 +89,44 @@ export const Components: FC = (): ReactElement => {
<h5 className="text-primary-500 font-medium">
Welcome to the team, Mentor - powered by IMPHNEN
</h5>
<form>
<form onSubmit={(e) => e.preventDefault()}>
<div className={`${step !== 1 ? 'hidden' : ''}`}>
<div className='mt-7 mb-2'>
<InputField
label="Nama Lengkap"
size="lg"
className="w-full"
placeholder="Nama Lengkap"
name={'fullname'}
label="Nama Lengkap" size="lg" className="w-full"
placeholder="Nama Lengkap" name="fullname"
value={form.fullname} onChange={(e) => setF('fullname', e.target.value)}
/>
</div>
<SelectField
label="Jenis Kelamin"
defaultValue=""
size="lg"
error=""
label="Jenis Kelamin" defaultValue="" size="lg" error=""
value={form.gender} onChange={(e) => setF('gender', e.target.value)}
>
<option value="" disabled hidden>
Pilih Jenis Kelamin
</option>
<option value="laki-laki">Laki - Laki</option>
<option value="perempuan">Perempuan</option>
<option value="" disabled hidden>Pilih Jenis Kelamin</option>
<option value="Laki-laki">Laki - Laki</option>
<option value="Perempuan">Perempuan</option>
</SelectField>
<div className='my-2'>
<InputField
label="No Telp/Whatsapp Only"
size="lg"
className="w-full"
placeholder="Nama Lengkap"
name={'fullname'}
label="No Telp/Whatsapp Only" size="lg" className="w-full"
placeholder="Nomor WhatsApp" name="phone_number"
value={form.phone_number} onChange={(e) => setF('phone_number', e.target.value)}
/>
</div>
<SelectField
label="Domisili"
defaultValue=""
size="lg"
error=""
label="Domisili" defaultValue="" size="lg" error=""
value={form.domicile} onChange={(e) => setF('domicile', e.target.value)}
>
<option value="" disabled hidden>
Pilih Domisili
</option>
<option value="aceh">Aceh</option>
<option value="jawa-selatan">Jawa Selatan</option>
<option value="" disabled hidden>Pilih Domisili</option>
<option value="Aceh">Aceh</option>
<option value="Jawa Barat">Jawa Barat</option>
</SelectField>
<div className='mt-2'>
<SelectField
label="Pendidikan Terakhir"
defaultValue=""
size="lg"
error=""
label="Pendidikan Terakhir" defaultValue="" size="lg" error=""
value={form.last_education} onChange={(e) => setF('last_education', e.target.value)}
>
<option value="" disabled hidden>
Apa pendidikan terakhir senpai
</option>
<option value="" disabled hidden>Apa pendidikan terakhir senpai</option>
<option value="SMA">SMA</option>
<option value="S1">S1</option>
<option value="S2">S2</option>
@@ -90,145 +136,85 @@ export const Components: FC = (): ReactElement => {
</div>
<div className={`${step !== 2 ? 'hidden' : ''}`}>
<div className='mt-7 mb-2 gap-2 flex flex-col'>
<InputField
label="Perusahaan Saat Ini"
size="lg"
className="w-full"
placeholder="Masukkan Perusahaan tempat kerja, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
name={'fullname'}
/>
<InputField
label="Masukan jabatan senpai di perusahaan"
size="lg"
className="w-full"
placeholder="Masukan jabatan senpai di perusahaan"
name={'fullname'}
/>
<SelectField
label="Topik Keahlian Yang Ingin Diajarkan"
defaultValue=""
size="lg"
error=""
>
<option value="" disabled hidden>
Apa yang ingin anda ajarkan senpaii
</option>
<option value="Frontend Developer">Frontend Developer</option>
<option value="Backend Developer">Backend Developer</option>
<option value="Other">Other</option>
</SelectField>
<InputField
label="Jika Mengisi “Other”, Tulis Topik yang Kamu Kuasai"
size="lg"
className="w-full"
placeholder="Tolong isi jika tidak ada di pilihan senpai"
name={'fullname'}
/>
<InputField
label="Skill Utama yang Kamu Miliki (boleh lebih dari satu)"
size="lg"
className="w-full"
placeholder="Isi Skill nya dong senpai"
name={'fullname'}
/>
<InputField label="Perusahaan Saat Ini" size="lg" className="w-full"
placeholder="Masukkan Perusahaan tempat kerja, Senpai~! ✨" name="current_company"
value={form.current_company} onChange={(e) => setF('current_company', e.target.value)} />
<InputField label="Masukan jabatan senpai di perusahaan" size="lg" className="w-full"
placeholder="Jabatan" name="current_role"
value={form.current_role} onChange={(e) => setF('current_role', e.target.value)} />
<InputField label="Topik Keahlian Yang Ingin Diajarkan (pisahkan koma)" size="lg" className="w-full"
placeholder="mis. Backend Developer, Rust" name="expertise"
value={form.expertise} onChange={(e) => setF('expertise', e.target.value)} />
<InputField label="Industri (pisahkan koma)" size="lg" className="w-full"
placeholder="mis. Software, Education" name="industries"
value={form.industries} onChange={(e) => setF('industries', e.target.value)} />
<InputField label="Skill Utama / Bahasa (pisahkan koma)" size="lg" className="w-full"
placeholder="mis. Rust, TypeScript" name="languages"
value={form.languages} onChange={(e) => setF('languages', e.target.value)} />
<InputField label="Tahun Pengalaman" type="number" size="lg" className="w-full"
placeholder="mis. 3" name="years_of_experience"
value={form.years_of_experience} onChange={(e) => setF('years_of_experience', e.target.value)} />
<InputField label="Bio Singkat" size="lg" className="w-full"
placeholder="Ceritakan pengalaman Anda" name="bio"
value={form.bio} onChange={(e) => setF('bio', e.target.value)} />
</div>
</div>
<div className={`${step !== 3 ? 'hidden' : ''}`}>
<div className='mt-7 mb-2 gap-2 flex flex-col'>
<SelectField
label="Durasi Sesi Mentoring"
defaultValue=""
size="lg"
error=""
>
<option value="" disabled hidden>
Placeholder
</option>
<InputField label="Topik Mentoring (pisahkan koma)" size="lg" className="w-full"
placeholder="mis. Rust Programming, Backend" name="topics_of_interest"
value={form.topics_of_interest} onChange={(e) => setF('topics_of_interest', e.target.value)} />
<SelectField label="Tingkat Mentee" defaultValue="" size="lg" error=""
value={form.preferred_mentee_level} onChange={(e) => setF('preferred_mentee_level', e.target.value)}>
<option value="" disabled hidden>Pilih tingkat mentee</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</SelectField>
<SelectField
label="Format Pengajaran yang Anda Sediakan"
defaultValue=""
size="lg"
error=""
>
<option value="" disabled hidden>
Placeholder
</option>
</SelectField>
<InputField
type="file"
label="Unggah Curiculum Vitae"
size="lg"
className="w-full"
/>
<InputField
label="Unggah Portofolio"
size="lg"
className="w-full"
placeholder="Google Drive, Behance, GitHub, Notion, dan lainnya"
/>
<InputField
type='number'
label="Honorarium yang Diharapkan per Sesi (Online) Durasi rata-rata sesi: 2,53 jam"
size="lg"
className="w-full"
placeholder="Isi Nominalnya dong senpaiii!!!"
/>
<InputField label="Format Pengajaran (pisahkan koma, mis. online,offline)" size="lg" className="w-full"
placeholder="online,offline" name="preferred_mentoring_formats"
value={form.preferred_mentoring_formats} onChange={(e) => setF('preferred_mentoring_formats', e.target.value)} />
<InputField label="Komitmen Ketersediaan" size="lg" className="w-full"
placeholder="mis. 2 jam per minggu" name="availability_commitment"
value={form.availability_commitment} onChange={(e) => setF('availability_commitment', e.target.value)} />
<InputField label="Honorarium yang Diharapkan per Sesi" type="number" size="lg" className="w-full"
placeholder="mis. 500000" name="mentoring_rate_amount"
value={form.mentoring_rate_amount} onChange={(e) => setF('mentoring_rate_amount', e.target.value)} />
<InputField label="URL LinkedIn" size="lg" className="w-full"
placeholder="https://linkedin.com/in/..." name="linkedin_url"
value={form.linkedin_url} onChange={(e) => setF('linkedin_url', e.target.value)} />
<InputField label="URL Portofolio" size="lg" className="w-full"
placeholder="Google Drive, GitHub, Behance" name="portfolio_url"
value={form.portfolio_url} onChange={(e) => setF('portfolio_url', e.target.value)} />
<h6 className='text-xs'>Silahkan isi dalam angka (Contoh:500.000)</h6>
</div>
</div>
<div className={`${step !== 4 ? 'hidden' : ''}`}>
<div className='mt-7 mb-2 gap-2 flex flex-col'>
<InputField
type='email'
label="Email"
size="lg"
className="w-full"
placeholder="isi emailnya dong senpai!!!"
/>
<InputField
type='password'
label="Buat Password"
size="lg"
className="w-full"
placeholder="Buat Password"
/>
<InputField
type='password'
label="Buat Password"
size="lg"
className="w-full"
placeholder="Buat Password"
/>
<InputField type='email' label="Email" size="lg" className="w-full"
placeholder="isi emailnya dong senpai!!!" name="email"
value={form.email} onChange={(e) => setF('email', e.target.value)} />
<InputField type='password' label="Buat Password" size="lg" className="w-full"
placeholder="Buat Password" name="password"
value={form.password} onChange={(e) => setF('password', e.target.value)} />
<InputField type='password' label="Konfirmasi Password" size="lg" className="w-full"
placeholder="Ulangi Password" name="confirmPassword"
value={form.confirmPassword} onChange={(e) => setF('confirmPassword', e.target.value)} />
</div>
</div>
{error && <p className="text-danger-600 text-xs mt-3 font-medium">{error}</p>}
<div className='flex justify-end mt-4 gap-2'>
<Button
variant='secondary'
onClick={() => setStep(step - 1)}
type='button'
className={`${step === 1 ? 'hidden' : ''}`}
>
<ArrowLeftOutlined />
Kembali
<Button variant='secondary' onClick={() => setStep(step - 1)} type='button'
className={`${step === 1 ? 'hidden' : ''}`}>
<ArrowLeftOutlined /> Kembali
</Button>
<Button
variant='primary'
onClick={() => setStep(step + 1)}
type='button'
className={`${step === 4 ? 'hidden' : ''}`}
>
Lanjutkan
<ArrowRightOutlined />
<Button variant='primary' onClick={() => setStep(step + 1)} type='button'
className={`${step === 4 ? 'hidden' : ''}`}>
Lanjutkan <ArrowRightOutlined />
</Button>
<Button
variant='primary'
onClick={() => setStep(step + 1)}
type='button'
className={`${step === 4 ? '' : 'hidden'}`}
>
Selesaikan
<ArrowRightOutlined />
<Button variant='primary' onClick={handleSubmit} type='button'
className={`${step === 4 ? '' : 'hidden'}`} disabled={submitting}>
{submitting ? "Mengirim..." : "Selesaikan"} <ArrowRightOutlined />
</Button>
</div>
</form>
@@ -239,4 +225,4 @@ export const Components: FC = (): ReactElement => {
);
};
export default Components;
export default Components;
+50
View File
@@ -1,8 +1,12 @@
import { api } from '../';
import {
TArticleDetail,
TArticleListItem,
TArticleListParams,
TMentorAvailability,
TMentorDetail,
TMentorListParams,
TMentorRegisterRequest,
TSessionFeedbackRequest,
TSessionListResponse,
TBookSessionRequest,
@@ -95,3 +99,49 @@ export const postSessionFeedback = async (
});
return data;
};
export const getArticles = async (
params?: TArticleListParams
): Promise<TArticleListItem[]> => {
const { data } = await api({
method: 'GET',
url: '/dimentorin/articles',
params,
});
return data.data;
};
export const getArticleCategories = async (): Promise<string[]> => {
const { data } = await api({
method: 'GET',
url: '/dimentorin/articles/categories',
});
return data.data;
};
export const getArticleBySlug = async (slug: string): Promise<TArticleDetail> => {
const { data } = await api({
method: 'GET',
url: `/dimentorin/articles/slug/${slug}`,
});
return data.data;
};
export const getArticleById = async (id: string): Promise<TArticleDetail> => {
const { data } = await api({
method: 'GET',
url: `/dimentorin/articles/${id}`,
});
return data.data;
};
export const postRegisterMentor = async (
payload: TMentorRegisterRequest
): Promise<TResponseMessage> => {
const { data } = await api({
method: 'POST',
url: '/dimentorin/mentors/create',
data: payload,
});
return data;
};
@@ -1,5 +1,9 @@
import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query';
import {
getArticleById,
getArticleBySlug,
getArticleCategories,
getArticles,
getMentorAvailability,
getMentorDetail,
getMentorMe,
@@ -8,13 +12,18 @@ import {
getMentors,
getMySessions,
postBookSession,
postRegisterMentor,
postSessionFeedback,
} from '../../api/dimentorin';
import {
TArticleDetail,
TArticleListItem,
TArticleListParams,
TBookSessionRequest,
TMentorAvailability,
TMentorDetail,
TMentorListParams,
TMentorRegisterRequest,
TSessionFeedbackRequest,
TSessionListResponse,
} from '../../types/dimentorin';
@@ -105,4 +114,51 @@ export const usePostSessionFeedback = (sessionId: string) => {
});
};
export const usePostRegisterMentor = () => {
return useMutation({
mutationKey: ['post-register-mentor'],
mutationFn: async (payload: TMentorRegisterRequest) =>
await postRegisterMentor(payload),
});
};
export const useGetArticles = (
params?: TArticleListParams
): UseQueryResult<TArticleListItem[], TResponseError> => {
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<TArticleDetail, TResponseError> => {
return useQuery({
queryKey: ['get-article-by-slug', slug],
queryFn: async () => await getArticleBySlug(slug),
enabled: !!slug,
});
};
export const useGetArticleById = (
id: string
): UseQueryResult<TArticleDetail, TResponseError> => {
return useQuery({
queryKey: ['get-article-by-id', id],
queryFn: async () => await getArticleById(id),
enabled: !!id,
});
};
export type { TResponseMessage };
@@ -97,3 +97,61 @@ 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;
};
export type TMentorRegisterRequest = {
email: string;
password: string;
fullname: string;
phone_number?: string | null;
identity_and_verification: {
legal_name: string;
gender?: string | null;
domicile?: string | null;
identity_document_url: string;
phone_for_verification?: string | null;
};
professional_profile: {
bio: string;
last_education?: string | null;
linkedin_url?: string | null;
github_url?: string | null;
cv_url?: string | null;
portfolio_url?: string | null;
industries: string[];
expertise: string[];
languages: string[];
current_company: string;
current_role: string;
years_of_experience: number;
};
mentoring_logistics: {
topics_of_interest: string[];
preferred_mentee_level: string[];
preferred_mentoring_formats: string[];
availability_commitment: string;
mentoring_rate_amount: number;
};
};