Added dark and light theme
All checks were successful
Build frontend / build (push) Successful in 56s

Added navbar
This commit is contained in:
Rahaf
2026-06-28 16:08:46 +03:00
parent df5801ee25
commit 4f58519bac
6 changed files with 152 additions and 52 deletions

View File

@ -5,8 +5,6 @@ import { useTranslation } from "react-i18next";
import Link from "next/link";
import Image from "next/image";
import { NavLink, MobileNavLink } from "./components/NavLinks";
import { FavoritesProvider } from '@/app/contexts/FavoritesContext';
import { NotificationsProvider } from '@/app/contexts/NotificationsContext';
import BottomNav from './components/BottomNav';
import {
Globe,
@ -35,6 +33,8 @@ import {
Clock,
DollarSign,
Star,
Moon,
Sun,
FileText,
} from "lucide-react";
import { useState, useEffect, useRef } from "react";
@ -43,6 +43,8 @@ import AuthService from "./services/AuthService";
import { UserRole, UserRoleLabels } from "./enums/UserRole";
import "./i18n/config";
import NotificationHandler from "./components/NotificationHandler";
import { useTheme } from '@/app/contexts/ThemeContext';
import { useNotifications } from '@/app/contexts/NotificationsContext';
export default function ClientLayout({ children }) {
const { t, i18n } = useTranslation();
@ -54,6 +56,8 @@ export default function ClientLayout({ children }) {
const [isMounted, setIsMounted] = useState(false);
const currentYear = new Date().getFullYear();
const menuRef = useRef(null);
const { theme, toggleTheme } = useTheme();
const { unreadCount } = useNotifications();
useEffect(() => {
setIsMounted(true);
@ -575,6 +579,15 @@ export default function ClientLayout({ children }) {
</div>
<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
whileHover={{ scale: 1.1, rotate: 360 }}
whileTap={{ scale: 0.9 }}
@ -711,11 +724,43 @@ export default function ClientLayout({ children }) {
</nav>
)}
<NotificationsProvider>
<FavoritesProvider>
<main
<main
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">
{children}
</div>
@ -723,8 +768,6 @@ export default function ClientLayout({ children }) {
<BottomNav isOwner={isOwner} isOwnerOrAgent={isOwnerOrAgent} />
)}
</main>
</FavoritesProvider>
</NotificationsProvider>
{pathname === "/" && (
<footer className="bg-gray-900 text-white py-12">

View File

@ -2,13 +2,11 @@
import Link from "next/link";
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 { useNotifications } from "@/app/contexts/NotificationsContext";
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
const pathname = usePathname();
const { unreadCount } = useNotifications();
const [isMounted, setIsMounted] = useState(false);
const bookingsHref = isOwner ? "/owner/reservations" : "/reservations";
@ -21,9 +19,7 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
{ href: "/properties", label: "عقاراتنا", icon: Building },
...(isOwnerOrAgent ? [{ href: "/owner/properties", label: "عقاراتي", icon: Briefcase }] : []),
{ href: bookingsHref, label: "الحجوزات", icon: Calendar },
{ href: "/favorites", label: "المفضلة", icon: Heart },
{ href: "/payments", label: "المدفوعات", icon: CreditCard },
{ href: "/notifications", label: "الإشعارات", icon: Bell, badge: isMounted ? unreadCount : 0 },
{ href: "/settings", label: "الإعدادات", icon: Settings },
];
@ -33,35 +29,34 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
};
return (
<nav className="bg-gray-50 pb-4 pt-2">
<div className="flex items-center justify-center gap-3">
{items.map((it) => {
const Icon = it.icon;
const active = isActive(it.href);
return (
<Link
key={it.href}
href={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"}`}
>
<div className="relative">
<Icon className="w-6 h-6" />
{it.badge > 0 && (
<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 > 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">
{it.label}
</div>
<div className="mt-1 text-xs text-center" aria-hidden>
{it.label}
</div>
</Link>
);
})}
</div>
</nav>
<div className="flex justify-center pb-4 pt-2">
<nav className="bg-gray-50 rounded-3xl px-4 py-2 shadow-[0_4px_24px_rgba(251,146,60,0.25)]">
<div className="flex items-center justify-center gap-1">
{items.map((it) => {
const Icon = it.icon;
const active = isActive(it.href);
return (
<Link
key={it.href}
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" />
{it.badge > 0 && (
<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 > 9 ? "9+" : it.badge}
</div>
)}
</div>
<div className="mt-1 text-xs text-center" aria-hidden>
{it.label}
</div>
</Link>
);
})}
</div>
</nav>
</div>
);
}

View 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);

View File

@ -1,5 +1,7 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
/* ─── Madani Arabic Font ─── */
@font-face {
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 {
font-family: 'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif;
}

View File

@ -1,6 +1,9 @@
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import ClientLayout from "./ClientLayout";
import { ThemeProvider } from "./contexts/ThemeContext";
import { NotificationsProvider } from "./contexts/NotificationsContext";
import { FavoritesProvider } from "./contexts/FavoritesContext";
const geistSans = Geist({
variable: "--font-geist-sans",
@ -52,7 +55,13 @@ export default function Layout({ children }) {
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
style={{ fontFamily: "'Madani Arabic', 'Noto Sans Arabic', 'Cairo', Arial, sans-serif" }}
>
<ClientLayout>{children}</ClientLayout>
<ThemeProvider>
<NotificationsProvider>
<FavoritesProvider>
<ClientLayout>{children}</ClientLayout>
</FavoritesProvider>
</NotificationsProvider>
</ThemeProvider>
</body>
</html>
);

View File

@ -1221,15 +1221,14 @@ const handleMarkerDragEnd = async (lat, lng) => {
<label className="block text-sm font-medium text-gray-700 mb-2">
العملة <span className="text-red-500">*</span>
</label>
<select
value={selectedCurrencyId}
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"
>
{Object.entries(CurrencyLabels).map(([id, label]) => (
<option key={id} value={id}>{label}</option>
))}
</select>
<select
value={selectedCurrencyId}
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"
>
<option value={Currency.USD}>دولار امريكي $</option>
<option value={Currency.SYP}>عملة سورية SP</option>
</select>
</div>
</motion.div>
)}