From 6a644b9a4ec57ffa2e25b8f875e410df08d37735 Mon Sep 17 00:00:00 2001 From: AminGhasempoor Date: Wed, 24 Dec 2025 11:49:17 +0330 Subject: [PATCH] theme provider --- package.json | 1 + pnpm-lock.yaml | 12 ++ public/icons/avatar-icon.svg | 4 + public/icons/check-icon.svg | 3 + public/icons/chevron-icon.svg | 3 + public/icons/close-icon.svg | 8 ++ public/icons/dark-mode-icon.svg | 3 + public/icons/google-maps.svg | 40 ++++++ public/icons/light-mode-icon.svg | 3 + public/icons/logout.svg | 4 + src/app/favicon.ico | Bin 2370 -> 4654 bytes src/app/layout.tsx | 23 ++- src/assets/index.ts | 6 + src/components/Modals/LogoutModal.tsx | 39 ++++++ src/components/Modals/MapLauncherModal.tsx | 104 ++++++++++++++ .../Modals/RedirectToSignInModal.tsx | 36 +++++ .../SideBar/SideBarContent/MenuItem.tsx | 33 +++++ .../SideBar/SideBarContent/SubMenu.tsx | 42 ++++++ .../SideBar/SideBarContent/index.tsx | 51 +++++++ src/components/SideBar/index.tsx | 131 ++++++++++++++++++ src/components/ThemeToggle/index.tsx | 39 ++++++ src/providers/AppShell.tsx | 13 ++ src/providers/ClientAppProvider.tsx | 35 ++--- src/stores/SidebarStore.ts | 13 ++ src/stores/useMapLayersStore.ts | 21 +++ tsconfig.json | 3 +- 26 files changed, 637 insertions(+), 33 deletions(-) create mode 100644 public/icons/avatar-icon.svg create mode 100644 public/icons/check-icon.svg create mode 100644 public/icons/chevron-icon.svg create mode 100644 public/icons/close-icon.svg create mode 100644 public/icons/dark-mode-icon.svg create mode 100644 public/icons/google-maps.svg create mode 100644 public/icons/light-mode-icon.svg create mode 100644 public/icons/logout.svg create mode 100644 src/components/Modals/LogoutModal.tsx create mode 100644 src/components/Modals/MapLauncherModal.tsx create mode 100644 src/components/Modals/RedirectToSignInModal.tsx create mode 100644 src/components/SideBar/SideBarContent/MenuItem.tsx create mode 100644 src/components/SideBar/SideBarContent/SubMenu.tsx create mode 100644 src/components/SideBar/SideBarContent/index.tsx create mode 100644 src/components/SideBar/index.tsx create mode 100644 src/components/ThemeToggle/index.tsx create mode 100644 src/providers/AppShell.tsx create mode 100644 src/stores/SidebarStore.ts create mode 100644 src/stores/useMapLayersStore.ts diff --git a/package.json b/package.json index 155a33f..c4689a0 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "axios": "^1.10.0", "clsx": "^2.1.1", "framer-motion": "^12.9.2", + "js-cookie": "^3.0.5", "maplibre-gl": "^5.4.0", "next": "16.1.1", "next-intl": "^4.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 385d6dc..d51e7b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,9 @@ importers: framer-motion: specifier: ^12.9.2 version: 12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 maplibre-gl: specifier: ^5.4.0 version: 5.15.0 @@ -4948,6 +4951,13 @@ packages: } hasBin: true + js-cookie@3.0.5: + resolution: + { + integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==, + } + engines: { node: ">=14" } + js-tokens@4.0.0: resolution: { @@ -10352,6 +10362,8 @@ snapshots: jiti@2.6.1: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: diff --git a/public/icons/avatar-icon.svg b/public/icons/avatar-icon.svg new file mode 100644 index 0000000..c0b550e --- /dev/null +++ b/public/icons/avatar-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/check-icon.svg b/public/icons/check-icon.svg new file mode 100644 index 0000000..e9dc75a --- /dev/null +++ b/public/icons/check-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/chevron-icon.svg b/public/icons/chevron-icon.svg new file mode 100644 index 0000000..7914948 --- /dev/null +++ b/public/icons/chevron-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/close-icon.svg b/public/icons/close-icon.svg new file mode 100644 index 0000000..475466c --- /dev/null +++ b/public/icons/close-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/dark-mode-icon.svg b/public/icons/dark-mode-icon.svg new file mode 100644 index 0000000..bcab3f0 --- /dev/null +++ b/public/icons/dark-mode-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/google-maps.svg b/public/icons/google-maps.svg new file mode 100644 index 0000000..492fe52 --- /dev/null +++ b/public/icons/google-maps.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/icons/light-mode-icon.svg b/public/icons/light-mode-icon.svg new file mode 100644 index 0000000..aef07df --- /dev/null +++ b/public/icons/light-mode-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/logout.svg b/public/icons/logout.svg new file mode 100644 index 0000000..dc10883 --- /dev/null +++ b/public/icons/logout.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/app/favicon.ico b/src/app/favicon.ico index 4de7d126720df44457a245bc026063b1171df920..5fb9cfc28a49688b52e3154530f1d286e67e62eb 100644 GIT binary patch delta 4646 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 { +export default async function RootLayout({ children }: { children: React.ReactNode }) { const locale = await getLocale(); const messages = await getMessages({ locale }); return ( @@ -39,7 +42,15 @@ export default async function RootLayout({ - {children} + + + + {children} + + + + + diff --git a/src/assets/index.ts b/src/assets/index.ts index 0171d4c..9afd110 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -1,2 +1,8 @@ export { default as CheckIcon } from "&/icons/check-icon.svg"; export { default as ChevronIcon } from "&/icons/chevron-icon.svg"; +export { default as GoogleMapsIcon } from "&/icons/google-maps.svg"; +export { default as CloseIcon } from "&/icons/close-icon.svg"; +export { default as LogoutIcon } from "&/icons/logout.svg"; +export { default as AvatarIcon } from "&/icons/avatar-icon.svg"; +export { default as DarkModeIcon } from "&/icons/dark-mode-icon.svg"; +export { default as LightModeIcon } from "&/icons/light-mode-icon.svg"; diff --git a/src/components/Modals/LogoutModal.tsx b/src/components/Modals/LogoutModal.tsx new file mode 100644 index 0000000..50b9abb --- /dev/null +++ b/src/components/Modals/LogoutModal.tsx @@ -0,0 +1,39 @@ +import { useAuth } from "@/hooks/useAuth"; +import { useModalStore } from "@/stores/useModalStore"; +import { useTranslations } from "next-intl"; + +const LogoutModal = () => { + const t = useTranslations("logout"); + const closeModal = useModalStore((s) => s.closeModal); + const { logout } = useAuth(); + + return ( +
+
+

