Upload perdana untuk deploy

This commit is contained in:
2026-06-11 13:17:31 +07:00
parent 5857b15aec
commit 72133d0a9a
63 changed files with 10057 additions and 457 deletions
@@ -0,0 +1,206 @@
"use client"
import type React from "react"
import { useState } from "react"
import { Eye, EyeOff, Lock, User, Loader2, ArrowRight } from "lucide-react"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
import { cn } from "@/lib/utils"
export function LoginForm() {
const [showPassword, setShowPassword] = useState(false)
const [loading, setLoading] = useState(false)
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setLoading(true)
setTimeout(() => setLoading(false), 1600)
}
const demoAccounts = [
{ icon: "👑", role: "Admin", user: "admin", pass: "password" },
{ icon: "📝", role: "Petugas", user: "petugas", pass: "password" },
{ icon: "👁️", role: "Guest", user: "guest", pass: "password" },
]
function fillDemo(user: string, pass: string) {
setUsername(user)
setPassword(pass)
}
return (
<div className="w-full max-w-md">
{/* Glassmorphism card */}
<div className="relative rounded-3xl border border-white/60 bg-white/70 p-8 shadow-[0_40px_120px_-30px_rgba(198,40,40,0.28),0_18px_50px_-20px_rgba(198,40,40,0.14)] backdrop-blur-xl sm:p-10">
{/* subtle red glow accent */}
<div
aria-hidden
className="pointer-events-none absolute -right-6 -top-6 h-24 w-24 rounded-full bg-primary/15 blur-2xl"
/>
{/* Brand */}
<div className="mb-8 flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-lg shadow-primary/25">
<svg
viewBox="0 0 24 24"
fill="none"
className="h-6 w-6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M12 21s-7-5.6-7-11a7 7 0 1 1 14 0c0 5.4-7 11-7 11Z" />
<circle cx="12" cy="10" r="2.5" />
</svg>
</div>
<div>
<p className="font-heading text-base font-bold leading-none tracking-tight text-foreground">
WebGIS Geospasial
</p>
<p className="mt-1 text-xs font-medium text-muted-foreground">Sistem Analisis Spasial</p>
</div>
</div>
<div className="mb-7">
<h2 className="font-heading text-2xl font-bold tracking-tight text-foreground text-balance">
Selamat Datang Kembali
</h2>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground text-pretty">
Masuk ke akun Anda untuk mengakses peta dan data analisis spasial.
</p>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
{/* Username */}
<div className="flex flex-col gap-2">
<Label htmlFor="username" className="text-sm font-semibold text-foreground">
Nama Pengguna
</Label>
<div className="group relative">
<User
aria-hidden
className="pointer-events-none absolute left-3.5 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-muted-foreground transition-colors group-focus-within:text-primary"
/>
<Input
id="username"
type="text"
required
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Masukkan nama pengguna"
className="h-12 rounded-xl border-border/80 bg-white/80 pl-11 text-sm shadow-sm transition-all hover:border-primary/60 focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-primary/20"
/>
</div>
</div>
{/* Password */}
<div className="flex flex-col gap-2">
<Label htmlFor="password" className="text-sm font-semibold text-foreground">
Kata Sandi
</Label>
<div className="group relative">
<Lock
aria-hidden
className="pointer-events-none absolute left-3.5 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-muted-foreground transition-colors group-focus-within:text-primary"
/>
<Input
id="password"
type={showPassword ? "text" : "password"}
required
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Masukkan kata sandi"
className="h-12 rounded-xl border-border/80 bg-white/80 pl-11 pr-11 text-sm shadow-sm transition-all hover:border-primary/60 focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-primary/20"
/>
<button
type="button"
onClick={() => setShowPassword((s) => !s)}
aria-label={showPassword ? "Sembunyikan kata sandi" : "Tampilkan kata sandi"}
className="absolute right-3 top-1/2 -translate-y-1/2 rounded-md p-1 text-muted-foreground transition-colors hover:text-primary"
>
{showPassword ? <EyeOff className="h-[18px] w-[18px]" /> : <Eye className="h-[18px] w-[18px]" />}
</button>
</div>
</div>
{/* Remember + forgot */}
<div className="flex items-center justify-between">
<label htmlFor="remember" className="flex cursor-pointer items-center gap-2 select-none">
<Checkbox
id="remember"
className="data-[state=checked]:border-primary data-[state=checked]:bg-primary"
/>
<span className="text-sm font-medium text-muted-foreground">Ingat saya</span>
</label>
<a
href="#"
className="text-sm font-semibold text-primary transition-colors hover:text-primary/80"
>
Lupa sandi?
</a>
</div>
{/* Submit */}
<button
type="submit"
disabled={loading}
className={cn(
"group mt-1 flex h-12 items-center justify-center gap-2 rounded-xl bg-primary text-sm font-semibold text-primary-foreground",
"shadow-lg shadow-primary/25 transition-all duration-300",
"hover:-translate-y-0.5 hover:bg-[oklch(0.46_0.21_27)] hover:shadow-xl hover:shadow-primary/35",
"active:translate-y-0 disabled:cursor-not-allowed disabled:opacity-80",
)}
>
{loading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Memproses...
</>
) : (
<>
Masuk
<ArrowRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" />
</>
)}
</button>
</form>
{/* Demo accounts helper */}
<div className="mt-7 rounded-xl border border-border/70 bg-gray-50 p-4">
<div className="mb-3 flex items-center gap-2">
<span className="h-1.5 w-1.5 rounded-full bg-primary" />
<p className="text-xs font-bold uppercase tracking-wide text-foreground">Info Akun Demo</p>
<span className="text-[11px] font-medium text-muted-foreground">· klik untuk mengisi</span>
</div>
<div className="flex flex-col gap-1.5">
{demoAccounts.map((acc) => (
<button
key={acc.role}
type="button"
onClick={() => fillDemo(acc.user, acc.pass)}
className="group flex cursor-pointer items-center justify-between gap-3 rounded-lg border border-transparent bg-white/60 px-3 py-2 text-left transition-all hover:border-primary/30 hover:bg-white hover:shadow-sm"
>
<span className="flex items-center gap-2.5">
<span aria-hidden className="text-sm leading-none">{acc.icon}</span>
<span className="text-xs font-semibold text-foreground">{acc.role}</span>
</span>
<span className="font-mono text-[11px] text-muted-foreground transition-colors group-hover:text-primary">
{acc.user} · {acc.pass}
</span>
</button>
))}
</div>
</div>
</div>
<p className="mt-6 text-center text-xs text-muted-foreground">
© {new Date().getFullYear()} WebGIS Geospasial · Kementerian Analisis Spasial
</p>
</div>
)
}
@@ -0,0 +1,58 @@
import { Button as ButtonPrimitive } from '@base-ui/react/button'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
outline:
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost:
'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive:
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default:
'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
icon: 'size-8',
'icon-xs':
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm':
'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
function Button({
className,
variant = 'default',
size = 'default',
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
@@ -0,0 +1,29 @@
"use client"
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
import { cn } from "@/lib/utils"
import { CheckIcon } from "lucide-react"
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
>
<CheckIcon
/>
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }
@@ -0,0 +1,20 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }
@@ -0,0 +1,20 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<label
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }