Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29937c9296 | ||
|
|
dd11bed598 | ||
|
|
09a11b4aec | ||
|
|
16628168ce | ||
|
|
be5e8d4b39 | ||
|
|
ca8d2d702f | ||
|
|
ddca7a5ccf | ||
|
|
e871d453a8 | ||
|
|
07e5bab31f | ||
|
|
98c3c2a326 | ||
|
|
69c934e8c4 | ||
|
|
522f283140 | ||
|
|
c04ca8a29d |
@@ -1,33 +0,0 @@
|
|||||||
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
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 351 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 511 KiB |
@@ -1,73 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { FC } from "react";
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import { For } from "../../../_components/logic/for";
|
|
||||||
|
|
||||||
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 = () => {
|
|
||||||
return (
|
|
||||||
<section 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 }}
|
|
||||||
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>
|
|
||||||
|
|
||||||
<div className="grid gap-6 max-w-7xl mx-auto md:grid-cols-3">
|
|
||||||
<For data={OFFERS}>
|
|
||||||
{(offer) => (
|
|
||||||
<motion.div
|
|
||||||
className="relative aspect-[3/2] overflow-hidden rounded-2xl md:aspect-[3/4] lg:aspect-[5/6]"
|
|
||||||
initial={{ opacity: 0, y: 30 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
|
||||||
import { HeroSection } from './_components/hero-section';
|
|
||||||
import { WhatWeOfferSection } from './_components/what-we-offer-section';
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<HeroSection />
|
|
||||||
<WhatWeOfferSection />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { CloseOutlined, MenuOutlined } from "@ant-design/icons";
|
|
||||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils";
|
|
||||||
import { FC, useState } from "react";
|
|
||||||
import { Link, useLocation } from "react-router-dom";
|
|
||||||
import { For } from "../../_components/logic/for";
|
|
||||||
import { Show } from "../../_components/logic/show";
|
|
||||||
|
|
||||||
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 [expandMenu, setExpandMenu] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full px-8 pt-8 top-0 z-50 md:px-[60px] md:pt-[60px] lg:px-20 sticky">
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Outlet } from "react-router-dom";
|
|
||||||
import { Header } from "./_components/header";
|
|
||||||
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { useSendOTP } from '@imphnen-frontend-service/utils';
|
|
||||||
|
|
||||||
export const useResendOtpHook = () => {
|
|
||||||
|
|
||||||
const { resendOTP, isLoading } = useSendOTP();
|
|
||||||
|
|
||||||
return {
|
|
||||||
resendOTP
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,38 +1,12 @@
|
|||||||
import { FC, ReactElement, useEffect, useState } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
import { ControlledInputField, RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
|
import { ControlledInputField, RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
|
||||||
import { useOtpHook } from '../../../_hooks/use-otp';
|
import { useOtpHook } from '../../../_hooks/use-otp';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useResendOtpHook } from '../../../_hooks/use-resend-otp';
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const { form, onSubmit, isLoading } = useOtpHook()
|
const { form, onSubmit, isLoading } = useOtpHook()
|
||||||
const [ searchParams ] = useSearchParams()
|
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 (
|
return (
|
||||||
<div className='flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]'>
|
<div className='flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]'>
|
||||||
@@ -52,13 +26,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
maxLength={6}
|
maxLength={6}
|
||||||
control={form.control}
|
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>
|
<Button className="xl:w-full mt-2" disabled={(!form.formState.isValid || isLoading)}>Linked Start !!!</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
return <>Hallo</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -10,7 +10,7 @@ const mappingPublicRoutes = [
|
|||||||
'/auth/login',
|
'/auth/login',
|
||||||
'/auth/forgot',
|
'/auth/forgot',
|
||||||
'/auth/forgot/otp',
|
'/auth/forgot/otp',
|
||||||
'/auth/register',
|
'/auth/register',
|
||||||
'/auth/register/otp',
|
'/auth/register/otp',
|
||||||
'/auth/register/success',
|
'/auth/register/success',
|
||||||
'/auth/new-password',
|
'/auth/new-password',
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
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);
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
services:
|
|
||||||
landing:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: docker/landing.Dockerfile
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
restart: unless-stopped
|
|
||||||
container_name: landing
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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;"]
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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;"]
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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;"]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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"]
|
|
||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
TLoginRequest,
|
TLoginRequest,
|
||||||
TLoginResponse,
|
TLoginResponse,
|
||||||
TRegisterRequest,
|
TRegisterRequest,
|
||||||
TSendOTPRequest,
|
|
||||||
TVerifyEmailRequest,
|
TVerifyEmailRequest,
|
||||||
} from '../../types/auth';
|
} from '../../types/auth';
|
||||||
import { TResponseMessage } from '../../types/common';
|
import { TResponseMessage } from '../../types/common';
|
||||||
@@ -36,18 +35,7 @@ export const postVerifyEmail = async (
|
|||||||
const { data } = await api({
|
const { data } = await api({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/auth/verify-email',
|
url: '/auth/verify-email',
|
||||||
data: {otp: parseInt(payload.otp), email: payload.email},
|
data: { otp: parseInt(payload.otp), email: payload.email },
|
||||||
});
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const postSendOtp = async (
|
|
||||||
payload: TSendOTPRequest
|
|
||||||
): Promise<TResponseMessage> => {
|
|
||||||
const { data } = await api({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/auth/send-otp',
|
|
||||||
data: payload,
|
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useMutation, UseMutationResult } from '@tanstack/react-query';
|
import { useMutation, UseMutationResult } from '@tanstack/react-query';
|
||||||
import { postLogin, postRegister, postSendOtp, postVerifyEmail } from '../../api/auth';
|
import { postLogin, postRegister, postVerifyEmail } from '../../api/auth';
|
||||||
import {
|
import {
|
||||||
TLoginRequest,
|
TLoginRequest,
|
||||||
TLoginResponse,
|
TLoginResponse,
|
||||||
TRegisterRequest,
|
TRegisterRequest,
|
||||||
TSendOTPRequest,
|
|
||||||
TVerifyEmailRequest,
|
TVerifyEmailRequest,
|
||||||
} from '../../types/auth';
|
} from '../../types/auth';
|
||||||
|
|
||||||
@@ -45,16 +44,3 @@ export const usePostVerifyEmail = (): UseMutationResult<
|
|||||||
mutationFn: async (payload) => await postVerifyEmail(payload),
|
mutationFn: async (payload) => await postVerifyEmail(payload),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePostSendOTP = (): UseMutationResult<
|
|
||||||
TResponseMessage,
|
|
||||||
TResponseError,
|
|
||||||
TSendOTPRequest,
|
|
||||||
unknown
|
|
||||||
> => {
|
|
||||||
return useMutation({
|
|
||||||
mutationKey: ['post-send-otp'],
|
|
||||||
mutationFn: async (payload) => await postSendOtp(payload),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,3 @@ export type TVerifyEmailRequest = {
|
|||||||
email: string;
|
email: string;
|
||||||
otp: string;
|
otp: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSendOTPRequest = {
|
|
||||||
email: string
|
|
||||||
};
|
|
||||||
@@ -3,5 +3,4 @@ export * from './use-auth-store';
|
|||||||
export * from './use-modal-login';
|
export * from './use-modal-login';
|
||||||
export * from './use-session';
|
export * from './use-session';
|
||||||
export * from './use-register';
|
export * from './use-register';
|
||||||
export * from './use-otp';
|
export * from './use-otp';
|
||||||
export * from './use-send-otp';
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { TSendOTPRequest, usePostSendOTP } from '@imphnen-frontend-service/service';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export const useSendOTP = () => {
|
|
||||||
const { mutate, isPending } = usePostSendOTP();
|
|
||||||
|
|
||||||
const resendOTP = (payload: TSendOTPRequest) => {
|
|
||||||
mutate(payload, {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
toast.success('Berhasil Mengirim Ulang OTP');
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
toast.error(
|
|
||||||
err?.response?.data?.message ??
|
|
||||||
'Terjadi Kesalahan yang tidak diketahui'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
resendOTP,
|
|
||||||
isLoading: isPending,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user