create banner
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "sanihome-dev",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "dev"],
|
||||||
|
"port": 3000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "sanihome-tmp",
|
"name": "sanihome",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sanihome-tmp",
|
"name": "sanihome",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "16.3.0",
|
"next": "16.3.0",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8"
|
"react-dom": "19.2.8",
|
||||||
|
"swiper": "^14.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -631,9 +632,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -650,9 +648,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -669,9 +664,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -688,9 +680,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -707,9 +696,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -726,9 +712,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -745,9 +728,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -764,9 +744,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -783,9 +760,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -808,9 +782,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -833,9 +804,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -858,9 +826,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -883,9 +848,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -908,9 +870,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -933,9 +892,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -958,9 +914,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1228,9 +1181,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1247,9 +1197,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1266,9 +1213,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1285,9 +1229,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1526,9 +1467,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1546,9 +1484,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1566,9 +1501,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1586,9 +1518,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2139,9 +2068,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2156,9 +2082,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2173,9 +2096,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2190,9 +2110,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2207,9 +2124,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2224,9 +2138,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2241,9 +2152,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2258,9 +2166,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2275,9 +2180,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2292,9 +2194,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4979,9 +4878,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5003,9 +4899,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5027,9 +4920,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5051,9 +4941,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6372,6 +6259,25 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swiper": {
|
||||||
|
"version": "14.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swiper/-/swiper-14.1.0.tgz",
|
||||||
|
"integrity": "sha512-ppit/AqmGvThKlXy4I1/9N/gEyb9s/BDHWciWNeIxbfteFKUhPQXrYuOMKszomXKd9WiLd+/0vKdPHW+0N0mhA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "custom",
|
||||||
|
"url": "https://sponsors.nolimits4web.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nolimits4web"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "16.3.0",
|
"next": "16.3.0",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8"
|
"react-dom": "19.2.8",
|
||||||
|
"swiper": "^14.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 178 KiB |
@@ -7,29 +7,49 @@ import { SpecialSaleCarousel } from "@/components/home/SpecialSaleCarousel";
|
|||||||
import { TwoBanners } from "@/components/home/TwoBanners";
|
import { TwoBanners } from "@/components/home/TwoBanners";
|
||||||
import { TopSellingCarousel } from "@/components/home/TopSellingCarousel";
|
import { TopSellingCarousel } from "@/components/home/TopSellingCarousel";
|
||||||
import { ThreeBanners } from "@/components/home/ThreeBanners";
|
import { ThreeBanners } from "@/components/home/ThreeBanners";
|
||||||
import { SamsungCarousel } from "@/components/home/SamsungCarousel";
|
import { BrandGroupCarousels } from "@/components/home/BrandGroupCarousels";
|
||||||
import { AboutSanihome } from "@/components/home/AboutSanihome";
|
import { AboutSanihome } from "@/components/home/AboutSanihome";
|
||||||
import { BlogCarousel } from "@/components/home/BlogCarousel";
|
import { BlogCarousel } from "@/components/home/BlogCarousel";
|
||||||
import { Footer } from "@/components/home/Footer";
|
import { Footer } from "@/components/home/Footer";
|
||||||
|
import {
|
||||||
|
getBusinessInfo,
|
||||||
|
getCategories,
|
||||||
|
getCategoryGroups,
|
||||||
|
getProducts,
|
||||||
|
getBrandGroups,
|
||||||
|
getStoreSpecials,
|
||||||
|
} from "@/lib/meshkee";
|
||||||
|
|
||||||
|
export default async function Home() {
|
||||||
|
const [categoryGroups, categories, storeSpecials, productsResult, brandGroups, businessInfo] =
|
||||||
|
await Promise.all([
|
||||||
|
getCategoryGroups(),
|
||||||
|
getCategories(),
|
||||||
|
getStoreSpecials(),
|
||||||
|
getProducts({ pageSize: 12 }),
|
||||||
|
getBrandGroups(),
|
||||||
|
getBusinessInfo(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const specialSaleProducts = storeSpecials[0]?.items ?? [];
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<Header />
|
<Header categoryGroups={categoryGroups} categories={categories} />
|
||||||
<main>
|
<main>
|
||||||
<HeroSlider />
|
<HeroSlider />
|
||||||
<SpecialSaleCarousel />
|
<SpecialSaleCarousel products={specialSaleProducts} />
|
||||||
<FeatureBar />
|
<FeatureBar />
|
||||||
<FeaturedCategories />
|
<FeaturedCategories categories={categories} />
|
||||||
<TwoBanners />
|
<TwoBanners />
|
||||||
<TopSellingCarousel />
|
<TopSellingCarousel products={productsResult.items} />
|
||||||
<ThreeBanners />
|
<ThreeBanners />
|
||||||
<SamsungCarousel />
|
<BrandGroupCarousels brandGroups={brandGroups} />
|
||||||
<AboutSanihome />
|
<AboutSanihome />
|
||||||
<BlogCarousel />
|
<BlogCarousel />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer businessInfo={businessInfo} categories={categories} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Carousel } from "@/components/ui/Carousel";
|
||||||
|
import { ProductCard } from "@/components/ui/ProductCard";
|
||||||
|
import { SectionTitle } from "@/components/ui/SectionTitle";
|
||||||
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
|
import type { ProductGroup } from "@/lib/meshkee";
|
||||||
|
|
||||||
|
export function BrandGroupCarousels({ brandGroups }: { brandGroups: ProductGroup[] }) {
|
||||||
|
if (brandGroups.length === 0) {
|
||||||
|
return (
|
||||||
|
<section className="py-10 md:py-14">
|
||||||
|
<div className="mx-auto max-w-7xl px-4">
|
||||||
|
<SectionTitle title="محصولات برندها" enTitle="Brand Collections" />
|
||||||
|
<EmptyState message="هنوز محصولی برای برندها ثبت نشده است." />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{brandGroups.map((group) => (
|
||||||
|
<section key={group.id} className="py-10 md:py-14">
|
||||||
|
<div className="mx-auto max-w-7xl px-4">
|
||||||
|
<SectionTitle title={group.title} enTitle={group.title} action="مشاهده همه" />
|
||||||
|
<Carousel ariaLabel={`کاروسل ${group.title}`}>
|
||||||
|
{group.items.map((product) => (
|
||||||
|
<ProductCard
|
||||||
|
key={product.id}
|
||||||
|
name={product.name}
|
||||||
|
brand={product.brand ?? ""}
|
||||||
|
price={product.price}
|
||||||
|
image={product.image ?? "/images/placeholder.svg"}
|
||||||
|
variant="brand"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
|
import { FreeMode } from "swiper/modules";
|
||||||
|
import "swiper/css";
|
||||||
|
import "swiper/css/free-mode";
|
||||||
import { Placeholder } from "@/components/ui/Placeholder";
|
import { Placeholder } from "@/components/ui/Placeholder";
|
||||||
import { SectionTitle } from "@/components/ui/SectionTitle";
|
import { SectionTitle } from "@/components/ui/SectionTitle";
|
||||||
import { categories } from "@/data/sample";
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
|
import type { Category } from "@/lib/meshkee";
|
||||||
|
|
||||||
export function FeaturedCategories() {
|
export function FeaturedCategories({ categories }: { categories: Category[] }) {
|
||||||
return (
|
return (
|
||||||
<section className="py-10 md:py-14">
|
<section className="py-10 md:py-14">
|
||||||
<div className="mx-auto max-w-7xl px-4">
|
<div className="mx-auto max-w-7xl px-4">
|
||||||
@@ -11,29 +18,47 @@ export function FeaturedCategories() {
|
|||||||
enTitle="Featured Categories"
|
enTitle="Featured Categories"
|
||||||
action="همه دستهها"
|
action="همه دستهها"
|
||||||
/>
|
/>
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-8">
|
{categories.length === 0 ? (
|
||||||
{categories.map((cat, i) => (
|
<EmptyState message="هنوز دستهبندیای ثبت نشده است." />
|
||||||
<a
|
) : (
|
||||||
key={cat.id}
|
<Swiper
|
||||||
href="#"
|
modules={[FreeMode]}
|
||||||
className="animate-slide-in group flex flex-col items-center rounded-xl border border-border bg-surface p-3 transition-all hover:border-red hover:shadow-md"
|
dir="rtl"
|
||||||
style={{ animationDelay: `${i * 60}ms` }}
|
slidesPerView={2.3}
|
||||||
>
|
spaceBetween={12}
|
||||||
<Placeholder
|
breakpoints={{
|
||||||
src={cat.image}
|
640: { slidesPerView: 4.3, spaceBetween: 12 },
|
||||||
alt={cat.name}
|
1024: { slidesPerView: 8, spaceBetween: 12 },
|
||||||
width="72px"
|
}}
|
||||||
height="72px"
|
freeMode={{ enabled: true, momentum: true, sticky: true }}
|
||||||
label="72×72"
|
grabCursor
|
||||||
className="mb-3 rounded-full transition-colors group-hover:border-red"
|
className="!px-1 !py-2"
|
||||||
/>
|
>
|
||||||
<span className="text-center text-xs font-medium leading-relaxed md:text-sm">
|
{categories.map((cat, i) => (
|
||||||
{cat.name}
|
<SwiperSlide key={cat.id}>
|
||||||
</span>
|
<a
|
||||||
<span className="mt-1 text-[11px] text-muted">{cat.count} محصول</span>
|
href="#"
|
||||||
</a>
|
className="animate-slide-in group flex h-full flex-col items-center rounded-xl border border-border bg-surface p-3 transition-all hover:border-red hover:shadow-md"
|
||||||
))}
|
style={{ animationDelay: `${i * 60}ms` }}
|
||||||
</div>
|
>
|
||||||
|
<Placeholder
|
||||||
|
src={cat.image ?? "/images/placeholder.svg"}
|
||||||
|
alt={cat.name}
|
||||||
|
width="72px"
|
||||||
|
height="72px"
|
||||||
|
className="mb-3 rounded-full transition-colors group-hover:border-red"
|
||||||
|
/>
|
||||||
|
<span className="w-full truncate text-center text-xs font-medium leading-relaxed md:text-sm">
|
||||||
|
{cat.name}
|
||||||
|
</span>
|
||||||
|
{cat.count != null && (
|
||||||
|
<span className="mt-1 text-[11px] text-muted">{cat.count} محصول</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,31 @@
|
|||||||
export function Footer() {
|
import type { BusinessInfo, Category } from "@/lib/meshkee";
|
||||||
|
|
||||||
|
const quickLinks = ["درباره ما", "تماس با ما", "سوالات متداول", "قوانین و مقررات", "پیگیری سفارش"];
|
||||||
|
|
||||||
|
const socialLabels: Record<string, string> = {
|
||||||
|
instagram: "IG",
|
||||||
|
telegram: "TG",
|
||||||
|
whatsapp: "WA",
|
||||||
|
linkedin: "IN",
|
||||||
|
youtube: "YT",
|
||||||
|
aparat: "AP",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Footer({
|
||||||
|
businessInfo,
|
||||||
|
categories,
|
||||||
|
}: {
|
||||||
|
businessInfo: BusinessInfo | null;
|
||||||
|
categories: Category[];
|
||||||
|
}) {
|
||||||
|
const socialEntries = businessInfo
|
||||||
|
? Object.entries(businessInfo.socialMedia).filter(([, url]) => !!url)
|
||||||
|
: [];
|
||||||
|
const hasContactInfo = Boolean(
|
||||||
|
businessInfo &&
|
||||||
|
(businessInfo.addresses.length || businessInfo.phoneNumbers.length || businessInfo.emails.length),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="border-t border-red-dark/20 bg-red-dark text-white">
|
<footer className="border-t border-red-dark/20 bg-red-dark text-white">
|
||||||
<div className="mx-auto max-w-7xl px-4 py-12">
|
<div className="mx-auto max-w-7xl px-4 py-12">
|
||||||
@@ -12,60 +39,69 @@ export function Footer() {
|
|||||||
SANIHOME
|
SANIHOME
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm leading-relaxed text-white/70">
|
{businessInfo?.about && (
|
||||||
فروشگاه آنلاین لوازم خانگی با بیش از ۱۵ سال سابقه. محصولات اصل،
|
<p className="text-sm leading-relaxed text-white/70">{businessInfo.about}</p>
|
||||||
گارانتی معتبر، ارسال سریع.
|
)}
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-3 text-sm font-bold">دسترسی سریع</h4>
|
<h4 className="mb-3 text-sm font-bold">دسترسی سریع</h4>
|
||||||
<ul className="space-y-2 text-sm text-white/70">
|
<ul className="space-y-2 text-sm text-white/70">
|
||||||
{["درباره ما", "تماس با ما", "سوالات متداول", "قوانین و مقررات", "پیگیری سفارش"].map(
|
{quickLinks.map((link) => (
|
||||||
(link) => (
|
<li key={link}>
|
||||||
<li key={link}>
|
<a href="#" className="transition-colors hover:text-white">
|
||||||
<a href="#" className="transition-colors hover:text-white">
|
{link}
|
||||||
{link}
|
</a>
|
||||||
</a>
|
</li>
|
||||||
</li>
|
))}
|
||||||
),
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-3 text-sm font-bold">دستهبندیها</h4>
|
<h4 className="mb-3 text-sm font-bold">دستهبندیها</h4>
|
||||||
<ul className="space-y-2 text-sm text-white/70">
|
{categories.length === 0 ? (
|
||||||
{["یخچال و فریزر", "ماشین لباسشویی", "تلویزیون", "جاروبرقی", "لوازم آشپزخانه"].map(
|
<p className="text-sm text-white/50">هنوز دستهبندیای ثبت نشده است.</p>
|
||||||
(link) => (
|
) : (
|
||||||
<li key={link}>
|
<ul className="space-y-2 text-sm text-white/70">
|
||||||
|
{categories.slice(0, 5).map((cat) => (
|
||||||
|
<li key={cat.id}>
|
||||||
<a href="#" className="transition-colors hover:text-white">
|
<a href="#" className="transition-colors hover:text-white">
|
||||||
{link}
|
{cat.name}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
),
|
))}
|
||||||
)}
|
</ul>
|
||||||
</ul>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-3 text-sm font-bold">تماس با ما</h4>
|
<h4 className="mb-3 text-sm font-bold">تماس با ما</h4>
|
||||||
<ul className="space-y-2 text-sm text-white/70">
|
{hasContactInfo ? (
|
||||||
<li>تهران، خیابان ولیعصر</li>
|
<ul className="space-y-2 text-sm text-white/70">
|
||||||
<li>تلفن: ۰۲۱-۸۸۰۰۱۲۳۴</li>
|
{businessInfo?.addresses.map((address) => <li key={address}>{address}</li>)}
|
||||||
<li>ایمیل: info@sanihome.ir</li>
|
{businessInfo?.phoneNumbers.map((phone) => (
|
||||||
</ul>
|
<li key={phone}>تلفن: {phone}</li>
|
||||||
<div className="mt-4 flex gap-3">
|
))}
|
||||||
{["IG", "TG", "WA"].map((social) => (
|
{businessInfo?.emails.map((email) => <li key={email}>ایمیل: {email}</li>)}
|
||||||
<a
|
</ul>
|
||||||
key={social}
|
) : (
|
||||||
href="#"
|
<p className="text-sm text-white/50">هنوز اطلاعات تماس ثبت نشده است.</p>
|
||||||
className="font-display flex h-8 w-8 items-center justify-center rounded-full border border-white/20 text-[10px] font-bold transition-colors hover:border-white hover:bg-white/10"
|
)}
|
||||||
>
|
{socialEntries.length > 0 && (
|
||||||
{social}
|
<div className="mt-4 flex gap-3">
|
||||||
</a>
|
{socialEntries.map(([key, url]) => (
|
||||||
))}
|
<a
|
||||||
</div>
|
key={key}
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="font-display flex h-8 w-8 items-center justify-center rounded-full border border-white/20 text-[10px] font-bold transition-colors hover:border-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{socialLabels[key] ?? key.slice(0, 2).toUpperCase()}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { categories } from "@/data/sample";
|
import { createPortal } from "react-dom";
|
||||||
|
import type { Category, CategoryGroup } from "@/lib/meshkee";
|
||||||
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ label: "فروش ویژه", href: "#" },
|
{ label: "فروش ویژه", href: "#" },
|
||||||
@@ -10,31 +12,202 @@ const navLinks = [
|
|||||||
{ label: "تماس با ما", href: "#" },
|
{ label: "تماس با ما", href: "#" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const megaColumns = [
|
function Logo({ large = false }: { large?: boolean }) {
|
||||||
{
|
return (
|
||||||
title: "سرمایش و گرمایش",
|
<a href="/" className="flex shrink-0 items-center">
|
||||||
en: "Cooling & Heating",
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
items: ["یخچال و فریزر", "کولر گازی", "آبگرمکن", "بخاری"],
|
<img
|
||||||
},
|
src="/images/logo.png"
|
||||||
{
|
alt="سانیهوم"
|
||||||
title: "شستشو",
|
className={
|
||||||
en: "Laundry & Care",
|
large
|
||||||
items: ["ماشین لباسشویی", "ماشین ظرفشویی", "خشککن", "اتو بخار"],
|
? "h-[42px] w-auto object-contain md:h-[46px]"
|
||||||
},
|
: "h-8 w-auto md:h-9"
|
||||||
{
|
}
|
||||||
title: "صوتی و تصویری",
|
/>
|
||||||
en: "AV & Display",
|
</a>
|
||||||
items: ["تلویزیون", "ساندبار", "سینمای خانگی", "پروژکتور"],
|
);
|
||||||
},
|
}
|
||||||
{
|
|
||||||
title: "آشپزخانه",
|
|
||||||
en: "Kitchen",
|
|
||||||
items: ["مایکروویو", "جاروبرقی", "هود", "اجاق گاز"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Header() {
|
function ChevronDownIcon({ className = "" }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden>
|
||||||
|
<path
|
||||||
|
d="m6 9 6 6 6-6"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchBar() {
|
||||||
|
return (
|
||||||
|
<div className="flex overflow-hidden rounded-xl border border-border bg-background">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
placeholder="جستجوی محصول، برند یا دستهبندی..."
|
||||||
|
className="flex-1 bg-transparent px-4 py-2.5 text-sm outline-none placeholder:text-muted"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-red px-5 text-sm font-medium text-white transition-colors hover:bg-red-dark"
|
||||||
|
>
|
||||||
|
جستجو
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileDrawer({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
categoryGroups,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
categoryGroups: CategoryGroup[];
|
||||||
|
}) {
|
||||||
|
const [categoriesOpen, setCategoriesOpen] = useState(false);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = open ? "hidden" : "";
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", onKeyDown);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
if (!mounted) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="min-[1200px]:hidden" aria-hidden={!open}>
|
||||||
|
<div
|
||||||
|
onClick={onClose}
|
||||||
|
className={`fixed inset-0 z-[100] bg-black/50 backdrop-blur-sm transition-opacity duration-300 ${
|
||||||
|
open ? "opacity-100" : "pointer-events-none opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`fixed top-0 right-0 z-[110] h-dvh w-[85%] max-w-sm overflow-y-auto bg-surface shadow-2xl transition-transform duration-300 ease-out ${
|
||||||
|
open ? "translate-x-0" : "translate-x-full"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b border-border p-5">
|
||||||
|
<Logo />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="بستن منو"
|
||||||
|
className="flex h-9 w-9 items-center justify-center rounded-lg text-xl text-muted transition-colors hover:bg-red-light hover:text-red"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex flex-col text-sm">
|
||||||
|
<a href="/" className="border-b border-border px-5 py-4 transition-colors hover:bg-red-light/40 hover:text-red">
|
||||||
|
خانه
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div className="border-b border-border">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCategoriesOpen((v) => !v)}
|
||||||
|
aria-expanded={categoriesOpen}
|
||||||
|
className="flex w-full items-center justify-between px-5 py-4 text-right transition-colors hover:bg-red-light/40 hover:text-red"
|
||||||
|
>
|
||||||
|
دستهبندیها
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={`h-4 w-4 shrink-0 transition-transform duration-200 ${
|
||||||
|
categoriesOpen ? "rotate-180" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={`grid transition-all duration-300 ease-out ${
|
||||||
|
categoriesOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
{categoryGroups.length === 0 ? (
|
||||||
|
<div className="bg-background px-5 pb-4">
|
||||||
|
<EmptyState message="هنوز دستهبندیای ثبت نشده است." />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4 bg-background px-5 pb-4">
|
||||||
|
{categoryGroups.map((group) => (
|
||||||
|
<div key={group.id}>
|
||||||
|
<p className="mb-1.5 pt-3 text-xs font-bold text-red-dark">
|
||||||
|
{group.title}
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{group.items.map((item) => (
|
||||||
|
<li key={item.name}>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="block rounded-lg px-3 py-1.5 text-muted transition-colors hover:bg-red-light hover:text-red"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.label}
|
||||||
|
href={link.href}
|
||||||
|
className="border-b border-border px-5 py-4 transition-colors hover:bg-red-light/40 hover:text-red"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-2 px-5 py-4 text-right font-medium transition-colors hover:bg-red-light/40 hover:text-red"
|
||||||
|
>
|
||||||
|
<span aria-hidden>👤</span>
|
||||||
|
ورود
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({
|
||||||
|
categoryGroups,
|
||||||
|
categories,
|
||||||
|
}: {
|
||||||
|
categoryGroups: CategoryGroup[];
|
||||||
|
categories: Category[];
|
||||||
|
}) {
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -50,37 +223,31 @@ export function Header() {
|
|||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 border-b border-border bg-surface/95 backdrop-blur-sm">
|
<header className="sticky top-0 z-50 border-b border-border bg-surface/95 backdrop-blur-sm">
|
||||||
<div className="mx-auto max-w-7xl px-4">
|
<div className="mx-auto max-w-7xl px-4">
|
||||||
<div className="flex items-center justify-between gap-4 py-4">
|
<div className="grid grid-cols-3 items-center gap-4 py-4 min-[1200px]:flex min-[1200px]:justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-start">
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-red text-lg font-bold text-white">
|
<button
|
||||||
S
|
type="button"
|
||||||
</div>
|
onClick={() => setDrawerOpen(true)}
|
||||||
<div>
|
aria-label="منو"
|
||||||
<p className="font-display text-xl font-bold tracking-tight">
|
className="flex h-10 w-10 items-center justify-center rounded-lg border border-border text-xl transition-colors hover:border-red hover:text-red min-[1200px]:hidden"
|
||||||
<span className="text-red">SANI</span>
|
>
|
||||||
<span className="text-foreground">HOME</span>
|
<span aria-hidden>☰</span>
|
||||||
</p>
|
</button>
|
||||||
<p className="text-xs text-muted">فروشگاه لوازم خانگی</p>
|
<div className="hidden min-[1200px]:block">
|
||||||
|
<Logo />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden flex-1 px-6 md:block">
|
<div className="flex items-center justify-center min-[1200px]:flex-1 min-[1200px]:px-6">
|
||||||
<div className="flex overflow-hidden rounded-xl border border-border bg-background">
|
<div className="min-[1200px]:hidden">
|
||||||
<input
|
<Logo large />
|
||||||
type="search"
|
</div>
|
||||||
placeholder="جستجوی محصول، برند یا دستهبندی..."
|
<div className="hidden w-full min-[1200px]:block">
|
||||||
className="flex-1 bg-transparent px-4 py-2.5 text-sm outline-none placeholder:text-muted"
|
<SearchBar />
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-red px-5 text-sm font-medium text-white transition-colors hover:bg-red-dark"
|
|
||||||
>
|
|
||||||
جستجو
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 text-sm">
|
<div className="flex items-center justify-end gap-3 text-sm">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="hidden items-center gap-1.5 text-muted transition-colors hover:text-red sm:flex"
|
className="hidden items-center gap-1.5 text-muted transition-colors hover:text-red sm:flex"
|
||||||
@@ -101,7 +268,11 @@ export function Header() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="relative flex items-center gap-6 overflow-visible border-t border-border py-3 text-sm">
|
<div className="border-t border-border py-3 min-[1200px]:hidden">
|
||||||
|
<SearchBar />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="relative hidden items-center gap-6 overflow-visible border-t border-border py-3 text-sm min-[1200px]:flex">
|
||||||
<div
|
<div
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className="relative"
|
className="relative"
|
||||||
@@ -133,40 +304,45 @@ export function Header() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
{categoryGroups.length === 0 ? (
|
||||||
{megaColumns.map((column) => (
|
<EmptyState message="هنوز دستهبندیای ثبت نشده است." />
|
||||||
<div key={column.title}>
|
) : (
|
||||||
<p className="mb-0.5 text-sm font-bold text-red-dark">{column.title}</p>
|
<>
|
||||||
<p className="font-display mb-3 text-[10px] tracking-wider text-muted uppercase">
|
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
{column.en}
|
{categoryGroups.map((column) => (
|
||||||
</p>
|
<div key={column.id}>
|
||||||
<ul className="space-y-2">
|
<p className="mb-3 text-sm font-bold text-red-dark">{column.title}</p>
|
||||||
{column.items.map((item) => (
|
<ul className="space-y-2">
|
||||||
<li key={item}>
|
{column.items.map((item) => (
|
||||||
<a
|
<li key={item.name}>
|
||||||
href="#"
|
<a
|
||||||
className="text-sm text-muted transition-colors hover:text-red"
|
href="#"
|
||||||
>
|
className="text-sm text-muted transition-colors hover:text-red"
|
||||||
{item}
|
>
|
||||||
</a>
|
{item.name}
|
||||||
</li>
|
</a>
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 grid grid-cols-2 gap-2 border-t border-border pt-4 sm:grid-cols-4">
|
{categories.length > 0 && (
|
||||||
{categories.slice(0, 4).map((cat) => (
|
<div className="mt-5 grid grid-cols-2 gap-2 border-t border-border pt-4 sm:grid-cols-4">
|
||||||
<a
|
{categories.slice(0, 4).map((cat) => (
|
||||||
key={cat.id}
|
<a
|
||||||
href="#"
|
key={cat.id}
|
||||||
className="rounded-lg bg-red-light/50 px-3 py-2 text-center text-xs font-medium text-red-dark transition-colors hover:bg-red-light"
|
href="#"
|
||||||
>
|
className="rounded-lg bg-red-light/50 px-3 py-2 text-center text-xs font-medium text-red-dark transition-colors hover:bg-red-light"
|
||||||
{cat.name}
|
>
|
||||||
</a>
|
{cat.name}
|
||||||
))}
|
</a>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -182,6 +358,12 @@ export function Header() {
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MobileDrawer
|
||||||
|
open={drawerOpen}
|
||||||
|
onClose={() => setDrawerOpen(false)}
|
||||||
|
categoryGroups={categoryGroups}
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,48 +4,48 @@ import { useState } from "react";
|
|||||||
import { Placeholder } from "@/components/ui/Placeholder";
|
import { Placeholder } from "@/components/ui/Placeholder";
|
||||||
import { sliderSlides } from "@/data/sample";
|
import { sliderSlides } from "@/data/sample";
|
||||||
|
|
||||||
|
const PEEK_OFFSET = 70;
|
||||||
|
|
||||||
export function HeroSlider() {
|
export function HeroSlider() {
|
||||||
const [active, setActive] = useState(1);
|
const [active, setActive] = useState(0);
|
||||||
|
const len = sliderSlides.length;
|
||||||
|
|
||||||
const getIndex = (offset: number) => {
|
const getDiff = (i: number) => {
|
||||||
const len = sliderSlides.length;
|
let diff = i - active;
|
||||||
return (active + offset + len) % len;
|
if (diff > len / 2) diff -= len;
|
||||||
|
if (diff < -len / 2) diff += len;
|
||||||
|
return diff;
|
||||||
};
|
};
|
||||||
|
|
||||||
const positions = [
|
|
||||||
{ offset: -1, scale: "scale-[0.92]", opacity: "opacity-55", z: "z-10" },
|
|
||||||
{ offset: 0, scale: "scale-100", opacity: "opacity-100", z: "z-20" },
|
|
||||||
{ offset: 1, scale: "scale-[0.92]", opacity: "opacity-55", z: "z-10" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="overflow-x-clip bg-gradient-to-b from-red-light/30 to-background py-4 md:py-6">
|
<section className="overflow-x-clip bg-gradient-to-b from-red-light/30 to-background py-4 md:py-6">
|
||||||
<div className="relative mx-auto w-full max-w-7xl px-4">
|
<div className="relative mx-auto w-full max-w-7xl px-4">
|
||||||
<div className="relative h-[220px] sm:h-[280px] md:h-[340px] lg:h-[380px]">
|
<div className="relative h-[220px] sm:h-[280px] md:h-[340px] lg:h-[380px]">
|
||||||
{positions.map(({ offset, scale, opacity, z }) => {
|
{sliderSlides.map((slide, i) => {
|
||||||
const isCenter = offset === 0;
|
const diff = getDiff(i);
|
||||||
const slide = sliderSlides[getIndex(offset)];
|
const isCenter = diff === 0;
|
||||||
|
const isVisible = Math.abs(diff) <= 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={offset}
|
key={slide.id}
|
||||||
className={`absolute top-1/2 -translate-y-1/2 transition-all duration-500 ease-out ${scale} ${opacity} ${z} ${
|
className={`absolute inset-0 flex items-center justify-center ${
|
||||||
isCenter
|
isCenter ? "z-20" : "z-10 pointer-events-none"
|
||||||
? "inset-x-0 w-full origin-center"
|
}`}
|
||||||
: offset === -1
|
|
||||||
? "right-full mr-1.5 w-[78%] origin-right sm:w-[70%] md:w-[65%]"
|
|
||||||
: "left-full ml-1.5 w-[78%] origin-left sm:w-[70%] md:w-[65%]"
|
|
||||||
} ${isCenter ? "" : "pointer-events-none"}`}
|
|
||||||
>
|
>
|
||||||
<div className="overflow-hidden rounded-2xl border border-border bg-surface shadow-lg">
|
<div
|
||||||
|
className={`h-[200px] overflow-hidden rounded-2xl border border-border bg-surface shadow-lg transition-all duration-500 ease-out sm:h-[260px] md:h-[310px] lg:h-[380px] ${
|
||||||
|
isCenter
|
||||||
|
? "w-[94%] scale-100 opacity-100 sm:w-[90%] md:w-[86%] lg:w-[82%]"
|
||||||
|
: "w-[66%] scale-[0.85] opacity-55 sm:w-[60%] md:w-[54%] lg:w-[48%]"
|
||||||
|
} ${isVisible ? "" : "opacity-0"}`}
|
||||||
|
style={{ transform: `translateX(${-diff * PEEK_OFFSET}%)` }}
|
||||||
|
>
|
||||||
<Placeholder
|
<Placeholder
|
||||||
src={slide.image}
|
src={slide.image}
|
||||||
alt={slide.title}
|
alt={slide.title}
|
||||||
width="100%"
|
width="100%"
|
||||||
label={isCenter ? "1280×380" : "880×350"}
|
className="h-full w-full rounded-none border-0"
|
||||||
className={`w-full rounded-none border-0 h-[200px] sm:h-[260px] md:h-[310px] ${
|
|
||||||
isCenter ? "lg:h-[380px]" : "lg:h-[350px]"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Carousel } from "@/components/ui/Carousel";
|
|
||||||
import { ProductCard } from "@/components/ui/ProductCard";
|
|
||||||
import { SectionTitle } from "@/components/ui/SectionTitle";
|
|
||||||
import { samsungProducts } from "@/data/sample";
|
|
||||||
|
|
||||||
export function SamsungCarousel() {
|
|
||||||
return (
|
|
||||||
<section className="py-10 md:py-14">
|
|
||||||
<div className="mx-auto max-w-7xl px-4">
|
|
||||||
<SectionTitle
|
|
||||||
title="محصولات سامسونگ"
|
|
||||||
enTitle="Samsung Collection"
|
|
||||||
action="مشاهده همه"
|
|
||||||
/>
|
|
||||||
<Carousel ariaLabel="کاروسل محصولات سامسونگ">
|
|
||||||
{samsungProducts.map((product) => (
|
|
||||||
<ProductCard
|
|
||||||
key={product.id}
|
|
||||||
name={product.name}
|
|
||||||
brand={product.brand}
|
|
||||||
price={product.price}
|
|
||||||
image={product.image}
|
|
||||||
variant="brand"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Carousel>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,8 @@ import { Carousel } from "@/components/ui/Carousel";
|
|||||||
import { Placeholder } from "@/components/ui/Placeholder";
|
import { Placeholder } from "@/components/ui/Placeholder";
|
||||||
import { ProductCard } from "@/components/ui/ProductCard";
|
import { ProductCard } from "@/components/ui/ProductCard";
|
||||||
import { SectionTitle } from "@/components/ui/SectionTitle";
|
import { SectionTitle } from "@/components/ui/SectionTitle";
|
||||||
import { specialSaleProducts } from "@/data/sample";
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
|
import type { Product } from "@/lib/meshkee";
|
||||||
|
|
||||||
function SpecialSaleBanner() {
|
function SpecialSaleBanner() {
|
||||||
return (
|
return (
|
||||||
@@ -14,14 +15,13 @@ function SpecialSaleBanner() {
|
|||||||
src="/images/sale-banner.svg"
|
src="/images/sale-banner.svg"
|
||||||
alt="فروش ویژه"
|
alt="فروش ویژه"
|
||||||
width="100%"
|
width="100%"
|
||||||
label="240×380"
|
|
||||||
className="h-full min-h-[380px] w-full rounded-none border-0"
|
className="h-full min-h-[380px] w-full rounded-none border-0"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SpecialSaleCarousel() {
|
export function SpecialSaleCarousel({ products }: { products: Product[] }) {
|
||||||
return (
|
return (
|
||||||
<section className="border-y border-red-light bg-red-light/30 py-10 md:py-14">
|
<section className="border-y border-red-light bg-red-light/30 py-10 md:py-14">
|
||||||
<div className="mx-auto max-w-7xl px-4">
|
<div className="mx-auto max-w-7xl px-4">
|
||||||
@@ -30,21 +30,25 @@ export function SpecialSaleCarousel() {
|
|||||||
enTitle="Special Sale"
|
enTitle="Special Sale"
|
||||||
action="مشاهده همه"
|
action="مشاهده همه"
|
||||||
/>
|
/>
|
||||||
<Carousel ariaLabel="کاروسل فروش ویژه">
|
{products.length === 0 ? (
|
||||||
<SpecialSaleBanner />
|
<EmptyState message="هنوز محصولی برای فروش ویژه ثبت نشده است." />
|
||||||
{specialSaleProducts.map((product) => (
|
) : (
|
||||||
<ProductCard
|
<Carousel ariaLabel="کاروسل فروش ویژه">
|
||||||
key={product.id}
|
<SpecialSaleBanner />
|
||||||
name={product.name}
|
{products.map((product) => (
|
||||||
brand={product.brand}
|
<ProductCard
|
||||||
price={product.price}
|
key={product.id}
|
||||||
oldPrice={product.oldPrice}
|
name={product.name}
|
||||||
discount={product.discount}
|
brand={product.brand ?? ""}
|
||||||
image={product.image}
|
price={product.price}
|
||||||
variant="sale"
|
oldPrice={product.oldPrice}
|
||||||
/>
|
discount={product.discount}
|
||||||
))}
|
image={product.image ?? "/images/placeholder.svg"}
|
||||||
</Carousel>
|
variant="sale"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { Placeholder } from "@/components/ui/Placeholder";
|
|
||||||
|
|
||||||
const banners = [
|
const banners = [
|
||||||
{ title: "ماشین لباسشویی", subtitle: "از ۲۸ میلیون", label: "420×160", image: "/images/promo-1.svg" },
|
{ alt: "خانهای تمیزتر، نفسهایی آسودهتر", image: "/images/banner3/6.jpg" },
|
||||||
{ title: "جاروبرقی", subtitle: "ارسال فوری", label: "420×160", image: "/images/promo-2.svg" },
|
{ alt: "خنکای دلنشین، در تمام لحظهها", image: "/images/banner3/7.jpg" },
|
||||||
{ title: "لوازم آشپزخانه", subtitle: "تا ۲۰٪ تخفیف", label: "420×160", image: "/images/promo-3.svg" },
|
{ alt: "هر تصویر، یک تجربه تازه", image: "/images/banner3/8.jpg" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ThreeBanners() {
|
export function ThreeBanners() {
|
||||||
@@ -13,22 +11,12 @@ export function ThreeBanners() {
|
|||||||
<div className="grid gap-4 md:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
{banners.map((banner) => (
|
{banners.map((banner) => (
|
||||||
<a
|
<a
|
||||||
key={banner.title}
|
key={banner.image}
|
||||||
href="#"
|
href="#"
|
||||||
className="group relative overflow-hidden rounded-xl border border-border bg-surface transition-all hover:border-red hover:shadow-md"
|
className="group overflow-hidden rounded-xl border border-border bg-surface transition-all hover:border-red hover:shadow-md"
|
||||||
>
|
>
|
||||||
<Placeholder
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
src={banner.image}
|
<img src={banner.image} alt={banner.alt} className="h-auto w-full" />
|
||||||
alt={banner.title}
|
|
||||||
width="100%"
|
|
||||||
height="160px"
|
|
||||||
label={banner.label}
|
|
||||||
className="w-full rounded-none border-0"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 flex flex-col items-start justify-end bg-gradient-to-t from-black/60 to-transparent p-5">
|
|
||||||
<h3 className="font-bold text-white">{banner.title}</h3>
|
|
||||||
<p className="text-sm text-white/75">{banner.subtitle}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Carousel } from "@/components/ui/Carousel";
|
import { Carousel } from "@/components/ui/Carousel";
|
||||||
import { ProductCard } from "@/components/ui/ProductCard";
|
import { ProductCard } from "@/components/ui/ProductCard";
|
||||||
import { SectionTitle } from "@/components/ui/SectionTitle";
|
import { SectionTitle } from "@/components/ui/SectionTitle";
|
||||||
import { topSellingProducts } from "@/data/sample";
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
|
import type { Product } from "@/lib/meshkee";
|
||||||
|
|
||||||
export function TopSellingCarousel() {
|
export function TopSellingCarousel({ products }: { products: Product[] }) {
|
||||||
return (
|
return (
|
||||||
<section className="py-10 md:py-14">
|
<section className="py-10 md:py-14">
|
||||||
<div className="mx-auto max-w-7xl px-4">
|
<div className="mx-auto max-w-7xl px-4">
|
||||||
@@ -12,17 +13,21 @@ export function TopSellingCarousel() {
|
|||||||
enTitle="Top Selling"
|
enTitle="Top Selling"
|
||||||
action="مشاهده همه"
|
action="مشاهده همه"
|
||||||
/>
|
/>
|
||||||
<Carousel ariaLabel="کاروسل پرفروشترینها">
|
{products.length === 0 ? (
|
||||||
{topSellingProducts.map((product) => (
|
<EmptyState message="هنوز محصولی ثبت نشده است." />
|
||||||
<ProductCard
|
) : (
|
||||||
key={product.id}
|
<Carousel ariaLabel="کاروسل پرفروشترینها">
|
||||||
name={product.name}
|
{products.map((product) => (
|
||||||
brand={product.brand}
|
<ProductCard
|
||||||
price={product.price}
|
key={product.id}
|
||||||
image={product.image}
|
name={product.name}
|
||||||
/>
|
brand={product.brand ?? ""}
|
||||||
))}
|
price={product.price}
|
||||||
</Carousel>
|
image={product.image ?? "/images/placeholder.svg"}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { Placeholder } from "@/components/ui/Placeholder";
|
|
||||||
|
|
||||||
export function TwoBanners() {
|
export function TwoBanners() {
|
||||||
return (
|
return (
|
||||||
<section className="py-10 md:py-14">
|
<section className="py-10 md:py-14">
|
||||||
@@ -7,43 +5,21 @@ export function TwoBanners() {
|
|||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
className="group overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
||||||
>
|
>
|
||||||
<Placeholder
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
src="/images/banner-1.svg"
|
<img src="/images/banner/5.jpg" alt="وقتی که انرژی لازمی" className="h-auto w-full" />
|
||||||
alt="تخفیف ویژه یخچال و فریزر"
|
|
||||||
width="100%"
|
|
||||||
height="200px"
|
|
||||||
label="640×200"
|
|
||||||
className="w-full rounded-none border-0"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-red-dark/80 to-transparent p-6">
|
|
||||||
<p className="font-display text-xs tracking-widest text-red-muted uppercase">
|
|
||||||
Summer Sale
|
|
||||||
</p>
|
|
||||||
<h3 className="text-lg font-bold text-white">تخفیف ویژه یخچال و فریزر</h3>
|
|
||||||
<p className="mt-1 text-sm text-white/80">تا ۲۵٪ تخفیف — تا پایان مرداد</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
className="group overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
||||||
>
|
>
|
||||||
<Placeholder
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
src="/images/banner-2.svg"
|
<img
|
||||||
alt="تلویزیونهای هوشمند ۲۰۲۶"
|
src="/images/banner/4.jpg"
|
||||||
width="100%"
|
alt="قدرت بخار، درخشش بینقص"
|
||||||
height="200px"
|
className="h-auto w-full"
|
||||||
label="640×200"
|
|
||||||
className="w-full rounded-none border-0"
|
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-red-dark/80 to-transparent p-6">
|
|
||||||
<p className="font-display text-xs tracking-widest text-red-muted uppercase">
|
|
||||||
Smart TV
|
|
||||||
</p>
|
|
||||||
<h3 className="text-lg font-bold text-white">تلویزیونهای هوشمند ۲۰۲۶</h3>
|
|
||||||
<p className="mt-1 text-sm text-white/80">QLED و OLED با اقساط ۱۲ ماهه</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef } from "react";
|
import { Children, useRef } from "react";
|
||||||
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
|
import { FreeMode } from "swiper/modules";
|
||||||
|
import type { Swiper as SwiperClass } from "swiper";
|
||||||
|
import "swiper/css";
|
||||||
|
import "swiper/css/free-mode";
|
||||||
|
|
||||||
type CarouselProps = {
|
type CarouselProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -36,14 +41,7 @@ function ChevronRightIcon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Carousel({ children, ariaLabel }: CarouselProps) {
|
export function Carousel({ children, ariaLabel }: CarouselProps) {
|
||||||
const trackRef = useRef<HTMLDivElement>(null);
|
const swiperRef = useRef<SwiperClass | null>(null);
|
||||||
|
|
||||||
const scroll = (direction: "prev" | "next") => {
|
|
||||||
const track = trackRef.current;
|
|
||||||
if (!track) return;
|
|
||||||
const amount = direction === "next" ? -280 : 280;
|
|
||||||
track.scrollBy({ left: amount, behavior: "smooth" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const btnClass =
|
const btnClass =
|
||||||
"absolute top-1/2 z-10 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/40 bg-white/40 text-red shadow-sm backdrop-blur-md transition-colors hover:border-red/40 hover:bg-white/60 hover:text-red-dark";
|
"absolute top-1/2 z-10 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/40 bg-white/40 text-red shadow-sm backdrop-blur-md transition-colors hover:border-red/40 hover:bg-white/60 hover:text-red-dark";
|
||||||
@@ -52,7 +50,7 @@ export function Carousel({ children, ariaLabel }: CarouselProps) {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => scroll("prev")}
|
onClick={() => swiperRef.current?.slidePrev()}
|
||||||
aria-label="قبلی"
|
aria-label="قبلی"
|
||||||
className={`${btnClass} right-1`}
|
className={`${btnClass} right-1`}
|
||||||
>
|
>
|
||||||
@@ -60,20 +58,30 @@ export function Carousel({ children, ariaLabel }: CarouselProps) {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => scroll("next")}
|
onClick={() => swiperRef.current?.slideNext()}
|
||||||
aria-label="بعدی"
|
aria-label="بعدی"
|
||||||
className={`${btnClass} left-1`}
|
className={`${btnClass} left-1`}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
</button>
|
</button>
|
||||||
<div
|
<Swiper
|
||||||
ref={trackRef}
|
modules={[FreeMode]}
|
||||||
|
onSwiper={(swiper) => {
|
||||||
|
swiperRef.current = swiper;
|
||||||
|
}}
|
||||||
|
dir="rtl"
|
||||||
|
slidesPerView="auto"
|
||||||
|
spaceBetween={16}
|
||||||
|
freeMode={{ enabled: true, momentum: true, sticky: true }}
|
||||||
|
grabCursor
|
||||||
role="region"
|
role="region"
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
className="carousel-track flex items-stretch gap-4 overflow-x-auto scroll-smooth px-1 py-2"
|
className="!px-1 !py-2"
|
||||||
>
|
>
|
||||||
{children}
|
{Children.map(children, (child) => (
|
||||||
</div>
|
<SwiperSlide style={{ width: "auto" }}>{child}</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
type EmptyStateProps = {
|
||||||
|
message?: string;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EmptyState({
|
||||||
|
message = "هنوز محتوایی ثبت نشده است.",
|
||||||
|
className = "",
|
||||||
|
}: EmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-center rounded-2xl border border-dashed border-border bg-surface py-10 text-sm text-muted ${className}`}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,164 +1,24 @@
|
|||||||
export const categories = [
|
export const sliderSlides = [
|
||||||
{ id: 1, name: "یخچال و فریزر", count: 48, image: "/images/category-1.svg" },
|
|
||||||
{ id: 2, name: "ماشین لباسشویی", count: 36, image: "/images/category-2.svg" },
|
|
||||||
{ id: 3, name: "تلویزیون", count: 52, image: "/images/category-3.svg" },
|
|
||||||
{ id: 4, name: "جاروبرقی", count: 29, image: "/images/category-4.svg" },
|
|
||||||
{ id: 5, name: "مایکروویو", count: 18, image: "/images/category-5.svg" },
|
|
||||||
{ id: 6, name: "ظرفشویی", count: 22, image: "/images/category-6.svg" },
|
|
||||||
{ id: 7, name: "کولر و گرمایش", count: 31, image: "/images/category-7.svg" },
|
|
||||||
{ id: 8, name: "لوازم آشپزخانه", count: 64, image: "/images/category-8.svg" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const specialSaleProducts = [
|
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "یخچال فریزر ساید بای ساید سامسونگ",
|
title: "جشنواره تابستانه لوازم خانگی",
|
||||||
brand: "Samsung",
|
subtitle: "تا ۳۰٪ تخفیف روی برندهای منتخب",
|
||||||
price: 89500000,
|
cta: "مشاهده پیشنهادها",
|
||||||
oldPrice: 102000000,
|
image: "/images/slider/1.jpg",
|
||||||
discount: 12,
|
|
||||||
image: "/images/product-1.svg",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: "ماشین لباسشویی ۹ کیلویی الجی",
|
title: "سری جدید Samsung Bespoke",
|
||||||
brand: "LG",
|
subtitle: "طراحی هوشمند برای آشپزخانه مدرن",
|
||||||
price: 42800000,
|
cta: "کاوش مجموعه",
|
||||||
oldPrice: 48500000,
|
image: "/images/slider/2.jpg",
|
||||||
discount: 11,
|
|
||||||
image: "/images/product-2.svg",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
name: "تلویزیون ۵۵ اینچ QLED سامسونگ",
|
title: "ارسال رایگان به سراسر کشور",
|
||||||
brand: "Samsung",
|
subtitle: "برای سفارشهای بالای ۵ میلیون تومان",
|
||||||
price: 56200000,
|
cta: "خرید کنید",
|
||||||
oldPrice: 62000000,
|
image: "/images/slider/3.jpg",
|
||||||
discount: 9,
|
|
||||||
image: "/images/product-3.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "جاروبرقی رباتیک شیائومی",
|
|
||||||
brand: "Xiaomi",
|
|
||||||
price: 18900000,
|
|
||||||
oldPrice: 23500000,
|
|
||||||
discount: 19,
|
|
||||||
image: "/images/product-4.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: "مایکروویو ۳۰ لیتری بوش",
|
|
||||||
brand: "Bosch",
|
|
||||||
price: 12400000,
|
|
||||||
oldPrice: 14800000,
|
|
||||||
discount: 16,
|
|
||||||
image: "/images/product-5.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: "ظرفشویی ۱۴ نفره بوش",
|
|
||||||
brand: "Bosch",
|
|
||||||
price: 35600000,
|
|
||||||
oldPrice: 39900000,
|
|
||||||
discount: 10,
|
|
||||||
image: "/images/product-6.svg",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const topSellingProducts = [
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
name: "یخچال دوقلو الجی",
|
|
||||||
brand: "LG",
|
|
||||||
price: 67800000,
|
|
||||||
sold: 124,
|
|
||||||
image: "/images/product-7.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
name: "تلویزیون ۶۵ اینچ OLED الجی",
|
|
||||||
brand: "LG",
|
|
||||||
price: 98500000,
|
|
||||||
sold: 89,
|
|
||||||
image: "/images/product-8.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
name: "ماشین لباسشویی ۸ کیلویی سامسونگ",
|
|
||||||
brand: "Samsung",
|
|
||||||
price: 38900000,
|
|
||||||
sold: 156,
|
|
||||||
image: "/images/product-9.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
name: "کولر گازی ۲۴ هزار اینورتر",
|
|
||||||
brand: "Gree",
|
|
||||||
price: 24500000,
|
|
||||||
sold: 203,
|
|
||||||
image: "/images/product-10.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
name: "اجاق گاز رومیزی بوش",
|
|
||||||
brand: "Bosch",
|
|
||||||
price: 16800000,
|
|
||||||
sold: 67,
|
|
||||||
image: "/images/product-11.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
name: "جاروبرقی سایکلون سامسونگ",
|
|
||||||
brand: "Samsung",
|
|
||||||
price: 8900000,
|
|
||||||
sold: 178,
|
|
||||||
image: "/images/product-12.svg",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const samsungProducts = [
|
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
name: "یخچال Bespoke سامسونگ",
|
|
||||||
brand: "Samsung",
|
|
||||||
price: 112000000,
|
|
||||||
image: "/images/product-13.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
name: "ماشین لباسشویی AI EcoBubble",
|
|
||||||
brand: "Samsung",
|
|
||||||
price: 45600000,
|
|
||||||
image: "/images/product-14.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 15,
|
|
||||||
name: "تلویزیون Neo QLED 8K",
|
|
||||||
brand: "Samsung",
|
|
||||||
price: 185000000,
|
|
||||||
image: "/images/product-15.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 16,
|
|
||||||
name: "جاروبرقی Jet ۹۰",
|
|
||||||
brand: "Samsung",
|
|
||||||
price: 21500000,
|
|
||||||
image: "/images/product-16.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 17,
|
|
||||||
name: "مایکروویو Grill سامسونگ",
|
|
||||||
brand: "Samsung",
|
|
||||||
price: 13800000,
|
|
||||||
image: "/images/product-17.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 18,
|
|
||||||
name: "کولر گازی WindFree",
|
|
||||||
brand: "Samsung",
|
|
||||||
price: 32800000,
|
|
||||||
image: "/images/product-18.svg",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -197,30 +57,6 @@ export const blogPosts = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const sliderSlides = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "جشنواره تابستانه لوازم خانگی",
|
|
||||||
subtitle: "تا ۳۰٪ تخفیف روی برندهای منتخب",
|
|
||||||
cta: "مشاهده پیشنهادها",
|
|
||||||
image: "/images/slider-1.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "سری جدید Samsung Bespoke",
|
|
||||||
subtitle: "طراحی هوشمند برای آشپزخانه مدرن",
|
|
||||||
cta: "کاوش مجموعه",
|
|
||||||
image: "/images/slider-2.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "ارسال رایگان به سراسر کشور",
|
|
||||||
subtitle: "برای سفارشهای بالای ۵ میلیون تومان",
|
|
||||||
cta: "خرید کنید",
|
|
||||||
image: "/images/slider-3.svg",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function formatPrice(price: number): string {
|
export function formatPrice(price: number): string {
|
||||||
return new Intl.NumberFormat("fa-IR").format(price);
|
return new Intl.NumberFormat("fa-IR").format(price);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
const API_BASE = "https://api.meshkee.com/api/v1";
|
||||||
|
const DOMAIN = "sanihome.ir";
|
||||||
|
|
||||||
|
async function apiGet<T>(
|
||||||
|
path: string,
|
||||||
|
searchParams?: Record<string, string | number | boolean | undefined>,
|
||||||
|
): Promise<T | null> {
|
||||||
|
const url = new URL(`${API_BASE}${path}`);
|
||||||
|
if (searchParams) {
|
||||||
|
for (const [key, value] of Object.entries(searchParams)) {
|
||||||
|
if (value !== undefined) url.searchParams.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url.toString(), { next: { revalidate: 300 } });
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return (await res.json()) as T;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BusinessInfo = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
nameFa: string;
|
||||||
|
about: string;
|
||||||
|
vision: string;
|
||||||
|
logoUrl: string | null;
|
||||||
|
faviconUrl: string | null;
|
||||||
|
emails: string[];
|
||||||
|
phoneNumbers: string[];
|
||||||
|
socialMedia: {
|
||||||
|
whatsapp?: string;
|
||||||
|
telegram?: string;
|
||||||
|
instagram?: string;
|
||||||
|
linkedin?: string;
|
||||||
|
youtube?: string;
|
||||||
|
aparat?: string;
|
||||||
|
};
|
||||||
|
addresses: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getBusinessInfo(): Promise<BusinessInfo | null> {
|
||||||
|
return apiGet<BusinessInfo>(`/tenants/${DOMAIN}/website/business-info`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Category = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image?: string | null;
|
||||||
|
count?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getCategories(): Promise<Category[]> {
|
||||||
|
const res = await apiGet<{ items: Category[] }>(`/tenants/${DOMAIN}/categories`, {
|
||||||
|
entityType: "product",
|
||||||
|
});
|
||||||
|
return res?.items ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CategoryGroup = {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
items: { name: string; href?: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeGroupLinkItem(raw: unknown): { name: string; href?: string } {
|
||||||
|
if (typeof raw === "string") return { name: raw };
|
||||||
|
const item = raw as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
name: String(item.name ?? item.title ?? item.label ?? ""),
|
||||||
|
href: typeof item.slug === "string" ? item.slug : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCategoryGroups(): Promise<CategoryGroup[]> {
|
||||||
|
const res = await apiGet<{ items: Record<string, unknown>[] }>(
|
||||||
|
`/tenants/${DOMAIN}/website/category-groups`,
|
||||||
|
);
|
||||||
|
return (res?.items ?? []).map((group) => ({
|
||||||
|
id: String(group.id ?? group.key ?? ""),
|
||||||
|
key: String(group.key ?? group.id ?? ""),
|
||||||
|
title: String(group.title ?? ""),
|
||||||
|
items: Array.isArray(group.items) ? group.items.map(normalizeGroupLinkItem) : [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Product = {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
brand?: string;
|
||||||
|
price: number;
|
||||||
|
oldPrice?: number;
|
||||||
|
discount?: number;
|
||||||
|
image?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeProduct(raw: Record<string, unknown>): Product {
|
||||||
|
const images = raw.images as unknown[] | undefined;
|
||||||
|
const firstImage = images?.[0];
|
||||||
|
const brand = raw.brand;
|
||||||
|
return {
|
||||||
|
id: String(raw.id ?? raw.slug ?? ""),
|
||||||
|
slug: String(raw.slug ?? raw.id ?? ""),
|
||||||
|
name: String(raw.name ?? raw.title ?? ""),
|
||||||
|
brand:
|
||||||
|
typeof brand === "string"
|
||||||
|
? brand
|
||||||
|
: ((brand as Record<string, unknown> | undefined)?.name as string | undefined) ??
|
||||||
|
(raw.brandName as string | undefined),
|
||||||
|
price: Number(raw.price ?? raw.salePrice ?? 0),
|
||||||
|
oldPrice: raw.oldPrice != null ? Number(raw.oldPrice) : undefined,
|
||||||
|
discount: raw.discount != null ? Number(raw.discount) : undefined,
|
||||||
|
image:
|
||||||
|
(raw.image as string | undefined) ??
|
||||||
|
(raw.imageUrl as string | undefined) ??
|
||||||
|
(typeof firstImage === "string"
|
||||||
|
? firstImage
|
||||||
|
: ((firstImage as Record<string, unknown> | undefined)?.url as string | undefined)) ??
|
||||||
|
null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProducts(params?: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
categoryId?: string;
|
||||||
|
brandId?: string;
|
||||||
|
tag?: string;
|
||||||
|
inStore?: boolean;
|
||||||
|
}): Promise<{ items: Product[]; total: number }> {
|
||||||
|
const res = await apiGet<{
|
||||||
|
items: Record<string, unknown>[];
|
||||||
|
total: number;
|
||||||
|
}>(`/tenants/${DOMAIN}/products`, params);
|
||||||
|
return { items: (res?.items ?? []).map(normalizeProduct), total: res?.total ?? 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProductGroup = {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
items: Product[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeProductGroups(items: Record<string, unknown>[] | undefined): ProductGroup[] {
|
||||||
|
return (items ?? []).map((group) => ({
|
||||||
|
id: String(group.id ?? group.key ?? ""),
|
||||||
|
key: String(group.key ?? group.id ?? ""),
|
||||||
|
title: String(group.title ?? ""),
|
||||||
|
items: Array.isArray(group.items)
|
||||||
|
? (group.items as Record<string, unknown>[]).map(normalizeProduct)
|
||||||
|
: [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBrandGroups(): Promise<ProductGroup[]> {
|
||||||
|
const res = await apiGet<{ items: Record<string, unknown>[] }>(
|
||||||
|
`/tenants/${DOMAIN}/website/brand-groups`,
|
||||||
|
);
|
||||||
|
return normalizeProductGroups(res?.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStoreSpecials(): Promise<ProductGroup[]> {
|
||||||
|
const res = await apiGet<{ items: Record<string, unknown>[] }>(
|
||||||
|
`/tenants/${DOMAIN}/store-specials`,
|
||||||
|
);
|
||||||
|
return normalizeProductGroups(res?.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Slide = {
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
image?: string | null;
|
||||||
|
href?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeSlide(raw: Record<string, unknown>): Slide {
|
||||||
|
return {
|
||||||
|
id: String(raw.id ?? ""),
|
||||||
|
title: raw.title as string | undefined,
|
||||||
|
subtitle: raw.subtitle as string | undefined,
|
||||||
|
image: (raw.image as string | undefined) ?? (raw.imageUrl as string | undefined) ?? null,
|
||||||
|
href: raw.link as string | undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSliderSlides(): Promise<Slide[]> {
|
||||||
|
const res = await apiGet<{ items: Record<string, unknown>[] }>(
|
||||||
|
`/tenants/${DOMAIN}/website/sliders`,
|
||||||
|
);
|
||||||
|
const firstSlider = res?.items?.[0];
|
||||||
|
const slides = (firstSlider?.slides ?? firstSlider?.items) as
|
||||||
|
| Record<string, unknown>[]
|
||||||
|
| undefined;
|
||||||
|
return (slides ?? []).map(normalizeSlide);
|
||||||
|
}
|
||||||