Connect the storefront to Meshkee catalog, blogs, and static image slots.
Wire homepage specials, product/blog pages, instant search, and the customer portal so the live pharmacy site can be managed from the dashboard. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
cd2f9dfc9d
commit
66e604c426
@@ -1,14 +1,23 @@
|
||||
import { Container } from "@/components/Container";
|
||||
import { Placeholder } from "@/components/Placeholder";
|
||||
import { SlotMedia } from "@/components/SlotMedia";
|
||||
import { about } from "@/lib/data";
|
||||
import type { SlotMediaItem } from "@/lib/static-images";
|
||||
|
||||
export function AboutDoctor({ image, aspectRatio }: { image?: SlotMediaItem; aspectRatio?: string }) {
|
||||
const src = image?.image ?? "/placeholders/doctor.svg";
|
||||
|
||||
export function AboutDoctor() {
|
||||
return (
|
||||
<section id="about" className="pt-10">
|
||||
<Container>
|
||||
<div className="grid items-center gap-8 overflow-hidden rounded-2xl bg-white p-5 sm:p-8 lg:grid-cols-[0.9fr_1.1fr]">
|
||||
<div className="overflow-hidden rounded-2xl bg-sky">
|
||||
<Placeholder src="/placeholders/doctor.svg" className="aspect-[4/5] w-full object-cover lg:aspect-[4/4.4]" />
|
||||
<SlotMedia
|
||||
src={src}
|
||||
href={image?.href}
|
||||
fallbackSrc={image?.fallbackSrc ?? "/placeholders/doctor.svg"}
|
||||
aspectRatio={aspectRatio}
|
||||
className={!aspectRatio ? "aspect-[4/5] lg:aspect-[4/4.4]" : ""}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[13px] font-medium text-primary">{about.subtitle}</p>
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import Link from "next/link";
|
||||
import { Container } from "@/components/Container";
|
||||
import { Placeholder } from "@/components/Placeholder";
|
||||
import { SlotMedia } from "@/components/SlotMedia";
|
||||
import type { SlotMediaItem } from "@/lib/static-images";
|
||||
|
||||
type Banner = { id: string; image: string; href: string };
|
||||
|
||||
export function BannerRow({ banners, columns }: { banners: Banner[]; columns: 2 | 3 }) {
|
||||
export function BannerRow({
|
||||
banners,
|
||||
columns,
|
||||
aspectRatio,
|
||||
}: {
|
||||
banners?: SlotMediaItem[];
|
||||
columns: 2 | 3;
|
||||
aspectRatio?: string;
|
||||
}) {
|
||||
return (
|
||||
<section className="pt-8">
|
||||
<Container>
|
||||
<div className={`grid gap-3 ${columns === 3 ? "sm:grid-cols-3" : "sm:grid-cols-2"}`}>
|
||||
{banners.map((banner) => (
|
||||
<Link key={banner.id} href={banner.href} className="overflow-hidden rounded-xl bg-sky">
|
||||
<Placeholder src={banner.image} className="h-[100px] w-full object-cover sm:h-[118px]" />
|
||||
</Link>
|
||||
{(banners ?? []).map((banner) => (
|
||||
<SlotMedia
|
||||
key={banner.id}
|
||||
src={banner.image}
|
||||
href={banner.href}
|
||||
fallbackSrc={banner.fallbackSrc}
|
||||
aspectRatio={aspectRatio}
|
||||
className={`rounded-xl bg-sky ${!aspectRatio ? "h-[100px] sm:h-[118px]" : ""}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -5,10 +5,26 @@ import Link from "next/link";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Container } from "@/components/Container";
|
||||
import { Placeholder } from "@/components/Placeholder";
|
||||
import { blogs } from "@/lib/data";
|
||||
import { blogs as sampleBlogs } from "@/lib/data";
|
||||
import { blogHref, blogImage, type BlogPost } from "@/lib/blog";
|
||||
import { formatFaDate } from "@/lib/format";
|
||||
|
||||
export function BlogCarousel() {
|
||||
export function BlogCarousel({ posts }: { posts?: BlogPost[] }) {
|
||||
const scroller = useRef<HTMLDivElement>(null);
|
||||
const list =
|
||||
posts && posts.length
|
||||
? posts.map((post) => ({
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
excerpt: post.abstract || "",
|
||||
date: formatFaDate(post.publishedAt || post.createdAt),
|
||||
image: blogImage(post),
|
||||
href: blogHref(post),
|
||||
}))
|
||||
: sampleBlogs.map((post) => ({
|
||||
...post,
|
||||
href: "/blog",
|
||||
}));
|
||||
|
||||
return (
|
||||
<section id="blog" className="pt-10">
|
||||
@@ -18,7 +34,7 @@ export function BlogCarousel() {
|
||||
<h2 className="text-[18px] font-bold text-navy">آخرین مطالب مجله سلامت</h2>
|
||||
<p className="mt-1 text-[13px] text-muted">نکتههای کاربردی از داروسازان داروخانه</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => scroller.current?.scrollBy({ left: 320, behavior: "smooth" })}
|
||||
@@ -35,18 +51,21 @@ export function BlogCarousel() {
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</button>
|
||||
<Link href="/blog" className="hidden text-[13px] text-primary sm:block">
|
||||
مشاهده همه
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={scroller} className="no-scrollbar flex snap-x gap-4 overflow-x-auto">
|
||||
{blogs.map((post) => (
|
||||
{list.map((post) => (
|
||||
<article key={post.id} className="w-[280px] shrink-0 snap-start overflow-hidden rounded-2xl bg-white sm:w-[320px]">
|
||||
<Link href="#" className="block overflow-hidden bg-sky">
|
||||
<Link href={post.href} className="block overflow-hidden bg-sky">
|
||||
<Placeholder src={post.image} className="aspect-[16/10] w-full object-cover" />
|
||||
</Link>
|
||||
<div className="p-4">
|
||||
<p className="text-[12px] text-muted">{post.date}</p>
|
||||
<h3 className="mt-2 text-[14px] font-bold leading-7 text-navy">
|
||||
<Link href="#">{post.title}</Link>
|
||||
<Link href={post.href}>{post.title}</Link>
|
||||
</h3>
|
||||
<p className="mt-2 line-clamp-2 text-[12.5px] leading-6 text-muted">{post.excerpt}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,45 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { Container } from "@/components/Container";
|
||||
import { Placeholder } from "@/components/Placeholder";
|
||||
import { SlotMedia } from "@/components/SlotMedia";
|
||||
import { categories } from "@/lib/data";
|
||||
import type { SlotMediaItem } from "@/lib/static-images";
|
||||
|
||||
function CardWrap({ href, children }: { href?: string; children: ReactNode }) {
|
||||
const className =
|
||||
"group flex flex-col items-center gap-2 rounded-2xl bg-white p-3 text-center transition hover:-translate-y-0.5";
|
||||
if (!href) return <div className={className}>{children}</div>;
|
||||
if (href.startsWith("/") || href.startsWith("#")) {
|
||||
return (
|
||||
<Link href={href} className={className}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a href={href} className={className}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function FeaturedCategories({
|
||||
items,
|
||||
aspectRatio,
|
||||
}: {
|
||||
items?: SlotMediaItem[];
|
||||
aspectRatio?: string;
|
||||
}) {
|
||||
const list: SlotMediaItem[] =
|
||||
items && items.length > 0
|
||||
? items
|
||||
: categories.map((category) => ({
|
||||
id: category.id,
|
||||
image: category.image,
|
||||
href: category.href,
|
||||
title: category.title,
|
||||
}));
|
||||
|
||||
export function FeaturedCategories() {
|
||||
return (
|
||||
<section id="categories" className="pt-10">
|
||||
<Container>
|
||||
@@ -14,19 +50,23 @@ export function FeaturedCategories() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-3 md:grid-cols-8">
|
||||
{categories.map((category) => (
|
||||
<Link
|
||||
key={category.id}
|
||||
href={category.href}
|
||||
className="group flex flex-col items-center gap-2 rounded-2xl bg-white p-3 text-center transition hover:-translate-y-0.5"
|
||||
>
|
||||
{list.map((category) => (
|
||||
<CardWrap key={category.id} href={category.href}>
|
||||
<span className="grid size-[72px] place-items-center overflow-hidden rounded-2xl bg-sky sm:size-[84px]">
|
||||
<Placeholder src={category.image} className="size-full object-cover" />
|
||||
<SlotMedia
|
||||
src={category.image}
|
||||
fallbackSrc={category.fallbackSrc}
|
||||
aspectRatio={aspectRatio}
|
||||
className="size-full overflow-hidden rounded-2xl"
|
||||
imgClassName="absolute inset-0 size-full object-cover"
|
||||
/>
|
||||
</span>
|
||||
<span className="text-[12px] font-medium text-ink group-hover:text-primary sm:text-[13px]">
|
||||
{category.title}
|
||||
</span>
|
||||
</Link>
|
||||
{category.title ? (
|
||||
<span className="text-[12px] font-medium text-ink group-hover:text-primary sm:text-[13px]">
|
||||
{category.title}
|
||||
</span>
|
||||
) : null}
|
||||
</CardWrap>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
+38
-24
@@ -1,15 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Container } from "@/components/Container";
|
||||
import { Placeholder } from "@/components/Placeholder";
|
||||
import { heroBanners } from "@/lib/data";
|
||||
import type { Slide } from "@/lib/media";
|
||||
import { SlotMedia } from "@/components/SlotMedia";
|
||||
import { parseAspectRatio } from "@/lib/static-images";
|
||||
import type { SlotMediaItem } from "@/lib/static-images";
|
||||
|
||||
export function Hero({ slides }: { slides: Slide[] }) {
|
||||
type HeroProps = {
|
||||
slides: SlotMediaItem[];
|
||||
banners: SlotMediaItem[];
|
||||
sliderAspectRatio?: string;
|
||||
bannerAspectRatio?: string;
|
||||
};
|
||||
|
||||
export function Hero({ slides = [], banners = [], sliderAspectRatio, bannerAspectRatio }: HeroProps) {
|
||||
const [index, setIndex] = useState(0);
|
||||
const sliderRatio = parseAspectRatio(sliderAspectRatio);
|
||||
|
||||
useEffect(() => {
|
||||
if (slides.length <= 1) return;
|
||||
@@ -27,24 +34,26 @@ export function Hero({ slides }: { slides: Slide[] }) {
|
||||
return (
|
||||
<section className="pt-5">
|
||||
<Container>
|
||||
<div className="relative overflow-hidden rounded-2xl bg-sky">
|
||||
<div className="relative aspect-[16/6] sm:aspect-[16/5.2]">
|
||||
{slides.map((slide, i) => (
|
||||
<Link
|
||||
key={slide.id}
|
||||
href={slide.href}
|
||||
className={`absolute inset-0 transition-opacity duration-500 ${i === index ? "opacity-100" : "pointer-events-none opacity-0"}`}
|
||||
>
|
||||
<Placeholder src={slide.image} className="h-full w-full object-cover" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-2xl bg-sky ${sliderRatio ? "" : "aspect-[16/6] sm:aspect-[16/5.2]"}`}
|
||||
style={sliderRatio ? { aspectRatio: sliderRatio } : undefined}
|
||||
>
|
||||
{slides.map((slide, i) => (
|
||||
<SlotMedia
|
||||
key={slide.id}
|
||||
fill
|
||||
src={slide.image}
|
||||
href={slide.href}
|
||||
fallbackSrc={slide.fallbackSrc}
|
||||
className={`transition-opacity duration-500 ${i === index ? "opacity-100" : "pointer-events-none opacity-0"}`}
|
||||
/>
|
||||
))}
|
||||
{slides.length > 1 ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => go(-1)}
|
||||
className="absolute top-1/2 right-3 grid size-9 -translate-y-1/2 place-items-center rounded-full bg-white/90 text-navy"
|
||||
className="absolute top-1/2 right-3 z-10 grid size-9 -translate-y-1/2 place-items-center rounded-full bg-white/90 text-navy"
|
||||
aria-label="اسلاید قبلی"
|
||||
>
|
||||
<ChevronRight className="size-5" />
|
||||
@@ -52,12 +61,12 @@ export function Hero({ slides }: { slides: Slide[] }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => go(1)}
|
||||
className="absolute top-1/2 left-3 grid size-9 -translate-y-1/2 place-items-center rounded-full bg-white/90 text-navy"
|
||||
className="absolute top-1/2 left-3 z-10 grid size-9 -translate-y-1/2 place-items-center rounded-full bg-white/90 text-navy"
|
||||
aria-label="اسلاید بعدی"
|
||||
>
|
||||
<ChevronLeft className="size-5" />
|
||||
</button>
|
||||
<div className="absolute bottom-3 left-1/2 flex -translate-x-1/2 gap-1.5">
|
||||
<div className="absolute bottom-3 left-1/2 z-10 flex -translate-x-1/2 gap-1.5">
|
||||
{slides.map((slide, i) => (
|
||||
<button
|
||||
key={slide.id}
|
||||
@@ -73,10 +82,15 @@ export function Hero({ slides }: { slides: Slide[] }) {
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{heroBanners.map((banner) => (
|
||||
<Link key={banner.id} href={banner.href} className="overflow-hidden rounded-xl bg-sky">
|
||||
<Placeholder src={banner.image} className="h-[92px] w-full object-cover sm:h-[110px]" />
|
||||
</Link>
|
||||
{banners.map((banner) => (
|
||||
<SlotMedia
|
||||
key={banner.id}
|
||||
src={banner.image}
|
||||
href={banner.href}
|
||||
fallbackSrc={banner.fallbackSrc}
|
||||
aspectRatio={bannerAspectRatio}
|
||||
className={`overflow-hidden rounded-xl bg-sky ${!bannerAspectRatio ? "h-[92px] sm:h-[110px]" : ""}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -20,7 +20,7 @@ export function ProductCarousel({
|
||||
id,
|
||||
title,
|
||||
subtitle,
|
||||
href = "#",
|
||||
href = "/products",
|
||||
products,
|
||||
accent = "default",
|
||||
}: Props) {
|
||||
|
||||
Reference in New Issue
Block a user