diff --git a/apps/landing/src/app/(frontend)/(auth)/_components/animated-background.tsx b/apps/landing/src/app/(frontend)/(auth)/_components/animated-background.tsx
new file mode 100644
index 0000000..60b146c
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/_components/animated-background.tsx
@@ -0,0 +1,86 @@
+'use client';
+
+import { motion } from 'framer-motion';
+import { useEffect, useMemo, useState } from 'react';
+import {
+ SiCplusplus,
+ SiCss3,
+ SiGo,
+ SiHtml5,
+ SiJavascript,
+ SiPhp,
+ SiPython,
+ SiRuby,
+ SiRust,
+ SiSwift,
+ SiTypescript,
+} from 'react-icons/si';
+
+export function AnimatedBackground() {
+ const [isClient, setIsClient] = useState(false);
+
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ const iconColorMap = useMemo(
+ () => [
+ { Icon: SiJavascript, color: '#F7DF1E' },
+ { Icon: SiTypescript, color: '#3178C6' },
+ { Icon: SiPython, color: '#3776AB' },
+ { Icon: SiCplusplus, color: '#00599C' },
+ { Icon: SiRuby, color: '#CC342D' },
+ { Icon: SiSwift, color: '#F05138' },
+ { Icon: SiRust, color: '#000000' },
+ { Icon: SiGo, color: '#00ADD8' },
+ { Icon: SiPhp, color: '#777BB4' },
+ { Icon: SiHtml5, color: '#E34F26' },
+ { Icon: SiCss3, color: '#1572B6' },
+ ],
+ []
+ );
+
+ const getRandom = (min: number, max: number) =>
+ Math.random() * (max - min) + min;
+
+ const floatingIcons = useMemo(() => {
+ if (!isClient) return [];
+
+ return Array.from({ length: 40 }).map((_, i) => {
+ const { Icon, color } = iconColorMap[i % iconColorMap.length];
+ return (
+
+
+
+ );
+ });
+ }, [isClient, iconColorMap]);
+
+ if (!isClient) return null;
+
+ return (
+
{floatingIcons}
+ );
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/layout.tsx b/apps/landing/src/app/(frontend)/(auth)/layout.tsx
index e3a0137..d66f2de 100644
--- a/apps/landing/src/app/(frontend)/(auth)/layout.tsx
+++ b/apps/landing/src/app/(frontend)/(auth)/layout.tsx
@@ -1,5 +1,19 @@
+import { Card } from '@components/atoms';
import { ReactNode } from 'react';
+import { AnimatedBackground } from './_components/animated-background';
export default function Layout({ children }: { children: ReactNode }) {
- return <>{children}>;
+ return (
+
+ );
}
diff --git a/apps/landing/src/app/(frontend)/(auth)/signin/_action/signin-action.ts b/apps/landing/src/app/(frontend)/(auth)/signin/_action/signin-action.ts
new file mode 100644
index 0000000..289bc40
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/signin/_action/signin-action.ts
@@ -0,0 +1,29 @@
+'use server';
+
+import { setAccessToken, setRefreshToken } from '@/lib/cookies';
+import { fetcher } from '@/lib/openapi';
+
+export async function SigninAction({
+ email,
+ password,
+}: {
+ email: string;
+ password: string;
+}) {
+ const { data: json, error } = await fetcher.POST('/v1/auth/login', {
+ body: {
+ email,
+ password,
+ },
+ });
+
+ if (error) throw new Error(error.message);
+
+ const accessToken = json.data.token.access_token;
+ const refreshToken = json.data.token.refresh_token;
+
+ await setAccessToken(accessToken);
+ await setRefreshToken(refreshToken);
+
+ return json;
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/signin/_components/signin-form.tsx b/apps/landing/src/app/(frontend)/(auth)/signin/_components/signin-form.tsx
new file mode 100644
index 0000000..bc7c1c9
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/signin/_components/signin-form.tsx
@@ -0,0 +1,92 @@
+'use client';
+
+import {
+ Button,
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ Input,
+} from '@components/atoms';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { authLoginSchema } from '@schemas';
+import { useMutation } from '@tanstack/react-query';
+import { useRouter } from 'next/navigation';
+import { useForm } from 'react-hook-form';
+import { LuLoader } from 'react-icons/lu';
+import { z } from 'zod';
+import { SigninAction } from '../_action/signin-action';
+
+type SigninData = z.infer;
+
+export function SigninForm() {
+ const router = useRouter();
+
+ const form = useForm({
+ resolver: zodResolver(authLoginSchema),
+ defaultValues: {
+ email: '',
+ password: '',
+ },
+ });
+
+ const { mutate, isPending, error } = useMutation({
+ mutationFn: SigninAction,
+ onSuccess: () => {
+ router.push('/');
+ },
+ onError: () => {
+ form.resetField('password');
+ },
+ });
+
+ const onSubmit = (values: SigninData) => {
+ mutate(values);
+ };
+
+ return (
+
+
+ );
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/signin/page.tsx b/apps/landing/src/app/(frontend)/(auth)/signin/page.tsx
index 7d1e0b7..97e22c7 100644
--- a/apps/landing/src/app/(frontend)/(auth)/signin/page.tsx
+++ b/apps/landing/src/app/(frontend)/(auth)/signin/page.tsx
@@ -1,3 +1,38 @@
+import { Metadata } from 'next';
+import Link from 'next/link';
+import { LogoSimple } from '../../_components/logo';
+import { SigninForm } from './_components/signin-form';
+
+export const metadata: Metadata = {
+ title: 'IMPHNEN - Signin',
+ description:
+ 'Komunitas Ingin Menjadi Programmer Handal Namung Enggan Ngonding',
+};
+
export default function Page() {
- return <>>;
+ return (
+ <>
+
+
+
+
+ Masuk untuk mengakses akunmu
+
+
+
+
+
+
+
+ Belum punya akun?{' '}
+
+ Daftar
+
+
+
+ >
+ );
}
diff --git a/apps/landing/src/app/(frontend)/(auth)/signup/page.tsx b/apps/landing/src/app/(frontend)/(auth)/signup/page.tsx
index 7d1e0b7..3fe5b43 100644
--- a/apps/landing/src/app/(frontend)/(auth)/signup/page.tsx
+++ b/apps/landing/src/app/(frontend)/(auth)/signup/page.tsx
@@ -1,3 +1,15 @@
+import { LogoSimple } from '../../_components/logo';
+
export default function Page() {
- return <>>;
+ return (
+ <>
+
+
+
+
+ Buat akunmu sekarang
+
+
+ >
+ );
}
diff --git a/apps/landing/src/app/(frontend)/_components/header/mobile-menu-hamburger.tsx b/apps/landing/src/app/(frontend)/_components/header/mobile-menu-hamburger.tsx
index 63ca3a6..519a7a0 100644
--- a/apps/landing/src/app/(frontend)/_components/header/mobile-menu-hamburger.tsx
+++ b/apps/landing/src/app/(frontend)/_components/header/mobile-menu-hamburger.tsx
@@ -1,7 +1,8 @@
'use client';
import { useMobileMenuStore } from '@/stores/mobile-menu-store';
-import { Button, MenuIcon, XIcon } from '@components/atoms';
+import { Button } from '@components/atoms';
+import { LuMenu, LuX } from 'react-icons/lu';
export function MobileMenuHamburger() {
const mobileMenuOpen = useMobileMenuStore((s) => s.mobileMenuOpen);
@@ -15,9 +16,9 @@ export function MobileMenuHamburger() {
onClick={toggleMobileMenu}
>
{mobileMenuOpen ? (
-
+
) : (
-
+
)}
);
diff --git a/apps/landing/src/app/(frontend)/_components/header/simple-theme-toggle.tsx b/apps/landing/src/app/(frontend)/_components/header/simple-theme-toggle.tsx
index 59b91cc..6b5c046 100644
--- a/apps/landing/src/app/(frontend)/_components/header/simple-theme-toggle.tsx
+++ b/apps/landing/src/app/(frontend)/_components/header/simple-theme-toggle.tsx
@@ -1,8 +1,9 @@
'use client';
-import { Button, MoonIcon, SunIcon } from '@components/atoms';
+import { Button } from '@components/atoms';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
+import { LuMoon, LuSun } from 'react-icons/lu';
export function SimpleThemeToggle() {
const { theme, setTheme } = useTheme();
@@ -28,9 +29,9 @@ export function SimpleThemeToggle() {
className="focus-visible:ring-0 cursor-pointer"
>
{theme === 'dark' ? (
-
+
) : (
-
+
)}
Toggle theme
diff --git a/apps/landing/src/app/(frontend)/_components/logo.tsx b/apps/landing/src/app/(frontend)/_components/logo.tsx
new file mode 100644
index 0000000..e9963a5
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/_components/logo.tsx
@@ -0,0 +1,69 @@
+export function Logo() {
+ return (
+
+ );
+}
+
+export function LogoSimple() {
+ return (
+
+ );
+}
diff --git a/apps/landing/tsconfig.json b/apps/landing/tsconfig.json
index 8ff8f12..74887f2 100644
--- a/apps/landing/tsconfig.json
+++ b/apps/landing/tsconfig.json
@@ -25,6 +25,7 @@
"paths": {
"@components/atoms": ["../../libs/shadcn-ui/src/atoms/index.ts"],
"@utils/ui": ["../../libs/utils/src/index.ts"],
+ "@schemas": ["../../libs/service/src/schemas/index.ts"],
"@/*": ["src/*"]
}
},
diff --git a/libs/shadcn-ui/package-lock.json b/libs/shadcn-ui/package-lock.json
index c12da2c..7e2e797 100644
--- a/libs/shadcn-ui/package-lock.json
+++ b/libs/shadcn-ui/package-lock.json
@@ -9,14 +9,16 @@
"version": "0.0.1",
"dependencies": {
"@radix-ui/react-avatar": "^1.1.9",
- "@radix-ui/react-slot": "^1.2.0",
+ "@radix-ui/react-label": "^2.1.6",
+ "@radix-ui/react-slot": "^1.2.2",
"@tailwindcss/vite": "^4.1.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.503.0",
"react-icons": "^5.5.0",
"tailwind-merge": "^3.2.0",
- "tailwindcss": "^4.1.4"
+ "tailwindcss": "^4.1.4",
+ "zod": "^3.24.4"
},
"devDependencies": {
"tw-animate-css": "^1.2.8"
@@ -504,6 +506,29 @@
}
}
},
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.6.tgz",
+ "integrity": "sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz",
@@ -1792,6 +1817,15 @@
"optional": true
}
}
+ },
+ "node_modules/zod": {
+ "version": "3.24.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz",
+ "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/libs/shadcn-ui/package.json b/libs/shadcn-ui/package.json
index 549d2d0..57a14b9 100644
--- a/libs/shadcn-ui/package.json
+++ b/libs/shadcn-ui/package.json
@@ -11,14 +11,16 @@
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.9",
- "@radix-ui/react-slot": "^1.2.0",
+ "@radix-ui/react-label": "^2.1.6",
+ "@radix-ui/react-slot": "^1.2.2",
"@tailwindcss/vite": "^4.1.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.503.0",
"react-icons": "^5.5.0",
"tailwind-merge": "^3.2.0",
- "tailwindcss": "^4.1.4"
+ "tailwindcss": "^4.1.4",
+ "zod": "^3.24.4"
},
"devDependencies": {
"tw-animate-css": "^1.2.8"
diff --git a/libs/shadcn-ui/src/atoms/avatar.tsx b/libs/shadcn-ui/src/atoms/avatar.tsx
index 6d03db7..36a64e0 100644
--- a/libs/shadcn-ui/src/atoms/avatar.tsx
+++ b/libs/shadcn-ui/src/atoms/avatar.tsx
@@ -2,7 +2,7 @@
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import * as React from 'react';
-import { cn } from './className';
+import { cn } from './cn';
const Avatar = React.forwardRef<
React.ElementRef,
diff --git a/libs/shadcn-ui/src/atoms/button.tsx b/libs/shadcn-ui/src/atoms/button.tsx
index 8dd501b..7b84da5 100644
--- a/libs/shadcn-ui/src/atoms/button.tsx
+++ b/libs/shadcn-ui/src/atoms/button.tsx
@@ -3,7 +3,7 @@
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
-import { cn } from './className';
+import { cn } from './cn';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
diff --git a/libs/shadcn-ui/src/atoms/card.tsx b/libs/shadcn-ui/src/atoms/card.tsx
new file mode 100644
index 0000000..a3bb7d5
--- /dev/null
+++ b/libs/shadcn-ui/src/atoms/card.tsx
@@ -0,0 +1,82 @@
+import * as React from 'react';
+import { cn } from './cn';
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = 'Card';
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = 'CardTitle';
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = 'CardDescription';
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = 'CardContent';
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = 'CardFooter';
+
+export {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+};
diff --git a/libs/shadcn-ui/src/atoms/className.ts b/libs/shadcn-ui/src/atoms/cn.ts
similarity index 100%
rename from libs/shadcn-ui/src/atoms/className.ts
rename to libs/shadcn-ui/src/atoms/cn.ts
diff --git a/libs/shadcn-ui/src/atoms/form.tsx b/libs/shadcn-ui/src/atoms/form.tsx
new file mode 100644
index 0000000..f2fe5e1
--- /dev/null
+++ b/libs/shadcn-ui/src/atoms/form.tsx
@@ -0,0 +1,167 @@
+'use client';
+
+import * as LabelPrimitive from '@radix-ui/react-label';
+import { Slot } from '@radix-ui/react-slot';
+import * as React from 'react';
+import {
+ Controller,
+ FormProvider,
+ useFormContext,
+ useFormState,
+ type ControllerProps,
+ type FieldPath,
+ type FieldValues,
+} from 'react-hook-form';
+import { cn } from './cn';
+import { Label } from './label';
+
+const Form = FormProvider;
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName;
+};
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+);
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ );
+};
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState } = useFormContext();
+ const formState = useFormState({ name: fieldContext.name });
+ const fieldState = getFieldState(fieldContext.name, formState);
+
+ if (!fieldContext) {
+ throw new Error('useFormField should be used within ');
+ }
+
+ const { id } = itemContext;
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ };
+};
+
+type FormItemContextValue = {
+ id: string;
+};
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+);
+
+function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
+ const id = React.useId();
+
+ return (
+
+
+
+ );
+}
+
+function FormLabel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ const { error, formItemId } = useFormField();
+
+ return (
+
+ );
+}
+
+function FormControl({ ...props }: React.ComponentProps) {
+ const { error, formItemId, formDescriptionId, formMessageId } =
+ useFormField();
+
+ return (
+
+ );
+}
+
+function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
+ const { formDescriptionId } = useFormField();
+
+ return (
+
+ );
+}
+
+function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message ?? '') : props.children;
+
+ if (!body) {
+ return null;
+ }
+
+ return (
+
+ {body}
+
+ );
+}
+
+export {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ useFormField,
+};
diff --git a/libs/shadcn-ui/src/atoms/icons.tsx b/libs/shadcn-ui/src/atoms/icons.tsx
deleted file mode 100644
index 9843457..0000000
--- a/libs/shadcn-ui/src/atoms/icons.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import {
- LuArrowDown as ArrowDownIcon,
- LuBookOpen as BookOpenIcon,
- LuCode as CodeIcon,
- LuFacebook as FacebookIcon,
- LuInstagram as InstagramIcon,
- LuLaptop as LaptopIcon,
- LuMenu as MenuIcon,
- LuMessageCircle as MessageCircleIcon,
- LuMoon as MoonIcon,
- LuQuote as QuoteIcon,
- LuShare2 as Share2Icon,
- LuSparkles as SparklesIcon,
- LuSun as SunIcon,
- LuUsers as UsersIcon,
- LuVideo as VideoIcon,
- LuX as XIcon,
-} from 'react-icons/lu';
-
-export {
- ArrowDownIcon,
- BookOpenIcon,
- CodeIcon,
- FacebookIcon,
- InstagramIcon,
- LaptopIcon,
- MenuIcon,
- MessageCircleIcon,
- MoonIcon,
- QuoteIcon,
- Share2Icon,
- SparklesIcon,
- SunIcon,
- UsersIcon,
- VideoIcon,
- XIcon,
-};
diff --git a/libs/shadcn-ui/src/atoms/index.ts b/libs/shadcn-ui/src/atoms/index.ts
index f82b36b..2c3bcf9 100644
--- a/libs/shadcn-ui/src/atoms/index.ts
+++ b/libs/shadcn-ui/src/atoms/index.ts
@@ -1,3 +1,6 @@
export * from './avatar';
export * from './button';
-export * from './icons';
+export * from './card';
+export * from './form';
+export * from './input';
+export * from './label';
diff --git a/libs/shadcn-ui/src/atoms/input.tsx b/libs/shadcn-ui/src/atoms/input.tsx
new file mode 100644
index 0000000..cd36c0b
--- /dev/null
+++ b/libs/shadcn-ui/src/atoms/input.tsx
@@ -0,0 +1,21 @@
+import * as React from 'react';
+import { cn } from './cn';
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+Input.displayName = 'Input';
+
+export { Input };
diff --git a/libs/shadcn-ui/src/atoms/label.tsx b/libs/shadcn-ui/src/atoms/label.tsx
new file mode 100644
index 0000000..5383479
--- /dev/null
+++ b/libs/shadcn-ui/src/atoms/label.tsx
@@ -0,0 +1,23 @@
+'use client';
+
+import * as LabelPrimitive from '@radix-ui/react-label';
+import * as React from 'react';
+import { cn } from './cn';
+
+function Label({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Label };
diff --git a/libs/shadcn-ui/src/index.css b/libs/shadcn-ui/src/index.css
index d3e908b..f11468a 100644
--- a/libs/shadcn-ui/src/index.css
+++ b/libs/shadcn-ui/src/index.css
@@ -1,5 +1,5 @@
@import 'tailwindcss';
-@import "tw-animate-css";
+@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
@@ -123,8 +123,8 @@
.container {
margin-left: auto;
margin-right: auto;
- padding-left: 2rem;
- padding-right: 2rem;
+ padding-left: 4rem;
+ padding-right: 4rem;
width: 100%;
}
@media (min-width: 1536px) {