edit1
This commit is contained in:
@@ -33,7 +33,6 @@ import ContentManagement from './pages/admin/ContentManagement';
|
|||||||
import RoomManagement from './pages/admin/RoomManagement';
|
import RoomManagement from './pages/admin/RoomManagement';
|
||||||
import BookingManagement from './pages/admin/BookingManagement';
|
import BookingManagement from './pages/admin/BookingManagement';
|
||||||
import MediaManagement from './pages/admin/MediaManagement';
|
import MediaManagement from './pages/admin/MediaManagement';
|
||||||
import BlogManagement from './pages/admin/BlogManagement';
|
|
||||||
import SettingsManagement from './pages/admin/SettingsManagement';
|
import SettingsManagement from './pages/admin/SettingsManagement';
|
||||||
|
|
||||||
// Loading Component
|
// Loading Component
|
||||||
@@ -93,18 +92,16 @@ function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* NEW: redirect /admin -> /admin/dashboard */}
|
|
||||||
<Route index element={<Navigate to="dashboard" replace />} />
|
<Route index element={<Navigate to="dashboard" replace />} />
|
||||||
|
|
||||||
<Route path="dashboard" element={<DashboardMain />} />
|
<Route path="dashboard" element={<DashboardMain />} />
|
||||||
<Route path="content" element={<ContentManagement />} />
|
<Route path="content" element={<ContentManagement />} />
|
||||||
<Route path="rooms" element={<RoomManagement />} />
|
<Route path="rooms" element={<RoomManagement />} />
|
||||||
<Route path="bookings" element={<BookingManagement />} />
|
<Route path="bookings" element={<BookingManagement />} />
|
||||||
<Route path="blog" element={<BlogManagement />} />
|
|
||||||
<Route path="media" element={<MediaManagement />} />
|
<Route path="media" element={<MediaManagement />} />
|
||||||
<Route path="settings" element={<SettingsManagement />} />
|
<Route path="settings" element={<SettingsManagement />} />
|
||||||
|
|
||||||
{/* Optional fallback: keep if you want unknown admin routes to land on dashboard */}
|
{/* أي مسار غير معروف داخل /admin يروح للداشبورد */}
|
||||||
<Route path="*" element={<DashboardMain />} />
|
<Route path="*" element={<DashboardMain />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -22,37 +22,28 @@ import {
|
|||||||
import {
|
import {
|
||||||
Menu as MenuIcon,
|
Menu as MenuIcon,
|
||||||
Dashboard as DashboardIcon,
|
Dashboard as DashboardIcon,
|
||||||
Article as ArticleIcon,
|
|
||||||
Hotel as HotelIcon,
|
Hotel as HotelIcon,
|
||||||
CalendarMonth as CalendarIcon,
|
CalendarMonth as CalendarIcon,
|
||||||
Settings as SettingsIcon,
|
Settings as SettingsIcon,
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
ContentPaste as ContentIcon,
|
ContentPaste as ContentIcon,
|
||||||
People as PeopleIcon,
|
|
||||||
Logout as LogoutIcon,
|
Logout as LogoutIcon,
|
||||||
AccountCircle as AccountIcon,
|
AccountCircle as AccountIcon
|
||||||
Assessment as AssessmentIcon
|
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
|
|
||||||
const drawerWidth = 260;
|
const drawerWidth = 260;
|
||||||
|
|
||||||
|
// ✅ Removed: Blog, Guests, Analytics (UI only)
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ title: 'Dashboard', path: '/admin/dashboard', icon: <DashboardIcon /> },
|
{ title: 'Dashboard', path: '/admin/dashboard', icon: <DashboardIcon /> },
|
||||||
{ title: 'Content', path: '/admin/content', icon: <ContentIcon /> },
|
{ title: 'Content', path: '/admin/content', icon: <ContentIcon /> },
|
||||||
{ title: 'Rooms', path: '/admin/rooms', icon: <HotelIcon /> },
|
{ title: 'Rooms', path: '/admin/rooms', icon: <HotelIcon /> },
|
||||||
{ title: 'Bookings', path: '/admin/bookings', icon: <CalendarIcon /> },
|
{ title: 'Bookings', path: '/admin/bookings', icon: <CalendarIcon /> },
|
||||||
{ title: 'Blog', path: '/admin/blog', icon: <ArticleIcon /> },
|
|
||||||
{ title: 'Media', path: '/admin/media', icon: <ImageIcon /> },
|
{ title: 'Media', path: '/admin/media', icon: <ImageIcon /> },
|
||||||
{ title: 'Guests', path: '/admin/guests', icon: <PeopleIcon /> },
|
|
||||||
{ title: 'Analytics', path: '/admin/analytics', icon: <AssessmentIcon /> },
|
|
||||||
{ title: 'Settings', path: '/admin/settings', icon: <SettingsIcon /> },
|
{ title: 'Settings', path: '/admin/settings', icon: <SettingsIcon /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ✅ hide from sidebar only (keep code/routes for future)
|
|
||||||
const hiddenMenuTitles = new Set(['Guests', 'Blog']);
|
|
||||||
const adminMenuItems = menuItems.filter(item => !hiddenMenuTitles.has(item.title));
|
|
||||||
|
|
||||||
const AdminLayout = () => {
|
const AdminLayout = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
@@ -82,9 +73,7 @@ const AdminLayout = () => {
|
|||||||
|
|
||||||
const handleNavigate = (path) => {
|
const handleNavigate = (path) => {
|
||||||
navigate(path);
|
navigate(path);
|
||||||
if (isMobile) {
|
if (isMobile) setMobileOpen(false);
|
||||||
setMobileOpen(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawer = (
|
const drawer = (
|
||||||
@@ -106,7 +95,7 @@ const AdminLayout = () => {
|
|||||||
</Toolbar>
|
</Toolbar>
|
||||||
<Divider />
|
<Divider />
|
||||||
<List>
|
<List>
|
||||||
{adminMenuItems.map((item) => (
|
{menuItems.map((item) => (
|
||||||
<ListItem key={item.title} disablePadding>
|
<ListItem key={item.title} disablePadding>
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
selected={location.pathname === item.path}
|
selected={location.pathname === item.path}
|
||||||
@@ -157,6 +146,7 @@ const AdminLayout = () => {
|
|||||||
>
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||||
The Old Vine Hotel
|
The Old Vine Hotel
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -175,14 +165,8 @@ const AdminLayout = () => {
|
|||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
open={Boolean(anchorEl)}
|
open={Boolean(anchorEl)}
|
||||||
onClose={handleProfileMenuClose}
|
onClose={handleProfileMenuClose}
|
||||||
anchorOrigin={{
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||||
vertical: 'bottom',
|
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<MenuItem disabled>
|
<MenuItem disabled>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
@@ -206,38 +190,25 @@ const AdminLayout = () => {
|
|||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
|
||||||
<Box
|
<Box component="nav" sx={{ width: { md: drawerWidth }, flexShrink: { md: 0 } }}>
|
||||||
component="nav"
|
|
||||||
sx={{ width: { md: drawerWidth }, flexShrink: { md: 0 } }}
|
|
||||||
>
|
|
||||||
{/* Mobile drawer */}
|
|
||||||
<Drawer
|
<Drawer
|
||||||
variant="temporary"
|
variant="temporary"
|
||||||
open={mobileOpen}
|
open={mobileOpen}
|
||||||
onClose={handleDrawerToggle}
|
onClose={handleDrawerToggle}
|
||||||
ModalProps={{
|
ModalProps={{ keepMounted: true }}
|
||||||
keepMounted: true, // Better open performance on mobile.
|
|
||||||
}}
|
|
||||||
sx={{
|
sx={{
|
||||||
display: { xs: 'block', md: 'none' },
|
display: { xs: 'block', md: 'none' },
|
||||||
'& .MuiDrawer-paper': {
|
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
||||||
boxSizing: 'border-box',
|
|
||||||
width: drawerWidth,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{drawer}
|
{drawer}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
{/* Desktop drawer */}
|
|
||||||
<Drawer
|
<Drawer
|
||||||
variant="permanent"
|
variant="permanent"
|
||||||
sx={{
|
sx={{
|
||||||
display: { xs: 'none', md: 'block' },
|
display: { xs: 'none', md: 'block' },
|
||||||
'& .MuiDrawer-paper': {
|
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
||||||
boxSizing: 'border-box',
|
|
||||||
width: drawerWidth,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
open
|
open
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
Container,
|
||||||
@@ -24,11 +24,74 @@ import {
|
|||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import api from '../../utils/api';
|
||||||
|
|
||||||
|
// ✅ Convert any weird value (like {coordinates}) into a safe string
|
||||||
|
const toText = (val, fallback = '') => {
|
||||||
|
if (val == null) return fallback;
|
||||||
|
if (typeof val === 'string') return val;
|
||||||
|
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
||||||
|
if (Array.isArray(val)) return val.map(v => toText(v, '')).filter(Boolean).join(', ');
|
||||||
|
|
||||||
|
if (typeof val === 'object') {
|
||||||
|
// common cases
|
||||||
|
if (typeof val.text === 'string') return val.text;
|
||||||
|
if (typeof val.label === 'string') return val.label;
|
||||||
|
if (typeof val.name === 'string') return val.name;
|
||||||
|
if (typeof val.address === 'string') return val.address;
|
||||||
|
|
||||||
|
// GeoJSON-ish: { coordinates: [lng, lat] } or { coordinates: {lat,lng} }
|
||||||
|
if (val.coordinates) {
|
||||||
|
if (Array.isArray(val.coordinates) && val.coordinates.length >= 2) {
|
||||||
|
const [lng, lat] = val.coordinates;
|
||||||
|
if (lat != null && lng != null) return `${lat}, ${lng}`;
|
||||||
|
}
|
||||||
|
if (typeof val.coordinates === 'object') {
|
||||||
|
const lat = val.coordinates.lat ?? val.coordinates.latitude;
|
||||||
|
const lng = val.coordinates.lng ?? val.coordinates.lon ?? val.coordinates.longitude;
|
||||||
|
if (lat != null && lng != null) return `${lat}, ${lng}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [settings, setSettings] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/api/settings/public');
|
||||||
|
if (mounted) setSettings(res?.data?.data?.settings || null);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Footer settings load error:', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => { mounted = false; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hotelName = toText(settings?.hotel?.name, 'The Old Vine Hotel');
|
||||||
|
const address = toText(settings?.hotel?.address, 'Old Damascus City');
|
||||||
|
const phone = toText(settings?.hotel?.phone, '0112241609');
|
||||||
|
const email = toText(settings?.hotel?.email, 'reservations@oldvinehotel.com');
|
||||||
|
const whatsapp = toText(settings?.hotel?.whatsapp, '+963986105010');
|
||||||
|
|
||||||
|
const social = settings?.hotel?.socialMedia || {};
|
||||||
|
const socialLinks = useMemo(() => ([
|
||||||
|
{ icon: <Facebook />, url: toText(social.facebook, ''), label: 'Facebook' },
|
||||||
|
{ icon: <Instagram />, url: toText(social.instagram, ''), label: 'Instagram' },
|
||||||
|
{ icon: <Twitter />, url: toText(social.twitter, ''), label: 'Twitter' },
|
||||||
|
{ icon: <LinkedIn />, url: toText(social.linkedin, ''), label: 'LinkedIn' },
|
||||||
|
].filter(s => s.url)), [social]);
|
||||||
|
|
||||||
const quickLinks = [
|
const quickLinks = [
|
||||||
{ label: t('nav.home'), path: '/' },
|
{ label: t('nav.home'), path: '/' },
|
||||||
{ label: t('nav.about'), path: '/about' },
|
{ label: t('nav.about'), path: '/about' },
|
||||||
@@ -38,13 +101,6 @@ const Footer = () => {
|
|||||||
{ label: t('nav.contact'), path: '/contact' },
|
{ label: t('nav.contact'), path: '/contact' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const socialLinks = [
|
|
||||||
{ icon: <Facebook />, url: 'https://facebook.com/oldvinehotel', label: 'Facebook' },
|
|
||||||
{ icon: <Instagram />, url: 'https://instagram.com/oldvinehotel', label: 'Instagram' },
|
|
||||||
{ icon: <Twitter />, url: 'https://twitter.com/oldvinehotel', label: 'Twitter' },
|
|
||||||
{ icon: <LinkedIn />, url: 'https://linkedin.com/company/oldvinehotel', label: 'LinkedIn' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
component="footer"
|
component="footer"
|
||||||
@@ -58,7 +114,6 @@ const Footer = () => {
|
|||||||
>
|
>
|
||||||
<Container maxWidth="lg">
|
<Container maxWidth="lg">
|
||||||
<Grid container spacing={4}>
|
<Grid container spacing={4}>
|
||||||
{/* Hotel Information */}
|
|
||||||
<Grid item xs={12} md={4}>
|
<Grid item xs={12} md={4}>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@@ -75,19 +130,19 @@ const Footer = () => {
|
|||||||
color: theme.palette.secondary.main,
|
color: theme.palette.secondary.main,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
The Old Vine Hotel
|
{hotelName}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body2" sx={{ mb: 3, lineHeight: 1.7 }}>
|
<Typography variant="body2" sx={{ mb: 3, lineHeight: 1.7 }}>
|
||||||
{t('footer.description')}
|
{t('footer.description')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Social Media Links */}
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, mb: 3 }}>
|
<Box sx={{ display: 'flex', gap: 1, mb: 3 }}>
|
||||||
{socialLinks.map((social) => (
|
{socialLinks.map((s) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={social.label}
|
key={s.label}
|
||||||
component="a"
|
component="a"
|
||||||
href={social.url}
|
href={s.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -100,14 +155,13 @@ const Footer = () => {
|
|||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.3s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{social.icon}
|
{s.icon}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Quick Links */}
|
|
||||||
<Grid item xs={12} sm={6} md={2}>
|
<Grid item xs={12} sm={6} md={2}>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@@ -122,31 +176,29 @@ const Footer = () => {
|
|||||||
>
|
>
|
||||||
{t('footer.quickLinks')}
|
{t('footer.quickLinks')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box component="nav">
|
<Box component="nav">
|
||||||
{quickLinks.map((link) => (
|
{quickLinks.map((l) => (
|
||||||
<Link
|
<Link
|
||||||
key={link.path}
|
key={l.path}
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
to={link.path}
|
to={l.path}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'block',
|
display: 'block',
|
||||||
color: 'rgba(255, 255, 255, 0.8)',
|
color: 'rgba(255, 255, 255, 0.8)',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
mb: 1,
|
mb: 1,
|
||||||
transition: 'color 0.3s ease',
|
transition: 'color 0.3s ease',
|
||||||
'&:hover': {
|
'&:hover': { color: theme.palette.secondary.main },
|
||||||
color: theme.palette.secondary.main,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{link.label}
|
{l.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Contact Information */}
|
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
<Grid item xs={12} sm={6} md={3}>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@@ -164,35 +216,26 @@ const Footer = () => {
|
|||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
<LocationOn sx={{ mr: 1, fontSize: 20 }} />
|
<LocationOn sx={{ mr: 1, fontSize: 20 }} />
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">{address}</Typography>
|
||||||
Old Damascus City
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
<Phone sx={{ mr: 1, fontSize: 20 }} />
|
<Phone sx={{ mr: 1, fontSize: 20 }} />
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">{phone}</Typography>
|
||||||
0112241609
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
<Email sx={{ mr: 1, fontSize: 20 }} />
|
<Email sx={{ mr: 1, fontSize: 20 }} />
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">{email}</Typography>
|
||||||
reservations@oldvinehotel.com
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
<WhatsApp sx={{ mr: 1, fontSize: 20 }} />
|
<WhatsApp sx={{ mr: 1, fontSize: 20 }} />
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">{whatsapp}</Typography>
|
||||||
+963986105010
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Newsletter Signup */}
|
|
||||||
<Grid item xs={12} md={3}>
|
<Grid item xs={12} md={3}>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@@ -220,15 +263,9 @@ const Footer = () => {
|
|||||||
'& .MuiOutlinedInput-root': {
|
'& .MuiOutlinedInput-root': {
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
'& fieldset': {
|
'& fieldset': { borderColor: 'rgba(255, 255, 255, 0.3)' },
|
||||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
'&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.5)' },
|
||||||
},
|
'&.Mui-focused fieldset': { borderColor: theme.palette.secondary.main },
|
||||||
'&:hover fieldset': {
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
||||||
},
|
|
||||||
'&.Mui-focused fieldset': {
|
|
||||||
borderColor: theme.palette.secondary.main,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'& .MuiInputBase-input::placeholder': {
|
'& .MuiInputBase-input::placeholder': {
|
||||||
color: 'rgba(255, 255, 255, 0.7)',
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
@@ -236,13 +273,7 @@ const Footer = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button variant="contained" color="secondary" sx={{ fontWeight: 600 }}>
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 600,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('footer.subscribe')}
|
{t('footer.subscribe')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -252,7 +283,6 @@ const Footer = () => {
|
|||||||
|
|
||||||
<Divider sx={{ my: 4, borderColor: 'rgba(255, 255, 255, 0.2)' }} />
|
<Divider sx={{ my: 4, borderColor: 'rgba(255, 255, 255, 0.2)' }} />
|
||||||
|
|
||||||
{/* Bottom Section */}
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -263,7 +293,7 @@ const Footer = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2" color="rgba(255, 255, 255, 0.8)">
|
<Typography variant="body2" color="rgba(255, 255, 255, 0.8)">
|
||||||
© {new Date().getFullYear()} The Old Vine Hotel. {t('footer.rights')}
|
© {new Date().getFullYear()} {hotelName}. {t('footer.rights')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: 3 }}>
|
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||||
@@ -273,9 +303,7 @@ const Footer = () => {
|
|||||||
color: 'rgba(255, 255, 255, 0.8)',
|
color: 'rgba(255, 255, 255, 0.8)',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
'&:hover': {
|
'&:hover': { color: theme.palette.secondary.main },
|
||||||
color: theme.palette.secondary.main,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('footer.privacy')}
|
{t('footer.privacy')}
|
||||||
@@ -286,9 +314,7 @@ const Footer = () => {
|
|||||||
color: 'rgba(255, 255, 255, 0.8)',
|
color: 'rgba(255, 255, 255, 0.8)',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
'&:hover': {
|
'&:hover': { color: theme.palette.secondary.main },
|
||||||
color: theme.palette.secondary.main,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('footer.terms')}
|
{t('footer.terms')}
|
||||||
|
|||||||
@@ -36,12 +36,6 @@ const Header = () => {
|
|||||||
|
|
||||||
const lightHeaderText = '#1a1a1a';
|
const lightHeaderText = '#1a1a1a';
|
||||||
|
|
||||||
const WA_NUMBER = '963986105010';
|
|
||||||
const WA_TEXT = encodeURIComponent(
|
|
||||||
'For all booking inquiries and reservation confirmations, kindly contact us via WhatsApp'
|
|
||||||
);
|
|
||||||
const WA_LINK = `https://wa.me/${WA_NUMBER}?text=${WA_TEXT}`;
|
|
||||||
|
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
{ label: t('nav.home'), path: '/' },
|
{ label: t('nav.home'), path: '/' },
|
||||||
{ label: t('nav.about'), path: '/about' },
|
{ label: t('nav.about'), path: '/about' },
|
||||||
@@ -125,12 +119,11 @@ const Header = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* ✅ بدل واتساب: يروح على صفحة /booking */}
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
component="a"
|
component={Link}
|
||||||
href={WA_LINK}
|
to="/booking"
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
sx={{
|
sx={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
backgroundColor: 'secondary.main',
|
backgroundColor: 'secondary.main',
|
||||||
@@ -235,16 +228,15 @@ const Header = () => {
|
|||||||
<LanguageIcon />
|
<LanguageIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
|
{/* ✅ بدل واتساب: يروح على صفحة /booking */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.5, delay: 0.3 }}
|
transition={{ duration: 0.5, delay: 0.3 }}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
component="a"
|
component={Link}
|
||||||
href={WA_LINK}
|
to="/booking"
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
sx={{ fontWeight: 600, px: 3 }}
|
sx={{ fontWeight: 600, px: 3 }}
|
||||||
|
|||||||
42
client/src/context/SettingsContext.js
Normal file
42
client/src/context/SettingsContext.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
import api from '../utils/api';
|
||||||
|
|
||||||
|
const SettingsContext = createContext({ settings: null, loading: true });
|
||||||
|
|
||||||
|
const unwrapSettings = (res) => {
|
||||||
|
// يدعم كذا شكل للريسبونس
|
||||||
|
return res?.data?.data?.settings || res?.data?.data || res?.data || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsProvider = ({ children }) => {
|
||||||
|
const [settings, setSettings] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// ✅ public endpoint
|
||||||
|
const res = await api.get('/api/settings/public');
|
||||||
|
if (mounted) setSettings(unwrapSettings(res));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error loading public settings:', e);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => { mounted = false; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ settings, loading }), [settings, loading]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</SettingsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSettings = () => useContext(SettingsContext);
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Container, Typography, Box, TextField, Button,
|
Container,
|
||||||
MenuItem, Alert, CircularProgress
|
Typography,
|
||||||
|
Box,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
MenuItem,
|
||||||
|
Alert,
|
||||||
|
CircularProgress
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import api from '../utils/api';
|
import api from '../utils/api';
|
||||||
|
|
||||||
const Booking = () => {
|
const Booking = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [rooms, setRooms] = useState([]);
|
const [rooms, setRooms] = useState([]);
|
||||||
const [loadingRooms, setLoadingRooms] = useState(true);
|
const [loadingRooms, setLoadingRooms] = useState(true);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@@ -25,35 +34,69 @@ const Booking = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
const loadRooms = async () => {
|
const loadRooms = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/api/rooms?limit=100');
|
const res = await api.get('/api/rooms?limit=100');
|
||||||
setRooms(res?.data?.data?.rooms || []);
|
const fetched = res?.data?.data?.rooms || [];
|
||||||
|
if (mounted) setRooms(fetched);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMessage({ type: 'error', text: 'Failed to load rooms' });
|
if (mounted) setMessage({ type: 'error', text: 'Failed to load rooms' });
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingRooms(false);
|
if (mounted) setLoadingRooms(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadRooms();
|
loadRooms();
|
||||||
|
|
||||||
|
return () => { mounted = false; };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleChange = (key) => (e) => {
|
const handleChange = (key) => (e) => {
|
||||||
setForm(prev => ({ ...prev, [key]: e.target.value }));
|
setForm(prev => ({ ...prev, [key]: e.target.value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateBeforeSubmit = () => {
|
||||||
|
// check dates
|
||||||
|
if (form.checkInDate && form.checkOutDate) {
|
||||||
|
const inD = new Date(form.checkInDate);
|
||||||
|
const outD = new Date(form.checkOutDate);
|
||||||
|
if (inD >= outD) {
|
||||||
|
setMessage({ type: 'error', text: 'Check-out date must be after check-in date' });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const adults = Number(form.adults);
|
||||||
|
const children = Number(form.children || 0);
|
||||||
|
if (!Number.isFinite(adults) || adults < 1) {
|
||||||
|
setMessage({ type: 'error', text: 'Adults must be at least 1' });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(children) || children < 0) {
|
||||||
|
setMessage({ type: 'error', text: 'Children cannot be negative' });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSubmitting(true);
|
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
|
|
||||||
|
if (!validateBeforeSubmit()) return;
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
guestInfo: {
|
guestInfo: {
|
||||||
firstName: form.firstName,
|
firstName: form.firstName.trim(),
|
||||||
lastName: form.lastName,
|
lastName: form.lastName.trim(),
|
||||||
email: form.email,
|
email: form.email.trim(),
|
||||||
phone: form.phone
|
phone: form.phone.trim()
|
||||||
},
|
},
|
||||||
roomId: form.roomId,
|
roomId: form.roomId,
|
||||||
checkInDate: form.checkInDate,
|
checkInDate: form.checkInDate,
|
||||||
@@ -65,24 +108,37 @@ const Booking = () => {
|
|||||||
specialRequests: form.specialRequests
|
specialRequests: form.specialRequests
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ هذا لازم يكون موجود بالـ CMS: POST /api/bookings/request
|
||||||
const res = await api.post('/api/bookings/request', payload);
|
const res = await api.post('/api/bookings/request', payload);
|
||||||
const bookingNumber = res?.data?.data?.booking?.bookingNumber;
|
|
||||||
|
|
||||||
setMessage({
|
const booking = res?.data?.data?.booking || null;
|
||||||
type: 'success',
|
const bookingNumber = booking?.bookingNumber || res?.data?.data?.bookingNumber || null;
|
||||||
text: bookingNumber
|
|
||||||
? `Booking request submitted! Booking #: ${bookingNumber}`
|
const selectedRoom = rooms.find(r => r._id === form.roomId);
|
||||||
: 'Booking request submitted successfully!'
|
|
||||||
});
|
// ✅ معلومات نبعثها لصفحة BookingConfirmation
|
||||||
|
const requestForConfirmation = {
|
||||||
|
fullName: `${form.firstName} ${form.lastName}`.trim(),
|
||||||
|
phone: form.phone,
|
||||||
|
email: form.email,
|
||||||
|
checkInDate: form.checkInDate,
|
||||||
|
checkOutDate: form.checkOutDate,
|
||||||
|
adults: Number(form.adults),
|
||||||
|
children: Number(form.children || 0),
|
||||||
|
roomCategory: selectedRoom?.name || '',
|
||||||
|
message: form.specialRequests || '',
|
||||||
|
bookingNumber: bookingNumber || '',
|
||||||
|
autoOpenWhatsApp: true // إذا صفحة الـ Confirmation عندك بتفتح واتساب تلقائيًا
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ روح على صفحة التأكيد (بدون ما نظل هون)
|
||||||
|
navigate('/booking/confirmation', { state: { request: requestForConfirmation } });
|
||||||
|
|
||||||
// reset minimal
|
|
||||||
setForm(prev => ({ ...prev, specialRequests: '' }));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessage({
|
setMessage({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: error.response?.data?.message || 'Failed to submit booking request'
|
text: error.response?.data?.message || 'Failed to submit booking request'
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -109,7 +165,11 @@ const Booking = () => {
|
|||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
<Box
|
||||||
|
component="form"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
|
||||||
|
>
|
||||||
<TextField label="First Name" value={form.firstName} onChange={handleChange('firstName')} required />
|
<TextField label="First Name" value={form.firstName} onChange={handleChange('firstName')} required />
|
||||||
<TextField label="Last Name" value={form.lastName} onChange={handleChange('lastName')} required />
|
<TextField label="Last Name" value={form.lastName} onChange={handleChange('lastName')} required />
|
||||||
<TextField label="Email" type="email" value={form.email} onChange={handleChange('email')} required />
|
<TextField label="Email" type="email" value={form.email} onChange={handleChange('email')} required />
|
||||||
|
|||||||
@@ -1,27 +1,115 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { Container, Typography, Box } from '@mui/material';
|
import { Container, Typography, Box, Button, Paper, Divider } from '@mui/material';
|
||||||
|
import { useLocation, Link } from 'react-router-dom';
|
||||||
|
|
||||||
const BookingConfirmation = () => {
|
const BookingConfirmation = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// نتوقع إنه صفحة الحجز تبعتنا رح تعمل navigate وتبعت state فيه request
|
||||||
|
const request = location.state?.request || null;
|
||||||
|
|
||||||
|
const WA_NUMBER = useMemo(() => {
|
||||||
|
// إذا عندك رقم ثابت حاليا
|
||||||
|
return '963986105010';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const waText = useMemo(() => {
|
||||||
|
if (!request) {
|
||||||
|
return encodeURIComponent('Hello, I would like to book a room at Old Vine Hotel.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
'New booking request from website:',
|
||||||
|
`Name: ${request.fullName || ''}`,
|
||||||
|
`Phone: ${request.phone || ''}`,
|
||||||
|
request.email ? `Email: ${request.email}` : null,
|
||||||
|
request.checkInDate ? `Check-in: ${new Date(request.checkInDate).toLocaleDateString()}` : null,
|
||||||
|
request.checkOutDate ? `Check-out: ${new Date(request.checkOutDate).toLocaleDateString()}` : null,
|
||||||
|
request.adults != null ? `Adults: ${request.adults}` : null,
|
||||||
|
request.children != null ? `Children: ${request.children}` : null,
|
||||||
|
request.roomCategory ? `Category: ${request.roomCategory}` : null,
|
||||||
|
request.message ? `Message: ${request.message}` : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
return encodeURIComponent(lines.join('\n'));
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const WA_LINK = useMemo(() => `https://wa.me/${WA_NUMBER}?text=${waText}`, [WA_NUMBER, waText]);
|
||||||
|
|
||||||
|
// خيار: تفتح واتساب أوتوماتيك بعد نجاح الإرسال
|
||||||
|
useEffect(() => {
|
||||||
|
if (request?.autoOpenWhatsApp) {
|
||||||
|
window.open(WA_LINK, '_blank', 'noopener,noreferrer');
|
||||||
|
}
|
||||||
|
}, [request, WA_LINK]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="lg" sx={{ py: 8 }}>
|
<Container maxWidth="lg" sx={{ py: 8 }}>
|
||||||
<Box sx={{ textAlign: 'center', mb: 6 }}>
|
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
||||||
<Typography variant="h3" component="h1" gutterBottom>
|
<Typography variant="h3" component="h1" gutterBottom>
|
||||||
Booking Confirmation
|
Booking Request Sent
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h5" color="text.secondary">
|
<Typography variant="h5" color="text.secondary">
|
||||||
Your reservation has been confirmed
|
We received your request ✅
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{
|
|
||||||
display: 'flex',
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
flexDirection: 'column',
|
<Paper sx={{ p: 4, width: '100%', maxWidth: 720 }}>
|
||||||
alignItems: 'center',
|
<Typography variant="body1" sx={{ textAlign: 'center', mb: 3 }}>
|
||||||
gap: 4
|
Thank you! Our team will contact you soon. You can also confirm faster via WhatsApp.
|
||||||
}}>
|
|
||||||
<Typography variant="body1" sx={{ textAlign: 'center', maxWidth: 800 }}>
|
|
||||||
Thank you for your booking. You will receive a confirmation email shortly.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
{/* Add booking confirmation details here */}
|
|
||||||
|
{request && (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
|
Request Summary
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'grid', gap: 1 }}>
|
||||||
|
<Typography><b>Name:</b> {request.fullName || '—'}</Typography>
|
||||||
|
<Typography><b>Phone:</b> {request.phone || '—'}</Typography>
|
||||||
|
{request.email && <Typography><b>Email:</b> {request.email}</Typography>}
|
||||||
|
{request.roomCategory && <Typography><b>Category:</b> {request.roomCategory}</Typography>}
|
||||||
|
{request.checkInDate && <Typography><b>Check-in:</b> {new Date(request.checkInDate).toLocaleDateString()}</Typography>}
|
||||||
|
{request.checkOutDate && <Typography><b>Check-out:</b> {new Date(request.checkOutDate).toLocaleDateString()}</Typography>}
|
||||||
|
{(request.adults != null || request.children != null) && (
|
||||||
|
<Typography><b>Guests:</b> {request.adults ?? 0} adults, {request.children ?? 0} children</Typography>
|
||||||
|
)}
|
||||||
|
{request.message && <Typography><b>Message:</b> {request.message}</Typography>}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mt: 4, justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href={WA_LINK}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
sx={{ px: 4 }}
|
||||||
|
>
|
||||||
|
Open WhatsApp
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button component={Link} to="/rooms" variant="outlined" sx={{ px: 4 }}>
|
||||||
|
Browse Rooms
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button component={Link} to="/" variant="text" sx={{ px: 4 }}>
|
||||||
|
Back Home
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{!request && (
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', textAlign: 'center', mt: 2 }}>
|
||||||
|
(No request details were passed to this page.)
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
Hotel as HotelIcon,
|
Hotel as HotelIcon,
|
||||||
CalendarMonth as CalendarIcon,
|
CalendarMonth as CalendarIcon,
|
||||||
People as PeopleIcon,
|
People as PeopleIcon,
|
||||||
Article as ArticleIcon,
|
TrendingUp as TrendingUpIcon,
|
||||||
TrendingUp as TrendingUpIcon
|
ContentPaste as ContentIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
@@ -73,9 +73,7 @@ const DashboardMain = () => {
|
|||||||
const fetchStats = async () => {
|
const fetchStats = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
console.log('📊 Dashboard: Fetching stats...');
|
|
||||||
const response = await api.get('/api/admin/stats');
|
const response = await api.get('/api/admin/stats');
|
||||||
console.log('📊 Dashboard: Stats received:', response.data);
|
|
||||||
setStats(response.data.data.stats);
|
setStats(response.data.data.stats);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('📊 Dashboard: Error fetching stats:', err);
|
console.error('📊 Dashboard: Error fetching stats:', err);
|
||||||
@@ -93,9 +91,7 @@ const DashboardMain = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) return <Alert severity="error">{error}</Alert>;
|
||||||
return <Alert severity="error">{error}</Alert>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
@@ -109,7 +105,6 @@ const DashboardMain = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{/* Stats Cards */}
|
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={12} sm={6} md={4}>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Total Rooms"
|
title="Total Rooms"
|
||||||
@@ -139,17 +134,7 @@ const DashboardMain = () => {
|
|||||||
color="#7CBF9E"
|
color="#7CBF9E"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{/*
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
|
||||||
<StatCard
|
|
||||||
title="Blog Posts"
|
|
||||||
value={stats?.blogPosts || 0}
|
|
||||||
subtitle="Published articles"
|
|
||||||
icon={<ArticleIcon />}
|
|
||||||
color="#3A635F"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={12} sm={6} md={4}>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Monthly Revenue"
|
title="Monthly Revenue"
|
||||||
@@ -159,6 +144,7 @@ const DashboardMain = () => {
|
|||||||
color="#0F2A26"
|
color="#0F2A26"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
*/}
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
@@ -166,14 +152,12 @@ const DashboardMain = () => {
|
|||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
Quick Actions
|
Quick Actions
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
<Grid item xs={12} sm={6} md={4}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
|
||||||
cursor: 'pointer',
|
onClick={() => (window.location.href = '/admin/rooms')}
|
||||||
'&:hover': { bgcolor: 'action.hover' },
|
|
||||||
}}
|
|
||||||
onClick={() => window.location.href = '/admin/rooms'}
|
|
||||||
>
|
>
|
||||||
<CardContent sx={{ textAlign: 'center', py: 3 }}>
|
<CardContent sx={{ textAlign: 'center', py: 3 }}>
|
||||||
<HotelIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
|
<HotelIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
|
||||||
@@ -182,13 +166,10 @@ const DashboardMain = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
<Grid item xs={12} sm={6} md={4}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
|
||||||
cursor: 'pointer',
|
onClick={() => (window.location.href = '/admin/bookings')}
|
||||||
'&:hover': { bgcolor: 'action.hover' },
|
|
||||||
}}
|
|
||||||
onClick={() => window.location.href = '/admin/bookings'}
|
|
||||||
>
|
>
|
||||||
<CardContent sx={{ textAlign: 'center', py: 3 }}>
|
<CardContent sx={{ textAlign: 'center', py: 3 }}>
|
||||||
<CalendarIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
|
<CalendarIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
|
||||||
@@ -197,31 +178,13 @@ const DashboardMain = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
<Grid item xs={12} sm={6} md={4}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
|
||||||
cursor: 'pointer',
|
onClick={() => (window.location.href = '/admin/content')}
|
||||||
'&:hover': { bgcolor: 'action.hover' },
|
|
||||||
}}
|
|
||||||
onClick={() => window.location.href = '/admin/blog'}
|
|
||||||
>
|
>
|
||||||
<CardContent sx={{ textAlign: 'center', py: 3 }}>
|
<CardContent sx={{ textAlign: 'center', py: 3 }}>
|
||||||
<ArticleIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
|
<ContentIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
|
||||||
<Typography variant="h6">Write Blog Post</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
|
||||||
<Card
|
|
||||||
sx={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
'&:hover': { bgcolor: 'action.hover' },
|
|
||||||
}}
|
|
||||||
onClick={() => window.location.href = '/admin/content'}
|
|
||||||
>
|
|
||||||
<CardContent sx={{ textAlign: 'center', py: 3 }}>
|
|
||||||
<ArticleIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
|
|
||||||
<Typography variant="h6">Edit Content</Typography>
|
<Typography variant="h6">Edit Content</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -230,7 +193,6 @@ const DashboardMain = () => {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Recent Activity - Placeholder */}
|
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Paper sx={{ p: 3 }}>
|
<Paper sx={{ p: 3 }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
@@ -249,4 +211,3 @@ const DashboardMain = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default DashboardMain;
|
export default DashboardMain;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user