Desktop: account area (shared sidebar) + brands list

- new profile.tsx layout: sticky desktop sidebar (user card + menu) wrapping all
  /profile/* pages; mobile renders page content only (unchanged)
- ProfilePagesHeader hidden at lg (one change covers all account sub-pages +
  cart); profile index shows a welcome on desktop (menu lives in the sidebar)
- sellers: 5-col brand grid with name overlay, contained, mobile header hidden
- MyLayout: desktop gate now covers /profile/* and /sellers
This commit is contained in:
Arda Samadi 2026-06-16 12:46:22 +03:30
parent 96034fe61a
commit 3156fb4204
5 changed files with 126 additions and 13 deletions

View File

@ -41,7 +41,8 @@ const MyLayout = React.memo(({ children }: { children: React.ReactNode }) => {
p.startsWith("/collection/") ||
p === "/cart" ||
p.startsWith("/cart/") ||
p === "/profile";
p === "/sellers" ||
p.startsWith("/profile");
// Auth pages get a full-width desktop canvas (so they can center their own
// card) but no shopping header/footer.
const isAuth = p === "/login" || p === "/logout";

View File

@ -20,7 +20,7 @@ const ProfilePagesHeader = memo(function ProfilePagesHeader({
}, [navigate]);
return (
<div className="flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
<div className="lg:hidden flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
{!hideBackButton && (
<ArrowRight
onClick={handleBack}

View File

@ -125,11 +125,17 @@ export default function Profile() {
];
const navigate = useNavigate();
return (
<div className="flex flex-col gap-0 lg:max-w-[1000px] lg:mx-auto lg:w-full lg:px-8 lg:py-8">
<h1 className="hidden lg:block text-[28px] font-extrabold mb-2">
حساب کاربری
</h1>
<ProfileTopBar user={user} />
<div className="flex flex-col gap-0">
{/* Desktop: welcome (the sidebar provides the menu) */}
<div className="hidden lg:flex flex-col items-center justify-center text-center py-24 border border-WHITE3 rounded-2xl">
<CircleUserRound size={64} className="text-GRAY mb-4" />
<h2 className="text-[22px] font-extrabold mb-1">حساب کاربری</h2>
<p className="text-GRAY">یک گزینه را از منوی کناری انتخاب کنید</p>
</div>
{/* Mobile: account menu */}
<div className="flex flex-col gap-0 lg:hidden">
<ProfileTopBar user={user} />
<div
className={`flex flex-row h-[55px] w-full items-center gap-2 justify-between px-[16px] active:bg-hover transition`}
>
@ -157,6 +163,7 @@ export default function Profile() {
setLogoutModalOpen(true);
}}
/>
</div>
<StoreModal open={storeModalOpen} onOpenChange={setStoreModalOpen} />
<LogoutModal open={logoutModalOpen} onOpenChange={setLogoutModalOpen} />
<LoginRequiredDialog

92
app/routes/profile.tsx Normal file
View File

