initial project

This commit is contained in:
Hamed
2026-07-29 12:42:13 +04:00
parent 7920aea3b5
commit e03a4dfcb0
25 changed files with 3330 additions and 2 deletions
+1
View File
@@ -0,0 +1 @@
NEXT_PUBLIC_API_BASE_URL=https://api.robotmarket.ir/v1
+7
View File
@@ -0,0 +1,7 @@
node_modules
.next
.env*.local
.DS_Store
out
build
*.log
+70
View File
@@ -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">{
&quot;associatedIndex&quot;: 2,
&quot;fromUser&quot;: false
}</component>
<component name="ProjectId" id="3H7KB12mCznfw4hRrFCPWKMzxsB" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;codeWithMe.voiceChat.enabledByDefault&quot;: &quot;false&quot;,
&quot;js.debugger.nextJs.config.created.client&quot;: &quot;true&quot;,
&quot;js.debugger.nextJs.config.created.server&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;D:/Project/robot-market-landing/robot-market&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</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>
+29 -2
View File
@@ -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` استفاده کنید.
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"jsx": "react"
}
}
+14
View File
@@ -0,0 +1,14 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
],
},
};
module.exports = nextConfig;
+1883
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -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"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+74
View File
@@ -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;
}
}
+30
View File
@@ -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>
);
}
+21
View File
@@ -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>
);
}
+132
View File
@@ -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>
);
}
+89
View File
@@ -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>
);
}
+111
View File
@@ -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>
);
}
+216
View File
@@ -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>
);
}
+61
View File
@@ -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;
}
}
+64
View File
@@ -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>
);
}
+127
View File
@@ -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>
);
}
+35
View File
@@ -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;
}
+103
View File
@@ -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>
);
}
+101
View File
@@ -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") + " ریال";
}
+56
View File
@@ -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;
+20
View File
@@ -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 };
+48
View File
@@ -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: [],
};