diff --git a/.env b/.env
new file mode 100644
index 0000000..7ba2fea
--- /dev/null
+++ b/.env
@@ -0,0 +1,3 @@
+DATABASE_URL=postgresql://postgres:postgres@localhost:5432/la_huasca
+JWT_SECRET=your-jwt-secret-here
+NEXT_PUBLIC_SITE_NAME=Hotel La Huasca
\ No newline at end of file
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 904ca18..2aa659a 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -1,16 +1,71 @@
'use client';
-import { useEffect, useState } from 'react';
-import { useRouter } from 'next/navigation';
+import { useState, useEffect } from 'react';
-interface ImageRecord {
+// Types
+type Image = {
id: number;
- filename: string;
data: string;
- uploaded_at: string;
-}
+ filename: string;
+ category: string;
+ room_id: number | null;
+ location_target: string;
+};
-interface Room {
+type FooterInfo = {
+ facebook_url: string;
+ instagram_url: string;
+ whatsapp_url: string;
+ address: string;
+ phone: string;
+ email: string;
+};
+
+type Slide = {
+ id: number;
+ title: string;
+ subtitle: string;
+ image_id: number | null;
+ image_url: string;
+ active: boolean;
+ sort_order: number;
+};
+
+type CalendarEvent = {
+ id: number;
+ date: string;
+ title: string;
+ type: 'holiday' | 'season' | 'weather' | 'event';
+ color: string;
+ description: string;
+ active: boolean;
+};
+
+type SocialEvent = {
+ id: number;
+ name: string;
+ description: string;
+ price: string;
+ date: string;
+ photos: string[];
+ featured_photo: string | null;
+ active: boolean;
+ sort_order: number;
+ reservation_count: number;
+};
+
+type SocialEventReservation = {
+ id: number;
+ event_id: number;
+ username: string;
+ event_name: string;
+ guests: number;
+ notes: string;
+ status: 'pending' | 'confirmed' | 'cancelled';
+ created_at: string;
+};
+
+type Room = {
id: number;
name: string;
description: string;
@@ -19,40 +74,18 @@ interface Room {
featured_photo: string | null;
active: boolean;
sort_order: number;
-}
+};
-interface RoomForm {
- id?: number;
- name: string;
- description: string;
- price: string;
- photos: string[];
- featured_photo: string | null;
- active: boolean;
- sort_order: number;
-}
-
-interface RoomComment {
+type RoomComment = {
id: number;
room_id: number;
- photo_url: string | null;
- user_id: number;
first_name: string;
- last_initial: string | null;
+ last_initial: string;
text: string;
created_at: string;
-}
+};
-interface User {
- id: number;
- username: string;
- role: 'admin' | 'user';
- first_name: string | null;
- last_name: string | null;
- comments_disabled: boolean;
-}
-
-interface MenuItem {
+type MenuItem = {
id: number;
category: string;
name: string;
@@ -61,438 +94,710 @@ interface MenuItem {
is_daily_special: boolean;
active: boolean;
sort_order: number;
-}
+};
-interface Place {
+type Place = {
id: number;
name: string;
description: string;
photos: string[];
featured_photo: string | null;
- distance_km: string | null;
- visit_time: string | null;
- suggestion: string | null;
+ distance_km: string;
+ visit_time: string;
+ suggestion: string;
active: boolean;
sort_order: number;
-}
+};
-interface Review {
+type Review = {
id: number;
author_name: string;
rating: number;
text: string;
approved: boolean;
-}
-
-interface Slide {
- id: number;
- title: string | null;
- subtitle: string | null;
- image_id: number | null;
- image_url: string | null;
- active: boolean;
- sort_order: number;
-}
-
-interface CalendarEvent {
- id: number;
- date: string;
- title: string;
- type: 'holiday' | 'season' | 'weather' | 'event';
- description: string | null;
- color: string | null;
- active: boolean;
-}
-
-interface SocialEvent {
- id: number;
- name: string;
- description: string;
- price: string | null;
- date: string | null;
- photos: string[];
- featured_photo: string | null;
- active: boolean;
- sort_order: number;
- reservation_count: number;
-}
-
-interface SocialEventReservation {
- id: number;
- social_event_id: number;
- user_id: number;
- username: string;
- event_name: string;
- guests: number;
- notes: string;
- status: 'pending' | 'confirmed' | 'cancelled';
- created_at: string;
-}
+};
+// Constants
const gold = '#E8A849';
-const navy = '#001321';
-const warm = '#a6683c';
+const navy = '#010D1E';
+const warm = '#7A5A3A';
+
+// Components
+function Text({ label, value, onChange, type = 'text' }: { label: string; value: string; onChange: (value: string) => void; type?: string }) {
+ return (
+
+ );
+}
+
+function Number({ label, value, onChange }: { label: string; value: number; onChange: (value: string) => void }) {
+ return (
+
+ );
+}
+
+function ImageSelect({ label, images, value, onChange }: { label: string; images: Image[]; value: string; onChange: (value: string) => void }) {
+ return (
+
+ );
+}
+
+function PhotoPicker({ label, images, photos, featuredPhoto, onChange }: { label: string; images: Image[]; photos: string[]; featuredPhoto: string | null; onChange: (photos: string[], featured: string | null) => void }) {
+ return (
+
+ {label}
+
+ {images
+ .filter((img) => photos.includes(img.data))
+ .map((img) => (
+
+

+
+ {featuredPhoto === img.data && (
+
+ Featured
+
+ )}
+
+ ))}
+
+
+
+ );
+}
+
+function ListTable({ rows, onEdit, onDelete, onComments }: { rows: any[][]; onEdit?: (index: number) => void; onDelete?: (index: number) => void; onComments?: (index: number) => void }) {
+ return (
+
+
+
+
+ {rows[0]?.map((_, i) => (
+ |
+ Column {i + 1}
+ |
+ ))}
+
+ Actions
+ |
+
+
+
+ {rows.map((row, i) => (
+
+ {row.map((cell, j) => (
+ |
+ {cell}
+ |
+ ))}
+
+
+ {onComments && (
+
+ )}
+ {onEdit && (
+
+ )}
+ {onDelete && (
+
+ )}
+
+ |
+
+ ))}
+
+
+
+ );
+}
+
+function AccordionSection({ title, children, defaultOpen = false }: { title: string; children: React.ReactNode; defaultOpen?: boolean }) {
+ const [open, setOpen] = useState(defaultOpen);
+ return (
+
+ setOpen((v) => !v)}>
+
{title}
+ {open ? 'Minimize' : 'Maximize'}
+
+ {open && {children}
}
+
+ );
+}
+
+function Button({ children, type = 'button', disabled, onClick }: { children: React.ReactNode; type?: 'button' | 'submit'; disabled?: boolean; onClick?: () => void }) {
+ return (
+
+ );
+}
+
+function SecondaryButton({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
+ return (
+
+ );
+}
+
+function DangerButton({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
+ return (
+
+ );
+}
+
+function PageWrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function ActionBar({ children }: { children: React.ReactNode }) {
+ return {children}
;
+}
+
+function StatsCards({ stats }: { stats: { label: string; value: string | number }[] }) {
+ return (
+
+ {stats.map((s) => (
+
+
{s.label}
+
{s.value}
+
+ ))}
+
+ );
+}
export default function AdminPage() {
- const router = useRouter();
- const [images, setImages] = useState([]);
+ // State
const [count, setCount] = useState(0);
- const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [message, setMessage] = useState(null);
const [logoUrl, setLogoUrl] = useState('');
const [faviconUrl, setFaviconUrl] = useState('');
- const [tagline, setTagline] = useState('Hotel · Restaurant · Andean Highlands');
-
+ const [tagline, setTagline] = useState('');
+ const [footer, setFooter] = useState({
+ facebook_url: '',
+ instagram_url: '',
+ whatsapp_url: '',
+ address: '',
+ phone: '',
+ email: '',
+ });
+ const [uploadCategory, setUploadCategory] = useState('rooms');
+ const [uploadRoomId, setUploadRoomId] = useState('');
+ const [uploadLocationTarget, setUploadLocationTarget] = useState('gallery');
+ const [images, setImages] = useState([]);
const [rooms, setRooms] = useState([]);
- const [roomForm, setRoomForm] = useState({ name: '', description: '', price: '', photos: [], featured_photo: null, active: true, sort_order: 0 });
- const [editingRoom, setEditingRoom] = useState(null);
- const [roomComments, setRoomComments] = useState([]);
- const [roomCommentRoomId, setRoomCommentRoomId] = useState(null);
- const [editingUser, setEditingUser] = useState(null);
- const [userEditForm, setUserEditForm] = useState({ first_name: '', last_name: '', comments_disabled: false });
-
- const [menu, setMenu] = useState