Upload perdana untuk deploy
This commit is contained in:
@@ -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 }
|
||||
Reference in New Issue
Block a user