Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce0579de5d | ||
|
|
7df4592a47 | ||
|
|
77eb755783 | ||
|
|
8c6a85090d | ||
|
|
a6134cb32a | ||
|
|
d9c5ff0b18 | ||
|
|
924354366c | ||
|
|
dce74c5c7c | ||
|
|
d4c263cad3 | ||
|
|
228f92e9ed | ||
|
|
dcd4ef88d6 | ||
|
|
3e232dcdc2 |
@@ -0,0 +1,35 @@
|
|||||||
|
name: Deploy Backoffice
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/backoffice/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-backoffice ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-backoffice
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-backoffice && git pull
|
||||||
|
fi
|
||||||
|
echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" > ~/imphnen-frontend-service-backoffice/apps/backoffice/.env
|
||||||
|
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml down || true
|
||||||
|
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml build
|
||||||
|
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: Deploy Dimentorin
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/dimentorin/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-dimentorin ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-dimentorin
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-dimentorin && git pull
|
||||||
|
fi
|
||||||
|
echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" > ~/imphnen-frontend-service-dimentorin/apps/dimentorin/.env
|
||||||
|
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml down || true
|
||||||
|
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml build
|
||||||
|
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: Deploy Gacha
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/gacha/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-gacha ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-gacha
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-gacha && git pull
|
||||||
|
fi
|
||||||
|
echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" > ~/imphnen-frontend-service-gacha/apps/gacha/.env
|
||||||
|
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml down || true
|
||||||
|
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml build
|
||||||
|
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -2,6 +2,8 @@ name: Deploy Landing
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
paths:
|
paths:
|
||||||
- 'apps/landing/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
- 'apps/landing/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -3,7 +3,7 @@ import { motion } from 'framer-motion'
|
|||||||
|
|
||||||
export function HeroSection() {
|
export function HeroSection() {
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full py-20 md:py-28 lg:py-32">
|
<section className="relative w-full z-0 py-20 md:py-28 lg:py-32">
|
||||||
{/* Background gradients and shapes */}
|
{/* Background gradients and shapes */}
|
||||||
<div className="absolute inset-0 -z-10 overflow-x-clip hidden md:block">
|
<div className="absolute inset-0 -z-10 overflow-x-clip hidden md:block">
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { CloseOutlined, MenuOutlined } from "@ant-design/icons";
|
|||||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
import { cn, For, Show } from "@imphnen-frontend-service/utils";
|
import { cn, For, Show } from "@imphnen-frontend-service/utils";
|
||||||
import { motion, useMotionValueEvent, useScroll, Variants } from "framer-motion";
|
import { motion, useMotionValueEvent, useScroll, Variants } from "framer-motion";
|
||||||
import { FC, useState } from "react";
|
import { FC, useMemo, useState } from "react";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, NavLink, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
const MENUS: { label: string; href: string }[] = [
|
const MENUS: { label: string; href: string }[] = [
|
||||||
{ label: "Home", href: "/" },
|
{ label: "Home", href: "/" },
|
||||||
@@ -30,8 +30,15 @@ export const Header: FC = () => {
|
|||||||
else setShow(true)
|
else setShow(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isActive = useMemo(() => {
|
||||||
|
return (path: string) => {
|
||||||
|
if (path === '/' && location.pathname !== '/') return false
|
||||||
|
return location.pathname.includes(path)
|
||||||
|
}
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-8 pt-8 top-0 z-50 md:px-[60px] md:pt-[60px] lg:px-20 sticky overflow-hidden">
|
<div className={cn("w-full px-8 pt-8 top-0 z-50 md:px-[60px] md:pt-[60px] lg:px-20 sticky", !show && "overflow-hidden")}>
|
||||||
<motion.header
|
<motion.header
|
||||||
className="bg-white shadow-lg rounded-lg h-12 flex justify-between w-full max-w-7xl xl:mx-auto md:h-[60px] lg:h-[71px]"
|
className="bg-white shadow-lg rounded-lg h-12 flex justify-between w-full max-w-7xl xl:mx-auto md:h-[60px] lg:h-[71px]"
|
||||||
aria-roledescription="nav"
|
aria-roledescription="nav"
|
||||||
@@ -63,18 +70,22 @@ export const Header: FC = () => {
|
|||||||
variant="text"
|
variant="text"
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-10 py-1.5 text-neutral-300 hover:text-neutral-400 lg:px-2.5 lg:py-2",
|
"px-10 py-1.5 text-neutral-300 hover:text-neutral-400 lg:px-2.5 lg:py-2",
|
||||||
location.pathname === menu.href && "text-primary-500 hover:text-primary-500"
|
isActive(menu.href) && "text-primary-500 hover:text-primary-500"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link to={menu.href}>{menu.label}</Link>
|
<NavLink to={menu.href}>{menu.label}</NavLink>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Button type="button" className="px-12 py-1 md:hidden">Login</Button>
|
<Link to="/auth/login">
|
||||||
|
<Button type="button" className="px-12 py-1 lg:hidden">Login</Button>
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
<Link to="/auth/login">
|
||||||
<Button type="button" className="px-5 py-2 hidden lg:block">Login</Button>
|
<Button type="button" className="px-5 py-2 hidden lg:block">Login</Button>
|
||||||
|
</Link>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="text"
|
variant="text"
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
export type ArticleCardProps = {
|
||||||
|
variant?: "default" | "recent" | "featured"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default' }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
variant === "recent" && "md:grid md:grid-cols-2 md:gap-7 xl:flex xl:items-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-full h-auto aspect-16/10 overflow-hidden mb-8 md:h-60 md:w-full xl:aspect-auto",
|
||||||
|
variant === "featured" && "md:h-[300px]",
|
||||||
|
variant === "recent" && "md:h-auto md:w-full md:aspect-15/7 md:mb-0 xl:aspect-square xl:w-[162px] xl:h-auto",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp"
|
||||||
|
alt="Cover's Article"
|
||||||
|
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
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
to="/articles/detail"
|
||||||
|
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.
|
||||||
|
</Link>
|
||||||
|
<Show condition={variant !== "recent"}>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-xs text-neutral-600 mb-6 md:text-[15px] md:mb-8 xl:text-[19px]",
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex justify-between items-center text-xs text-neutral-600 md:text-[15px] xl:text-[19px]",
|
||||||
|
variant === "default" && "mb-4",
|
||||||
|
variant === "featured" && "text-[15px] md:text-[19px] xl:text-base",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p>Writer’s Name</p>
|
||||||
|
<p>14 March 2025</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show condition={variant === "default"}>
|
||||||
|
<Button type="button" size="sm" className="px-2.5 py-2">
|
||||||
|
Baca Artikel
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { ReactElement, useRef, useState } from "react";
|
||||||
|
import { ArticleCard } from "./_components/card/article";
|
||||||
|
import { cn, For } 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]
|
||||||
|
|
||||||
|
export default function Components(): ReactElement {
|
||||||
|
const [activeTab, setActiveTab] = useState<Category>('UI/UX Design')
|
||||||
|
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main ref={ref}>
|
||||||
|
<motion.section
|
||||||
|
className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto mb-8 md:mb-20">
|
||||||
|
<motion.h1
|
||||||
|
className="text-[23px] font-semibold text-neutral-800 mb-9 md:text-[29px] xl:text-[46px] xl:font-bold"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Featuring Articles
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<div className="grid gap-12 xl:grid-cols-2">
|
||||||
|
<motion.div variants={childVariants}>
|
||||||
|
<ArticleCard variant="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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<motion.h1
|
||||||
|
className="text-[19px] text-neutral-800 font-semibold mb-4 md:text-[23px] xl:text-[29px]"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Category
|
||||||
|
</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}>
|
||||||
|
{(category) => (
|
||||||
|
<Button
|
||||||
|
key={category}
|
||||||
|
type="button"
|
||||||
|
variant="text"
|
||||||
|
onClick={() => setActiveTab(category)}
|
||||||
|
className={cn(
|
||||||
|
"relative text-[10px] text-neutral-400 font-medium px-3 py-2 rounded-4xl md:text-xs md:font-semibold xl:text-[15px]",
|
||||||
|
"before:w-0 before:absolute before:h-0.5 before:mx-auto before:inset-x-0 before:bg-primary-500 hover:before:w-full before:bottom-0 before:left-0 before:transition-all before:duration-300",
|
||||||
|
activeTab === category && "text-primary-500 before:w-full"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ export default function Layout() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col bg-primary-50">
|
<div className="flex min-h-screen flex-col bg-primary-50">
|
||||||
<Header />
|
<Header />
|
||||||
<main className="flex-1 z-0">
|
<main className="flex-1">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { CloseOutlined } from "@ant-design/icons"
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
|
import { FC, useCallback, useEffect, useState } from "react"
|
||||||
|
import { TopicStep } from "./steps/topic"
|
||||||
|
import { cn, Show } from "@imphnen-frontend-service/utils"
|
||||||
|
import { ScheduleStep } from "./steps/schedule"
|
||||||
|
import { ProfileStep } from "./steps/profile"
|
||||||
|
import { QrisPaymentStep } from "./steps/qris-payement"
|
||||||
|
import { VAPaymentStep } from "./steps/va-payment"
|
||||||
|
import { SuccessStep } from "./steps/success"
|
||||||
|
import { PaymentStep } from "./steps/payment"
|
||||||
|
|
||||||
|
const STEPS = ['topic', 'schedule', 'profile', 'payment', 'qr-payment', 'va-payment', 'success'] as const
|
||||||
|
type Step = typeof STEPS[number]
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
||||||
|
const [step, setStep] = useState<Step>('topic')
|
||||||
|
const [selectedTopics, setSelectedTopics] = useState<number[]>([])
|
||||||
|
|
||||||
|
const handleStep = (action: 'next' | 'prev') => {
|
||||||
|
if (action === 'next' && step === 'success') {
|
||||||
|
setOpen(false)
|
||||||
|
} else if (action === 'next') {
|
||||||
|
setStep(STEPS[STEPS.indexOf(step) + 1])
|
||||||
|
} else if (action === 'prev' && step !== 'topic') {
|
||||||
|
setStep(STEPS[STEPS.indexOf(step) - 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEscapeKey = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape" && open) setOpen(false)
|
||||||
|
},
|
||||||
|
[open, setOpen]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
document.body.style.overflow = "hidden"
|
||||||
|
window.addEventListener("keydown", handleEscapeKey)
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = ""
|
||||||
|
window.removeEventListener("keydown", handleEscapeKey)
|
||||||
|
}
|
||||||
|
}, [handleEscapeKey, open])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, top: "50%" }}
|
||||||
|
animate={{ opacity: 1, top: 0 }}
|
||||||
|
exit={{ opacity: 0, top: "50%" }}
|
||||||
|
className="fixed inset-0 z-100 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-primary-900/30" onClick={() => setOpen(false)} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative bg-primary-50 px-5 py-6 w-full max-w-[280px] mx-4 rounded-lg md:max-w-[627px] md:py-10 md:px-[60px] md:rounded-xl xl:max-w-[668px]",
|
||||||
|
step === 'success' && 'md:max-w-[400px] md:p-10 xl:max-w-[400px]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Show condition={step !== 'success'}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="text"
|
||||||
|
className="absolute top-3 right-3 bg-primary-200 p-1 shadow md:bg-white md:p-2 md:top-8 md:right-10"
|
||||||
|
>
|
||||||
|
<CloseOutlined className="md:text-lg" />
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{step !== 'success' && (
|
||||||
|
<img
|
||||||
|
src="/logos/simple.svg"
|
||||||
|
alt="IMPHNEN Logo"
|
||||||
|
className="mb-6 h-8 w-auto mx-auto md:h-12 xl:h-[60px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="scrollbar-hide max-h-[50dvh] overflow-y-auto">
|
||||||
|
{step !== 'success' && (
|
||||||
|
<div className="relative mb-7 bg-primary-500 p-4 overflow-hidden rounded-md md:px-7 md:py-5 md:flex md:justify-between md:gap-x-6">
|
||||||
|
<p className="text-xs font-semibold text-primary-50 md:text-[19px] md:w-[290px] xl:text-[23px]">
|
||||||
|
<Show
|
||||||
|
condition={step !== 'qr-payment' && step !== 'va-payment'}
|
||||||
|
fallback="Yosha~! Saatnya Membayar :)"
|
||||||
|
>
|
||||||
|
Yosha~! Saatnya Level Up dengan Sesi Mentoring bersama Senpai!
|
||||||
|
</Show>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="/image/mascot-character.webp"
|
||||||
|
alt="IMPHNEN Mascot"
|
||||||
|
className="hidden w-[166px] h-auto absolute right-0 -bottom-3 md:block xl:w-[207px] xl:-bottom-5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} />}
|
||||||
|
{step === 'schedule' && <ScheduleStep />}
|
||||||
|
{step === 'profile' && <ProfileStep />}
|
||||||
|
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} />}
|
||||||
|
{step === 'qr-payment' && <QrisPaymentStep />}
|
||||||
|
{step === 'va-payment' && <VAPaymentStep />}
|
||||||
|
{step === 'success' && <SuccessStep />}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show condition={step !== 'success'}>
|
||||||
|
<hr className="my-6 border-primary-300" />
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="text"
|
||||||
|
className={cn((step === 'topic' || step === 'success') && 'hidden')}
|
||||||
|
onClick={() => handleStep('prev')}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="primary"
|
||||||
|
className={cn((step === 'topic' || step === 'success') && 'w-full')}
|
||||||
|
disabled={selectedTopics.length === 0 && step === 'topic'}
|
||||||
|
onClick={() => handleStep('next')}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
condition={step !== 'success'}
|
||||||
|
fallback="Halman Booking"
|
||||||
|
>
|
||||||
|
<Show condition={step !== 'payment'} fallback="Bayar Sekarang">
|
||||||
|
Selanjutnya
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { For, Show } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
import { TOPICS } from "../../../sections/topics"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedTopics: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaymentStep: FC<Props> = ({ selectedTopics }) => {
|
||||||
|
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"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<div className="space-y-6 xl:col-span-4">
|
||||||
|
<div>
|
||||||
|
<p className="w-max mx-auto text-[10px] text-neutral-400 font-medium mb-2 md:text-xs md:ms-0">
|
||||||
|
Senpai Kamu
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<div className="aspect-square mb-2 size-10 mx-auto rounded-full overflow-hidden md:size-20 md:ms-0">
|
||||||
|
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
|
||||||
|
</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.
|
||||||
|
</h3>
|
||||||
|
<p className="text-[10px] text-neutral-600 md:text-xs">
|
||||||
|
UI Designer at Oray orayan Studios
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-400 font-medium mb-2 md:text-xs">
|
||||||
|
Topics :
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<For data={TOPICS}>
|
||||||
|
{(topic) => (
|
||||||
|
<Show key={topic.id} condition={selectedTopics.includes(topic.id)}>
|
||||||
|
<div
|
||||||
|
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
|
||||||
|
>
|
||||||
|
<span>{topic.icon} </span>
|
||||||
|
<span>{topic.name}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6 xl:col-span-5">
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-400 font-medium mb-2 md:text-xs">
|
||||||
|
Metode Pembayaran
|
||||||
|
</p>
|
||||||
|
<div className="mb-3">
|
||||||
|
<p className="text-[8px] text-neutral-400 mb-2 md:text-[10px]">
|
||||||
|
Virtual Account
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-3 gap-2 md:grid-cols-4">
|
||||||
|
<For data={['BCA', 'BRI', 'BNI', 'MANDIRI', 'BCA-Virtual', 'BNI-Virtual', 'MANDIRI-Virtual']}>
|
||||||
|
{(bank, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="p-1 bg-white rounded-xs border border-primary-50 flex items-center gap-x-1"
|
||||||
|
>
|
||||||
|
<input type="radio" name="payment" id={bank} className="size-2" />
|
||||||
|
<label htmlFor={bank} className="block">
|
||||||
|
<img src="/image/payment/bca.webp" alt={bank} className="object-scale-down" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[8px] text-neutral-400 mb-2 md:text-[10px]">
|
||||||
|
QRIS
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-3 gap-2 md:grid-cols-4">
|
||||||
|
<div
|
||||||
|
className="p-1 bg-white rounded-xs border border-primary-50 flex items-center gap-x-1"
|
||||||
|
>
|
||||||
|
<input type="radio" name="payment" id="QRIS" className="size-2" />
|
||||||
|
<label htmlFor="QRIS" className="block">
|
||||||
|
<img src="/image/payment/qris.webp" alt="QRIS" className="object-scale-down" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-400 font-medium mb-2 md:text-xs">
|
||||||
|
Detail Pembayaran
|
||||||
|
</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="font-normal">Service Fee :</p>
|
||||||
|
<p className="text-right">Rp. 2.000</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Input } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
|
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 ProfileStep = () => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="bg-white px-6 py-5 rounded-md md:px-6"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-xs font-semibold mb-1.5 md:text-[15px] md:mb-2 xl:text-[19px]">
|
||||||
|
Langkah 3 dari 3
|
||||||
|
</h3>
|
||||||
|
<p className="text-[10px] text-neutral-600 mb-5 md:text-xs xl:text-[15px]">
|
||||||
|
Lengkapi profil kamu agar senpai dapat mengenal kamu lebih baik.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2.5 md:space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Nomor WhatsApp</label>
|
||||||
|
<Input className="min-w-full w-full" placeholder="080000000" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>CV (Opsional)</label>
|
||||||
|
<Input type="file" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Portoflio (Opsional)</label>
|
||||||
|
<Input className="min-w-full w-full" placeholder="Link Url" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
|
const PAYMENT_STEP = [
|
||||||
|
'Buka aplikasi e-wallet atau m-banking kamu',
|
||||||
|
'Cari fitur bayar menggunakan QRIS',
|
||||||
|
'Scan kode QR di samping',
|
||||||
|
'Konfimasi pembayaran, dan proses selesai.'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const QrisPaymentStep = () => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="bg-white px-6 py-5 rounded-md md:flex md:justify-between md:gap-8"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<div className="mb-7 md:mb-0">
|
||||||
|
<h3 className="text-xs font-semibold mb-4 text-neutral-600 text-center md:text-start">
|
||||||
|
Silahkan ikuti instruksi pembayaran di bawah ini
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-[10px] text-neutral-400 mb-1.5">
|
||||||
|
Cara melakukan pembayaran
|
||||||
|
</p>
|
||||||
|
<ul className="list-decimal pl-2.5 mb-4 md:mb-7">
|
||||||
|
<For data={PAYMENT_STEP}>
|
||||||
|
{(step, index) => (
|
||||||
|
<li key={index} className="text-[10px] text-neutral-400 leading-tight font-medium">
|
||||||
|
{step}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="text-center md:text-start">
|
||||||
|
<p className="text-[10px] text-neutral-400 mb-1.5 font-medium md:text-xs">
|
||||||
|
Biaya yang harus dibayarkan
|
||||||
|
</p>
|
||||||
|
<p className="text-xs font-semibold text-neutral-700 md:text-[15px]">Rp. 52.000</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-400 mb-2 font-medium text-center md:text-xs">
|
||||||
|
QR Code Pembayaran
|
||||||
|
</p>
|
||||||
|
<div className="bg-primary-50 p-2.5 rounded-lg size-[120px] mx-auto md:size-[142px] md:me-0">
|
||||||
|
<img src="/image/sample-qrcode.webp" alt="QR Code" className="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
|
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 ___.
|
||||||
|
1.Pertanyaan Anda
|
||||||
|
2. ...
|
||||||
|
3. ...`
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="bg-white px-6 py-5 rounded-md md:px-6"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-xs font-semibold mb-1.5 md:text-[15px] md:mb-2 xl:text-[19px]">
|
||||||
|
Langkah 2 dari 3
|
||||||
|
</h3>
|
||||||
|
<p className="text-[10px] text-neutral-600 mb-5 md:text-xs xl:text-[15px]">
|
||||||
|
Pilih jadwal sesuai preferensi kamu & siapkan pertanyaanmu
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Waktu</label>
|
||||||
|
<Input type="time" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div className="relative md:col-span-full">
|
||||||
|
<label className={labelClass}>Lokasi</label>
|
||||||
|
<Select className="min-w-full w-full">
|
||||||
|
<option value="online">Online</option>
|
||||||
|
<option value="offline">Offline</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-full">
|
||||||
|
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
|
||||||
|
<Textarea
|
||||||
|
className="min-w-full w-full h-40"
|
||||||
|
placeholder={placeholder} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { CheckOutlined } from "@ant-design/icons"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
|
export const SuccessStep = () => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="pb-6 rounded-md md:pb-8"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<h2 className="text-[15px] font-semibold mb-3 text-primary-500 text-center md:text-[19px] md:mb-7 xl:text-[23px] xl:mb-8">
|
||||||
|
Booking Senpai Berhasil
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mb-3 flex justify-center items-center mx-auto size-[60px] bg-green-200 rounded-full",
|
||||||
|
"md:size-[100px] md:mb-7 xl:size-[120px] xl:mb-8"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="bg-green-500 rounded-full size-10 flex justify-center items-center text-white md:size-16 xl:size-20">
|
||||||
|
<CheckOutlined className="text-xl md:text-3xl xl:text-5xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[10px] text-neutral-400 text-center md:text-xs md:max-w-[220px] md:mx-auto xl:text-[15px]">
|
||||||
|
Untuk detail dan instruksi berikutnya dapat dilihat di dashboard pada halaman booking
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { CheckOutlined } from "@ant-design/icons"
|
||||||
|
import { cn, For, Show } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
import { TOPICS } from "../../../sections/topics"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedTopics: number[]
|
||||||
|
setSelectedTopics: (topics: number[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TopicStep: FC<Props> = ({ selectedTopics, setSelectedTopics }) => {
|
||||||
|
const handleSelectTopic = (topicId: number) => {
|
||||||
|
if (selectedTopics.includes(topicId)) {
|
||||||
|
setSelectedTopics(selectedTopics.filter((id) => id !== topicId))
|
||||||
|
} else {
|
||||||
|
setSelectedTopics([...selectedTopics, topicId])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
className="mb-6 flex items-center gap-x-4 bg-white rounded-md overflow-hidden shadow/5 md:gap-x-10"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<div className="w-16 aspect-4/5 overflow-hidden md:w-[134px] xl:w-[140px]">
|
||||||
|
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 pe-4">
|
||||||
|
<h3 className="text-xs font-semibold mb-1 line-clamp-2 md:text-[19px] md:mb-2 xl:text-[23px]">
|
||||||
|
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
|
||||||
|
</h3>
|
||||||
|
<p className="text-[10px] line-clamp-2 text-neutral-600 md:text-[15px] xl:text-[19px]">
|
||||||
|
UI Designer at Oray orayan Studios
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-white p-5 rounded-md shadow/5 md:px-6"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-xs font-semibold mb-1.5 md:text-[15px] md:mb-2 xl:text-[19px]">
|
||||||
|
Langkah 1 dari 3
|
||||||
|
</h3>
|
||||||
|
<p className="text-[10px] text-neutral-600 mb-5 md:text-xs xl:text-[15px]">
|
||||||
|
Pilih topik yang ingin kamu diskusikan dengan Senpai kamu~
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid gap-3 md:grid-cols-2 md:content-center">
|
||||||
|
<For data={TOPICS}>
|
||||||
|
{(topic) => (
|
||||||
|
<div
|
||||||
|
key={topic.id}
|
||||||
|
className={cn(
|
||||||
|
"relative px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium cursor-pointer",
|
||||||
|
"md:text-xs xl:text-xs xl:font-semibold",
|
||||||
|
selectedTopics.includes(topic.id) && "bg-primary-50 border-primary-200"
|
||||||
|
)}
|
||||||
|
onClick={() => handleSelectTopic(topic.id)}
|
||||||
|
>
|
||||||
|
<span>{topic.icon} </span>
|
||||||
|
<span>{topic.name}</span>
|
||||||
|
|
||||||
|
<Show condition={selectedTopics.includes(topic.id)}>
|
||||||
|
<div className="absolute top-2 right-2 bg-primary-500 size-[14px] rounded-sm flex justify-center items-center text-white">
|
||||||
|
<CheckOutlined />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
|
const PAYMENT_STEP = [
|
||||||
|
'Buka aplikasi e-wallet atau m-banking kamu',
|
||||||
|
'Pergi ke fitur transfer via Virtual Account',
|
||||||
|
'Pilih bank dan kode VA sesuai dengan yang ada pada halaman ini',
|
||||||
|
'Konfimasi pembayaran, dan proses selesai.'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const VAPaymentStep = () => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="bg-white px-6 py-5 rounded-md md:flex md:items-center md:gap-11"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<div className="mb-6 md:mb-0">
|
||||||
|
<h3 className="text-xs font-semibold mb-4 text-neutral-600 text-center md:text-start">
|
||||||
|
Silahkan ikuti instruksi pembayaran di bawah ini
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-[10px] text-neutral-400 mb-1.5">
|
||||||
|
Cara melakukan pembayaran
|
||||||
|
</p>
|
||||||
|
<ul className="list-decimal pl-2.5">
|
||||||
|
<For data={PAYMENT_STEP}>
|
||||||
|
{(step, index) => (
|
||||||
|
<li key={index} className="text-[10px] text-neutral-400 leading-tight font-medium">
|
||||||
|
{step}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center md:text-start">
|
||||||
|
<div className="w-max mx-auto mb-3 md:ms-0 md:mb-4">
|
||||||
|
<img src="/image/payment/bca.webp" alt="VA BCA" className="h-5 w-auto" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[10px] text-neutral-400 mb-1 font-medium md:text-xs">
|
||||||
|
Kode Virtual Account
|
||||||
|
</p>
|
||||||
|
<p className="text-xs font-semibold text-neutral-700 mb-1.5 md:text-[15px]">
|
||||||
|
8091239861969812
|
||||||
|
</p>
|
||||||
|
<p className="text-[8px] text-neutral-400 md:text-[10px]">
|
||||||
|
A/N Unknown
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
|
||||||
|
const EDUCATION = [
|
||||||
|
{ name: 'Universitas Widyabakti', major: 'Intern Front End', duration: '2 Months', range: 'Oct 2024 - Present' },
|
||||||
|
{ name: 'SMKN 99 Banjaran', major: 'Rekayasa Perangkat Lunak', duration: '6 Months', range: 'May 2024 - Oct 2024' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const EducationSection = () => {
|
||||||
|
return (
|
||||||
|
<div className="px-7 py-8 rounded-md shadow-md">
|
||||||
|
<h2 className="text-xs font-semibold mb-5 md:text-[15px] xl:text-[19px]">Education</h2>
|
||||||
|
<div className="space-y-4 divide-y">
|
||||||
|
<For data={EDUCATION}>
|
||||||
|
{(item, index) => (
|
||||||
|
<div key={index} className={cn("flex items-center gap-x-4", index !== EDUCATION.length - 1 && "pb-4")}>
|
||||||
|
<div className="rounded-full size-6 bg-neutral-200 md:size-7 xl:size-8"></div>
|
||||||
|
<div className='flex-1 text-[10px] font-medium'>
|
||||||
|
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.name}</p>
|
||||||
|
<p>
|
||||||
|
<span className="text-neutral-600 xl:text-xs">{item.major}</span>
|
||||||
|
<span className="text-neutral-300"> · </span>
|
||||||
|
<span className="text-neutral-400 font-normal">{item.duration}</span>
|
||||||
|
<span className="text-neutral-300"> · </span>
|
||||||
|
<span className="text-neutral-400 font-normal xl:font-medium">{item.range}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
const EXPERIENCE = [
|
||||||
|
{ name: 'Sunday.com', position: 'Intern Front End', duration: '2 Months', range: 'Oct 2024 - Present' },
|
||||||
|
{ name: 'CodeX Digital', position: 'Intern Front End', duration: '6 Months', range: 'May 2024 - Oct 2024' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const ExperienceSection: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="px-7 py-8 rounded-md shadow-md">
|
||||||
|
<h2 className="text-xs font-semibold mb-5 md:text-[15px] xl:text-[19px]">Experience</h2>
|
||||||
|
<div className="space-y-4 divide-y">
|
||||||
|
<For data={EXPERIENCE}>
|
||||||
|
{(item, index) => (
|
||||||
|
<div key={index} className={cn("flex items-center gap-x-4", index !== EXPERIENCE.length - 1 && "pb-4")}>
|
||||||
|
<div className="rounded-full size-6 bg-neutral-200 md:size-7 xl:size-8"></div>
|
||||||
|
<div className='flex-1 text-[10px] font-medium'>
|
||||||
|
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.name}</p>
|
||||||
|
<p>
|
||||||
|
<span className="text-neutral-600 xl:text-xs">{item.position}</span>
|
||||||
|
<span className="text-neutral-300"> · </span>
|
||||||
|
<span className="text-neutral-400 font-normal">{item.duration}</span>
|
||||||
|
<span className="text-neutral-300"> · </span>
|
||||||
|
<span className="text-neutral-400 font-normal xl:font-medium">{item.range}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { StarFilled } from "@ant-design/icons"
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { TMentorDetail } from "@imphnen-frontend-service/service"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onBook: () => void
|
||||||
|
mentor?: TMentorDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProfileSection: React.FC<Props> = ({ onBook, mentor }) => {
|
||||||
|
const name = mentor?.fullname || mentor?.legal_name || "Mentor"
|
||||||
|
const role = mentor?.current_role || "Mentor"
|
||||||
|
const company = mentor?.current_company || "IMPHNEN"
|
||||||
|
const expertise = mentor?.expertise ?? []
|
||||||
|
const softSkills = mentor?.languages ?? []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white px-4 py-5 space-y-6 md:px-8 md:pt-6 md:pb-0 xl:space-y-0 xl:py-[30px] xl:flex xl:gap-x-9 xl:justify-between">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center justify-center",
|
||||||
|
"md:flex-row md:justify-start md:gap-x-8 md:items-start",
|
||||||
|
"xl:items-center xl:gap-x-10",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mb-5 h-20 w-auto aspect-4/5 rounded-sm overflow-hidden",
|
||||||
|
"md:aspect-33/40 md:h-40 md:rounded-md md:border md:border-primary-50 md:mb-0",
|
||||||
|
"xl:aspect-5/6 md:h-60 md:rounded-lg"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:max-w-[242px] xl:max-w-[306px]">
|
||||||
|
<h1
|
||||||
|
className={cn(
|
||||||
|
"mb-1 text-[15px] font-semibold text-center leading-snug",
|
||||||
|
"md:text-[19px] md:mb-2 md:text-start xl:text-[23px]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs mb-4 text-neutral-600 md:mb-5 md:text-[15px] xl:text-[19px] xl:mb-5">
|
||||||
|
{role} at {company}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-x-2.5 w-full max-w-max border border-primary-50 p-1.5 rounded-md mx-auto md:ms-0">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-tr from-primary-500 to-primary-200 rounded-sm text-white size-5 flex justify-center items-center xl:size-[29.4px]"
|
||||||
|
>
|
||||||
|
<StarFilled className="text-xs xl:text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-medium mb-1 text-neutral-800 xl:text-xs">Excelent Sensei</p>
|
||||||
|
<p className="text-[8px] text-neutral-600 xl:text-[10px]">4.8/5.0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-y-3 xl:gap-y-4 xl:max-w-[402px]">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Expertise</h2>
|
||||||
|
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
||||||
|
<For data={expertise}>
|
||||||
|
{(item) => (
|
||||||
|
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Soft Skills</h2>
|
||||||
|
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
||||||
|
<For data={softSkills}>
|
||||||
|
{(item) => (
|
||||||
|
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden md:flex xl:hidden justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-primary-500 text-[15px] font-semibold">Jum, 4 April 2025</p>
|
||||||
|
<p className="text-neutral-500 text-xs font-medium">Jum, 4 April 2025</p>
|
||||||
|
</div>
|
||||||
|
<Button type="button" size="sm" onClick={onBook}>
|
||||||
|
Book Your Senpai!
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SenpaiScheduleSection: FC<Props> = ({ onBook }) => {
|
||||||
|
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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<hr className="border-primary-200" />
|
||||||
|
|
||||||
|
<Button type="button" size="sm" className="w-full" onClick={onBook}>
|
||||||
|
Book Your Senpai!
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { StarFilled } from "@ant-design/icons";
|
||||||
|
import { For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
const SENPAI_STATISTIC = [
|
||||||
|
{ name: 'Total Sessions', count: 8 },
|
||||||
|
{ name: 'Mentee Impact', count: 1000 },
|
||||||
|
{ name: 'Response Time', count: '30 Minute' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const StatisticsSection: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="md:mb-10">
|
||||||
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Statistics</h2>
|
||||||
|
<div className="grid gap-4 xl:flex">
|
||||||
|
<For data={SENPAI_STATISTIC}>
|
||||||
|
{(item, index) => (
|
||||||
|
<div key={index} className="px-2.5 py-2 border border-primary-50 rounded-md shadow flex items-center gap-x-2.5">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-tr from-primary-500 to-primary-200 rounded-sm text-white size-7 flex justify-center items-center xl:size-[29.4px]"
|
||||||
|
>
|
||||||
|
<StarFilled className="text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-600 font-medium md:text-xs xl:font-semibold">{item.count}</p>
|
||||||
|
<p className="text-[8px] text-neutral-800 md:text-[10px]">{item.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
export const TOPICS = [
|
||||||
|
{ id: 1, icon: '💼', name: 'Career & Self Development' },
|
||||||
|
{ id: 2, icon: '🏢', name: 'Industry Insight' },
|
||||||
|
{ id: 3, icon: '🗂️', name: 'Project Management & IT Tools' },
|
||||||
|
{ id: 4, icon: '🖥️', name: 'Basic IT' },
|
||||||
|
{ id: 5, icon: '💻', name: 'Programming/Software Dev' },
|
||||||
|
{ id: 6, icon: '🗃️', name: 'Data & Database' },
|
||||||
|
{ id: 7, icon: <span className="font-bold text-primary-500">AI</span>, name: 'AI Tips' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const TopicsSection: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Topics</h2>
|
||||||
|
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5">
|
||||||
|
<For data={TOPICS}>
|
||||||
|
{(item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
|
||||||
|
>
|
||||||
|
<span>{item.icon} </span>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { ProfileSection } from './_components/sections/profile'
|
||||||
|
import { StatisticsSection } from './_components/sections/senpai-statistics'
|
||||||
|
import { TopicsSection } from './_components/sections/topics'
|
||||||
|
import { ExperienceSection } from './_components/sections/experience'
|
||||||
|
import { EducationSection } from './_components/sections/education'
|
||||||
|
import { SenpaiScheduleSection } from './_components/sections/senpai-schedule'
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||||
|
import { AppointmentModal } from './_components/modals/appointment'
|
||||||
|
import { useGetMentorDetail } from '@imphnen-frontend-service/service'
|
||||||
|
import { Show } from '@imphnen-frontend-service/utils'
|
||||||
|
|
||||||
|
export const Components: FC = () => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const { id } = useParams()
|
||||||
|
const { data: mentor, isLoading, isError } = useGetMentorDetail(id ?? '')
|
||||||
|
|
||||||
|
const resume = mentor?.bio ?? ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Show
|
||||||
|
condition={!isLoading && !isError}
|
||||||
|
fallback={
|
||||||
|
<section className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20">
|
||||||
|
<div className="max-w-7xl mx-auto text-center text-neutral-500 py-10">
|
||||||
|
{isError ? 'Mentor tidak ditemukan' : 'Memuat profil mentor...'}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<section className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-8 md:bg-white xl:bg-transparent">
|
||||||
|
<ProfileSection onBook={() => setOpen(true)} mentor={mentor} />
|
||||||
|
|
||||||
|
<Button type="button" size="sm" className="w-full md:hidden" onClick={() => setOpen(true)}>
|
||||||
|
Book Your Senpai!
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="bg-white px-4 py-5 md:px-8 md:pb-6 md:pt-0 xl:py-7 xl:flex xl:gap-x-10">
|
||||||
|
<div className="space-y-10 md:space-y-7 xl:flex-1">
|
||||||
|
<StatisticsSection />
|
||||||
|
<TopicsSection />
|
||||||
|
|
||||||
|
<div className="px-6 py-8 rounded-md shadow-md">
|
||||||
|
<h2 className="text-xs text-neutral-800 font-semibold mb-5 md:text-[15px] xl:text-[19px]">Senpai Resume</h2>
|
||||||
|
<p className="text-[10px] font-medium text-neutral-600 text-pretty md:text-[15px]">
|
||||||
|
{resume || 'Belum ada deskripsi dari senpai ini.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ExperienceSection />
|
||||||
|
<EducationSection />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden xl:block xl:w-[400px]">
|
||||||
|
<SenpaiScheduleSection onBook={() => setOpen(true)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<AppointmentModal open={open} setOpen={setOpen} />
|
||||||
|
</Show>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Components
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
import { FC, useRef } from 'react';
|
||||||
|
import { motion, useInView, Variants } from 'framer-motion';
|
||||||
|
|
||||||
|
export const BannerSection: FC = () => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={ref} className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20">
|
||||||
|
<motion.div
|
||||||
|
className={cn(
|
||||||
|
"relative max-w-7xl mx-auto bg-white p-6 rounded-lg overflow-hidden shadow-md",
|
||||||
|
"md:flex md:justify-between md:items-center md:px-5 md:rounded-xl lg:px-16 lg:py-10",
|
||||||
|
)}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="hidden absolute -left-9 -top-24 w-[17rem] md:block lg:w-[28rem] lg:-left-14 lg:-top-52"
|
||||||
|
initial={{ x: -120, opacity: 0, rotate: -6 }}
|
||||||
|
animate={{ x: 0, opacity: 1, rotate: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<img src="/image/mentoring-banner.webp" alt="" className="w-full" />
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="text-center md:text-start md:max-w-[calc(100%-14rem)] md:ms-auto lg:max-w-[calc(100%-20rem)]"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
>
|
||||||
|
<motion.h1 className="mb-4 text-xl font-semibold text-primary-500 leading-tight md:text-2xl lg:text-5xl lg:font-bold lg:mb-6" variants={childVariants}>
|
||||||
|
Find your perfect sensei
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p className="text-xs font-semibold text-neutral-500 md:text-[15px] md:font-medium lg:text-xl" variants={childVariants}>
|
||||||
|
Di sini, kamu nggak cuma grinding sendirian—sensei dari dunia nyata siap membimbingmu, dan AI mentor bakal jadi support system terbaikmu. Saatnya jadi protagonist dalam perjalanan karier IT-mu!
|
||||||
|
</motion.p>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { For } from '@imphnen-frontend-service/utils';
|
||||||
|
import { TMentorDetail } from '@imphnen-frontend-service/service';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
type TMentorCardProps = {
|
||||||
|
mentor: TMentorDetail;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MentorCard: FC<TMentorCardProps> = ({ mentor }) => {
|
||||||
|
const experience =
|
||||||
|
mentor.years_of_experience > 0
|
||||||
|
? `${mentor.years_of_experience} Years Experience`
|
||||||
|
: 'New Mentor';
|
||||||
|
const role = mentor.current_role || 'Mentor';
|
||||||
|
const company = mentor.current_company || 'IMPHNEN';
|
||||||
|
const softSkills = mentor.expertise || [];
|
||||||
|
const shownSkills = softSkills.slice(0, 2);
|
||||||
|
const extraCount = Math.max(0, softSkills.length - shownSkills.length);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-2.5 rounded-md bg-white shadow flex gap-x-4 items-start md:p-4 md:flex-col md:rounded-lg md:gap-y-4">
|
||||||
|
<div className="size-[60px] rounded-md overflow-hidden md:w-full md:h-auto md:aspect-square">
|
||||||
|
<img src="/image/testimonial.webp" alt={mentor.fullname ?? 'Mentor'} className="w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button variant="text" size="sm" className="bg-primary-100 h-auto px-1.5 py-1 text-[8px] font-normal mb-2 hover:bg-primary-100 md:text-[10px] md:font-medium md:mb-4">
|
||||||
|
{experience}
|
||||||
|
</Button>
|
||||||
|
<h2 className="mb-1">
|
||||||
|
<Link to={`/mentoring/${mentor.id}`} className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
|
||||||
|
{mentor.fullname || mentor.legal_name || 'Mentor'}
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
<p className="text-[8px] text-neutral-500 mb-3 md:text-[10px] md:font-medium md:mb-4 lg:text-xs">
|
||||||
|
{role} at {company}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{softSkills.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Soft Skills :</p>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<For data={shownSkills}>
|
||||||
|
{(skill) => (
|
||||||
|
<Button
|
||||||
|
key={skill}
|
||||||
|
variant="text"
|
||||||
|
size="sm"
|
||||||
|
className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]"
|
||||||
|
>
|
||||||
|
{skill}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
{extraCount > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="sm"
|
||||||
|
className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]"
|
||||||
|
>
|
||||||
|
+{extraCount}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { CloudServerOutlined, CodeOutlined, DatabaseOutlined, ProjectOutlined, TabletOutlined } from '@ant-design/icons'
|
||||||
|
import { Icon } from '@iconify/react'
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||||
|
import { For } from '@imphnen-frontend-service/utils'
|
||||||
|
import { FC, ReactElement, useRef } from 'react'
|
||||||
|
import { motion, useInView, Variants } from 'framer-motion'
|
||||||
|
|
||||||
|
const TOPICS: {
|
||||||
|
name: string
|
||||||
|
icon: React.ReactNode
|
||||||
|
}[] = [
|
||||||
|
{ name: 'UI/UX & Design', icon: <TabletOutlined /> },
|
||||||
|
{ name: 'Software/Web Dev', icon: <CodeOutlined /> },
|
||||||
|
{ name: 'Data & AI', icon: <DatabaseOutlined /> },
|
||||||
|
{ name: 'Cloud & DevOps', icon: <CloudServerOutlined /> },
|
||||||
|
{ name: 'Cybersecurity', icon: <Icon icon="mage:security-shield" /> },
|
||||||
|
{ name: 'IT & Networking', icon: <Icon icon="carbon:data-structured" /> },
|
||||||
|
{ name: 'Project Management', icon: <ProjectOutlined /> },
|
||||||
|
{ name: 'QA & Testing', icon: <Icon icon="carbon:exam-mode" /> }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const Topics: FC = (): ReactElement => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.3,
|
||||||
|
delay: 0.4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<motion.div
|
||||||
|
className="grid grid-cols-4 gap-6 px-3 md:px-12 md:gap-x-10 md:gap-y-14 lg:grid-cols-8 lg:px-0"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
|
>
|
||||||
|
<For data={TOPICS}>
|
||||||
|
{(topic) => (
|
||||||
|
<motion.div key={topic.name} className="flex flex-col items-center" variants={childVariants}>
|
||||||
|
<Button variant="secondary" size='sm' className="size-7 mb-3 p-0 md:mb-4 md:size-8 md:text-base lg:size-10 lg:text-lg">
|
||||||
|
{topic.icon}
|
||||||
|
</Button>
|
||||||
|
<p className="text-[8px] text-neutral-700 text-center break-words md:text-xs md:font-medium lg:text-[15px]">
|
||||||
|
{topic.name}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { FC, ReactElement, useRef, useState } from 'react';
|
||||||
|
import { BannerSection } from './_components/banner-section';
|
||||||
|
import { Topics } from './_components/topics';
|
||||||
|
import { Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
|
import { For, Show } from '@imphnen-frontend-service/utils';
|
||||||
|
import { MentorCard } from './_components/mentor-card';
|
||||||
|
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { useGetMentors } from '@imphnen-frontend-service/service';
|
||||||
|
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
|
||||||
|
import { motion, useInView, Variants } from 'framer-motion';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 8,
|
||||||
|
});
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const { data: mentors, isLoading } = useGetMentors({ search });
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: mentors ?? [],
|
||||||
|
columns: [],
|
||||||
|
state: { pagination },
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageMentors = table.getRowModel().rows.map((row) => row.original);
|
||||||
|
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<BannerSection />
|
||||||
|
|
||||||
|
<section className="w-full px-8 md:px-[60px] md:pb-[60px] lg:px-20">
|
||||||
|
<motion.div
|
||||||
|
className="max-w-7xl mx-auto bg-white rounded-lg py-6 md:rounded-4xl"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
>
|
||||||
|
<div className="mb-10 md:px-6 lg:mb-[60px]">
|
||||||
|
<motion.h1
|
||||||
|
className="text-center text-[15px] text-primary-500 font-semibold mb-8 md:text-2xl md:mb-10 lg:text-3xl lg:mb-[60px]"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, delay: 0.3 }}
|
||||||
|
>
|
||||||
|
Choose Your Topics
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<Topics />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div ref={ref} className="md:px-6">
|
||||||
|
<motion.div
|
||||||
|
className="relative mb-6 md:px-12 lg:px-0 lg:w-[480px] lg:mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama, posisi/peran"
|
||||||
|
className="relative min-w-full w-full"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
<SearchOutlined className="absolute right-2.5 top-1/2 -translate-y-1/2 text-primary-500 size-2.5 cursor-text md:me-12 lg:me-0" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
condition={!isLoading}
|
||||||
|
fallback={
|
||||||
|
<motion.p variants={childVariants} className="text-center text-neutral-500 col-span-full py-10">
|
||||||
|
Memuat data mentor...
|
||||||
|
</motion.p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
condition={pageMentors.length > 0}
|
||||||
|
fallback={
|
||||||
|
<motion.p variants={childVariants} className="text-center text-neutral-500 col-span-full py-10">
|
||||||
|
Tidak ada mentor ditemukan
|
||||||
|
</motion.p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For data={pageMentors}>
|
||||||
|
{(mentor, index) => (
|
||||||
|
<motion.div key={mentor.id ?? index} variants={childVariants}>
|
||||||
|
<MentorCard mentor={mentor} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<Pagination table={table} />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Components
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { MailOutlined } from "@ant-design/icons";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC, useRef } from "react";
|
||||||
|
import { motion, useInView, Variants } from "framer-motion";
|
||||||
|
|
||||||
|
export const Collaborate: FC = () => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, scale: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={ref} className="w-full p-8 md:py-14 md:px-[60px] lg:pt-0 lg:pb-16 lg:px-20">
|
||||||
|
<div className="max-w-7xl py-6 mx-auto md:flex md:gap-[60px] md:items-center xl:py-14">
|
||||||
|
<div className="relative mb-8 flex items-center justify-center h-[124px] md:mb-0 md:h-[180px] md:w-[202px] xl:h-[380px] xl:w-[428px]">
|
||||||
|
<motion.div
|
||||||
|
className="relative"
|
||||||
|
variants={containerVariants}
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="size-[104px] bg-primary-200 rounded-full flex justify-between items-center md:size-[148px] xl:size-[304px]"
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
whileInView={{ scale: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="size-[88px] rounded-full bg-gradient-to-bl from-primary-500 to-primary-300 mx-auto md:size-[132px] xl:size-[276px]"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute bg-white shadow rounded-md -top-3 -right-2 p-1 rotate-6 xl:p-2.5 xl:rounded-lg"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<Icon icon="bi:terminal" className="text-primary-500 size-4 md:size-[18px] xl:size-9" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute bg-white shadow rounded-md top-1/3 -left-6 p-1.5 -rotate-10 xl:p-2.5 xl:-left-12"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<Icon icon="ion:git-branch-outline" className="text-primary-500 size-6 md:size-[30px] xl:size-14" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute bg-white shadow rounded-md top-3/5 -right-3 p-1 xl:p-2 xl:-right-6"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<Icon icon="bi:github" className="size-[18px] md:size-[26px] xl:size-[52px]" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="h-full w-auto aspect-3/5 absolute inset-0 mx-auto bg-contain bg-no-repeat"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.5 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/image/anime-girl.webp"
|
||||||
|
alt="Anime Girl"
|
||||||
|
style={{
|
||||||
|
opacity: "1",
|
||||||
|
maskImage: "linear-gradient(to bottom, black 80%, transparent 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className={cn(
|
||||||
|
"px-6 py-5 rounded-lg bg-white border-2 border-primary-200 text-center text-primary-500 shadow",
|
||||||
|
"md:flex-1 md:text-start xl:py-8 xl:px-12"
|
||||||
|
)}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<motion.h1
|
||||||
|
className="text-[15px] font-semibold mb-3 md:text-[23px] xl:text-[37px]"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
Let's Collaborate!
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
className="text-xs font-medium leading-tight mb-6 md:text-[15px] xl:text-[19px]"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
|
>
|
||||||
|
Klik tombol di bawah ini untuk mendaftar sebagai partner resource dan bantu membangun dunia IT yang lebih seru!
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center mx-auto md:ms-0"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<Button type="button" size="sm" className="flex items-center gap-2">
|
||||||
|
<span>Collaboration Now</span>
|
||||||
|
<MailOutlined />
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { FC, useRef } from "react";
|
||||||
|
import { motion, useInView, Variants } from "framer-motion";
|
||||||
|
|
||||||
|
export const Contribute: FC = () => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const inView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={ref} className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20 xl:py-20">
|
||||||
|
<div className="relative max-w-7xl mx-auto xl:py-14">
|
||||||
|
<div className="md:max-w-[514px] mx-auto">
|
||||||
|
<motion.h1
|
||||||
|
className="font-semibold text-[19px] text-primary-500 mb-4 text-center leading-tight md:text-[37px] md:mb-6 xl:text-[46px] xl:font-bold"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
Want to contribute? <br /> help senpai build the IT ecosystem!
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
className="text-center text-xs text-neutral-500 md:text-[15px] md:font-medium md:w-[395px] md:mx-auto xl:text-[19px] xl:w-full"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
|
>
|
||||||
|
Punya platform, artikel, atau tool keren untuk membantu mentee belajar IT? Bergabunglah dengan kami dan jadilah bagian dari ekosistem pembelajaran yang lebih besar!
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="hidden xl:block absolute inset-0 text-neutral-800"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={inView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-0 left-0 text-[23px] font-semibold bg-white border rounded-md px-5 py-2 shadow-lg -rotate-6"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Learning Platform
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-0 right-36 text-2xl font-semibold bg-white border rounded-md px-5 py-2 shadow-lg rotate-3"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
AI Tools
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-1/3 left-24 text-[23px] font-semibold bg-white border rounded-md px-5 py-2 shadow-lg"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
UI Assets
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-1/3 right-0 text-[27.38px] font-semibold bg-white border rounded-md px-6 py-2 shadow-lg -rotate-3"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Refferences
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-2/3 left-4 text-[31.62px] font-semibold bg-white border rounded-md px-6 py-2.5 shadow-lg leading-tight rotate-1"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Coding <br />Playground
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-[70%] right-16 text-2xl font-semibold bg-white border rounded-md px-5 py-2.5 shadow-lg leading-tight rotate-6"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Challenge <br />Platform
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { FC, useRef } from "react";
|
||||||
|
import { motion, useInView, Variants } from "framer-motion";
|
||||||
|
|
||||||
|
export const WorkWithUs: FC = () => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const inView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={ref} className="w-full px-8 md:px-[60px] lg:py-16 lg:px-20">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<motion.h1
|
||||||
|
className="text-[19px] font-semibold text-primary-500 mb-5 text-center md:text-[23px] md:mb-7 xl:text-[37px] xl:mb-10"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
Why Work with Us
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="grid gap-4 md:grid-cols-3 md:gap-5 xl:grid-flow-col xl:grid-cols-2 xl:grid-rows-2 xl:gap-6"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={inView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="px-5 py-6 bg-white rounded-lg shadow md:px-0 md:py-8 xl:row-span-2 xl:px-6 xl:flex xl:flex-col xl:justify-end"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<div className="h-[60px] w-full mb-6 xl:h-auto xl:w-full xl:mb-8">
|
||||||
|
<img src="/image/work-with-us/exposure.webp" alt="Exposure" className="h-full w-auto object-scale-down mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center md:px-6 xl:text-start xl:px-0">
|
||||||
|
<h1 className="text-primary-500 text-[15px] mb-2 font-semibold leading-tight md:text-[19px] md:px-4 xl:px-0">
|
||||||
|
Exposure Luas
|
||||||
|
</h1>
|
||||||
|
<p className="text-[10px] text-neutral-500 leading-tight md:text-[15px] md:px-1 xl:px-0">
|
||||||
|
Web kamu akan tampil di halaman resource kami!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="px-5 py-6 bg-white rounded-lg shadow md:px-0 md:py-8 xl:flex xl:items-center xl:px-6 xl:py-2 xl:gap-8"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<div className="h-[60px] w-full mb-6 xl:w-auto xl:h-[124px] xl:mb-0">
|
||||||
|
<img src="/image/work-with-us/handshake.webp" alt="Handshake" className="h-full w-auto object-scale-down mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center md:px-6 xl:text-start">
|
||||||
|
<h1 className="text-primary-500 text-[15px] mb-2 font-semibold leading-tight md:text-[19px]">
|
||||||
|
Bantu Generasi Baru
|
||||||
|
</h1>
|
||||||
|
<p className="text-[10px] text-neutral-500 leading-tight md:text-[15px]">
|
||||||
|
Bagikan ilmu dan bantu lebih banyak orang berkembang!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="px-5 py-6 bg-white rounded-lg shadow md:px-0 xl:flex xl:items-center xl:px-6 xl:py-2 xl:gap-8"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<div className="h-[60px] w-full mb-6 xl:w-auto xl:h-[124px] xl:mb-0">
|
||||||
|
<img src="/image/work-with-us/networking.webp" alt="Networking" className="h-full w-auto object-scale-down mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center md:px-6 xl:text-start">
|
||||||
|
<h1 className="text-primary-500 text-[15px] mb-2 font-semibold leading-tight md:text-[19px] md:px-4 xl:px-0">
|
||||||
|
Networking Komunitas
|
||||||
|
</h1>
|
||||||
|
<p className="text-[10px] text-neutral-500 leading-tight md:text-[15px] md:px-1 xl:px-0">
|
||||||
|
Bertemu dengan para developer, mentor, dan inovator lainnya!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import { Contribute } from "./_components/contribute";
|
||||||
|
import { WorkWithUs } from "./_components/work-with-us";
|
||||||
|
import { Collaborate } from "./_components/collaborate";
|
||||||
|
|
||||||
|
export default function Components(): ReactElement {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Contribute />
|
||||||
|
<WorkWithUs />
|
||||||
|
<Collaborate />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Outlet, ScrollRestoration } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function RootLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Outlet />
|
||||||
|
<ScrollRestoration />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,7 +16,10 @@ const mappingPublicRoutes = [
|
|||||||
'/auth/new-password',
|
'/auth/new-password',
|
||||||
'/auth/register-mentor',
|
'/auth/register-mentor',
|
||||||
'/auth/register-mentor/pending',
|
'/auth/register-mentor/pending',
|
||||||
'/auth/register-mentor/success'
|
'/auth/register-mentor/success',
|
||||||
|
'/mentoring',
|
||||||
|
'/resources',
|
||||||
|
'/articles',
|
||||||
];
|
];
|
||||||
|
|
||||||
const mappingRoutePermissions = [
|
const mappingRoutePermissions = [
|
||||||
@@ -74,6 +77,11 @@ const mappingRoutePermissions = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const mappingPublicPrefixRoutes = [
|
||||||
|
'/mentoring',
|
||||||
|
'/articles',
|
||||||
|
]
|
||||||
|
|
||||||
//TODO : Fix this later
|
//TODO : Fix this later
|
||||||
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
|
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
|
||||||
// const fallback = mappingRoutePermissions.find((route) =>
|
// const fallback = mappingRoutePermissions.find((route) =>
|
||||||
@@ -91,6 +99,13 @@ export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
|||||||
const userPermissions =
|
const userPermissions =
|
||||||
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
|
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
|
||||||
|
|
||||||
|
// Allow to access the landing page without authentication
|
||||||
|
// So, if the route prefix is in the mappingPublicPrefixRoutes, we return null
|
||||||
|
// to indicate that we don't need to authenticate the user
|
||||||
|
if (mappingPublicPrefixRoutes.some((prefix) => pathname.startsWith(prefix))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (mappingPublicRoutes.includes(pathname)) {
|
if (mappingPublicRoutes.includes(pathname)) {
|
||||||
if (token) return redirect('/dashboard');
|
if (token) return redirect('/dashboard');
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
backoffice:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/backoffice.Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3001:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: backoffice
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
dimentorin:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/dimentorin.Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3002:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: dimentorin
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
gacha:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/gacha.Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3003:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: gacha
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { api } from '../';
|
||||||
|
import {
|
||||||
|
TMentorAvailability,
|
||||||
|
TMentorDetail,
|
||||||
|
TMentorListParams,
|
||||||
|
TSessionFeedbackRequest,
|
||||||
|
TSessionListResponse,
|
||||||
|
TBookSessionRequest,
|
||||||
|
} from '../../types/dimentorin';
|
||||||
|
import { TResponseMessage } from '../../types/common';
|
||||||
|
|
||||||
|
export const getMentors = async (
|
||||||
|
params?: TMentorListParams
|
||||||
|
): Promise<TMentorDetail[]> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/dimentorin/mentors/public',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMentorDetail = async (id: string): Promise<TMentorDetail> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/dimentorin/mentors/public/${id}`,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMentorMe = async (): Promise<TMentorDetail> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/dimentorin/mentors/me',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMentorMeStatus = async (): Promise<{ status: string }> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/dimentorin/mentors/me/status',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMentorAvailability = async (
|
||||||
|
id: string
|
||||||
|
): Promise<TMentorAvailability> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/dimentorin/mentors/${id}/availability`,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMentorSessions = async (
|
||||||
|
id: string
|
||||||
|
): Promise<TSessionListResponse> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/dimentorin/mentors/${id}/sessions`,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMySessions = async (): Promise<TSessionListResponse> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/dimentorin/sessions/me',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const postBookSession = async (
|
||||||
|
mentorId: string,
|
||||||
|
payload: TBookSessionRequest
|
||||||
|
): Promise<TResponseMessage> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/dimentorin/mentors/${mentorId}/sessions/create`,
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const postSessionFeedback = async (
|
||||||
|
sessionId: string,
|
||||||
|
payload: TSessionFeedbackRequest
|
||||||
|
): Promise<TResponseMessage> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/dimentorin/sessions/${sessionId}/feedback/create`,
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||