Initial commit for OakTasty website.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-08 10:41:35 +03:30
co-authored by Cursor
commit 5fd7d00563
76 changed files with 7516 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env files
.env*
!.env.example
# typescript
*.tsbuildinfo
next-env.d.ts
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+14
View File
@@ -0,0 +1,14 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [...compat.extends("next/core-web-vitals", "next/typescript")];
export default eslintConfig;
+11
View File
@@ -0,0 +1,11 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
dangerouslyAllowSVG: true,
contentDispositionType: "attachment",
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
},
};
export default nextConfig;
+5533
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"name": "oak-tasty",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^15.5.2",
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "^15.5.2",
"typescript": "^5"
}
}
+10
View File
@@ -0,0 +1,10 @@
create menu website - its for cafe - pastry and bakery
lang: fa
dir: rtl
use next js
current website is
https://www.oaktasty.com/menu
design home page only for now
use logo and color pallete only, its home page is a list of product
use static data for products
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" fill="none">
<rect width="480" height="480" fill="url(#g2)"/>
<circle cx="240" cy="210" r="78" fill="#BE762E" opacity="0.22"/>
<g transform="translate(190 150)" fill="#BE762E" opacity="0.65">
<rect x="42" y="0" width="22" height="18" rx="2" transform="rotate(12 53 9)"/>
<path d="M20 28h86c6 0 10 5 10 11v18c0 42-30 78-53 94-23-16-53-52-53-94V39c0-6 4-11 10-11z"/>
<rect x="14" y="18" width="98" height="28" rx="4"/>
</g>
<defs>
<linearGradient id="g2" x1="40" y1="20" x2="440" y2="460" gradientUnits="userSpaceOnUse">
<stop stop-color="#F3E4CF"/>
<stop offset="1" stop-color="#D9B98A"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 752 B

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" fill="none">
<rect width="480" height="480" fill="#EFE2CF"/>
<rect x="0" y="0" width="480" height="480" fill="url(#g)"/>
<g transform="translate(190 150)" fill="#BE762E" opacity="0.55">
<rect x="42" y="0" width="22" height="18" rx="2" transform="rotate(12 53 9)"/>
<path d="M20 28h86c6 0 10 5 10 11v18c0 42-30 78-53 94-23-16-53-52-53-94V39c0-6 4-11 10-11z"/>
<rect x="14" y="18" width="98" height="28" rx="4"/>
</g>
<defs>
<linearGradient id="g" x1="80" y1="40" x2="400" y2="440" gradientUnits="userSpaceOnUse">
<stop stop-color="#F7EFE3"/>
<stop offset="1" stop-color="#E8D3B4"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 745 B

+45
View File
@@ -0,0 +1,45 @@
import type { Metadata } from "next";
import { SiteChrome } from "@/components/SiteChrome";
import { aboutContent } from "@/data/site";
export const metadata: Metadata = {
title: "درباره ی اک | اُک",
description: aboutContent.intro,
};
export default function AboutPage() {
return (
<SiteChrome eyebrow="درباره ی اک">
<main className="page-main">
<section className="page-hero">
<p className="page-kicker" lang="en" dir="ltr">
{aboutContent.titleEn}
</p>
<h2 className="page-title">{aboutContent.title}</h2>
<p className="page-lead">{aboutContent.intro}</p>
</section>
<section className="page-block" aria-label="داستان اک">
{aboutContent.story.map((paragraph) => (
<p key={paragraph} className="page-text">
{paragraph}
</p>
))}
</section>
<section className="about-highlights" aria-label="ویژگی‌ها">
{aboutContent.highlights.map((item, index) => (
<article
key={item.title}
className="about-highlight"
style={{ animationDelay: `${index * 80}ms` }}
>
<h3>{item.title}</h3>
<p>{item.text}</p>
</article>
))}
</section>
</main>
</SiteChrome>
);
}
+72
View File
@@ -0,0 +1,72 @@
import type { Metadata } from "next";
import { ContactForm } from "@/components/ContactForm";
import { SiteChrome } from "@/components/SiteChrome";
import { contactContent } from "@/data/site";
export const metadata: Metadata = {
title: "تماس با ما | اُک",
description: contactContent.intro,
};
export default function ContactPage() {
return (
<SiteChrome eyebrow="تماس با ما">
<main className="page-main">
<section className="page-hero">
<p className="page-kicker" lang="en" dir="ltr">
{contactContent.titleEn}
</p>
<h2 className="page-title">{contactContent.title}</h2>
<p className="page-lead">{contactContent.intro}</p>
</section>
<section className="contact-details" aria-label="راه‌های ارتباطی">
<a className="contact-line" href={contactContent.phoneHref}>
<span className="contact-line__label">تلفن</span>
<span className="contact-line__value">{contactContent.phone}</span>
</a>
<a className="contact-line" href={contactContent.emailHref}>
<span className="contact-line__label">ایمیل</span>
<span className="contact-line__value" lang="en" dir="ltr">
{contactContent.email}
</span>
</a>
<div className="contact-line">
<span className="contact-line__label">آدرس</span>
<span className="contact-line__value">{contactContent.address}</span>
<span className="contact-line__hint">{contactContent.mapHint}</span>
</div>
</section>
<section className="page-block" aria-label="ساعات کاری">
<h3 className="page-subtitle">ساعات کاری</h3>
<ul className="hours-list">
{contactContent.hours.map((item) => (
<li key={item.day}>
<span>{item.day}</span>
<span className="hours-list__time">{item.time}</span>
</li>
))}
</ul>
</section>
<section className="page-block" aria-label="شبکه‌های اجتماعی">
<h3 className="page-subtitle">ما را دنبال کنید</h3>
<div className="social-links">
{contactContent.socials.map((item) => (
<a key={item.label} href={item.href} target="_blank" rel="noreferrer">
{item.label}
</a>
))}
</div>
</section>
<section className="page-block" aria-label="فرم تماس">
<h3 className="page-subtitle">ارسال پیام</h3>
<p className="page-text page-text--soft">{contactContent.formNote}</p>
<ContactForm />
</section>
</main>
</SiteChrome>
);
}
+872
View File
@@ -0,0 +1,872 @@
@font-face {
font-family: "Ayandeh";
src: url("/fonts/Ayandeh-Light.ttf") format("truetype");
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Ayandeh";
src: url("/fonts/Ayandeh-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Ayandeh";
src: url("/fonts/Ayandeh-Bold.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Ayandeh";
src: url("/fonts/Ayandeh-Black.ttf") format("truetype");
font-weight: 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "OakNum";
src: url("/fonts/iranyekanwebboldfanum.woff") format("woff");
font-weight: 700;
font-style: normal;
font-display: swap;
}
:root {
--oak-bg: #f5eadc;
--oak-primary: #be762e;
--oak-primary-soft: hsl(30, 53%, 57%);
--oak-ink: #3a2412;
--oak-muted: #9a7a5c;
--oak-white: #ffffff;
--oak-shadow: rgba(174, 121, 62, 0.2);
--oak-radius: 14px;
--oak-max: 504px;
--font-brand: "Ayandeh", "Segoe UI", Tahoma, sans-serif;
--font-num: "OakNum", "Ayandeh", sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
min-height: 100%;
}
html {
background:
radial-gradient(120% 80% at 50% -10%, rgba(190, 118, 46, 0.12), transparent 55%),
linear-gradient(180deg, #f8f0e4 0%, var(--oak-bg) 40%, #efe0cc 100%);
}
body {
font-family: var(--font-brand);
color: var(--oak-ink);
background: transparent;
-webkit-font-smoothing: antialiased;
}
a {
color: inherit;
text-decoration: none;
}
img {
display: block;
max-width: 100%;
}
button {
font: inherit;
border: 0;
background: none;
cursor: pointer;
color: inherit;
}
.app-shell {
width: min(100%, var(--oak-max));
min-height: 100vh;
min-height: 100dvh;
margin: 0 auto;
display: flex;
flex-direction: column;
padding: 12px 10px 28px;
}
.brand-header {
position: relative;
overflow: hidden;
background-color: var(--oak-white);
background-image: url("/img/pattern.png");
background-repeat: repeat;
border-radius: var(--oak-radius);
box-shadow: 0 0 20px var(--oak-shadow);
padding: 12px 14px 14px;
animation: rise 0.7s ease both;
scroll-margin-top: 12px;
}
.brand-header__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.brand-header__identity {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
color: inherit;
}
.brand-logo {
width: 64px;
height: auto;
flex-shrink: 0;
filter: drop-shadow(0 2px 6px rgba(190, 118, 46, 0.18));
}
.brand-name {
margin: 0;
font-size: clamp(0.95rem, 3.6vw, 1.15rem);
font-weight: 900;
line-height: 1.35;
color: var(--oak-primary);
letter-spacing: -0.01em;
}
.watermark {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
margin: 2px 0 12px;
font-weight: 700;
font-size: 0.95rem;
color: var(--oak-primary);
}
.watermark::before,
.watermark::after {
content: "";
width: 52px;
height: 10px;
background: url("/img/horizontal-pattern.png") center / contain no-repeat;
opacity: 0.9;
}
.hamburger {
width: 42px;
height: 42px;
flex-shrink: 0;
display: grid;
place-content: center;
gap: 5px;
border-radius: 10px;
background: rgba(190, 118, 46, 0.08);
transition: background 0.2s ease;
}
.hamburger:hover {
background: rgba(190, 118, 46, 0.14);
}
.hamburger span {
display: block;
width: 18px;
height: 2px;
border-radius: 2px;
background: var(--oak-primary);
transition: transform 0.25s ease, opacity 0.2s ease;
}
.hamburger.is-open span:nth-child(1) {
transform: translateY(7px) rotate(45deg);
}
.hamburger.is-open span:nth-child(2) {
opacity: 0;
}
.hamburger.is-open span:nth-child(3) {
transform: translateY(-7px) rotate(-45deg);
}
.menu-backdrop {
position: fixed;
inset: 0;
z-index: 40;
background: rgba(58, 36, 18, 0.35);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.menu-backdrop.is-open {
opacity: 1;
pointer-events: auto;
}
.side-menu {
position: fixed;
top: 0;
right: 0;
z-index: 50;
width: min(78vw, 280px);
height: 100%;
height: 100dvh;
padding: 88px 28px 32px;
background: var(--oak-white);
box-shadow: -12px 0 40px rgba(90, 55, 20, 0.16);
transform: translateX(0);
}
.side-menu__list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 8px;
}
.side-menu__list a {
display: block;
padding: 12px 4px;
font-size: 1.05rem;
font-weight: 700;
color: var(--oak-primary);
border-bottom: 1px solid rgba(190, 118, 46, 0.12);
transition: color 0.2s ease, padding-right 0.2s ease;
}
.side-menu__list a:hover {
color: var(--oak-ink);
padding-right: 6px;
}
.category-nav {
position: sticky;
top: 0;
z-index: 10;
margin: 12px 0 8px;
padding: 8px 0 10px;
background: color-mix(in srgb, var(--oak-bg) 92%, white);
backdrop-filter: blur(8px);
animation: fade-in 0.7s ease 0.2s both;
}
.category-scroller {
display: flex;
gap: 22px;
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
padding-bottom: 2px;
}
.category-scroller::-webkit-scrollbar {
display: none;
}
.category-btn {
flex-shrink: 0;
padding: 6px 0;
font-size: 0.9rem;
color: var(--oak-primary-soft);
border-bottom: 2px solid transparent;
transition: color 0.25s ease, border-color 0.25s ease;
white-space: nowrap;
}
.category-btn:hover {
color: var(--oak-primary);
}
.category-btn.is-active {
color: var(--oak-primary);
border-bottom-color: var(--oak-primary);
font-weight: 700;
}
.category-section {
scroll-margin-top: 58px;
padding-top: 8px;
margin-bottom: 18px;
}
.category-section__title {
margin: 0 4px 10px;
font-size: 1rem;
font-weight: 900;
color: var(--oak-primary);
}
.product-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
padding-top: 4px;
}
.info-section {
scroll-margin-top: 72px;
margin-top: 8px;
padding: 22px 8px 10px;
border-top: 1px solid rgba(190, 118, 46, 0.16);
}
.info-section h2 {
margin: 0 0 8px;
font-size: 1.05rem;
font-weight: 900;
color: var(--oak-primary);
}
.info-section p {
margin: 0;
font-size: 0.9rem;
line-height: 1.8;
color: var(--oak-muted);
}
.product-card {
border-radius: var(--oak-radius);
animation: rise 0.55s ease both;
}
.product-card__button {
display: block;
width: 100%;
padding: 6px;
text-align: inherit;
border-radius: var(--oak-radius);
transition: transform 0.25s ease, background 0.2s ease;
}
.product-card__button:hover {
transform: translateY(-2px);
background: rgba(255, 255, 255, 0.35);
}
.product-card__button:focus-visible {
outline: 2px solid var(--oak-primary);
outline-offset: 2px;
}
.product-card__media {
position: relative;
aspect-ratio: 1;
overflow: hidden;
border-radius: 8px;
background: #efe2cf;
}
.product-card__media img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.45s ease;
}
.product-card:hover .product-card__media img {
transform: scale(1.04);
}
.product-card__title {
margin: 8px 2px 0;
font-size: 0.8rem;
font-weight: 700;
color: var(--oak-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-card__title-en {
margin: 3px 2px 8px;
font-family: var(--font-en), "Fraunces", Georgia, serif;
font-size: 0.62rem;
font-weight: 500;
font-style: italic;
line-height: 1.2;
letter-spacing: 0.03em;
color: var(--oak-muted);
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-card__price {
display: flex;
align-items: baseline;
gap: 4px;
margin: 0 2px 2px;
color: var(--oak-primary);
}
.product-card__price-value {
font-family: var(--font-num);
font-size: 1.05rem;
font-weight: 900;
line-height: 1;
}
.product-card__price-unit {
font-size: 0.65rem;
font-weight: 700;
}
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 48px 12px;
color: var(--oak-muted);
font-size: 0.95rem;
}
.product-modal {
position: fixed;
inset: 0;
z-index: 80;
display: grid;
place-items: center;
padding: 18px 12px;
}
.product-modal__backdrop {
position: absolute;
inset: 0;
background: rgba(58, 36, 18, 0.45);
backdrop-filter: blur(3px);
}
.product-modal__dialog {
position: relative;
z-index: 1;
width: min(100%, 420px);
max-height: min(92dvh, 720px);
overflow: auto;
background: var(--oak-white);
border-radius: 18px;
box-shadow: 0 24px 60px rgba(58, 36, 18, 0.28);
animation: rise 0.28s ease both;
}
.product-modal__close {
position: absolute;
top: 10px;
left: 10px;
z-index: 2;
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
color: var(--oak-primary);
box-shadow: 0 4px 14px rgba(90, 55, 20, 0.16);
}
.product-modal__close svg,
.product-modal__nav svg {
width: 16px;
height: 16px;
display: block;
}
.product-modal__gallery {
position: relative;
}
.product-modal__media {
aspect-ratio: 1;
background: #efe2cf;
overflow: hidden;
}
.product-modal__media img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-modal__nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
color: var(--oak-primary);
box-shadow: 0 4px 14px rgba(90, 55, 20, 0.16);
}
.product-modal__nav--prev {
right: 10px;
}
.product-modal__nav--next {
left: 10px;
}
.product-modal__dots {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 6px;
}
.product-modal__dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.55);
transition: background 0.2s ease, transform 0.2s ease;
}
.product-modal__dot.is-active {
background: var(--oak-primary);
transform: scale(1.15);
}
.product-modal__body {
padding: 16px 18px 22px;
}
.product-modal__title {
margin: 0;
font-size: 1.2rem;
font-weight: 900;
color: var(--oak-primary);
}
.product-modal__title-en {
margin: 4px 0 0;
font-family: var(--font-en), "Fraunces", Georgia, serif;
font-size: 0.78rem;
font-weight: 500;
font-style: italic;
letter-spacing: 0.03em;
color: var(--oak-muted);
text-align: right;
}
.product-modal__price {
display: flex;
align-items: baseline;
gap: 5px;
margin: 12px 0 0;
color: var(--oak-primary);
}
.product-modal__price-value {
font-family: var(--font-num);
font-size: 1.35rem;
font-weight: 900;
}
.product-modal__price-unit {
font-size: 0.75rem;
font-weight: 700;
}
.product-modal__desc {
margin: 12px 0 0;
font-size: 0.92rem;
line-height: 1.85;
color: var(--oak-ink);
}
.page-main {
padding: 8px 2px 20px;
animation: fade-in 0.55s ease both;
}
.page-hero {
padding: 10px 4px 18px;
}
.page-kicker {
margin: 0 0 6px;
font-family: var(--font-en), "Fraunces", Georgia, serif;
font-size: 0.72rem;
font-style: italic;
letter-spacing: 0.06em;
color: var(--oak-muted);
text-align: right;
}
.page-title {
margin: 0;
font-size: clamp(1.35rem, 5vw, 1.7rem);
font-weight: 900;
color: var(--oak-primary);
line-height: 1.25;
}
.page-lead {
margin: 10px 0 0;
font-size: 0.98rem;
line-height: 1.9;
color: var(--oak-ink);
}
.page-block {
padding: 8px 4px 18px;
}
.page-subtitle {
margin: 0 0 10px;
font-size: 1.05rem;
font-weight: 900;
color: var(--oak-primary);
}
.page-text {
margin: 0 0 12px;
font-size: 0.94rem;
line-height: 1.95;
color: var(--oak-ink);
}
.page-text--soft {
color: var(--oak-muted);
}
.about-highlights {
display: grid;
gap: 12px;
padding: 4px 0 10px;
}
.about-highlight {
padding: 14px 4px 12px;
border-top: 1px solid rgba(190, 118, 46, 0.18);
animation: rise 0.5s ease both;
}
.about-highlight h3 {
margin: 0 0 6px;
font-size: 1rem;
font-weight: 900;
color: var(--oak-primary);
}
.about-highlight p {
margin: 0;
font-size: 0.9rem;
line-height: 1.8;
color: var(--oak-muted);
}
.contact-details {
display: grid;
gap: 12px;
padding: 4px 0 10px;
}
.contact-line {
display: grid;
gap: 4px;
padding: 12px 4px;
border-top: 1px solid rgba(190, 118, 46, 0.18);
transition: color 0.2s ease;
}
a.contact-line:hover .contact-line__value {
color: var(--oak-primary);
}
.contact-line__label {
font-size: 0.78rem;
font-weight: 700;
color: var(--oak-muted);
}
.contact-line__value {
font-size: 1rem;
font-weight: 700;
color: var(--oak-ink);
}
.contact-line__hint {
font-size: 0.82rem;
color: var(--oak-muted);
line-height: 1.6;
}
.hours-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 10px;
}
.hours-list li {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
font-size: 0.92rem;
color: var(--oak-ink);
}
.hours-list__time {
font-family: var(--font-num);
font-weight: 700;
color: var(--oak-primary);
white-space: nowrap;
}
.social-links {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.social-links a {
padding: 8px 14px;
border-radius: 999px;
background: rgba(190, 118, 46, 0.1);
color: var(--oak-primary);
font-size: 0.88rem;
font-weight: 700;
transition: background 0.2s ease, color 0.2s ease;
}
.social-links a:hover {
background: var(--oak-primary);
color: var(--oak-white);
}
.contact-form {
display: grid;
gap: 12px;
margin-top: 12px;
}
.contact-form__field {
display: grid;
gap: 6px;
}
.contact-form__field span {
font-size: 0.82rem;
font-weight: 700;
color: var(--oak-muted);
}
.contact-form__field input,
.contact-form__field textarea {
width: 100%;
border: 1px solid rgba(190, 118, 46, 0.22);
border-radius: 12px;
background: rgba(255, 255, 255, 0.72);
padding: 12px 14px;
font: inherit;
color: var(--oak-ink);
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.contact-form__field input:focus,
.contact-form__field textarea:focus {
border-color: var(--oak-primary);
box-shadow: 0 0 0 3px rgba(190, 118, 46, 0.12);
}
.contact-form__field textarea {
resize: vertical;
min-height: 110px;
}
.contact-form__submit {
margin-top: 4px;
padding: 12px 18px;
border-radius: 12px;
background: var(--oak-primary);
color: var(--oak-white);
font-size: 0.95rem;
font-weight: 700;
transition: transform 0.2s ease, background 0.2s ease;
}
.contact-form__submit:hover {
background: #a86628;
transform: translateY(-1px);
}
.contact-form__success {
margin: 0;
font-size: 0.88rem;
line-height: 1.7;
color: var(--oak-primary);
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (min-width: 640px) {
html {
display: grid;
place-items: start center;
}
.app-shell {
margin-block: 18px;
min-height: calc(100vh - 36px);
border-radius: 22px;
box-shadow: 0 18px 50px rgba(90, 55, 20, 0.12);
background: var(--oak-bg);
border: 1px solid rgba(190, 118, 46, 0.12);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation: none !important;
transition: none !important;
}
}
+29
View File
@@ -0,0 +1,29 @@
import type { Metadata } from "next";
import { Fraunces } from "next/font/google";
import "./globals.css";
const fraunces = Fraunces({
subsets: ["latin"],
variable: "--font-en",
display: "swap",
});
export const metadata: Metadata = {
title: "اُک | منوی کافه و قنادی",
description: "اُک؛ روایت امروز بلوط — منوی کافه، شیرینی و بیکری",
icons: {
icon: "/img/logo.png",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="fa" dir="rtl" className={fraunces.variable}>
<body>{children}</body>
</html>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { MenuHome } from "@/components/MenuHome";
export default function HomePage() {
return <MenuHome />;
}
+52
View File
@@ -0,0 +1,52 @@
"use client";
import { FormEvent, useState } from "react";
export function ContactForm() {
const [sent, setSent] = useState(false);
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setSent(true);
};
return (
<form className="contact-form" onSubmit={onSubmit}>
<label className="contact-form__field">
<span>نام</span>
<input name="name" type="text" placeholder="نام شما" required />
</label>
<label className="contact-form__field">
<span>شماره تماس</span>
<input
name="phone"
type="tel"
inputMode="tel"
placeholder="۰۹۱۲..."
required
/>
</label>
<label className="contact-form__field">
<span>پیام</span>
<textarea
name="message"
rows={4}
placeholder="پیام خود را بنویسید..."
required
/>
</label>
<button type="submit" className="contact-form__submit">
ارسال پیام
</button>
{sent ? (
<p className="contact-form__success" role="status">
پیام نمونه ثبت شد. بعداً به سیستم واقعی وصل میشود.
</p>
) : null}
</form>
);
}
+155
View File
@@ -0,0 +1,155 @@
"use client";
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
import {
formatPrice,
menuSections,
type CategoryId,
type Product,
} from "@/data/products";
import { ProductModal } from "@/components/ProductModal";
import { SiteChrome } from "@/components/SiteChrome";
export function MenuHome() {
const [activeCategory, setActiveCategory] = useState<CategoryId>(
menuSections[0]?.id ?? "hot-drinks",
);
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const tabRefs = useRef<Record<string, HTMLButtonElement | null>>({});
useEffect(() => {
const sections = menuSections
.map((section) => document.getElementById(`category-${section.id}`))
.filter((el): el is HTMLElement => Boolean(el));
if (sections.length === 0) return;
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort(
(a, b) =>
a.boundingClientRect.top - b.boundingClientRect.top,
);
if (visible[0]?.target.id) {
const id = visible[0].target.id.replace(
"category-",
"",
) as CategoryId;
setActiveCategory(id);
}
},
{
root: null,
rootMargin: "-35% 0px -55% 0px",
threshold: 0,
},
);
sections.forEach((section) => observer.observe(section));
return () => observer.disconnect();
}, []);
useEffect(() => {
const activeTab = tabRefs.current[activeCategory];
if (!activeTab) return;
activeTab.scrollIntoView({
behavior: "smooth",
inline: "center",
block: "nearest",
});
}, [activeCategory]);
const scrollToCategory = (id: CategoryId) => {
const el = document.getElementById(`category-${id}`);
if (!el) return;
el.scrollIntoView({ behavior: "smooth", block: "start" });
setActiveCategory(id);
};
return (
<SiteChrome eyebrow="منوی کافه و قنادی">
<nav className="category-nav" aria-label="دسته‌بندی منو">
<div className="category-scroller">
{menuSections.map((category) => {
const isActive = category.id === activeCategory;
return (
<button
key={category.id}
type="button"
ref={(node) => {
tabRefs.current[category.id] = node;
}}
className={`category-btn${isActive ? " is-active" : ""}`}
aria-current={isActive ? "true" : undefined}
onClick={() => scrollToCategory(category.id)}
>
{category.label}
</button>
);
})}
</div>
</nav>
<main>
{menuSections.map((section) => (
<section
key={section.id}
id={`category-${section.id}`}
className="category-section"
aria-labelledby={`heading-${section.id}`}
>
<h2 id={`heading-${section.id}`} className="category-section__title">
{section.label}
</h2>
<div className="product-grid">
{section.products.map((product, index) => (
<article
key={product.id}
className="product-card"
style={{ animationDelay: `${Math.min(index, 8) * 40}ms` }}
>
<button
type="button"
className="product-card__button"
onClick={() => setSelectedProduct(product)}
>
<div className="product-card__media">
<Image
src={product.images[0] ?? "/product-placeholder.svg"}
alt={product.name}
width={240}
height={240}
sizes="(max-width: 504px) 48vw, 240px"
/>
</div>
<h3 className="product-card__title">{product.name}</h3>
<p className="product-card__title-en" lang="en" dir="ltr">
{product.nameEn}
</p>
<p className="product-card__price">
<span className="product-card__price-value">
{formatPrice(product.price)}
</span>
<span className="product-card__price-unit">تومان</span>
</p>
</button>
</article>
))}
</div>
</section>
))}
</main>
{selectedProduct ? (
<ProductModal
product={selectedProduct}
onClose={() => setSelectedProduct(null)}
/>
) : null}
</SiteChrome>
);
}
+160
View File
@@ -0,0 +1,160 @@
"use client";
import Image from "next/image";
import { useEffect, useState } from "react";
import { formatPrice, type Product } from "@/data/products";
type ProductModalProps = {
product: Product;
onClose: () => void;
};
export function ProductModal({ product, onClose }: ProductModalProps) {
const [activeImage, setActiveImage] = useState(0);
const images = product.images.length > 0 ? product.images : ["/product-placeholder.svg"];
const hasMultiple = images.length > 1;
useEffect(() => {
setActiveImage(0);
}, [product.id]);
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
if (!hasMultiple) return;
if (event.key === "ArrowLeft") {
setActiveImage((index) => (index + 1) % images.length);
}
if (event.key === "ArrowRight") {
setActiveImage((index) => (index - 1 + images.length) % images.length);
}
};
document.addEventListener("keydown", onKeyDown);
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKeyDown);
document.body.style.overflow = previousOverflow;
};
}, [hasMultiple, images.length, onClose]);
const showPrev = () => {
setActiveImage((index) => (index - 1 + images.length) % images.length);
};
const showNext = () => {
setActiveImage((index) => (index + 1) % images.length);
};
return (
<div className="product-modal" role="presentation">
<div className="product-modal__backdrop" onClick={onClose} />
<div
className="product-modal__dialog"
role="dialog"
aria-modal="true"
aria-labelledby="product-modal-title"
>
<button
type="button"
className="product-modal__close"
onClick={onClose}
aria-label="بستن"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M6 6l12 12M18 6L6 18"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
/>
</svg>
</button>
<div className="product-modal__gallery">
<div className="product-modal__media">
<Image
src={images[activeImage]}
alt={product.name}
width={640}
height={640}
sizes="(max-width: 504px) 90vw, 420px"
priority
/>
</div>
{hasMultiple ? (
<>
<button
type="button"
className="product-modal__nav product-modal__nav--prev"
onClick={showPrev}
aria-label="تصویر قبلی"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M9 5l7 7-7 7"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<button
type="button"
className="product-modal__nav product-modal__nav--next"
onClick={showNext}
aria-label="تصویر بعدی"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M15 5l-7 7 7 7"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<div className="product-modal__dots" role="tablist" aria-label="تصاویر محصول">
{images.map((image, index) => (
<button
key={`${image}-${index}`}
type="button"
role="tab"
aria-selected={index === activeImage}
className={`product-modal__dot${index === activeImage ? " is-active" : ""}`}
onClick={() => setActiveImage(index)}
aria-label={`تصویر ${index + 1}`}
/>
))}
</div>
</>
) : null}
</div>
<div className="product-modal__body">
<h2 id="product-modal-title" className="product-modal__title">
{product.name}
</h2>
<p className="product-modal__title-en" lang="en" dir="ltr">
{product.nameEn}
</p>
<p className="product-modal__price">
<span className="product-modal__price-value">
{formatPrice(product.price)}
</span>
<span className="product-modal__price-unit">تومان</span>
</p>
<p className="product-modal__desc">{product.description}</p>
</div>
</div>
</div>
);
}
+90
View File
@@ -0,0 +1,90 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState, type ReactNode } from "react";
import { navLinks } from "@/data/nav";
type SiteChromeProps = {
eyebrow: string;
children: ReactNode;
};
export function SiteChrome({ eyebrow, children }: SiteChromeProps) {
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
if (!menuOpen) return;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setMenuOpen(false);
};
document.addEventListener("keydown", onKeyDown);
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKeyDown);
document.body.style.overflow = previousOverflow;
};
}, [menuOpen]);
return (
<div className="app-shell">
<p className="watermark">{eyebrow}</p>
<header className="brand-header">
<div className="brand-header__row">
<Link href="/" className="brand-header__identity">
<Image
src="/img/logo.png"
alt="اک"
width={100}
height={67}
className="brand-logo"
priority
/>
<h1 className="brand-name">اک، روایت امروز بلوط</h1>
</Link>
<button
type="button"
className={`hamburger${menuOpen ? " is-open" : ""}`}
aria-label={menuOpen ? "بستن منو" : "باز کردن منو"}
aria-expanded={menuOpen}
aria-controls="site-menu"
onClick={() => setMenuOpen((open) => !open)}
>
<span />
<span />
<span />
</button>
</div>
</header>
{menuOpen ? (
<>
<div
className="menu-backdrop is-open"
onClick={() => setMenuOpen(false)}
aria-hidden="true"
/>
<nav id="site-menu" className="side-menu is-open">
<ul className="side-menu__list">
{navLinks.map((link) => (
<li key={link.href}>
<Link href={link.href} onClick={() => setMenuOpen(false)}>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
</>
) : null}
{children}
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
export const navLinks = [
{ href: "/", label: "منو" },
{ href: "/about", label: "درباره ی اک" },
{ href: "/contact", label: "تماس با ما" },
] as const;
+307
View File
@@ -0,0 +1,307 @@
export type CategoryId =
| "hot-drinks"
| "cold-drinks"
| "juices"
| "gelato-shake"
| "smoothie"
| "pastry"
| "dessert"
| "cake"
| "bakery"
| "boxed-sweets";
export type Category = {
id: CategoryId;
label: string;
};
export type Product = {
id: number;
name: string;
nameEn: string;
price: number;
category: CategoryId;
images: string[];
description: string;
};
export const categories: Category[] = [
{ id: "hot-drinks", label: "نوشیدنی گرم" },
{ id: "cold-drinks", label: "نوشیدنی سرد" },
{ id: "juices", label: "آبمیوه‌های طبیعی" },
{ id: "gelato-shake", label: "ژلاتو شیک" },
{ id: "smoothie", label: "اسموتی" },
{ id: "pastry", label: "شیرینی تر" },
{ id: "dessert", label: "دسر" },
{ id: "cake", label: "کیک تولد" },
{ id: "bakery", label: "نان و بیکری" },
{ id: "boxed-sweets", label: "شیرینی جعبه‌ای" },
];
const img1 = "/product-placeholder.svg";
const img2 = "/product-placeholder-2.svg";
function product(
partial: Omit<Product, "images" | "description"> & {
images?: string[];
description?: string;
},
): Product {
return {
images: [img1],
description: "تازه‌آماده‌شده در اک؛ ترکیبی از طعم اصیل و کیفیت روزانه.",
...partial,
};
}
export const products: Product[] = [
product({
id: 21760,
name: "اسپرسو",
nameEn: "Espresso",
price: 120000,
category: "hot-drinks",
images: [img1, img2],
description:
"شات غلیظ اسپرسو با عطر قوی و کرمای طلایی؛ انتخاب کلاسیک دوستداران قهوه.",
}),
product({
id: 21761,
name: "لاته",
nameEn: "Latte",
price: 180000,
category: "hot-drinks",
images: [img1, img2],
description: "اسپرسو با شیر مخملی و بافت نرم؛ متعادل، گرم و دلپذیر.",
}),
product({
id: 21762,
name: "کاپوچینو",
nameEn: "Cappuccino",
price: 170000,
category: "hot-drinks",
description: "ترکیب یک‌سوم اسپرسو، شیر و فوم؛ کلاسیک ایتالیایی اک.",
}),
product({
id: 21774,
name: "نسکافه",
nameEn: "Nescafe",
price: 260000,
category: "hot-drinks",
description: "نسکافه خامه‌ای با طعم ملایم و سرو گرم.",
}),
product({
id: 21763,
name: "آیس لاته",
nameEn: "Iced Latte",
price: 190000,
category: "cold-drinks",
images: [img2, img1],
description: "لاته سرد روی یخ؛ سبک، خنک و مناسب روزهای گرم.",
}),
product({
id: 21764,
name: "آیس آمریکانو",
nameEn: "Iced Americano",
price: 160000,
category: "cold-drinks",
description: "اسپرسو رقیق‌شده با آب و یخ؛ تلخ و تازه.",
}),
product({
id: 21765,
name: "آب پرتقال",
nameEn: "Orange Juice",
price: 140000,
category: "juices",
images: [img1, img2],
description: "آب پرتقال تازه؛ سرشار از طعم طبیعی و ویتامین.",
}),
product({
id: 21766,
name: "آب سیب",
nameEn: "Apple Juice",
price: 130000,
category: "juices",
description: "آب سیب طبیعی با شیرینی متعادل.",
}),
product({
id: 21767,
name: "آب هویج",
nameEn: "Carrot Juice",
price: 130000,
category: "juices",
description: "آب هویج تازه و مغذی.",
}),
product({
id: 21773,
name: "موز",
nameEn: "Banana",
price: 270000,
category: "gelato-shake",
images: [img2, img1],
description: "ژلاتو شیک موزی؛ خامه‌ای، خنک و سیرکننده.",
}),
product({
id: 21772,
name: "وانیل",
nameEn: "Vanilla",
price: 250000,
category: "gelato-shake",
description: "ژلاتو شیک وانیلی کلاسیک با بافت نرم.",
}),
product({
id: 21771,
name: "شکلات",
nameEn: "Chocolate",
price: 260000,
category: "gelato-shake",
images: [img1, img2],
description: "ژلاتو شیک شکلاتی غنی برای عاشقان کاکائو.",
}),
product({
id: 21770,
name: "توت فرنگی",
nameEn: "Strawberry",
price: 270000,
category: "gelato-shake",
description: "ژلاتو شیک توت‌فرنگی با طعم میوه‌ای تازه.",
}),
product({
id: 21769,
name: "اورئو",
nameEn: "Oreo",
price: 300000,
category: "gelato-shake",
images: [img2, img1],
description: "ژلاتو شیک اورئو با تکه‌های بیسکوییت ترد.",
}),
product({
id: 21784,
name: "شیرموز انبه",
nameEn: "Mango Banana Milk",
price: 250000,
category: "smoothie",
description: "اسموتی موز و انبه؛ گرمسیری و انرژی‌بخش.",
}),
product({
id: 21783,
name: "شیرموز پسته",
nameEn: "Pistachio Banana Milk",
price: 230000,
category: "smoothie",
description: "شیرموز با پسته؛ طعمی ایرانی و خاص.",
}),
product({
id: 21782,
name: "شیرموز خرما",
nameEn: "Date Banana Milk",
price: 190000,
category: "smoothie",
description: "شیرموز با خرما؛ شیرین طبیعی و مغذی.",
}),
product({
id: 21781,
name: "شیرموز",
nameEn: "Banana Milk",
price: 170000,
category: "smoothie",
description: "شیرموز ساده و کلاسیک اک.",
}),
product({
id: 21780,
name: "معجون میکس",
nameEn: "Mixed Elixir",
price: 300000,
category: "smoothie",
images: [img1, img2],
description: "معجون میکس با ترکیب میوه و مغز؛ مقوی و کامل.",
}),
product({
id: 21776,
name: "شیر انبه",
nameEn: "Mango Milk",
price: 260000,
category: "smoothie",
description: "شیر انبه خنک با طعم تابستانی.",
}),
product({
id: 21785,
name: "شیرینی تر مخلوط",
nameEn: "Assorted Soft Pastry",
price: 645000,
category: "pastry",
images: [img1, img2],
description: "باکس مخلوط شیرینی تر روز؛ تازه و متنوع.",
}),
product({
id: 21786,
name: "انواع دسر",
nameEn: "Assorted Desserts",
price: 150000,
category: "dessert",
description: "انتخابی از دسرهای روز اک.",
}),
product({
id: 21791,
name: "کیک شکلاتی",
nameEn: "Chocolate Cake",
price: 800000,
category: "cake",
images: [img2, img1],
description: "کیک تولد شکلاتی مرطوب با کاور شکلاتی.",
}),
product({
id: 21790,
name: "کیک وانیلی",
nameEn: "Vanilla Cake",
price: 600000,
category: "cake",
images: [img1, img2],
description: "کیک تولد وانیلی نرم با تزئین کلاسیک.",
}),
product({
id: 21792,
name: "انواع بیکری",
nameEn: "Assorted Bakery",
price: 645000,
category: "bakery",
images: [img1, img2],
description: "مجموعه‌ای از نان و بیکری تازه روز.",
}),
product({
id: 21788,
name: "استریپس",
nameEn: "Strips",
price: 150000,
category: "bakery",
description: "استریپس ترد و طلایی از فر اک.",
}),
product({
id: 21787,
name: "اشترودل",
nameEn: "Strudel",
price: 150000,
category: "bakery",
description: "اشترودل لایه‌لایه با مغز میوه‌ای.",
}),
product({
id: 21789,
name: "انواع کوکی",
nameEn: "Assorted Cookies",
price: 150000,
category: "boxed-sweets",
images: [img2, img1],
description: "کوکی‌های جعبه‌ای با طعم‌های متنوع.",
}),
];
export const menuSections = categories
.map((category) => ({
...category,
products: products.filter((item) => item.category === category.id),
}))
.filter((section) => section.products.length > 0);
export function formatPrice(price: number): string {
return new Intl.NumberFormat("fa-IR").format(price);
}
+45
View File
@@ -0,0 +1,45 @@
export const aboutContent = {
title: "درباره ی اک",
titleEn: "About Oak",
intro:
"اک، روایت امروز بلوط است؛ جایی برای قهوه تازه، شیرینی‌های دست‌ساز و نان گرم روز.",
story: [
"ما از یک آشپزخانه کوچک شروع کردیم و امروز فضایی ساخته‌ایم که طعم، عطر و آرامش کنار هم باشند.",
"هر روز بیکری، دسر و نوشیدنی‌ها را با مواد تازه آماده می‌کنیم تا تجربه‌ای ساده و صادقانه از طعم خوب بسازیم.",
],
highlights: [
{
title: "تازگی روزانه",
text: "نان، شیرینی و دسر هر روز از صفر آماده می‌شوند.",
},
{
title: "طعم اصیل",
text: "ترکیب دستورهای کلاسیک با نگاه امروزی اک.",
},
{
title: "فضای گرم",
text: "جایی برای توقف کوتاه، دیدار دوستانه یا کار آرام.",
},
],
};
export const contactContent = {
title: "تماس با ما",
titleEn: "Contact",
intro: "برای سفارش، هماهنگی مراسم یا پرسش‌های روزمره با اک در ارتباط باشید.",
phone: "۰۹۱۲ ۱۲۳ ۴۵۶۷",
phoneHref: "tel:+989121234567",
email: "hello@oaktasty.com",
emailHref: "mailto:hello@oaktasty.com",
address: "تهران، خیابان نمونه، پلاک ۱۲",
mapHint: "نزدیک میدان نمونه — پارکینگ در دسترس",
hours: [
{ day: "شنبه تا چهارشنبه", time: "۸:۰۰ تا ۲۲:۰۰" },
{ day: "پنجشنبه و جمعه", time: "۹:۰۰ تا ۲۳:۰۰" },
],
socials: [
{ label: "اینستاگرام", href: "https://instagram.com/" },
{ label: "واتساپ", href: "https://wa.me/989121234567" },
],
formNote: "پیام شما به‌صورت نمونه ثبت می‌شود؛ بعداً به سیستم واقعی وصل می‌شود.",
};
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}