Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2be5105e9 | ||
|
|
eb5ff2e46c | ||
|
|
e1269f4daa | ||
|
|
487623e4fb | ||
|
|
eff44c42a1 | ||
|
|
ec13f2c693 | ||
|
|
aa52b62ae7 | ||
|
|
302ee37ca1 | ||
|
|
b7282f9c99 | ||
|
|
2c347d1cc5 | ||
|
|
3dec77bd0a | ||
|
|
ec81922001 | ||
|
|
d7b70b21d1 | ||
|
|
a7cf85dd66 | ||
|
|
791f43986d | ||
|
|
9f25941a4a | ||
|
|
aaf5b4262d | ||
|
|
bb22068856 | ||
|
|
209fa3fff1 | ||
|
|
935d23887c | ||
|
|
0f46244d2b | ||
|
|
363f1582c9 | ||
|
|
1a9b47953a | ||
|
|
dd03b3da85 | ||
|
|
182ff7facc | ||
|
|
af8968ac01 | ||
|
|
4bd66af0ff | ||
|
|
73826fb4c3 | ||
|
|
2f9407e258 | ||
|
|
534bf0e10a | ||
|
|
df479a0a8d | ||
|
|
1948d2c67f |
@@ -0,0 +1,33 @@
|
||||
name: Deploy Landing
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'apps/landing/**' # 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 ]; then
|
||||
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service
|
||||
else
|
||||
cd ~/imphnen-frontend-service && git pull
|
||||
fi
|
||||
echo "NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}" > ~/imphnen-frontend-service/apps/landing/.env
|
||||
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml down || true
|
||||
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml build
|
||||
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml up -d
|
||||
docker image prune -af
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 511 KiB |
@@ -0,0 +1,154 @@
|
||||
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 CTASection: 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 scaleVariant: Variants = {
|
||||
hidden: { scale: 0 },
|
||||
visible: {
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const contentVariant: Variants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<section ref={ref} className="w-full mx-auto px-8 py-4 md:px-[60px] md:py-8 lg:px-20">
|
||||
<motion.div
|
||||
className="relative max-w-7xl mx-auto bg-white rounded-lg border-2 border-primary-200 py-20 overflow-hidden md:rounded-3xl lg:py-24"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
>
|
||||
{/* Background gradients and shapes */}
|
||||
<div className="absolute inset-0">
|
||||
<motion.div
|
||||
className={cn("absolute top-8 -left-14 size-28 bg-gradient-to-tl from-primary-400 to-primary-300 rounded-full",
|
||||
"md:size-52 md:-top-20 md:-left-10 lg:size-72 lg:-top-16 lg:-left-14"
|
||||
)}
|
||||
variants={scaleVariant}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
/>
|
||||
<motion.div
|
||||
className={cn(
|
||||
"absolute top-24 -right-14 size-[89px] bg-gradient-to-b from-primary-400 to-primary-300 rounded-full blur-[1.8px]",
|
||||
"md:size-[182px] md:-right-24 md:blur-[2px] lg:size-[392px] lg:top-16 lg:-right-52 lg:blur-[4px]"
|
||||
)}
|
||||
variants={scaleVariant}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
/>
|
||||
<motion.div
|
||||
variants={scaleVariant}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
className={cn(
|
||||
"absolute bottom-10 -left-[88px] size-28 bg-primary-200 rounded-full flex justify-center items-center",
|
||||
"md:size-56 md:-bottom-28 md:-left-28 lg:size-[274px] lg:-bottom-24 lg:-left-24"
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: isInView ? 1 : 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
className="size-[72px] bg-primary-50 rounded-full md:size-40 lg:size-[200px]"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-x-0 bottom-0 h-1/4 bg-gradient-to-t from-primary-400 via-primary-200 opacity-90",
|
||||
"md:h-[22%] md:opacity-85 lg:h-1/4"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.img
|
||||
src="/logos/logo.svg"
|
||||
alt="IMPHNEN Logo"
|
||||
className="relative w-28 mx-auto mb-8 md:w-[186px]"
|
||||
variants={contentVariant}
|
||||
/>
|
||||
|
||||
<div className="relative mb-8 max-w-52 mx-auto md:max-w-[446px] lg:max-w-[688px]">
|
||||
<motion.h1
|
||||
className={cn(
|
||||
"mb-3 text-xl text-center font-semibold leading-tight text-primary-500",
|
||||
"md:text-4xl md:mb-2 lg:text-5xl lg:font-bold"
|
||||
)}
|
||||
variants={contentVariant}
|
||||
>
|
||||
Start Your IT Journey Now!
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
className={cn(
|
||||
"mb-6 text-[15px] font-medium text-center leading-tight text-primary-400",
|
||||
"md:text-2xl md:mb-8 lg:text-[29px]"
|
||||
)}
|
||||
variants={contentVariant}
|
||||
>
|
||||
Jangan biarkan progress mu hanya jadi side story!
|
||||
</motion.p>
|
||||
<motion.p
|
||||
className={cn(
|
||||
"text-xs font-semibold text-center leading-tight text-primary-400",
|
||||
"md:text-[19px] lg:text-[23px] lg:max-w-[628px] lg:mx-auto"
|
||||
)}
|
||||
variants={contentVariant}
|
||||
>
|
||||
Dapatkan mentor terbaik dan gunakan AI untuk belajar lebih cepat. Waktunya jadi protagonist dalam perjalanan IT-mu!
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="relative max-w-52 mx-auto space-y-3 md:max-w-full md:flex md:gap-x-6 md:justify-center md:space-y-0 md:items-center"
|
||||
variants={contentVariant}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full h-auto py-2 text-[10px] font-medium md:w-max md:text-sm md:px-2.5 md:font-semibold lg:text-base"
|
||||
>
|
||||
Mulai Belajar Dengan AI
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="w-full h-auto py-2 text-[10px] font-medium md:w-max md:text-sm md:px-2.5 md:font-semibold lg:text-base"
|
||||
>
|
||||
Temukan Mentor
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { Accordion, AccordionOptions } from '@imphnen-frontend-service/ui/molecules'
|
||||
import { FC, useRef } from 'react'
|
||||
import { motion, useInView, Variants } from 'framer-motion'
|
||||
import { cn, For } from '@imphnen-frontend-service/utils'
|
||||
|
||||
const FAQ_DATA: AccordionOptions[] = [
|
||||
{ title: 'Apakah saya bisa memilih mentor sendiri?', description: 'Tentu! Kamu bisa memilih mentor yang sesuai dengan kebutuhan dan level skill kamu.' },
|
||||
{ title: 'Apakah mentoring ini cocok untuk pemula?', description: 'Yap! Baik newbie maupun seasoned dev bisa menemukan mentor yang pas untuk mempercepat progres belajarnya.' },
|
||||
{ title: 'Apakah ini seperti sekolah coding biasa?', description: 'Nope! Ini lebih dari sekadar kursus-kursus AI guide dan mentor berpengalaman, kamu mendapatkan pengalaman belajar yang jauh lebih personal dan efektif.' },
|
||||
]
|
||||
|
||||
export const FAQSection: 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="relative mx-auto py-4 px-8 overflow-x-clip md:py-8 md:px-[60px] lg:px-20 md:pb-8">
|
||||
{/* Background shapes */}
|
||||
<div className="absolute inset-0">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: isInView ? 1 : 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className={cn(
|
||||
"absolute -top-10 -right-14 size-60 bg-primary-200 rounded-full justify-center items-center hidden",
|
||||
"md:flex lg:size-[344px] lg:top-48 lg:-right-[88px]"
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: isInView ? 1 : 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
className="size-[168px] bg-primary-50 rounded-full lg:size-64"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative mb-6 flex justify-center md:mb-10"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
className="h-auto px-3 py-2 font-semibold bg-primary-100 md:text-base md:px-5 md:py-2.5 lg:text-2xl lg:px-6 lg:py-3"
|
||||
>
|
||||
Frequently Ask Question
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"relative grid max-w-7xl mx-auto gap-y-7 divide-y-2 divide-primary-200",
|
||||
"md:grid-cols-11 md:gap-x-10 md:divide-y-0 md:divide-x-2 lg:grid-cols-7 lg:gap-x-[72px]"
|
||||
)}
|
||||
>
|
||||
<motion.h1
|
||||
className={cn(
|
||||
"pb-7 text-2xl font-semibold text-primary-500 text-center leading-none",
|
||||
"md:pe-10 md:pb-0 md:text-start md:col-span-5 lg:text-4xl lg:col-span-3 lg:pe-[72px]"
|
||||
)}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
Console log for new developers
|
||||
</motion.h1>
|
||||
|
||||
<motion.div
|
||||
className="flex flex-col gap-4 md:col-span-6 lg:col-span-4"
|
||||
variants={containerVariants}
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
>
|
||||
<For data={FAQ_DATA}>
|
||||
{(props) => (
|
||||
<motion.div key={props.title} variants={childVariants}>
|
||||
<Accordion {...props} />
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
export function HeroSection() {
|
||||
return (
|
||||
<section className="relative w-full 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
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="absolute top-10 -left-64 size-[448px] bg-primary-200 rounded-full justify-center items-center hidden lg:flex"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
className="size-[296px] bg-primary-50 rounded-full"
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="absolute top-3/5 -right-56 size-96 bg-primary-500 rounded-full flex justify-center items-center lg:top-1/2 lg:-right-64 lg:size-[600px]"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
className="size-64 bg-primary-50 rounded-full lg:size-[400px]"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<div className="absolute -top-28 -left-20 size-[248px] bg-radial from-primary-300 rounded-full blur-3xl" />
|
||||
<div className="absolute top-0 -right-72 size-[480px] bg-radial from-primary-400 rounded-full blur-[80px]" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-y-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="flex flex-col items-center gap-y-6"
|
||||
>
|
||||
<h1 className="text-2xl font-semibold text-primary-500 md:text-4xl md:font-bold lg:text-5xl">DIMENTORIN</h1>
|
||||
<p className="max-w-52 text-center font-semibold text-xl leading-tight text-primary-500 md:max-w-72 lg:text-4xl lg:max-w-lg">
|
||||
Learn IT smarter with AI & expert mentors!
|
||||
</p>
|
||||
<p className="max-w-3xs text-xs text-neutral-500 text-center font-medium md:max-w-lg md:text-base lg:text-lg lg:max-w-[646px]">
|
||||
Apakah kamu siap untuk grind skill TI-mu ke level Ultimate? Dengan AI sebagai navigator dan mentor profesional sebagai sensei-mu, perjalanan belajarmu akan lebih cepat, efektif, dan penuh EXP!
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.2 }} viewport={{ once: true }}>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="text-[10px] h-auto px-2.5 py-2 md:text-sm"
|
||||
>
|
||||
Belajar Dengan AI Sekarang
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { For } from '@imphnen-frontend-service/utils'
|
||||
import { FC, useRef } from 'react'
|
||||
import { motion, useInView, Variants } from 'framer-motion'
|
||||
import { StarFilled } from '@ant-design/icons'
|
||||
|
||||
export const TestimonialSection: 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="mx-auto py-4 md:py-8 xl:px-20">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
className="h-auto px-3 py-2 font-semibold bg-primary-100 md:text-base md:px-5 md:py-2.5 lg:text-2xl lg:px-6 lg:py-3"
|
||||
>
|
||||
Testimonial
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="my-6 text-lg font-semibold mx-auto text-center text-primary-500 md:text-2xl lg:my-10 lg:text-4xl"
|
||||
>
|
||||
Words from our dellow devs
|
||||
</motion.h1>
|
||||
|
||||
<div className="scrollbar-hide w-full mx-auto max-w-7xl px-8 overflow-auto md:px-[60px] xl:px-0">
|
||||
<motion.div
|
||||
className="mb-2 flex gap-x-5 min-w-max w-auto md:gap-x-6 xl:min-w-full xl:max-w-7xl xl:grid xl:grid-cols-2"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
<For data={Array.from({ length: 2 })}>
|
||||
{(_, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={childVariants}
|
||||
className="flex flex-col gap-6 p-6 w-52 bg-white rounded-xl shadow-md md:w-xl md:flex-row md:gap-8 xl:w-full"
|
||||
>
|
||||
<div className="w-full aspect-[8/7] rounded-lg overflow-hidden md:w-40 md:aspect-[7/6] lg:aspect-square">
|
||||
<img
|
||||
src="/image/testimonial.webp"
|
||||
alt="Testimonial"
|
||||
className="w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="mb-3 font-semibold text-primary-500 md:text-lg lg:text-2xl">
|
||||
Riko, Junior Web Developer
|
||||
</h3>
|
||||
<p className="mb-6 text-[10px] text-neutral-500 font-medium md:text-sm lg:text-base">
|
||||
Rasanya seperti punya AI waifu yang ngajarin ngoding! Mentoringnya juga super insightful. Thanks, Dimentorin!
|
||||
</p>
|
||||
<motion.div className="w-max" whileHover={{ scale: 1.1, rotate: -3 }}>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
className="bg-primary-50 flex items-center gap-x-2 h-auto px-2 py-1 hover:bg-primary-50"
|
||||
>
|
||||
<StarFilled className="md:text-lg" />
|
||||
<span>
|
||||
<span className="text-sm font-medium md:text-lg md:font-semibold lg:text-xl">4.8</span>
|
||||
<span className="text-[10px] text-primary-300 md:text-sm md:font-medium lg:text-base">/5.0</span>
|
||||
</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||
import { For } from "@imphnen-frontend-service/utils";
|
||||
import { FC, useRef } from "react";
|
||||
import { motion, useInView, Variants } from 'framer-motion'
|
||||
|
||||
const OFFERS: { title: string; description: string; image: string }[] = [
|
||||
{ title: "AI-Powered Learning", description: "Level Up Instantly!", image: "/image/what-we-offer/ai-powered-learning.webp" },
|
||||
{ title: "1-on-1 Mentoring", description: "Sensei dari Dunia Nyata!", image: "/image/what-we-offer/1-on-1-mentoring.webp" },
|
||||
{ title: "Community & Resources", description: "Connect dengan Fellow Devs!", image: "/image/what-we-offer/community.webp" },
|
||||
]
|
||||
|
||||
export const WhatWeOfferSection: 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 mx-auto px-8 pb-4 md:px-[60px] lg:px-20 lg:pt-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="mb-6 flex justify-center lg:mb-10"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
className="h-auto px-3 py-2 font-semibold bg-primary-100 md:text-base md:px-5 md:py-2.5 lg:text-2xl lg:px-6 lg:py-3"
|
||||
>
|
||||
Apa yang Kami Tawarkan
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-6 max-w-7xl mx-auto md:grid-cols-3"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
>
|
||||
<For data={OFFERS}>
|
||||
{(offer) => (
|
||||
<motion.div
|
||||
key={offer.title}
|
||||
className="relative aspect-[16/10] overflow-hidden rounded-lg md:aspect-[24/35] lg:aspect-[6/7]"
|
||||
variants={childVariants}
|
||||
>
|
||||
<img src={offer.image} alt={offer.title} className="size-full object-cover" />
|
||||
<div
|
||||
className="absolute bottom-0 inset-x-0 p-4 h-1/2 text-primary-50 bg-gradient-to-t from-primary-500 via-primary-500/80 flex flex-col items-center justify-end gap-y-1 md:items-start lg:px-6 lg:pb-8"
|
||||
>
|
||||
<p className="font-semibold lg:text-[26px] lg:font-bold">{offer.title}</p>
|
||||
<p className="text-xs lg:text-[22px] lg:font-medium">{offer.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</motion.div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { HeroSection } from './_components/hero-section';
|
||||
import { WhatWeOfferSection } from './_components/what-we-offer-section';
|
||||
import { TestimonialSection } from './_components/testimonial-section';
|
||||
import { FAQSection } from './_components/faq-section';
|
||||
import { CTASection } from './_components/cta-section';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
return (
|
||||
<>
|
||||
<HeroSection />
|
||||
<WhatWeOfferSection />
|
||||
<TestimonialSection />
|
||||
<FAQSection />
|
||||
<CTASection />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
@@ -0,0 +1,157 @@
|
||||
import { DiscordOutlined, GithubOutlined, InstagramOutlined } from '@ant-design/icons';
|
||||
import { cn, For } from '@imphnen-frontend-service/utils';
|
||||
import React, { FC, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion, useInView, Variants } from 'framer-motion';
|
||||
import { SteamIcon } from '../../_components/icons';
|
||||
|
||||
const PAGES: { label: string; href: string }[] = [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: 'Mentoring', href: '/mentoring' },
|
||||
{ label: 'Resources', href: '/resources' },
|
||||
{ label: 'Articles', href: '/articles' },
|
||||
]
|
||||
|
||||
const COMMUNITY: { label: string; href: string; icon: React.ReactNode }[] = [
|
||||
{ label: 'Discord', href: 'https://discord.gg/imphnen', icon: <DiscordOutlined /> },
|
||||
{ label: 'Steam', href: 'https://steamcommunity.com/groups/IMPHNEN', icon: <SteamIcon /> },
|
||||
{ label: 'Github', href: 'https://github.com/IMPHNEN', icon: <GithubOutlined /> },
|
||||
{ label: 'Instagram', href: 'https://www.instagram.com/imphnen.dev', icon: <InstagramOutlined /> },
|
||||
]
|
||||
|
||||
export const Footer: FC = () => {
|
||||
const ref = useRef(null)
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
}
|
||||
|
||||
const childVariants: Variants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const mascotVariants: Variants = {
|
||||
hidden: { opacity: 0, y: 20, rotate: -12 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
rotate: 0,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const mascotTextVariants: Variants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
delay: 0.4,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<footer ref={ref} className="w-full mx-auto px-8 py-4 md:px-[60px] md:pt-8 md:pb-16 lg:px-20 lg:pb-20">
|
||||
<motion.div
|
||||
className={cn(
|
||||
"relative max-w-7xl mx-auto px-10 py-8 bg-primary-500 text-primary-50 grid gap-10 rounded-lg",
|
||||
"lg:grid-cols-3 lg:rounded-xl"
|
||||
)}
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
>
|
||||
{/* Mascot */}
|
||||
<motion.div
|
||||
className="hidden absolute bottom-0 right-0 size-40 md:block lg:size-72"
|
||||
variants={mascotVariants}
|
||||
>
|
||||
<motion.img
|
||||
src="/image/mascot-character.webp"
|
||||
alt="IMPHNEN Mascot"
|
||||
className="w-full h-auto absolute inset-x-0 bottom-0"
|
||||
/>
|
||||
<motion.img
|
||||
src="/image/mascot-text.webp"
|
||||
alt="IMPHNEN Mascot"
|
||||
className="w-4/6 absolute top-0 left-0"
|
||||
variants={mascotTextVariants}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="text-center lg:text-start lg:mb-16">
|
||||
<motion.img
|
||||
src="/logos/logo.svg"
|
||||
alt="IMPHNEN Logo"
|
||||
className="w-[84px] mb-6 mx-auto md:w-32 lg:w-40 lg:ms-0"
|
||||
variants={childVariants}
|
||||
/>
|
||||
|
||||
<motion.h4 className="mb-2 font-semibold text-[15px] md:mb-2.5 md:text-xl" variants={childVariants}>
|
||||
Dimentorin by IMPHNEN
|
||||
</motion.h4>
|
||||
<motion.p className="font-medium text-xs md:text-[15px] md:max-w-[232px] md:mx-auto lg:ms-0" variants={childVariants}>
|
||||
Karena Belajar IT Itu Harusnya Se-Seru Anime Favoritmu!
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<motion.p className="relative hidden text-xs font-medium text-[15px] lg:block" variants={childVariants}>
|
||||
© IMPHNEN {new Date().getFullYear()} All Rights Reserved
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<motion.div className="relative grid gap-10 md:flex md:justify-center lg:gap-x-20" variants={childVariants}>
|
||||
<div className="text-center text-xs md:text-[15px] lg:text-start">
|
||||
<h4 className="mb-5 font-medium md:mb-7 lg:text-xl">Pages</h4>
|
||||
<ul className="font-semibold space-y-5">
|
||||
<For data={PAGES}>
|
||||
{({ label, href }) => (
|
||||
<li key={label}>
|
||||
<Link to={href}>{label}</Link>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-xs md:text-[15px] lg:text-start">
|
||||
<h4 className="mb-5 font-medium md:mb-7 lg:text-xl">Join Our Community</h4>
|
||||
<ul className="font-semibold space-y-5">
|
||||
<For data={COMMUNITY}>
|
||||
{({ label, href, icon }) => (
|
||||
<li key={label}>
|
||||
<Link to={href} target="_blank" className="flex items-center justify-center gap-x-3 lg:justify-start">
|
||||
<span className="text-primary-900">{icon}</span>
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.p className="relative text-center text-xs font-medium md:text-[15px] lg:hidden" variants={childVariants}>
|
||||
© IMPHNEN {new Date().getFullYear()} All Rights Reserved
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
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";
|
||||
|
||||
const MENUS: { label: string; href: string }[] = [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: 'Mentoring', href: '/mentoring' },
|
||||
{ label: 'Resources', href: '/resources' },
|
||||
{ label: 'Articles', href: '/articles' },
|
||||
]
|
||||
|
||||
export const Header: FC = () => {
|
||||
const location = useLocation();
|
||||
const { scrollY } = useScroll()
|
||||
|
||||
const [expandMenu, setExpandMenu] = useState(false);
|
||||
const [show, setShow] = useState(true)
|
||||
|
||||
const headerVariants: Variants = {
|
||||
hidden: { opacity: 0, y: -100 },
|
||||
show: { opacity: 1, y: 0 },
|
||||
};
|
||||
|
||||
useMotionValueEvent(scrollY, "change", (latest) => {
|
||||
const prev = scrollY.getPrevious() || 0
|
||||
if (latest > prev && latest > 100) setShow(false)
|
||||
else setShow(true)
|
||||
})
|
||||
|
||||
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">
|
||||
<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"
|
||||
variants={headerVariants}
|
||||
animate={show ? "show" : "hidden"}
|
||||
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between p-4 md:px-8 md:py-2.5">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src="/logos/simple.svg"
|
||||
alt="IMPHNEN Logo"
|
||||
className="h-8 md:h-10 lg:h-[52px] w-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
className={cn(
|
||||
"flex flex-col items-center py-2 fixed top-24 bg-white shadow-lg rounded-lg transition-all duration-300",
|
||||
"md:top-32 lg:static lg:py-0 lg:flex-row lg:bg-transparent lg:shadow-none lg:gap-x-4",
|
||||
!expandMenu ? "-right-96" : "right-8 md:right-[60px]"
|
||||
)}
|
||||
>
|
||||
<For data={MENUS}>
|
||||
{(menu) => (
|
||||
<Button
|
||||
key={menu.label}
|
||||
type="button"
|
||||
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"
|
||||
)}
|
||||
>
|
||||
<Link to={menu.href}>{menu.label}</Link>
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
<Button type="button" className="px-12 py-1 md:hidden">Login</Button>
|
||||
</nav>
|
||||
|
||||
<div>
|
||||
<Button type="button" className="px-5 py-2 hidden lg:block">Login</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
className={cn("transition-transform duration-300 rotate-0 lg:hidden", expandMenu && "rotate-90")}
|
||||
onClick={() => setExpandMenu(prev => !prev)}
|
||||
>
|
||||
<Show condition={!expandMenu} fallback={<CloseOutlined />}>
|
||||
<MenuOutlined />
|
||||
</Show>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Header } from "./_components/header";
|
||||
import { Footer } from "./_components/footer";
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-primary-50">
|
||||
<Header />
|
||||
<main className="flex-1 z-0">
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './steam';
|
||||
@@ -12,12 +12,13 @@ export const useLogin = () => {
|
||||
mode: 'all',
|
||||
});
|
||||
|
||||
const { signIn } = useSession();
|
||||
const { signIn, isLoading } = useSession();
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => signIn(data));
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
isLoading
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
TVerifyOtpRequest,
|
||||
verifyEmailSchema,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useOtp } from '@imphnen-frontend-service/utils';
|
||||
|
||||
export const useOtpHook = () => {
|
||||
const form = useForm<TVerifyOtpRequest>({
|
||||
resolver: zodResolver(verifyEmailSchema),
|
||||
mode: 'all',
|
||||
});
|
||||
|
||||
const { otp, isLoading } = useOtp();
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => otp(data));
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
isLoading
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
authRegisterSchema,
|
||||
TRegisterRequest,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRegister } from '@imphnen-frontend-service/utils';
|
||||
|
||||
export const useRegisterHook = () => {
|
||||
const form = useForm<TRegisterRequest>({
|
||||
resolver: zodResolver(authRegisterSchema),
|
||||
mode: 'all',
|
||||
});
|
||||
|
||||
const { register, isLoading } = useRegister();
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => register(data));
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
isLoading
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useSendOTP } from '@imphnen-frontend-service/utils';
|
||||
|
||||
export const useResendOtpHook = () => {
|
||||
|
||||
const { resendOTP, isLoading } = useSendOTP();
|
||||
|
||||
return {
|
||||
resendOTP
|
||||
};
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { useLogin } from '../../_hooks/use-login';
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const { form, onSubmit } = useLogin();
|
||||
const { form, onSubmit, isLoading } = useLogin();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||
@@ -42,7 +42,7 @@ export const Components: FC = (): ReactElement => {
|
||||
Lupa Password ?
|
||||
</a>
|
||||
</div>
|
||||
<Button className="w-full" type='submit'>Enter Isekai</Button>
|
||||
<Button className="w-full" type='submit' disabled={(!form.formState.isValid || isLoading)}>Enter Isekai</Button>
|
||||
</form>
|
||||
|
||||
<div className="flex my-3 gap-3 justify-center">
|
||||
@@ -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>
|
||||
@@ -74,6 +79,13 @@ export const Components: FC = (): ReactElement => {
|
||||
<ArrowLeftOutlined />
|
||||
Kembali Ke Homepage
|
||||
</Button>
|
||||
<Button
|
||||
className='xl:hidden w-full gap-3'
|
||||
variant='secondary'
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
Kembali Ke Homepage
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,242 @@
|
||||
import { FC, ReactElement, useState } from 'react';
|
||||
import { ControlledInputField, RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button, Select } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { InputField, RegisterMentorStep, SelectField } from '@imphnen-frontend-service/ui/molecules';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [ step, setStep ] = useState(1)
|
||||
|
||||
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]">
|
||||
<Button
|
||||
className='xl:hidden gap-3'
|
||||
variant='secondary'
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
Login
|
||||
</Button>
|
||||
<RegisterMentorStep step={step} />
|
||||
<h3 className="mt-5 text-3xl font-semibold text-primary-500">
|
||||
Register
|
||||
</h3>
|
||||
<h5 className="text-primary-500 font-medium">
|
||||
Welcome to the team, Mentor - powered by IMPHNEN
|
||||
</h5>
|
||||
<form>
|
||||
<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'}
|
||||
/>
|
||||
</div>
|
||||
<SelectField
|
||||
label="Jenis Kelamin"
|
||||
defaultValue=""
|
||||
size="lg"
|
||||
error=""
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Pilih Jenis Kelamin
|
||||
</option>
|
||||
<option value="laki-laki">Laki - Laki</option>
|
||||
<option value="perempuan">Perempuan</option>
|
||||
</SelectField>
|
||||
<div className='my-2'>
|
||||
<InputField
|
||||
label="No Telp/Whatsapp Only"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Nama Lengkap"
|
||||
name={'fullname'}
|
||||
/>
|
||||
</div>
|
||||
<SelectField
|
||||
label="Domisili"
|
||||
defaultValue=""
|
||||
size="lg"
|
||||
error=""
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Pilih Domisili
|
||||
</option>
|
||||
<option value="aceh">Aceh</option>
|
||||
<option value="jawa-selatan">Jawa Selatan</option>
|
||||
</SelectField>
|
||||
<div className='mt-2'>
|
||||
<SelectField
|
||||
label="Pendidikan Terakhir"
|
||||
defaultValue=""
|
||||
size="lg"
|
||||
error=""
|
||||
>
|
||||
<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~! ✨ (Pastikan tidak typo, ya~ 😆)"
|
||||
name={'fullname'}
|
||||
/>
|
||||
<InputField
|
||||
label="Masukan jabatan senpai di perusahaan"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Masukan jabatan senpai di perusahaan"
|
||||
name={'fullname'}
|
||||
/>
|
||||
<SelectField
|
||||
label="Topik Keahlian Yang Ingin Diajarkan"
|
||||
defaultValue=""
|
||||
size="lg"
|
||||
error=""
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Apa yang ingin anda ajarkan senpaii
|
||||
</option>
|
||||
<option value="Frontend Developer">Frontend Developer</option>
|
||||
<option value="Backend Developer">Backend Developer</option>
|
||||
<option value="Other">Other</option>
|
||||
</SelectField>
|
||||
<InputField
|
||||
label="Jika Mengisi “Other”, Tulis Topik yang Kamu Kuasai"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Tolong isi jika tidak ada di pilihan senpai"
|
||||
name={'fullname'}
|
||||
/>
|
||||
<InputField
|
||||
label="Skill Utama yang Kamu Miliki (boleh lebih dari satu)"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Isi Skill nya dong senpai"
|
||||
name={'fullname'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${step !== 3 ? 'hidden' : ''}`}>
|
||||
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||
<SelectField
|
||||
label="Durasi Sesi Mentoring"
|
||||
defaultValue=""
|
||||
size="lg"
|
||||
error=""
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Placeholder
|
||||
</option>
|
||||
</SelectField>
|
||||
<SelectField
|
||||
label="Format Pengajaran yang Anda Sediakan"
|
||||
defaultValue=""
|
||||
size="lg"
|
||||
error=""
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Placeholder
|
||||
</option>
|
||||
</SelectField>
|
||||
<InputField
|
||||
type="file"
|
||||
label="Unggah Curiculum Vitae"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Unggah Portofolio"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Google Drive, Behance, GitHub, Notion, dan lainnya"
|
||||
/>
|
||||
<InputField
|
||||
type='number'
|
||||
label="Honorarium yang Diharapkan per Sesi (Online) Durasi rata-rata sesi: 2,5–3 jam"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Isi Nominalnya dong senpaiii!!!"
|
||||
/>
|
||||
<h6 className='text-xs'>Silahkan isi dalam angka (Contoh:500.000)</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${step !== 4 ? 'hidden' : ''}`}>
|
||||
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||
<InputField
|
||||
type='email'
|
||||
label="Email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="isi emailnya dong senpai!!!"
|
||||
/>
|
||||
<InputField
|
||||
type='password'
|
||||
label="Buat Password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Buat Password"
|
||||
/>
|
||||
<InputField
|
||||
type='password'
|
||||
label="Buat Password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Buat Password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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={() => setStep(step + 1)}
|
||||
type='button'
|
||||
className={`${step === 4 ? '' : 'hidden'}`}
|
||||
>
|
||||
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;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { FC, ReactElement, useEffect } from 'react';
|
||||
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||
<div className="bg-white xl:min-w-[1220px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
<RegisterResetBanner />
|
||||
<div className="border-2 border-primary-500/50 xl:w-[696px] rounded-lg py-[32px] px-[48px] flex justify-center bg-primary-50">
|
||||
<div className="xl:w-[704px] flex flex-col justify-center items-center">
|
||||
<h2 className="text-4xl text-primary-500 font-semibold">
|
||||
Register Akun Mentor Berhasil
|
||||
</h2>
|
||||
<h4 className="mt-3 text-primary-500 font-medium text-xl">
|
||||
Akun Kamu Telah Berhasil Diverifikasi!
|
||||
</h4>
|
||||
<svg
|
||||
width="198"
|
||||
height="198"
|
||||
viewBox="0 0 198 198"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mt-10"
|
||||
>
|
||||
<circle cx="99" cy="99" r="99" fill="#BCF8B0" />
|
||||
<rect
|
||||
x="31.1152"
|
||||
y="32.9141"
|
||||
width="133.971"
|
||||
height="133.971"
|
||||
rx="66.9857"
|
||||
fill="#35BA43"
|
||||
/>
|
||||
<path
|
||||
d="M127.935 75.8846H122.722C121.991 75.8846 121.297 76.2202 120.85 76.7945L90.0997 115.748L75.355 97.0657C75.1319 96.7825 74.8476 96.5535 74.5234 96.3959C74.1992 96.2384 73.8435 96.1563 73.483 96.1558H68.2697C67.77 96.1558 67.4941 96.7301 67.7999 97.1179L88.2277 122.998C89.1824 124.206 91.0171 124.206 91.9792 122.998L128.405 76.8392C128.711 76.4589 128.435 75.8846 127.935 75.8846Z"
|
||||
fill="#E0FBD8"
|
||||
/>
|
||||
</svg>
|
||||
<h4 className="mt-10 text-primary-500 font-medium text-xl text-center">
|
||||
Kamu sekarang bisa login dan mulai menggunakan layanan kami. Selamat bergabung! SENPAIIII
|
||||
</h4>
|
||||
<a href="/auth/login">
|
||||
<Button className="gap-3 mt-10" size="lg">
|
||||
Masuk Jalur Mentor Isekai
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
</a>
|
||||
</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,70 @@
|
||||
import { FC, ReactElement, useEffect, useState } from 'react';
|
||||
import { ControlledInputField, RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
|
||||
import { useOtpHook } from '../../../_hooks/use-otp';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useResendOtpHook } from '../../../_hooks/use-resend-otp';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const { form, onSubmit, isLoading } = useOtpHook()
|
||||
const [ searchParams ] = useSearchParams()
|
||||
const [ disabled, setDisabled] = useState(true);
|
||||
const [ timeLeft, setTimeLeft ] = useState(5 * 60);
|
||||
const { resendOTP } = useResendOtpHook()
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
const interval = setInterval(() => {
|
||||
setTimeLeft(prev => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(interval);
|
||||
setDisabled(false);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]'>
|
||||
<div className='bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6'>
|
||||
<RegisterResetBanner />
|
||||
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[48px] flex flex-col justify-center'>
|
||||
<h3 className='mt-5 text-3xl font-semibold text-primary-500'>Verifikasi Email</h3>
|
||||
<h5 className='mt-2 text-xl font-medium text-primary-500'>Yeay~! Pesan dari dunia lain sudah dikirimkan ke {searchParams.get("email")}</h5>
|
||||
<form onSubmit={onSubmit} className='mt-7'>
|
||||
<ControlledInputField
|
||||
label="OTP"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="XXXXXX"
|
||||
max={6}
|
||||
name={'otp'}
|
||||
maxLength={6}
|
||||
control={form.control}
|
||||
/>
|
||||
<Button type="button" className="mt-2" disabled={disabled} onClick={
|
||||
() => {
|
||||
resendOTP({email: searchParams.get("email") || ""})
|
||||
setDisabled(true)
|
||||
setTimeLeft(5 * 60)
|
||||
}
|
||||
}>Kirim ulang otp {disabled? `(${formatTime(timeLeft)})`: ""}</Button>
|
||||
<Button className="xl:w-full mt-2" disabled={(!form.formState.isValid || isLoading)}>Linked Start !!!</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
@@ -1,96 +1,108 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { ControlledInputField, RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { useRegisterHook } from '../../_hooks/use-register';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const { form, onSubmit, isLoading } = useRegisterHook();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||
<div className="bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
<div className="bg-white xl:min-w-[1130px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
<RegisterResetBanner />
|
||||
<div className="border-2 border-primary-500/50 w-[696px] rounded-lg py-[32px] px-[48px] flex justify-center">
|
||||
<div className="w-[704px]">
|
||||
<div className="xl:border-2 xl:border-primary-500/50 xl:w-[726px] rounded-lg py-[32px] px-[48px] flex justify-center">
|
||||
<div className="xl:w-[714px]">
|
||||
<Button
|
||||
className='xl:hidden gap-3'
|
||||
variant='secondary'
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
Login
|
||||
</Button>
|
||||
<h3 className="mt-5 text-3xl font-semibold text-primary-500">
|
||||
Register
|
||||
</h3>
|
||||
<h5 className="text-primary-500 font-medium">
|
||||
Yosha~! Saatnya Bergabung dengan Dimentorin
|
||||
</h5>
|
||||
<div className="grid grid-flow-row-dense my-7 grid-cols-2 gap-2">
|
||||
<InputField
|
||||
label="First Name"
|
||||
size="lg"
|
||||
placeholder="Nama Depan"
|
||||
/>
|
||||
<InputField
|
||||
label="Last Name"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Nama Belakang"
|
||||
/>
|
||||
<InputField
|
||||
label="Email"
|
||||
size="lg"
|
||||
placeholder="Contoh : yourname@mail.com"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="font-medium">OTP Code</h4>
|
||||
<Input
|
||||
className="w-full"
|
||||
size="lg"
|
||||
placeholder="Kode OTP"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Button size="sm" className="mt-[30px]">
|
||||
Kirim OTP
|
||||
</Button>
|
||||
</div>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className='my-7'>
|
||||
<ControlledInputField
|
||||
label="Full name"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Nama Lengkap"
|
||||
name={'fullname'}
|
||||
control={form.control}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InputField
|
||||
label="Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Buat password sekeren jurus ultimate-mu!"
|
||||
/>
|
||||
<h6 className="text-sm text-gray-500 mt-1">
|
||||
"Senpai~! Pastikan password-mu sekuat pertahanan kastil!"
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500 mt-2">
|
||||
Tips membuat password yang OP:
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500">
|
||||
- Minimal 8 karakter (semakin panjang, semakin power-up!{' '}
|
||||
<span role="img" aria-label="emoji">
|
||||
⚡
|
||||
</span>
|
||||
)
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500">
|
||||
- Campur huruf besar, kecil, angka, dan simbol untuk kombinasi
|
||||
ultimate!
|
||||
<span role="img" aria-label="emoji">
|
||||
🔥
|
||||
</span>
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500 mb-2">
|
||||
- Jangan pakai password yang gampang ditebak, nanti ketahuan
|
||||
musuh!
|
||||
<span role="img" aria-label="emoji">
|
||||
🚨
|
||||
</span>
|
||||
</h6>
|
||||
<InputField
|
||||
label="Repeat Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Ulangi password-mu, Senpai~!"
|
||||
/>
|
||||
<Button className="w-full mt-2">Linked Start !!!</Button>
|
||||
<div className="grid grid-flow-row-dense my-7 lg:grid-cols-2 gap-2">
|
||||
<ControlledInputField
|
||||
label="Email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Contoh : yourname@mail.com"
|
||||
type='email'
|
||||
control={form.control}
|
||||
name={'email'}
|
||||
/>
|
||||
<ControlledInputField
|
||||
label="No Hp"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Contoh : 088877665544"
|
||||
control={form.control}
|
||||
name={'phone_number'}
|
||||
/>
|
||||
</div>
|
||||
<ControlledInputField
|
||||
label="Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Buat password sekeren jurus ultimate-mu!"
|
||||
control={form.control}
|
||||
name={'password'}
|
||||
/>
|
||||
<h6 className="text-sm text-gray-500 mt-1">
|
||||
"Senpai~! Pastikan password-mu sekuat pertahanan kastil!"
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500 mt-2">
|
||||
Tips membuat password yang OP:
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500">
|
||||
- Minimal 8 karakter (semakin panjang, semakin power-up!{' '}
|
||||
<span role="img" aria-label="emoji">
|
||||
⚡
|
||||
</span>
|
||||
)
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500">
|
||||
- Campur huruf besar, kecil, angka, dan simbol untuk kombinasi
|
||||
ultimate!
|
||||
<span role="img" aria-label="emoji">
|
||||
🔥
|
||||
</span>
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500 mb-2">
|
||||
- Jangan pakai password yang gampang ditebak, nanti ketahuan
|
||||
musuh!
|
||||
<span role="img" aria-label="emoji">
|
||||
🚨
|
||||
</span>
|
||||
</h6>
|
||||
<ControlledInputField
|
||||
label="Repeat Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Ulangi password-mu, Senpai~!"
|
||||
control={form.control}
|
||||
name={"confirm_password"}
|
||||
/>
|
||||
<Button className="xl:w-full mt-2" disabled={(!form.formState.isValid || isLoading)}>Linked Start !!!</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { FC, ReactElement, useEffect } from 'react';
|
||||
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
document.location.href = "/auth/login"
|
||||
}, 10000)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||
<div className="bg-white min-w-[1220px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
<div className="bg-white xl:min-w-[1220px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
<RegisterResetBanner />
|
||||
<div className="border-2 border-primary-500/50 w-[696px] rounded-lg py-[32px] px-[48px] flex justify-center bg-primary-50">
|
||||
<div className="w-[704px] flex flex-col justify-center items-center">
|
||||
<div className="border-2 border-primary-500/50 xl:w-[696px] rounded-lg py-[32px] px-[48px] flex justify-center bg-primary-50">
|
||||
<div className="xl:w-[704px] flex flex-col justify-center items-center">
|
||||
<h2 className="text-4xl text-primary-500 font-semibold">
|
||||
Register Successfull
|
||||
</h2>
|
||||
@@ -41,10 +47,12 @@ export const Components: FC = (): ReactElement => {
|
||||
<h4 className="mt-10 text-primary-500 font-medium text-xl">
|
||||
Kamu akan memasuki isekai dalam 10 dtk
|
||||
</h4>
|
||||
<Button className="gap-3 mt-10" size="lg">
|
||||
Masuk Isekai
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
<a href="/auth/login">
|
||||
<Button className="gap-3 mt-10" size="lg">
|
||||
Masuk Isekai
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Navbar } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { FC, ReactElement } from 'react';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
return <>Hallo</>;
|
||||
return (
|
||||
<div className='bg-blue-50'>
|
||||
<Navbar />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
|
||||
@@ -119,3 +119,9 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
add404PageToRoutesChildren,
|
||||
addErrorElementToRoutes,
|
||||
convertPagesToRoute,
|
||||
ModalLoginProvider,
|
||||
QueryProvider,
|
||||
} from '@imphnen-frontend-service/utils';
|
||||
import { Toaster } from 'sonner';
|
||||
@@ -35,8 +36,10 @@ if (!rootElement) throw new Error('Failed to find the root element');
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<QueryProvider>
|
||||
<Toaster position="top-right" />
|
||||
<RouterProvider router={router} />
|
||||
<ModalLoginProvider>
|
||||
<Toaster position="top-right" />
|
||||
<RouterProvider router={router} />
|
||||
</ModalLoginProvider>
|
||||
</QueryProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,14 @@ const mappingPublicRoutes = [
|
||||
'/',
|
||||
'/auth/login',
|
||||
'/auth/forgot',
|
||||
'/auth/forgot/otp',
|
||||
'/auth/register',
|
||||
'/auth/register/otp',
|
||||
'/auth/register/success',
|
||||
'/auth/new-password',
|
||||
'/auth/register-mentor',
|
||||
'/auth/register-mentor/pending',
|
||||
'/auth/register-mentor/success'
|
||||
];
|
||||
|
||||
const mappingRoutePermissions = [
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { useLogin } from '../../_hooks/use-login';
|
||||
import ModalFormVerifyEmail from './modal-form-verify-email';
|
||||
|
||||
interface IModalFormLogin {
|
||||
isOpen: boolean;
|
||||
@@ -16,61 +17,87 @@ const ModalFormLogin = ({
|
||||
onForgotPassword,
|
||||
setIsOpenRegisterModal,
|
||||
}: IModalFormLogin) => {
|
||||
const { form, onSubmit } = useLogin();
|
||||
const {
|
||||
form,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
showVerifyModal,
|
||||
verifyForm,
|
||||
onVerifySubmit,
|
||||
closeVerifyModal,
|
||||
isVerifying,
|
||||
emailToVerify,
|
||||
} = useLogin();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Modal.Header className="space-y-4">
|
||||
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
||||
Login
|
||||
</h1>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form className="space-y-4" onSubmit={onSubmit}>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Email"
|
||||
placeholder="Masukkan Email"
|
||||
type="email"
|
||||
name="email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Password"
|
||||
placeholder="Masukkan Password"
|
||||
type="password"
|
||||
name="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<h1 className="text-end text-primary-500 text-xl font-medium">
|
||||
<Button variant="text" onClick={onForgotPassword}>
|
||||
Lupa Password?
|
||||
</Button>
|
||||
</h1>
|
||||
<Button type="submit" size="md" className="w-full">
|
||||
<>
|
||||
<Modal
|
||||
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||
isOpen={isOpen && !showVerifyModal}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Modal.Header className="space-y-4">
|
||||
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
||||
Login
|
||||
</Button>
|
||||
<div className="flex justify-center gap-2 pt-2.5 font-medium">
|
||||
<p className="text-neutral-500">Belum punya akun?</p>
|
||||
</h1>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form className="space-y-4" onSubmit={onSubmit}>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Email"
|
||||
placeholder="Masukkan Email"
|
||||
type="email"
|
||||
name="email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Password"
|
||||
placeholder="Masukkan Password"
|
||||
type="password"
|
||||
name="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<h1 className="text-end text-primary-500 text-xl font-medium">
|
||||
<Button variant="text" type="button" onClick={onForgotPassword}>
|
||||
Lupa Password?
|
||||
</Button>
|
||||
</h1>
|
||||
<Button
|
||||
variant="text"
|
||||
className="text-primary-500 hover:text-primary-600 m-0 p-0"
|
||||
onClick={() => setIsOpenRegisterModal(true)}
|
||||
type="submit"
|
||||
size="md"
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Daftar
|
||||
{isLoading ? 'Loading...' : 'Login'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
<div className="flex justify-center gap-2 pt-2.5 font-medium">
|
||||
<p className="text-neutral-500">Belum punya akun?</p>
|
||||
<Button
|
||||
variant="text"
|
||||
className="text-primary-500 hover:text-primary-600 m-0 p-0"
|
||||
onClick={() => setIsOpenRegisterModal(true)}
|
||||
>
|
||||
Daftar
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
|
||||
<ModalFormVerifyEmail
|
||||
isOpen={showVerifyModal}
|
||||
onClose={closeVerifyModal}
|
||||
verifyForm={verifyForm}
|
||||
onVerifySubmit={onVerifySubmit}
|
||||
isVerifying={isVerifying}
|
||||
email={emailToVerify}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
import { useRegister } from '../../_hooks/use-register';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { TRegisterRequest } from '@imphnen-frontend-service/service';
|
||||
import { useEffect } from 'react';
|
||||
import ModalFormVerifyEmail from './modal-form-verify-email';
|
||||
|
||||
interface IModalFormRegisterProps {
|
||||
isOpen: boolean;
|
||||
@@ -13,168 +10,100 @@ interface IModalFormRegisterProps {
|
||||
}
|
||||
|
||||
const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
|
||||
const { form, onSubmit, isStepOneValid } = useRegister();
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
nextStep,
|
||||
prevStep,
|
||||
resetStep,
|
||||
} = useQueryState('registerStep', {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
form,
|
||||
verifyForm,
|
||||
onSubmit,
|
||||
onVerifySubmit,
|
||||
showVerifyModal,
|
||||
closeVerifyModal,
|
||||
isVerifying,
|
||||
isRegistering,
|
||||
registeredEmail,
|
||||
} = useRegister();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isStepOneValid && currentStep === 2) {
|
||||
prevStep();
|
||||
}
|
||||
}, [isStepOneValid, currentStep, prevStep]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
>
|
||||
<Modal.Header className="space-y-4">
|
||||
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
||||
Register
|
||||
</h1>
|
||||
<h2 className="text-neutral-500 text-lg text-center">
|
||||
{currentStep === 1 ? 'Info Akun' : 'Informasi Pengiriman Hadiah'}
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
{currentStep === 1 && (
|
||||
<StepOne
|
||||
form={form}
|
||||
nextStep={nextStep}
|
||||
isStepOneValid={isStepOneValid}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 2 && <StepTwo form={form} prevStep={prevStep} />}
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepOneProps {
|
||||
form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>;
|
||||
nextStep: () => void;
|
||||
isStepOneValid: boolean;
|
||||
}
|
||||
|
||||
const StepOne = ({ form, nextStep, isStepOneValid }: IStepOneProps) => {
|
||||
return (
|
||||
<>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="fullname"
|
||||
label="Nama Lengkap"
|
||||
placeholder="Masukkan Nama Lengkap"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Email"
|
||||
placeholder="Masukkan Email Anda"
|
||||
type="email"
|
||||
name="email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="Masukkan Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="confirm_password"
|
||||
label="Ulangi Password"
|
||||
placeholder="Masukkan Ulang Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<Button
|
||||
size="md"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
disabled={!isStepOneValid}
|
||||
<Modal
|
||||
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||
isOpen={isOpen && !showVerifyModal}
|
||||
onClose={onClose}
|
||||
>
|
||||
Selanjutnya
|
||||
</Button>
|
||||
<Modal.Header className="space-y-4">
|
||||
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
||||
Register
|
||||
</h1>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="fullname"
|
||||
label="Nama Lengkap"
|
||||
placeholder="Masukkan Nama Lengkap"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Email"
|
||||
placeholder="Masukkan Email Anda"
|
||||
type="email"
|
||||
name="email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="Masukkan Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="confirm_password"
|
||||
label="Ulangi Password"
|
||||
placeholder="Masukkan Ulang Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="phone_number"
|
||||
label="Nomor Telepon"
|
||||
placeholder="Masukkan Nomor Telepon Aktif"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<Button
|
||||
size="md"
|
||||
className="w-full"
|
||||
type="submit"
|
||||
disabled={isRegistering || !form.formState.isValid}
|
||||
>
|
||||
{isRegistering ? 'Mendaftar...' : 'Register'}
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
|
||||
<ModalFormVerifyEmail
|
||||
isOpen={showVerifyModal}
|
||||
onClose={closeVerifyModal}
|
||||
verifyForm={verifyForm}
|
||||
onVerifySubmit={onVerifySubmit}
|
||||
isVerifying={isVerifying}
|
||||
email={registeredEmail}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>;
|
||||
prevStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ form, prevStep }: IStepTwoProps) => (
|
||||
<>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="phone_number"
|
||||
label="Nomor Telepon"
|
||||
placeholder="Masukkan Nomor Telepon Aktif"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="referral_code"
|
||||
label="Kode Referral"
|
||||
placeholder="Masukkan Kode Referral"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="referred_by"
|
||||
label="Referral By"
|
||||
placeholder="Masukkan Referral By"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="student_type"
|
||||
label="Tipe Pelajar"
|
||||
placeholder="Masukkan Tipe Pelajar"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex gap-6">
|
||||
<Button size="md" variant="text" className="w-[40%]" onClick={prevStep}>
|
||||
Kembali
|
||||
</Button>
|
||||
<Button size="md" className="w-full" type="submit">
|
||||
Gacha Sekarang
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export default ModalFormRegister;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
|
||||
interface IModalFormVerifyEmailProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
verifyForm: UseFormReturn<{ otp: string }, any>;
|
||||
onVerifySubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
isVerifying: boolean;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
const ModalFormVerifyEmail = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
verifyForm,
|
||||
onVerifySubmit,
|
||||
isVerifying,
|
||||
email,
|
||||
}: IModalFormVerifyEmailProps) => {
|
||||
return (
|
||||
<Modal
|
||||
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Modal.Header className="space-y-4">
|
||||
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
||||
Verifikasi Email
|
||||
</h1>
|
||||
<h2 className="text-neutral-500 text-lg text-center">
|
||||
Masukkan kode OTP yang telah dikirim ke {email ? email : 'email Anda'}
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onVerifySubmit} className="space-y-6">
|
||||
<ControlledInputField
|
||||
control={verifyForm.control}
|
||||
name="otp"
|
||||
label="Kode OTP"
|
||||
placeholder="Masukkan Kode OTP"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<Button
|
||||
size="md"
|
||||
className="w-full"
|
||||
type="submit"
|
||||
disabled={isVerifying}
|
||||
>
|
||||
{isVerifying ? 'Memverifikasi...' : 'Verifikasi'}
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalFormVerifyEmail;
|
||||
@@ -2,9 +2,13 @@ import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
authLoginSchema,
|
||||
TLoginRequest,
|
||||
usePostLogin,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useSession } from '@imphnen-frontend-service/utils';
|
||||
import { useAuthStore } from '@imphnen-frontend-service/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useVerifyEmail } from './use-verify-email';
|
||||
|
||||
export const useLogin = () => {
|
||||
const form = useForm<TLoginRequest>({
|
||||
@@ -12,12 +16,63 @@ export const useLogin = () => {
|
||||
mode: 'all',
|
||||
});
|
||||
|
||||
const { signIn } = useSession();
|
||||
const navigate = useNavigate();
|
||||
const { mutate, isPending } = usePostLogin();
|
||||
const { setLoading, setSession, clearSession } = useAuthStore();
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => signIn(data));
|
||||
const {
|
||||
openVerifyModal,
|
||||
showVerifyModal,
|
||||
verifyForm,
|
||||
onVerifySubmit,
|
||||
closeVerifyModal,
|
||||
isVerifying,
|
||||
emailToVerify,
|
||||
} = useVerifyEmail();
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
mutate(data, {
|
||||
onSuccess: (response) => {
|
||||
toast.success('Login sukses');
|
||||
|
||||
if (response.data.token && response.data.user) {
|
||||
setSession(response.data);
|
||||
} else {
|
||||
const { token, ...userData } = response.data;
|
||||
|
||||
const loginData = {
|
||||
token,
|
||||
userData,
|
||||
};
|
||||
setSession(loginData);
|
||||
}
|
||||
|
||||
navigate(0);
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error?.response?.data?.message;
|
||||
|
||||
if (errorMessage === 'Account not active, please verify your email') {
|
||||
toast.error('Akun belum aktif, silakan verifikasi email Anda');
|
||||
openVerifyModal(data.email);
|
||||
} else {
|
||||
toast.error(errorMessage ?? 'Terjadi Kesalahan yang tidak diketahui');
|
||||
clearSession();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
isLoading: isPending,
|
||||
showVerifyModal,
|
||||
verifyForm,
|
||||
onVerifySubmit,
|
||||
closeVerifyModal,
|
||||
isVerifying,
|
||||
emailToVerify,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,9 +6,20 @@ import {
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { toast } from 'sonner';
|
||||
import { useVerifyEmail } from './use-verify-email';
|
||||
|
||||
export const useRegister = () => {
|
||||
const postRegister = usePostRegister();
|
||||
const {
|
||||
verifyForm,
|
||||
onVerifySubmit,
|
||||
showVerifyModal,
|
||||
openVerifyModal,
|
||||
closeVerifyModal,
|
||||
isVerifying,
|
||||
emailToVerify,
|
||||
} = useVerifyEmail();
|
||||
|
||||
const form = useForm<TRegisterRequest>({
|
||||
resolver: zodResolver(authRegisterSchema),
|
||||
mode: 'all',
|
||||
@@ -23,13 +34,23 @@ export const useRegister = () => {
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
postRegister.mutate(data, {
|
||||
onSuccess: () => toast.success('Registrasi sukses'),
|
||||
onError: (error) => toast.error(error.message),
|
||||
onSuccess: () => {
|
||||
toast.success('Registrasi sukses. Silakan verifikasi email Anda.');
|
||||
openVerifyModal(data.email);
|
||||
},
|
||||
onError: (error) => toast.error(error.message || 'Registrasi gagal'),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
verifyForm,
|
||||
onSubmit,
|
||||
onVerifySubmit,
|
||||
showVerifyModal,
|
||||
closeVerifyModal,
|
||||
isVerifying,
|
||||
isRegistering: postRegister.isPending,
|
||||
registeredEmail: emailToVerify,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
TVerifyEmailRequest,
|
||||
usePostVerifyEmail,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const useVerifyEmail = () => {
|
||||
const [showVerifyModal, setShowVerifyModal] = useState(false);
|
||||
const [emailToVerify, setEmailToVerify] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const postVerifyEmail = usePostVerifyEmail();
|
||||
|
||||
const verifyForm = useForm<{ otp: string }>({
|
||||
mode: 'all',
|
||||
defaultValues: {
|
||||
otp: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onVerifySubmit = verifyForm.handleSubmit((data) => {
|
||||
const otpNumber = data.otp;
|
||||
|
||||
const verifyData: TVerifyEmailRequest = {
|
||||
email: emailToVerify,
|
||||
otp: otpNumber,
|
||||
};
|
||||
|
||||
postVerifyEmail.mutate(verifyData, {
|
||||
onSuccess: () => {
|
||||
toast.success('Verifikasi email sukses');
|
||||
|
||||
navigate(0);
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error.message || 'Verifikasi email gagal'),
|
||||
});
|
||||
});
|
||||
|
||||
const openVerifyModal = (email: string) => {
|
||||
setEmailToVerify(email);
|
||||
setShowVerifyModal(true);
|
||||
};
|
||||
|
||||
const closeVerifyModal = () => {
|
||||
setShowVerifyModal(false);
|
||||
verifyForm.reset();
|
||||
};
|
||||
|
||||
return {
|
||||
verifyForm,
|
||||
onVerifySubmit,
|
||||
showVerifyModal,
|
||||
openVerifyModal,
|
||||
closeVerifyModal,
|
||||
isVerifying: postVerifyEmail.isPending,
|
||||
emailToVerify,
|
||||
};
|
||||
};
|
||||
@@ -66,11 +66,15 @@ export const Components: FC = (): ReactElement => {
|
||||
<div className="order-1 lg:order-none flex flex-col col-span-4 justify-center items-center mb-8 relative">
|
||||
<div className="relative h-[150px] md:h-[230px]">
|
||||
<img
|
||||
width={400}
|
||||
height={400}
|
||||
src="/merch/1.png"
|
||||
alt="Merch 1"
|
||||
className="relative top-[3px] md:top-[6px] left-[4px] md:left-[7px] z-10 w-[165px] md:w-[266px]"
|
||||
/>
|
||||
<img
|
||||
width={400}
|
||||
height={400}
|
||||
className="absolute top-0 left-0 min-w-[174px] md:min-w-[280px] z-0"
|
||||
src="/merch/Vector-1.svg"
|
||||
alt=""
|
||||
@@ -88,7 +92,7 @@ export const Components: FC = (): ReactElement => {
|
||||
~ 175k ~
|
||||
</div>
|
||||
</div>
|
||||
<div className="order-2 flex flex-col col-span-4 justify-center items-center mb-8 relative overflow-hidden">
|
||||
<div className="order-2 flex flex-col col-span-4 justify-center items-center mb-8 relative">
|
||||
<div className="relative h-[130px] md:h-[230px]">
|
||||
<img
|
||||
width={400}
|
||||
@@ -105,8 +109,6 @@ export const Components: FC = (): ReactElement => {
|
||||
alt="Merch 3"
|
||||
/>
|
||||
<img
|
||||
width={400}
|
||||
height={400}
|
||||
className="absolute hidden lg:block -bottom-[100px] -left-[55px]"
|
||||
src="/landing-arrow-2.svg"
|
||||
alt="Merch 1"
|
||||
|
||||
@@ -35,7 +35,7 @@ if (!rootElement) throw new Error('Failed to find the root element');
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<QueryProvider>
|
||||
<Toaster />
|
||||
<Toaster position="top-right"/>
|
||||
<RouterProvider router={router} />
|
||||
</QueryProvider>
|
||||
</StrictMode>
|
||||
|
||||
@@ -1,109 +1,15 @@
|
||||
import {
|
||||
PERMISSIONS,
|
||||
SessionToken,
|
||||
SessionUser,
|
||||
} from '@imphnen-frontend-service/utils';
|
||||
import { LoaderFunctionArgs, redirect } from 'react-router';
|
||||
// import { SessionToken, SessionUser } from '@imphnen-frontend-service/utils';
|
||||
import { LoaderFunctionArgs } from 'react-router';
|
||||
|
||||
const mappingPublicRoutes = [
|
||||
'/',
|
||||
'/auth/login',
|
||||
'/auth/forgot',
|
||||
'/auth/new-password',
|
||||
];
|
||||
// const mappingPublicRoutes = ['/'];
|
||||
|
||||
const mappingRoutePermissions = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
permissions: [PERMISSIONS.USERS.READ_LIST],
|
||||
},
|
||||
{
|
||||
path: '/users/create',
|
||||
permissions: [PERMISSIONS.USERS.CREATE],
|
||||
},
|
||||
{
|
||||
path: '/users/update',
|
||||
permissions: [PERMISSIONS.USERS.UPDATE],
|
||||
},
|
||||
{
|
||||
path: '/users/detail',
|
||||
permissions: [PERMISSIONS.USERS.READ_DETAIL],
|
||||
},
|
||||
{
|
||||
path: '/roles',
|
||||
permissions: [PERMISSIONS.ROLES.READ_LIST],
|
||||
},
|
||||
{
|
||||
path: '/roles/create',
|
||||
permissions: [PERMISSIONS.ROLES.CREATE],
|
||||
},
|
||||
{
|
||||
path: '/roles/update',
|
||||
permissions: [PERMISSIONS.ROLES.UPDATE],
|
||||
},
|
||||
{
|
||||
path: '/roles/detail',
|
||||
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
|
||||
},
|
||||
{
|
||||
path: '/permissions',
|
||||
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
|
||||
},
|
||||
{
|
||||
path: '/permissions/create',
|
||||
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
|
||||
},
|
||||
{
|
||||
path: '/permissions/update',
|
||||
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
|
||||
},
|
||||
{
|
||||
path: '/permissions/detail',
|
||||
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
|
||||
},
|
||||
];
|
||||
|
||||
//TODO : Fix this later
|
||||
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
|
||||
// const fallback = mappingRoutePermissions.find((route) =>
|
||||
// route.permissions.some((perm) => userPermissions.includes(perm))
|
||||
// );
|
||||
// return redirect(fallback?.path ?? '/auth/login');
|
||||
// };
|
||||
// const mappingRoutePermissions = [
|
||||
// {
|
||||
// path: '/',
|
||||
// permissions: [],
|
||||
// },
|
||||
// ];
|
||||
|
||||
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
const session = SessionUser.get();
|
||||
const session_token = SessionToken.get();
|
||||
const token = session_token?.token?.access_token;
|
||||
const userPermissions =
|
||||
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
|
||||
|
||||
if (mappingPublicRoutes.includes(pathname)) {
|
||||
if (token) return redirect('/dashboard');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!session) return redirect('/auth/login');
|
||||
|
||||
const matchedRoute = mappingRoutePermissions.find(
|
||||
(route) => route.path === pathname
|
||||
);
|
||||
|
||||
if (matchedRoute) {
|
||||
const hasPermission =
|
||||
!matchedRoute.permissions ||
|
||||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
|
||||
|
||||
if (!hasPermission) {
|
||||
return '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
CMS_SECRET=your-very-long-secret-key
|
||||
CMS_POSTGRES_URL=postgres://user:password@host:port/database
|
||||
CMS_STORAGE_ENDPOINT=https://your-s3-endpoint.com
|
||||
CMS_STORAGE_ACCESS_KEY_ID=your-access-key-id
|
||||
CMS_STORAGE_SECRET_ACCESS_KEY=your-secret-access-key
|
||||
CMS_STORAGE_REGION=ap-southeast-1
|
||||
NEXT_PUBLIC_API_URL=
|
||||
|
||||
TURNSTILE_SECRET_KEY=
|
||||
NEXT_PUBLIC_TURNSTILE_SITEKEY=
|
||||