Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72dbe3c261 | ||
|
|
da7f30659a | ||
|
|
c3da21d642 | ||
|
|
81894bf0cb | ||
|
|
9851c3b9e4 | ||
|
|
f4baae0749 | ||
|
|
bc03ca23e9 | ||
|
|
7321d28a76 | ||
|
|
890afa98db | ||
|
|
62e18e5f7e | ||
|
|
b266aa8796 | ||
|
|
57222b655c | ||
|
|
72888bd090 | ||
|
|
a5bde58bc8 | ||
|
|
302ee37ca1 |
@@ -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';
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
return <>Hallo</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -119,3 +119,9 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
const { composePlugins, withNx } = require('@nx/next');
|
const { composePlugins, withNx } = require('@nx/next');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
/** @type {import('@nx/next/plugins/with-nx').WithNxOptions} */
|
/** @type {import('@nx/next/plugins/with-nx').WithNxOptions} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
nx: { svgr: false },
|
nx: { svgr: false },
|
||||||
|
output: 'standalone',
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = composePlugins(withNx)(nextConfig);
|
module.exports = composePlugins(withNx)(nextConfig);
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
landing:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/landing.Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: landing
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN npm run backoffice:build
|
||||||
|
|
||||||
|
FROM nginx:alpine AS runner
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
COPY --from=builder /app/dist/apps/backoffice /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN npm run dimentorin:build
|
||||||
|
|
||||||
|
FROM nginx:alpine AS runner
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
COPY --from=builder /app/dist/apps/dimentorin /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN npm run gacha:build
|
||||||
|
|
||||||
|
FROM nginx:alpine AS runner
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
COPY --from=builder /app/dist/apps/gacha /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run landing:build
|
||||||
|
|
||||||
|
FROM node:22-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/dist/apps/landing/.next/standalone .
|
||||||
|
COPY --from=builder /app/dist/apps/landing/public apps/landing/public
|
||||||
|
COPY --from=builder /app/dist/apps/landing/.next/static dist/apps/landing/.next/static
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENTRYPOINT ["node", "apps/landing/server.js"]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { cn, Show } from "@imphnen-frontend-service/utils"
|
||||||
|
import { DetailedHTMLProps, FC, HTMLAttributes, useState } from "react"
|
||||||
|
import { AnimatePresence, motion, Variants } from "framer-motion"
|
||||||
|
import { MinusOutlined, PlusOutlined } from "@ant-design/icons"
|
||||||
|
|
||||||
|
export type AccordionOptions = {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AccordionProps = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & AccordionOptions & {
|
||||||
|
titleClassName?: string
|
||||||
|
contentClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Accordion: FC<AccordionProps> = ({ title, description, className, titleClassName, contentClassName, ...rest }) => {
|
||||||
|
const [expand, setExpand] = useState(false)
|
||||||
|
|
||||||
|
const variants: Variants = {
|
||||||
|
expand: { opacity: 1, y: 0, marginTop: 16, height: 'auto' },
|
||||||
|
collapse: { opacity: 0, y: -10, marginTop: 0, height: 0 },
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("bg-white rounded-lg shadow cursor-pointer pb-4", className)} {...rest}>
|
||||||
|
<div className="px-5 pt-4 select-none flex justify-between items-center gap-x-2" onClick={() => setExpand(prev => !prev)}>
|
||||||
|
<h4 className={cn("text-primary-500 font-semibold text-lg leading-tight md:text-xl", titleClassName)}>{title}</h4>
|
||||||
|
<div className={cn("text-primary-500 transition-all duration-300", expand && "rotate-90")}>
|
||||||
|
<Show condition={expand} fallback={<PlusOutlined />}>
|
||||||
|
<MinusOutlined className="rotate-90" />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AnimatePresence>
|
||||||
|
{expand && (
|
||||||
|
<motion.p
|
||||||
|
className={cn("mt-4 px-5 text-neutral-500 text-sm font-medium cursor-text md:text-base", contentClassName)}
|
||||||
|
variants={variants}
|
||||||
|
initial="collapse"
|
||||||
|
animate="expand"
|
||||||
|
exit="collapse"
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</motion.p>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './accordion';
|
||||||
@@ -4,3 +4,4 @@ export * from './input-field';
|
|||||||
export * from './pagination';
|
export * from './pagination';
|
||||||
export * from './modal/modal';
|
export * from './modal/modal';
|
||||||
export * from './stepper';
|
export * from './stepper';
|
||||||
|
export * from './accordion';
|
||||||
@@ -6,3 +6,4 @@ export * from './local-storage';
|
|||||||
export * from './cookies';
|
export * from './cookies';
|
||||||
export * from './session';
|
export * from './session';
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
|
export * from './logic';
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export interface ForProps<T, U> {
|
||||||
|
data: readonly T[]
|
||||||
|
children: (item: T, index: number) => U | null
|
||||||
|
fallback?: React.ReactNode | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A functional component that renders a list of children components from a given
|
||||||
|
* array of data.
|
||||||
|
*
|
||||||
|
* @param data - The array of data to render
|
||||||
|
* @param children - A function that takes the current item and index and returns
|
||||||
|
* the child component to render
|
||||||
|
* @param fallback - An optional fallback component to render when the data is empty
|
||||||
|
*
|
||||||
|
* @returns An array of rendered child components if the data is not empty, otherwise
|
||||||
|
* the fallback component if it is provided, null otherwise
|
||||||
|
*/
|
||||||
|
export function For<T, U extends React.JSX.Element>({
|
||||||
|
data,
|
||||||
|
children,
|
||||||
|
fallback = null
|
||||||
|
}: ForProps<T, U>): (U | null)[] | React.ReactNode | null {
|
||||||
|
if (!Array.isArray(data) || !data?.length) return fallback
|
||||||
|
|
||||||
|
return data.map((item, idx) => children(item, idx))
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './for'
|
||||||
|
export * from './show'
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export interface ShowProps {
|
||||||
|
condition: boolean
|
||||||
|
children: React.ReactNode
|
||||||
|
fallback?: React.ReactNode | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditionally renders the `children` component if `condition` is true. If
|
||||||
|
* `condition` is false, renders the `fallback` component instead.
|
||||||
|
*
|
||||||
|
* If `fallback` is `null`, renders nothing.
|
||||||
|
*
|
||||||
|
* @param condition - The condition to check
|
||||||
|
* @param children - The component to render if `condition` is true
|
||||||
|
* @param fallback - The component to render if `condition` is false
|
||||||
|
*/
|
||||||
|
export function Show({ condition, children, fallback = null }: ShowProps) {
|
||||||
|
return condition ? children : fallback
|
||||||
|
}
|
||||||