initial project
This commit is contained in:
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_API_BASE_URL=https://api.robotmarket.ir/v1
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.next
|
||||
.env*.local
|
||||
.DS_Store
|
||||
out
|
||||
build
|
||||
*.log
|
||||
Generated
+70
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="01b90d18-2bd3-488f-bee5-27ff6a73414d" name="Changes" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 2,
|
||||
"fromUser": false
|
||||
}</component>
|
||||
<component name="ProjectId" id="3H7KB12mCznfw4hRrFCPWKMzxsB" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||
"js.debugger.nextJs.config.created.client": "true",
|
||||
"js.debugger.nextJs.config.created.server": "true",
|
||||
"last_opened_file_path": "D:/Project/robot-market-landing/robot-market",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</component>
|
||||
<component name="RunManager" selected="npm.Next.js: server-side">
|
||||
<configuration name="Next.js: debug client-side" type="JavascriptDebugType" uri="http://localhost:3000/">
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="Next.js: server-side" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="dev" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-js-predefined-d6986cc7102b-31caf2ab9e3c-JavaScript-WS-261.23567.141" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="01b90d18-2bd3-488f-bee5-27ff6a73414d" name="Changes" comment="" />
|
||||
<created>1785213528048</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1785213528048</updated>
|
||||
<workItem from="1785213529281" duration="2289000" />
|
||||
<workItem from="1785300366173" duration="6178000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,2 +1,29 @@
|
||||
# robot-market-landing
|
||||
robot-market-landing
|
||||
# روبات مارکت — سایت نمایش محصولات
|
||||
|
||||
## نصب و اجرا
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
سپس آدرس `http://localhost:3000` را در مرورگر باز کنید.
|
||||
|
||||
## ساختار پروژه
|
||||
|
||||
```
|
||||
src/
|
||||
app/ # layout.js و page.js (App Router)
|
||||
components/ # کامپوننتهای بخشهای سایت
|
||||
data/ # محتوای متنی و محصولات (content.js)
|
||||
utils/api.js # نمونه Axios برای اتصال به API در آینده
|
||||
```
|
||||
|
||||
## نکات
|
||||
|
||||
- رنگ اصلی برند در `tailwind.config.js` روی `#00a693` تنظیم شده است.
|
||||
- فونت وزیرمتن بهصورت خودکار از `next/font/google` بارگذاری میشود.
|
||||
- برای جایگزینی عکس واقعی دستگاه بهجای طرح SVG، فایل
|
||||
`src/components/VendingMachineParts.js` را ویرایش کنید.
|
||||
- برای اتصال فرم "سفارش و مشاوره" یا دکمه "افزودن به سبد" به بکاند واقعی،
|
||||
از توابع آماده در `src/utils/api.js` استفاده کنید.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "**",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
Generated
+1883
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "robot-market",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.2.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"gsap": "^3.12.5",
|
||||
"axios": "^1.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f2fffd;
|
||||
color: #04211d;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #00a693;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Thin scrollbar, brand-tinted */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f2fffd;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #00a693;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.blueprint-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 166, 147, 0.09) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 166, 147, 0.09) 1px, transparent 1px);
|
||||
background-size: 42px 42px;
|
||||
}
|
||||
|
||||
.corner-brackets {
|
||||
position: relative;
|
||||
}
|
||||
.corner-brackets::before,
|
||||
.corner-brackets::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-color: #00a693;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.corner-brackets::before {
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
border-top: 2px solid;
|
||||
border-right: 2px solid;
|
||||
}
|
||||
.corner-brackets::after {
|
||||
bottom: -6px;
|
||||
left: -6px;
|
||||
border-bottom: 2px solid;
|
||||
border-left: 2px solid;
|
||||
}
|
||||
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Vazirmatn } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const vazir = Vazirmatn({
|
||||
subsets: ["arabic"],
|
||||
weight: ["300", "400", "500", "600", "700", "800", "900"],
|
||||
variable: "--font-vazir",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata = {
|
||||
title: "روبات مارکت | تولیدکننده دستگاههای وندینگ ماشین صنعتی",
|
||||
description:
|
||||
"روبات مارکت، تولیدکننده تخصصی دستگاههای خودپرداز (وندینگ ماشین) خنککننده با استانداردهای مهندسی روز و بازگشت سرمایه مطمئن. سفارش و مشاوره رایگان.",
|
||||
keywords: [
|
||||
"وندینگ ماشین",
|
||||
"دستگاه خودپرداز",
|
||||
"روبات مارکت",
|
||||
"دستگاه فروش خودکار",
|
||||
"vending machine",
|
||||
],
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="fa" dir="rtl" className={vazir.variable}>
|
||||
<body className="font-vazir antialiased">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Header from "@/components/Header";
|
||||
import Hero from "@/components/Hero";
|
||||
import About from "@/components/About";
|
||||
import Locations from "@/components/Locations";
|
||||
import Products from "@/components/Products";
|
||||
import Footer from "@/components/Footer";
|
||||
import ScrollRefresh from "@/components/ScrollRefresh";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
<ScrollRefresh />
|
||||
<Header />
|
||||
<Hero />
|
||||
<About />
|
||||
<Locations />
|
||||
<Products />
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import {useEffect, useRef} from "react";
|
||||
import {gsap} from "@/utils/gsap";
|
||||
|
||||
const TIMELINE = [
|
||||
{
|
||||
year: "۱۳۹۶",
|
||||
title: "آغاز فعالیت در تجهیزات زعفرانکوبی",
|
||||
desc: "شروع کار با طراحی و تولید ماشینآلات فرآوری زعفران با تمرکز بر دقت مهندسی و کیفیت ساخت داخلی.",
|
||||
},
|
||||
{
|
||||
year: "۱۴۰۰",
|
||||
title: "ورود به صنعت اتوماسیون فروش",
|
||||
desc: "توسعه دانش فنی تیم مهندسی به سمت سامانههای خردهفروشی خودکار و مکانیزمهای دیسپنس هوشمند.",
|
||||
},
|
||||
{
|
||||
year: "۱۴۰۲",
|
||||
title: "تولید نخستین وندینگ ماشین خنککننده",
|
||||
desc: "معرفی نسل اول دستگاههای خودپرداز خنککننده روبات مارکت با استاندارد سرمایش صنعتی.",
|
||||
},
|
||||
{
|
||||
year: "۱۴۰۳",
|
||||
title: "توسعه خط تولید و شبکه فروش سراسری",
|
||||
desc: "افزایش ظرفیت تولید، تنوع مدلها (۳۵ تا ۶۰ کانال) و پشتیبانی فنی در سراسر کشور.",
|
||||
},
|
||||
];
|
||||
|
||||
const STANDARDS = [
|
||||
{label: "استاندارد مهندسی برق و ایمنی صنعتی"},
|
||||
{label: "بدنه فولادی ضدضربه با پوشش ضدزنگ"},
|
||||
{label: "سیستم سرمایش کمپرسوری استاندارد"},
|
||||
{label: "بازگشت سرمایه (ROI) مطمئن و قابل پیشبینی"},
|
||||
];
|
||||
|
||||
export default function About() {
|
||||
const rootRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const ctx = gsap.context(() => {
|
||||
gsap.utils.toArray(".timeline-item").forEach((item, i) => {
|
||||
gsap.fromTo(
|
||||
item,
|
||||
{opacity: 0, y: 30},
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.7,
|
||||
ease: "power2.out",
|
||||
scrollTrigger: {
|
||||
trigger: item,
|
||||
start: "top 85%",
|
||||
},
|
||||
delay: i * 0.05,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
gsap.fromTo(
|
||||
".timeline-line",
|
||||
{scaleY: 0},
|
||||
{
|
||||
scaleY: 1,
|
||||
transformOrigin: "top",
|
||||
ease: "none",
|
||||
scrollTrigger: {
|
||||
trigger: ".timeline-wrap",
|
||||
start: "top 70%",
|
||||
end: "bottom 80%",
|
||||
scrub: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
}, rootRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="about" ref={rootRef} className="relative py-24 md:py-32 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-5 md:px-8">
|
||||
<div className="grid lg:grid-cols-[0.9fr_1.1fr] gap-16">
|
||||
{/* Left: intro + standards */}
|
||||
<div>
|
||||
<span className="text-xs font-bold text-primary tracking-wide">معرفی شرکت</span>
|
||||
<h2 className="mt-3 text-3xl md:text-4xl font-black text-ink-900 leading-snug">
|
||||
مسیری مهندسیشده از صنعت زعفران تا اتوماسیون فروش
|
||||
</h2>
|
||||
<p className="mt-5 text-ink-500 leading-8">
|
||||
روبات مارکت با پیشینهای در طراحی ماشینآلات دقیق صنعتی، امروز یکی از تولیدکنندگان
|
||||
تخصصی وندینگ ماشینهای خنککننده در ایران است. تیم مهندسی ما هر دستگاه را با
|
||||
استانداردهای برق صنعتی، ایمنی مصرفکننده و دوام بلندمدت طراحی میکند تا سرمایهگذاری
|
||||
شما با کمترین ریسک و بیشترین بازده همراه باشد.
|
||||
</p>
|
||||
|
||||
<ul className="mt-8 space-y-4">
|
||||
{STANDARDS.map((s) => (
|
||||
<li key={s.label} className="flex items-center gap-3">
|
||||
<span
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary-light text-primary">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
|
||||
<path d="M5 13l4 4L19 7" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="text-sm md:text-base text-ink-700 font-medium">{s.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Right: timeline */}
|
||||
<div className="timeline-wrap relative pr-8">
|
||||
<div className="timeline-line absolute right-[7px] top-1 bottom-1 w-[2px] bg-primary/25"/>
|
||||
<div className="space-y-10">
|
||||
{TIMELINE.map((t) => (
|
||||
<div key={t.year} className="timeline-item relative opacity-0">
|
||||
<span
|
||||
className="absolute right-[-25px] top-1 h-4 w-4 rounded-full bg-primary ring-4 ring-primary-light"/>
|
||||
<span className="inline-block text-primary font-black text-lg mb-1">
|
||||
{t.year}
|
||||
</span>
|
||||
<h3 className="font-bold text-ink-900 text-base md:text-lg">{t.title}</h3>
|
||||
<p className="text-sm text-ink-500 mt-1.5 leading-7">{t.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { CONTACT, NAV_LINKS } from "@/data/content";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer id="contact" className="relative bg-ink-900 text-white pt-20 pb-8">
|
||||
<div className="max-w-7xl mx-auto px-5 md:px-8">
|
||||
<div className="grid md:grid-cols-[1.2fr_1fr_1fr] gap-12 pb-14 border-b border-white/10">
|
||||
{/* Brand + CTA */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary text-white font-black text-sm">
|
||||
RM
|
||||
</span>
|
||||
<span className="font-extrabold text-lg">روبات مارکت</span>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-white/60 leading-7 max-w-sm">
|
||||
تولیدکننده تخصصی دستگاههای خودپرداز خنککننده با استانداردهای مهندسی روز، برای
|
||||
فروش هوشمند و بیوقفه در هر مکان.
|
||||
</p>
|
||||
<a
|
||||
href={`tel:${CONTACT.phoneHref}`}
|
||||
className="mt-6 inline-flex items-center gap-2 rounded-full bg-primary px-6 py-3 text-sm font-bold hover:bg-primary-hover transition-colors"
|
||||
>
|
||||
سفارش و مشاوره رایگان
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<div>
|
||||
<h4 className="font-bold text-sm text-white/80 mb-4">دسترسی سریع</h4>
|
||||
<ul className="space-y-3">
|
||||
{NAV_LINKS.map((l) => (
|
||||
<li key={l.href}>
|
||||
<a href={l.href} className="text-sm text-white/60 hover:text-primary transition-colors">
|
||||
{l.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div>
|
||||
<h4 className="font-bold text-sm text-white/80 mb-4">اطلاعات تماس</h4>
|
||||
<ul className="space-y-3 text-sm text-white/60 leading-7">
|
||||
<li>{CONTACT.factory}</li>
|
||||
<li>{CONTACT.office}</li>
|
||||
<li dir="ltr" className="text-right">
|
||||
<a href={`tel:${CONTACT.phoneHref}`} className="hover:text-primary transition-colors">
|
||||
{CONTACT.phone}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="mt-5 flex items-center gap-3">
|
||||
<a
|
||||
href={CONTACT.instagram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="اینستاگرام روبات مارکت"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full bg-white/10 hover:bg-primary transition-colors"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
<rect x="3" y="3" width="18" height="18" rx="5" />
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<circle cx="17.5" cy="6.5" r="1" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href={CONTACT.bale}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="بله روبات مارکت"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full bg-white/10 hover:bg-primary transition-colors text-xs font-bold"
|
||||
>
|
||||
بله
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 flex flex-col md:flex-row items-center justify-between gap-3 text-xs text-white/40">
|
||||
<span>© {new Date().getFullYear()} روبات مارکت. تمامی حقوق محفوظ است.</span>
|
||||
<span>طراحی و توسعه با ❤ در مشهد</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { NAV_LINKS, CONTACT } from "@/data/content";
|
||||
|
||||
export default function Header() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 24);
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`fixed top-0 inset-x-0 z-50 transition-all duration-300 ${
|
||||
scrolled
|
||||
? "bg-white/90 backdrop-blur-md shadow-[0_1px_0_0_rgba(0,166,147,0.15)]"
|
||||
: "bg-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 md:px-8 flex items-center justify-between h-14 md:h-20">
|
||||
{/* Logo */}
|
||||
<a href="#top" className="flex items-center gap-2 group min-w-0">
|
||||
<span className="relative flex h-7 w-7 md:h-9 md:w-9 shrink-0 items-center justify-center rounded-lg bg-primary text-white font-black text-[10px] md:text-sm">
|
||||
RM
|
||||
<span className="absolute inset-0 rounded-lg ring-2 ring-primary/30 scale-110 group-hover:scale-125 transition-transform" />
|
||||
</span>
|
||||
<span className="flex flex-col leading-tight min-w-0">
|
||||
<span className="font-extrabold text-ink-900 text-xs sm:text-sm md:text-lg truncate">
|
||||
روبات مارکت
|
||||
</span>
|
||||
<span className="hidden sm:block text-[10px] md:text-xs text-ink-500 tracking-wide truncate">
|
||||
تولیدکننده وندینگ ماشین صنعتی
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
{/* Desktop nav */}
|
||||
<nav className="hidden lg:flex items-center gap-8">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm font-medium text-ink-700 hover:text-primary transition-colors relative after:absolute after:-bottom-1 after:right-0 after:h-[2px] after:w-0 after:bg-primary after:transition-all hover:after:w-full"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="hidden lg:flex items-center gap-4">
|
||||
<a
|
||||
href={`tel:${CONTACT.phoneHref}`}
|
||||
dir="ltr"
|
||||
className="text-sm font-semibold text-ink-700 hover:text-primary transition-colors"
|
||||
>
|
||||
{CONTACT.phone}
|
||||
</a>
|
||||
<a
|
||||
href="#contact"
|
||||
className="rounded-full bg-primary px-5 py-2.5 text-sm font-bold text-white shadow-lg shadow-primary/20 hover:bg-primary-hover hover:-translate-y-0.5 transition-all"
|
||||
>
|
||||
سفارش و مشاوره
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Mobile toggle */}
|
||||
<button
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className="lg:hidden flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-primary/20 text-primary"
|
||||
aria-label="باز کردن منو"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
{open ? (
|
||||
<path d="M6 6l12 12M18 6L6 18" strokeLinecap="round" />
|
||||
) : (
|
||||
<path d="M3 6h18M3 12h18M3 18h18" strokeLinecap="round" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{open && (
|
||||
<div className="lg:hidden bg-white border-t border-primary/10 px-4 py-4 flex flex-col gap-3">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className="text-sm font-medium text-ink-700"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href="#contact"
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded-full bg-primary px-5 py-2.5 text-sm font-bold text-white text-center"
|
||||
>
|
||||
سفارش و مشاوره
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
"use client";
|
||||
|
||||
import {useEffect, useRef} from "react";
|
||||
import {VMTop, VMMiddle, VMBottom} from "./VendingMachineParts";
|
||||
import {gsap, ScrollTrigger} from "@/utils/gsap";
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
id: "ui",
|
||||
label: "نمایشگر لمسی هوشمند",
|
||||
desc: "رابط کاربری فارسی و انتخاب سریع کالا",
|
||||
top: "8%",
|
||||
side: "left",
|
||||
},
|
||||
{
|
||||
id: "cooling",
|
||||
label: "سیستم خنککننده صنعتی",
|
||||
desc: "کمپرسور مستقل با کنترل دمای دقیق",
|
||||
top: "42%",
|
||||
side: "right",
|
||||
},
|
||||
{
|
||||
id: "payment",
|
||||
label: "ترمینال پرداخت هوشمند",
|
||||
desc: "کارتخوان بانکی و پرداخت آنلاین",
|
||||
top: "78%",
|
||||
side: "left",
|
||||
},
|
||||
{
|
||||
id: "body",
|
||||
label: "بدنه فولادی ضدضربه",
|
||||
desc: "مقاوم در برابر سرقت و ضربه",
|
||||
top: "60%",
|
||||
side: "right",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Hero() {
|
||||
const sectionRef = useRef(null);
|
||||
const topRef = useRef(null);
|
||||
const midRef = useRef(null);
|
||||
const botRef = useRef(null);
|
||||
const calloutsRef = useRef([]);
|
||||
const eyebrowRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const mm = gsap.matchMedia();
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
mm.add(
|
||||
{
|
||||
isDesktop: "(min-width: 1024px)",
|
||||
isMobile: "(max-width: 1023px)",
|
||||
},
|
||||
(context) => {
|
||||
const {isMobile} = context.conditions;
|
||||
const distance = isMobile ? 120 : 220;
|
||||
const scrollLength = isMobile ? 1200 : 1800;
|
||||
|
||||
const tl = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: sectionRef.current,
|
||||
start: "top top",
|
||||
end: `+=${scrollLength}`,
|
||||
scrub: 0.8,
|
||||
pin: true,
|
||||
anticipatePin: 1,
|
||||
invalidateOnRefresh: true,
|
||||
},
|
||||
});
|
||||
|
||||
tl.fromTo(
|
||||
topRef.current,
|
||||
{y: -distance, opacity: 0.4},
|
||||
{y: 0, opacity: 1, duration: 1, ease: "power2.out"},
|
||||
0
|
||||
)
|
||||
.fromTo(
|
||||
botRef.current,
|
||||
{y: distance, opacity: 0.4},
|
||||
{y: 0, opacity: 1, duration: 1, ease: "power2.out"},
|
||||
0
|
||||
)
|
||||
.fromTo(
|
||||
midRef.current,
|
||||
{opacity: 0, scale: 0.92},
|
||||
{opacity: 1, scale: 1, duration: 1, ease: "power2.out"},
|
||||
0.15
|
||||
)
|
||||
.to({}, {duration: 0.35}) // brief hold once assembled (pin breathing room)
|
||||
.fromTo(
|
||||
calloutsRef.current,
|
||||
{opacity: 0, x: (i) => (i % 2 === 0 ? -24 : 24)},
|
||||
{opacity: 1, x: 0, stagger: 0.15, duration: 0.6, ease: "power2.out"},
|
||||
">-0.1"
|
||||
)
|
||||
.fromTo(
|
||||
eyebrowRef.current,
|
||||
{opacity: 1},
|
||||
{opacity: 0.5, duration: 0.3},
|
||||
"<"
|
||||
);
|
||||
|
||||
return () => tl.scrollTrigger?.kill();
|
||||
}
|
||||
);
|
||||
}, sectionRef);
|
||||
|
||||
// Fonts/SVG layout can settle a beat after this effect runs — refresh
|
||||
// once more on the next frame so the pin/scrub distances are accurate
|
||||
// on the very first load, not just after a manual scroll-refresh.
|
||||
const raf = requestAnimationFrame(() => ScrollTrigger.refresh());
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
ctx.revert();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="top"
|
||||
ref={sectionRef}
|
||||
className="relative min-h-screen bg-primary-light blueprint-grid pt-16 sm:pt-20 md:pt-28"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-5 md:px-8 grid lg:grid-cols-2 gap-10 items-center h-full">
|
||||
{/* Copy */}
|
||||
<div className="order-2 lg:order-none relative z-10 pb-10 lg:pb-0 text-center lg:text-right">
|
||||
<span
|
||||
ref={eyebrowRef}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-primary/30 bg-white px-3 md:px-4 py-1.5 text-[11px] md:text-xs font-bold text-primary"
|
||||
>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-primary animate-pulse"/>
|
||||
از تجهیزات زعفرانکوبی ۱۳۹۶ تا وندینگ ماشین هوشمند
|
||||
</span>
|
||||
<h1 className="mt-6 text-3xl sm:text-4xl md:text-6xl font-black text-ink-900 leading-[1.2] text-balance">
|
||||
روبات مارکت،
|
||||
<span className="text-primary"> مهندسی فروش خودکار</span> برای کسبوکار شما
|
||||
</h1>
|
||||
<p className="mt-5 text-sm sm:text-base md:text-lg text-ink-500 leading-7 md:leading-8 max-w-xl mx-auto lg:mx-0">
|
||||
تولیدکننده وندینگ ماشینهای خنککننده صنعتی با استانداردهای مهندسی روز، طراحیشده
|
||||
برای فروش بدونوقفه، ۲۴ ساعته و بازگشت سرمایه مطمئن.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap justify-center lg:justify-start gap-4">
|
||||
<a
|
||||
href="#products"
|
||||
className="rounded-full bg-primary px-6 md:px-7 py-3 md:py-3.5 text-sm font-bold text-white shadow-lg shadow-primary/25 hover:bg-primary-hover hover:-translate-y-0.5 transition-all"
|
||||
>
|
||||
مشاهده محصولات
|
||||
</a>
|
||||
<a
|
||||
href="#contact"
|
||||
className="rounded-full border-2 border-primary/30 px-6 md:px-7 py-3 md:py-3.5 text-sm font-bold text-ink-900 hover:border-primary hover:text-primary transition-all"
|
||||
>
|
||||
سفارش و مشاوره رایگان
|
||||
</a>
|
||||
</div>
|
||||
<p className="mt-6 text-xs text-ink-500">
|
||||
برای مشاهده اسکرول کنید — دستگاه بهصورت زنده در برابر چشمان شما مونتاژ میشود
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* GSAP Assembly Machine */}
|
||||
<div
|
||||
className="order-1 lg:order-none relative h-[46vh] sm:h-[55vh] lg:h-[80vh] flex items-center justify-center">
|
||||
<div className="relative w-[170px] sm:w-[220px] md:w-[260px] lg:w-[300px]">
|
||||
<div ref={topRef} className="relative z-30 will-change-transform">
|
||||
<VMTop/>
|
||||
</div>
|
||||
<div ref={midRef} className="relative z-20 -mt-px will-change-transform">
|
||||
<VMMiddle/>
|
||||
</div>
|
||||
<div ref={botRef} className="relative z-30 -mt-px will-change-transform">
|
||||
<VMBottom/>
|
||||
</div>
|
||||
|
||||
{/* Feature callouts */}
|
||||
{FEATURES.map((f, i) => (
|
||||
<div
|
||||
key={f.id}
|
||||
ref={(el) => (calloutsRef.current[i] = el)}
|
||||
className={`hidden sm:flex absolute items-center gap-2 w-40 md:w-48 opacity-0 ${
|
||||
f.side === "left" ? "right-full ml-3 flex-row-reverse text-right" : "left-full mr-3 text-right"
|
||||
}`}
|
||||
style={{top: f.top}}
|
||||
>
|
||||
{f.side === "left" && (
|
||||
<span className="h-px w-6 md:w-10 bg-primary/60 shrink-0"/>
|
||||
)}
|
||||
<span>
|
||||
<span className="block text-xs md:text-sm font-bold text-ink-900">
|
||||
{f.label}
|
||||
</span>
|
||||
<span className="block text-[10px] md:text-xs text-ink-500">{f.desc}</span>
|
||||
</span>
|
||||
{f.side === "right" && (
|
||||
<span className="h-px w-6 md:w-10 bg-primary/60 shrink-0"/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile feature list (callout lines are desktop-only) */}
|
||||
<div className="sm:hidden max-w-md mx-auto px-5 pb-10 grid grid-cols-2 gap-3">
|
||||
{FEATURES.map((f) => (
|
||||
<div key={f.id} className="rounded-xl bg-white/70 border border-primary/15 p-3">
|
||||
<p className="text-xs font-bold text-ink-900">{f.label}</p>
|
||||
<p className="text-[10px] text-ink-500 mt-1">{f.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
export function Icon({ name, className = "h-6 w-6" }) {
|
||||
const common = {
|
||||
className,
|
||||
viewBox: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
strokeWidth: 1.8,
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
};
|
||||
|
||||
switch (name) {
|
||||
case "hotel":
|
||||
return (
|
||||
<svg {...common}>
|
||||
<path d="M3 21V7a1 1 0 0 1 1-1h4v15" />
|
||||
<path d="M8 21V3h9a1 1 0 0 1 1 1v17" />
|
||||
<path d="M3 21h18" />
|
||||
<path d="M11 7h4M11 11h4M11 15h4" />
|
||||
</svg>
|
||||
);
|
||||
case "school":
|
||||
return (
|
||||
<svg {...common}>
|
||||
<path d="M2 9l10-5 10 5-10 5-10-5z" />
|
||||
<path d="M6 11v5c0 1.5 2.5 3 6 3s6-1.5 6-3v-5" />
|
||||
<path d="M22 9v6" />
|
||||
</svg>
|
||||
);
|
||||
case "pharmacy":
|
||||
return (
|
||||
<svg {...common}>
|
||||
<rect x="3" y="7" width="18" height="14" rx="2" />
|
||||
<path d="M12 11v6M9 14h6" />
|
||||
<path d="M8 7V5a4 4 0 0 1 8 0v2" />
|
||||
</svg>
|
||||
);
|
||||
case "hospital":
|
||||
return (
|
||||
<svg {...common}>
|
||||
<rect x="4" y="3" width="16" height="18" rx="2" />
|
||||
<path d="M12 8v6M9 11h6" />
|
||||
<path d="M9 21v-3M15 21v-3" />
|
||||
</svg>
|
||||
);
|
||||
case "moon":
|
||||
return (
|
||||
<svg {...common}>
|
||||
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
|
||||
</svg>
|
||||
);
|
||||
case "brand":
|
||||
return (
|
||||
<svg {...common}>
|
||||
<path d="M12 2l2.4 6.6L21 11l-6.6 2.4L12 20l-2.4-6.6L3 11l6.6-2.4z" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { LOCATIONS } from "@/data/content";
|
||||
import { Icon } from "./Icons";
|
||||
import { gsap } from "@/utils/gsap";
|
||||
|
||||
export default function Locations() {
|
||||
const rootRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const ctx = gsap.context(() => {
|
||||
gsap.fromTo(
|
||||
".location-card",
|
||||
{ opacity: 0, y: 36 },
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.6,
|
||||
ease: "power2.out",
|
||||
stagger: 0.08,
|
||||
scrollTrigger: {
|
||||
trigger: ".location-grid",
|
||||
start: "top 82%",
|
||||
},
|
||||
}
|
||||
);
|
||||
}, rootRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="locations" ref={rootRef} className="relative py-24 md:py-32 bg-primary-light">
|
||||
<div className="max-w-7xl mx-auto px-5 md:px-8">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<span className="text-xs font-bold text-primary tracking-wide">مکانهای مناسب نصب</span>
|
||||
<h2 className="mt-3 text-3xl md:text-4xl font-black text-ink-900">
|
||||
هرجا نیاز به فروش بیوقفه باشد، جای وندینگ ماشین است
|
||||
</h2>
|
||||
<p className="mt-4 text-ink-500 leading-7">
|
||||
دستگاههای روبات مارکت برای انطباق با فضاهای مختلف طراحی شدهاند؛ از فضاهای پرتردد تا
|
||||
نصب پشت ویترین برای فروش شبانه.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="location-grid mt-14 grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{LOCATIONS.map((loc) => (
|
||||
<div
|
||||
key={loc.id}
|
||||
className="location-card group relative rounded-2xl bg-white p-6 border border-primary/10 shadow-sm hover:shadow-xl hover:shadow-primary/10 hover:-translate-y-1 transition-all duration-300 opacity-0"
|
||||
>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-light text-primary group-hover:bg-primary group-hover:text-white transition-colors">
|
||||
<Icon name={loc.icon} />
|
||||
</div>
|
||||
<h3 className="mt-5 font-bold text-ink-900 text-lg">{loc.title}</h3>
|
||||
<p className="mt-2 text-sm text-ink-500 leading-7">{loc.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { PRODUCTS, formatRial } from "@/data/content";
|
||||
import { gsap } from "@/utils/gsap";
|
||||
|
||||
export default function Products() {
|
||||
const rootRef = useRef(null);
|
||||
const [added, setAdded] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const ctx = gsap.context(() => {
|
||||
gsap.fromTo(
|
||||
".product-card",
|
||||
{ opacity: 0, y: 40 },
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.7,
|
||||
ease: "power2.out",
|
||||
stagger: 0.12,
|
||||
scrollTrigger: {
|
||||
trigger: ".product-grid",
|
||||
start: "top 80%",
|
||||
},
|
||||
}
|
||||
);
|
||||
}, rootRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
const handleAdd = (id) => {
|
||||
setAdded((prev) => ({ ...prev, [id]: true }));
|
||||
// Static demo action — wire this to `submitConsultationRequest` in
|
||||
// src/utils/api.js once a cart/order API is available.
|
||||
setTimeout(() => setAdded((prev) => ({ ...prev, [id]: false })), 2200);
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="products" ref={rootRef} className="relative py-24 md:py-32 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-5 md:px-8">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<span className="text-xs font-bold text-primary tracking-wide">محصولات تخصصی</span>
|
||||
<h2 className="mt-3 text-3xl md:text-4xl font-black text-ink-900">
|
||||
مدلهای وندینگ ماشین روبات مارکت
|
||||
</h2>
|
||||
<p className="mt-4 text-ink-500 leading-7">
|
||||
سه مدل با ظرفیتهای مختلف، متناسب با ترافیک و فضای محل نصب شما.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="product-grid mt-14 grid md:grid-cols-3 gap-6">
|
||||
{PRODUCTS.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className={`product-card opacity-0 relative rounded-3xl p-7 flex flex-col border transition-all duration-300 ${
|
||||
p.highlight
|
||||
? "bg-ink-900 border-primary text-white lg:-translate-y-4 shadow-2xl shadow-primary/20"
|
||||
: "bg-primary-light border-primary/10 text-ink-900 hover:-translate-y-2 hover:shadow-xl"
|
||||
}`}
|
||||
>
|
||||
{p.highlight && (
|
||||
<span className="absolute -top-3 right-7 rounded-full bg-primary px-4 py-1 text-[11px] font-bold text-white">
|
||||
پرفروشترین
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={`text-xs font-bold tracking-wide ${
|
||||
p.highlight ? "text-info" : "text-primary"
|
||||
}`}
|
||||
>
|
||||
{p.channels} کانال
|
||||
</span>
|
||||
<h3 className="mt-2 text-xl font-black">{p.name}</h3>
|
||||
<p className={`mt-1 text-sm ${p.highlight ? "text-white/60" : "text-ink-500"}`}>
|
||||
{p.tagline}
|
||||
</p>
|
||||
|
||||
<div className="mt-6">
|
||||
<span className="text-2xl md:text-3xl font-black" dir="ltr">
|
||||
{formatRial(p.price)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul className="mt-6 space-y-3 flex-1">
|
||||
{p.features.map((f) => (
|
||||
<li key={f} className="flex items-start gap-2.5 text-sm">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
className={`mt-0.5 shrink-0 ${p.highlight ? "text-info" : "text-primary"}`}
|
||||
>
|
||||
<path d="M5 13l4 4L19 7" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<span className={p.highlight ? "text-white/85" : "text-ink-700"}>{f}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
onClick={() => handleAdd(p.id)}
|
||||
className={`mt-8 w-full rounded-full py-3.5 text-sm font-bold transition-all ${
|
||||
p.highlight
|
||||
? "bg-primary text-white hover:bg-primary-hover"
|
||||
: "bg-ink-900 text-white hover:bg-primary"
|
||||
} ${added[p.id] ? "!bg-success" : ""}`}
|
||||
>
|
||||
{added[p.id] ? "به سبد سفارش اضافه شد ✓" : "افزودن به سبد سفارش"}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-center text-xs text-ink-500">
|
||||
قیمتها بهروزرسانی دورهای دارند؛ برای استعلام نهایی و امکان پرداخت اقساطی با کارشناسان
|
||||
فروش تماس بگیرید.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { ScrollTrigger } from "@/utils/gsap";
|
||||
|
||||
/**
|
||||
* Mounted once near the root of the page. Fonts (Vazirmatn) and the
|
||||
* inline SVG artwork can finish loading/laying out slightly after
|
||||
* ScrollTrigger has already taken its initial measurements, which makes
|
||||
* pinned sections (like the Hero assembly) start blank until the user
|
||||
* scrolls back up and down again. Forcing a refresh once everything has
|
||||
* settled fixes that on the very first load.
|
||||
*/
|
||||
export default function ScrollRefresh() {
|
||||
useEffect(() => {
|
||||
const refresh = () => ScrollTrigger.refresh();
|
||||
|
||||
if (document.fonts && document.fonts.ready) {
|
||||
document.fonts.ready.then(refresh);
|
||||
}
|
||||
|
||||
window.addEventListener("load", refresh);
|
||||
// Fallback in case fonts/images settle without firing the above events.
|
||||
const t1 = setTimeout(refresh, 300);
|
||||
const t2 = setTimeout(refresh, 1000);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("load", refresh);
|
||||
clearTimeout(t1);
|
||||
clearTimeout(t2);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Schematic / blueprint-style artwork for the vending machine, split into
|
||||
// three physically-accurate parts so the Hero section can animate them
|
||||
// together with GSAP. Replace with real product photography later by
|
||||
// swapping the <svg> content inside each part — the wrapper sizes/ids stay.
|
||||
|
||||
export function VMTop() {
|
||||
return (
|
||||
<svg viewBox="0 0 400 150" className="w-full block" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 150 V40 Q20 10 50 10 H350 Q380 10 380 40 V150 Z" fill="#0c3a34" />
|
||||
<path d="M20 150 V40 Q20 10 50 10 H350 Q380 10 380 40 V150 Z" fill="url(#topGrad)" opacity="0.9" />
|
||||
<defs>
|
||||
<linearGradient id="topGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stopColor="#0f4a42" />
|
||||
<stop offset="1" stopColor="#0c3a34" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* brand plate */}
|
||||
<rect x="150" y="28" width="100" height="16" rx="4" fill="#00a693" />
|
||||
<text x="200" y="40" textAnchor="middle" fontSize="10" fill="#f2fffd" fontFamily="sans-serif" fontWeight="700">
|
||||
ROBOT MARKET
|
||||
</text>
|
||||
{/* smart UI screen */}
|
||||
<rect x="130" y="58" width="140" height="80" rx="8" fill="#04211d" stroke="#00bba6" strokeWidth="2" />
|
||||
<rect x="140" y="68" width="120" height="46" rx="4" fill="#00a693" opacity="0.25" />
|
||||
<rect x="140" y="68" width="70" height="6" rx="3" fill="#00bba6" />
|
||||
<rect x="140" y="80" width="90" height="4" rx="2" fill="#00bba6" opacity="0.6" />
|
||||
<rect x="140" y="90" width="60" height="4" rx="2" fill="#00bba6" opacity="0.6" />
|
||||
<circle cx="250" cy="126" r="6" fill="#00bba6" />
|
||||
{/* cooling vents */}
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<rect key={i} x={36} y={34 + i * 8} width="60" height="3" rx="1.5" fill="#00bba6" opacity="0.5" />
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function VMMiddle() {
|
||||
return (
|
||||
<svg viewBox="0 0 400 300" className="w-full block" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="20" y="0" width="360" height="300" fill="#0e4841" />
|
||||
{/* glass front */}
|
||||
<rect x="42" y="18" width="316" height="264" rx="6" fill="#04211d" stroke="#00a693" strokeWidth="2" />
|
||||
{/* product coils - 3 rows x 4 columns */}
|
||||
{[0, 1, 2].map((row) => (
|
||||
<g key={row}>
|
||||
{[0, 1, 2, 3].map((col) => (
|
||||
<g key={col}>
|
||||
<rect
|
||||
x={58 + col * 74}
|
||||
y={34 + row * 84}
|
||||
width="60"
|
||||
height="70"
|
||||
rx="4"
|
||||
fill="#0c3a34"
|
||||
stroke="#00bba6"
|
||||
strokeOpacity="0.5"
|
||||
/>
|
||||
{[0, 1, 2, 3].map((c) => (
|
||||
<circle
|
||||
key={c}
|
||||
cx={58 + col * 74 + 12 + c * 12}
|
||||
cy={34 + row * 84 + 35}
|
||||
r="9"
|
||||
fill="#00a693"
|
||||
opacity="0.65"
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
))}
|
||||
</g>
|
||||
))}
|
||||
{/* LED strip */}
|
||||
<rect x="20" y="0" width="8" height="300" fill="#00bba6" opacity="0.8" />
|
||||
<rect x="372" y="0" width="8" height="300" fill="#00bba6" opacity="0.8" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function VMBottom() {
|
||||
return (
|
||||
<svg viewBox="0 0 400 150" className="w-full block" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 0 H380 V110 Q380 140 350 140 H50 Q20 140 20 110 Z" fill="#0c3a34" />
|
||||
{/* payment terminal */}
|
||||
<rect x="70" y="20" width="90" height="60" rx="6" fill="#04211d" stroke="#00a693" strokeWidth="2" />
|
||||
<rect x="80" y="30" width="70" height="16" rx="3" fill="#00a693" opacity="0.4" />
|
||||
<rect x="80" y="52" width="30" height="18" rx="3" fill="#00bba6" />
|
||||
<text x="95" y="65" textAnchor="middle" fontSize="9" fill="#04211d" fontWeight="700">
|
||||
POS
|
||||
</text>
|
||||
{/* coin slot + card reader */}
|
||||
<rect x="150" y="52" width="4" height="16" rx="2" fill="#00bba6" />
|
||||
{/* dispense flap */}
|
||||
<rect x="240" y="30" width="110" height="60" rx="8" fill="#04211d" stroke="#00bba6" strokeWidth="2" />
|
||||
<rect x="255" y="70" width="80" height="8" rx="4" fill="#00a693" />
|
||||
<text x="295" y="52" textAnchor="middle" fontSize="9" fill="#00bba6" fontFamily="sans-serif">
|
||||
محل تحویل کالا
|
||||
</text>
|
||||
{/* legs */}
|
||||
<rect x="60" y="132" width="16" height="16" rx="2" fill="#0c3a34" stroke="#00a693" />
|
||||
<rect x="324" y="132" width="16" height="16" rx="2" fill="#0c3a34" stroke="#00a693" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
export const NAV_LINKS = [
|
||||
{ label: "معرفی شرکت", href: "#about" },
|
||||
{ label: "مکانهای نصب", href: "#locations" },
|
||||
{ label: "محصولات", href: "#products" },
|
||||
{ label: "تماس با ما", href: "#contact" },
|
||||
];
|
||||
|
||||
export const LOCATIONS = [
|
||||
{
|
||||
id: "hotels",
|
||||
title: "هتلها و اقامتگاهها",
|
||||
desc: "دسترسی شبانهروزی مهمانان به نوشیدنی و تنقلات، بدون نیاز به نیروی انسانی اضافه در لابی.",
|
||||
icon: "hotel",
|
||||
},
|
||||
{
|
||||
id: "education",
|
||||
title: "مراکز آموزشی و دانشگاهها",
|
||||
desc: "پاسخگویی سریع به تراکم بالای دانشآموزان و دانشجویان در زنگهای تفریح و فاصله کلاسها.",
|
||||
icon: "school",
|
||||
},
|
||||
{
|
||||
id: "pharmacy",
|
||||
title: "داروخانهها و سوپرمارکتها",
|
||||
desc: "افزایش ساعات فروش فعال بدون افزایش هزینه پرسنل، مکمل فروش حضوری فروشگاه.",
|
||||
icon: "pharmacy",
|
||||
},
|
||||
{
|
||||
id: "hospital",
|
||||
title: "بیمارستانها و مراکز درمانی",
|
||||
desc: "تامین نیاز همراهان بیمار در تمام ساعات شبانهروز در راهروها و بخشهای انتظار.",
|
||||
icon: "hospital",
|
||||
},
|
||||
{
|
||||
id: "night-shift",
|
||||
title: "فروش شبانه پشت ویترین",
|
||||
desc: "نصب پشت شیشه مغازه برای فروش ۲۴ ساعته حتی در زمان تعطیلی فروشگاه، بدون ریسک امنیتی.",
|
||||
icon: "moon",
|
||||
},
|
||||
{
|
||||
id: "branding",
|
||||
title: "برندسازی اختصاصی",
|
||||
desc: "امکان طراحی نما و رابط کاربری دستگاه مطابق هویت بصری برند شما برای فضاهای تجاری.",
|
||||
icon: "brand",
|
||||
},
|
||||
];
|
||||
|
||||
export const PRODUCTS = [
|
||||
{
|
||||
id: "vm-35",
|
||||
channels: 35,
|
||||
name: "وندینگ ماشین ۳۵ کانال",
|
||||
tagline: "مناسب فضاهای کوچک و متوسط",
|
||||
price: 3860000000,
|
||||
features: [
|
||||
"ظرفیت ۳۵ کانال فروش مستقل",
|
||||
"سیستم خنککننده کمپرسوری",
|
||||
"نمایشگر لمسی هوشمند",
|
||||
"ترمینال پرداخت کارتخوان و آنلاین",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "vm-48",
|
||||
channels: 48,
|
||||
name: "وندینگ ماشین ۴۸ کانال",
|
||||
tagline: "پرفروشترین مدل، تعادل ظرفیت و ابعاد",
|
||||
price: 4480000000,
|
||||
features: [
|
||||
"ظرفیت ۴۸ کانال فروش مستقل",
|
||||
"کنترل دما دیجیتال دقیق",
|
||||
"پنل کاربری فارسی با گزارش فروش",
|
||||
"بدنه فولادی ضدضربه و ضدسرقت",
|
||||
],
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
id: "vm-60",
|
||||
channels: 60,
|
||||
name: "وندینگ ماشین ۶۰ کانال",
|
||||
tagline: "بیشترین ظرفیت، مناسب ترافیک بالا",
|
||||
price: 4980000000,
|
||||
features: [
|
||||
"ظرفیت ۶۰ کانال فروش مستقل",
|
||||
"دو کمپرسور مستقل برای پایداری دما",
|
||||
"اتصال اینترنتی و مانیتورینگ از راه دور",
|
||||
"طراحی مدولار برای شارژ سریع کالا",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const CONTACT = {
|
||||
factory: "کارخانه: مشهد، شهرک صنعتی توس",
|
||||
office: "دفتر مرکزی: مشهد، برج نخل",
|
||||
phone: "۰۹۱۵۱۶۰۱۸۴۷",
|
||||
phoneHref: "+989151601847",
|
||||
instagram: "https://instagram.com/robot.market",
|
||||
bale: "https://ble.ir/robot.market",
|
||||
};
|
||||
|
||||
export function formatRial(amount) {
|
||||
return amount.toLocaleString("fa-IR") + " ریال";
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* Central Axios instance for Robot Market.
|
||||
* Every future API call (orders, consultation requests, product sync, ...)
|
||||
* should go through this instance so headers, base URL and error
|
||||
* handling only need to be configured in one place.
|
||||
*/
|
||||
const api = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.robotmarket.ir/v1",
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
// Attach an auth token automatically, if one is available client-side.
|
||||
api.interceptors.request.use((config) => {
|
||||
if (typeof window !== "undefined") {
|
||||
const token = window.localStorage.getItem("rm_token");
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Normalize errors so components can rely on a consistent shape.
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const message =
|
||||
error?.response?.data?.message || error?.message || "خطای ناشناخته در ارتباط با سرور";
|
||||
return Promise.reject({ ...error, message });
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Example future usage:
|
||||
* submitConsultationRequest({ name, phone, city })
|
||||
*/
|
||||
export async function submitConsultationRequest(payload) {
|
||||
const { data } = await api.post("/leads/consultation", payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example future usage:
|
||||
* fetchProducts()
|
||||
*/
|
||||
export async function fetchProducts() {
|
||||
const { data } = await api.get("/products");
|
||||
return data;
|
||||
}
|
||||
|
||||
export default api;
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
|
||||
// Registering the plugin synchronously at module scope (guarded for SSR)
|
||||
// avoids the race condition where ScrollTrigger measures the page before
|
||||
// fonts/images have finished loading, which is what caused the "blank on
|
||||
// first load, fixed after scrolling back up" bug.
|
||||
if (typeof window !== "undefined") {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
// Mobile browsers fire resize events when the address bar shows/hides
|
||||
// while scrolling. ScrollTrigger would otherwise treat that as a real
|
||||
// viewport change and re-run its (expensive, sometimes wrong) pin
|
||||
// calculations mid-scroll, which is what made the assembled machine
|
||||
// disappear or freeze on phones.
|
||||
ScrollTrigger.config({ ignoreMobileResize: true });
|
||||
}
|
||||
|
||||
export { gsap, ScrollTrigger };
|
||||
@@ -0,0 +1,48 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/app/**/*.{js,jsx}",
|
||||
"./src/components/**/*.{js,jsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: "#00a693",
|
||||
hover: "#009b89",
|
||||
light: "#f2fffd",
|
||||
},
|
||||
success: "#00ab97",
|
||||
error: "#005148",
|
||||
info: "#00bba6",
|
||||
ink: {
|
||||
900: "#04211d",
|
||||
700: "#0c3a34",
|
||||
500: "#3a615c",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
vazir: ["var(--font-vazir)", "Tahoma", "sans-serif"],
|
||||
},
|
||||
backgroundImage: {
|
||||
"grid-pattern":
|
||||
"linear-gradient(rgba(0,166,147,0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(0,166,147,0.08) 1px, transparent 1px)",
|
||||
},
|
||||
keyframes: {
|
||||
floatY: {
|
||||
"0%, 100%": { transform: "translateY(0px)" },
|
||||
"50%": { transform: "translateY(-12px)" },
|
||||
},
|
||||
pulseRing: {
|
||||
"0%": { transform: "scale(0.9)", opacity: "0.8" },
|
||||
"100%": { transform: "scale(1.6)", opacity: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
floatY: "floatY 4s ease-in-out infinite",
|
||||
pulseRing: "pulseRing 2s ease-out infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
Reference in New Issue
Block a user