Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
import { useState } from "react";
import { ThemeToggleButton } from "../common/ThemeToggleButton";
import NotificationDropdown from "./NotificationDropdown";
import UserDropdown from "./UserDropdown";
import { Link } from "react-router";
// Define the interface for the props
interface HeaderProps {
onClick?: () => void; // Optional function that takes no arguments and returns void
onToggle: () => void;
}
const Header: React.FC<HeaderProps> = ({ onClick, onToggle }) => {
const [isApplicationMenuOpen, setApplicationMenuOpen] = useState(false);
const toggleApplicationMenu = () => {
setApplicationMenuOpen(!isApplicationMenuOpen);
};
return (
<header className="sticky top-0 flex w-full bg-white border-gray-200 z-99999 dark:border-gray-800 dark:bg-gray-900 lg:border-b">
<div className="flex flex-col items-center justify-between grow lg:flex-row lg:px-6">
<div className="flex items-center justify-between w-full gap-2 px-3 py-3 border-b border-gray-200 dark:border-gray-800 sm:gap-4 lg:justify-normal lg:border-b-0 lg:px-0 lg:py-4">
<button
className="block w-10 h-10 text-gray-500 lg:hidden dark:text-gray-400"
onClick={onToggle}
>
{/* Hamburger Icon */}
<svg
className={`block`}
width="16"
height="12"
viewBox="0 0 16 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
fill="currentColor"
/>
</svg>
<svg
className="hidden"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51256 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z"
fill="currentColor"
/>
</svg>
{/* Cross Icon */}
</button>
<button
onClick={onClick}
className="items-center justify-center hidden w-10 h-10 text-gray-500 border-gray-200 rounded-lg z-99999 dark:border-gray-800 lg:flex dark:text-gray-400 lg:h-11 lg:w-11 lg:border"
>
<svg
className="hidden fill-current lg:block"
width="16"
height="12"
viewBox="0 0 16 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
fill=""
/>
</svg>
</button>
<Link to="/" className="lg:hidden">
<img
className="dark:hidden"
src="./images/logo/logo.svg"
alt="Logo"
/>
<img
className="hidden dark:block"
src="./images/logo/logo-dark.svg"
alt="Logo"
/>
</Link>
<button
onClick={toggleApplicationMenu}
className="flex items-center justify-center w-10 h-10 text-gray-700 rounded-lg z-99999 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 lg:hidden"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.99902 10.4951C6.82745 10.4951 7.49902 11.1667 7.49902 11.9951V12.0051C7.49902 12.8335 6.82745 13.5051 5.99902 13.5051C5.1706 13.5051 4.49902 12.8335 4.49902 12.0051V11.9951C4.49902 11.1667 5.1706 10.4951 5.99902 10.4951ZM17.999 10.4951C18.8275 10.4951 19.499 11.1667 19.499 11.9951V12.0051C19.499 12.8335 18.8275 13.5051 17.999 13.5051C17.1706 13.5051 16.499 12.8335 16.499 12.0051V11.9951C16.499 11.1667 17.1706 10.4951 17.999 10.4951ZM13.499 11.9951C13.499 11.1667 12.8275 10.4951 11.999 10.4951C11.1706 10.4951 10.499 11.1667 10.499 11.9951V12.0051C10.499 12.8335 11.1706 13.5051 11.999 13.5051C12.8275 13.5051 13.499 12.8335 13.499 12.0051V11.9951Z"
fill="currentColor"
/>
</svg>
</button>
<div className="hidden lg:block">
<form action="https://formbold.com/s/unique_form_id" method="POST">
<div className="relative">
<button className="absolute -translate-y-1/2 left-4 top-1/2">
<svg
className="fill-gray-500 dark:fill-gray-400"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.04175 9.37363C3.04175 5.87693 5.87711 3.04199 9.37508 3.04199C12.8731 3.04199 15.7084 5.87693 15.7084 9.37363C15.7084 12.8703 12.8731 15.7053 9.37508 15.7053C5.87711 15.7053 3.04175 12.8703 3.04175 9.37363ZM9.37508 1.54199C5.04902 1.54199 1.54175 5.04817 1.54175 9.37363C1.54175 13.6991 5.04902 17.2053 9.37508 17.2053C11.2674 17.2053 13.003 16.5344 14.357 15.4176L17.177 18.238C17.4699 18.5309 17.9448 18.5309 18.2377 18.238C18.5306 17.9451 18.5306 17.4703 18.2377 17.1774L15.418 14.3573C16.5365 13.0033 17.2084 11.2669 17.2084 9.37363C17.2084 5.04817 13.7011 1.54199 9.37508 1.54199Z"
fill=""
/>
</svg>
</button>
<input
type="text"
placeholder="Search or type command..."
className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-200 bg-transparent py-2.5 pl-12 pr-14 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:bg-gray-900 dark:bg-white/[0.03] dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 xl:w-[430px]"
/>
<button className="absolute right-2.5 top-1/2 inline-flex -translate-y-1/2 items-center gap-0.5 rounded-lg border border-gray-200 bg-gray-50 px-[7px] py-[4.5px] text-xs -tracking-[0.2px] text-gray-500 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400">
<span> </span>
<span> K </span>
</button>
</div>
</form>
</div>
</div>
<div
className={`${
isApplicationMenuOpen ? "flex" : "hidden"
} items-center justify-between w-full gap-4 px-5 py-4 lg:flex shadow-theme-md lg:justify-end lg:px-0 lg:shadow-none`}
>
<div className="flex items-center gap-2 2xsm:gap-3">
{/* <!-- Dark Mode Toggler --> */}
<ThemeToggleButton />
{/* <!-- Dark Mode Toggler --> */}
<NotificationDropdown />
{/* <!-- Notification Menu Area --> */}
</div>
{/* <!-- User Area --> */}
<UserDropdown />
</div>
</div>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { useHeaderMetrics } from '../../context/HeaderMetricsContext';
export const HeaderMetrics: React.FC = () => {
const { metrics } = useHeaderMetrics();
if (!metrics || metrics.length === 0) return null;
return (
<div className="igny8-header-metrics hidden lg:flex">
{metrics.map((metric, index) => (
<React.Fragment key={index}>
<div className="igny8-header-metric">
<div className={`igny8-header-metric-accent ${metric.accentColor}`}></div>
<span className="igny8-header-metric-label">{metric.label}</span>
<span className="igny8-header-metric-value">
{typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value}
</span>
</div>
{index < metrics.length - 1 && <div className="igny8-header-metric-separator"></div>}
</React.Fragment>
))}
</div>
);
};

View File

@@ -0,0 +1,380 @@
import { useState } from "react";
import { Dropdown } from "../ui/dropdown/Dropdown";
import { DropdownItem } from "../ui/dropdown/DropdownItem";
import { Link } from "react-router";
export default function NotificationDropdown() {
const [isOpen, setIsOpen] = useState(false);
const [notifying, setNotifying] = useState(true);
function toggleDropdown() {
setIsOpen(!isOpen);
}
function closeDropdown() {
setIsOpen(false);
}
const handleClick = () => {
toggleDropdown();
setNotifying(false);
};
return (
<div className="relative">
<button
className="relative flex items-center justify-center text-gray-500 transition-colors bg-white border border-gray-200 rounded-full dropdown-toggle hover:text-gray-700 h-11 w-11 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
onClick={handleClick}
>
<span
className={`absolute right-0 top-0.5 z-10 h-2 w-2 rounded-full bg-orange-400 ${
!notifying ? "hidden" : "flex"
}`}
>
<span className="absolute inline-flex w-full h-full bg-orange-400 rounded-full opacity-75 animate-ping"></span>
</span>
<svg
className="fill-current"
width="20"
height="20"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.75 2.29248C10.75 1.87827 10.4143 1.54248 10 1.54248C9.58583 1.54248 9.25004 1.87827 9.25004 2.29248V2.83613C6.08266 3.20733 3.62504 5.9004 3.62504 9.16748V14.4591H3.33337C2.91916 14.4591 2.58337 14.7949 2.58337 15.2091C2.58337 15.6234 2.91916 15.9591 3.33337 15.9591H4.37504H15.625H16.6667C17.0809 15.9591 17.4167 15.6234 17.4167 15.2091C17.4167 14.7949 17.0809 14.4591 16.6667 14.4591H16.375V9.16748C16.375 5.9004 13.9174 3.20733 10.75 2.83613V2.29248ZM14.875 14.4591V9.16748C14.875 6.47509 12.6924 4.29248 10 4.29248C7.30765 4.29248 5.12504 6.47509 5.12504 9.16748V14.4591H14.875ZM8.00004 17.7085C8.00004 18.1228 8.33583 18.4585 8.75004 18.4585H11.25C11.6643 18.4585 12 18.1228 12 17.7085C12 17.2943 11.6643 16.9585 11.25 16.9585H8.75004C8.33583 16.9585 8.00004 17.2943 8.00004 17.7085Z"
fill="currentColor"
/>
</svg>
</button>
<Dropdown
isOpen={isOpen}
onClose={closeDropdown}
className="absolute -right-[240px] mt-[17px] flex h-[480px] w-[350px] flex-col rounded-2xl border border-gray-200 bg-white p-3 shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark sm:w-[361px] lg:right-0"
>
<div className="flex items-center justify-between pb-3 mb-3 border-b border-gray-100 dark:border-gray-700">
<h5 className="text-lg font-semibold text-gray-800 dark:text-gray-200">
Notification
</h5>
<button
onClick={toggleDropdown}
className="text-gray-500 transition dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
>
<svg
className="fill-current"
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51256 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z"
fill="currentColor"
/>
</svg>
</button>
</div>
<ul className="flex flex-col h-auto overflow-y-auto custom-scrollbar">
{/* Example notification items */}
<li>
<DropdownItem
onItemClick={closeDropdown}
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
>
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
<img
width={40}
height={40}
src="/images/user/user-02.jpg"
alt="User"
className="w-full overflow-hidden rounded-full"
/>
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-success-500 dark:border-gray-900"></span>
</span>
<span className="block">
<span className="mb-1.5 block text-theme-sm text-gray-500 dark:text-gray-400 space-x-1">
<span className="font-medium text-gray-800 dark:text-white/90">
Terry Franci
</span>
<span> requests permission to change</span>
<span className="font-medium text-gray-800 dark:text-white/90">
Project - Nganter App
</span>
</span>
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
<span>Project</span>
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
<span>5 min ago</span>
</span>
</span>
</DropdownItem>
</li>
<li>
<DropdownItem
onItemClick={closeDropdown}
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
>
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
<img
width={40}
height={40}
src="/images/user/user-03.jpg"
alt="User"
className="w-full overflow-hidden rounded-full"
/>
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-success-500 dark:border-gray-900"></span>
</span>
<span className="block">
<span className="mb-1.5 block space-x-1 text-theme-sm text-gray-500 dark:text-gray-400">
<span className="font-medium text-gray-800 dark:text-white/90">
Alena Franci
</span>
<span>requests permission to change</span>
<span className="font-medium text-gray-800 dark:text-white/90">
Project - Nganter App
</span>
</span>
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
<span>Project</span>
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
<span>8 min ago</span>
</span>
</span>
</DropdownItem>
</li>
<li>
<DropdownItem
onItemClick={closeDropdown}
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
>
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
<img
width={40}
height={40}
src="/images/user/user-04.jpg"
alt="User"
className="w-full overflow-hidden rounded-full"
/>
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-success-500 dark:border-gray-900"></span>
</span>
<span className="block">
<span className="mb-1.5 block space-x-1 text-theme-sm text-gray-500 dark:text-gray-400">
<span className="font-medium text-gray-800 dark:text-white/90">
Jocelyn Kenter
</span>
<span> requests permission to change</span>
<span className="font-medium text-gray-800 dark:text-white/90">
Project - Nganter App
</span>
</span>
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
<span>Project</span>
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
<span>15 min ago</span>
</span>
</span>
</DropdownItem>
</li>
<li>
<DropdownItem
onItemClick={closeDropdown}
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
to="/"
>
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
<img
width={40}
height={40}
src="/images/user/user-05.jpg"
alt="User"
className="w-full overflow-hidden rounded-full"
/>
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-error-500 dark:border-gray-900"></span>
</span>
<span className="block">
<span className="mb-1.5 space-x-1 block text-theme-sm text-gray-500 dark:text-gray-400">
<span className="font-medium text-gray-800 dark:text-white/90">
Brandon Philips
</span>
<span>requests permission to change</span>
<span className="font-medium text-gray-800 dark:text-white/90">
Project - Nganter App
</span>
</span>
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
<span>Project</span>
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
<span>1 hr ago</span>
</span>
</span>
</DropdownItem>
</li>
<li>
<DropdownItem
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
onItemClick={closeDropdown}
>
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
<img
width={40}
height={40}
src="/images/user/user-02.jpg"
alt="User"
className="w-full overflow-hidden rounded-full"
/>
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-success-500 dark:border-gray-900"></span>
</span>
<span className="block">
<span className="mb-1.5 block space-x-1 text-theme-sm text-gray-500 dark:text-gray-400">
<span className="font-medium text-gray-800 dark:text-white/90">
Terry Franci
</span>
<span> requests permission to change</span>
<span className="font-medium text-gray-800 dark:text-white/90">
Project - Nganter App
</span>
</span>
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
<span>Project</span>
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
<span>5 min ago</span>
</span>
</span>
</DropdownItem>
</li>
<li>
<DropdownItem
onItemClick={closeDropdown}
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
>
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
<img
width={40}
height={40}
src="/images/user/user-03.jpg"
alt="User"
className="w-full overflow-hidden rounded-full"
/>
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-success-500 dark:border-gray-900"></span>
</span>
<span className="block">
<span className="mb-1.5 block space-x-1 text-theme-sm text-gray-500 dark:text-gray-400">
<span className="font-medium text-gray-800 dark:text-white/90">
Alena Franci
</span>
<span> requests permission to change</span>
<span className="font-medium text-gray-800 dark:text-white/90">
Project - Nganter App
</span>
</span>
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
<span>Project</span>
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
<span>8 min ago</span>
</span>
</span>
</DropdownItem>
</li>
<li>
<DropdownItem
onItemClick={closeDropdown}
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
>
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
<img
width={40}
height={40}
src="/images/user/user-04.jpg"
alt="User"
className="w-full overflow-hidden rounded-full"
/>
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-success-500 dark:border-gray-900"></span>
</span>
<span className="block">
<span className="mb-1.5 block space-x-1 text-theme-sm text-gray-500 dark:text-gray-400">
<span className="font-medium text-gray-800 dark:text-white/90">
Jocelyn Kenter
</span>
<span> requests permission to change</span>
<span className="font-medium text-gray-800 dark:text-white/90">
Project - Nganter App
</span>
</span>
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
<span>Project</span>
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
<span>15 min ago</span>
</span>
</span>
</DropdownItem>
</li>
<li>
<DropdownItem
onItemClick={closeDropdown}
className="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
>
<span className="relative block w-full h-10 rounded-full z-1 max-w-10">
<img
width={40}
height={40}
src="/images/user/user-05.jpg"
alt="User"
className="overflow-hidden rounded-full"
/>
<span className="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white bg-error-500 dark:border-gray-900"></span>
</span>
<span className="block">
<span className="mb-1.5 block space-x-1 text-theme-sm text-gray-500 dark:text-gray-400">
<span className="font-medium text-gray-800 dark:text-white/90">
Brandon Philips
</span>
<span>requests permission to change</span>
<span className="font-medium text-gray-800 dark:text-white/90">
Project - Nganter App
</span>
</span>
<span className="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
<span>Project</span>
<span className="w-1 h-1 bg-gray-400 rounded-full"></span>
<span>1 hr ago</span>
</span>
</span>
</DropdownItem>
</li>
{/* Add more items as needed */}
</ul>
<Link
to="/"
className="block px-4 py-2 mt-3 text-sm font-medium text-center text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700"
>
View All Notifications
</Link>
</Dropdown>
</div>
);
}

View File

@@ -0,0 +1,219 @@
import { useState, useEffect, useRef } from "react";
import { useLocation } from "react-router";
import { Dropdown } from "../ui/dropdown/Dropdown";
import { DropdownItem } from "../ui/dropdown/DropdownItem";
import { fetchSites, Site, setActiveSite as apiSetActiveSite } from "../../services/api";
import { useToast } from "../ui/toast/ToastContainer";
import { useSiteStore } from "../../store/siteStore";
import { useAuthStore } from "../../store/authStore";
/**
* SiteSwitcher Component
*
* Shows a dropdown menu with all active sites for the user.
* Should only be visible on planner and writer pages (and their submodules).
*
* Paths where switcher should be shown:
* - /planner (dashboard and all subpages)
* - /writer (dashboard and all subpages)
*
* Paths where switcher should be hidden:
* - /settings (including /settings/sites)
* - /dashboard
* - /analytics
* - /schedules
* - /thinker
* - /signin, /signup
*/
// Paths where switcher should be shown (planner and writer modules)
const SITE_SWITCHER_SHOWN_PATHS = [
'/planner',
'/writer',
];
// Paths where switcher should be hidden (override shown paths)
const SITE_SWITCHER_HIDDEN_PATHS = [
'/settings',
'/dashboard',
'/analytics',
'/schedules',
'/thinker',
];
interface SiteSwitcherProps {
hiddenPaths?: string[]; // Optional override for hidden paths
}
export default function SiteSwitcher({ hiddenPaths }: SiteSwitcherProps) {
const location = useLocation();
const toast = useToast();
const { activeSite, setActiveSite, loadActiveSite } = useSiteStore();
const { user, refreshUser, isAuthenticated } = useAuthStore();
const [isOpen, setIsOpen] = useState(false);
const [sites, setSites] = useState<Site[]>([]);
const [loading, setLoading] = useState(true);
const buttonRef = useRef<HTMLButtonElement>(null);
// Determine if switcher should be shown
const hiddenPathsToUse = hiddenPaths || SITE_SWITCHER_HIDDEN_PATHS;
const shownPaths = SITE_SWITCHER_SHOWN_PATHS;
let shouldHide = false;
if (shownPaths.length > 0) {
// If shown paths are defined, only show on those paths
shouldHide = !shownPaths.some(path => location.pathname.startsWith(path));
} else {
// Otherwise, hide on explicitly hidden paths
shouldHide = hiddenPathsToUse.some(path => location.pathname.startsWith(path));
}
// Refresh user data when component mounts or user changes
// This ensures we have latest account/plan info for proper site filtering
useEffect(() => {
if (isAuthenticated && user) {
// Refresh user data to get latest account/plan changes
// This is important so site filtering works correctly
refreshUser().catch((error) => {
// Silently fail - user might still be valid, just couldn't refresh
console.debug('SiteSwitcher: Failed to refresh user (non-critical):', error);
});
}
}, [isAuthenticated]); // Only refresh when auth state changes, not on every render
useEffect(() => {
if (shouldHide) {
setLoading(false);
return;
}
loadSites();
// Load active site from store (only once, not on every render)
if (!activeSite) {
loadActiveSite();
}
}, [shouldHide, location.pathname, user?.account?.id]); // Also reload when user's account changes
const loadSites = async () => {
try {
setLoading(true);
const response = await fetchSites();
// Filter to only show active sites in the switcher
const activeSites = (response.results || []).filter(site => site.is_active);
setSites(activeSites);
} catch (error: any) {
console.error('Failed to load sites:', error);
toast.error(`Failed to load sites: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleSiteSelect = async (siteId: number) => {
try {
await apiSetActiveSite(siteId);
const selectedSite = sites.find(s => s.id === siteId);
if (selectedSite) {
// Update the global site store - this will dispatch siteChanged event
setActiveSite(selectedSite);
toast.success(`Switched to "${selectedSite.name}"`);
}
setIsOpen(false);
} catch (error: any) {
toast.error(`Failed to switch site: ${error.message}`);
}
};
function toggleDropdown() {
setIsOpen(!isOpen);
}
// Don't render if should be hidden
if (shouldHide) {
return null;
}
// Don't render if loading or no sites
if (loading || sites.length === 0) {
return null;
}
return (
<div className="relative inline-block">
<button
ref={buttonRef}
onClick={toggleDropdown}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:hover:bg-gray-700 dropdown-toggle"
aria-label="Switch site"
>
<span className="flex items-center gap-2">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
<span className="max-w-[150px] truncate">
{activeSite?.name || 'Select Site'}
</span>
</span>
<svg
className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
<Dropdown
isOpen={isOpen}
onClose={() => setIsOpen(false)}
anchorRef={buttonRef}
placement="bottom-left"
className="w-64 p-2"
>
{sites.map((site) => (
<DropdownItem
key={site.id}
onItemClick={() => handleSiteSelect(site.id)}
className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${
activeSite?.id === site.id
? "bg-blue-50 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300"
: "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
}`}
>
<span className="flex-1">{site.name}</span>
{activeSite?.id === site.id && (
<svg
className="w-4 h-4 text-blue-600 dark:text-blue-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
)}
</DropdownItem>
))}
</Dropdown>
</div>
);
}

