import React, { useState, useEffect, useMemo, useRef } from 'react';
import {
Calendar as CalendarIcon, MapPin, Wifi, Wind, Car, Utensils, Tv,
CheckCircle, XCircle, Clock, Home, Settings, CreditCard,
Image as ImageIcon, LayoutDashboard, Trash2, Plus, ArrowLeft,
MoreVertical, CalendarDays, BookOpen, User, Phone, List, ShieldAlert,
ChevronRight, ChevronLeft, Moon, Sun, Eye, EyeOff, Search, ChevronUp, Bell, Edit, Shield
} from 'lucide-react';
// ================= FONT, DARK MODE & GLOBAL STYLES =================
const FontSetup = () => (
);
// ================= IN-MEMORY DB (Meniru Supabase) =================
const INITIAL_STATE = {
homepage: {
homestay_name: 'Deloka Senja',
tagline: 'Keselesaan Mewah di Tengah Alam Semulajadi',
short_description: 'Alami percutian mendamaikan di villa premium kami. Konsep moden kontemporari khusus untuk keselesaan keluarga besar anda dengan privasi penuh.',
whatsapp_number: '60123456789',
address: 'Taman Deloka, Sungai Petani, Kedah',
check_in_time: '15:00',
check_out_time: '12:00',
},
pricing: { weekday: 250, weekend: 350, public_holiday: 400, school_holiday: 300, peak_season: 450, deposit: 100, cleaning_fee: 50, extra_guest_fee: 30, max_guests: 10, min_nights: 1 },
special_dates: [],
gallery: [
'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?auto=format&fit=crop&q=80&w=2000',
'https://images.unsplash.com/photo-1600607687920-4e2a09cf159d?auto=format&fit=crop&q=80&w=800',
'https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&q=80&w=800',
'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&q=80&w=800',
'https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&fit=crop&q=80&w=800'
],
facilities: [
{ id: 1, name: 'WiFi Pantas', icon: 'Wifi' }, { id: 2, name: 'Aircond Penuh', icon: 'Wind' },
{ id: 3, name: 'Parkir 3 Kereta', icon: 'Car' }, { id: 4, name: 'Dapur Lengkap', icon: 'Utensils' },
{ id: 5, name: 'Smart TV', icon: 'Tv' }, { id: 6, name: 'Kolam Renang', icon: 'CheckCircle' }
],
rules: [
{ id: 1, title: 'Waktu Daftar', desc: 'Check-in selepas 3.00 PM, Check-out sebelum 12.00 PM' },
{ id: 2, title: 'Larangan Merokok', desc: 'Tidak dibenarkan merokok di dalam rumah' },
{ id: 3, title: 'Haiwan Peliharaan', desc: 'Tidak dibenarkan membawa haiwan peliharaan' }
],
bookings: [
// Mock data untuk ujian
{ id: 'BK1001', guest_name: 'Ahmad Albab', guest_phone: '0123456789', check_in: '2026-05-20', check_out: '2026-05-22', guests: 4, total_nights: 2, total_price: 600, status: 'pending', created_at: new Date().toISOString() }
],
admins: [
{ id: 1, username: 'admin', password: 'admin123', role: 'Superadmin' }
]
};
// ================= UTILS & LOGIC =================
const formatCurrency = (amount) => `RM ${parseFloat(amount).toFixed(0)}`;
const formatDate = (dateString) => {
if (!dateString) return '';
const d = new Date(dateString);
const pad = n => n < 10 ? '0' + n : n;
return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()}`; // DD/MM/YYYY
};
const checkOverlap = (newIn, newOut, bookings, specialDates, ignoreBookingId = null) => {
const nIn = new Date(newIn).setHours(0,0,0,0);
const nOut = new Date(newOut).setHours(0,0,0,0);
const isBooked = bookings.some(b => {
if (b.id === ignoreBookingId || b.status === 'cancelled') return false;
const eIn = new Date(b.check_in).setHours(0,0,0,0);
const eOut = new Date(b.check_out).setHours(0,0,0,0);
return nIn < eOut && nOut > eIn;
});
const isBlocked = specialDates.some(sd => {
if (sd.status !== 'blocked') return false;
const sIn = new Date(sd.start).setHours(0,0,0,0);
const sOut = new Date(sd.end).setHours(0,0,0,0);
return nIn <= sOut && nOut >= sIn;
});
return isBooked || isBlocked;
};
const calculatePrice = (checkIn, checkOut, pricing, specialDates, guests) => {
let start = new Date(checkIn);
const end = new Date(checkOut);
let totalNights = 0;
let subtotal = 0;
const breakdown = [];
while (start < end) {
const currentDate = new Date(start);
const dateStr = currentDate.toISOString().split('T')[0];
const day = currentDate.getDay();
const isWeekend = day === 5 || day === 6 || day === 0;
let nightPrice = isWeekend ? pricing.weekend : pricing.weekday;
let nightType = isWeekend ? 'Hujung Minggu' : 'Hari Biasa';
let priority = 6;
specialDates.forEach(sd => {
const sStart = new Date(sd.start).setHours(0,0,0,0);
const sEnd = new Date(sd.end).setHours(0,0,0,0);
const curr = currentDate.setHours(0,0,0,0);
if (curr >= sStart && curr <= sEnd && sd.status !== 'blocked') {
const typePriority = { custom_price: 1, peak_season: 2, public_holiday: 3, school_holiday: 4 };
const currPriority = typePriority[sd.type] || 5;
if (currPriority < priority) {
priority = currPriority;
nightPrice = sd.price || pricing[sd.type] || nightPrice;
nightType = sd.type.replace('_', ' ').toUpperCase();
}
}
});
breakdown.push({ date: dateStr, type: nightType, price: nightPrice });
subtotal += parseFloat(nightPrice);
start.setDate(start.getDate() + 1);
totalNights++;
}
const extraGuests = Math.max(0, guests - 6);
const extraGuestTotal = extraGuests * pricing.extra_guest_fee * totalNights;
const grandTotal = subtotal + parseFloat(pricing.cleaning_fee) + extraGuestTotal;
const balance = grandTotal - parseFloat(pricing.deposit);
return { totalNights, breakdown, subtotal, cleaningFee: pricing.cleaning_fee, extraGuestFee: extraGuestTotal, grandTotal, deposit: pricing.deposit, balance };
};
const IconMap = { Wifi, Wind, Car, Utensils, Tv, CheckCircle };
// ================= REUSABLE COMPONENTS =================
const Toast = ({ message, type, onClose }) => {
useEffect(() => { const t = setTimeout(onClose, 3000); return () => clearTimeout(t); }, [onClose]);
const isErr = type === 'error';
return (
);
};
const ScrollToTop = () => {
const [visible, setVisible] = useState(false);
useEffect(() => {
const toggle = () => setVisible(window.scrollY > 300);
window.addEventListener('scroll', toggle);
return () => window.removeEventListener('scroll', toggle);
}, []);
if (!visible) return null;
return (
);
};
const Footer = () => (
);
const MonthCalendar = ({ bookings, specialDates }) => {
const [offset, setOffset] = useState(0);
const d = new Date();
d.setMonth(d.getMonth() + offset);
const year = d.getFullYear();
const month = d.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const days = Array(firstDay).fill(null).concat(Array.from({length: daysInMonth}, (_, i) => new Date(year, month, i + 1)));
const getStatus = (date) => {
if (!date) return null;
const time = date.getTime();
const today = new Date().setHours(0,0,0,0);
if (time < today) return 'past';
let isBlocked = false, isSpecial = false;
specialDates.forEach(sd => {
const s = new Date(sd.start).setHours(0,0,0,0);
const e = new Date(sd.end).setHours(0,0,0,0);
if (time >= s && time <= e) {
if (sd.status === 'blocked') isBlocked = true;
else isSpecial = true;
}
});
if (isBlocked) return 'blocked';
const isBooked = bookings.some(b => b.status !== 'cancelled' && time >= new Date(b.check_in).setHours(0,0,0,0) && time < new Date(b.check_out).setHours(0,0,0,0));
if (isBooked) return 'booked';
if (isSpecial) return 'special';
return 'available';
};
const monthNames = ["Januari", "Februari", "Mac", "April", "Mei", "Jun", "Julai", "Ogos", "September", "Oktober", "November", "Disember"];
return (
{monthNames[month]} {year}
{days.map((date, i) => {
const status = getStatus(date);
let style = "";
if (!date) return
;
if (status === 'past') style = "text-gray-300 opacity-50";
else if (status === 'blocked' || status === 'booked') style = "bg-red-50 text-red-500 line-through decoration-red-400 font-medium";
else if (status === 'special') style = "bg-emerald-100 text-emerald-800 font-bold ring-1 ring-emerald-300";
else style = "bg-gray-50 text-gray-700 font-bold hover:bg-emerald-100 hover:text-emerald-800 cursor-pointer shadow-sm border border-gray-100/50";
return
{date.getDate()}
;
})}
);
};
// ================= CUSTOMER VIEW =================
const CustomerView = ({ state, dispatch, setRoute, isDark, toggleDark }) => {
const { homepage, pricing, special_dates, facilities, gallery, rules, bookings } = state;
const [checkIn, setCheckIn] = useState('');
const [checkOut, setCheckOut] = useState('');
const [guestName, setGuestName] = useState('');
const [guestPhone, setGuestPhone] = useState('');
const [guests, setGuests] = useState(1);
const [toast, setToast] = useState(null);
const [selectedGalleryImg, setSelectedGalleryImg] = useState(0);
const bookingRef = useRef(null);
const today = new Date().toISOString().split('T')[0];
const minCheckOut = checkIn ? new Date(new Date(checkIn).getTime() + 86400000).toISOString().split('T')[0] : today;
const priceCalc = useMemo(() => {
if (checkIn && checkOut && new Date(checkIn) < new Date(checkOut)) {
return calculatePrice(checkIn, checkOut, pricing, special_dates, guests);
}
return null;
}, [checkIn, checkOut, pricing, special_dates, guests]);
const handleSubmit = (e) => {
e.preventDefault();
if (!checkIn || !checkOut || !guestName || !guestPhone) return setToast({ type: 'error', message: 'Sila lengkapkan borang.' });
if (checkOverlap(checkIn, checkOut, bookings, special_dates)) return setToast({ type: 'error', message: 'Tarikh telah ditempah atau ditutup.' });
const newBk = {
id: `BK${Math.floor(Math.random() * 10000)}`,
guest_name: guestName, guest_phone: guestPhone, check_in: checkIn, check_out: checkOut,
guests, total_nights: priceCalc.totalNights, total_price: priceCalc.grandTotal, status: 'pending', created_at: new Date().toISOString()
};
dispatch({ type: 'ADD_BOOKING', payload: newBk });
const waMsg = `*Tempahan ${homepage.homestay_name}*\n\nNama: ${guestName}\nIn: ${formatDate(checkIn)}\nOut: ${formatDate(checkOut)}\nMalam: ${priceCalc.totalNights}\nTetamu: ${guests}\nJumlah: ${formatCurrency(priceCalc.grandTotal)}\nDeposit: ${formatCurrency(priceCalc.deposit)}\n\nMohon pengesahan.`;
window.open(`https://wa.me/${homepage.whatsapp_number}?text=${encodeURIComponent(waMsg)}`, '_blank');
setCheckIn(''); setCheckOut(''); setGuestName(''); setGuestPhone('');
setToast({ type: 'success', message: 'Berjaya direkod! Membuka WhatsApp...' });
};
return (
{/* ================= DESKTOP WEB MENU ================= */}
{/* Mobile Top App-Like Controls (Floating) */}
{/* LEFT / MAIN CONTENT */}
{/* Gallery - Mobile Edge-to-Edge */}
{gallery.map((img, i) =>

)}
{/* Gallery - Desktop (1 Main Image + Thumbnail slides below) */}
{gallery.map((img, i) => (
))}
{/* Info Card */}
{homepage.homestay_name}
{formatCurrency(pricing.weekday)}
Bermula / Malam
{homepage.address}
Pengenalan
{homepage.short_description}
{/* Facilities */}
Kemudahan Disediakan
{facilities.map(f => {
const IconC = IconMap[f.icon] || CheckCircle;
return (
{f.name}
)
})}
{/* Rules */}
{/* ================= FULL VIEW CALENDAR ================= */}
Semak Kekosongan
Lihat tarikh kosong di bawah sebelum membuat tempahan.
{/* Legend */}
Tersedia
X
Tutup
Tarikh Khas
{/* RIGHT / BOOKING CARD */}
{/* ================= MOBILE FLOATING MENU (Telegram Style) ================= */}
{toast &&
setToast(null)} />}
);
};
// ================= ADMIN LOGIN =================
const AdminLogin = ({ state, setRoute }) => {
const [un, setUn] = useState('');
const [pw, setPw] = useState('');
const [showPw, setShowPw] = useState(false);
const [remember, setRemember] = useState(false);
const [toast, setToast] = useState(null);
useEffect(() => {
const saved = localStorage.getItem('delokaAdminAuth');
if (saved) {
const data = JSON.parse(saved);
setUn(data.un); setPw(data.pw); setRemember(true);
}
}, []);
const handleLogin = (e) => {
e.preventDefault();
const valid = state.admins.find(a => a.username === un && a.password === pw);
if (valid) {
if (remember) localStorage.setItem('delokaAdminAuth', JSON.stringify({un, pw}));
else localStorage.removeItem('delokaAdminAuth');
setRoute('admin-panel');
} else {
setToast({type: 'error', message: 'ID atau Kata Laluan Salah!'});
}
};
return (
Admin Panel
{toast &&
setToast(null)} />}
);
};
// ================= ADMIN PANEL =================
const AdminView = ({ state, dispatch, setRoute, isDark, toggleDark }) => {
const [activeTab, setActiveTab] = useState('dashboard');
const [toast, setToast] = useState(null);
const AdminDashboard = () => {
const today = new Date().setHours(0,0,0,0);
const next7Days = today + 7 * 86400000;
// Analisis & Notifikasi
const upcoming = state.bookings.filter(b => b.status === 'confirmed' && new Date(b.check_in).getTime() >= today && new Date(b.check_in).getTime() <= next7Days);
const jualanSah = state.bookings.filter(b => b.status === 'confirmed').reduce((sum,b)=>sum+b.total_price, 0);
const unjuranRugi = state.bookings.filter(b => b.status === 'cancelled').reduce((sum,b)=>sum+b.total_price, 0);
const totalTetamu = state.bookings.filter(b => b.status === 'confirmed').reduce((sum,b)=>sum+Number(b.guests), 0);
return (
Dashboard Keseluruhan
{[{t: 'Total Tempahan', v: state.bookings.length, c: 'text-gray-900'},
{t: 'Jualan Bersih', v: formatCurrency(jualanSah), c: 'text-emerald-600'},
{t: 'Batal (Unjuran Rugi)', v: formatCurrency(unjuranRugi), c: 'text-red-500'},
{t: 'Jumlah Tetamu Sah', v: `${totalTetamu} Orang`, c: 'text-blue-600'}
].map((s,i) => (
))}
Peringatan: Tempahan 7 Hari Terdekat
{upcoming.length > 0 ? (
{upcoming.map(b => (
{b.guest_name}
{formatDate(b.check_in)} - {formatDate(b.check_out)} | {b.guest_phone}
Sah
))}
) : (
Tiada tempahan terdekat untuk 7 hari akan datang.
)}
);
};
const AdminBookings = () => {
const [filter, setFilter] = useState('all');
const [search, setSearch] = useState('');
const filtered = state.bookings.filter(b => {
const matchStatus = filter === 'all' || b.status === filter;
const matchSearch = b.guest_name.toLowerCase().includes(search.toLowerCase()) || b.id.toLowerCase().includes(search.toLowerCase());
return matchStatus && matchSearch;
});
return (
Senarai Tempahan
{/* Compact Minimalist Filter & Search */}
{filtered.map(b => (
{b.guest_name.charAt(0).toUpperCase()}
{b.guest_phone} • {formatDate(b.check_in)} - {formatDate(b.check_out)} • {b.total_nights} mlm • {b.guests} Pax
{b.status}
{formatCurrency(b.total_price)}
{b.status === 'pending' && }
{b.status !== 'cancelled' && }
))}
{filtered.length===0 &&
Tiada rekod tempahan dijumpai.
}
);
};
const AdminFormConfig = ({ title, data, actionType, icon:Icon }) => {
const [form, setForm] = useState(data);
return (
);
};
const AdminUsers = () => {
const [un, setUn] = useState(''); const [pw, setPw] = useState(''); const [role, setRole] = useState('Admin');
return (
);
};
const tabs = [
{id:'dashboard', icon: LayoutDashboard, label: 'Dashboard', comp:
},
{id:'bookings', icon: BookOpen, label: 'Tempahan', comp:
},
{id:'calendar', icon: CalendarDays, label: 'Kalendar', comp:
Kalendar Semasa
},
{id:'pricing', icon: CreditCard, label: 'Harga Asas', comp:
},
{id:'special', icon: CalendarIcon, label: 'Tarikh Khas', comp: Pengurusan Tarikh Khas/Cuti
Form pengurusan tarikh dikekalkan integrasi backend (Simulasi).
}, // Minimized for code length, functional logic remains
{id:'homepage', icon: Home, label: 'Laman Utama', comp: },
{id:'users', icon: Shield, label: 'Urus Admin', comp: },
];
return (
{/* SIDEBAR DESKTOP - With neat scrollable area */}
{/* MOBILE TOP BAR */}
{tabs.map(t => (
))}
{tabs.find(t=>t.id===activeTab)?.comp}
{toast &&
setToast(null)} />}
);
};
// ================= MAIN APP COMPONENT =================
export default function DelokaSenjaApp() {
const [route, setRoute] = useState('customer'); // customer, admin-login, admin-panel
const [isDark, setIsDark] = useState(false);
useEffect(() => {
if(isDark) document.documentElement.classList.add('dark');
else document.documentElement.classList.remove('dark');
}, [isDark]);
const reducer = (state, action) => {
switch(action.type) {
case 'ADD_BOOKING': return {...state, bookings: [...state.bookings, action.payload]};
case 'UPDATE_BOOK': return {...state, bookings: state.bookings.map(b=>b.id===action.id ? {...b, status:action.val} : b)};
case 'DEL_BOOK': return {...state, bookings: state.bookings.filter(b=>b.id !== action.id)};
case 'UPDATE_PRICING': return {...state, pricing: action.payload};
case 'UPDATE_HOMEPAGE': return {...state, homepage: action.payload};
case 'ADD_SPECIAL': return {...state, special_dates: [...state.special_dates, action.payload]};
case 'DEL_SPECIAL': return {...state, special_dates: state.special_dates.filter(s=>s.id !== action.id)};
case 'ADD_ADMIN': return {...state, admins: [...state.admins, action.payload]};
case 'DEL_ADMIN': return {...state, admins: state.admins.filter(a=>a.id !== action.id)};
default: return state;
}
};
const [state, dispatch] = React.useReducer(reducer, INITIAL_STATE);
return (
<>
{route === 'customer' && setIsDark(!isDark)} />}
{route === 'admin-login' && }
{route === 'admin-panel' && setIsDark(!isDark)} />}
>
);
}