initial commit

This commit is contained in:
panggilsajarey 2026-02-10 11:53:16 +07:00
parent 236fbd6d42
commit a6d4670f06
18 changed files with 1270 additions and 68 deletions

75
app/actions.ts Normal file
View File

@ -0,0 +1,75 @@
'use server'
import { supabase } from '@/lib/supabase'
import { redirect } from 'next/navigation'
import { cookies } from 'next/headers'
export async function login(prevState: any, formData: FormData) {
const username = formData.get('username') as string
const password = formData.get('password') as string
const remember = formData.get('remember') === 'on'
if (!username || !password) {
return { message: 'Username and password are required' }
}
try {
// 1. Check Petugas (Admin - Role 1/Admin)
const { data: petugas, error: petugasError } = await supabase
.from('petugas_posyandu')
.select('*')
.eq('username', username)
.eq('password', password) // Plain text password check as requested
.single()
if (petugas) {
// Set session/cookie for Admin
// In a real app, use a secure session library. For this demo, simple cookies.
const cookieStore = await cookies()
cookieStore.set('user_session', JSON.stringify({
id: petugas.id,
role: 'admin',
username: petugas.username,
name: petugas.nama
}), { secure: true, httpOnly: true, maxAge: remember ? 60 * 60 * 24 * 7 : 60 * 60 * 24 })
redirect('/dashboard') // Redirect to dashboard or appropriate page
}
// 2. Check Akun Balita (User - Role 2/User)
const { data: user, error: userError } = await supabase
.from('akun_balita')
.select('*')
.eq('username', username)
.eq('password', password) // Plain text password check as requested
.single()
if (user) {
// Set session/cookie for User
const cookieStore = await cookies()
cookieStore.set('user_session', JSON.stringify({
id: user.id,
role: 'user',
username: user.username,
name: user.nama_orang_tua // Or nama_anak depending on preference
}), { secure: true, httpOnly: true, maxAge: remember ? 60 * 60 * 24 * 7 : 60 * 60 * 24 })
redirect('/user-dashboard') // Redirect to user dashboard
}
return { message: 'Invalid username or password' }
} catch (error: any) {
if (error.message === 'NEXT_REDIRECT') {
throw error
}
console.error('Login error:', error)
return { message: 'An error occurred during login' }
}
}
export async function logout() {
const cookieStore = await cookies()
cookieStore.delete('user_session')
redirect('/')
}

View File

@ -0,0 +1,59 @@
import { FeatureCard } from '@/components/feature-card'
import { UserCog, Users, ArrowLeft } from 'lucide-react'
import { Activity } from 'lucide-react'
import { LogoutButton } from '@/components/logout-button'
import Link from 'next/link'
export default function ManajemenAkunPage() {
return (
<div className="min-h-screen bg-white font-sans text-black flex flex-col">
{/* Header */}
<header className="flex justify-between items-center px-8 py-6 border-b border-gray-100">
<div className="flex items-center gap-4">
<Link href="/dashboard" className="group flex items-center gap-2 text-sm font-bold hover:text-gray-600 transition-colors">
<div className="p-2 rounded-full border border-black flex items-center justify-center group-hover:bg-black group-hover:text-white transition-all">
<ArrowLeft className="h-5 w-5" />
</div>
<span className="hidden md:block">Kembali ke Dashboard</span>
</Link>
<div className="h-8 w-px bg-gray-200 mx-2 hidden md:block"></div>
<div className="flex flex-col justify-center">
<h1 className="text-xl font-bold leading-none">Manajemen Akun</h1>
<p className="text-[10px] text-gray-500 tracking-widest uppercase mt-0.5">PILIH OPSI PENGELOLAAN</p>
</div>
</div>
<LogoutButton />
</header>
<main className="p-8 max-w-6xl mx-auto flex-1 w-full flex items-center justify-center">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-4xl">
{/* Kelola Akun Petugas */}
<div className="h-96">
<FeatureCard
title="Kelola Akun Petugas"
description="Kelola akun anda sebagai petugas. Ubah profil, password, dan informasi petugas lainnya."
icon={UserCog}
href="#"
color="green"
className="h-full"
/>
</div>
{/* Kelola Akun Pengguna */}
<div className="h-96">
<FeatureCard
title="Kelola Akun Pengguna"
description="Kelola data akun pengguna (Masyarakat). Reset password, pemblokiran, dan manajemen akses."
icon={Users}
href="#"
color="blue"
className="h-full"
/>
</div>
</div>
</main>
</div>
)
}