View File

@@ -0,0 +1,185 @@
import { useState } from "react";
import { useNavigate } from "react-router";
import { DropdownItem } from "../ui/dropdown/DropdownItem";
import { Dropdown } from "../ui/dropdown/Dropdown";
import { Link } from "react-router";
import { useAuthStore } from "../../store/authStore";
export default function UserDropdown() {
const [isOpen, setIsOpen] = useState(false);
const navigate = useNavigate();
const { user, logout } = useAuthStore();
function toggleDropdown() {
setIsOpen(!isOpen);
}
function closeDropdown() {
setIsOpen(false);
}
const handleLogout = () => {
logout();
navigate("/signin", { replace: true });
closeDropdown();
};
return (
<div className="relative">
<button
onClick={toggleDropdown}
className="flex items-center text-gray-700 dropdown-toggle dark:text-gray-400"
>
<span className="mr-3 overflow-hidden rounded-full h-11 w-11 bg-brand-500 flex items-center justify-center">
{user?.email ? (
<span className="text-white font-semibold text-sm">
{user.email.charAt(0).toUpperCase()}
</span>
) : (
<img src="/images/user/owner.jpg" alt="User" />
)}
</span>
<span className="block mr-1 font-medium text-theme-sm">
{user?.username || user?.email?.split("@")[0] || "User"}
</span>
<svg
className={`stroke-gray-500 dark:stroke-gray-400 transition-transform duration-200 ${
isOpen ? "rotate-180" : ""
}`}
width="18"
height="20"
viewBox="0 0 18 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.3125 8.65625L9 13.3437L13.6875 8.65625"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<Dropdown
isOpen={isOpen}
onClose={closeDropdown}
className="absolute right-0 mt-[17px] flex w-[260px] flex-col rounded-2xl border border-gray-200 bg-white p-3 shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark"
>
<div>
<span className="block font-medium text-gray-700 text-theme-sm dark:text-gray-400">
{user?.username || "User"}
</span>
<span className="mt-0.5 block text-theme-xs text-gray-500 dark:text-gray-400">
{user?.email || "No email"}
</span>
{user?.role && (
<span className="mt-1 inline-block px-2 py-0.5 text-xs font-medium text-brand-600 bg-brand-50 rounded dark:text-brand-400 dark:bg-brand-900/20">
{user.role}
</span>
)}
</div>
<ul className="flex flex-col gap-1 pt-4 pb-3 border-b border-gray-200 dark:border-gray-800">
<li>
<DropdownItem
onItemClick={closeDropdown}
tag="a"
to="/profile"
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
>
<svg
className="fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 3.5C7.30558 3.5 3.5 7.30558 3.5 12C3.5 14.1526 4.3002 16.1184 5.61936 17.616C6.17279 15.3096 8.24852 13.5955 10.7246 13.5955H13.2746C15.7509 13.5955 17.8268 15.31 18.38 17.6167C19.6996 16.119 20.5 14.153 20.5 12C20.5 7.30558 16.6944 3.5 12 3.5ZM17.0246 18.8566V18.8455C17.0246 16.7744 15.3457 15.0955 13.2746 15.0955H10.7246C8.65354 15.0955 6.97461 16.7744 6.97461 18.8455V18.856C8.38223 19.8895 10.1198 20.5 12 20.5C13.8798 20.5 15.6171 19.8898 17.0246 18.8566ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM11.9991 7.25C10.8847 7.25 9.98126 8.15342 9.98126 9.26784C9.98126 10.3823 10.8847 11.2857 11.9991 11.2857C13.1135 11.2857 14.0169 10.3823 14.0169 9.26784C14.0169 8.15342 13.1135 7.25 11.9991 7.25ZM8.48126 9.26784C8.48126 7.32499 10.0563 5.75 11.9991 5.75C13.9419 5.75 15.5169 7.32499 15.5169 9.26784C15.5169 11.2107 13.9419 12.7857 11.9991 12.7857C10.0563 12.7857 8.48126 11.2107 8.48126 9.26784Z"
fill=""
/>
</svg>
Edit profile
</DropdownItem>
</li>
<li>
<DropdownItem
onItemClick={closeDropdown}
tag="a"
to="/profile"
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
>
<svg
className="fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.4858 3.5L13.5182 3.5C13.9233 3.5 14.2518 3.82851 14.2518 4.23377C14.2518 5.9529 16.1129 7.02795 17.602 6.1682C17.9528 5.96567 18.4014 6.08586 18.6039 6.43667L20.1203 9.0631C20.3229 9.41407 20.2027 9.86286 19.8517 10.0655C18.3625 10.9253 18.3625 13.0747 19.8517 13.9345C20.2026 14.1372 20.3229 14.5859 20.1203 14.9369L18.6039 17.5634C18.4013 17.9142 17.9528 18.0344 17.602 17.8318C16.1129 16.9721 14.2518 18.0471 14.2518 19.7663C14.2518 20.1715 13.9233 20.5 13.5182 20.5H10.4858C10.0804 20.5 9.75182 20.1714 9.75182 19.766C9.75182 18.0461 7.88983 16.9717 6.40067 17.8314C6.04945 18.0342 5.60037 17.9139 5.39767 17.5628L3.88167 14.937C3.67903 14.586 3.79928 14.1372 4.15026 13.9346C5.63949 13.0748 5.63946 10.9253 4.15025 10.0655C3.79926 9.86282 3.67901 9.41401 3.88165 9.06303L5.39764 6.43725C5.60034 6.08617 6.04943 5.96581 6.40065 6.16858C7.88982 7.02836 9.75182 5.9539 9.75182 4.23399C9.75182 3.82862 10.0804 3.5 10.4858 3.5ZM13.5182 2L10.4858 2C9.25201 2 8.25182 3.00019 8.25182 4.23399C8.25182 4.79884 7.64013 5.15215 7.15065 4.86955C6.08213 4.25263 4.71559 4.61859 4.0986 5.68725L2.58261 8.31303C1.96575 9.38146 2.33183 10.7477 3.40025 11.3645C3.88948 11.647 3.88947 12.3531 3.40026 12.6355C2.33184 13.2524 1.96578 14.6186 2.58263 15.687L4.09863 18.3128C4.71562 19.3814 6.08215 19.7474 7.15067 19.1305C7.64015 18.8479 8.25182 19.2012 8.25182 19.766C8.25182 20.9998 9.25201 22 10.4858 22H13.5182C14.7519 22 15.7518 20.9998 15.7518 19.7663C15.7518 19.2015 16.3632 18.8487 16.852 19.1309C17.9202 19.7476 19.2862 19.3816 19.9029 18.3134L21.4193 15.6869C22.0361 14.6185 21.6701 13.2523 20.6017 12.6355C20.1125 12.3531 20.1125 11.647 20.6017 11.3645C21.6701 10.7477 22.0362 9.38152 21.4193 8.3131L19.903 5.68667C19.2862 4.61842 17.9202 4.25241 16.852 4.86917C16.3632 5.15138 15.7518 4.79856 15.7518 4.23377C15.7518 3.00024 14.7519 2 13.5182 2ZM9.6659 11.9999C9.6659 10.7103 10.7113 9.66493 12.0009 9.66493C13.2905 9.66493 14.3359 10.7103 14.3359 11.9999C14.3359 13.2895 13.2905 14.3349 12.0009 14.3349C10.7113 14.3349 9.6659 13.2895 9.6659 11.9999ZM12.0009 8.16493C9.88289 8.16493 8.1659 9.88191 8.1659 11.9999C8.1659 14.1179 9.88289 15.8349 12.0009 15.8349C14.1189 15.8349 15.8359 14.1179 15.8359 11.9999C15.8359 9.88191 14.1189 8.16493 12.0009 8.16493Z"
fill=""
/>
</svg>
Account settings
</DropdownItem>
</li>
<li>
<DropdownItem
onItemClick={closeDropdown}
tag="a"
to="/profile"
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
>
<svg
className="fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.5 12C3.5 7.30558 7.30558 3.5 12 3.5C16.6944 3.5 20.5 7.30558 20.5 12C20.5 16.6944 16.6944 20.5 12 20.5C7.30558 20.5 3.5 16.6944 3.5 12ZM12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM11.0991 7.52507C11.0991 8.02213 11.5021 8.42507 11.9991 8.42507H12.0001C12.4972 8.42507 12.9001 8.02213 12.9001 7.52507C12.9001 7.02802 12.4972 6.62507 12.0001 6.62507H11.9991C11.5021 6.62507 11.0991 7.02802 11.0991 7.52507ZM12.0001 17.3714C11.5859 17.3714 11.2501 17.0356 11.2501 16.6214V10.9449C11.2501 10.5307 11.5859 10.1949 12.0001 10.1949C12.4143 10.1949 12.7501 10.5307 12.7501 10.9449V16.6214C12.7501 17.0356 12.4143 17.3714 12.0001 17.3714Z"
fill=""
/>
</svg>
Support
</DropdownItem>
</li>
</ul>
<button
onClick={handleLogout}
className="flex items-center gap-3 px-3 py-2 mt-3 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300 w-full text-left"
>
<svg
className="fill-gray-500 group-hover:fill-gray-700 dark:group-hover:fill-gray-300"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.1007 19.247C14.6865 19.247 14.3507 18.9112 14.3507 18.497L14.3507 14.245H12.8507V18.497C12.8507 19.7396 13.8581 20.747 15.1007 20.747H18.5007C19.7434 20.747 20.7507 19.7396 20.7507 18.497L20.7507 5.49609C20.7507 4.25345 19.7433 3.24609 18.5007 3.24609H15.1007C13.8581 3.24609 12.8507 4.25345 12.8507 5.49609V9.74501L14.3507 9.74501V5.49609C14.3507 5.08188 14.6865 4.74609 15.1007 4.74609L18.5007 4.74609C18.9149 4.74609 19.2507 5.08188 19.2507 5.49609L19.2507 18.497C19.2507 18.9112 18.9149 19.247 18.5007 19.247H15.1007ZM3.25073 11.9984C3.25073 12.2144 3.34204 12.4091 3.48817 12.546L8.09483 17.1556C8.38763 17.4485 8.86251 17.4487 9.15549 17.1559C9.44848 16.8631 9.44863 16.3882 9.15583 16.0952L5.81116 12.7484L16.0007 12.7484C16.4149 12.7484 16.7507 12.4127 16.7507 11.9984C16.7507 11.5842 16.4149 11.2484 16.0007 11.2484L5.81528 11.2484L9.15585 7.90554C9.44864 7.61255 9.44847 7.13767 9.15547 6.84488C8.86248 6.55209 8.3876 6.55226 8.09481 6.84525L3.52309 11.4202C3.35673 11.5577 3.25073 11.7657 3.25073 11.9984Z"
fill=""
/>
</svg>
Sign out
</button>
</Dropdown>
</div>
);
}