theme provider
This commit is contained in:
39
src/components/Modals/LogoutModal.tsx
Normal file
39
src/components/Modals/LogoutModal.tsx
Normal file
@@ -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 (
|
||||
<div className="bg-desktop-primary text-text flex flex-col gap-4 rounded-xl p-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-base font-semibold">{t("logout")}</h2>
|
||||
<p className="text-text/75 max-w-3xs text-sm">{t("confirmLogout")}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="border-lines/40 hover:bg-lines/10 flex-1 rounded-full border px-3 py-1 text-sm"
|
||||
>
|
||||
{t("cancel")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
logout().then(() => {
|
||||
closeModal();
|
||||
});
|
||||
}}
|
||||
className="bg-error hover:bg-error/75 flex-1 rounded-full px-3 py-1 text-sm text-white"
|
||||
>
|
||||
{t("logout")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogoutModal;
|
||||
104
src/components/Modals/MapLauncherModal.tsx
Normal file
104
src/components/Modals/MapLauncherModal.tsx
Normal file
@@ -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(
|
||||
<div>
|
||||
<div className="bg-desktop-primary text-text flex flex-col gap-4 rounded-xl p-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-text/75 max-w-3xs text-sm">
|
||||
به زودی میتوانید از اپلیکیشن 141 برای مسیریابی استفاده کنید.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="border-lines/40 hover:bg-lines/10 flex-1 rounded-full border px-3 py-1 text-sm"
|
||||
>
|
||||
متوجه شدم
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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: <GoogleMapsIcon className="size-10" />,
|
||||
label: "Google Maps",
|
||||
onClick: openGoogleMaps,
|
||||
},
|
||||
{
|
||||
id: "141",
|
||||
icon: <Image src={logo} alt="141 Map" className="size-10" />,
|
||||
label: "141",
|
||||
onClick: open141Map,
|
||||
},
|
||||
{
|
||||
id: "neshan",
|
||||
icon: <Image src={neshanLogo} alt="neshan Map" className="size-10" />,
|
||||
label: "نشان",
|
||||
onClick: openNeshanMap,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-pwa-primary text-text w-full max-w-100 min-w-81 rounded-2xl">
|
||||
<div className="border-lines/20 flex items-center justify-between border-b px-4 py-3">
|
||||
<button onClick={closeModal} aria-label="Close">
|
||||
<CloseIcon className="text-text/80 size-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 p-4 py-6">
|
||||
{launchers.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={item.onClick}
|
||||
className="group border-lines/20 bg-buttons/50 flex flex-col items-center justify-center gap-2 rounded-xl border p-3 shadow-sm transition hover:shadow-md active:scale-[0.97]"
|
||||
>
|
||||
<div className="flex items-center justify-center">{item.icon}</div>
|
||||
|
||||
<span className="text-text/80 text-xs font-medium">{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
src/components/Modals/RedirectToSignInModal.tsx
Normal file
36
src/components/Modals/RedirectToSignInModal.tsx
Normal file
@@ -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 (
|
||||
<div className="bg-desktop-primary text-text flex flex-col gap-4 rounded-xl p-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-base font-semibold">{t("signInModalTitle")}</h2>
|
||||
<p className="text-text/75 max-w-3xs text-sm">{t("signInModalDescription")}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="border-lines/40 hover:bg-lines/10 flex-1 rounded-full border px-3 py-1 text-sm"
|
||||
>
|
||||
{t("abort")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
closeModal();
|
||||
router.push("/auth");
|
||||
}}
|
||||
className="bg-neo-aqua hover:bg-neo-aqua/75 flex-1 rounded-full px-3 py-1 text-sm text-white"
|
||||
>
|
||||
{t("signin")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
src/components/SideBar/SideBarContent/MenuItem.tsx
Normal file
33
src/components/SideBar/SideBarContent/MenuItem.tsx
Normal file
@@ -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 (
|
||||
<Link
|
||||
href={href || "#"}
|
||||
prefetch={href !== "/complaints"}
|
||||
onClick={handleClick}
|
||||
className="hover:bg-desktop-primary/25 flex w-full cursor-pointer items-center justify-between rounded-md px-2 py-2 text-sm font-bold"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{icon}
|
||||
<p className="text-text line-clamp-1">{label}</p>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
42
src/components/SideBar/SideBarContent/SubMenu.tsx
Normal file
42
src/components/SideBar/SideBarContent/SubMenu.tsx
Normal file
@@ -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 (
|
||||
<div className="w-full">
|
||||
<button
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
className="flex w-full cursor-pointer items-center justify-between rounded-md px-2 py-2 text-sm font-bold"
|
||||
>
|
||||
<span className="text-text line-clamp-1">{label}</span>
|
||||
<motion.div
|
||||
animate={{ rotate: isOpen ? 180 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="bg-lines/25 rounded-lg p-1"
|
||||
>
|
||||
<ChevronIcon className="text-text size-4 rotate-270 opacity-50" />
|
||||
</motion.div>
|
||||
</button>
|
||||
<AnimatePresence initial={false}>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="pr-3 pl-6"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
src/components/SideBar/SideBarContent/index.tsx
Normal file
51
src/components/SideBar/SideBarContent/index.tsx
Normal file
@@ -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 (
|
||||
<div className="z-50 mt-2 flex flex-col gap-1">
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.title + index}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
className="mb-3"
|
||||
>
|
||||
{item.children ? (
|
||||
<>
|
||||
<AnimatePresence initial={false}>
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<SubMenu label={item.title}>
|
||||
<SideBarComponent items={item.children} level={level + 1} />
|
||||
</SubMenu>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</>
|
||||
) : (
|
||||
<MenuItem href={item.href} label={item.title} />
|
||||
)}
|
||||
{level === 0 && <Separator className="mt-2" />}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
131
src/components/SideBar/index.tsx
Normal file
131
src/components/SideBar/index.tsx
Normal file
@@ -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 (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="bg-desktop-primary/5 fixed inset-0 z-50 backdrop-blur-xl"
|
||||
onClick={close}
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center justify-center p-4">
|
||||
<motion.aside
|
||||
initial={{ x: "100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "100%" }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||
onClick={(e) => 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 */}
|
||||
<motion.div
|
||||
key="close"
|
||||
initial={{ opacity: 0, rotate: -45 }}
|
||||
animate={{ opacity: 1, rotate: 0 }}
|
||||
exit={{ opacity: 0, rotate: 45 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
className={"mb-4 flex justify-between px-2"}
|
||||
>
|
||||
<button onClick={close}>
|
||||
<CloseIcon className="text-text size-6" />
|
||||
</button>
|
||||
<ThemeToggle />
|
||||
</motion.div>
|
||||
|
||||
{/* Nav Items */}
|
||||
<nav className="overflow-y-auto">
|
||||
{isAuth ? (
|
||||
<div className="flex w-full flex-col justify-center gap-2 py-6">
|
||||
<Link
|
||||
onClick={handleClick}
|
||||
href="/panel/profile"
|
||||
className="bg-neo-aqua flex flex-row items-center justify-center gap-2 rounded-lg px-2 py-1 transition-all duration-200 hover:scale-101 active:scale-99"
|
||||
>
|
||||
<AvatarIcon className="size-4 text-white" />
|
||||
<div className="h-4 w-[1.5px] bg-white" />
|
||||
<p className="text-white">{ta("profile")}</p>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
openModal(<LogoutModal />);
|
||||
}}
|
||||
className="bg-error flex flex-row items-center justify-center gap-2 rounded-lg px-2 py-1 transition-all duration-200 hover:scale-101 active:scale-99"
|
||||
>
|
||||
<LogoutIcon className="size-5 rotate-180 text-white" />
|
||||
<p className="text-white">{ta("logout")}</p>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 py-6">
|
||||
<button className="bg-neo-aqua rounded-lg p-2">
|
||||
<AvatarIcon className="size-4 text-white" />
|
||||
</button>
|
||||
<div className="bg-text hover:text-pwa-primary rounded-lg px-3 py-1 transition-all duration-300 hover:scale-101 active:scale-99">
|
||||
<Link
|
||||
href={`/auth?back_url=${pathName}`}
|
||||
className="text-desktop-primary line-clamp-1 flex cursor-pointer flex-row items-center gap-2"
|
||||
>
|
||||
<p>{t("login")}</p>
|
||||
|
||||
<div className="bg-desktop-primary h-4 w-[1.5px]" />
|
||||
|
||||
<p>{t("register")}</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<SideBarComponent items={mobileMenuItems} />
|
||||
</nav>
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
<div className="text-text mt-auto pt-4 text-center text-[8px]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="bg-desktop-primary/10 mx-auto mb-3 flex w-fit items-center justify-center gap-4 rounded-2xl p-3 shadow-md"
|
||||
></motion.div>
|
||||
{t("policy")}
|
||||
</div>
|
||||
</motion.aside>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
39
src/components/ThemeToggle/index.tsx
Normal file
39
src/components/ThemeToggle/index.tsx
Normal file
@@ -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 (
|
||||
<AnimatePresence>
|
||||
<button onClick={handleThemeToggle} className="flex cursor-pointer items-center py-2">
|
||||
<motion.div
|
||||
key={theme}
|
||||
initial={{ rotate: -90, opacity: 0 }}
|
||||
animate={{ rotate: 0, opacity: 1 }}
|
||||
exit={{ rotate: 90, opacity: 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<DarkModeIcon className="text-text size-6" />
|
||||
) : (
|
||||
<LightModeIcon className="text-text size-6" />
|
||||
)}
|
||||
</motion.div>
|
||||
</button>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user