Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd266c057c | |||
| 2adaac8ec4 | |||
| 2c9b43c84a | |||
| 36763aace0 | |||
| 35beea16bd | |||
| 979281bfb1 | |||
| fcf89eb2a1 | |||
| ea7745179b | |||
| 9a58990cae | |||
| 7bb9240f21 |
@@ -12,7 +12,7 @@ const Button: FC<Props> = ({ variant = "primary", className, children, ...rest }
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex h-12 items-center justify-center rounded-full px-6 text-sm transition-colors disabled:pointer-events-none disabled:opacity-50",
|
||||
"inline-flex h-10 items-center justify-center rounded-full px-6 text-sm transition-colors disabled:pointer-events-none disabled:opacity-50",
|
||||
variant === "primary" && "bg-primary text-white hover:bg-primary/90",
|
||||
variant === "outline" && "border border-primary bg-transparent text-primary hover:bg-primary/10",
|
||||
className,
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/app/lib/cn";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "iconsax-reactjs";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
type FC,
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
type CarouselApi = {
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
slidesPerView: number;
|
||||
gap: number;
|
||||
};
|
||||
|
||||
type ViewportRef = (node: HTMLElement | null) => void;
|
||||
|
||||
const CarouselContext = createContext<CarouselApi | null>(null);
|
||||
const CarouselViewportContext = createContext<ViewportRef | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = useContext(CarouselContext);
|
||||
if (!context) {
|
||||
throw new Error("Carousel components must be used within <Carousel>");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
type ResponsiveNumber = number | { base: number; md?: number; lg?: number };
|
||||
|
||||
function useResponsiveNumber(value: ResponsiveNumber): number {
|
||||
const base = typeof value === "number" ? value : value.base;
|
||||
const md = typeof value === "number" ? undefined : value.md;
|
||||
const lg = typeof value === "number" ? undefined : value.lg;
|
||||
|
||||
const resolve = () => {
|
||||
if (typeof window === "undefined") return base;
|
||||
if (window.matchMedia("(min-width: 1024px)").matches) return lg ?? md ?? base;
|
||||
if (window.matchMedia("(min-width: 768px)").matches) return md ?? base;
|
||||
return base;
|
||||
};
|
||||
|
||||
const [resolved, setResolved] = useState(base);
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => setResolved(resolve());
|
||||
update();
|
||||
window.addEventListener("resize", update);
|
||||
return () => window.removeEventListener("resize", update);
|
||||
}, [base, md, lg]);
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
type CarouselProps = {
|
||||
children: ReactNode;
|
||||
slidesPerView?: ResponsiveNumber;
|
||||
slidesToScroll?: number;
|
||||
gap?: ResponsiveNumber;
|
||||
direction?: "rtl" | "ltr";
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Carousel: FC<CarouselProps> = ({
|
||||
children,
|
||||
slidesPerView = 5,
|
||||
slidesToScroll,
|
||||
gap = 40,
|
||||
direction = "rtl",
|
||||
className,
|
||||
}) => {
|
||||
const resolvedSlidesPerView = useResponsiveNumber(slidesPerView);
|
||||
const resolvedGap = useResponsiveNumber(gap);
|
||||
const scrollBy = slidesToScroll ?? Math.max(1, Math.floor(resolvedSlidesPerView));
|
||||
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||
align: "start",
|
||||
direction,
|
||||
slidesToScroll: scrollBy,
|
||||
containScroll: "trimSnaps",
|
||||
});
|
||||
|
||||
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi) return;
|
||||
|
||||
const updateButtons = () => {
|
||||
setCanScrollPrev(emblaApi.canScrollPrev());
|
||||
setCanScrollNext(emblaApi.canScrollNext());
|
||||
};
|
||||
|
||||
updateButtons();
|
||||
emblaApi.on("select", updateButtons);
|
||||
emblaApi.on("reInit", updateButtons);
|
||||
|
||||
return () => {
|
||||
emblaApi.off("select", updateButtons);
|
||||
emblaApi.off("reInit", updateButtons);
|
||||
};
|
||||
}, [emblaApi]);
|
||||
|
||||
useEffect(() => {
|
||||
emblaApi?.reInit({ slidesToScroll: scrollBy });
|
||||
}, [emblaApi, scrollBy, resolvedSlidesPerView, resolvedGap]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
scrollPrev: () => emblaApi?.scrollPrev(),
|
||||
scrollNext: () => emblaApi?.scrollNext(),
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
slidesPerView: resolvedSlidesPerView,
|
||||
gap: resolvedGap,
|
||||
}}
|
||||
>
|
||||
<CarouselViewportContext.Provider value={emblaRef}>
|
||||
<div className={className}>{children}</div>
|
||||
</CarouselViewportContext.Provider>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
type CarouselControlsProps = {
|
||||
className?: string;
|
||||
prevLabel?: string;
|
||||
nextLabel?: string;
|
||||
};
|
||||
|
||||
const CarouselControls: FC<CarouselControlsProps> = ({
|
||||
className,
|
||||
prevLabel = "قبلی",
|
||||
nextLabel = "بعدی",
|
||||
}) => {
|
||||
const { scrollPrev, scrollNext, canScrollPrev, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<div className={cn("flex gap-2", className)}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={prevLabel}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
className="size-8 bg-primary rounded-full flex justify-center items-center disabled:opacity-40 disabled:cursor-not-allowed transition-opacity"
|
||||
>
|
||||
<ArrowRight size={20} color="white" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={nextLabel}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
className="size-8 bg-primary rounded-full flex justify-center items-center disabled:opacity-40 disabled:cursor-not-allowed transition-opacity"
|
||||
>
|
||||
<ArrowLeft size={20} color="white" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type CarouselTrackProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const CarouselTrack: FC<CarouselTrackProps> = ({ children, className }) => {
|
||||
const emblaRef = useContext(CarouselViewportContext);
|
||||
const { gap } = useCarousel();
|
||||
|
||||
if (!emblaRef) {
|
||||
throw new Error("CarouselTrack must be used within <Carousel>");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("overflow-hidden", className)} ref={emblaRef}>
|
||||
<div className="flex touch-pan-y" style={{ gap }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type CarouselSlideProps = HTMLAttributes<HTMLDivElement> & {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const CarouselSlide: FC<CarouselSlideProps> = ({ children, className, style, ...rest }) => {
|
||||
const { slidesPerView, gap } = useCarousel();
|
||||
const totalGap = (slidesPerView - 1) * gap;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("min-w-0 shrink-0 grow-0", className)}
|
||||
style={{
|
||||
flexBasis: `calc((100% - ${totalGap}px) / ${slidesPerView})`,
|
||||
...style,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Carousel, CarouselControls, CarouselSlide, CarouselTrack };
|
||||
export default Carousel;
|
||||
@@ -18,7 +18,7 @@ const Input: FC<Props> = (props) => {
|
||||
<input
|
||||
type={variant === "search" ? "search" : "text"}
|
||||
className={cn(
|
||||
"h-12 w-full rounded-2xl border border-secondary bg-white px-5 text-right text-sm text-[#3A4147] placeholder:text-[#D7E0E8] transition-colors focus:border-primary",
|
||||
"h-10 w-full rounded-2xl border border-secondary bg-white px-5 text-right text-sm text-[#3A4147] placeholder:text-[#D7E0E8] transition-colors focus:border-primary",
|
||||
variant === "search" && "pr-13",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { cn } from "@/app/lib/cn";
|
||||
import { ArrowDown2 } from "iconsax-reactjs";
|
||||
import { FC } from "react";
|
||||
|
||||
export type SelectOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
options?: SelectOption[];
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
} & Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "placeholder">;
|
||||
|
||||
const Select: FC<Props> = ({ options, className, placeholder, children, ...rest }) => {
|
||||
return (
|
||||
<div className="relative w-full" dir="rtl">
|
||||
<select
|
||||
className={cn(
|
||||
"h-10 w-full appearance-none rounded-2xl border border-secondary bg-white px-4 pl-13 text-right text-sm transition-colors focus:border-primary disabled:pointer-events-none disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{placeholder && (
|
||||
<option value="" disabled hidden>
|
||||
{placeholder}
|
||||
</option>
|
||||
)}
|
||||
{options?.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
{children}
|
||||
</select>
|
||||
<ArrowDown2 aria-hidden="true" className="pointer-events-none absolute top-1/2 left-4 -translate-y-1/2 text-black" color="currentColor" size={18} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
||||
@@ -1,5 +1,11 @@
|
||||
import BannerSection from "./components/BannerSection";
|
||||
import BestSellerCategories from "./components/BestSellerCategories";
|
||||
import BlogsSection from "./components/BlogsSection";
|
||||
import CategorySection from "./components/CategorySection";
|
||||
import ContactCtaSection from "./components/ContactCtaSection";
|
||||
import OrderByApplication from "./components/OrderByApplication";
|
||||
import OrderRegistrationSteps from "./components/OrderRegistrationSteps";
|
||||
import ProductSection from "./components/ProductSection";
|
||||
import StepSection from "./components/StepSection";
|
||||
|
||||
const Home = () => {
|
||||
@@ -8,6 +14,23 @@ const Home = () => {
|
||||
<BannerSection />
|
||||
<StepSection />
|
||||
<CategorySection />
|
||||
<ProductSection />
|
||||
<OrderByApplication />
|
||||
<BestSellerCategories />
|
||||
<OrderRegistrationSteps />
|
||||
<BlogsSection />
|
||||
|
||||
<div className="mt-12 px-4 sm:mt-16 sm:px-8 lg:mt-[120px] lg:px-[120px]">
|
||||
<div className="bg-[#004A9005] p-6 sm:p-10 lg:p-16 w-full rounded-3xl lg:rounded-4xl border border-[#E9EEF2]">
|
||||
<div className="font-bold text-lg sm:text-xl lg:text-2xl">چاپ جعبه، بستهبندی و محصولات تبلیغاتی با طراحی اختصاصی</div>
|
||||
<div className="mt-4 sm:mt-6 leading-6 font-bold text-[#194873] text-sm">
|
||||
جعبه، اولین چیزی است که مشتری از برند شما میبیند. ما با ارائه خدمات چاپ جعبه، لیبل، استیکر، کارت ویزیت و سایر محصولات تبلیغاتی، به شما کمک میکنیم تا بستهبندی حرفهای و ماندگاری برای
|
||||
محصولات خود داشته باشید. با استفاده از متریال باکیفیت، طراحی اختصاصی و چاپ دقیق، سفارش شما در کوتاهترین زمان آماده و ارسال میشود.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ContactCtaSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,20 +8,26 @@ const BannerSection: FC = () => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="relative">
|
||||
<Image src={banner} alt="banner" width={2000} height={574} className="w-full max-h-[574px] object-cover" />
|
||||
<div className="absolute h-full right-[120px] top-0 bottom-0 m-auto flex flex-col justify-center">
|
||||
<div className="text-base">چاپ و بسته بندی به روش هوشمندانه</div>
|
||||
<div className="text-2xl font-black mt-4">مجــتمع تخصصــــــی</div>
|
||||
<div className="text-2xl font-black mt-4">
|
||||
<Image
|
||||
src={banner}
|
||||
alt="banner"
|
||||
width={2000}
|
||||
height={574}
|
||||
className="w-full h-[420px] sm:h-[480px] lg:h-auto lg:max-h-[574px] object-cover"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-4 sm:right-8 lg:right-[120px] flex flex-col justify-center max-w-[min(100%,420px)] lg:max-w-none">
|
||||
<div className="text-sm sm:text-base">چاپ و بسته بندی به روش هوشمندانه</div>
|
||||
<div className="text-xl sm:text-2xl font-black mt-3 sm:mt-4">مجــتمع تخصصــــــی</div>
|
||||
<div className="text-xl sm:text-2xl font-black mt-3 sm:mt-4">
|
||||
چــــــاپ و بستــــه بندی <span className="text-primary">دستی</span> و <span className="text-primary">هوشمند</span>{" "}
|
||||
</div>
|
||||
<div className="mt-4 text-base flex flex-col gap-3">
|
||||
<div className="mt-3 sm:mt-4 text-sm sm:text-base flex flex-col gap-2 sm:gap-3">
|
||||
<div>قیمتهای عالی</div>
|
||||
<div>خدمات مشتریان بینظیر</div>
|
||||
<div>سیستم سفارشگیری ساده</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex gap-6 items-center">
|
||||
<div className="mt-4 flex flex-wrap gap-3 sm:gap-6 items-center">
|
||||
<Button>
|
||||
<div className="flex gap-2.5">
|
||||
<div>محصولات</div>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { cn } from "@/app/lib/cn";
|
||||
import bannersImage from "@/assets/images/best-sellers/banners.jpg";
|
||||
import boxesImage from "@/assets/images/best-sellers/boxes.jpg";
|
||||
import businessCardsImage from "@/assets/images/best-sellers/business-cards.jpg";
|
||||
import calendarsImage from "@/assets/images/best-sellers/calendars.jpg";
|
||||
import catalogsImage from "@/assets/images/best-sellers/catalogs.png";
|
||||
import { ArrowLeft } from "iconsax-reactjs";
|
||||
import Image, { type StaticImageData } from "next/image";
|
||||
import { type FC } from "react";
|
||||
|
||||
type CategoryItem = {
|
||||
title: string;
|
||||
image: StaticImageData;
|
||||
featured?: boolean;
|
||||
};
|
||||
|
||||
const categories: CategoryItem[] = [
|
||||
{ title: "جعبه ها", image: boxesImage, featured: true },
|
||||
{ title: "کاتالوگ ها", image: catalogsImage },
|
||||
{ title: "کارت ویزیت", image: businessCardsImage },
|
||||
{ title: "تقویم ها", image: calendarsImage },
|
||||
{ title: "بنرها", image: bannersImage },
|
||||
];
|
||||
|
||||
type CategoryTileProps = {
|
||||
title: string;
|
||||
image: StaticImageData;
|
||||
featured?: boolean;
|
||||
};
|
||||
|
||||
const CategoryTile: FC<CategoryTileProps> = ({ title, image, featured }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-[24px] sm:rounded-[40px]",
|
||||
featured ? "col-span-2 row-span-2 min-h-[220px] sm:min-h-0" : "aspect-square",
|
||||
)}
|
||||
>
|
||||
<Image src={image} alt={title} fill className="object-cover" sizes={featured ? "(max-width: 768px) 100vw, 50vw" : "(max-width: 768px) 50vw, 25vw"} />
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-x-3 bottom-3 sm:inset-x-4 sm:bottom-4 flex items-center justify-between rounded-2xl sm:rounded-3xl bg-[#0A1B2C]/60 backdrop-blur-[2px]",
|
||||
featured ? "px-4 py-3 sm:px-6 sm:py-3.5" : "px-3 py-2.5 sm:px-5 sm:py-3",
|
||||
)}
|
||||
>
|
||||
<div className={cn("text-white font-bold", featured ? "text-base sm:text-lg" : "text-sm sm:text-base")}>{title}</div>
|
||||
<div className="flex items-center gap-1.5 text-white">
|
||||
<span className={cn("font-bold", featured ? "text-xs sm:text-sm" : "text-xs")}>مشاهده</span>
|
||||
<ArrowLeft size={featured ? 20 : 18} color="currentColor" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BestSellerCategories: FC = () => {
|
||||
return (
|
||||
<div className="mt-12 px-4 sm:mt-16 sm:px-8 lg:mt-[120px] lg:px-[120px]">
|
||||
<div className="font-bold text-lg sm:text-2xl">دسته بندی های پرفروش</div>
|
||||
|
||||
<div className="mt-8 sm:mt-12 grid grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 lg:gap-10">
|
||||
{categories.map((category) => (
|
||||
<CategoryTile key={category.title} title={category.title} image={category.image} featured={category.featured} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BestSellerCategories;
|
||||
@@ -0,0 +1,21 @@
|
||||
import BlogImage from "@/assets/images/blog-image.png";
|
||||
import { ArrowLeft } from "iconsax-reactjs";
|
||||
import Image from "next/image";
|
||||
import { FC } from "react";
|
||||
|
||||
const BlogCard: FC = () => {
|
||||
return (
|
||||
<div className="bg-white shadow-[0_4px_20px_rgba(0,0,0,0.1)] p-4 rounded-3xl">
|
||||
<Image src={BlogImage} alt="Blog Image" width={1000} height={1000} className="w-full h-auto rounded-2xl" />
|
||||
<div className="mt-4 text-[13px] text-[#A7ADB3] font-bold">۰۴ تیر ۱۴۰۵</div>
|
||||
<div className="mt-2 font-bold">راهنمای انتخاب بنر مناسب</div>
|
||||
<p className="mt-2 text-sm text-[#67727C]">با انواع بنر، کاربردها و نکات مهم انتخاب آشنا شوید تا بهترین گزینه را متناسب با نیاز و بودجه خود انتخاب کنید.</p>
|
||||
<div className="mt-2 flex items-center gap-2 text-primary">
|
||||
<div className="text-xs font-bold">مشاهده</div>
|
||||
<ArrowLeft size={16} color="currentColor" className="mt-px" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogCard;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { FC } from "react";
|
||||
import BlogCard from "./BlogCard";
|
||||
|
||||
const BlogsSection: FC = () => {
|
||||
return (
|
||||
<div className="mt-12 px-4 sm:mt-16 sm:px-8 lg:mt-[120px] lg:px-[120px]">
|
||||
<div className="text-lg sm:text-2xl font-bold">مقالات ما</div>
|
||||
<div className="mt-8 sm:mt-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-10">
|
||||
<BlogCard />
|
||||
<BlogCard />
|
||||
<BlogCard />
|
||||
<BlogCard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogsSection;
|
||||
@@ -9,9 +9,9 @@ type CategoryCardProps = {
|
||||
|
||||
const CategoryCard: FC<CategoryCardProps> = ({ title }) => {
|
||||
return (
|
||||
<div className="w-full bg-[#D4E3F154]/33 rounded-[40px] p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-[#21588C] font-bold">{title}</div>
|
||||
<div className="w-full bg-[#D4E3F154]/33 rounded-[24px] sm:rounded-[40px] p-4 sm:p-6">
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="text-[#21588C] font-bold text-sm sm:text-base">{title}</div>
|
||||
<div className="bg-white/40 flex justify-center items-center">
|
||||
<ArrowLeft size={20} color="currentColor" className="text-primary rotate-45" />
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, ArrowRight } from "iconsax-reactjs";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Carousel, CarouselControls, CarouselSlide, CarouselTrack } from "@/app/components/Carousel";
|
||||
import CategoryCard from "./CategoryCard";
|
||||
|
||||
const categories = [
|
||||
@@ -19,72 +15,22 @@ const categories = [
|
||||
];
|
||||
|
||||
const CategorySection = () => {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||
align: "start",
|
||||
direction: "rtl",
|
||||
slidesToScroll: 5,
|
||||
containScroll: "trimSnaps",
|
||||
});
|
||||
|
||||
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi) return;
|
||||
|
||||
const updateButtons = () => {
|
||||
setCanScrollPrev(emblaApi.canScrollPrev());
|
||||
setCanScrollNext(emblaApi.canScrollNext());
|
||||
};
|
||||
|
||||
updateButtons();
|
||||
emblaApi.on("select", updateButtons);
|
||||
emblaApi.on("reInit", updateButtons);
|
||||
|
||||
return () => {
|
||||
emblaApi.off("select", updateButtons);
|
||||
emblaApi.off("reInit", updateButtons);
|
||||
};
|
||||
}, [emblaApi]);
|
||||
|
||||
return (
|
||||
<div className="mt-[120px] px-[120px]">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="font-bold text-2xl">دسته بندی های محصولات</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="دستهبندیهای قبلی"
|
||||
disabled={!canScrollPrev}
|
||||
onClick={() => emblaApi?.scrollPrev()}
|
||||
className="size-8 bg-primary rounded-full flex justify-center items-center disabled:opacity-40 disabled:cursor-not-allowed transition-opacity"
|
||||
>
|
||||
<ArrowRight size={20} color="white" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="دستهبندیهای بعدی"
|
||||
disabled={!canScrollNext}
|
||||
onClick={() => emblaApi?.scrollNext()}
|
||||
className="size-8 bg-primary rounded-full flex justify-center items-center disabled:opacity-40 disabled:cursor-not-allowed transition-opacity"
|
||||
>
|
||||
<ArrowLeft size={20} color="white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-12 px-4 sm:mt-16 sm:px-8 lg:mt-[120px] lg:px-[120px]">
|
||||
<Carousel slidesPerView={{ base: 1.4, md: 3, lg: 5 }} gap={{ base: 16, md: 24, lg: 40 }}>
|
||||
<div className="flex justify-between items-center gap-4">
|
||||
<div className="font-bold text-lg sm:text-2xl">دسته بندی های محصولات</div>
|
||||
<CarouselControls prevLabel="دستهبندیهای قبلی" nextLabel="دستهبندیهای بعدی" />
|
||||
</div>
|
||||
|
||||
<div className="mt-12 overflow-hidden" ref={emblaRef}>
|
||||
<div className="flex touch-pan-y gap-10">
|
||||
<CarouselTrack className="mt-8 sm:mt-12">
|
||||
{categories.map((title) => (
|
||||
<div
|
||||
key={title}
|
||||
className="min-w-0 shrink-0 grow-0 basis-[calc((100%-10rem)/5)]"
|
||||
>
|
||||
<CarouselSlide key={title}>
|
||||
<CategoryCard title={title} />
|
||||
</div>
|
||||
</CarouselSlide>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CarouselTrack>
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import Button from "@/app/components/Button";
|
||||
import deliveryTruckImage from "@/assets/images/delivery-truck.png";
|
||||
import { CallCalling } from "iconsax-reactjs";
|
||||
import Image from "next/image";
|
||||
import { type FC } from "react";
|
||||
|
||||
const ContactCtaSection: FC = () => {
|
||||
return (
|
||||
<div className="mt-12 sm:mt-16 lg:mt-[120px] flex flex-col sm:flex-row items-center justify-center gap-4 sm:gap-6 bg-linear-to-l from-primary/10 to-transparent px-4 py-8 sm:py-10">
|
||||
<div className="flex items-center gap-3 text-center sm:text-right">
|
||||
<Image src={deliveryTruckImage} alt="" width={40} height={40} className="size-10 object-contain shrink-0" />
|
||||
<p className="text-base sm:text-xl font-bold text-[#0A1B2C] sm:whitespace-nowrap">
|
||||
ارسال سریع به سراسر ایران | مشاوره رایگان قبل از سفارش
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button className="gap-2 ps-5 pe-6 font-bold shrink-0">
|
||||
<CallCalling size={20} color="currentColor" variant="Linear" />
|
||||
تماس با ما
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactCtaSection;
|
||||
@@ -0,0 +1,45 @@
|
||||
import Carousel, { CarouselControls, CarouselSlide, CarouselTrack } from "@/app/components/Carousel";
|
||||
import Select from "@/app/components/Select";
|
||||
import { FC } from "react";
|
||||
import ProductCard from "./ProductCard";
|
||||
|
||||
const OrderByApplication: FC = () => {
|
||||
return (
|
||||
<div className="px-4 py-12 sm:px-8 sm:py-16 lg:p-[120px] bg-[#F1F6FA] mt-12 sm:mt-16 lg:mt-[120px]">
|
||||
<div className="text-lg sm:text-2xl font-bold">سفارش بر اساس کاربرد</div>
|
||||
<Carousel slidesPerView={{ base: 1.4, md: 3, lg: 5 }} gap={{ base: 16, md: 24, lg: 40 }}>
|
||||
<div className="flex mt-8 sm:mt-12 justify-between items-center gap-4">
|
||||
<div className="min-w-0 flex-1 max-w-[240px] sm:max-w-none sm:flex-none">
|
||||
<Select
|
||||
options={[
|
||||
{
|
||||
label: "محبوبترین محصولات",
|
||||
value: "popular",
|
||||
},
|
||||
{
|
||||
label: "جدیدترین محصولات",
|
||||
value: "newest",
|
||||
},
|
||||
{
|
||||
label: "پرفروشترین محصولات",
|
||||
value: "best_selling",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<CarouselControls prevLabel="محصولات قبلی" nextLabel="محصولات بعدی" />
|
||||
</div>
|
||||
|
||||
<CarouselTrack className="mt-8 sm:mt-12 px-2 sm:px-3 py-4 -mx-2 sm:-mx-3">
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<CarouselSlide key={index}>
|
||||
<ProductCard />
|
||||
</CarouselSlide>
|
||||
))}
|
||||
</CarouselTrack>
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderByApplication;
|
||||
@@ -0,0 +1,20 @@
|
||||
import ImageSrc from "@/assets/images/OrderRegistrationSteps.png";
|
||||
import Image from "next/image";
|
||||
import { FC } from "react";
|
||||
|
||||
const OrderRegistrationSteps: FC = () => {
|
||||
return (
|
||||
<div className="px-4 py-12 sm:px-8 sm:py-16 lg:p-[120px] bg-[#F1F6FA] mt-12 sm:mt-16 lg:mt-[120px]">
|
||||
<div className="text-lg sm:text-2xl font-bold">سفارش بر اساس کاربرد</div>
|
||||
<Image
|
||||
src={ImageSrc}
|
||||
alt="Order Registration Steps"
|
||||
width={1000}
|
||||
height={1000}
|
||||
className="w-full mt-8 sm:mt-12 h-auto object-contain"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderRegistrationSteps;
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
import Button from "@/app/components/Button";
|
||||
import ProductImage from "@/assets/images/product_image.png";
|
||||
import Image from "next/image";
|
||||
import { type FC } from "react";
|
||||
import { Rating } from "react-simple-star-rating";
|
||||
|
||||
const ProductCard: FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-xl overflow-hidden">
|
||||
<Image src={ProductImage} alt="product-card" width={500} height={500} className="w-full aspect-square object-cover" />
|
||||
</div>
|
||||
<div className="relative bg-white p-4 rounded-xl -mt-6 shadow-[0_4px_20px_rgba(0,0,0,0.1)]">
|
||||
<div className="text-[#2A3950] font-bold text-center">جعبه کیبوردی</div>
|
||||
<div className="mt-2 flex justify-center items-center gap-1.5">
|
||||
<Rating initialValue={4.5} size={20} readonly rtl allowFraction SVGstyle={{ display: "inline-block" }} />
|
||||
<div className="text-xs mt-1">4.5</div>
|
||||
</div>
|
||||
<div className="mt-2 text-[#0A1B2C8F]/56 text-sm text-center">قیمت از </div>
|
||||
<div className="flex gap-1.5 items-center justify-center">
|
||||
<div className="text-[#0A1B2C8F]/56 line-through decoration-[#D00003] text-[10px]">10,000,000</div>
|
||||
<div className="text-sm">1,000,000 تومان</div>
|
||||
</div>
|
||||
<div className="mt-3 px-4">
|
||||
<Button variant="outline" className="h-8! w-full">
|
||||
جزییات
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductCard;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Carousel, CarouselControls, CarouselSlide, CarouselTrack } from "@/app/components/Carousel";
|
||||
import { type FC } from "react";
|
||||
import ProductCard from "./ProductCard";
|
||||
|
||||
const ProductSection: FC = () => {
|
||||
return (
|
||||
<div className="mt-12 px-4 sm:mt-16 sm:px-8 lg:mt-[120px] lg:px-[120px]">
|
||||
<Carousel slidesPerView={{ base: 1.4, md: 3, lg: 5 }} gap={{ base: 16, md: 24, lg: 40 }}>
|
||||
<div className="flex justify-between items-center gap-4">
|
||||
<div className="font-bold text-lg sm:text-2xl">محبوبترین محصولات</div>
|
||||
<CarouselControls prevLabel="محصولات قبلی" nextLabel="محصولات بعدی" />
|
||||
</div>
|
||||
|
||||
<CarouselTrack className="mt-8 sm:mt-12 px-2 sm:px-3 py-4 -mx-2 sm:-mx-3">
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<CarouselSlide key={index}>
|
||||
<ProductCard />
|
||||
</CarouselSlide>
|
||||
))}
|
||||
</CarouselTrack>
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductSection;
|
||||
@@ -1,32 +1,25 @@
|
||||
import { ColorSwatch, Element4, LampCharge, Magicpen, Setting2, TruckFast } from "iconsax-reactjs";
|
||||
import { FC } from "react";
|
||||
|
||||
const steps = [
|
||||
{ label: "تحلیل نیاز برند", Icon: LampCharge },
|
||||
{ label: "طراحی و آمادهسازی فایل", Icon: Magicpen },
|
||||
{ label: "انتخاب فناوری و متریال", Icon: Element4 },
|
||||
{ label: "کنترل رنگ و نمونهگیری", Icon: ColorSwatch },
|
||||
{ label: "عملیات تکمیلی", Icon: Setting2 },
|
||||
{ label: "تحویل به موقع", Icon: TruckFast },
|
||||
];
|
||||
|
||||
const StepSection: FC = () => {
|
||||
return (
|
||||
<div className="w-full px-[120px] bg-secondary flex justify-between items-center h-[80px]">
|
||||
<div className="flex gap-4 3tems-center">
|
||||
<LampCharge variant="Bulk" size={24} color="currentColor" className="text-primary" />
|
||||
<div className="font-bold text-sm">تحلیل نیاز برند</div>
|
||||
<div className="w-full px-4 sm:px-8 lg:px-[120px] bg-secondary">
|
||||
<div className="flex gap-6 lg:gap-4 lg:justify-between items-center h-auto py-4 lg:py-0 lg:h-[80px] overflow-x-auto scrollbar-none [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
|
||||
{steps.map(({ label, Icon }) => (
|
||||
<div key={label} className="flex gap-3 items-center shrink-0">
|
||||
<Icon variant="Bulk" size={24} color="currentColor" className="text-primary" />
|
||||
<div className="font-bold text-sm whitespace-nowrap">{label}</div>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center">
|
||||
<Magicpen variant="Bulk" size={24} color="currentColor" className="text-primary" />
|
||||
<div className="font-bold text-sm">طراحی و آمادهسازی فایل</div>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center">
|
||||
<Element4 variant="Bulk" size={24} color="currentColor" className="text-primary" />
|
||||
<div className="font-bold text-sm">انتخاب فناوری و متریال</div>
|
||||
</div>
|
||||
<div className="flex gap-4 i3ems-center">
|
||||
<ColorSwatch variant="Bulk" size={24} color="currentColor" className="text-primary" />
|
||||
<div className="font-bold text-sm">کنترل رنگ و نمونهگیری</div>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center">
|
||||
<Setting2 variant="Bulk" size={24} color="currentColor" className="text-primary" />
|
||||
<div className="font-bold text-sm">عملیات تکمیلی</div>
|
||||
</div>
|
||||
<div className="flex gap-43items-center">
|
||||
<TruckFast variant="Bulk" size={24} color="currentColor" className="text-primary" />
|
||||
<div className="font-bold text-sm">تحویل به موقع</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import "@/assets/iranyekan/fonts.css";
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Footer from "./shared/Footer";
|
||||
import Header from "./shared/Header";
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -25,10 +26,11 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
|
||||
<body className="min-h-full flex flex-col">
|
||||
<html lang="fa" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
|
||||
<body className="flex min-h-full flex-col">
|
||||
<Header />
|
||||
{children}
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import Seprator from "@/app/components/Seprator";
|
||||
import { type FC } from "react";
|
||||
import FooterContact from "./components/FooterContact";
|
||||
import FooterLinkColumn from "./components/FooterLinkColumn";
|
||||
import FooterNewsletter from "./components/FooterNewsletter";
|
||||
import FooterSocial from "./components/FooterSocial";
|
||||
import FooterTrustBadges from "./components/FooterTrustBadges";
|
||||
import FooterTrustValues from "./components/FooterTrustValues";
|
||||
|
||||
const guideLinks = ["سوالات متداول", "راهنمای ثبت سفارش", "راهنمای طراحی فایل", "روشهای ارسال", "شیوههای پرداخت", "استعلام قیمت"];
|
||||
const serviceLinks = ["درباره ما", "تماس با ما", "قوانین و مقررات", "حریم خصوصی", "شرایط ثبت سفارش", "فرصتهای همکاری"];
|
||||
const infoLinks = ["پیگیری سفارش", "باشگاه مشتریان", "بلاگ و مقالات", "پیشنهادهای ویژه", "همکاری با ما", "نقشه سایت"];
|
||||
const categoryLinks = ["چاپ جعبه و بستهبندی", "جعبه هاردباکس", "چاپ لیبل و برچسب", "چاپ استیکر", "چاپ ساک دستی", "چاپ کارت ویزیت", "چاپ تراکت و بروشور"];
|
||||
|
||||
const Footer: FC = () => {
|
||||
return (
|
||||
<footer className="mt-auto border-t border-[#E9EEF2] bg-[#F7F9FB]">
|
||||
<div className="px-4 sm:px-8 lg:px-[120px] pt-8 sm:pt-12 pb-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:flex xl:justify-between gap-8 xl:gap-6">
|
||||
<FooterContact />
|
||||
<FooterLinkColumn title="راهنمای خرید" links={guideLinks} />
|
||||
<FooterLinkColumn title="خدمات مشتریان" links={serviceLinks} />
|
||||
<FooterLinkColumn title="اطلاعات" links={infoLinks} />
|
||||
<FooterLinkColumn title="دستهبندی محصولات" links={categoryLinks} />
|
||||
<FooterTrustValues />
|
||||
</div>
|
||||
|
||||
<Seprator className="my-6 sm:my-8 h-px w-full" />
|
||||
|
||||
<div className="flex flex-col lg:flex-row items-center lg:items-start justify-between gap-8">
|
||||
<FooterSocial />
|
||||
<Seprator className="hidden lg:block h-16 w-px shrink-0" />
|
||||
<FooterTrustBadges />
|
||||
<Seprator className="hidden lg:block h-16 w-px shrink-0" />
|
||||
<FooterNewsletter />
|
||||
</div>
|
||||
|
||||
<Seprator className="my-6 sm:my-8 h-px w-full" />
|
||||
|
||||
<p className="text-center text-xs text-[#6C7680] leading-5 px-2">تمامی حقوق برای سایت نیکوپکجینگ بوده و با ذکر نام لینک به منبع مجاز میباشد.</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -8,33 +8,51 @@ import HeaderMenu from "./components/HeaderMenu";
|
||||
const Header = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="h-12 bg-secondary flex items-center justify-between px-[120px]">
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className="text-primary flex items-center gap-2">
|
||||
<Truck size={24} color="currentColor" />
|
||||
<div className="text-[#3A4147] text-sm font-bold">ارسال رایگان برای خرید بالای ۱۰۰ میلیون تومان</div>
|
||||
<div className="min-h-12 bg-secondary flex items-center justify-between gap-3 px-4 sm:px-8 lg:px-[120px] py-2">
|
||||
<div className="flex gap-2 items-center min-w-0">
|
||||
<span className="text-primary flex items-center gap-2 min-w-0">
|
||||
<Truck size={20} color="currentColor" className="shrink-0 sm:size-6" />
|
||||
<div className="text-[#3A4147] text-xs sm:text-sm font-bold truncate">ارسال رایگان برای خرید بالای ۱۰۰ میلیون تومان</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex text-sm font-bold gap-2 items-center">
|
||||
<div className="hidden md:flex text-sm font-bold gap-2 items-center shrink-0">
|
||||
<span className="text-primary">20%</span>
|
||||
<span>تخفیف برای اولین سفارش خود دریافت کنید |</span>
|
||||
<span className="hidden lg:inline">تخفیف برای اولین سفارش خود دریافت کنید |</span>
|
||||
<span>
|
||||
کد تخفیف : <span className="text-primary">FIRSTORDER</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center">
|
||||
<Profile className="text-primary" size={24} color="currentColor" />
|
||||
<div>سلام حمید</div>
|
||||
<div className="flex gap-2 items-center shrink-0">
|
||||
<Profile className="text-primary" size={20} color="currentColor" />
|
||||
<div className="hidden sm:block text-sm">سلام حمید</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white h-[120px] flex gap-16 items-center border border-secondary px-[120px]">
|
||||
<Image src={logo} alt="logo" width={116} height={72} />
|
||||
<div className="flex-1">
|
||||
|
||||
<div className="bg-white flex flex-col gap-4 lg:flex-row lg:gap-16 lg:items-center lg:h-[120px] border border-secondary px-4 sm:px-8 lg:px-[120px] py-4 lg:py-0">
|
||||
<div className="flex items-center justify-between gap-4 lg:contents">
|
||||
<Image src={logo} alt="logo" width={116} height={72} className="h-12 w-auto sm:h-[72px] shrink-0" />
|
||||
|
||||
<div className="flex gap-4 items-center lg:hidden">
|
||||
<button type="button" aria-label="پیگیری سفارش" className="text-[#6C7680]">
|
||||
<TruckFast variant="Bold" color="currentColor" size={22} />
|
||||
</button>
|
||||
<button type="button" aria-label="تماس" className="text-[#6C7680]">
|
||||
<Call variant="Bold" color="currentColor" size={22} />
|
||||
</button>
|
||||
<div className="relative">
|
||||
<ShoppingCart variant="Bold" color="black" size={22} />
|
||||
<div className="absolute -top-1.5 -right-1.5 bg-primary text-white text-xs font-medium rounded-full size-4 flex items-center justify-center">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 w-full">
|
||||
<Input variant="search" placeholder="جستجوی محصول" />
|
||||
</div>
|
||||
<div className="flex gap-8 items-center">
|
||||
|
||||
<div className="hidden lg:flex gap-8 items-center shrink-0">
|
||||
<div className="flex gap-2 items-center">
|
||||
<TruckFast variant="Bold" color="black" />
|
||||
<div className="text-[#6C7680] text-sm">پیگیری سفارش</div>
|
||||
@@ -44,7 +62,7 @@ const Header = () => {
|
||||
|
||||
<div className="flex gap-2 items-center">
|
||||
<Call variant="Bold" color="black" />
|
||||
<div className="text-[#6C7680] text-sm">(30 خط) 02134782000</div>
|
||||
<div className="text-[#6C7680] text-sm whitespace-nowrap">(30 خط) 02134782000</div>
|
||||
</div>
|
||||
|
||||
<Seprator className="h-5 w-px" />
|
||||
@@ -55,6 +73,7 @@ const Header = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HeaderMenu />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -42,18 +42,19 @@ const columns: CategoryGroup[][] = [
|
||||
|
||||
const AllProductsMenu: FC = () => {
|
||||
return (
|
||||
<div className="absolute top-full right-[120px] z-50 mt-2 w-[1099px] rounded-[32px] bg-[#F9FAFB]/90 px-8 pt-8 pb-10">
|
||||
<div className="flex w-full items-start justify-between">
|
||||
<div className="absolute top-full z-50 mt-2 inset-x-4 sm:inset-x-8 lg:inset-x-auto lg:right-[120px] w-auto lg:w-[min(1099px,calc(100vw-240px))] max-h-[70vh] overflow-y-auto rounded-2xl sm:rounded-[32px] bg-[#F9FAFB]/95 backdrop-blur-sm px-4 sm:px-8 pt-6 sm:pt-8 pb-6 sm:pb-10 shadow-lg border border-[#E9EEF2]/80">
|
||||
<div className="flex w-full flex-col lg:flex-row items-start justify-between gap-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-0 lg:contents w-full lg:w-auto">
|
||||
{columns.map((groups) => (
|
||||
<div key={groups[0].title} className="flex w-[228px] shrink-0 flex-col gap-6">
|
||||
<div key={groups[0].title} className="flex w-full lg:w-[228px] shrink-0 flex-col gap-6">
|
||||
{groups.map((group) => (
|
||||
<div key={group.title} className="flex flex-col gap-2">
|
||||
<div className="flex h-10 items-center px-4 text-base font-bold text-black">{group.title}</div>
|
||||
<div className="flex h-10 items-center px-2 sm:px-4 text-sm sm:text-base font-bold text-black">{group.title}</div>
|
||||
{group.items.map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
className="flex h-6 w-full items-center px-4 text-right text-base font-medium text-[#011E3A] transition-colors hover:text-primary"
|
||||
className="flex h-6 w-full items-center px-2 sm:px-4 text-right text-sm sm:text-base font-medium text-[#011E3A] transition-colors hover:text-primary"
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
@@ -62,8 +63,9 @@ const AllProductsMenu: FC = () => {
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="relative h-[157px] w-[163px] shrink-0 overflow-hidden rounded-2xl">
|
||||
<div className="relative mx-auto lg:mx-0 h-[140px] w-[145px] sm:h-[157px] sm:w-[163px] shrink-0 overflow-hidden rounded-2xl">
|
||||
<Image src={businessCardMenu} alt="کارت ویزیت" fill className="object-cover" sizes="163px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Call, Location, MessageQuestion, Sms } from "iconsax-reactjs";
|
||||
import { type FC, type ReactNode } from "react";
|
||||
|
||||
type ContactItemProps = {
|
||||
icon: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const ContactItem: FC<ContactItemProps> = ({ icon, children }) => {
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg border border-primary text-primary">{icon}</div>
|
||||
<div className="text-sm leading-6 text-[#3A4147]">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FooterContact: FC = () => {
|
||||
return (
|
||||
<div className="flex max-w-none sm:max-w-[280px] flex-col gap-5">
|
||||
<ContactItem icon={<Location size={18} color="currentColor" variant="Linear" />}>
|
||||
جاده خاوران، شریف آباد، شهرک صنعتی شنزار، بعد از میدان چهارم، خیابان بوستان پنجم، خیابان گلبهار، پلاک ۱۲۲
|
||||
</ContactItem>
|
||||
|
||||
<ContactItem icon={<Call size={18} color="currentColor" variant="Linear" />}>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span>۰۲۱-۳۶۹۱۱۵۹۹ | ۰۲۱-۳۶۹۱۰۷۹۹</span>
|
||||
<span>۰۲۱۳۴۷۸۲۰۰۰ (۳۰ خط)</span>
|
||||
</div>
|
||||
</ContactItem>
|
||||
|
||||
<ContactItem icon={<Sms size={18} color="currentColor" variant="Linear" />}>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="font-bold text-[#0A1B2C]">پاسخگوی سوالات شما</span>
|
||||
<a href="mailto:info@example.com" className="hover:text-primary" dir="ltr">
|
||||
info@example.com
|
||||
</a>
|
||||
</div>
|
||||
</ContactItem>
|
||||
|
||||
<ContactItem icon={<MessageQuestion size={18} color="currentColor" variant="Linear" />}>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span>نیاز به راهنمایی دارید؟</span>
|
||||
<a href="#" className="font-bold text-[#0A1B2C] hover:text-primary">
|
||||
از طریق سوالات متداول پاسخ خود را پیدا کنید.
|
||||
</a>
|
||||
</div>
|
||||
</ContactItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterContact;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { type FC } from "react";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
links: string[];
|
||||
};
|
||||
|
||||
const FooterLinkColumn: FC<Props> = ({ title, links }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-sm font-bold text-[#0A1B2C]">{title}</h3>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
{links.map((link) => (
|
||||
<li key={link}>
|
||||
<a href="#" className="text-sm text-[#3A4147] transition-colors hover:text-primary">
|
||||
{link}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterLinkColumn;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ArrowLeft } from "iconsax-reactjs";
|
||||
import { type FC } from "react";
|
||||
|
||||
const FooterNewsletter: FC = () => {
|
||||
return (
|
||||
<div className="flex w-full max-w-[360px] flex-col gap-4 lg:ms-auto">
|
||||
<p className="text-sm leading-6 font-bold text-[#0A1B2C]">برای دریافت آخرین پیشنهادها، اخبار و مطالب الهامبخش، در خبرنامه ما عضو شوید.</p>
|
||||
<form className="relative" action="#">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="ایمیل شما"
|
||||
className="h-12 w-full rounded-full border border-[#E9EEF2] bg-[#F1F6FA] ps-5 pe-14 text-sm text-[#3A4147] placeholder:text-[#9AA6B2]"
|
||||
/>
|
||||
<button type="submit" aria-label="عضویت در خبرنامه" className="absolute top-1/2 left-1.5 flex size-9 -translate-y-1/2 items-center justify-center rounded-full bg-[#0A1B2C] text-white transition-colors hover:bg-[#0A1B2C]/90">
|
||||
<ArrowLeft size={18} color="currentColor" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterNewsletter;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { type FC, type ReactNode } from "react";
|
||||
|
||||
type SocialLink = {
|
||||
label: string;
|
||||
href: string;
|
||||
bg: string;
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
const socialLinks: SocialLink[] = [
|
||||
{
|
||||
label: "Facebook",
|
||||
href: "#",
|
||||
bg: "bg-[#1877F2]",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" className="size-4 fill-white" aria-hidden>
|
||||
<path d="M14 8.5h2.5V5.2C16.1 5.1 15 5 13.8 5 11.4 5 9.8 6.5 9.8 9.2V12H7v3.5h2.8V23h3.5v-7.5H16l.5-3.5h-3.2V9.4c0-1 .3-1.9 1.7-1.9z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Instagram",
|
||||
href: "#",
|
||||
bg: "bg-linear-to-br from-[#F58529] via-[#DD2A7B] to-[#8134AF]",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" className="size-4 fill-white" aria-hidden>
|
||||
<path d="M12 7.2A4.8 4.8 0 1 0 12 16.8 4.8 4.8 0 1 0 12 7.2zm0 7.9a3.1 3.1 0 1 1 0-6.2 3.1 3.1 0 0 1 0 6.2zm6.3-8.1a1.1 1.1 0 1 1-2.2 0 1.1 1.1 0 0 1 2.2 0zM12 3.5c-2.3 0-2.6 0-3.5.1-2.3.1-3.4 1.2-3.5 3.5-.1.9-.1 1.2-.1 3.5s0 2.6.1 3.5c.1 2.3 1.2 3.4 3.5 3.5.9.1 1.2.1 3.5.1s2.6 0 3.5-.1c2.3-.1 3.4-1.2 3.5-3.5.1-.9.1-1.2.1-3.5s0-2.6-.1-3.5c-.1-2.3-1.2-3.4-3.5-3.5-.9-.1-1.2-.1-3.5-.1zm0 1.5c2.3 0 2.5 0 3.4.1 1.8.1 2.6.9 2.7 2.7.1.9.1 1.1.1 3.4s0 2.5-.1 3.4c-.1 1.8-.9 2.6-2.7 2.7-.9.1-1.1.1-3.4.1s-2.5 0-3.4-.1c-1.8-.1-2.6-.9-2.7-2.7-.1-.9-.1-1.1-.1-3.4s0-2.5.1-3.4c.1-1.8.9-2.6 2.7-2.7.9-.1 1.1-.1 3.4-.1z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "LinkedIn",
|
||||
href: "#",
|
||||
bg: "bg-[#0A66C2]",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" className="size-4 fill-white" aria-hidden>
|
||||
<path d="M6.5 9.5H3.8V20h2.7V9.5zM5.1 4a1.6 1.6 0 1 0 0 3.2 1.6 1.6 0 0 0 0-3.2zM20.2 20h-2.7v-5.5c0-1.3 0-3-1.8-3s-2.1 1.4-2.1 2.9V20H11V9.5h2.6v1.4h.1c.4-.7 1.3-1.5 2.7-1.5 2.9 0 3.4 1.9 3.4 4.4V20z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Telegram",
|
||||
href: "#",
|
||||
bg: "bg-[#2AABEE]",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" className="size-4 fill-white" aria-hidden>
|
||||
<path d="M9.8 15.3 9.5 19c.4 0 .6-.2.8-.4l2-1.9 4.1 3c.8.4 1.3.2 1.5-.7l2.7-12.7c.2-1.1-.4-1.5-1.2-1.2L4.2 10.3c-1 .3-1 .9-.2 1.2l4.4 1.4 10.2-6.4c.5-.3.9-.1.5.2L9.8 15.3z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "WhatsApp",
|
||||
href: "#",
|
||||
bg: "bg-[#25D366]",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" className="size-4 fill-white" aria-hidden>
|
||||
<path d="M12 3.5A8.4 8.4 0 0 0 5.2 16.3L4 20l3.8-1.2A8.4 8.4 0 1 0 12 3.5zm4.9 12c-.2.6-1.2 1.1-1.7 1.1-.4 0-.9.2-3-.8-2.5-1.2-4.1-4-4.2-4.2-.1-.2-1-1.4-1-2.6s.6-1.9.9-2.1c.2-.2.5-.3.7-.3h.5c.2 0 .4 0 .5.4l.8 1.9c.1.2 0 .4-.1.5l-.3.4c-.1.2-.3.3-.1.6.2.3.7 1.2 1.6 1.9 1.1.9 2 1.2 2.3 1.3.3.1.5.1.7-.1l.6-.7c.2-.2.4-.2.6-.1l1.8.8c.2.1.4.2.4.4 0 .2 0 1.2-.5 1.8z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const FooterSocial: FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 items-center lg:items-start text-center lg:text-right">
|
||||
<h3 className="text-sm font-bold text-[#0A1B2C]">ما را در شبکههای اجتماعی دنبال کنید</h3>
|
||||
<div className="flex items-center gap-3 flex-wrap justify-center lg:justify-start">
|
||||
{socialLinks.map((item) => (
|
||||
<a key={item.label} href={item.href} aria-label={item.label} className={`flex size-9 items-center justify-center rounded-full ${item.bg}`}>
|
||||
{item.icon}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterSocial;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { type FC } from "react";
|
||||
|
||||
const badges = ["اینماد", "ساماندهی", "اتحادیه"];
|
||||
|
||||
const FooterTrustBadges: FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<h3 className="text-sm font-bold text-[#0A1B2C]">نماد های اعتماد</h3>
|
||||
<div className="flex items-center gap-3 sm:gap-4 flex-wrap justify-center">
|
||||
{badges.map((badge) => (
|
||||
<div key={badge} className="flex size-14 sm:size-16 items-center justify-center rounded-full border border-[#E9EEF2] bg-white text-center text-[10px] font-bold text-[#6C7680]">
|
||||
{badge}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterTrustBadges;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { TickCircle } from "iconsax-reactjs";
|
||||
import { type FC } from "react";
|
||||
|
||||
const values = ["تضمین کیفیت چاپ", "طراحی اختصاصی", "تحویل سریع", "پشتیبانی قبل و بعد از خرید", "ارسال به سراسر ایران"];
|
||||
|
||||
const FooterTrustValues: FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-sm font-bold text-[#0A1B2C]">اعتماد شما، سرمایه ما</h3>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
{values.map((value) => (
|
||||
<li key={value} className="flex items-center gap-2 text-sm text-[#3A4147]">
|
||||
<TickCircle size={18} color="currentColor" variant="Bold" className="shrink-0 text-primary" />
|
||||
{value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterTrustValues;
|
||||
@@ -36,21 +36,21 @@ const HeaderMenu: FC = () => {
|
||||
}, [isAllProductsOpen]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative h-[72px] w-full flex-1 px-[120px]">
|
||||
<div className="flex h-full items-center justify-center gap-8">
|
||||
<div ref={containerRef} className="relative h-14 sm:h-[72px] w-full flex-1 px-4 sm:px-8 lg:px-[120px]">
|
||||
<div className="flex h-full items-center gap-4 sm:gap-6 lg:gap-8 overflow-x-auto scrollbar-none [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden lg:justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className={cn("flex items-center gap-2 font-bold transition-colors", isAllProductsOpen && "text-primary")}
|
||||
className={cn("flex items-center gap-2 font-bold transition-colors shrink-0 text-sm sm:text-base", isAllProductsOpen && "text-primary")}
|
||||
onClick={() => setIsAllProductsOpen((open) => !open)}
|
||||
aria-expanded={isAllProductsOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<HamburgerMenu size={24} color="currentColor" />
|
||||
<HamburgerMenu size={22} color="currentColor" />
|
||||
<span>همه محصولات</span>
|
||||
</button>
|
||||
|
||||
{menuItems.map((item) => (
|
||||
<div key={item} className="font-bold">
|
||||
<div key={item} className="font-bold shrink-0 whitespace-nowrap text-sm sm:text-base">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 775 KiB |
|
After Width: | Height: | Size: 640 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 428 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 602 KiB |
@@ -12,7 +12,8 @@
|
||||
"iconsax-reactjs": "^0.0.8",
|
||||
"next": "16.2.10",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
"react-dom": "19.2.4",
|
||||
"react-simple-star-rating": "^5.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
@@ -5718,6 +5719,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-simple-star-rating": {
|
||||
"version": "5.1.7",
|
||||
"resolved": "https://registry.npmjs.org/react-simple-star-rating/-/react-simple-star-rating-5.1.7.tgz",
|
||||
"integrity": "sha512-NTFkW8W3uwvI82Fv7JW5i7gmDjEZKxJmj+Z9vn+BjYIXT6ILdnU9qnSUP2cWrWN/WAUlue81f9SgM4CQcenltQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"iconsax-reactjs": "^0.0.8",
|
||||
"next": "16.2.10",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
"react-dom": "19.2.4",
|
||||
"react-simple-star-rating": "^5.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
||||