edit1
This commit is contained in:
@@ -33,7 +33,6 @@ import ContentManagement from './pages/admin/ContentManagement';
|
||||
import RoomManagement from './pages/admin/RoomManagement';
|
||||
import BookingManagement from './pages/admin/BookingManagement';
|
||||
import MediaManagement from './pages/admin/MediaManagement';
|
||||
import BlogManagement from './pages/admin/BlogManagement';
|
||||
import SettingsManagement from './pages/admin/SettingsManagement';
|
||||
|
||||
// Loading Component
|
||||
@@ -93,18 +92,16 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
>
|
||||
{/* NEW: redirect /admin -> /admin/dashboard */}
|
||||
<Route index element={<Navigate to="dashboard" replace />} />
|
||||
|
||||
<Route path="dashboard" element={<DashboardMain />} />
|
||||
<Route path="content" element={<ContentManagement />} />
|
||||
<Route path="rooms" element={<RoomManagement />} />
|
||||
<Route path="bookings" element={<BookingManagement />} />
|
||||
<Route path="blog" element={<BlogManagement />} />
|
||||
<Route path="media" element={<MediaManagement />} />
|
||||
<Route path="settings" element={<SettingsManagement />} />
|
||||
|
||||
{/* Optional fallback: keep if you want unknown admin routes to land on dashboard */}
|
||||
{/* أي مسار غير معروف داخل /admin يروح للداشبورد */}
|
||||
<Route path="*" element={<DashboardMain />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -22,37 +22,28 @@ import {
|
||||
import {
|
||||
Menu as MenuIcon,
|
||||
Dashboard as DashboardIcon,
|
||||
Article as ArticleIcon,
|
||||
Hotel as HotelIcon,
|
||||
CalendarMonth as CalendarIcon,
|
||||
Settings as SettingsIcon,
|
||||
Image as ImageIcon,
|
||||
ContentPaste as ContentIcon,
|
||||
People as PeopleIcon,
|
||||
Logout as LogoutIcon,
|
||||
AccountCircle as AccountIcon,
|
||||
Assessment as AssessmentIcon
|
||||
AccountCircle as AccountIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
|
||||
const drawerWidth = 260;
|
||||
|
||||
// ✅ Removed: Blog, Guests, Analytics (UI only)
|
||||
const menuItems = [
|
||||
{ title: 'Dashboard', path: '/admin/dashboard', icon: <DashboardIcon /> },
|
||||
{ title: 'Content', path: '/admin/content', icon: <ContentIcon /> },
|
||||
{ title: 'Rooms', path: '/admin/rooms', icon: <HotelIcon /> },
|
||||
{ title: 'Bookings', path: '/admin/bookings', icon: <CalendarIcon /> },
|
||||
{ title: 'Blog', path: '/admin/blog', icon: <ArticleIcon /> },
|
||||
{ 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 /> },
|
||||
];
|
||||
|
||||
// ✅ 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 theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
@@ -82,9 +73,7 @@ const AdminLayout = () => {
|
||||
|
||||
const handleNavigate = (path) => {
|
||||
navigate(path);
|
||||
if (isMobile) {
|
||||
setMobileOpen(false);
|
||||
}
|
||||
if (isMobile) setMobileOpen(false);
|
||||
};
|
||||
|
||||
const drawer = (
|
||||
@@ -106,7 +95,7 @@ const AdminLayout = () => {
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
<List>
|
||||
{adminMenuItems.map((item) => (
|
||||
{menuItems.map((item) => (
|
||||
<ListItem key={item.title} disablePadding>
|
||||
<ListItemButton
|
||||
selected={location.pathname === item.path}
|
||||
@@ -157,6 +146,7 @@ const AdminLayout = () => {
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
The Old Vine Hotel
|
||||
</Typography>
|
||||
@@ -175,14 +165,8 @@ const AdminLayout = () => {
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleProfileMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
<MenuItem disabled>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
@@ -206,38 +190,25 @@ const AdminLayout = () => {
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Box
|
||||
component="nav"
|
||||
sx={{ width: { md: drawerWidth }, flexShrink: { md: 0 } }}
|
||||
>
|
||||
{/* Mobile drawer */}
|
||||
<Box component="nav" sx={{ width: { md: drawerWidth }, flexShrink: { md: 0 } }}>
|
||||
<Drawer
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true, // Better open performance on mobile.
|
||||
}}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
sx={{
|
||||
display: { xs: 'block', md: 'none' },
|
||||
'& .MuiDrawer-paper': {
|
||||
boxSizing: 'border-box',
|
||||
width: drawerWidth,
|
||||
},
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
|
||||
{/* Desktop drawer */}
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
display: { xs: 'none', md: 'block' },
|
||||
'& .MuiDrawer-paper': {
|
||||
boxSizing: 'border-box',
|
||||
width: drawerWidth,
|
||||
},
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
||||
}}
|
||||
open
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
@@ -24,11 +24,74 @@ import {
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 { t } = useTranslation();
|
||||
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 = [
|
||||
{ label: t('nav.home'), path: '/' },
|
||||
{ label: t('nav.about'), path: '/about' },
|
||||
@@ -38,13 +101,6 @@ const Footer = () => {
|
||||
{ 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 (
|
||||
<Box
|
||||
component="footer"
|
||||
@@ -58,7 +114,6 @@ const Footer = () => {
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
<Grid container spacing={4}>
|
||||
{/* Hotel Information */}
|
||||
<Grid item xs={12} md={4}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@@ -75,19 +130,19 @@ const Footer = () => {
|
||||
color: theme.palette.secondary.main,
|
||||
}}
|
||||
>
|
||||
The Old Vine Hotel
|
||||
{hotelName}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" sx={{ mb: 3, lineHeight: 1.7 }}>
|
||||
{t('footer.description')}
|
||||
</Typography>
|
||||
|
||||
{/* Social Media Links */}
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 3 }}>
|
||||
{socialLinks.map((social) => (
|
||||
{socialLinks.map((s) => (
|
||||
<IconButton
|
||||
key={social.label}
|
||||
key={s.label}
|
||||
component="a"
|
||||
href={social.url}
|
||||
href={s.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{
|
||||
@@ -100,14 +155,13 @@ const Footer = () => {
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
>
|
||||
{social.icon}
|
||||
{s.icon}
|
||||
</IconButton>
|
||||
))}
|
||||
</Box>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
|
||||
{/* Quick Links */}
|
||||
<Grid item xs={12} sm={6} md={2}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@@ -122,31 +176,29 @@ const Footer = () => {
|
||||
>
|
||||
{t('footer.quickLinks')}
|
||||
</Typography>
|
||||
|
||||
<Box component="nav">
|
||||
{quickLinks.map((link) => (
|
||||
{quickLinks.map((l) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
key={l.path}
|
||||
component={RouterLink}
|
||||
to={link.path}
|
||||
to={l.path}
|
||||
sx={{
|
||||
display: 'block',
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
textDecoration: 'none',
|
||||
mb: 1,
|
||||
transition: 'color 0.3s ease',
|
||||
'&:hover': {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
'&:hover': { color: theme.palette.secondary.main },
|
||||
}}
|
||||
>
|
||||
{link.label}
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
</Box>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
|
||||
{/* Contact Information */}
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@@ -161,38 +213,29 @@ const Footer = () => {
|
||||
>
|
||||
{t('footer.contactInfo')}
|
||||
</Typography>
|
||||
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<LocationOn sx={{ mr: 1, fontSize: 20 }} />
|
||||
<Typography variant="body2">
|
||||
Old Damascus City
|
||||
</Typography>
|
||||
<Typography variant="body2">{address}</Typography>
|
||||
</Box>
|
||||
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Phone sx={{ mr: 1, fontSize: 20 }} />
|
||||
<Typography variant="body2">
|
||||
0112241609
|
||||
</Typography>
|
||||
<Typography variant="body2">{phone}</Typography>
|
||||
</Box>
|
||||
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Email sx={{ mr: 1, fontSize: 20 }} />
|
||||
<Typography variant="body2">
|
||||
reservations@oldvinehotel.com
|
||||
</Typography>
|
||||
<Typography variant="body2">{email}</Typography>
|
||||
</Box>
|
||||
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<WhatsApp sx={{ mr: 1, fontSize: 20 }} />
|
||||
<Typography variant="body2">
|
||||
+963986105010
|
||||
</Typography>
|
||||
<Typography variant="body2">{whatsapp}</Typography>
|
||||
</Box>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
|
||||
{/* Newsletter Signup */}
|
||||
<Grid item xs={12} md={3}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@@ -210,7 +253,7 @@ const Footer = () => {
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||
{t('footer.newsletterText')}
|
||||
</Typography>
|
||||
|
||||
|
||||
<Box component="form" sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
placeholder={t('footer.emailPlaceholder')}
|
||||
@@ -220,15 +263,9 @@ const Footer = () => {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'white',
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: theme.palette.secondary.main,
|
||||
},
|
||||
'& fieldset': { borderColor: 'rgba(255, 255, 255, 0.3)' },
|
||||
'&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.5)' },
|
||||
'&.Mui-focused fieldset': { borderColor: theme.palette.secondary.main },
|
||||
},
|
||||
'& .MuiInputBase-input::placeholder': {
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
@@ -236,13 +273,7 @@ const Footer = () => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<Button variant="contained" color="secondary" sx={{ fontWeight: 600 }}>
|
||||
{t('footer.subscribe')}
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -252,7 +283,6 @@ const Footer = () => {
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: 'rgba(255, 255, 255, 0.2)' }} />
|
||||
|
||||
{/* Bottom Section */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -263,9 +293,9 @@ const Footer = () => {
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||
<Link
|
||||
href="/privacy"
|
||||
@@ -273,9 +303,7 @@ const Footer = () => {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
textDecoration: 'none',
|
||||
fontSize: '0.875rem',
|
||||
'&:hover': {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
'&:hover': { color: theme.palette.secondary.main },
|
||||
}}
|
||||
>
|
||||
{t('footer.privacy')}
|
||||
@@ -286,9 +314,7 @@ const Footer = () => {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
textDecoration: 'none',
|
||||
fontSize: '0.875rem',
|
||||
'&:hover': {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
'&:hover': { color: theme.palette.secondary.main },
|
||||
}}
|
||||
>
|
||||
{t('footer.terms')}
|
||||
@@ -300,4 +326,4 @@ const Footer = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
export default Footer;
|
||||
|
||||
@@ -36,12 +36,6 @@ const Header = () => {
|
||||
|
||||
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 = [
|
||||
{ label: t('nav.home'), path: '/' },
|
||||
{ label: t('nav.about'), path: '/about' },
|
||||
@@ -125,12 +119,11 @@ const Header = () => {
|
||||
</ListItem>
|
||||
))}
|
||||
|
||||
{/* ✅ بدل واتساب: يروح على صفحة /booking */}
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton
|
||||
component="a"
|
||||
href={WA_LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
component={Link}
|
||||
to="/booking"
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
backgroundColor: 'secondary.main',
|
||||
@@ -235,16 +228,15 @@ const Header = () => {
|
||||
<LanguageIcon />
|
||||
</IconButton>
|
||||
|
||||
{/* ✅ بدل واتساب: يروح على صفحة /booking */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<Button
|
||||
component="a"
|
||||
href={WA_LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
component={Link}
|
||||
to="/booking"
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
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 {
|
||||
Container, Typography, Box, TextField, Button,
|
||||
MenuItem, Alert, CircularProgress
|
||||
Container,
|
||||
Typography,
|
||||
Box,
|
||||
TextField,
|
||||
Button,
|
||||
MenuItem,
|
||||
Alert,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import api from '../utils/api';
|
||||
|
||||
const Booking = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [rooms, setRooms] = useState([]);
|
||||
const [loadingRooms, setLoadingRooms] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@@ -25,35 +34,69 @@ const Booking = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const loadRooms = async () => {
|
||||
try {
|
||||
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) {
|
||||
setMessage({ type: 'error', text: 'Failed to load rooms' });
|
||||
if (mounted) setMessage({ type: 'error', text: 'Failed to load rooms' });
|
||||
} finally {
|
||||
setLoadingRooms(false);
|
||||
if (mounted) setLoadingRooms(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadRooms();
|
||||
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
|
||||
const handleChange = (key) => (e) => {
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
setMessage(null);
|
||||
|
||||
if (!validateBeforeSubmit()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
guestInfo: {
|
||||
firstName: form.firstName,
|
||||
lastName: form.lastName,
|
||||
email: form.email,
|
||||
phone: form.phone
|
||||
firstName: form.firstName.trim(),
|
||||
lastName: form.lastName.trim(),
|
||||
email: form.email.trim(),
|
||||
phone: form.phone.trim()
|
||||
},
|
||||
roomId: form.roomId,
|
||||
checkInDate: form.checkInDate,
|
||||
@@ -65,24 +108,37 @@ const Booking = () => {
|
||||
specialRequests: form.specialRequests
|
||||
};
|
||||
|
||||
// ✅ هذا لازم يكون موجود بالـ CMS: POST /api/bookings/request
|
||||
const res = await api.post('/api/bookings/request', payload);
|
||||
const bookingNumber = res?.data?.data?.booking?.bookingNumber;
|
||||
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: bookingNumber
|
||||
? `Booking request submitted! Booking #: ${bookingNumber}`
|
||||
: 'Booking request submitted successfully!'
|
||||
});
|
||||
const booking = res?.data?.data?.booking || null;
|
||||
const bookingNumber = booking?.bookingNumber || res?.data?.data?.bookingNumber || null;
|
||||
|
||||
const selectedRoom = rooms.find(r => r._id === form.roomId);
|
||||
|
||||
// ✅ معلومات نبعثها لصفحة 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) {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: error.response?.data?.message || 'Failed to submit booking request'
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
@@ -109,7 +165,11 @@ const Booking = () => {
|
||||
<CircularProgress />
|
||||
</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="Last Name" value={form.lastName} onChange={handleChange('lastName')} required />
|
||||
<TextField label="Email" type="email" value={form.email} onChange={handleChange('email')} required />
|
||||
|
||||
@@ -1,30 +1,118 @@
|
||||
import React from 'react';
|
||||
import { Container, Typography, Box } from '@mui/material';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Container, Typography, Box, Button, Paper, Divider } from '@mui/material';
|
||||
import { useLocation, Link } from 'react-router-dom';
|
||||
|
||||
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 (
|
||||
<Container maxWidth="lg" sx={{ py: 8 }}>
|
||||
<Box sx={{ textAlign: 'center', mb: 6 }}>
|
||||
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
||||
<Typography variant="h3" component="h1" gutterBottom>
|
||||
Booking Confirmation
|
||||
Booking Request Sent
|
||||
</Typography>
|
||||
<Typography variant="h5" color="text.secondary">
|
||||
Your reservation has been confirmed
|
||||
We received your request ✅
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
}}>
|
||||
<Typography variant="body1" sx={{ textAlign: 'center', maxWidth: 800 }}>
|
||||
Thank you for your booking. You will receive a confirmation email shortly.
|
||||
</Typography>
|
||||
{/* Add booking confirmation details here */}
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Paper sx={{ p: 4, width: '100%', maxWidth: 720 }}>
|
||||
<Typography variant="body1" sx={{ textAlign: 'center', mb: 3 }}>
|
||||
Thank you! Our team will contact you soon. You can also confirm faster via WhatsApp.
|
||||
</Typography>
|
||||
|
||||
{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>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookingConfirmation;
|
||||
export default BookingConfirmation;
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
Hotel as HotelIcon,
|
||||
CalendarMonth as CalendarIcon,
|
||||
People as PeopleIcon,
|
||||
Article as ArticleIcon,
|
||||
TrendingUp as TrendingUpIcon
|
||||
TrendingUp as TrendingUpIcon,
|
||||
ContentPaste as ContentIcon
|
||||
} from '@mui/icons-material';
|
||||
import api from '../../utils/api';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
@@ -73,9 +73,7 @@ const DashboardMain = () => {
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('📊 Dashboard: Fetching stats...');
|
||||
const response = await api.get('/api/admin/stats');
|
||||
console.log('📊 Dashboard: Stats received:', response.data);
|
||||
setStats(response.data.data.stats);
|
||||
} catch (err) {
|
||||
console.error('📊 Dashboard: Error fetching stats:', err);
|
||||
@@ -93,9 +91,7 @@ const DashboardMain = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Alert severity="error">{error}</Alert>;
|
||||
}
|
||||
if (error) return <Alert severity="error">{error}</Alert>;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
@@ -109,7 +105,6 @@ const DashboardMain = () => {
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* Stats Cards */}
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<StatCard
|
||||
title="Total Rooms"
|
||||
@@ -139,17 +134,7 @@ const DashboardMain = () => {
|
||||
color="#7CBF9E"
|
||||
/>
|
||||
</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}>
|
||||
<StatCard
|
||||
title="Monthly Revenue"
|
||||
@@ -159,6 +144,7 @@ const DashboardMain = () => {
|
||||
color="#0F2A26"
|
||||
/>
|
||||
</Grid>
|
||||
*/}
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Grid item xs={12}>
|
||||
@@ -166,14 +152,12 @@ const DashboardMain = () => {
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Quick Actions
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Card
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: 'action.hover' },
|
||||
}}
|
||||
onClick={() => window.location.href = '/admin/rooms'}
|
||||
sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
|
||||
onClick={() => (window.location.href = '/admin/rooms')}
|
||||
>
|
||||
<CardContent sx={{ textAlign: 'center', py: 3 }}>
|
||||
<HotelIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
|
||||
@@ -182,13 +166,10 @@ const DashboardMain = () => {
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Card
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: 'action.hover' },
|
||||
}}
|
||||
onClick={() => window.location.href = '/admin/bookings'}
|
||||
sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
|
||||
onClick={() => (window.location.href = '/admin/bookings')}
|
||||
>
|
||||
<CardContent sx={{ textAlign: 'center', py: 3 }}>
|
||||
<CalendarIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
|
||||
@@ -197,31 +178,13 @@ const DashboardMain = () => {
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Card
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: 'action.hover' },
|
||||
}}
|
||||
onClick={() => window.location.href = '/admin/blog'}
|
||||
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">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 }} />
|
||||
<ContentIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
|
||||
<Typography variant="h6">Edit Content</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -230,7 +193,6 @@ const DashboardMain = () => {
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* Recent Activity - Placeholder */}
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
@@ -249,4 +211,3 @@ const DashboardMain = () => {
|
||||
};
|
||||
|
||||
export default DashboardMain;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user