Mobile profile page already has this action, but the desktop chrome had no way for a seller or staff member to jump into a store they manage — they'd have to type the URL. Add a compact "فروشگاه من" pill in the header action row, gated on useMyStores() so it only renders for users who actually manage a store. Selection mirrors the mobile flow: owned first, then the first store they staff.
250 lines
9.8 KiB
TypeScript
250 lines
9.8 KiB
TypeScript
import { Link, useLocation, useNavigate } from "@remix-run/react";
|
||
import { useState, useRef, useEffect, type FormEvent } from "react";
|
||
import { Search, Heart, ShoppingBag, User, Store } from "lucide-react";
|
||
import { useRootData } from "~/hooks/use-root-data";
|
||
import { useCartCount } from "~/requestHandler/use-cart-hooks";
|
||
import { useMyStores } from "~/requestHandler/use-seller-hooks";
|
||
import { useSearchAutocomplete } from "~/requestHandler/use-search-hooks";
|
||
import { useDebounce } from "~/hooks/useDebounce";
|
||
import { SunMoonToggle } from "~/components/SunMoonToggle";
|
||
import { AutocompleteSuggestionTypeEnum } from "../../../src/api/types";
|
||
import logo from "~/assets/logo/SVG-07.svg";
|
||
|
||
const NAV = [
|
||
{ label: "خانه", to: "/" },
|
||
{ label: "کاوش", to: "/explore" },
|
||
{ label: "برندها", to: "/sellers" },
|
||
{ label: "کالکشنها", to: "/collections" },
|
||
];
|
||
|
||
/**
|
||
* Desktop-only top chrome (sticky header with brand, nav, search and actions).
|
||
* Hidden below the `lg` breakpoint; the mobile experience is unchanged.
|
||
*/
|
||
const DesktopHeader = () => {
|
||
const location = useLocation();
|
||
const navigate = useNavigate();
|
||
const { user, theme } = useRootData();
|
||
const cartCount = useCartCount();
|
||
const { data: myStores } = useMyStores();
|
||
// Mirror the mobile "ورود به فروشگاه من" flow: prefer a store the user owns,
|
||
// fall back to the first store they staff. If they don't manage any store,
|
||
// the CTA is hidden.
|
||
const targetStore =
|
||
myStores?.find((s) => s.role === "owner") ?? myStores?.[0];
|
||
const [q, setQ] = useState("");
|
||
const [open, setOpen] = useState(false);
|
||
const searchRef = useRef<HTMLDivElement>(null);
|
||
const isLoggedIn = !!user?.access_token;
|
||
|
||
const debouncedQ = useDebounce(q.trim(), 400);
|
||
const { data: autocompleteData, isLoading: isSuggesting } =
|
||
useSearchAutocomplete(debouncedQ);
|
||
const suggestions = autocompleteData?.suggestions ?? [];
|
||
const showSuggestions = open && q.trim().length > 0;
|
||
|
||
// Close the suggestions dropdown when clicking outside the search box.
|
||
useEffect(() => {
|
||
const onClickOutside = (e: MouseEvent) => {
|
||
if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
|
||
setOpen(false);
|
||
}
|
||
};
|
||
document.addEventListener("mousedown", onClickOutside);
|
||
return () => document.removeEventListener("mousedown", onClickOutside);
|
||
}, []);
|
||
|
||
const isActive = (to: string) =>
|
||
to === "/" ? location.pathname === "/" : location.pathname.startsWith(to);
|
||
|
||
const goToSearch = (term: string) => {
|
||
const t = term.trim();
|
||
if (!t) return;
|
||
setOpen(false);
|
||
navigate(`/search/${encodeURIComponent(t)}`);
|
||
};
|
||
|
||
const onSearch = (e: FormEvent) => {
|
||
e.preventDefault();
|
||
goToSearch(q);
|
||
};
|
||
|
||
const onSelectSuggestion = (sug: {
|
||
type: AutocompleteSuggestionTypeEnum;
|
||
text?: string;
|
||
name?: string;
|
||
id?: string;
|
||
}) => {
|
||
if (sug.type === AutocompleteSuggestionTypeEnum.Seller && sug.id) {
|
||
setOpen(false);
|
||
navigate(`/seller/${sug.id}`);
|
||
return;
|
||
}
|
||
goToSearch(sug.text || sug.name || "");
|
||
};
|
||
|
||
return (
|
||
<header className="hidden lg:block sticky top-0 z-40 bg-WHITE/85 backdrop-blur-xl border-b border-WHITE3">
|
||
<div className="max-w-[1320px] mx-auto h-[72px] px-8 flex items-center gap-7">
|
||
<Link to="/" className="flex items-center gap-2.5 shrink-0">
|
||
<img src={logo} alt="ویترون" className="w-7 h-9 object-contain" />
|
||
<b className="text-[23px] font-extrabold tracking-tight text-VITROWN_BLUE">
|
||
ویترون
|
||
</b>
|
||
</Link>
|
||
|
||
<nav className="flex items-center gap-1">
|
||
{NAV.map((n) => (
|
||
<Link
|
||
key={n.to}
|
||
to={n.to}
|
||
prefetch="intent"
|
||
className={`px-3.5 py-2 rounded-[10px] text-[14.5px] transition-colors ${
|
||
isActive(n.to)
|
||
? "text-BLACK font-bold"
|
||
: "text-BLACK2 font-semibold hover:bg-WHITE3 hover:text-BLACK"
|
||
}`}
|
||
>
|
||
{n.label}
|
||
</Link>
|
||
))}
|
||
</nav>
|
||
|
||
<div ref={searchRef} className="flex-1 max-w-[460px] mx-auto relative">
|
||
<form
|
||
onSubmit={onSearch}
|
||
className="flex items-center gap-2.5 bg-WHITE3 border border-transparent rounded-xl px-3.5 h-11 transition-colors focus-within:bg-WHITE focus-within:border-GRAY3"
|
||
>
|
||
<Search size={20} className="text-GRAY shrink-0" />
|
||
<input
|
||
value={q}
|
||
onChange={(e) => {
|
||
setQ(e.target.value);
|
||
setOpen(true);
|
||
}}
|
||
onFocus={() => setOpen(true)}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Escape") setOpen(false);
|
||
}}
|
||
name="q"
|
||
autoComplete="off"
|
||
placeholder="جستجوی محصول، برند یا فروشگاه…"
|
||
aria-label="جستجو"
|
||
className="w-full bg-transparent outline-none text-[14.5px] text-BLACK placeholder:text-GRAY"
|
||
/>
|
||
</form>
|
||
|
||
{showSuggestions ? (
|
||
<div
|
||
dir="rtl"
|
||
className="absolute top-full inset-x-0 mt-2 max-h-[360px] overflow-y-auto rounded-xl bg-WHITE border border-WHITE3 shadow-xl py-1.5 z-50"
|
||
>
|
||
{isSuggesting && suggestions.length === 0 ? (
|
||
<p className="px-4 py-3 text-[13px] text-GRAY">در حال جستجو...</p>
|
||
) : suggestions.length > 0 ? (
|
||
suggestions.map((sug, index) => {
|
||
const isSeller =
|
||
sug.type === AutocompleteSuggestionTypeEnum.Seller;
|
||
const label = isSeller ? sug.name : sug.text;
|
||
if (!label) return null;
|
||
return (
|
||
<button
|
||
key={`${sug.type}-${sug.id ?? sug.text ?? index}`}
|
||
type="button"
|
||
onClick={() => onSelectSuggestion(sug)}
|
||
className="w-full flex items-center gap-2.5 px-4 h-11 text-right hover:bg-WHITE2 transition-colors"
|
||
>
|
||
{isSeller ? (
|
||
<Store size={17} className="text-GRAY shrink-0" />
|
||
) : (
|
||
<Search size={17} className="text-GRAY shrink-0" />
|
||
)}
|
||
<span className="text-[14px] text-BLACK truncate">
|
||
{label}
|
||
</span>
|
||
{isSeller ? (
|
||
<span className="mr-auto text-[11px] text-GRAY shrink-0">
|
||
فروشگاه
|
||
</span>
|
||
) : null}
|
||
</button>
|
||
);
|
||
})
|
||
) : (
|
||
<button
|
||
type="button"
|
||
onClick={() => goToSearch(q)}
|
||
className="w-full flex items-center gap-2.5 px-4 h-11 text-right hover:bg-WHITE2 transition-colors"
|
||
>
|
||
<Search size={17} className="text-GRAY shrink-0" />
|
||
<span className="text-[14px] text-BLACK truncate">{q}</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2.5 shrink-0">
|
||
<SunMoonToggle theme={theme} />
|
||
{isLoggedIn ? (
|
||
<>
|
||
{targetStore ? (
|
||
<Link
|
||
to={`/store/${targetStore.username}`}
|
||
aria-label="ورود به فروشگاه من"
|
||
prefetch="intent"
|
||
className="inline-flex items-center gap-2 h-[42px] px-3.5 rounded-xl bg-WHITE3 text-BLACK text-[13.5px] font-bold hover:bg-WHITE2 transition-colors whitespace-nowrap"
|
||
>
|
||
<Store size={18} />
|
||
<span>فروشگاه من</span>
|
||
</Link>
|
||
) : null}
|
||
<Link
|
||
to="/profile/bookmarks"
|
||
aria-label="نشانشدهها"
|
||
prefetch="intent"
|
||
className="w-[42px] h-[42px] rounded-xl grid place-items-center text-BLACK hover:bg-WHITE3 transition-colors"
|
||
>
|
||
<Heart size={21} />
|
||
</Link>
|
||
<Link
|
||
to="/cart"
|
||
aria-label="سبد خرید"
|
||
prefetch="intent"
|
||
className="relative w-[42px] h-[42px] rounded-xl grid place-items-center text-BLACK hover:bg-WHITE3 transition-colors"
|
||
>
|
||
{cartCount > 0 ? (
|
||
<span className="absolute top-1 left-1 min-w-[17px] h-[17px] bg-RED text-white rounded-full text-[10.5px] font-bold grid place-items-center px-1 border-2 border-WHITE">
|
||
{cartCount}
|
||
</span>
|
||
) : null}
|
||
<ShoppingBag size={21} />
|
||
</Link>
|
||
<Link
|
||
to="/profile"
|
||
aria-label="پروفایل"
|
||
prefetch="intent"
|
||
className="w-[42px] h-[42px] rounded-xl grid place-items-center text-BLACK hover:bg-WHITE3 transition-colors"
|
||
>
|
||
<span className="w-9 h-9 rounded-full bg-WHITE3 grid place-items-center text-GRAY overflow-hidden">
|
||
<User size={20} />
|
||
</span>
|
||
</Link>
|
||
</>
|
||
) : (
|
||
<Link
|
||
to="/login"
|
||
prefetch="intent"
|
||
className="inline-flex items-center h-10 px-4 rounded-xl bg-VITROWN_BLUE text-white text-[14px] font-bold hover:bg-[#012f6b] transition-colors whitespace-nowrap"
|
||
>
|
||
ورود / ثبت نام
|
||
</Link>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</header>
|
||
);
|
||
};
|
||
|
||
export default DesktopHeader;
|