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 ( +
+ + +
+ +
+
{children}
+
+
+
+
+ ); } 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 ( +
+ + {error && ( +
+ {(error as Error).message} +
+ )} + + ( + + Email + + + + + + )} + /> + + ( + + Password + + + + + + )} + /> + + + + + ); +} 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 ( +