Add responsive desktop layout for home page
Single responsive tree (Tailwind lg:) — mobile experience unchanged; desktop chrome is gated to the home route for now. - DesktopHeader / DesktopFooter components (visible only at lg+), wired to real routes with live cart badge and working search - MyLayout: render desktop header/footer on home, expand container and hide mobile bottom nav at lg - Home: hide mobile top bar at lg, 1320px centered container, desktop hero grid (carousel + 2 promos), larger section headers - SellerTilesGrid: 2-col mobile -> 5-col desktop with name overlay Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d75c6065e7
commit
e1c5899002
@ -8,6 +8,8 @@ import homeFilled from "app/assets/icons/navbar/home-filled.svg";
|
||||
import profileFilled from "app/assets/icons/navbar/profile-filled.svg";
|
||||
import searchFilled from "app/assets/icons/navbar/search-filled.svg";
|
||||
import { Link, useLocation, useParams } from "@remix-run/react";
|
||||
import DesktopHeader from "~/components/desktop/DesktopHeader";
|
||||
import DesktopFooter from "~/components/desktop/DesktopFooter";
|
||||
import { detectPage } from "~/utils/helpers";
|
||||
import { useRootData } from "~/hooks/use-root-data";
|
||||
import { useCartCount } from "~/requestHandler/use-cart-hooks";
|
||||
@ -25,25 +27,43 @@ import productsOutline from "~/assets/icons/navbar/products-outline.svg";
|
||||
*/
|
||||
const MyLayout = React.memo(({ children }: { children: React.ReactNode }) => {
|
||||
const location = useLocation();
|
||||
// Desktop chrome is rolled out per-page. Only routes listed here get the
|
||||
// full-width desktop layout; everything else keeps the current (mobile)
|
||||
// experience at all widths so nothing regresses while we build the rest.
|
||||
const isDesktopReady = location.pathname === "/";
|
||||
const isThread = !!location.pathname.split("threads")[1];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 lg:bg-gray-200">
|
||||
<div className="max-w-md mx-auto bg-WHITE min-h-screen shadow-lg lg:shadow-xl">
|
||||
<div className="pb-[calc(50px+env(safe-area-inset-bottom))]">
|
||||
{isDesktopReady && <DesktopHeader />}
|
||||
<div
|
||||
className={`max-w-md mx-auto bg-WHITE min-h-screen shadow-lg lg:shadow-xl ${
|
||||
isDesktopReady
|
||||
? "lg:max-w-none lg:mx-0 lg:min-h-0 lg:shadow-none"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`pb-[calc(50px+env(safe-area-inset-bottom))] ${
|
||||
isDesktopReady ? "lg:pb-0" : ""
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
{location.pathname.split("threads")[1] ? (
|
||||
<></>
|
||||
) : (
|
||||
<MobileBottomNavigation />
|
||||
)}
|
||||
{isThread ? <></> : <MobileBottomNavigation hideOnDesktop={isDesktopReady} />}
|
||||
</div>
|
||||
</div>
|
||||
{isDesktopReady && <DesktopFooter />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
MyLayout.displayName = "Layout";
|
||||
|
||||
const MobileBottomNavigation = () => {
|
||||
const MobileBottomNavigation = ({
|
||||
hideOnDesktop = false,
|
||||
}: {
|
||||
hideOnDesktop?: boolean;
|
||||
}) => {
|
||||
const { theme } = useRootData();
|
||||
const location = useLocation();
|
||||
const currentPage = useMemo(
|
||||
@ -309,7 +329,11 @@ const MobileBottomNavigation = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-[50px] z-20 bg-WHITE flex gap-[48px] px-[20px] fixed bottom-0 border-t w-full max-w-md mx-auto items-center justify-center pb-[env(safe-area-inset-bottom)] lg:left-1/2 lg:transform lg:-translate-x-1/2">
|
||||
<div
|
||||
className={`h-[50px] z-20 bg-WHITE flex gap-[48px] px-[20px] fixed bottom-0 border-t w-full max-w-md mx-auto items-center justify-center pb-[env(safe-area-inset-bottom)] lg:left-1/2 lg:transform lg:-translate-x-1/2 ${
|
||||
hideOnDesktop ? "lg:hidden" : ""
|
||||
}`}
|
||||
>
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.link}
|
||||
|
||||
@ -44,14 +44,16 @@ const SellerTilesGrid = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-4 px-4">
|
||||
{/* Grid Container - 2x2 layout */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{sellers.slice(0, 4).map((seller, index) => (
|
||||
<div className="w-full flex flex-col gap-4 px-4 lg:px-0">
|
||||
{/* 2x2 on mobile, single row of 5 on desktop */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-2 lg:gap-4">
|
||||
{sellers.slice(0, 5).map((seller, index) => (
|
||||
<Link
|
||||
key={seller.id || index}
|
||||
to={`/seller/${seller.sellerUsername}`}
|
||||
className="relative aspect-square overflow-hidden rounded-lg group"
|
||||
className={`relative aspect-square overflow-hidden rounded-lg group ${
|
||||
index === 4 ? "hidden lg:block" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Background Image */}
|
||||
{seller.imageUrl ? (
|
||||
@ -65,6 +67,18 @@ const SellerTilesGrid = ({
|
||||
<Store size={48} className="text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Desktop-only name overlay (matches the desktop design) */}
|
||||
{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>
|
||||
|
||||
100
app/components/desktop/DesktopFooter.tsx
Normal file
100
app/components/desktop/DesktopFooter.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { Instagram, Send } from "lucide-react";
|
||||
import logo from "~/assets/logo/SVG-07.svg";
|
||||
|
||||
type FooterLink = { label: string; to: string };
|
||||
type FooterCol = { heading: string; links: FooterLink[] };
|
||||
|
||||
const COLUMNS: FooterCol[] = [
|
||||
{
|
||||
heading: "خرید",
|
||||
links: [
|
||||
{ label: "تازهها", to: "/explore" },
|
||||
{ label: "تخفیفها", to: "/" },
|
||||
{ label: "کالکشنها", to: "/collections" },
|
||||
{ label: "برندها", to: "/sellers" },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "ویترون",
|
||||
links: [
|
||||
{ label: "درباره ما", to: "/profile/about" },
|
||||
{ label: "سوالات متداول", to: "/profile/faq" },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "پشتیبانی",
|
||||
links: [
|
||||
{ label: "راهنمای خرید", to: "/profile/faq" },
|
||||
{ label: "پیگیری سفارش", to: "/profile/orders" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Desktop-only footer. Hidden below the `lg` breakpoint.
|
||||
*/
|
||||
const DesktopFooter = () => {
|
||||
return (
|
||||
<footer className="hidden lg:block mt-24 border-t border-WHITE3 bg-WHITE2">
|
||||
<div className="max-w-[1320px] mx-auto px-8 pt-14 pb-8 grid grid-cols-[1.6fr_1fr_1fr_1fr] gap-10">
|
||||
<div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<img src={logo} alt="ویترون" className="w-6 h-8 object-contain" />
|
||||
<b className="text-[22px] font-extrabold text-VITROWN_BLUE">ویترون</b>
|
||||
</div>
|
||||
<p className="text-GRAY text-[13.5px] max-w-[280px] my-3 leading-8">
|
||||
ویترین آنلاین برندهای پوشاک ایران. از بهترین فروشگاهها کشف کن، دنبال
|
||||
کن و خرید کن.
|
||||
</p>
|
||||
<Link
|
||||
to="/store/create"
|
||||
className="inline-flex items-center justify-center h-[38px] px-3.5 rounded-[10px] bg-BLACK text-white text-[13.5px] font-bold hover:bg-black transition-colors"
|
||||
>
|
||||
فروشگاه خود را بسازید
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{COLUMNS.map((col) => (
|
||||
<div key={col.heading}>
|
||||
<h4 className="text-[14px] font-bold mb-3.5">{col.heading}</h4>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
{col.links.map((l) => (
|
||||
<li key={l.label + l.to}>
|
||||
<Link
|
||||
to={l.to}
|
||||
className="text-BLACK2 text-[13.5px] hover:text-BLACK transition-colors"
|
||||
>
|
||||
{l.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="max-w-[1320px] mx-auto px-8 py-5 pb-10 flex justify-between items-center border-t border-WHITE3 text-GRAY text-[12.5px]">
|
||||
<span>© ۱۴۰۴ ویترون — تمامی حقوق محفوظ است.</span>
|
||||
<div className="flex gap-2.5">
|
||||
<a
|
||||
href="#"
|
||||
aria-label="اینستاگرام"
|
||||
className="w-[38px] h-[38px] rounded-full bg-WHITE border border-GRAY3 grid place-items-center text-BLACK2 hover:text-BLACK hover:border-BLACK transition-colors"
|
||||
>
|
||||
<Instagram size={18} />
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
aria-label="تلگرام"
|
||||
className="w-[38px] h-[38px] rounded-full bg-WHITE border border-GRAY3 grid place-items-center text-BLACK2 hover:text-BLACK hover:border-BLACK transition-colors"
|
||||
>
|
||||
<Send size={18} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesktopFooter;
|
||||
115
app/components/desktop/DesktopHeader.tsx
Normal file
115
app/components/desktop/DesktopHeader.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { Link, useLocation, useNavigate } from "@remix-run/react";
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { Search, Heart, ShoppingBag, User } from "lucide-react";
|
||||
import { useRootData } from "~/hooks/use-root-data";
|
||||
import { useCartCount } from "~/requestHandler/use-cart-hooks";
|
||||
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 } = useRootData();
|
||||
const cartCount = useCartCount();
|
||||
const [q, setQ] = useState("");
|
||||
|
||||
const isActive = (to: string) =>
|
||||
to === "/" ? location.pathname === "/" : location.pathname.startsWith(to);
|
||||
|
||||
const onSearch = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
const term = q.trim();
|
||||
if (term) navigate(`/search/${encodeURIComponent(term)}`);
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<form
|
||||
onSubmit={onSearch}
|
||||
className="flex-1 max-w-[460px] mx-auto 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)}
|
||||
name="q"
|
||||
placeholder="جستجوی محصول، برند یا فروشگاه…"
|
||||
aria-label="جستجو"
|
||||
className="w-full bg-transparent outline-none text-[14.5px] text-BLACK placeholder:text-GRAY"
|
||||
/>
|
||||
</form>
|
||||
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<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 && user?.access_token ? (
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesktopHeader;
|
||||
@ -75,30 +75,68 @@ export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col pb-12">
|
||||
<HomePageTopBar />
|
||||
<div className="lg:hidden">
|
||||
<HomePageTopBar />
|
||||
</div>
|
||||
|
||||
{/* Adding padding to account for fixed header */}
|
||||
<div className="flex flex-col gap-10">
|
||||
{/* Banners Section */}
|
||||
<div className="w-full mt-2 h-[260px]">
|
||||
{isLoadingBanners ? (
|
||||
<Skeleton className="w-full h-full" />
|
||||
) : (
|
||||
<ProductImageCarousel
|
||||
images={banners?.map((banner) => banner.imageUrl || "") || []}
|
||||
hideIndex={true}
|
||||
borderRadius={0}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-10 lg:gap-14 lg:max-w-[1320px] lg:w-full lg:mx-auto lg:px-8">
|
||||
{/* Hero: mobile = full-bleed banner carousel; desktop = main + 2 promos */}
|
||||
<div className="w-full mt-2 lg:mt-7 lg:grid lg:grid-cols-3 lg:gap-4">
|
||||
<div className="h-[260px] lg:h-[408px] lg:col-span-2 lg:rounded-[22px] lg:overflow-hidden lg:shadow-md">
|
||||
{isLoadingBanners ? (
|
||||
<Skeleton className="w-full h-full" />
|
||||
) : (
|
||||
<ProductImageCarousel
|
||||
images={banners?.map((banner) => banner.imageUrl || "") || []}
|
||||
hideIndex={true}
|
||||
borderRadius={0}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop-only side promos */}
|
||||
<div className="hidden lg:grid lg:grid-rows-2 lg:gap-4">
|
||||
<Link
|
||||
to="/explore"
|
||||
className="relative block rounded-[22px] overflow-hidden shadow-sm group"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#6f7359] to-[#454935] transition-transform duration-500 group-hover:scale-105" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
|
||||
<div className="absolute inset-x-0 bottom-0 p-5 text-white">
|
||||
<small className="text-[12px] font-bold opacity-90">
|
||||
تازه رسید
|
||||
</small>
|
||||
<h3 className="text-[20px] font-extrabold mt-1 leading-snug">
|
||||
کالکشن جدید
|
||||
</h3>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="relative block rounded-[22px] overflow-hidden shadow-sm group"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#b06a3c] to-[#7c4220] transition-transform duration-500 group-hover:scale-105" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
|
||||
<div className="absolute inset-x-0 bottom-0 p-5 text-white">
|
||||
<small className="text-[12px] font-bold opacity-90">
|
||||
فروش ویژه
|
||||
</small>
|
||||
<h3 className="text-[20px] font-extrabold mt-1 leading-snug">
|
||||
تا ۵۰٪ تخفیف
|
||||
</h3>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section
|
||||
id="section1"
|
||||
className="w-full gap-2 flex flex-col max-w-[100vw]"
|
||||
className="w-full gap-2 flex flex-col max-w-[100vw] lg:max-w-none lg:gap-5"
|
||||
>
|
||||
<div className="w-full px-4">
|
||||
<p className="text-B16 font-bold">تخفیفها</p>
|
||||
<p className="text-R14">
|
||||
<div className="w-full px-4 lg:px-0">
|
||||
<p className="text-B16 lg:text-B24 font-bold">تخفیفها</p>
|
||||
<p className="text-R14 lg:text-R16 text-GRAY">
|
||||
فرصتهایی محدود برای انتخابهای هوشمندانه
|
||||
</p>
|
||||
</div>
|
||||
@ -122,12 +160,12 @@ export default function HomePage() {
|
||||
</section>
|
||||
<section
|
||||
id="section2"
|
||||
className="w-full gap-2 flex flex-col max-w-[100vw]"
|
||||
className="w-full gap-2 flex flex-col max-w-[100vw] lg:max-w-none lg:gap-5"
|
||||
>
|
||||
<div className="w-full px-4 flex items-center justify-between">
|
||||
<div className="w-full px-4 lg:px-0 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-B16 font-bold">برترین فروشگاهها</p>
|
||||
<p className="text-R14">
|
||||
<p className="text-B16 lg:text-B24 font-bold">برترین فروشگاهها</p>
|
||||
<p className="text-R14 lg:text-R16 text-GRAY">
|
||||
برگرفته از بالاترین سطح رضایت کاربران
|
||||
</p>
|
||||
</div>
|
||||
@ -152,11 +190,13 @@ export default function HomePage() {
|
||||
</section>
|
||||
<section
|
||||
id="collections"
|
||||
className="w-full gap-2 flex flex-col max-w-[100vw]"
|
||||
className="w-full gap-2 flex flex-col max-w-[100vw] lg:max-w-none lg:gap-5"
|
||||
>
|
||||
<div className="w-full px-4">
|
||||
<p className="text-B16 font-bold">کالکشنها</p>
|
||||
<p className="text-R14">مجموعههای منتخب از بهترین محصولات</p>
|
||||
<div className="w-full px-4 lg:px-0">
|
||||
<p className="text-B16 lg:text-B24 font-bold">کالکشنها</p>
|
||||
<p className="text-R14 lg:text-R16 text-GRAY">
|
||||
مجموعههای منتخب از بهترین محصولات
|
||||
</p>
|
||||
</div>
|
||||
<HorizontalCollectionsList
|
||||
collections={
|
||||
@ -176,11 +216,13 @@ export default function HomePage() {
|
||||
</section>
|
||||
<section
|
||||
id="section3"
|
||||
className="w-full gap-2 flex flex-col max-w-[100vw]"
|
||||
className="w-full gap-2 flex flex-col max-w-[100vw] lg:max-w-none lg:gap-5"
|
||||
>
|
||||
<div className="w-full px-4">
|
||||
<p className="text-B16 font-bold">برای شما</p>
|
||||
<p className="text-R14">با الهام از انتخابهای شما</p>
|
||||
<div className="w-full px-4 lg:px-0">
|
||||
<p className="text-B16 lg:text-B24 font-bold">برای شما</p>
|
||||
<p className="text-R14 lg:text-R16 text-GRAY">
|
||||
با الهام از انتخابهای شما
|
||||
</p>
|
||||
</div>
|
||||
<MondrianProductList
|
||||
products={
|
||||
|
||||
Loading…
Reference in New Issue
Block a user