Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c5c5db47b | ||
|
|
583b6b224e | ||
|
|
ce0579de5d | ||
|
|
7df4592a47 | ||
|
|
77eb755783 | ||
|
|
8c6a85090d | ||
|
|
a6134cb32a | ||
|
|
d9c5ff0b18 | ||
|
|
924354366c | ||
|
|
dce74c5c7c | ||
|
|
d4c263cad3 | ||
|
|
228f92e9ed | ||
|
|
e2be5105e9 | ||
|
|
eb5ff2e46c | ||
|
|
dcd4ef88d6 | ||
|
|
e1269f4daa | ||
|
|
487623e4fb | ||
|
|
eff44c42a1 | ||
|
|
3e232dcdc2 | ||
|
|
ec13f2c693 | ||
|
|
aa52b62ae7 | ||
|
|
3dec77bd0a | ||
|
|
ec81922001 | ||
|
|
d7b70b21d1 | ||
|
|
a7cf85dd66 | ||
|
|
bb22068856 | ||
|
|
209fa3fff1 | ||
|
|
935d23887c | ||
|
|
363f1582c9 | ||
|
|
1a9b47953a | ||
|
|
182ff7facc | ||
|
|
af8968ac01 | ||
|
|
4bd66af0ff | ||
|
|
73826fb4c3 | ||
|
|
2f9407e258 | ||
|
|
534bf0e10a | ||
|
|
df479a0a8d | ||
|
|
1948d2c67f |
@@ -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:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- '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() {
|
||||
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 */}
|
||||
<div className="absolute inset-0 -z-10 overflow-x-clip hidden md:block">
|
||||
<motion.div
|
||||
|
||||
@@ -2,8 +2,8 @@ import { CloseOutlined, MenuOutlined } from "@ant-design/icons";
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||
import { cn, For, Show } from "@imphnen-frontend-service/utils";
|
||||
import { motion, useMotionValueEvent, useScroll, Variants } from "framer-motion";
|
||||
import { FC, useState } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { FC, useMemo, useState } from "react";
|
||||
import { Link, NavLink, useLocation } from "react-router-dom";
|
||||
|
||||
const MENUS: { label: string; href: string }[] = [
|
||||
{ label: "Home", href: "/" },
|
||||
@@ -30,8 +30,15 @@ export const Header: FC = () => {
|
||||
else setShow(true)
|
||||
})
|
||||
|
||||
const isActive = useMemo(() => {
|
||||
return (path: string) => {
|
||||
if (path === '/' && location.pathname !== '/') return false
|
||||
return location.pathname.includes(path)
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
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
|
||||
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"
|
||||
@@ -63,18 +70,22 @@ export const Header: FC = () => {
|
||||
variant="text"
|
||||
className={cn(
|
||||
"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>
|
||||
)}
|
||||
</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>
|
||||
|
||||
<div>
|
||||
<Link to="/auth/login">
|
||||
<Button type="button" className="px-5 py-2 hidden lg:block">Login</Button>
|
||||
</Link>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Icon } from "@iconify/react";
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||
import { cn, For, Show } from "@imphnen-frontend-service/utils";
|
||||
import { motion } from "framer-motion";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useGetArticleBySlug } from "@imphnen-frontend-service/service";
|
||||
|
||||
export default function DetailArticle() {
|
||||
const { slug } = useParams()
|
||||
const { data: article, isLoading, isError } = useGetArticleBySlug(slug ?? "")
|
||||
|
||||
const paragraphs = article?.content.split("\n").filter((p) => p.trim()) ?? []
|
||||
const date = article?.created_at
|
||||
? new Date(article.created_at).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })
|
||||
: ""
|
||||
|
||||
return (
|
||||
<main>
|
||||
<motion.section
|
||||
className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||
>
|
||||
<Show
|
||||
condition={!isLoading && !isError && !!article}
|
||||
fallback={
|
||||
<div className="w-full max-w-[900px] mx-auto text-center text-neutral-500 py-20">
|
||||
{isError ? "Artikel tidak ditemukan." : "Memuat artikel..."}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="w-full max-w-[900px] mx-auto mb-8 md:mb-[60px]">
|
||||
<div className="mb-10 flex items-center justify-between">
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
className="flex gap-x-2 text-[10px] px-2.5 py-2 md:text-[15px]"
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
<Icon icon="material-symbols:arrow-back-rounded" />
|
||||
<span>Back</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
className="flex gap-x-2 text-[10px] px-2.5 py-2 md:text-[15px]"
|
||||
>
|
||||
<span>Read Next</span>
|
||||
<Icon icon="material-symbols:arrow-forward-rounded" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-center leading-tight">
|
||||
<h1
|
||||
className="text-[23px] text-neutral-800 font-semibold mb-6 md:text-[37px] md:mb-10 xl:text-[46px]"
|
||||
>
|
||||
{article?.title}
|
||||
</h1>
|
||||
<p
|
||||
className="text-[15px] font-semibold text-neutral-800 mb-2 md:text-[19px] md:mb-1 xl:text-[23px] xl:mb-2"
|
||||
>
|
||||
{article?.author_name || "IMPHNEN Editorial"}
|
||||
</p>
|
||||
<p className="text-xs text-neutral-600 mb-6 md:text-[15px] xl:text-[19px]">
|
||||
{date}
|
||||
</p>
|
||||
<div
|
||||
className="text-[10px] text-primary-600 font-medium bg-primary-200 px-3 py-1 rounded-4xl w-max mx-auto md:text-xs xl:text-[15px]"
|
||||
>
|
||||
{article?.category}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-[900px] mx-auto">
|
||||
<div className="mb-8 w-full h-auto aspect-16/10 overflow-hidden md:aspect-31/16 md:mb-[60px] xl:aspect-45/26">
|
||||
<img
|
||||
src={article?.cover_url ?? "/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp"}
|
||||
alt="Cover Article"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"text-balance text-[15px] font-medium text-neutral-600 space-y-8",
|
||||
"md:space-y-12 md:text-[19px] xl:text-[23px] xl:max-w-[780px] xl:mx-auto",
|
||||
)}
|
||||
>
|
||||
<p className="leading-tight">{article?.excerpt}</p>
|
||||
<For data={paragraphs}>
|
||||
{(paragraph) => (
|
||||
<p key={paragraph.slice(0, 40)} className="leading-tight">
|
||||
{paragraph}
|
||||
</p>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</motion.section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||
import { cn, Show } from "@imphnen-frontend-service/utils"
|
||||
import { FC } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { TArticleListItem } from "@imphnen-frontend-service/service"
|
||||
|
||||
export type ArticleCardProps = {
|
||||
variant?: "default" | "recent" | "featured"
|
||||
article?: TArticleListItem
|
||||
}
|
||||
|
||||
function formatDate(iso?: string) {
|
||||
if (!iso) return ""
|
||||
return new Date(iso).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })
|
||||
}
|
||||
|
||||
export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default', article }) => {
|
||||
const title = article?.title ?? "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
const excerpt = article?.excerpt ?? "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
const author = article?.author_name ?? "IMPHNEN Editorial"
|
||||
const date = formatDate(article?.created_at)
|
||||
const slug = article?.slug ?? ""
|
||||
const cover = article?.cover_url
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
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={cover ?? "/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp"}
|
||||
alt={title}
|
||||
className={cn("w-full object-cover", variant === "recent" && "h-full")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cn("leading-tight", variant === "recent" && "xl:flex-1")}>
|
||||
<div className="w-max bg-primary-200 text-primary-600 px-3 py-1 text-xs font-medium rounded-4xl mb-4 xl:mb-[15px]">
|
||||
{article?.category ?? "Artikel"}
|
||||
</div>
|
||||
<Link
|
||||
to={slug ? `/articles/${slug}` : "/articles"}
|
||||
className={cn(
|
||||
"text-[15px] text-neutral-800 font-semibold mb-4 line-clamp-2 md:text-[19px] xl:mb-[23px]",
|
||||
variant === "featured" && "text-[19px] md:text-[23px]",
|
||||
variant === "recent" && "mb-7 xl:text-[27px]"
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</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",
|
||||
)}
|
||||
>
|
||||
{excerpt}
|
||||
</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>{author}</p>
|
||||
<p>{date}</p>
|
||||
</div>
|
||||
|
||||
<Show condition={variant === "default"}>
|
||||
<Link to={slug ? `/articles/${slug}` : "/articles"}>
|
||||
<Button type="button" size="sm" className="px-2.5 py-2">
|
||||
Baca Artikel
|
||||
</Button>
|
||||
</Link>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { ReactElement, useRef, useState } from "react";
|
||||
import { ArticleCard } from "./_components/card/article";
|
||||
import { cn, For, Show } from "@imphnen-frontend-service/utils";
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||
import { motion, useInView, Variants } from "framer-motion";
|
||||
import { useGetArticleCategories, useGetArticles } from "@imphnen-frontend-service/service";
|
||||
|
||||
export default function Components(): ReactElement {
|
||||
const [activeTab, setActiveTab] = useState<string>("Semua")
|
||||
const { data: articles = [], isLoading } = useGetArticles({ per_page: 20 })
|
||||
const { data: categories = [] } = useGetArticleCategories()
|
||||
|
||||
const tabs = ["Semua", ...categories]
|
||||
|
||||
const featured = articles[0]
|
||||
const recent = articles.slice(1, 4)
|
||||
const filtered = activeTab === "Semua"
|
||||
? articles
|
||||
: articles.filter((a) => a.category === activeTab)
|
||||
|
||||
const ref = useRef(null)
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||
|
||||
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>
|
||||
|
||||
<Show
|
||||
condition={!isLoading}
|
||||
fallback={<p className="text-neutral-500">Memuat artikel...</p>}
|
||||
>
|
||||
<div className="grid gap-12 xl:grid-cols-2">
|
||||
<motion.div variants={childVariants}>
|
||||
<ArticleCard variant="featured" article={featured} />
|
||||
</motion.div>
|
||||
|
||||
<div>
|
||||
<motion.h1
|
||||
className="text-[19px] text-neutral-800 font-semibold mb-10 md:text-[29px] xl:text-[37px] xl:mb-12"
|
||||
variants={childVariants}
|
||||
>
|
||||
Recent Articles
|
||||
</motion.h1>
|
||||
<div className="grid gap-10">
|
||||
<For data={recent}>
|
||||
{(article, index) => (
|
||||
<motion.div key={article.id ?? index} variants={childVariants}>
|
||||
<ArticleCard variant="recent" article={article} />
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</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={tabs}>
|
||||
{(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>
|
||||
|
||||
<Show
|
||||
condition={filtered.length > 0}
|
||||
fallback={<p className="text-neutral-500 text-center py-10">Belum ada artikel di kategori ini.</p>}
|
||||
>
|
||||
<div className="grid gap-8 xl:grid-cols-3">
|
||||
<For data={filtered}>
|
||||
{(article, index) => (
|
||||
<motion.div key={article.id ?? index} variants={childVariants}>
|
||||
<ArticleCard article={article} />
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</motion.section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export default function Layout() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-primary-50">
|
||||
<Header />
|
||||
<main className="flex-1 z-0">
|
||||
<main className="flex-1">
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
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"
|
||||
import { TBookSessionRequest, usePostBookSession } from "@imphnen-frontend-service/service"
|
||||
|
||||
const STEPS = ['topic', 'schedule', 'profile', 'payment', 'qr-payment', 'va-payment', 'success'] as const
|
||||
type Step = typeof STEPS[number]
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
mentorId?: string
|
||||
mentor?: { fullname?: string | null; current_role?: string; current_company?: string }
|
||||
}
|
||||
|
||||
export const AppointmentModal: FC<Props> = ({ open, setOpen, mentorId, mentor }) => {
|
||||
const [step, setStep] = useState<Step>('topic')
|
||||
const [selectedTopics, setSelectedTopics] = useState<number[]>([])
|
||||
const [booking, setBooking] = useState<TBookSessionRequest | null>(null)
|
||||
const [error, setError] = useState("")
|
||||
const bookMutation = usePostBookSession(mentorId ?? "")
|
||||
|
||||
const handleStep = async (action: 'next' | 'prev') => {
|
||||
setError("")
|
||||
if (step === 'payment' && action === 'next') {
|
||||
if (!mentorId || !booking) {
|
||||
setError("Data booking tidak lengkap.")
|
||||
return
|
||||
}
|
||||
try {
|
||||
await bookMutation.mutateAsync({ ...booking, topic: booking.topic || `Mentoring #${selectedTopics[0] ?? 1}` })
|
||||
setStep('success')
|
||||
} catch {
|
||||
setError("Gagal membuat sesi mentoring. Coba lagi.")
|
||||
}
|
||||
return
|
||||
}
|
||||
if (action === 'next' && step === 'success') {
|
||||
setOpen(false)
|
||||
} else if (action === 'next') {
|
||||
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 onChange={(data) => setBooking(data)} />}
|
||||
{step === 'profile' && <ProfileStep />}
|
||||
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} mentor={mentor} booking={booking} />}
|
||||
{step === 'qr-payment' && <QrisPaymentStep />}
|
||||
{step === 'va-payment' && <VAPaymentStep />}
|
||||
{step === 'success' && <SuccessStep />}
|
||||
</AnimatePresence>
|
||||
|
||||
{error && (
|
||||
<p className="text-danger-600 text-xs mt-4 font-medium">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Show condition={step !== 'success'}>
|
||||
<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={bookMutation.isPending ? "Memproses..." : "Bayar Sekarang"}>
|
||||
Selanjutnya
|
||||
</Show>
|
||||
</Show>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { For, Show } from "@imphnen-frontend-service/utils"
|
||||
import { FC } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import { TOPICS } from "../../../sections/topics"
|
||||
import { TBookSessionRequest } from "@imphnen-frontend-service/service"
|
||||
|
||||
type Props = {
|
||||
selectedTopics: number[]
|
||||
mentor?: { fullname?: string | null; current_role?: string; current_company?: string } | null
|
||||
booking?: TBookSessionRequest | null
|
||||
}
|
||||
|
||||
export const PaymentStep: FC<Props> = ({ selectedTopics, mentor, booking }) => {
|
||||
const rate = 50000
|
||||
const serviceFee = 2000
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="bg-white px-6 py-5 rounded-md grid gap-6 md:grid-cols-2 md:gap-8 xl:grid-cols-9"
|
||||
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]">
|
||||
{mentor?.fullname || "Mentor"}
|
||||
</h3>
|
||||
<p className="text-[10px] text-neutral-600 md:text-xs">
|
||||
{mentor?.current_role || "Mentor"} at {mentor?.current_company || "IMPHNEN"}
|
||||
</p>
|
||||
{booking?.scheduled_at && (
|
||||
<p className="text-[10px] text-neutral-500 md:text-xs mt-1">
|
||||
{new Date(booking.scheduled_at).toLocaleString("id-ID", { dateStyle: "full", timeStyle: "short" })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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. {rate.toLocaleString("id-ID")}</p>
|
||||
<p className="font-normal">Service Fee :</p>
|
||||
<p className="text-right">Rp. {serviceFee.toLocaleString("id-ID")}</p>
|
||||
|
||||
<hr className="border-neutral-200 col-span-full mt-1" />
|
||||
|
||||
<p className="text-neutral-700">Total :</p>
|
||||
<p className="text-right text-neutral-700">Rp. {(rate + serviceFee).toLocaleString("id-ID")}</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,80 @@
|
||||
import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms"
|
||||
import { cn } from "@imphnen-frontend-service/utils"
|
||||
import { motion } from "framer-motion"
|
||||
import { useEffect, useState } from "react"
|
||||
import { TBookSessionRequest } from "@imphnen-frontend-service/service"
|
||||
|
||||
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
|
||||
|
||||
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
|
||||
|
||||
Saya ingin tahu secara khusus tentang ___.
|
||||
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]')
|
||||
|
||||
type Props = {
|
||||
onChange: (data: TBookSessionRequest) => void
|
||||
}
|
||||
|
||||
export const ScheduleStep = ({ onChange }: Props) => {
|
||||
const [date, setDate] = useState("")
|
||||
const [time, setTime] = useState("")
|
||||
const [sessionType, setSessionType] = useState("online")
|
||||
const [description, setDescription] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
if (!date || !time) return
|
||||
onChange({
|
||||
topic: "Mentoring Session",
|
||||
scheduled_at: `${date}T${time}:00`,
|
||||
description,
|
||||
duration_minutes: 60,
|
||||
session_type: sessionType,
|
||||
})
|
||||
}, [date, time, sessionType, description])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="bg-white px-6 py-5 rounded-md md:px-6"
|
||||
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" value={date} onChange={(e) => setDate(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Waktu</label>
|
||||
<Input type="time" className="min-w-full w-full" value={time} onChange={(e) => setTime(e.target.value)} />
|
||||
</div>
|
||||
<div className="relative md:col-span-full">
|
||||
<label className={labelClass}>Lokasi</label>
|
||||
<Select className="min-w-full w-full" value={sessionType} onChange={(e) => setSessionType(e.target.value)}>
|
||||
<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}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)} />
|
||||
</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,84 @@
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||
import { For, Show } from "@imphnen-frontend-service/utils"
|
||||
import { useGetMentorAvailability } from "@imphnen-frontend-service/service"
|
||||
import { FC } from "react"
|
||||
|
||||
type Props = {
|
||||
onBook: () => void
|
||||
mentorId?: string
|
||||
}
|
||||
|
||||
const DAY_NAMES = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"]
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
const d = new Date(dateStr + "T00:00:00")
|
||||
return `${DAY_NAMES[d.getDay()]}, ${d.getDate()} ${d.toLocaleString("id-ID", { month: "long" })} ${d.getFullYear()}`
|
||||
}
|
||||
|
||||
function formatTime(timeStr: string) {
|
||||
const [h] = timeStr.split(":").map(Number)
|
||||
return `${String(h).padStart(2, "0")}:00 WIB`
|
||||
}
|
||||
|
||||
export const SenpaiScheduleSection: FC<Props> = ({ onBook, mentorId }) => {
|
||||
const { data: availability, isLoading } = useGetMentorAvailability(mentorId ?? "")
|
||||
|
||||
const slots = availability?.slots ?? []
|
||||
const byDate = new Map<string, typeof slots>()
|
||||
for (const s of slots) {
|
||||
if (!byDate.has(s.date)) byDate.set(s.date, [])
|
||||
byDate.get(s.date)!.push(s)
|
||||
}
|
||||
const days = [...byDate.entries()]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Schedule</h2>
|
||||
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md space-y-5">
|
||||
<Show
|
||||
condition={!isLoading && days.length > 0}
|
||||
fallback={
|
||||
<p className="text-xs text-neutral-500">
|
||||
{isLoading ? "Memuat jadwal senpai..." : "Belum ada jadwal tersedia."}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<For data={days}>
|
||||
{([date, daySlots]) => (
|
||||
<div key={date}>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<p className="text-[15px] font-medium">{formatDate(date)}</p>
|
||||
<p className="rounded-4xl py-1 px-2.5 text-xs font-medium bg-primary-200 text-primary-500">
|
||||
{daySlots.length} slot tersisa
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-2">
|
||||
<For data={daySlots}>
|
||||
{(schedule) => (
|
||||
<div
|
||||
key={`${schedule.date}-${schedule.time}`}
|
||||
className={`px-3 py-2 text-xs rounded-md font-semibold ${
|
||||
schedule.available
|
||||
? "bg-primary-300 text-primary-600"
|
||||
: "bg-neutral-200 text-neutral-400 line-through"
|
||||
}`}
|
||||
>
|
||||
{formatTime(schedule.time)}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
<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)} mentorId={mentor?.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<AppointmentModal open={open} setOpen={setOpen} mentorId={mentor?.id} mentor={mentor} />
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -51,6 +51,11 @@ export const Components: FC = (): ReactElement => {
|
||||
Daftar Disini
|
||||
</a>
|
||||
</div>
|
||||
<div className='text-center w-full mb-1'>
|
||||
<a href="/auth/register-mentor" className="text-primary-500 font-medium">
|
||||
Daftar Sebagai Mentor
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="flex-grow border-t border-blue-400 opacity-50"></div>
|
||||
<span className="px-3 text-blue-400">Or</span>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export const AppLayout: FC = (): ReactElement => {
|
||||
return (
|
||||
<main className="bg-primary-50 min-h-screen">
|
||||
<Outlet />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -0,0 +1,228 @@
|
||||
import { FC, ReactElement, useState } from 'react';
|
||||
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { InputField, RegisterMentorStep, SelectField } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { usePostRegisterMentor, TMentorRegisterRequest } from '@imphnen-frontend-service/service';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [step, setStep] = useState(1)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [form, setForm] = useState({
|
||||
email: "", password: "", confirmPassword: "",
|
||||
fullname: "", phone_number: "", gender: "", domicile: "",
|
||||
legal_name: "", phone_for_verification: "", identity_document_url: "",
|
||||
last_education: "", bio: "", linkedin_url: "", github_url: "",
|
||||
cv_url: "", portfolio_url: "", industries: "", expertise: "", languages: "",
|
||||
current_company: "", current_role: "", years_of_experience: "",
|
||||
topics_of_interest: "", preferred_mentee_level: "", preferred_mentoring_formats: "",
|
||||
availability_commitment: "", mentoring_rate_amount: "",
|
||||
})
|
||||
const setF = (k: string, v: string) => setForm((f) => ({ ...f, [k]: v }))
|
||||
const registerMutation = usePostRegisterMentor()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setError("")
|
||||
if (form.password !== form.confirmPassword) {
|
||||
setError("Password dan konfirmasi tidak sama.")
|
||||
return
|
||||
}
|
||||
const payload: TMentorRegisterRequest = {
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
fullname: form.fullname,
|
||||
phone_number: form.phone_number || null,
|
||||
identity_and_verification: {
|
||||
legal_name: form.legal_name || form.fullname,
|
||||
gender: form.gender || null,
|
||||
domicile: form.domicile || null,
|
||||
identity_document_url: form.identity_document_url || "https://example.com/identity.jpg",
|
||||
phone_for_verification: form.phone_for_verification || null,
|
||||
},
|
||||
professional_profile: {
|
||||
bio: form.bio,
|
||||
last_education: form.last_education || null,
|
||||
linkedin_url: form.linkedin_url || null,
|
||||
github_url: form.github_url || null,
|
||||
cv_url: form.cv_url || null,
|
||||
portfolio_url: form.portfolio_url || null,
|
||||
industries: form.industries.split(",").map((s) => s.trim()).filter(Boolean),
|
||||
expertise: form.expertise.split(",").map((s) => s.trim()).filter(Boolean),
|
||||
languages: form.languages.split(",").map((s) => s.trim()).filter(Boolean),
|
||||
current_company: form.current_company,
|
||||
current_role: form.current_role,
|
||||
years_of_experience: Number(form.years_of_experience) || 0,
|
||||
},
|
||||
mentoring_logistics: {
|
||||
topics_of_interest: form.topics_of_interest.split(",").map((s) => s.trim()).filter(Boolean),
|
||||
preferred_mentee_level: [form.preferred_mentee_level].filter(Boolean),
|
||||
preferred_mentoring_formats: form.preferred_mentoring_formats.split(",").map((s) => s.trim()).filter(Boolean),
|
||||
availability_commitment: form.availability_commitment,
|
||||
mentoring_rate_amount: Number(form.mentoring_rate_amount) || 0,
|
||||
},
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await registerMutation.mutateAsync(payload)
|
||||
navigate('/auth/register-mentor/pending')
|
||||
} catch (e: unknown) {
|
||||
const msg = (e as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
setError(msg || "Gagal mendaftar sebagai mentor. Coba lagi.")
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||
<div className="bg-white xl:min-w-[1130px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
<RegisterResetBanner />
|
||||
<div className="xl:border-2 xl:border-primary-500/50 xl:w-[726px] rounded-lg py-[32px] px-[48px] flex justify-center">
|
||||
<div className="xl:w-[714px]">
|
||||
<RegisterMentorStep step={step} />
|
||||
<h3 className="mt-5 text-3xl font-semibold text-primary-500">
|
||||
Register
|
||||
</h3>
|
||||
<h5 className="text-primary-500 font-medium">
|
||||
Welcome to the team, Mentor - powered by IMPHNEN
|
||||
</h5>
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<div className={`${step !== 1 ? 'hidden' : ''}`}>
|
||||
<div className='mt-7 mb-2'>
|
||||
<InputField
|
||||
label="Nama Lengkap" size="lg" className="w-full"
|
||||
placeholder="Nama Lengkap" name="fullname"
|
||||
value={form.fullname} onChange={(e) => setF('fullname', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<SelectField
|
||||
label="Jenis Kelamin" defaultValue="" size="lg" error=""
|
||||
value={form.gender} onChange={(e) => setF('gender', e.target.value)}
|
||||
>
|
||||
<option value="" disabled hidden>Pilih Jenis Kelamin</option>
|
||||
<option value="Laki-laki">Laki - Laki</option>
|
||||
<option value="Perempuan">Perempuan</option>
|
||||
</SelectField>
|
||||
<div className='my-2'>
|
||||
<InputField
|
||||
label="No Telp/Whatsapp Only" size="lg" className="w-full"
|
||||
placeholder="Nomor WhatsApp" name="phone_number"
|
||||
value={form.phone_number} onChange={(e) => setF('phone_number', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<SelectField
|
||||
label="Domisili" defaultValue="" size="lg" error=""
|
||||
value={form.domicile} onChange={(e) => setF('domicile', e.target.value)}
|
||||
>
|
||||
<option value="" disabled hidden>Pilih Domisili</option>
|
||||
<option value="Aceh">Aceh</option>
|
||||
<option value="Jawa Barat">Jawa Barat</option>
|
||||
</SelectField>
|
||||
<div className='mt-2'>
|
||||
<SelectField
|
||||
label="Pendidikan Terakhir" defaultValue="" size="lg" error=""
|
||||
value={form.last_education} onChange={(e) => setF('last_education', e.target.value)}
|
||||
>
|
||||
<option value="" disabled hidden>Apa pendidikan terakhir senpai</option>
|
||||
<option value="SMA">SMA</option>
|
||||
<option value="S1">S1</option>
|
||||
<option value="S2">S2</option>
|
||||
<option value="S3">S3</option>
|
||||
</SelectField>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${step !== 2 ? 'hidden' : ''}`}>
|
||||
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||
<InputField label="Perusahaan Saat Ini" size="lg" className="w-full"
|
||||
placeholder="Masukkan Perusahaan tempat kerja, Senpai~! ✨" name="current_company"
|
||||
value={form.current_company} onChange={(e) => setF('current_company', e.target.value)} />
|
||||
<InputField label="Masukan jabatan senpai di perusahaan" size="lg" className="w-full"
|
||||
placeholder="Jabatan" name="current_role"
|
||||
value={form.current_role} onChange={(e) => setF('current_role', e.target.value)} />
|
||||
<InputField label="Topik Keahlian Yang Ingin Diajarkan (pisahkan koma)" size="lg" className="w-full"
|
||||
placeholder="mis. Backend Developer, Rust" name="expertise"
|
||||
value={form.expertise} onChange={(e) => setF('expertise', e.target.value)} />
|
||||
<InputField label="Industri (pisahkan koma)" size="lg" className="w-full"
|
||||
placeholder="mis. Software, Education" name="industries"
|
||||
value={form.industries} onChange={(e) => setF('industries', e.target.value)} />
|
||||
<InputField label="Skill Utama / Bahasa (pisahkan koma)" size="lg" className="w-full"
|
||||
placeholder="mis. Rust, TypeScript" name="languages"
|
||||
value={form.languages} onChange={(e) => setF('languages', e.target.value)} />
|
||||
<InputField label="Tahun Pengalaman" type="number" size="lg" className="w-full"
|
||||
placeholder="mis. 3" name="years_of_experience"
|
||||
value={form.years_of_experience} onChange={(e) => setF('years_of_experience', e.target.value)} />
|
||||
<InputField label="Bio Singkat" size="lg" className="w-full"
|
||||
placeholder="Ceritakan pengalaman Anda" name="bio"
|
||||
value={form.bio} onChange={(e) => setF('bio', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${step !== 3 ? 'hidden' : ''}`}>
|
||||
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||
<InputField label="Topik Mentoring (pisahkan koma)" size="lg" className="w-full"
|
||||
placeholder="mis. Rust Programming, Backend" name="topics_of_interest"
|
||||
value={form.topics_of_interest} onChange={(e) => setF('topics_of_interest', e.target.value)} />
|
||||
<SelectField label="Tingkat Mentee" defaultValue="" size="lg" error=""
|
||||
value={form.preferred_mentee_level} onChange={(e) => setF('preferred_mentee_level', e.target.value)}>
|
||||
<option value="" disabled hidden>Pilih tingkat mentee</option>
|
||||
<option value="beginner">Beginner</option>
|
||||
<option value="intermediate">Intermediate</option>
|
||||
<option value="advanced">Advanced</option>
|
||||
</SelectField>
|
||||
<InputField label="Format Pengajaran (pisahkan koma, mis. online,offline)" size="lg" className="w-full"
|
||||
placeholder="online,offline" name="preferred_mentoring_formats"
|
||||
value={form.preferred_mentoring_formats} onChange={(e) => setF('preferred_mentoring_formats', e.target.value)} />
|
||||
<InputField label="Komitmen Ketersediaan" size="lg" className="w-full"
|
||||
placeholder="mis. 2 jam per minggu" name="availability_commitment"
|
||||
value={form.availability_commitment} onChange={(e) => setF('availability_commitment', e.target.value)} />
|
||||
<InputField label="Honorarium yang Diharapkan per Sesi" type="number" size="lg" className="w-full"
|
||||
placeholder="mis. 500000" name="mentoring_rate_amount"
|
||||
value={form.mentoring_rate_amount} onChange={(e) => setF('mentoring_rate_amount', e.target.value)} />
|
||||
<InputField label="URL LinkedIn" size="lg" className="w-full"
|
||||
placeholder="https://linkedin.com/in/..." name="linkedin_url"
|
||||
value={form.linkedin_url} onChange={(e) => setF('linkedin_url', e.target.value)} />
|
||||
<InputField label="URL Portofolio" size="lg" className="w-full"
|
||||
placeholder="Google Drive, GitHub, Behance" name="portfolio_url"
|
||||
value={form.portfolio_url} onChange={(e) => setF('portfolio_url', e.target.value)} />
|
||||
<h6 className='text-xs'>Silahkan isi dalam angka (Contoh:500.000)</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${step !== 4 ? 'hidden' : ''}`}>
|
||||
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||
<InputField type='email' label="Email" size="lg" className="w-full"
|
||||
placeholder="isi emailnya dong senpai!!!" name="email"
|
||||
value={form.email} onChange={(e) => setF('email', e.target.value)} />
|
||||
<InputField type='password' label="Buat Password" size="lg" className="w-full"
|
||||
placeholder="Buat Password" name="password"
|
||||
value={form.password} onChange={(e) => setF('password', e.target.value)} />
|
||||
<InputField type='password' label="Konfirmasi Password" size="lg" className="w-full"
|
||||
placeholder="Ulangi Password" name="confirmPassword"
|
||||
value={form.confirmPassword} onChange={(e) => setF('confirmPassword', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
{error && <p className="text-danger-600 text-xs mt-3 font-medium">{error}</p>}
|
||||
<div className='flex justify-end mt-4 gap-2'>
|
||||
<Button variant='secondary' onClick={() => setStep(step - 1)} type='button'
|
||||
className={`${step === 1 ? 'hidden' : ''}`}>
|
||||
<ArrowLeftOutlined /> Kembali
|
||||
</Button>
|
||||
<Button variant='primary' onClick={() => setStep(step + 1)} type='button'
|
||||
className={`${step === 4 ? 'hidden' : ''}`}>
|
||||
Lanjutkan <ArrowRightOutlined />
|
||||
</Button>
|
||||
<Button variant='primary' onClick={handleSubmit} type='button'
|
||||
className={`${step === 4 ? '' : 'hidden'}`} disabled={submitting}>
|
||||
{submitting ? "Mengirim..." : "Selesaikan"} <ArrowRightOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export const AppLayout: FC = (): ReactElement => {
|
||||
return (
|
||||
<main className="bg-primary-50 min-h-screen">
|
||||
<Outlet />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export const AppLayout: FC = (): ReactElement => {
|
||||
return (
|
||||
<main className="bg-primary-50 min-h-screen">
|
||||
<Outlet />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||