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 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
|
||||
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">
|
||||
|
||||
@ -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,8 +29,9 @@ 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">
|
||||
<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);
|
||||
@ -42,7 +39,7 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
||||
<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"}`}
|
||||
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" />
|
||||
@ -52,9 +49,6 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
||||
</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>
|
||||
@ -63,5 +57,6 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@ -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" }}
|
||||
>
|
||||
<ThemeProvider>
|
||||
<NotificationsProvider>
|
||||
<FavoritesProvider>
|
||||
<ClientLayout>{children}</ClientLayout>
|
||||
</FavoritesProvider>
|
||||
</NotificationsProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -1226,9 +1226,8 @@ const handleMarkerDragEnd = async (lat, lng) => {
|
||||
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>
|
||||
))}
|
||||
<option value={Currency.USD}>دولار امريكي $</option>
|
||||
<option value={Currency.SYP}>عملة سورية SP</option>
|
||||
</select>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user