@ -0,0 +1,92 @@
import { Link, Outlet, useLocation } from "@remix-run/react";
import {
ShoppingBag,
MessageCircle,
CircleFadingPlus,
BookmarkMinus,
Layers,
Wallet,
MapPin,
Settings,
LogOut,
CircleUserRound,
} from "lucide-react";
import { useServiceGetProfile } from "~/utils/RequestHandler";
import { useRootData } from "~/hooks/use-root-data";
const NAV = [
{ label: "سفارش‌ها", to: "/profile/orders", icon: ShoppingBag },
{ label: "پیام‌ها", to: "/profile/threads", icon: MessageCircle },
{ label: "دنبال‌شده‌ها", to: "/profile/following", icon: CircleFadingPlus },
{ label: "ذخیره‌شده‌ها", to: "/profile/bookmarks", icon: BookmarkMinus },
{ label: "ست‌های من", to: "/profile/sets", icon: Layers },
{ label: "کیف پول", to: "/profile/wallet", icon: Wallet },
{ label: "آدرس‌ها", to: "/profile/location", icon: MapPin },
{ label: "تنظیمات", to: "/profile/setting", icon: Settings },
];
/**
* Layout for the account area. On desktop it renders a sticky sidebar
* (user card + menu) next to the page content; on mobile it's just the
* page content (each profile page keeps its existing mobile UI).
*/
export default function ProfileLayout() {
const location = useLocation();
const { user } = useRootData();
const { data: profile } = useServiceGetProfile();
const isActive = (to: string) =>
location.pathname === to || location.pathname.startsWith(to + "/");
return (
<div className="lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8 lg:py-8 lg:grid lg:grid-cols-[280px_1fr] lg:gap-7 lg:items-start">
{/* Desktop sidebar */}
<aside className="hidden lg:block lg:sticky lg:top-[88px] border border-WHITE3 rounded-2xl overflow-hidden">
<div className="p-5 border-b border-WHITE3 text-center">
<div className="w-20 h-20 rounded-full bg-WHITE3 grid place-items-center mx-auto mb-3 text-GRAY">
<CircleUserRound size={40} />
</div>
<p className="font-bold text-[16px]">
{profile?.username || "کاربر ویترون"}
</p>
{profile?.phoneNumber ? (
<p className="text-GRAY text-[13px] mt-1" dir="ltr">
{profile.phoneNumber}
</p>
) : null}
</div>
<nav className="p-2.5 flex flex-col gap-0.5">
{NAV.map((n) => (
<Link
key={n.to}
to={n.to}
prefetch="intent"
className={`flex items-center gap-3 px-3.5 py-3 rounded-xl text-[14.5px] font-semibold transition-colors ${
isActive(n.to)
? "bg-BLACK text-white"
: "text-BLACK2 hover:bg-WHITE2"
}`}
>
<n.icon size={20} />
{n.label}
</Link>
))}
{user?.access_token ? (
<Link
to="/logout"
className="flex items-center gap-3 px-3.5 py-3 rounded-xl text-[14.5px] font-semibold text-RED hover:bg-red-50 transition-colors"
>
<LogOut size={20} />
خروج از حساب
</Link>
) : null}
</nav>
</aside>
{/* Page content */}
<div className="min-w-0">
<Outlet />
</div>
</div>
);
}

View File

@ -13,7 +13,7 @@ export default function Sellers() {
// Loading skeleton
const SellersSkeleton = () => (
<div className="grid grid-cols-2 gap-3 px-4">
<div className="grid grid-cols-2 lg:grid-cols-5 gap-3 lg:gap-4 px-4 lg:px-0">
{Array.from({ length: 8 }).map((_, index) => (
<Skeleton key={index} className="aspect-square rounded-lg" />
))}
@ -61,8 +61,8 @@ export default function Sellers() {
return (
<div className="flex flex-col bg-WHITE pb-20">
{/* Header */}
<div className="flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
{/* Header (mobile) */}
<div className="lg:hidden flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
<ArrowRight
onClick={handleBack}
className="absolute top-5 right-5 cursor-pointer"
@ -71,7 +71,10 @@ export default function Sellers() {
</div>
{/* Content */}
<div className="flex flex-col py-6">
<div className="flex flex-col py-6 lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8">
<h1 className="hidden lg:block text-[28px] font-extrabold mb-6">
فروشگاهها
</h1>
{status === "pending" ? (
<SellersSkeleton />
) : status === "error" ? (
@ -79,11 +82,11 @@ export default function Sellers() {
) : allSellers.length === 0 ? (
<EmptyState />
) : (
<div className="grid grid-cols-2 gap-3 px-4">
<div className="grid grid-cols-2 lg:grid-cols-5 gap-3 lg:gap-4 px-4 lg:px-0">
{allSellers.map((seller, index) => (
<Link
key={seller.id || index}
to={`/seller/${seller.sellerId}`}
to={`/seller/${seller.sellerUsername || seller.sellerId}`}
className="relative aspect-square overflow-hidden rounded-lg group"
>
{/* Background Image */}
@ -98,6 +101,16 @@ export default function Sellers() {
<Store size={48} className="text-gray-400" />
</div>
)}
{seller.sellerName ? (
<>
<div className="hidden lg:block absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
<div className="hidden lg:flex absolute inset-x-0 bottom-0 p-4 items-end">
<span className="text-white font-bold text-[15px] truncate">
{seller.sellerName}
</span>
</div>
</>
) : null}
</Link>
))}
</div>