Compare commits

..

2 Commits

@ -2,13 +2,6 @@
import { useActionState } from "react"; import { useActionState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { import {
Field, Field,
FieldError, FieldError,
@ -20,45 +13,119 @@ import { login, type LoginState } from "../../actions";
const initialState: LoginState = {}; const initialState: LoginState = {};
const fieldInputClass =
"h-9 rounded-none border-x-0 border-t-0 border-b border-dashed border-input bg-transparent px-0 font-mono text-[15px] text-foreground focus-visible:border-foreground focus-visible:ring-0 md:text-[15px] dark:bg-transparent";
const fieldLabelClass =
"font-mono text-[10px] font-normal tracking-[0.18em] text-muted-foreground uppercase";
export function LoginForm() { export function LoginForm() {
const [state, formAction, pending] = useActionState(login, initialState); const [state, formAction, pending] = useActionState(login, initialState);
return ( return (
<Card> <div className="animate-in fade-in slide-in-from-bottom-2 duration-500 ease-out motion-reduce:animate-none">
<CardHeader> <div className="drop-shadow-[0_20px_32px_rgba(29,28,23,0.16)] dark:drop-shadow-[0_20px_32px_rgba(0,0,0,0.6)]">
<CardTitle>Masuk</CardTitle> <section className="bg-card px-7 pt-7 pb-6 text-card-foreground">
<CardDescription>Cleo Minimarket back-office</CardDescription> <p className="text-center text-[10px] font-medium tracking-[0.3em] text-muted-foreground uppercase">
</CardHeader> Cleo Minimarket
<CardContent> </p>
<form action={formAction}>
<FieldGroup> <div
<Field> aria-hidden="true"
<FieldLabel htmlFor="username">Username</FieldLabel> className="mt-4 border-t border-dashed border-border"
/>
<h1 className="mt-6 text-[28px] leading-none font-semibold tracking-[-0.04em]">
Masuk
</h1>
<span
aria-hidden="true"
className="mt-2 block h-1.5 w-14 -rotate-2 bg-[#FFD34D]"
/>
<p className="mt-4 text-[13px] leading-relaxed text-muted-foreground">
Gunakan akun karyawan toko untuk melanjutkan.
</p>
<form action={formAction} className="mt-7">
<FieldGroup className="gap-5">
<Field className="gap-1.5">
<FieldLabel htmlFor="username" className={fieldLabelClass}>
Username
</FieldLabel>
<Input <Input
id="username" id="username"
name="username" name="username"
autoComplete="username" autoComplete="username"
defaultValue={state.username} defaultValue={state.username}
required required
className={fieldInputClass}
/> />
</Field> </Field>
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel> <Field className="gap-1.5">
<FieldLabel htmlFor="password" className={fieldLabelClass}>
Password
</FieldLabel>
<Input <Input
id="password" id="password"
name="password" name="password"
type="password" type="password"
autoComplete="current-password" autoComplete="current-password"
required required
className={fieldInputClass}
/> />
</Field> </Field>
{state.error && <FieldError>{state.error}</FieldError>}
<Button type="submit" disabled={pending}> {state.error && (
<FieldError className="border border-dashed border-destructive/40 bg-destructive/5 px-3 py-2 text-[12px] leading-relaxed">
{state.error}
</FieldError>
)}
<Button
type="submit"
disabled={pending}
className="h-11 w-full rounded-none text-[11px] tracking-[0.24em] uppercase"
>
{pending ? "Memproses..." : "Masuk"} {pending ? "Memproses..." : "Masuk"}
</Button> </Button>
</FieldGroup> </FieldGroup>
</form> </form>
</CardContent>
</Card> <div
aria-hidden="true"
className="mt-7 border-t border-dashed border-border"
/>
<p className="mt-4 text-center text-[10px] leading-relaxed text-muted-foreground">
Butuh akses? Hubungi petugas administrasi.
</p>
</section>
<ReceiptEdge />
</div>
</div>
);
}
function ReceiptEdge() {
const width = 320;
const depth = 9;
const tooth = 20;
let d = `M0 0H${width}`;
for (let x = width; x > 0; x -= tooth) {
d += `L${x - tooth / 2} ${depth}L${x - tooth} 0`;
}
return (
<svg
viewBox={`0 0 ${width} ${depth}`}
preserveAspectRatio="none"
aria-hidden="true"
className="block h-[9px] w-full fill-card"
>
<path d={`${d}Z`} />
</svg>
); );
} }

@ -5,13 +5,13 @@ import { redirectIfLoggedIn } from "@/data/auth";
import { LoginForm } from "./_components/login-form"; import { LoginForm } from "./_components/login-form";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Masuk", title: "Masuk · Cleo Minimarket",
}; };
export default function LoginPage() { export default function LoginPage() {
return ( return (
<main className="flex flex-1 items-center justify-center p-4"> <main className="flex flex-1 items-center justify-center bg-[#E8E7E1] px-4 py-8 sm:py-12 dark:bg-[#0B0C0B]">
<div className="w-full max-w-sm"> <div className="w-full max-w-[22rem]">
<Suspense fallback={null}> <Suspense fallback={null}>
<LoginContent /> <LoginContent />
</Suspense> </Suspense>

@ -5,6 +5,7 @@ import {
SidebarTrigger, SidebarTrigger,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { ThemeSwitch } from "@/components/theme-switch";
import { AppSidebar } from "./_components/app-sidebar"; import { AppSidebar } from "./_components/app-sidebar";
import { SidebarUser } from "./_components/sidebar-user"; import { SidebarUser } from "./_components/sidebar-user";
@ -15,6 +16,7 @@ export default function DashboardLayout({ children }: LayoutProps<"/">) {
footer={ footer={
<Suspense fallback={<Skeleton className="h-9 w-full" />}> <Suspense fallback={<Skeleton className="h-9 w-full" />}>
<SidebarUser /> <SidebarUser />
<ThemeSwitch />
</Suspense> </Suspense>
} }
/> />

@ -1,6 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { ThemeProvider } from "next-themes";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@ -22,8 +23,18 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
<html <html
lang="en" lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
suppressHydrationWarning
> >
<body className="min-h-full flex flex-col">{children}</body> <body className="min-h-full flex flex-col">
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html> </html>
); );
} }

@ -0,0 +1,31 @@
"use client";
import { useSyncExternalStore } from "react";
import { MoonIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
const subscribe = () => () => {};
export function ThemeSwitch() {
const { resolvedTheme, setTheme } = useTheme();
// Tema baru diketahui di browser; false di server dan saat hydration supaya HTML-nya sama.
const mounted = useSyncExternalStore(
subscribe,
() => true,
() => false,
);
return (
<Label className="px-2 py-1 font-normal">
<MoonIcon className="size-4 text-muted-foreground" />
<span className="flex-1">Mode gelap</span>
<Switch
checked={mounted && resolvedTheme === "dark"}
disabled={!mounted}
onCheckedChange={(checked) => setTheme(checked ? "dark" : "light")}
/>
</Label>
);
}

@ -0,0 +1,31 @@
"use client"
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
import { cn } from "cn"
function Switch({
className,
size = "default",
...props
}: SwitchPrimitive.Root.Props & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

@ -24,6 +24,7 @@
"lucide-react": "^1.59.0", "lucide-react": "^1.59.0",
"mysql2": "^3.24.6", "mysql2": "^3.24.6",
"next": "16.4.2", "next": "16.4.2",
"next-themes": "^0.4.6",
"pg": "^8.23.1", "pg": "^8.23.1",
"react": "19.3.0", "react": "19.3.0",
"react-dom": "19.3.0", "react-dom": "19.3.0",

@ -32,6 +32,9 @@ importers:
next: next:
specifier: 16.4.2 specifier: 16.4.2
version: 16.4.2(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) version: 16.4.2(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
pg: pg:
specifier: ^8.23.1 specifier: ^8.23.1
version: 8.23.1 version: 8.23.1
@ -3019,6 +3022,12 @@ packages:
resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==}
engines: {node: '>=18'} engines: {node: '>=18'}
next-themes@0.4.6:
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
peerDependencies:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next@16.4.2: next@16.4.2:
resolution: {integrity: sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==} resolution: {integrity: sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==}
engines: {node: '>=20.9.0'} engines: {node: '>=20.9.0'}
@ -6514,6 +6523,11 @@ snapshots:
dependencies: dependencies:
content-type: 2.1.0 content-type: 2.1.0
next-themes@0.4.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
dependencies:
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
next@16.4.2(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): next@16.4.2(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
dependencies: dependencies:
'@next/env': 16.4.2 '@next/env': 16.4.2

Loading…
Cancel
Save