Added dark and light theme
All checks were successful
Build frontend / build (push) Successful in 56s
All checks were successful
Build frontend / build (push) Successful in 56s
Added navbar
This commit is contained in:
@ -5,8 +5,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { NavLink, MobileNavLink } from "./components/NavLinks";
|
import { NavLink, MobileNavLink } from "./components/NavLinks";
|
||||||
import { FavoritesProvider } from '@/app/contexts/FavoritesContext';
|
|
||||||
import { NotificationsProvider } from '@/app/contexts/NotificationsContext';
|
|
||||||
import BottomNav from './components/BottomNav';
|
import BottomNav from './components/BottomNav';
|
||||||
import {
|
import {
|
||||||
Globe,
|
Globe,
|
||||||
@ -35,6 +33,8 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Star,
|
Star,
|
||||||
|
Moon,
|
||||||
|
Sun,
|
||||||
FileText,
|
FileText,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
@ -43,6 +43,8 @@ import AuthService from "./services/AuthService";
|
|||||||
import { UserRole, UserRoleLabels } from "./enums/UserRole";
|
import { UserRole, UserRoleLabels } from "./enums/UserRole";
|
||||||
import "./i18n/config";
|
import "./i18n/config";
|
||||||
import NotificationHandler from "./components/NotificationHandler";
|
import NotificationHandler from "./components/NotificationHandler";
|
||||||
|
import { useTheme } from '@/app/contexts/ThemeContext';
|
||||||
|
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
||||||
|
|
||||||
export default function ClientLayout({ children }) {
|
export default function ClientLayout({ children }) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
@ -54,6 +56,8 @@ export default function ClientLayout({ children }) {
|
|||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const menuRef = useRef(null);
|
const menuRef = useRef(null);
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
const { unreadCount } = useNotifications();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMounted(true);
|
setIsMounted(true);
|
||||||
@ -575,6 +579,15 @@ export default function ClientLayout({ children }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:hidden flex items-center gap-3">
|
<div className="md:hidden flex items-center gap-3">
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors"
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? <Sun className="w-5 h-5 text-gray-700" /> : <Moon className="w-5 h-5 text-gray-700" />}
|
||||||
|
</motion.button>
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.1, rotate: 360 }}
|
whileHover={{ scale: 1.1, rotate: 360 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
@ -711,11 +724,43 @@ export default function ClientLayout({ children }) {
|
|||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<NotificationsProvider>
|
<main
|
||||||
<FavoritesProvider>
|
|
||||||
<main
|
|
||||||
className={`${!isAuthPage && !isProfilePage && !isAuthenticated ? "pt-20" : ""} flex-1 flex flex-col bg-gray-50 ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
|
className={`${!isAuthPage && !isProfilePage && !isAuthenticated ? "pt-20" : ""} flex-1 flex flex-col bg-gray-50 ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
|
||||||
>
|
>
|
||||||
|
{!isAuthPage && (
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-200 bg-gray-50/90 backdrop-blur-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`text-sm font-semibold px-3 py-1 rounded-lg ${!isAuthenticated ? 'bg-gray-200 text-gray-600' : isOwner ? 'bg-amber-100 text-amber-700' : 'bg-blue-100 text-blue-700'}`}>
|
||||||
|
{!isAuthenticated ? 'زائر' : isOwner ? 'مالك' : 'عميل'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Link href="/favorites" className="relative group flex items-center justify-center w-9 h-9 text-gray-600 hover:text-amber-600 hover:bg-gray-100 rounded-full transition-colors">
|
||||||
|
<Heart className="w-5 h-5" />
|
||||||
|
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
|
||||||
|
المفضلة
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<Link href="/notifications" className="relative group flex items-center justify-center w-9 h-9 text-gray-600 hover:text-amber-600 hover:bg-gray-100 rounded-full transition-colors">
|
||||||
|
<Bell className="w-5 h-5" />
|
||||||
|
{isMounted && unreadCount > 0 && (
|
||||||
|
<div className="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">
|
||||||
|
{unreadCount > 9 ? '9+' : unreadCount}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
|
||||||
|
الإشعارات
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<button onClick={toggleTheme} className="relative group flex items-center justify-center w-9 h-9 text-gray-600 hover:text-amber-600 hover:bg-gray-100 rounded-full transition-colors">
|
||||||
|
{theme === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
|
||||||
|
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
|
||||||
|
{theme === 'dark' ? 'وضع نهاري' : 'وضع ليلي'}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@ -723,8 +768,6 @@ export default function ClientLayout({ children }) {
|
|||||||
<BottomNav isOwner={isOwner} isOwnerOrAgent={isOwnerOrAgent} />
|
<BottomNav isOwner={isOwner} isOwnerOrAgent={isOwnerOrAgent} />
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</FavoritesProvider>
|
|
||||||
</NotificationsProvider>
|
|
||||||
|
|
||||||
{pathname === "/" && (
|
{pathname === "/" && (
|
||||||
<footer className="bg-gray-900 text-white py-12">
|
<footer className="bg-gray-900 text-white py-12">
|
||||||
|
|||||||
@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Home, Building, Calendar, Heart, Bell, Settings, CreditCard, Briefcase } from "lucide-react";
|
import { Home, Building, Calendar, Settings, CreditCard, Briefcase } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useNotifications } from "@/app/contexts/NotificationsContext";
|
|
||||||
|
|
||||||
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { unreadCount } = useNotifications();
|
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const bookingsHref = isOwner ? "/owner/reservations" : "/reservations";
|
const bookingsHref = isOwner ? "/owner/reservations" : "/reservations";
|
||||||
|
|
||||||
@ -21,9 +19,7 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
|||||||
{ href: "/properties", label: "عقاراتنا", icon: Building },
|
{ href: "/properties", label: "عقاراتنا", icon: Building },
|
||||||
...(isOwnerOrAgent ? [{ href: "/owner/properties", label: "عقاراتي", icon: Briefcase }] : []),
|
...(isOwnerOrAgent ? [{ href: "/owner/properties", label: "عقاراتي", icon: Briefcase }] : []),
|
||||||
{ href: bookingsHref, label: "الحجوزات", icon: Calendar },
|
{ href: bookingsHref, label: "الحجوزات", icon: Calendar },
|
||||||
{ href: "/favorites", label: "المفضلة", icon: Heart },
|
|
||||||
{ href: "/payments", label: "المدفوعات", icon: CreditCard },
|
{ href: "/payments", label: "المدفوعات", icon: CreditCard },
|
||||||
{ href: "/notifications", label: "الإشعارات", icon: Bell, badge: isMounted ? unreadCount : 0 },
|
|
||||||
{ href: "/settings", label: "الإعدادات", icon: Settings },
|
{ href: "/settings", label: "الإعدادات", icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -33,35 +29,34 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bg-gray-50 pb-4 pt-2">
|
<div className="flex justify-center pb-4 pt-2">
|
||||||
<div className="flex items-center justify-center gap-3">
|
<nav className="bg-gray-50 rounded-3xl px-4 py-2 shadow-[0_4px_24px_rgba(251,146,60,0.25)]">
|
||||||
{items.map((it) => {
|
<div className="flex items-center justify-center gap-1">
|
||||||
const Icon = it.icon;
|
{items.map((it) => {
|
||||||
const active = isActive(it.href);
|
const Icon = it.icon;
|
||||||
return (
|
const active = isActive(it.href);
|
||||||
<Link
|
return (
|
||||||
key={it.href}
|
<Link
|
||||||
href={it.href}
|
key={it.href}
|
||||||
className={`relative group flex flex-col items-center justify-center px-2.5 py-2 transition-colors ${active ? "text-amber-600" : "text-gray-700 hover:text-amber-600"}`}
|
href={it.href}
|
||||||
>
|
className={`relative flex flex-col items-center justify-center px-3 py-2 rounded-xl transition-all duration-200 ease-out hover:shadow-md hover:bg-white/80 ${active ? "bg-white shadow-sm text-amber-600" : "text-gray-700 hover:text-amber-600"}`}
|
||||||
<div className="relative">
|
>
|
||||||
<Icon className="w-6 h-6" />
|
<div className="relative">
|
||||||
{it.badge > 0 && (
|
<Icon className="w-6 h-6" />
|
||||||
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">
|
{it.badge > 0 && (
|
||||||
{it.badge > 9 ? "9+" : it.badge}
|
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">
|
||||||
</div>
|
{it.badge > 9 ? "9+" : it.badge}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="absolute -top-10 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2.5 py-1.5 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
|
</div>
|
||||||
{it.label}
|
<div className="mt-1 text-xs text-center" aria-hidden>
|
||||||
</div>
|
{it.label}
|
||||||
<div className="mt-1 text-xs text-center" aria-hidden>
|
</div>
|
||||||
{it.label}
|
</Link>
|
||||||
</div>
|
);
|
||||||
</Link>
|
})}
|
||||||
);
|
</div>
|
||||||
})}
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
32
app/contexts/ThemeContext.js
Normal file
32
app/contexts/ThemeContext.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext, useState, useEffect } from "react";
|
||||||
|
|
||||||
|
const ThemeContext = createContext();
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }) {
|
||||||
|
const [theme, setTheme] = useState("light");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem("theme");
|
||||||
|
if (stored === "dark" || (!stored && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
||||||
|
setTheme("dark");
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
const next = theme === "light" ? "dark" : "light";
|
||||||
|
setTheme(next);
|
||||||
|
document.documentElement.classList.toggle("dark", next === "dark");
|
||||||
|
localStorage.setItem("theme", next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTheme = () => useContext(ThemeContext);
|
||||||
@ -1,5 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
/* ─── Madani Arabic Font ─── */
|
/* ─── Madani Arabic Font ─── */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Madani Arabic';
|
font-family: 'Madani Arabic';
|
||||||
@ -92,6 +94,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: #0f172a;
|
||||||
|
--foreground: #e2e8f0;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--color-white: #1e293b;
|
||||||
|
--color-gray-50: #0f172a;
|
||||||
|
--color-gray-100: #1e293b;
|
||||||
|
--color-gray-200: #334155;
|
||||||
|
--color-gray-300: #475569;
|
||||||
|
--color-gray-400: #94a3b8;
|
||||||
|
--color-gray-500: #64748b;
|
||||||
|
--color-gray-600: #475569;
|
||||||
|
--color-gray-700: #cbd5e1;
|
||||||
|
--color-gray-800: #e2e8f0;
|
||||||
|
--color-gray-900: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
font-family: 'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif;
|
font-family: 'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import ClientLayout from "./ClientLayout";
|
import ClientLayout from "./ClientLayout";
|
||||||
|
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||||
|
import { NotificationsProvider } from "./contexts/NotificationsContext";
|
||||||
|
import { FavoritesProvider } from "./contexts/FavoritesContext";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@ -52,7 +55,13 @@ export default function Layout({ children }) {
|
|||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
style={{ fontFamily: "'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif" }}
|
style={{ fontFamily: "'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif" }}
|
||||||
>
|
>
|
||||||
<ClientLayout>{children}</ClientLayout>
|
<ThemeProvider>
|
||||||
|
<NotificationsProvider>
|
||||||
|
<FavoritesProvider>
|
||||||
|
<ClientLayout>{children}</ClientLayout>
|
||||||
|
</FavoritesProvider>
|
||||||
|
</NotificationsProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1221,15 +1221,14 @@ const handleMarkerDragEnd = async (lat, lng) => {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
العملة <span className="text-red-500">*</span>
|
العملة <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedCurrencyId}
|
value={selectedCurrencyId}
|
||||||
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
|
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
>
|
>
|
||||||
{Object.entries(CurrencyLabels).map(([id, label]) => (
|
<option value={Currency.USD}>دولار امريكي $</option>
|
||||||
<option key={id} value={id}>{label}</option>
|
<option value={Currency.SYP}>عملة سورية SP</option>
|
||||||
))}
|
</select>
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user