164
app/dashboard/page.tsx Normal file
View File

@ -0,0 +1,164 @@
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { supabase } from '@/lib/supabase'
import { LogoutButton } from '@/components/logout-button'
import { RealtimeClock } from '@/components/realtime-clock'
import { FeatureCard } from '@/components/feature-card'
import { Activity, User, CreditCard, Phone, Users, TrendingUp, ClipboardList, Building2, Calendar } from 'lucide-react'
export default async function DashboardPage() {
const cookieStore = await cookies()
const sessionCookie = cookieStore.get('user_session')
if (!sessionCookie) {
redirect('/')
}
const session = JSON.parse(sessionCookie.value)
if (session.role !== 'admin') {
redirect('/')
}
const { data: petugas, error } = await supabase
.from('petugas_posyandu')
.select('*')
.eq('id', session.id)
.single()
if (error || !petugas) {
return <div>Error loading profile.</div>
}
return (
<div className="min-h-screen bg-white font-sans text-black flex flex-col">
{/* Header */}
<header className="flex justify-between items-center px-8 py-6 border-b border-gray-100">
<div className="flex items-center gap-3">
<div className="p-2 rounded-full border border-black flex items-center justify-center">
<Activity className="h-5 w-5 text-black" />
</div>
<div className="flex flex-col justify-center">
<h1 className="text-xl font-bold leading-none">HealthPortal</h1>
<p className="text-[10px] text-gray-500 tracking-widest uppercase mt-0.5">DASHBOARD</p>
</div>
</div>
<LogoutButton />
</header>
<main className="p-8 max-w-6xl mx-auto">
{/* Main Single Frame */}
<div className="bg-white rounded-xl border border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] overflow-hidden">
{/* Top Section - Welcome & Clock */}
<div className="p-8 border-b border-black flex flex-col md:flex-row justify-between items-start md:items-center">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2 mb-2">
<Activity className="h-6 w-6" />
Selamat Datang!
</h2>
<p className="text-gray-600">
Anda log in sebagai <span className="font-bold text-black">{petugas.nama}</span>
</p>
</div>
<div className="mt-6 md:mt-0">
<RealtimeClock />
</div>
</div>
{/* Bottom Section - Details Grid */}
<div className="grid grid-cols-1 md:grid-cols-2">
{/* Nama */}
<div className="p-6 border-b md:border-b-0 md:border-r border-black flex flex-col gap-2">
<div className="flex items-center gap-2 text-gray-500 text-[10px] font-bold uppercase tracking-widest">
<User className="h-3 w-3" />
Nama
</div>
<div className="text-lg font-bold">{petugas.nama}</div>
</div>
{/* Username */}
<div className="p-6 border-b md:border-b-0 border-black flex flex-col gap-2">
<div className="flex items-center gap-2 text-gray-500 text-[10px] font-bold uppercase tracking-widest">
<User className="h-3 w-3" />
Username
</div>
<div className="text-lg font-bold">{petugas.username}</div>
</div>
{/* Nomor Petugas - Full Width */}
<div className="p-6 border-b border-black border-t md:col-span-2 flex flex-col gap-2">
<div className="flex items-center gap-2 text-gray-500 text-[10px] font-bold uppercase tracking-widest">
<CreditCard className="h-3 w-3" />
Nomor Petugas
</div>
<div className="text-lg font-bold">{petugas.nomor_petugas}</div>
</div>
{/* No Telp - Full Width */}
<div className="p-6 md:col-span-2 flex flex-col gap-2">
<div className="flex items-center gap-2 text-gray-500 text-[10px] font-bold uppercase tracking-widest">
<Phone className="h-3 w-3" />
Nomor Telepon
</div>
<div className="text-lg font-bold">{petugas.no_telp || '-'}</div>
</div>
</div>
</div>
{/* Feature Menu Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
{/* 1. Manajemen Akun */}
<FeatureCard
title="Manajemen Akun"
description="Kelola data akun pengguna dan hak akses dalam sistem."
icon={Users}
href="/dashboard/manajemen-akun"
color="green"
/>
{/* 2. Trend Stunting Daerah */}
<FeatureCard
title="Trend Stunting Daerah"
description="Analisis grafik dan data statistik stunting di wilayah kerja."
icon={TrendingUp}
href="#"
color="blue"
/>
{/* 3. Kelola Data */}
<FeatureCard
title="Kelola Data"
description="Input, ubah, dan validasi data posyandu secara terpusat."
icon={ClipboardList}
href="#"
color="orange"
/>
{/* 4. Manajemen Posyandu */}
<FeatureCard
title="Manajemen Posyandu"
description="Pengaturan operasional dan administrasi posyandu."
icon={Building2}
href="#"
color="purple"
/>
{/* 5. Kelola Jadwal Posyandu */}
<FeatureCard
title="Kelola Jadwal Posyandu"
description="Buat dan atur jadwal kegiatan posyandu bulanan."
icon={Calendar}
href="#"
color="red"
className="md:col-span-2"
/>
</div>
</main>
</div>
)
}

