Compare commits

..

No commits in common. 'a59eb3aef595078c7d96f8e958e444f7a64a3551' and '1ab0c2e321f4097f0b3d3644ab63233d3ad2881a' have entirely different histories.

@ -2,6 +2,13 @@
import { useActionState } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Field,
FieldError,
@ -13,119 +20,45 @@ import { login, type LoginState } from "../../actions";
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() {
const [state, formAction, pending] = useActionState(login, initialState);
return (
<div className="animate-in fade-in slide-in-from-bottom-2 duration-500 ease-out motion-reduce:animate-none">
<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)]">
<section className="bg-card px-7 pt-7 pb-6 text-card-foreground">
<p className="text-center text-[10px] font-medium tracking-[0.3em] text-muted-foreground uppercase">
Cleo Minimarket
</p>
<div
aria-hidden="true"
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
id="username"
name="username"
autoComplete="username"
defaultValue={state.username}
required
className={fieldInputClass}
/>
</Field>
<Field className="gap-1.5">
<FieldLabel htmlFor="password" className={fieldLabelClass}>
Password
</FieldLabel>
<Input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className={fieldInputClass}
/>
</Field>
{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"}
</Button>
</FieldGroup>
</form>
<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>
<Card>
<CardHeader>
<CardTitle>Masuk</CardTitle>
<CardDescription>Cleo Minimarket back-office</CardDescription>
</CardHeader>
<CardContent>
<form action={formAction}>
<FieldGroup>
<Field>
<FieldLabel htmlFor="username">Username</FieldLabel>
<Input
id="username"
name="username"
autoComplete="username"
defaultValue={state.username}
required
/>
</Field>
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel>
<Input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
/>
</Field>
{state.error && <FieldError>{state.error}</FieldError>}
<Button type="submit" disabled={pending}>
{pending ? "Memproses..." : "Masuk"}
</Button>
</FieldGroup>
</form>
</CardContent>
</Card>
);
}

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

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

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

@ -1,31 +0,0 @@
"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>
);
}

@ -1,31 +0,0 @@
"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,7 +24,6 @@
"lucide-react": "^1.59.0",
"mysql2": "^3.24.6",
"next": "16.4.2",
"next-themes": "^0.4.6",
"pg": "^8.23.1",
"react": "19.3.0",
"react-dom": "19.3.0",

@ -32,9 +32,6 @@ importers:
next:
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)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
pg:
specifier: ^8.23.1
version: 8.23.1
@ -3022,12 +3019,6 @@ packages:
resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==}
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:
resolution: {integrity: sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==}
engines: {node: '>=20.9.0'}
@ -6523,11 +6514,6 @@ snapshots:
dependencies:
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):
dependencies:
'@next/env': 16.4.2

Loading…
Cancel
Save