Compare commits
2
Commits
ce0579de5d
...
6c5c5db47b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c5c5db47b | ||
|
|
583b6b224e |
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>Writer’s 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>
|
||||
|
||||
+29
-5
@@ -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>
|
||||
|
||||
+17
-6
@@ -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>
|
||||
|
||||
+30
-6
@@ -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>
|
||||
|
||||
+63
-54
@@ -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,5–3 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;
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user