View File

@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { DashboardFooter } from "@/components/dashboard-footer";
const geistSans = Geist({
variable: "--font-geist-sans",
@ -25,9 +26,12 @@ export default function RootLayout({
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen flex flex-col`}
>
{children}
<main className="flex-1">
{children}
</main>
<DashboardFooter />
</body>
</html>
);

View File

@ -1,65 +1,185 @@
import Image from "next/image";
'use client'
import { useActionState } from 'react'
import { login } from './actions'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Checkbox } from '@/components/ui/checkbox'
import { Loader2, Eye, EyeOff, Activity } from 'lucide-react'
import { useState } from 'react'
export default function LoginPage() {
const [state, formAction, isPending] = useActionState(login, null)
const [showPassword, setShowPassword] = useState(false)
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
<div className="flex min-h-screen bg-black text-white font-sans overflow-hidden">
{/* Left Side - Guide */}
<div className="hidden lg:flex w-1/2 flex-col justify-center p-12 relative overflow-hidden">
{/* Background Accents */}
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(ellipse_at_top_left,_var(--tw-gradient-stops))] from-gray-900 via-black to-black -z-10"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-gray-800 rounded-full blur-3xl opacity-20"></div>
<div className="mb-12">
<div className="flex items-center gap-2 mb-2">
<div className="bg-white/10 p-2 rounded-full border border-white/20">
<Activity className="h-6 w-6 text-white" />
</div>
<h1 className="text-2xl font-bold tracking-tight">HealthPortal</h1>
</div>
<p className="text-gray-400 text-sm tracking-widest uppercase">SISTEM INFORMASI KESEHATAN</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
<h2 className="text-4xl font-bold mb-8">Panduan Login</h2>
<div className="space-y-8 relative">
<div className="absolute left-3.5 top-2 bottom-2 w-0.5 bg-gray-800 -z-10"></div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-900 border border-gray-700 flex items-center justify-center font-bold text-sm">1</div>
<div>
<h3 className="text-lg font-semibold text-white">Masukkan Username</h3>
<p className="text-gray-400 text-sm mt-1">Gunakan username yang telah diberikan oleh administrator sistem.</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-900 border border-gray-700 flex items-center justify-center font-bold text-sm">2</div>
<div>
<h3 className="text-lg font-semibold text-white">Masukkan Password</h3>
<p className="text-gray-400 text-sm mt-1">Ketik password Anda dengan benar. Password bersifat case-sensitive.</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-900 border border-gray-700 flex items-center justify-center font-bold text-sm">3</div>
<div>
<h3 className="text-lg font-semibold text-white">Klik Tombol Masuk</h3>
<p className="text-gray-400 text-sm mt-1">Setelah mengisi data, klik tombol "Masuk" untuk mengakses sistem.</p>
</div>
</div>
</div>
</main>
<div className="mt-12 bg-gray-900/50 border border-gray-800 rounded-lg p-6 flex gap-4 backdrop-blur-sm">
<div className="mt-1">
<div className="w-5 h-5 rounded-full border border-gray-500 flex items-center justify-center text-xs text-gray-500 font-bold">?</div>
</div>
<div>
<h4 className="font-semibold text-white">Lupa Password?</h4>
<p className="text-gray-400 text-sm mt-1">Hubungi administrator untuk reset password atau gunakan fitur "Lupa Password" di form login.</p>
</div>
</div>
<div className="mt-12 text-gray-500 text-sm">
<p>Bantuan: (021) 555-1234</p>
<div className="mt-4 border-t border-gray-800 pt-4 flex items-center gap-2">
<div className="p-1 bg-white/5 rounded-full">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" className="text-gray-500"><path d="M7.49999 0.875C3.84099 0.875 0.874992 3.841 0.874992 7.5C0.874992 11.159 3.84099 14.125 7.49999 14.125C11.159 14.125 14.125 11.159 14.125 7.5C14.125 3.841 11.159 0.875 7.49999 0.875ZM7.49999 1.875C10.6066 1.875 13.125 4.3934 13.125 7.5C13.125 10.6066 10.6066 13.125 7.49999 13.125C4.39339 13.125 1.87499 10.6066 1.87499 7.5C1.87499 4.3934 4.39339 1.875 7.49999 1.875ZM6.89999 10.5H8.09999V11.7H6.89999V10.5ZM6.89999 3.3H8.09999V9.3H6.89999V3.3Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
</div>
<span>Sistem ini dilindungi dan diawasi. Akses tidak sah akan ditindak sesuai hukum yang berlaku.</span>
</div>
</div>
</div>
{/* Right Side - Login Form */}
<div className="w-full lg:w-1/2 bg-white text-black flex flex-col items-center justify-center p-8 relative">
<div className="w-full max-w-md bg-white p-8 rounded-2xl shadow-[0_20px_50px_rgba(0,0,0,0.1)] border border-gray-100 relative z-10 mb-10">
{/* Decorative Elements for Card style */}
<div className="absolute top-0 left-0 w-full h-2 bg-black rounded-t-2xl"></div>
<div className="text-center mb-10">
<h2 className="text-3xl font-bold mb-2">Login</h2>
<p className="text-gray-500 text-sm">Masukkan Akun Anda untuk melanjutkan</p>
</div>
<form action={formAction} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="username" className="text-xs font-bold uppercase tracking-wider text-gray-700">Nama Pengguna</Label>
<div className="relative">
<div className="absolute left-3 top-2.5 text-gray-400">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
</div>
<Input
id="username"
name="username"
placeholder="Masukkan nama pengguna"
required
className="pl-10 h-12 bg-gray-50 border-gray-200 focus:border-black focus:ring-black transition-all"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-xs font-bold uppercase tracking-wider text-gray-700">Kata Sandi</Label>
<div className="relative">
<div className="absolute left-3 top-2.5 text-gray-400">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
</div>
<Input
id="password"
name="password"
type={showPassword ? "text" : "password"}
placeholder="Masukkan kata sandi"
required
className="pl-10 pr-10 h-12 bg-gray-50 border-gray-200 focus:border-black focus:ring-black transition-all"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-gray-400 hover:text-black transition-colors"
>
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox id="remember" name="remember" className="data-[state=checked]:bg-black data-[state=checked]:border-black" />
<label
htmlFor="remember"
className="text-xs font-bold uppercase tracking-wider text-gray-500 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Ingat Saya
</label>
</div>
<a href="#" className="text-xs font-bold uppercase tracking-wider text-black hover:underline">Lupa Kata Sandi?</a>
</div>
{state?.message && (
<div className="bg-red-50 text-red-600 p-3 rounded-md text-sm font-medium flex items-center gap-2">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
{state.message}
</div>
)}
<Button type="submit" className="w-full h-12 bg-black hover:bg-gray-800 text-white font-bold uppercase tracking-wider text-sm shadow-lg shadow-black/20" disabled={isPending}>
{isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : 'Masuk'}
</Button>
<div className="text-center pt-2">
<p className="text-xs text-gray-400 font-medium flex items-center justify-center gap-2">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
KONEKSI AMAN & TERENKRIPSI
</p>
</div>
</form>
</div>
<div className="flex gap-6 text-xs font-bold text-gray-400 uppercase tracking-widest mt-auto mb-4">
<a href="#" className="hover:text-black transition-colors">Privasi</a>
<span className="text-gray-300"></span>
<a href="#" className="hover:text-black transition-colors">Ketentuan</a>
<span className="text-gray-300"></span>
<a href="#" className="hover:text-black transition-colors">Bantuan</a>
</div>
<div className="text-[10px] text-gray-300 mb-6">
© 2024 HealthPortal. Hak Cipta Dilindungi.
</div>
</div>
</div>
);
)
}

View File

@ -0,0 +1,35 @@
'use client'
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
export default function TestPage() {
const [status, setStatus] = useState('Checking connection...')
useEffect(() => {
async function checkConnection() {
try {
const { data, error } = await supabase.auth.getSession()
if (error) {
setStatus('Error connecting to Supabase: ' + error.message)
} else {
setStatus('Success! Connected to Supabase. Session data available.')
}
} catch (err: any) {
setStatus('Unexpected error: ' + err.message)
}
}
checkConnection()
}, [])
return (
<div className="p-10 font-sans">
<h1 className="text-2xl font-bold mb-4">Supabase Connection Test</h1>
<div className={`p-4 rounded border ${status.includes('Success') ? 'bg-green-100 border-green-500 text-green-700' : 'bg-red-100 border-red-500 text-red-700'}`}>
{status}
</div>
<p className="mt-4 text-gray-600">
If you see a success message, the Supabase client is correctly initialized with the provided URL and Anon Key.
</p>
</div>
)
}

View File

@ -0,0 +1,33 @@
'use client'
import { usePathname } from 'next/navigation'
export function DashboardFooter() {
const pathname = usePathname()
// Don't show footer on login page (/)
if (pathname === '/') {
return null
}
return (
<footer className="bg-black text-white py-8 px-8 mt-auto w-full">
<div className="max-w-6xl mx-auto flex flex-col items-center gap-4 text-center">
<div className="flex flex-col items-center gap-2">
<p className="text-sm font-medium">
&copy; 2024 <span className="font-bold">HealthPortal Posyandu</span>. Hak Cipta Dilindungi.
</p>
<div className="flex items-center gap-2 text-[10px] font-bold tracking-widest text-gray-400 uppercase">
<a href="#" className="hover:text-white transition-colors">Privasi</a>
<span></span>
<a href="#" className="hover:text-white transition-colors">Syarat & Ketentuan</a>
<span></span>
<a href="#" className="hover:text-white transition-colors">Bantuan</a>
</div>
</div>
</div>
</footer>
)
}

View File

@ -0,0 +1,51 @@
import { LucideIcon } from 'lucide-react'
import Link from 'next/link'
import { cn } from '@/lib/utils'
interface FeatureCardProps {
title: string
description?: string
icon: LucideIcon
href: string
color: 'green' | 'blue' | 'orange' | 'purple' | 'red'
className?: string
}
export function FeatureCard({ title, description, icon: Icon, href, color, className }: FeatureCardProps) {
const colorStyles = {
green: 'border-green-500 text-green-600',
blue: 'border-blue-500 text-blue-600',
orange: 'border-orange-500 text-orange-600',
purple: 'border-purple-500 text-purple-600',
red: 'border-red-500 text-red-600',
}
const iconBgStyles = {
green: 'bg-green-50 text-green-600 border-green-200',
blue: 'bg-blue-50 text-blue-600 border-blue-200',
orange: 'bg-orange-50 text-orange-600 border-orange-200',
purple: 'bg-purple-50 text-purple-600 border-purple-200',
red: 'bg-red-50 text-red-600 border-red-200',
}
return (
<Link href={href} className={cn("block group h-full", className)}>
<div className={cn(
"h-full bg-white rounded-xl p-6 transition-all duration-300 relative overflow-hidden group-hover:-translate-y-1",
"border-2 hover:shadow-lg",
colorStyles[color]
)}>
<div className="flex flex-col items-center text-center h-full">
<div className={cn("w-16 h-16 rounded-full flex items-center justify-center mb-4 border-2 transition-transform group-hover:scale-110", iconBgStyles[color])}>
<Icon className="w-8 h-8" />
</div>
<h3 className="text-xl font-bold mb-2 text-black">{title}</h3>
{description && (
<p className="text-gray-500 text-sm leading-relaxed">{description}</p>
)}
</div>
</div>
</Link>
)
}

View File

@ -0,0 +1,18 @@
'use client'
import { logout } from '@/app/actions'
import { Button } from '@/components/ui/button'
import { LogOut } from 'lucide-react'
export function LogoutButton() {
return (
<Button
variant="outline"
onClick={() => logout()}
className="gap-2 border-black bg-white text-black hover:bg-gray-100 hover:text-black transition-colors"
>
<LogOut className="h-4 w-4" />
Logout
</Button>
)
}

View File

@ -0,0 +1,35 @@
'use client'
import { useEffect, useState } from 'react'
export function RealtimeClock() {
const [date, setDate] = useState<Date | null>(null) // Start null to avoid hydration mismatch
useEffect(() => {
setDate(new Date()) // Set initial date on client
const timer = setInterval(() => {
setDate(new Date())
}, 1000)
return () => clearInterval(timer)
}, [])
if (!date) {
// Render a placeholder or nothing during SSR/initial mount to prevent flicker
return (
<div className="text-right">
<div className="text-3xl font-bold opacity-0">00:00:00 <span className="text-sm font-normal text-gray-500">WIB</span></div>
<div className="text-xs text-gray-400 font-bold tracking-widest opacity-0">LOADING...</div>
</div>
)
}
const formattedTime = date.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', second: '2-digit' }).replace(/\./g, ':')
const formattedDate = date.toLocaleDateString('id-ID', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }).toUpperCase()
return (
<div className="text-right">
<div className="text-3xl font-bold">{formattedTime} <span className="text-sm font-normal text-gray-500">WIB</span></div>
<div className="text-xs text-gray-400 font-bold tracking-widest">{formattedDate}</div>
</div>
)
}

55
components/ui/button.tsx Normal file
View File

@ -0,0 +1,55 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

25
components/ui/input.tsx Normal file
View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> { }
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

26
components/ui/label.tsx Normal file
View File

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

7
lib/supabase.ts Normal file
View File

@ -0,0 +1,7 @@
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

463
package-lock.json generated
View File

@ -8,9 +8,17 @@
"name": "web-cloud",
"version": "0.1.0",
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@supabase/supabase-js": "^2.95.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@ -1226,6 +1234,286 @@
"node": ">=12.4.0"
}
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-checkbox": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
"integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"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-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-label": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
"integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.4"
},
"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-label/node_modules/@radix-ui/react-primitive": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
"integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.4"
},
"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-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"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.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"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/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
"integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-size": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@ -1233,6 +1521,86 @@
"dev": true,
"license": "MIT"
},
"node_modules/@supabase/auth-js": {
"version": "2.95.3",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.95.3.tgz",
"integrity": "sha512-vD2YoS8E2iKIX0F7EwXTmqhUpaNsmbU6X2R0/NdFcs02oEfnHyNP/3M716f3wVJ2E5XHGiTFXki6lRckhJ0Thg==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.95.3",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.95.3.tgz",
"integrity": "sha512-uTuOAKzs9R/IovW1krO0ZbUHSJnsnyJElTXIRhjJTqymIVGcHzkAYnBCJqd7468Fs/Foz1BQ7Dv6DCl05lr7ig==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "2.95.3",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.95.3.tgz",
"integrity": "sha512-LTrRBqU1gOovxRm1vRXPItSMPBmEFqrfTqdPTRtzOILV4jPSueFz6pES5hpb4LRlkFwCPRmv3nQJ5N625V2Xrg==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.95.3",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.95.3.tgz",
"integrity": "sha512-D7EAtfU3w6BEUxDACjowWNJo/ZRo7sDIuhuOGKHIm9FHieGeoJV5R6GKTLtga/5l/6fDr2u+WcW/m8I9SYmaIw==",
"license": "MIT",
"dependencies": {
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"tslib": "2.8.1",
"ws": "^8.18.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.95.3",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.95.3.tgz",
"integrity": "sha512-4GxkJiXI3HHWjxpC3sDx1BVrV87O0hfX+wvJdqGv67KeCu+g44SPnII8y0LL/Wr677jB7tpjAxKdtVWf+xhc9A==",
"license": "MIT",
"dependencies": {
"iceberg-js": "^0.8.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.95.3",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.95.3.tgz",
"integrity": "sha512-Fukw1cUTQ6xdLiHDJhKKPu6svEPaCEDvThqCne3OaQyZvuq2qjhJAd91kJu3PXLG18aooCgYBaB6qQz35hhABg==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.95.3",
"@supabase/functions-js": "2.95.3",
"@supabase/postgrest-js": "2.95.3",
"@supabase/realtime-js": "2.95.3",
"@supabase/storage-js": "2.95.3"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@ -1549,17 +1917,22 @@
"version": "20.19.33",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/phoenix": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.2.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@ -1569,12 +1942,21 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz",
@ -2570,12 +2952,33 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
"license": "Apache-2.0",
"dependencies": {
"clsx": "^2.1.1"
},
"funding": {
"url": "https://polar.sh/cva"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -2629,7 +3032,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@ -3890,6 +4293,15 @@
"hermes-estree": "0.25.1"
}
},
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -4833,6 +5245,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.563.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz",
"integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@ -6018,6 +6439,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tailwind-merge": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
@ -6297,7 +6728,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unrs-resolver": {
@ -6491,6 +6921,27 @@
"node": ">=0.10.0"
}
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@ -9,9 +9,17 @@
"lint": "eslint"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@supabase/supabase-js": "^2.95.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",