{t("logout")}

+

{t("confirmLogout")}

+
+ +
+ + +
+
+ ); +}; + +export default LogoutModal; diff --git a/src/components/Modals/MapLauncherModal.tsx b/src/components/Modals/MapLauncherModal.tsx new file mode 100644 index 0000000..3a68aa0 --- /dev/null +++ b/src/components/Modals/MapLauncherModal.tsx @@ -0,0 +1,104 @@ +import neshanLogo from "&/icons/neshan-logo.png"; +import logo from "&/logo/144px.png"; +import { CloseIcon, GoogleMapsIcon } from "@/assets"; +import { useLocationStore } from "@/stores/LocationStore"; +import { useModalStore } from "@/stores/useModalStore"; +import Image from "next/image"; +import { JSX } from "react"; + +type MapLauncherItem = { + id: string; + icon: JSX.Element; + label: string; + onClick: () => void; +}; + +export default function MapLauncherModal() { + const destination = useLocationStore((s) => s.destination); + const origin = useLocationStore((s) => s.origin); + const closeModal = useModalStore((s) => s.closeModal); + const openModal = useModalStore((s) => s.openModal); + + function openGoogleMaps() { + if (!destination) return; + const originParam = origin ? `&origin=${origin.lat},${origin.lng}` : ""; + const url = `https://www.google.com/maps/dir/?api=1${originParam}&destination=${destination.lat},${destination.lng}`; + window.location.href = url; + } + + function open141Map() { + openModal( +
+
+
+

+ به زودی می‌توانید از اپلیکیشن 141 برای مسیریابی استفاده کنید. +

+
+ +
+ +
+
+
+ ); + } + + function openNeshanMap() { + if (!destination) return; + + const url = `https://nshn.ir/?lat=${destination.lat}&lng=${destination.lng}`; + + window.location.href = url; + } + + const launchers: MapLauncherItem[] = [ + { + id: "google-maps", + icon: , + label: "Google Maps", + onClick: openGoogleMaps, + }, + { + id: "141", + icon: 141 Map, + label: "141", + onClick: open141Map, + }, + { + id: "neshan", + icon: neshan Map, + label: "نشان", + onClick: openNeshanMap, + }, + ]; + + return ( +
+
+ +
+ +
+ {launchers.map((item) => ( + + ))} +
+
+ ); +} diff --git a/src/components/Modals/RedirectToSignInModal.tsx b/src/components/Modals/RedirectToSignInModal.tsx new file mode 100644 index 0000000..02e2fb2 --- /dev/null +++ b/src/components/Modals/RedirectToSignInModal.tsx @@ -0,0 +1,36 @@ +import { useModalStore } from "@/stores/useModalStore"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; + +export default function RedirectToSignInModal({ confirmLabel = "ورود" }) { + const t = useTranslations("Bookmarks"); + const closeModal = useModalStore((s) => s.closeModal); + const router = useRouter(); + + return ( +
+
+

{t("signInModalTitle")}

+

{t("signInModalDescription")}

+
+ +
+ + +
+
+ ); +} diff --git a/src/components/SideBar/SideBarContent/MenuItem.tsx b/src/components/SideBar/SideBarContent/MenuItem.tsx new file mode 100644 index 0000000..a538552 --- /dev/null +++ b/src/components/SideBar/SideBarContent/MenuItem.tsx @@ -0,0 +1,33 @@ +import { ReactNode } from "react"; +import Link from "next/link"; +import { useSidebarStore } from "@/stores/SidebarStore"; + +interface MenuItemProps { + label: string; + onClick?: () => void; + icon?: ReactNode; + href?: string; +} + +export default function MenuItem({ label, onClick, icon, href }: MenuItemProps) { + const close = useSidebarStore((state) => state.close); + + function handleClick() { + onClick?.(); + close(); + } + + return ( + + + {icon} +

{label}

+
+ + ); +} diff --git a/src/components/SideBar/SideBarContent/SubMenu.tsx b/src/components/SideBar/SideBarContent/SubMenu.tsx new file mode 100644 index 0000000..73f8cc5 --- /dev/null +++ b/src/components/SideBar/SideBarContent/SubMenu.tsx @@ -0,0 +1,42 @@ +import { ChevronIcon } from "@/assets"; +import { AnimatePresence, motion } from "framer-motion"; +import { ReactNode, useState } from "react"; + +interface SubMenuProps { + label: string; + children: ReactNode; +} + +export default function SubMenu({ label, children }: SubMenuProps) { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + + {isOpen && ( + + {children} + + )} + +
+ ); +} diff --git a/src/components/SideBar/SideBarContent/index.tsx b/src/components/SideBar/SideBarContent/index.tsx new file mode 100644 index 0000000..4a086dc --- /dev/null +++ b/src/components/SideBar/SideBarContent/index.tsx @@ -0,0 +1,51 @@ +import SubMenu from "./SubMenu"; +import MenuItem from "./MenuItem"; +import Separator from "@/components/UI/Separator"; +import { motion, AnimatePresence } from "framer-motion"; + +type Menu = { + title: string; + href?: string; + children?: Menu[]; +}; + +interface MenuRendererProps { + items: Menu[]; + level?: number; +} + +export function SideBarComponent({ items, level = 0 }: MenuRendererProps) { + return ( +
+ {items.map((item, index) => ( + + {item.children ? ( + <> + + + + + + + + + ) : ( + + )} + {level === 0 && } + + ))} +
+ ); +} diff --git a/src/components/SideBar/index.tsx b/src/components/SideBar/index.tsx new file mode 100644 index 0000000..1c8d939 --- /dev/null +++ b/src/components/SideBar/index.tsx @@ -0,0 +1,131 @@ +"use client"; +import { AvatarIcon, CloseIcon, LogoutIcon } from "@/assets"; +import { SideBarComponent } from "./SideBarContent"; +import ThemeToggle from "@/components/ThemeToggle"; +import { mobileMenuItems } from "@/data/sidebarMenu"; +import { useSidebarStore } from "@/stores/SidebarStore"; +import useUserStore from "@/stores/userStore"; +import { AnimatePresence, motion } from "framer-motion"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useEffect } from "react"; +import { useModalStore } from "@/stores/useModalStore"; +import LogoutModal from "@/components/Modals/LogoutModal"; + +export const Sidebar = () => { + const t = useTranslations("Sidebar"); + const ta = useTranslations("Auth"); + const isOpen = useSidebarStore((s) => s.isOpen); + const isAuth = useUserStore((s) => s.isAuth); + const close = useSidebarStore((state) => state.close); + const openModal = useModalStore((s) => s.openModal); + const pathName = usePathname(); + + function handleClick() { + close(); + } + + useEffect(() => { + return () => close(); + }, []); + + return ( + + {isOpen && ( + +
+ e.stopPropagation()} + className="bg-desktop-primary text-text flex h-full w-full max-w-md flex-col rounded-2xl p-4 shadow-lg" + > + {/* Header */} + + + + + + {/* Nav Items */} + + + {/* Footer */} + +
+ + {t("policy")} +
+
+
+
+ )} +
+ ); +}; diff --git a/src/components/ThemeToggle/index.tsx b/src/components/ThemeToggle/index.tsx new file mode 100644 index 0000000..b393f68 --- /dev/null +++ b/src/components/ThemeToggle/index.tsx @@ -0,0 +1,39 @@ +import { DarkModeIcon, LightModeIcon } from "@/assets"; +import { useMapLayersStore } from "@/stores/useMapLayersStore"; +import { AnimatePresence, motion } from "framer-motion"; +import { useTheme } from "next-themes"; + +export default function ThemeToggle() { + const { theme, setTheme } = useTheme(); + const setActiveLayer = useMapLayersStore((s) => s.setActiveLayer); + + function handleThemeToggle() { + if (theme === "dark") { + setActiveLayer("bright"); + setTheme("light"); + } else { + setActiveLayer("dark"); + setTheme("dark"); + } + } + + return ( + + + + ); +} diff --git a/src/providers/AppShell.tsx b/src/providers/AppShell.tsx new file mode 100644 index 0000000..951c74f --- /dev/null +++ b/src/providers/AppShell.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { AnimatePresence, motion } from "framer-motion"; + +export function AppShell({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/src/providers/ClientAppProvider.tsx b/src/providers/ClientAppProvider.tsx index 62096d0..17c1317 100644 --- a/src/providers/ClientAppProvider.tsx +++ b/src/providers/ClientAppProvider.tsx @@ -1,40 +1,23 @@ "use client"; -import { AnimatePresence, motion } from "framer-motion"; import { ThemeProvider } from "next-themes"; -import { useEffect, useState } from "react"; +import { useEffect, useEffectEvent, useState } from "react"; export function ClientAppProvider({ children }: { children: React.ReactNode }) { const [mounted, setMounted] = useState(false); - - // wait for hydration - useEffect(() => { + const syncDevice = useEffectEvent(() => { setMounted(true); + }); + + useEffect(() => { + syncDevice(); }, []); + if (!mounted) return null; + return ( - - {!mounted ? ( - - ) : ( - - {children} - - )} - + {children} ); } diff --git a/src/stores/SidebarStore.ts b/src/stores/SidebarStore.ts new file mode 100644 index 0000000..ab8545c --- /dev/null +++ b/src/stores/SidebarStore.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +type SidebarState = { + isOpen: boolean; + toggle: () => void; + close: () => void; +}; + +export const useSidebarStore = create((set) => ({ + isOpen: false, + toggle: () => set((state) => ({ isOpen: !state.isOpen })), + close: () => set({ isOpen: false }), +})); diff --git a/src/stores/useMapLayersStore.ts b/src/stores/useMapLayersStore.ts new file mode 100644 index 0000000..efe16ea --- /dev/null +++ b/src/stores/useMapLayersStore.ts @@ -0,0 +1,21 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +export type LayerType = "dark" | "bright"; + +type MapLayersStore = { + activeLayer: LayerType; + setActiveLayer: (layer: LayerType) => void; +}; + +export const useMapLayersStore = create()( + persist( + (set) => ({ + activeLayer: "dark", + setActiveLayer: (layer) => set({ activeLayer: layer }), + }), + { + name: "map-theme", // نام کلید در localStorage + } + ) +); diff --git a/tsconfig.json b/tsconfig.json index 516d777..81a75ed 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "&/*": ["./public/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts"],