diff --git a/crude/.gitignore b/crude/.gitignore
new file mode 100644
index 0000000..f650315
--- /dev/null
+++ b/crude/.gitignore
@@ -0,0 +1,27 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
\ No newline at end of file
diff --git a/crude/.vscode/commander.json b/crude/.vscode/commander.json
new file mode 100644
index 0000000..6256eb3
--- /dev/null
+++ b/crude/.vscode/commander.json
@@ -0,0 +1,12 @@
+{
+ "packageProcessor": "npm",
+ "commands": {
+ "Sample command": {
+ "description": "Sample command to be modified",
+ "command": "echo 'Hello World !'",
+ "environment": {
+ "sample-variable": "foobar"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/crude/app/globals.css b/crude/app/globals.css
new file mode 100644
index 0000000..1b521a5
--- /dev/null
+++ b/crude/app/globals.css
@@ -0,0 +1,780 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ --medical-blue: #1e40af;
+ --medical-blue-light: #3b82f6;
+ --medical-green: #059669;
+ --medical-green-light: #10b981;
+ --medical-red: #dc2626;
+ --medical-orange: #ea580c;
+ --clinical-white: #ffffff;
+ --clinical-gray-50: #f9fafb;
+ --clinical-gray-100: #f3f4f6;
+ --clinical-gray-200: #e5e7eb;
+ --clinical-gray-300: #d1d5db;
+ --clinical-gray-400: #9ca3af;
+ --clinical-gray-500: #6b7280;
+ --clinical-gray-600: #4b5563;
+ --clinical-gray-700: #374151;
+ --clinical-gray-800: #1f2937;
+ --clinical-gray-900: #111827;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --destructive-foreground: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --radius: 0.625rem;
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.145 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.145 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.985 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.396 0.141 25.723);
+ --destructive-foreground: oklch(0.637 0.237 25.331);
+ --border: oklch(0.269 0 0);
+ --input: oklch(0.269 0 0);
+ --ring: oklch(0.439 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(0.269 0 0);
+ --sidebar-ring: oklch(0.439 0 0);
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Inter", sans-serif;
+ background: var(--clinical-gray-50);
+ color: var(--clinical-gray-900);
+ line-height: 1.5;
+ font-size: 14px;
+}
+
+.medical-system {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.system-header {
+ background: var(--clinical-white);
+ border-bottom: 1px solid var(--clinical-gray-200);
+ padding: 0.75rem 1.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+.header-brand {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.brand-icon {
+ width: 32px;
+ height: 32px;
+ background: linear-gradient(135deg, var(--medical-blue) 0%, var(--medical-blue-light) 100%);
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+}
+
+.brand-text h1 {
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: var(--clinical-gray-900);
+ line-height: 1.2;
+}
+
+.brand-text p {
+ font-size: 0.75rem;
+ color: var(--clinical-gray-500);
+ font-weight: 500;
+}
+
+.header-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.user-info {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ background: var(--clinical-gray-100);
+ border-radius: 6px;
+ font-size: 0.8rem;
+ color: var(--clinical-gray-700);
+}
+
+.main-content {
+ flex: 1;
+ padding: 1.5rem;
+ max-width: 1400px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+.quick-stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+}
+
+.stat-card {
+ background: var(--clinical-white);
+ border: 1px solid var(--clinical-gray-200);
+ border-radius: 8px;
+ padding: 1rem;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ transition: all 0.2s ease;
+ position: relative;
+ overflow: hidden;
+}
+
+.stat-card::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: var(--accent-color);
+}
+
+.stat-card:hover {
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ transform: translateY(-1px);
+}
+
+.stat-card.patients::before {
+ --accent-color: var(--medical-blue);
+}
+.stat-card.appointments::before {
+ --accent-color: var(--medical-green);
+}
+.stat-card.contacts::before {
+ --accent-color: var(--medical-orange);
+}
+.stat-card.recent::before {
+ --accent-color: var(--medical-red);
+}
+
+.stat-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--icon-bg);
+ color: var(--icon-color);
+}
+
+.stat-card.patients .stat-icon {
+ --icon-bg: #dbeafe;
+ --icon-color: var(--medical-blue);
+}
+.stat-card.appointments .stat-icon {
+ --icon-bg: #d1fae5;
+ --icon-color: var(--medical-green);
+}
+.stat-card.contacts .stat-icon {
+ --icon-bg: #fed7aa;
+ --icon-color: var(--medical-orange);
+}
+.stat-card.recent .stat-icon {
+ --icon-bg: #fecaca;
+ --icon-color: var(--medical-red);
+}
+
+.stat-content h3 {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--clinical-gray-900);
+ line-height: 1;
+ margin-bottom: 0.25rem;
+}
+
+.stat-content p {
+ font-size: 0.8rem;
+ color: var(--clinical-gray-600);
+ font-weight: 500;
+}
+
+.control-panel {
+ background: var(--clinical-white);
+ border: 1px solid var(--clinical-gray-200);
+ border-radius: 8px;
+ padding: 1rem 1.25rem;
+ margin-bottom: 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.panel-title {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--clinical-gray-900);
+}
+
+.add-patient-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ background: var(--medical-blue);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 0.6rem 1rem;
+ font-size: 0.85rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.add-patient-btn:hover {
+ background: var(--medical-blue-light);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(30, 64, 175, 0.3);
+}
+
+.patients-section {
+ background: var(--clinical-white);
+ border: 1px solid var(--clinical-gray-200);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.section-header {
+ padding: 1rem 1.25rem;
+ border-bottom: 1px solid var(--clinical-gray-200);
+ background: var(--clinical-gray-50);
+}
+
+.section-title {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--clinical-gray-900);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.patient-count {
+ background: var(--medical-blue);
+ color: white;
+ padding: 0.2rem 0.5rem;
+ border-radius: 12px;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+
+.table-wrapper {
+ overflow-x: auto;
+}
+
+.patients-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.85rem;
+}
+
+.patients-table th {
+ background: var(--clinical-gray-50);
+ color: var(--clinical-gray-700);
+ font-weight: 600;
+ text-align: left;
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid var(--clinical-gray-200);
+ font-size: 0.8rem;
+ text-transform: uppercase;
+ letter-spacing: 0.025em;
+}
+
+.patients-table td {
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid var(--clinical-gray-100);
+ color: var(--clinical-gray-900);
+ vertical-align: middle;
+}
+
+.patients-table tr:hover {
+ background: var(--clinical-gray-50);
+}
+
+.patient-name {
+ font-weight: 600;
+ color: var(--clinical-gray-900);
+}
+
+.patient-id {
+ font-size: 0.75rem;
+ color: var(--clinical-gray-500);
+ font-family: monospace;
+}
+
+.contact-info {
+ display: flex;
+ flex-direction: column;
+ gap: 0.1rem;
+}
+
+.phone-number {
+ font-weight: 500;
+}
+
+.location-info {
+ font-size: 0.8rem;
+ color: var(--clinical-gray-600);
+}
+
+.date-info {
+ font-size: 0.8rem;
+ font-weight: 500;
+}
+
+.date-label {
+ font-size: 0.7rem;
+ color: var(--clinical-gray-500);
+ text-transform: uppercase;
+ letter-spacing: 0.025em;
+}
+
+.action-buttons {
+ display: flex;
+ gap: 0.25rem;
+}
+
+.action-btn {
+ width: 28px;
+ height: 28px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+}
+
+.edit-btn {
+ background: #dbeafe;
+ color: var(--medical-blue);
+}
+
+.edit-btn:hover {
+ background: var(--medical-blue);
+ color: white;
+}
+
+.delete-btn {
+ background: #fecaca;
+ color: var(--medical-red);
+}
+
+.delete-btn:hover {
+ background: var(--medical-red);
+ color: white;
+}
+
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(4px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ padding: 1rem;
+}
+
+.modal-content {
+ background: var(--clinical-white);
+ border-radius: 12px;
+ width: 100%;
+ max-width: 500px;
+ max-height: 90vh;
+ overflow-y: auto;
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
+}
+
+.modal-header {
+ padding: 1.25rem 1.5rem 1rem;
+ border-bottom: 1px solid var(--clinical-gray-200);
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.modal-header h2 {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--clinical-gray-900);
+}
+
+.modal-body {
+ padding: 1.5rem;
+}
+
+.form-grid {
+ display: grid;
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+}
+
+.form-label {
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--clinical-gray-700);
+ text-transform: uppercase;
+ letter-spacing: 0.025em;
+}
+
+.form-input {
+ padding: 0.6rem 0.75rem;
+ border: 1px solid var(--clinical-gray-300);
+ border-radius: 6px;
+ background: var(--clinical-white);
+ color: var(--clinical-gray-900);
+ font-size: 0.85rem;
+ transition: all 0.2s ease;
+}
+
+.form-input:focus {
+ outline: none;
+ border-color: var(--medical-blue);
+ box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
+}
+
+.form-input::placeholder {
+ color: var(--clinical-gray-400);
+}
+
+.form-actions {
+ display: flex;
+ gap: 0.75rem;
+ justify-content: flex-end;
+ padding-top: 1rem;
+ border-top: 1px solid var(--clinical-gray-200);
+}
+
+.save-btn {
+ background: var(--medical-green);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 0.6rem 1.25rem;
+ font-size: 0.85rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.save-btn:hover {
+ background: var(--medical-green-light);
+ transform: translateY(-1px);
+}
+
+.cancel-btn {
+ background: var(--clinical-gray-100);
+ color: var(--clinical-gray-700);
+ border: 1px solid var(--clinical-gray-300);
+ border-radius: 6px;
+ padding: 0.6rem 1.25rem;
+ font-size: 0.85rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.cancel-btn:hover {
+ background: var(--clinical-gray-200);
+}
+
+.empty-state {
+ text-align: center;
+ padding: 3rem 2rem;
+ color: var(--clinical-gray-500);
+}
+
+.empty-icon {
+ width: 64px;
+ height: 64px;
+ margin: 0 auto 1rem;
+ opacity: 0.5;
+ color: var(--clinical-gray-400);
+}
+
+.empty-title {
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+ color: var(--clinical-gray-700);
+}
+
+.empty-description {
+ font-size: 0.9rem;
+ color: var(--clinical-gray-500);
+}
+
+.toast-container {
+ position: fixed;
+ top: 1rem;
+ right: 1rem;
+ z-index: 1100;
+}
+
+.toast {
+ background: var(--clinical-white);
+ border: 1px solid var(--clinical-gray-200);
+ border-radius: 8px;
+ padding: 0.75rem 1rem;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ min-width: 280px;
+ animation: slideIn 0.3s ease;
+ border-left: 4px solid var(--toast-color);
+}
+
+.toast.success {
+ --toast-color: var(--medical-green);
+}
+.toast.error {
+ --toast-color: var(--medical-red);
+}
+.toast.info {
+ --toast-color: var(--medical-blue);
+}
+
+.toast-message {
+ color: var(--clinical-gray-900);
+ font-size: 0.85rem;
+ font-weight: 500;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+.system-footer {
+ background: var(--clinical-white);
+ border-top: 1px solid var(--clinical-gray-200);
+ padding: 1rem 1.5rem;
+ text-align: center;
+ color: var(--clinical-gray-500);
+ font-size: 0.8rem;
+ margin-top: auto;
+}
+
+.footer-text {
+ margin-bottom: 0.25rem;
+}
+
+.footer-version {
+ font-size: 0.75rem;
+ color: var(--clinical-gray-400);
+}
+
+@media (max-width: 768px) {
+ .main-content {
+ padding: 1rem;
+ }
+
+ .quick-stats {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 0.75rem;
+ }
+
+ .stat-card {
+ padding: 0.75rem;
+ }
+
+ .stat-content h3 {
+ font-size: 1.25rem;
+ }
+
+ .control-panel {
+ flex-direction: column;
+ gap: 1rem;
+ text-align: center;
+ }
+
+ .patients-table {
+ font-size: 0.8rem;
+ }
+
+ .patients-table th,
+ .patients-table td {
+ padding: 0.5rem 0.75rem;
+ }
+
+ .modal-content {
+ margin: 1rem;
+ max-width: calc(100vw - 2rem);
+ }
+
+ .form-actions {
+ flex-direction: column;
+ }
+
+ .header-actions .user-info {
+ display: none;
+ }
+}
+
+@media (max-width: 480px) {
+ .quick-stats {
+ grid-template-columns: 1fr;
+ }
+
+ .brand-text h1 {
+ font-size: 1.1rem;
+ }
+
+ .brand-text p {
+ display: none;
+ }
+
+ .patients-table th:nth-child(4),
+ .patients-table td:nth-child(4),
+ .patients-table th:nth-child(5),
+ .patients-table td:nth-child(5) {
+ display: none;
+ }
+}
diff --git a/crude/app/layout.tsx b/crude/app/layout.tsx
new file mode 100644
index 0000000..0828a4e
--- /dev/null
+++ b/crude/app/layout.tsx
@@ -0,0 +1,32 @@
+import type React from "react"
+import type { Metadata } from "next"
+import { GeistSans } from "geist/font/sans"
+import { GeistMono } from "geist/font/mono"
+import "./globals.css"
+
+export const metadata: Metadata = {
+ title: "v0 App",
+ description: "Created with v0",
+ generator: "v0.dev",
+}
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return (
+
+
+
+
+ {children}
+
+ )
+}
diff --git a/crude/app/page.tsx b/crude/app/page.tsx
new file mode 100644
index 0000000..fe3fd9b
--- /dev/null
+++ b/crude/app/page.tsx
@@ -0,0 +1,562 @@
+"use client"
+
+import type React from "react"
+import { useState, useCallback, useMemo } from "react"
+
+interface PatientRecord {
+ id: string
+ fullName: string
+ contactNumber: string
+ cityLocation: string
+ stateRegion: string
+ lastVisitDate: string
+ nextAppointmentDate: string
+ createdAt: string
+ updatedAt: string
+}
+
+interface NotificationState {
+ message: string
+ type: "success" | "error" | "info"
+ isVisible: boolean
+}
+
+interface FormData {
+ fullName: string
+ contactNumber: string
+ cityLocation: string
+ stateRegion: string
+ lastVisitDate: string
+ nextAppointmentDate: string
+}
+
+const useLocalStorage = (key: string, initialValue: T) => {
+ const [storedValue, setStoredValue] = useState(() => {
+ if (typeof window === "undefined") return initialValue
+ try {
+ const item = window.localStorage.getItem(key)
+ return item ? JSON.parse(item) : initialValue
+ } catch (error) {
+ console.error(`Error reading localStorage key "${key}":`, error)
+ return initialValue
+ }
+ })
+
+ const setValue = useCallback(
+ (value: T | ((val: T) => T)) => {
+ try {
+ const valueToStore = value instanceof Function ? value(storedValue) : value
+ setStoredValue(valueToStore)
+ if (typeof window !== "undefined") {
+ window.localStorage.setItem(key, JSON.stringify(valueToStore))
+ }
+ } catch (error) {
+ console.error(`Error setting localStorage key "${key}":`, error)
+ }
+ },
+ [key, storedValue],
+ )
+
+ return [storedValue, setValue] as const
+}
+
+const useNotification = () => {
+ const [notification, setNotification] = useState({
+ message: "",
+ type: "info",
+ isVisible: false,
+ })
+
+ const showNotification = useCallback((message: string, type: NotificationState["type"] = "info") => {
+ setNotification({ message, type, isVisible: true })
+ setTimeout(() => {
+ setNotification((prev) => ({ ...prev, isVisible: false }))
+ }, 4000)
+ }, [])
+
+ return { notification, showNotification }
+}
+
+const generatePatientId = (): string => {
+ return `PT${Date.now().toString().slice(-6)}${Math.random().toString(36).substr(2, 3).toUpperCase()}`
+}
+
+const formatDateDisplay = (dateString: string): string => {
+ if (!dateString) return "Não informado"
+ try {
+ const date = new Date(dateString)
+ return date.toLocaleDateString("pt-BR", {
+ day: "2-digit",
+ month: "2-digit",
+ year: "numeric",
+ })
+ } catch {
+ return "Data inválida"
+ }
+}
+
+const validateFormData = (data: FormData): string[] => {
+ const errors: string[] = []
+ if (!data.fullName.trim()) errors.push("Nome completo é obrigatório")
+ if (!data.contactNumber.trim()) errors.push("Número de contato é obrigatório")
+ if (!data.cityLocation.trim()) errors.push("Cidade é obrigatória")
+ if (data.fullName.trim().length < 2) errors.push("Nome deve ter pelo menos 2 caracteres")
+ if (data.contactNumber.trim().length < 10) errors.push("Número de contato deve ter pelo menos 10 dígitos")
+ return errors
+}
+
+export default function HospitalManagementSystem() {
+ const [patientRecords, setPatientRecords] = useLocalStorage("hospital_patients_v3", [])
+ const [isModalOpen, setIsModalOpen] = useState(false)
+ const [editingRecord, setEditingRecord] = useState(null)
+ const [formData, setFormData] = useState({
+ fullName: "",
+ contactNumber: "",
+ cityLocation: "",
+ stateRegion: "",
+ lastVisitDate: "",
+ nextAppointmentDate: "",
+ })
+
+ const { notification, showNotification } = useNotification()
+
+ const systemMetrics = useMemo(
+ () => ({
+ totalPatients: patientRecords.length,
+ scheduledAppointments: patientRecords.filter((record) => record.nextAppointmentDate).length,
+ contactsAvailable: patientRecords.filter((record) => record.contactNumber).length,
+ recentVisits: patientRecords.filter((record) => {
+ if (!record.lastVisitDate) return false
+ const visitDate = new Date(record.lastVisitDate)
+ const thirtyDaysAgo = new Date()
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
+ return visitDate >= thirtyDaysAgo
+ }).length,
+ }),
+ [patientRecords],
+ )
+
+ const resetFormState = useCallback(() => {
+ setFormData({
+ fullName: "",
+ contactNumber: "",
+ cityLocation: "",
+ stateRegion: "",
+ lastVisitDate: "",
+ nextAppointmentDate: "",
+ })
+ setEditingRecord(null)
+ setIsModalOpen(false)
+ }, [])
+
+ const handleInputChange = useCallback((field: keyof FormData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }))
+ }, [])
+
+ const openCreateModal = useCallback(() => {
+ resetFormState()
+ setIsModalOpen(true)
+ }, [resetFormState])
+
+ const openEditModal = useCallback((record: PatientRecord) => {
+ setFormData({
+ fullName: record.fullName,
+ contactNumber: record.contactNumber,
+ cityLocation: record.cityLocation,
+ stateRegion: record.stateRegion,
+ lastVisitDate: record.lastVisitDate,
+ nextAppointmentDate: record.nextAppointmentDate,
+ })
+ setEditingRecord(record)
+ setIsModalOpen(true)
+ }, [])
+
+ const handleSubmitForm = useCallback(
+ (e: React.FormEvent) => {
+ e.preventDefault()
+
+ const validationErrors = validateFormData(formData)
+ if (validationErrors.length > 0) {
+ showNotification(validationErrors[0], "error")
+ return
+ }
+
+ const timestamp = new Date().toISOString()
+
+ if (editingRecord) {
+ const updatedRecord: PatientRecord = {
+ ...editingRecord,
+ ...formData,
+ updatedAt: timestamp,
+ }
+ setPatientRecords((prev) => prev.map((record) => (record.id === editingRecord.id ? updatedRecord : record)))
+ showNotification("Dados do paciente atualizados com sucesso!", "success")
+ } else {
+ const newRecord: PatientRecord = {
+ id: generatePatientId(),
+ ...formData,
+ createdAt: timestamp,
+ updatedAt: timestamp,
+ }
+ setPatientRecords((prev) => [...prev, newRecord])
+ showNotification("Paciente cadastrado no sistema!", "success")
+ }
+
+ resetFormState()
+ },
+ [formData, editingRecord, setPatientRecords, showNotification, resetFormState],
+ )
+
+ const handleDeleteRecord = useCallback(
+ (recordId: string) => {
+ const confirmDelete = window.confirm(
+ "Confirma a remoção deste paciente do sistema? Esta ação não pode ser desfeita.",
+ )
+ if (confirmDelete) {
+ setPatientRecords((prev) => prev.filter((record) => record.id !== recordId))
+ showNotification("Paciente removido do sistema.", "success")
+ }
+ },
+ [setPatientRecords, showNotification],
+ )
+
+ return (
+
+
+
+
+
+
MedSystem Pro
+
Sistema Integrado de Gestão Hospitalar
+
+
+
+
+
+
+
+
+
+
+
{systemMetrics.totalPatients}
+
Total de Pacientes
+
+
+
+
+
+
+
+
+
{systemMetrics.scheduledAppointments}
+
Consultas Agendadas
+
+
+
+
+
+
+
{systemMetrics.contactsAvailable}
+
Contatos Disponíveis
+
+
+
+
+
+
+
{systemMetrics.recentVisits}
+
Visitas Recentes
+
+
+
+
+
+
Registro de Pacientes
+
+
+
+
+
+
+
+ Pacientes Cadastrados
+ {patientRecords.length}
+
+
+
+ {patientRecords.length === 0 ? (
+
+
+
Nenhum paciente cadastrado
+
Clique em "Novo Paciente" para começar
+
+ ) : (
+
+
+
+
+ Paciente |
+ Contato |
+ Localização |
+ Última Consulta |
+ Próxima Consulta |
+ Ações |
+
+
+
+ {patientRecords.map((record) => (
+
+
+ {record.fullName}
+ ID: {record.id}
+ |
+
+
+ {record.contactNumber}
+
+ |
+
+
+ {record.cityLocation}
+ {record.stateRegion && `, ${record.stateRegion}`}
+
+ |
+
+
+ Última
+ {formatDateDisplay(record.lastVisitDate)}
+
+ |
+
+
+ Próxima
+ {formatDateDisplay(record.nextAppointmentDate)}
+
+ |
+
+
+
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+ {isModalOpen && (
+
e.target === e.currentTarget && resetFormState()}>
+
+
+
+
{editingRecord ? "Editar Paciente" : "Novo Paciente"}
+
+
+
+
+
+ )}
+
+ {notification.isVisible && (
+
+
+
{notification.message}
+
+
+ )}
+
+ )
+}
diff --git a/crude/components.json b/crude/components.json
new file mode 100644
index 0000000..4ee62ee
--- /dev/null
+++ b/crude/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
diff --git a/crude/components/theme-provider.tsx b/crude/components/theme-provider.tsx
new file mode 100644
index 0000000..55c2f6e
--- /dev/null
+++ b/crude/components/theme-provider.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import * as React from 'react'
+import {
+ ThemeProvider as NextThemesProvider,
+ type ThemeProviderProps,
+} from 'next-themes'
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children}
+}
diff --git a/crude/components/ui/accordion.tsx b/crude/components/ui/accordion.tsx
new file mode 100644
index 0000000..4a8cca4
--- /dev/null
+++ b/crude/components/ui/accordion.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDownIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Accordion({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ )
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/crude/components/ui/alert-dialog.tsx b/crude/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..0863e40
--- /dev/null
+++ b/crude/components/ui/alert-dialog.tsx
@@ -0,0 +1,157 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+function AlertDialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogAction({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogCancel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/crude/components/ui/alert.tsx b/crude/components/ui/alert.tsx
new file mode 100644
index 0000000..1421354
--- /dev/null
+++ b/crude/components/ui/alert.tsx
@@ -0,0 +1,66 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+ {
+ variants: {
+ variant: {
+ default: "bg-card text-card-foreground",
+ destructive:
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Alert({
+ className,
+ variant,
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDescription({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/crude/components/ui/aspect-ratio.tsx b/crude/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..3df3fd0
--- /dev/null
+++ b/crude/components/ui/aspect-ratio.tsx
@@ -0,0 +1,11 @@
+"use client"
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+function AspectRatio({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+export { AspectRatio }
diff --git a/crude/components/ui/avatar.tsx b/crude/components/ui/avatar.tsx
new file mode 100644
index 0000000..71e428b
--- /dev/null
+++ b/crude/components/ui/avatar.tsx
@@ -0,0 +1,53 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/crude/components/ui/badge.tsx b/crude/components/ui/badge.tsx
new file mode 100644
index 0000000..0205413
--- /dev/null
+++ b/crude/components/ui/badge.tsx
@@ -0,0 +1,46 @@
+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 badgeVariants = cva(
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ destructive:
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"span"> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "span"
+
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/crude/components/ui/breadcrumb.tsx b/crude/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..eb88f32
--- /dev/null
+++ b/crude/components/ui/breadcrumb.tsx
@@ -0,0 +1,109 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
+ return
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
+ return (
+
+ )
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+ svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+ )
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+
+ More
+
+ )
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/crude/components/ui/button.tsx b/crude/components/ui/button.tsx
new file mode 100644
index 0000000..a2df8dc
--- /dev/null
+++ b/crude/components/ui/button.tsx
@@ -0,0 +1,59 @@
+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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/crude/components/ui/calendar.tsx b/crude/components/ui/calendar.tsx
new file mode 100644
index 0000000..4d7c46a
--- /dev/null
+++ b/crude/components/ui/calendar.tsx
@@ -0,0 +1,213 @@
+"use client"
+
+import * as React from "react"
+import {
+ ChevronDownIcon,
+ ChevronLeftIcon,
+ ChevronRightIcon,
+} from "lucide-react"
+import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { Button, buttonVariants } from "@/components/ui/button"
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ captionLayout = "label",
+ buttonVariant = "ghost",
+ formatters,
+ components,
+ ...props
+}: React.ComponentProps & {
+ buttonVariant?: React.ComponentProps["variant"]
+}) {
+ const defaultClassNames = getDefaultClassNames()
+
+ return (
+ svg]:rotate-180`,
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+ className
+ )}
+ captionLayout={captionLayout}
+ formatters={{
+ formatMonthDropdown: (date) =>
+ date.toLocaleString("default", { month: "short" }),
+ ...formatters,
+ }}
+ classNames={{
+ root: cn("w-fit", defaultClassNames.root),
+ months: cn(
+ "flex gap-4 flex-col md:flex-row relative",
+ defaultClassNames.months
+ ),
+ month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
+ nav: cn(
+ "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
+ defaultClassNames.nav
+ ),
+ button_previous: cn(
+ buttonVariants({ variant: buttonVariant }),
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
+ defaultClassNames.button_previous
+ ),
+ button_next: cn(
+ buttonVariants({ variant: buttonVariant }),
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
+ defaultClassNames.button_next
+ ),
+ month_caption: cn(
+ "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
+ defaultClassNames.month_caption
+ ),
+ dropdowns: cn(
+ "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
+ defaultClassNames.dropdowns
+ ),
+ dropdown_root: cn(
+ "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
+ defaultClassNames.dropdown_root
+ ),
+ dropdown: cn(
+ "absolute bg-popover inset-0 opacity-0",
+ defaultClassNames.dropdown
+ ),
+ caption_label: cn(
+ "select-none font-medium",
+ captionLayout === "label"
+ ? "text-sm"
+ : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
+ defaultClassNames.caption_label
+ ),
+ table: "w-full border-collapse",
+ weekdays: cn("flex", defaultClassNames.weekdays),
+ weekday: cn(
+ "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
+ defaultClassNames.weekday
+ ),
+ week: cn("flex w-full mt-2", defaultClassNames.week),
+ week_number_header: cn(
+ "select-none w-(--cell-size)",
+ defaultClassNames.week_number_header
+ ),
+ week_number: cn(
+ "text-[0.8rem] select-none text-muted-foreground",
+ defaultClassNames.week_number
+ ),
+ day: cn(
+ "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
+ defaultClassNames.day
+ ),
+ range_start: cn(
+ "rounded-l-md bg-accent",
+ defaultClassNames.range_start
+ ),
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
+ range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
+ today: cn(
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
+ defaultClassNames.today
+ ),
+ outside: cn(
+ "text-muted-foreground aria-selected:text-muted-foreground",
+ defaultClassNames.outside
+ ),
+ disabled: cn(
+ "text-muted-foreground opacity-50",
+ defaultClassNames.disabled
+ ),
+ hidden: cn("invisible", defaultClassNames.hidden),
+ ...classNames,
+ }}
+ components={{
+ Root: ({ className, rootRef, ...props }) => {
+ return (
+
+ )
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === "left") {
+ return (
+
+ )
+ }
+
+ if (orientation === "right") {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+ },
+ DayButton: CalendarDayButton,
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+
+ {children}
+
+ |
+ )
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ )
+}
+
+function CalendarDayButton({
+ className,
+ day,
+ modifiers,
+ ...props
+}: React.ComponentProps) {
+ const defaultClassNames = getDefaultClassNames()
+
+ const ref = React.useRef(null)
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus()
+ }, [modifiers.focused])
+
+ return (
+