Initial commit: AQ JAIYQ site (cafe + minimarket MVP)
- Next.js 14 + Tailwind CSS + Zustand - Home page with two sections, popular blocks, sales - Cafe catalog (114 dishes across 15 categories) - Market catalog (4 items, expandable) - Product detail pages with ingredients and tags - Cart with cafe/market separation - Checkout with WhatsApp order submission - Full search across both sections - Responsive mobile-first design - Brand colors from logo: gold + charcoal + cream
This commit is contained in:
commit
a236dcf4cf
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# claude code local settings
|
||||
.claude/
|
||||
318
CATALOG.md
Normal file
318
CATALOG.md
Normal file
@ -0,0 +1,318 @@
|
||||
# 📦 Каталог AQ JAIYQ
|
||||
|
||||
> Полный список товаров и блюд для загрузки в админку сайта.
|
||||
> Цены актуальны на момент составления документа.
|
||||
|
||||
---
|
||||
|
||||
## 🛒 МИНИМАРКЕТ
|
||||
|
||||
### 🍞 Хлеб
|
||||
|
||||
| Название | Цена | Ед. | Наличие |
|
||||
|----------|------|-----|---------|
|
||||
| Хлеб домашний | 165 ₸ | шт | ✅ |
|
||||
| Хлеб на сыворотке | 180 ₸ | шт | ✅ |
|
||||
|
||||
### 🧂 Бакалея
|
||||
|
||||
| Название | Цена | Ед. | Наличие |
|
||||
|----------|------|-----|---------|
|
||||
| Сахар | 569 ₸ | кг | ✅ |
|
||||
|
||||
### 🥬 Овощи
|
||||
|
||||
| Название | Цена | Ед. | Наличие |
|
||||
|----------|------|-----|---------|
|
||||
| Картофель | 319 ₸ | кг | ✅ |
|
||||
|
||||
> ⚠️ **Нужно дополнить!** Сейчас по минимаркету всего 4 позиции. Для запуска нужно минимум 30–50 товаров по всем 9 категориям. См. список ниже.
|
||||
|
||||
---
|
||||
|
||||
## 🍕 КАФЕ — Ақ Жайық
|
||||
|
||||
### 🌅 Завтраки
|
||||
|
||||
| Название | Цена | Состав | Метка |
|
||||
|----------|------|--------|-------|
|
||||
| Блины (2 шт) | 450 ₸ | — | — |
|
||||
| Сырники (2 шт) | 800 ₸ | — | — |
|
||||
| Завтрак классический | 1 000 ₸ | глазунья, сосиски, обжаренный хлеб | — |
|
||||
| Мини-чебуреки (6 шт) | 800 ₸ | — | — |
|
||||
| Мини-самса (6 шт) | 900 ₸ | — | — |
|
||||
| Омлет | 600 ₸ | яйцо, молоко, зелень | — |
|
||||
| Хлебная корзина | 800 ₸ | — | — |
|
||||
| Хлеб белый | 20 ₸ | — | — |
|
||||
| Хлеб чёрный | 30 ₸ | — | — |
|
||||
| Лепёшка | 350 ₸ | — | — |
|
||||
| Бауырсаки (0,5 кг) | 600 ₸ | — | 📅 Предзаказ |
|
||||
|
||||
### 🍲 Первые блюда
|
||||
|
||||
| Название | Цена | Состав | Метка |
|
||||
|----------|------|--------|-------|
|
||||
| Грибной суп-пюре | 1 200 ₸ | — | — |
|
||||
| Чечевичный суп | 1 200 ₸ | — | — |
|
||||
| Шурпа | 1 500 ₸ | баранина, морковь, картофель, болг. перец, лук, зелень | — |
|
||||
| Борщ | 1 200 ₸ | — | — |
|
||||
| Том-ям | 2 500 ₸ | сёмга, креветки, шампиньоны, чеснок, имбирь, лимон, перец чили, рис | 🌶️ Острое |
|
||||
| Пельмени с бульоном | 1 000 ₸ | — | — |
|
||||
| Куриный суп-лапша | 1 200 ₸ | — | — |
|
||||
| Солянка | 1 500 ₸ | — | — |
|
||||
|
||||
### 🍖 Вторые блюда
|
||||
|
||||
| Название | Цена | Состав | Метка |
|
||||
|----------|------|--------|-------|
|
||||
| Гуйру лагман | 2 200 ₸ | говядина, болг. перец, пекин. капуста, лук, чеснок, морковь, лапша | — |
|
||||
| Цомян лагман | 2 500 ₸ | говядина, болг. перец, пекин. капуста, чеснок, лук, морковь, лапша обжаренные в томате или на сое | — |
|
||||
| Манты | 1 500 ₸ | фарш из говядины, лук, перец, соль | — |
|
||||
| Манты с картофелем | 1 200 ₸ | фарш из говядины, картофель, лук, перец, соль | 📅 Предзаказ |
|
||||
| Мясо по-тайски | 2 500 ₸ | — | 🌶️ Острое |
|
||||
| Котлеты | 600 ₸ | + гарнир на выбор | — |
|
||||
| Плов | 1 200 ₸ | — | 🏆 Хит |
|
||||
| Бараньи рёбра | 2 300 ₸ | — | — |
|
||||
| Джигяр | 2 200 ₸ | жареная печень, курдюк, овощи | — |
|
||||
| Филе судака с овощами | 2 300 ₸ | — | 📅 Предзаказ |
|
||||
| Курица с грибами в сливочном соусе | 2 500 ₸ | + гарнир на выбор | — |
|
||||
| Куырдак из баранины | 1 500 ₸ | — | 🏆 Хит |
|
||||
| Картошка с грибами | 1 800 ₸ | — | — |
|
||||
| Требуха с овощами | 1 200 ₸ | — | — |
|
||||
|
||||
### 🥗 Салаты
|
||||
|
||||
| Название | Цена | Состав |
|
||||
|----------|------|--------|
|
||||
| Греческий | 1 800 ₸ | огурцы, помидоры, болг. перец, лук, фирм. соус, сыр фета |
|
||||
| Ачучук | 1 200 ₸ | помидоры, лук, ост. перец |
|
||||
| Тайский | 2 000 ₸ | огурцы, помидоры, болг. перец, говядина, соя, зелень |
|
||||
| Тбилиси | 2 500 ₸ | говядина, конс. фасоль, грец. орехи, болг. перец, чеснок, лук |
|
||||
| Нежность | 2 000 ₸ | гов. язык, кукуруза, огурцы, яйца, картофель пай |
|
||||
| Свежий | 1 000 ₸ | огурец, помидоры, редис, лук, зелень |
|
||||
| Под шубой | 1 500 ₸ | картофель, морковь, свёкла, сельдь, лук, яйца |
|
||||
| Оливье | 1 500 ₸ | картофель, морковь, сол. огурцы, горох, колбаса, яйца |
|
||||
| Морковный | 900 ₸ | морковь, лук, зелень |
|
||||
| Баварский | 2 500 ₸ | картофель, яйцо, марин. огурцы, опята, шампиньоны, лук |
|
||||
| Гнездо глухаря | 2 500 ₸ | филе курицы копч., огурец, яйцо, перепел. яйца, пай |
|
||||
| Цезарь классический | 2 500 ₸ | капуста, фирменный соус, филе курицы, переп. яйца, сухари |
|
||||
| Цезарь с креветками | 2 800 ₸ | капуста, фирменный соус, креветки, переп. яйца, сухари |
|
||||
| Малибу | 2 000 ₸ | филе курицы, огурцы, помидоры, сыр, сухарики |
|
||||
| Винегрет | 1 500 ₸ | картофель, кваш. капуста, лук, морковь, горох, свёкла |
|
||||
|
||||
### 🍢 Шашлык
|
||||
|
||||
| Название | Цена | Метка |
|
||||
|----------|------|-------|
|
||||
| Баранина | 2 000 ₸ | 🏆 Хит |
|
||||
| Курица | 1 500 ₸ | 🏆 Хит |
|
||||
| Филе утки | 1 500 ₸ | — |
|
||||
|
||||
### 🥩 Стейки
|
||||
|
||||
| Название | Цена |
|
||||
|----------|------|
|
||||
| Стейк T-bone | 2 500 ₸ |
|
||||
| Стейк сёмги | 4 500 ₸ |
|
||||
|
||||
### ❄️ Холодные закуски
|
||||
|
||||
| Название | Цена | Состав |
|
||||
|----------|------|--------|
|
||||
| Кавказская нарезка | 2 500 ₸ | огурцы, помидоры, болг. перец, зелень, брынза |
|
||||
| Закуска по-русски | 2 500 ₸ | сельдь, кваш. капуста, марин. огурцы, грибы, лук, картофель |
|
||||
| Казахские деликатесы | 5 500 ₸ | қазы, шұжық, кесек ет |
|
||||
| Рыбное плато | 5 500 ₸ | — |
|
||||
|
||||
### 🔥 Горячие закуски
|
||||
|
||||
| Название | Цена | Состав | Метка |
|
||||
|----------|------|--------|-------|
|
||||
| Куриные нагетсы (6 шт) | 650 ₸ | — | — |
|
||||
| Сырные палочки (6 шт) | 1 200 ₸ | — | — |
|
||||
| Хрустящие крылышки | 1 800 ₸ | — | — |
|
||||
| Чесночные гренки | 800 ₸ | — | — |
|
||||
| Колбаски говяжьи (5 шт) | 3 500 ₸ | — | — |
|
||||
| Колбаски бараньи (5 шт) | 3 500 ₸ | — | — |
|
||||
| Колбаски куриные (5 шт) | 3 000 ₸ | — | — |
|
||||
| Луковые кольца | 900 ₸ | — | — |
|
||||
| Баклажановые роллы (8 шт) | 1 500 ₸ | — | 📅 Предзаказ |
|
||||
| Кабачковые роллы (8 шт) | 1 500 ₸ | — | 📅 Предзаказ |
|
||||
| Спринг-роллы с курицей | 2 500 ₸ | — | 📅 Предзаказ |
|
||||
| Креветки к пиву | 2 500 ₸ | — | — |
|
||||
| Фирменная закуска «АҚ ЖАЙЫҚ» | 2 500 ₸ | фарш из брынзы и шпината в тесте, жаренные во фритюре | ⭐ Фирменное, 📅 Предзаказ |
|
||||
|
||||
### 🍝 Паста
|
||||
|
||||
| Название | Цена | Состав |
|
||||
|----------|------|--------|
|
||||
| Альфредо | 2 500 ₸ | паста феттучини, куриное филе, чеснок, сливки, тёртый пармезан, петрушка |
|
||||
| Болоньезе | 2 500 ₸ | говяжий фарш, томат, специи, паста на выбор (феттучини/спагетти), пармезан |
|
||||
| Паста с креветками в сливочно-чесночном соусе | 3 000 ₸ | — |
|
||||
| Том-ям паста | 3 000 ₸ | шампиньоны, креветки, черри, паста Том-ям, спагетти | 🌶️ Острое |
|
||||
|
||||
### 🍕 Пицца
|
||||
|
||||
| Название | Цена | Состав |
|
||||
|----------|------|--------|
|
||||
| Маргарита | 1 800 ₸ | моцарелла, помидоры |
|
||||
| Сырная | 2 500 ₸ | моцарелла |
|
||||
| Пепперони | 2 500 ₸ | моцарелла, колбаса |
|
||||
| Охотничья | 3 000 ₸ | моцарелла, охотничьи колбаски |
|
||||
| Bon dior | 3 000 ₸ | моцарелла, колбаса, брокколи |
|
||||
| 4 чезона | 3 000 ₸ | моцарелла, колбаса, грибы, курица, помидоры |
|
||||
| Pollo e funghi | 3 200 ₸ | курица с грибами, моцарелла |
|
||||
| Mexicano | 3 200 ₸ | моцарелла, курица, помидоры, кукуруза, маслины, халапенью |
|
||||
| Мясная | 3 200 ₸ | моцарелла, говядина, копч. колбаса, курица, помидоры, маслины |
|
||||
| Болоньезе | 3 200 ₸ | моцарелла, помидоры, фарш |
|
||||
| Сырный лосось | 3 500 ₸ | моцарелла, лосось, маслины, творожный сыр |
|
||||
|
||||
### 🌯 Донеры
|
||||
|
||||
**Max (большой):**
|
||||
|
||||
| Название | Цена | Метка |
|
||||
|----------|------|-------|
|
||||
| Max Классика | 1 500 ₸ | 🏆 Хит |
|
||||
| Max Чесночный | 1 500 ₸ | — |
|
||||
| Max Острый | 1 550 ₸ | 🌶️ Острое |
|
||||
| Max Сырный | 1 650 ₸ | — |
|
||||
| Max Сырно-острый | 1 700 ₸ | 🌶️ Острое |
|
||||
|
||||
**Mini (маленький):**
|
||||
|
||||
| Название | Цена | Метка |
|
||||
|----------|------|-------|
|
||||
| Mini Классика | 1 200 ₸ | — |
|
||||
| Mini Чесночный | 1 200 ₸ | — |
|
||||
| Mini Острый | 1 300 ₸ | 🌶️ Острое |
|
||||
| Mini Сырный | 1 450 ₸ | — |
|
||||
| Mini Сырно-острый | 1 500 ₸ | 🌶️ Острое |
|
||||
|
||||
### 🍚 Гарниры
|
||||
|
||||
| Название | Цена |
|
||||
|----------|------|
|
||||
| Рис | 500 ₸ |
|
||||
| Картофель запечённый | 600 ₸ |
|
||||
| Картофель фри | 600 ₸ |
|
||||
| Овощи гриль | 1 000 ₸ |
|
||||
|
||||
### 🥫 Соусы
|
||||
|
||||
| Название | Цена |
|
||||
|----------|------|
|
||||
| Майонез | ⚠️ нужно указать |
|
||||
| Кетчуп | ⚠️ нужно указать |
|
||||
| Сметана | ⚠️ нужно указать |
|
||||
|
||||
### 🎉 Блюда на компанию (10–12 человек)
|
||||
|
||||
| Название | Цена | Состав | Метка |
|
||||
|----------|------|--------|-------|
|
||||
| Манты | 12 000 ₸ | фарш из говядины, лук, специи | — |
|
||||
| Цыплёнок Табака (2 шт) | 12 000 ₸ | + гарнир на выбор | — |
|
||||
| Курица Галантин | 15 000 ₸ | тушка курицы, начинённая марин. курин. бедром, + гарнир | — |
|
||||
| Шашлычки на шпажках (15 шт) | 15 000 ₸ | говядина, курица, баранина-люля | — |
|
||||
| Томлёные говяжьи рёбра в тесте | 20 000 ₸ | говядина, слоёное тесто, картофель | — |
|
||||
| Балғын Ет | 20 000 ₸ | томлёная конина, картофель | — |
|
||||
| Запечённая баранья лопатка | 25 000 ₸ | + гарнир на выбор | — |
|
||||
| Казан-кебаб | 30 000 ₸ | обжаренный картофель, говядина, розочки из теста | ⭐ Фирменное |
|
||||
| Сёмга запечённая с овощами | 40 000 ₸ | + гарнир на выбор | — |
|
||||
|
||||
### 🎁 Сеты на компанию
|
||||
|
||||
| Название | Цена | Состав |
|
||||
|----------|------|--------|
|
||||
| Пивной сет | 5 500 ₸ | креветки, чечел жарен., чипсы, чесночные гренки |
|
||||
| Ассорти из колбасок | 11 000 ₸ | говядина, баранина, курица |
|
||||
| Мясной сет | 13 000 ₸ | шашлык, баранина, говядина, курица |
|
||||
|
||||
---
|
||||
|
||||
## 📊 Сводка по каталогу
|
||||
|
||||
### Кафе
|
||||
|
||||
| Категория | Кол-во позиций |
|
||||
|-----------|----------------|
|
||||
| Завтраки | 11 |
|
||||
| Первые блюда | 8 |
|
||||
| Вторые блюда | 14 |
|
||||
| Салаты | 15 |
|
||||
| Шашлык | 3 |
|
||||
| Стейки | 2 |
|
||||
| Холодные закуски | 4 |
|
||||
| Горячие закуски | 13 |
|
||||
| Паста | 4 |
|
||||
| Пицца | 11 |
|
||||
| Донеры | 10 |
|
||||
| Гарниры | 4 |
|
||||
| Соусы | 3 |
|
||||
| Блюда на компанию | 9 |
|
||||
| Сеты на компанию | 3 |
|
||||
| **ИТОГО** | **114 позиций** |
|
||||
|
||||
### Минимаркет
|
||||
|
||||
| Категория | Кол-во позиций |
|
||||
|-----------|----------------|
|
||||
| Хлеб | 2 |
|
||||
| Бакалея | 1 |
|
||||
| Овощи | 1 |
|
||||
| Молочные продукты | 0 |
|
||||
| Напитки | 0 |
|
||||
| Фрукты | 0 |
|
||||
| Заморозка | 0 |
|
||||
| Сладости | 0 |
|
||||
| Бытовые товары | 0 |
|
||||
| **ИТОГО** | **4 позиции** ⚠️ |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Что нужно дозаполнить
|
||||
|
||||
### 1. Минимаркет (критично!)
|
||||
По минимаркету пока только 4 товара — этого мало для запуска. Нужно минимум 5–10 позиций в каждую категорию:
|
||||
|
||||
- 🥛 **Молочные** — молоко, кефир, сметана, творог, йогурт, сыр, масло
|
||||
- 🥤 **Напитки** — вода, кола, сок, чай, лимонад
|
||||
- 🍎 **Фрукты** — яблоки, бананы, апельсины, груши, виноград
|
||||
- 🧊 **Заморозка** — пельмени, мороженое, замороженные овощи
|
||||
- 🍫 **Сладости** — шоколад, конфеты, печенье, вафли
|
||||
- 🧼 **Бытовые** — мыло, шампунь, порошок, моющее средство
|
||||
|
||||
Плюс расширить **хлеб** (батон, лаваш), **бакалею** (мука, соль, рис, гречка, макароны, масло) и **овощи** (морковь, лук, капуста, помидоры).
|
||||
|
||||
### 2. Цены соусов в кафе
|
||||
Майонез / кетчуп / сметана — цены не указаны.
|
||||
|
||||
### 3. Фото
|
||||
По каждому товару и блюду нужно **минимум 1 фото**. Идеально — 3–5.
|
||||
**Совет:** делать фото на телефон при хорошем дневном свете, на однотонном фоне (белая тарелка, деревянный стол).
|
||||
|
||||
### 4. Описания на казахском (KZ)
|
||||
Сейчас всё на русском. Названия и описания нужно перевести на казахский для второго языка.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Метки, которые я расставил предварительно
|
||||
|
||||
Я сам пометил очевидные позиции — **проверь и поправь**:
|
||||
|
||||
- 🏆 **Хит:** Плов, Куырдак, Шашлык (баранина и курица), Max Классика
|
||||
- 🌶️ **Острое:** Том-ям, Мясо по-тайски, Том-ям паста, Острые донеры
|
||||
- 📅 **Предзаказ:** Бауырсаки, Манты с картофелем, Филе судака, Роллы баклажановые/кабачковые, Спринг-роллы, Фирменная закуска
|
||||
- ⭐ **Фирменное:** Казан-кебаб, Закуска «АҚ ЖАЙЫҚ»
|
||||
|
||||
---
|
||||
|
||||
## 🚫 Что НЕ вошло в каталог (по решению)
|
||||
|
||||
Эти пункты были в твоём сообщении, но это **не для сайта**:
|
||||
|
||||
- ❌ **Обслуживание 10%** — это плата в зале, не для онлайн-заказов
|
||||
- ❌ **Порча мебели и оборудования** (стул 30000тг, диван 250000тг и т.д.) — это штрафы для оффлайн-зала, не товары
|
||||
- ❌ **Бой посуды** — тоже штрафы зала
|
||||
|
||||
Если эти позиции нужны — это отдельный документ «Правила заведения для гостей зала», не каталог сайта.
|
||||
83
PRD.md
Normal file
83
PRD.md
Normal file
@ -0,0 +1,83 @@
|
||||
# PRD — AQ JAIYQ
|
||||
|
||||
**Онлайн-сервис заказа еды из кафе и продуктов из минимаркета для жителей села Нура.**
|
||||
|
||||
---
|
||||
|
||||
## Что я хочу автоматизировать
|
||||
|
||||
Хочу, чтобы жители Нуры могли заказывать еду из моего кафе и продукты из моего минимаркета через простой сайт — без звонков и переписок в WhatsApp, чтобы заказы не терялись и приходили мне в одно место.
|
||||
|
||||
---
|
||||
|
||||
## Кто будет пользоваться
|
||||
|
||||
1. **Жители села Нура** — обычные люди, в том числе те, кто редко пользуется приложениями. Им нужно заказать еду или продукты домой или забрать самим.
|
||||
2. **Я (владелец)** — буду видеть все заказы, управлять товарами, ценами и акциями.
|
||||
3. **Сотрудник-оператор** — один человек, который принимает заказы и передаёт их на кухню или сборщику в магазин.
|
||||
4. **Курьер** — на старте один человек (знакомый или родственник), потом будем нанимать ещё.
|
||||
|
||||
---
|
||||
|
||||
## Что должно произойти на выходе
|
||||
|
||||
У меня будет один сайт **AQ JAIYQ** с двумя понятными разделами:
|
||||
|
||||
- **Кафе** — пицца, шашлык, донеры, напитки, комбо.
|
||||
- **Минимаркет** — молочка, хлеб, напитки, овощи, бытовые товары.
|
||||
|
||||
Клиент заходит на сайт, выбирает раздел, кладёт товары в корзину, оформляет заказ за 1–2 минуты и выбирает: **доставка или самовывоз**. Оплачивает удобным способом или платит при получении. Я и сотрудник получаем уведомление о новом заказе, готовим его, и клиент видит статус: «принят → готовится → в пути → доставлен».
|
||||
|
||||
---
|
||||
|
||||
## Главные функции (то, что обязательно нужно на старте)
|
||||
|
||||
### 1. Каталог с двумя разделами
|
||||
Раздельные витрины «Кафе» и «Минимаркет» с фотографиями, ценами и категориями. Поиск и сортировка. Корзины тоже раздельные, чтобы шашлык не лежал рядом с бытовой химией.
|
||||
|
||||
### 2. Заказ за 1–2 минуты
|
||||
Оформление заказа без обязательной регистрации (по номеру телефона). Кнопка «Заказать в 1 клик». Выбор: доставка или самовывоз. Указание адреса по улице и ориентирам (без точного номера дома — для села это нормально). Поддержка русского и казахского языков.
|
||||
|
||||
### 3. Удобная оплата
|
||||
На старте подключаем 5 способов:
|
||||
- Kaspi QR (фото QR-кода)
|
||||
- Kaspi Pay (онлайн-кнопка)
|
||||
- Halyk / Homebank
|
||||
- Перевод на карту
|
||||
- Наличными при получении
|
||||
|
||||
### 4. Уведомления через WhatsApp
|
||||
Клиент получает в WhatsApp: подтверждение заказа, статус («готовится», «курьер выехал», «доставлено») и сообщение, если что-то не так. Я и оператор получаем уведомление о каждом новом заказе.
|
||||
|
||||
### 5. Простая админка для меня
|
||||
Один экран, где я вижу:
|
||||
- все новые заказы,
|
||||
- статусы (принят / готовится / в пути / доставлен),
|
||||
- товары и цены (могу быстро поменять),
|
||||
- акции и промокоды,
|
||||
- остановить продажу товара, если он закончился.
|
||||
|
||||
---
|
||||
|
||||
## Что НЕ входит в задачу (пока)
|
||||
|
||||
Чтобы не растягивать запуск, на старте **не делаем**:
|
||||
|
||||
- ❌ Мобильное приложение (только сайт, который удобно открывается на телефоне).
|
||||
- ❌ Маркетплейс — другие магазины и кафе из Нуры подключать не будем. Только мой бизнес.
|
||||
- ❌ Регистрация продавцов, их кабинеты, комиссии платформы.
|
||||
- ❌ Бонусная и реферальная программы.
|
||||
- ❌ Подписка на доставку.
|
||||
- ❌ AI-рекомендации и голосовой заказ.
|
||||
- ❌ Telegram-бот.
|
||||
- ❌ Карта курьера в реальном времени (на старте хватит статусов «готовится / в пути / доставлен»).
|
||||
- ❌ Apple Pay / Google Pay, Freedom Pay — добавим позже, если будет спрос.
|
||||
- ❌ Сложная аналитика и финансовые отчёты — на старте смотрим заказы глазами.
|
||||
|
||||
---
|
||||
|
||||
## Главный принцип проекта
|
||||
|
||||
> **Сайт должен быть таким простым, чтобы даже бабушка из Нуры могла заказать хлеб за 2 минуты.**
|
||||
|
||||
Всё, что усложняет — выкидываем. Всё, что мешает быстро заказать — переделываем. Главное на старте: заказы не теряются, клиент получает свою еду или продукты, и он остался доволен.
|
||||
440
PROTOTYPE.md
Normal file
440
PROTOTYPE.md
Normal file
@ -0,0 +1,440 @@
|
||||
# 🎨 Прототип сайта AQ JAIYQ
|
||||
|
||||
> Как будет устроен сайт глазами обычного человека.
|
||||
> Пока без программирования — просто описание страниц и того, что на них происходит.
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Карта сайта (как страницы связаны между собой)
|
||||
|
||||
```
|
||||
[ ГЛАВНАЯ ]
|
||||
│
|
||||
┌─────────────┴─────────────┐
|
||||
│ │
|
||||
[ 🍕 КАФЕ ] [ 🛒 МИНИМАРКЕТ ]
|
||||
│ │
|
||||
[ Карточка [ Карточка
|
||||
блюда ] товара ]
|
||||
│ │
|
||||
└─────────────┬─────────────┘
|
||||
│
|
||||
[ КОРЗИНА ]
|
||||
│
|
||||
[ ОФОРМЛЕНИЕ ЗАКАЗА ]
|
||||
│
|
||||
[ ЗАКАЗ ПРИНЯТ ✅ ]
|
||||
│
|
||||
[ СТАТУС ЗАКАЗА ]
|
||||
│
|
||||
[ ЛИЧНЫЙ КАБИНЕТ ]
|
||||
(история заказов)
|
||||
```
|
||||
|
||||
Отдельно — **админка для тебя** (она спрятана от клиентов).
|
||||
|
||||
---
|
||||
|
||||
## 📄 Страница 1. ГЛАВНАЯ
|
||||
|
||||
**Что видит человек, когда заходит на сайт:**
|
||||
|
||||
🔝 **Сверху:**
|
||||
- Логотип **AQ JAIYQ**
|
||||
- Переключатель языка: **RU / KZ**
|
||||
- Маленькая иконка корзины 🛒 (если в ней что-то есть — показывается цифра)
|
||||
- Кнопка «Войти» (но входить необязательно)
|
||||
|
||||
🎯 **Главный экран (то, что видно сразу):**
|
||||
- Большой красивый баннер с акцией (например: «Шашлык + кола за 2500 ₸»)
|
||||
- Под баннером — **две большие плитки на весь экран:**
|
||||
|
||||
```
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ │ │ │
|
||||
│ 🍕 КАФЕ │ │ 🛒 МИНИМАРКЕТ │
|
||||
│ │ │ │
|
||||
│ Еда на заказ │ │ Продукты домой │
|
||||
│ за 30 минут │ │ за 1 час │
|
||||
│ │ │ │
|
||||
│ [ Открыть меню ] │ │ [ Открыть магазин ]│
|
||||
│ │ │ │
|
||||
└──────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
📋 **Блок «Как это работает»** (3 простых шага с иконками):
|
||||
1. **Выбери** товары или еду
|
||||
2. **Оформи** заказ за 2 минуты
|
||||
3. **Получи** домой или забери сам
|
||||
|
||||
🔥 **Блок «Популярное сегодня»** — 4–6 самых заказываемых товаров
|
||||
|
||||
📞 **Внизу страницы:**
|
||||
- Адрес кафе и магазина
|
||||
- Время работы
|
||||
- Кнопка **«Написать в WhatsApp»** (большая, зелёная, всегда видна)
|
||||
- Социальные сети
|
||||
|
||||
---
|
||||
|
||||
## 📄 Страница 2. РАЗДЕЛ КАФЕ 🍕
|
||||
|
||||
**Что видит человек:**
|
||||
|
||||
🔝 **Сверху:**
|
||||
- Кнопка «← На главную»
|
||||
- Заголовок: **Кафе AQ JAIYQ**
|
||||
- Поиск по блюдам
|
||||
|
||||
📁 **Слева (на телефоне — сверху горизонтально):** Категории
|
||||
- 🍕 Пицца
|
||||
- 🍢 Шашлык
|
||||
- 🌯 Донеры
|
||||
- 🥤 Напитки
|
||||
- 🍱 Комбо
|
||||
- 🔥 Акции
|
||||
|
||||
🍽️ **Основная часть:** Сетка карточек блюд
|
||||
|
||||
```
|
||||
┌───────────────┐ ┌───────────────┐
|
||||
│ [фото] │ │ [фото] │
|
||||
│ │ │ │
|
||||
│ Пицца Маргари │ │ Шашлык из │
|
||||
│ та │ │ баранины │
|
||||
│ │ │ │
|
||||
│ 2500 ₸ │ │ 1800 ₸ │
|
||||
│ │ │ │
|
||||
│ [+ В корзину] │ │ [+ В корзину] │
|
||||
└───────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
🛒 **Корзина (плавающая внизу или сверху):**
|
||||
Когда человек добавил что-то, появляется яркая полоска:
|
||||
> **Корзина: 3 товара • 5300 ₸ → [Оформить]**
|
||||
|
||||
---
|
||||
|
||||
## 📄 Страница 3. РАЗДЕЛ МИНИМАРКЕТ 🛒
|
||||
|
||||
**То же самое, что кафе, но:**
|
||||
|
||||
📁 **Категории другие:**
|
||||
- 🥛 Молочные продукты
|
||||
- 🍞 Хлеб и выпечка
|
||||
- 🥤 Напитки
|
||||
- 🥬 Овощи и фрукты
|
||||
- 🧼 Бытовая химия
|
||||
- 🧂 Бакалея
|
||||
|
||||
⚠️ **Важно:** Если в корзине есть еда из кафе, и человек добавляет товар из минимаркета — сайт спрашивает:
|
||||
> «У вас уже есть товары из кафе. Хотите оформить два отдельных заказа или совместить доставку?»
|
||||
|
||||
Это потому что еда — горячая и срочная, а продукты — обычные.
|
||||
|
||||
---
|
||||
|
||||
## 📄 Страница 4. КАРТОЧКА ТОВАРА / БЛЮДА
|
||||
|
||||
**Когда человек кликает на товар:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ │
|
||||
│ [БОЛЬШОЕ ФОТО] │
|
||||
│ │
|
||||
├─────────────────────────────────┤
|
||||
│ │
|
||||
│ Пицца Маргарита 30 см │
|
||||
│ │
|
||||
│ ⭐ 4.8 • 152 заказа │
|
||||
│ │
|
||||
│ Классическая итальянская │
|
||||
│ пицца с томатами и моцареллой │
|
||||
│ │
|
||||
│ Вес: 450 г │
|
||||
│ Время приготовления: 20 мин │
|
||||
│ │
|
||||
│ ────────────────────────── │
|
||||
│ │
|
||||
│ 2500 ₸ │
|
||||
│ │
|
||||
│ [ − ] 1 [ + ] │
|
||||
│ │
|
||||
│ [ ДОБАВИТЬ В КОРЗИНУ ] │
|
||||
│ │
|
||||
└─────────────────────────────────┘
|
||||
|
||||
С чем ещё заказывают:
|
||||
[Кола 0.5л] [Картофель фри] [Соус]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 Страница 5. КОРЗИНА
|
||||
|
||||
**Что видит человек:**
|
||||
|
||||
```
|
||||
Ваш заказ
|
||||
|
||||
🍕 КАФЕ
|
||||
─────────────────────────────────
|
||||
Пицца Маргарита 2500 ₸
|
||||
[ − ] 1 [ + ] [удалить]
|
||||
|
||||
Шашлык из баранины 1800 ₸
|
||||
[ − ] 1 [ + ] [удалить]
|
||||
|
||||
─────────────────────────────────
|
||||
Итого по кафе: 4300 ₸
|
||||
|
||||
|
||||
🛒 МИНИМАРКЕТ
|
||||
─────────────────────────────────
|
||||
Хлеб «Бородинский» 350 ₸
|
||||
[ − ] 1 [ + ] [удалить]
|
||||
|
||||
Молоко 1л 450 ₸
|
||||
[ − ] 2 [ + ] [удалить]
|
||||
|
||||
─────────────────────────────────
|
||||
Итого по магазину: 1250 ₸
|
||||
|
||||
|
||||
══════════════════════════════════
|
||||
ВСЕГО: 5550 ₸
|
||||
|
||||
[ Промокод? ]
|
||||
|
||||
[ ОФОРМИТЬ ЗАКАЗ → ]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 Страница 6. ОФОРМЛЕНИЕ ЗАКАЗА
|
||||
|
||||
> Это самая важная страница. Здесь нельзя терять клиента.
|
||||
> Всё должно помещаться на один экран и заполняться за минуту.
|
||||
|
||||
```
|
||||
ОФОРМЛЕНИЕ ЗАКАЗА
|
||||
|
||||
1️⃣ Как вас зовут?
|
||||
[ Айдана ]
|
||||
|
||||
2️⃣ Ваш телефон
|
||||
[ +7 (___) ___-__-__ ]
|
||||
☑ Получать уведомления в WhatsApp
|
||||
|
||||
3️⃣ Как получить?
|
||||
( ● ) 🚚 Доставка ( ○ ) 🏃 Самовывоз
|
||||
|
||||
4️⃣ Адрес доставки
|
||||
Улица: [ ул. Абая ]
|
||||
Дом / ориентир:[ дом 12, синие ворота ]
|
||||
Комментарий: [ позвонить за 10 мин ]
|
||||
|
||||
5️⃣ Когда привезти?
|
||||
( ● ) Как можно быстрее
|
||||
( ○ ) К конкретному времени → [выбор времени]
|
||||
|
||||
6️⃣ Как оплатить?
|
||||
( ○ ) 💳 Kaspi QR
|
||||
( ○ ) 💳 Kaspi Pay
|
||||
( ○ ) 💳 Halyk / Homebank
|
||||
( ○ ) 💸 Перевод на карту
|
||||
( ● ) 💵 Наличными при получении
|
||||
|
||||
══════════════════════════════════
|
||||
Сумма заказа: 5550 ₸
|
||||
Доставка: 500 ₸
|
||||
ИТОГО: 6050 ₸
|
||||
|
||||
[ ✅ ПОДТВЕРДИТЬ ЗАКАЗ ]
|
||||
```
|
||||
|
||||
**Важные мелочи:**
|
||||
- Регистрация **необязательна** — достаточно имени и телефона
|
||||
- Если человек уже заказывал — сайт сам подставит его имя, телефон и адрес
|
||||
- В Нуре часто нет точного номера дома → поэтому есть поле «ориентир» (синие ворота, рядом с мечетью и т.д.)
|
||||
|
||||
---
|
||||
|
||||
## 📄 Страница 7. ЗАКАЗ ПРИНЯТ ✅
|
||||
|
||||
**Что видит человек сразу после оформления:**
|
||||
|
||||
```
|
||||
✅
|
||||
ЗАКАЗ ПРИНЯТ!
|
||||
|
||||
Номер заказа: #142
|
||||
|
||||
Мы отправили детали
|
||||
в WhatsApp на номер
|
||||
+7 777 123-45-67
|
||||
|
||||
─────────────────────
|
||||
|
||||
📍 Статус заказа:
|
||||
|
||||
[●] Принят ✓
|
||||
[ ] Готовится
|
||||
[ ] В пути
|
||||
[ ] Доставлен
|
||||
|
||||
Примерное время: 35 минут
|
||||
|
||||
─────────────────────
|
||||
|
||||
[ 💬 Написать в WhatsApp ]
|
||||
[ 📋 Посмотреть детали ]
|
||||
[ 🏠 На главную ]
|
||||
```
|
||||
|
||||
Эта же страница потом превращается в **«Статус заказа»** — клиент может зайти и посмотреть, на каком этапе его заказ.
|
||||
|
||||
---
|
||||
|
||||
## 📄 Страница 8. ЛИЧНЫЙ КАБИНЕТ (если человек вошёл)
|
||||
|
||||
**Простой экран:**
|
||||
|
||||
```
|
||||
👤 Айдана
|
||||
|
||||
📦 Мои заказы
|
||||
|
||||
#142 • Сегодня • 6050 ₸
|
||||
✅ Доставлен
|
||||
[ Повторить заказ ]
|
||||
|
||||
#138 • Вчера • 2300 ₸
|
||||
✅ Доставлен
|
||||
[ Повторить заказ ]
|
||||
|
||||
📍 Мои адреса
|
||||
• ул. Абая, дом 12
|
||||
|
||||
⚙️ Настройки
|
||||
• Имя, телефон, язык
|
||||
```
|
||||
|
||||
Кнопка **«Повторить заказ»** — очень важная. Многие клиенты в Нуре будут заказывать одно и то же каждую неделю (хлеб, молоко).
|
||||
|
||||
---
|
||||
|
||||
## 🔧 АДМИНКА (только для тебя и оператора)
|
||||
|
||||
Заходишь по отдельной ссылке и видишь:
|
||||
|
||||
### Главный экран — Заказы
|
||||
|
||||
```
|
||||
🔔 НОВЫЕ ЗАКАЗЫ (3)
|
||||
─────────────────────────────────
|
||||
#142 • Айдана • 6050 ₸
|
||||
🚚 Доставка • ул. Абая 12
|
||||
💵 Наличными
|
||||
[Принять] [Отклонить]
|
||||
─────────────────────────────────
|
||||
#143 • Болат • 1200 ₸
|
||||
🏃 Самовывоз
|
||||
💳 Kaspi QR ✅ Оплачен
|
||||
[Принять] [Отклонить]
|
||||
─────────────────────────────────
|
||||
|
||||
|
||||
🔄 В РАБОТЕ (5)
|
||||
─────────────────────────────────
|
||||
#140 • готовится • 15 мин
|
||||
[Передать курьеру]
|
||||
─────────────────────────────────
|
||||
#141 • в пути • курьер: Аскар
|
||||
[Доставлен]
|
||||
```
|
||||
|
||||
### Другие разделы админки:
|
||||
|
||||
📦 **Товары и блюда**
|
||||
- Добавить / убрать товар
|
||||
- Изменить цену
|
||||
- Поставить «нет в наличии»
|
||||
- Загрузить фото
|
||||
|
||||
🏷️ **Акции и промокоды**
|
||||
- Создать промокод
|
||||
- Запустить акцию
|
||||
|
||||
🚴 **Курьеры**
|
||||
- Список курьеров
|
||||
- Назначить курьера на заказ
|
||||
|
||||
📊 **Простая статистика**
|
||||
- Сколько заказов сегодня
|
||||
- На какую сумму
|
||||
- Самые популярные товары
|
||||
|
||||
---
|
||||
|
||||
## 📱 Уведомления в WhatsApp (не страница, но важно)
|
||||
|
||||
**Клиент получает:**
|
||||
|
||||
1. После заказа:
|
||||
> 🟢 *AQ JAIYQ*
|
||||
> Ваш заказ #142 принят!
|
||||
> Сумма: 6050 ₸
|
||||
> Время доставки: ~35 мин
|
||||
|
||||
2. Когда заказ готов:
|
||||
> 🟢 *AQ JAIYQ*
|
||||
> Курьер выехал к вам! 🚚
|
||||
> Будет через ~15 мин.
|
||||
|
||||
3. После доставки:
|
||||
> 🟢 *AQ JAIYQ*
|
||||
> Спасибо за заказ! 🙏
|
||||
> Оцените нас: ⭐⭐⭐⭐⭐
|
||||
|
||||
**Ты получаешь:**
|
||||
|
||||
> 🔔 Новый заказ #142
|
||||
> Айдана, +7 777 123-45-67
|
||||
> Сумма: 6050 ₸
|
||||
> Доставка: ул. Абая 12
|
||||
> [Открыть в админке]
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Главные принципы прототипа
|
||||
|
||||
1. **Большие кнопки** — пальцем легко попасть.
|
||||
2. **Мало текста** — больше иконок и фото.
|
||||
3. **Один экран = одно действие** — не путаем человека.
|
||||
4. **Корзина всегда видна** — клиент не забывает про неё.
|
||||
5. **WhatsApp везде** — на каждой странице есть кнопка связи.
|
||||
6. **Заказ без регистрации** — самый короткий путь к покупке.
|
||||
7. **«Повторить заказ»** — для постоянных клиентов.
|
||||
8. **Сайт работает на медленном интернете** — мало картинок, быстрая загрузка.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Что у нас получилось
|
||||
|
||||
Всего **8 страниц для клиента** + **1 админка для тебя**:
|
||||
|
||||
| № | Страница | Зачем |
|
||||
|---|----------|-------|
|
||||
| 1 | Главная | Войти на сайт, выбрать раздел |
|
||||
| 2 | Кафе | Выбрать еду |
|
||||
| 3 | Минимаркет | Выбрать продукты |
|
||||
| 4 | Карточка товара | Посмотреть детали |
|
||||
| 5 | Корзина | Проверить заказ |
|
||||
| 6 | Оформление | Указать данные и оплатить |
|
||||
| 7 | Заказ принят | Получить номер и статус |
|
||||
| 8 | Личный кабинет | История и повтор заказов |
|
||||
| — | Админка | Управление всем сервисом |
|
||||
|
||||
Этого **достаточно для MVP**. Всё остальное (карта курьера, бонусы, реферальная программа) — добавим позже.
|
||||
174
README.md
Normal file
174
README.md
Normal file
@ -0,0 +1,174 @@
|
||||
# 🌙 AQ JAIYQ — сайт кафе и минимаркета
|
||||
|
||||
Сайт онлайн-заказов для AQ JAIYQ (АҚ ЖАЙЫҚ) в посёлке Нура.
|
||||
Кафе + минимаркет, корзина, оформление заказа через WhatsApp.
|
||||
|
||||
## ✨ Что умеет сайт
|
||||
|
||||
- 🏠 **Главная** с двумя разделами, акциями, популярными блюдами и товарами
|
||||
- 🍕 **Каталог кафе** — 114 блюд по 15 категориям
|
||||
- 🛒 **Каталог минимаркета** — 4 товара (расширяется через `data/market.json`)
|
||||
- 🔎 **Поиск** — мгновенный, по всему каталогу
|
||||
- 🧺 **Корзина** — раздельно по кафе и минимаркету
|
||||
- ✍️ **Оформление заказа** — за 1 экран, без регистрации
|
||||
- 💬 **Отправка заказа в WhatsApp** — одной кнопкой, готовое сообщение
|
||||
- 📱 **Адаптив** — отлично работает на телефоне
|
||||
- 🏷️ **Метки** — Хит, Острое, Предзаказ, Фирменное, Акция
|
||||
|
||||
## 🎨 Бренд
|
||||
|
||||
Цвета взяты с логотипа:
|
||||
- **Золотой** `#C8A876`
|
||||
- **Графит** `#3D3A3A`
|
||||
- **Кремовый** `#FAF8F4`
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
```
|
||||
project_1/
|
||||
├── app/ # Страницы (Next.js App Router)
|
||||
│ ├── layout.tsx # Общий каркас (хедер, футер)
|
||||
│ ├── page.tsx # Главная
|
||||
│ ├── cafe/ # Раздел "Кафе"
|
||||
│ ├── market/ # Раздел "Минимаркет"
|
||||
│ ├── product/[section]/[id]/ # Карточка блюда / товара
|
||||
│ ├── cart/ # Корзина
|
||||
│ ├── checkout/ # Оформление заказа
|
||||
│ ├── search/ # Поиск
|
||||
│ ├── about/ # О нас / контакты
|
||||
│ └── globals.css # Глобальные стили + Tailwind
|
||||
├── components/ # React-компоненты
|
||||
│ ├── Header.tsx # Шапка
|
||||
│ ├── Footer.tsx # Подвал
|
||||
│ ├── Logo.tsx # Логотип SVG
|
||||
│ ├── ProductCard.tsx # Карточка в каталоге
|
||||
│ ├── CatalogView.tsx # Универсальный экран каталога
|
||||
│ ├── CategoryNav.tsx # Меню категорий
|
||||
│ ├── CartBar.tsx # Плавающая полоса корзины
|
||||
│ ├── WhatsAppFloat.tsx # Плавающая кнопка WhatsApp
|
||||
│ ├── AddToCartButton.tsx # Кнопка "В корзину" с кол-вом
|
||||
│ ├── Badge.tsx # Метки (хит, острое, ...)
|
||||
│ ├── SectionTile.tsx # Большие плитки на главной
|
||||
│ └── PopularRow.tsx # Лента популярного
|
||||
├── lib/
|
||||
│ ├── config.ts # ⭐ Контакты, адрес, часы (меняй здесь)
|
||||
│ ├── types.ts # TypeScript-типы
|
||||
│ ├── catalog.ts # Логика каталога и поиска
|
||||
│ └── cart-store.ts # Корзина (Zustand)
|
||||
├── data/
|
||||
│ ├── cafe.json # ⭐ Каталог кафе (114 блюд)
|
||||
│ └── market.json # ⭐ Каталог минимаркета
|
||||
├── package.json
|
||||
├── tailwind.config.ts
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
## 🚀 Запуск на компьютере
|
||||
|
||||
### 1. Установить Node.js
|
||||
Скачай и установи Node.js 20+ с https://nodejs.org
|
||||
|
||||
### 2. Установить зависимости
|
||||
В папке проекта открой терминал и запусти:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Запустить сайт
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
Открой [http://localhost:3000](http://localhost:3000) — увидишь свой сайт.
|
||||
|
||||
## 🌐 Публикация в интернете (бесплатно)
|
||||
|
||||
### Через Vercel — самый простой вариант:
|
||||
|
||||
1. Зайди на [vercel.com](https://vercel.com) и зарегистрируйся через GitHub.
|
||||
2. Залей проект в GitHub (см. ниже).
|
||||
3. На Vercel нажми **«Add New → Project»**, выбери репозиторий — он сам всё развернёт.
|
||||
4. Готово! Сайт будет жить на адресе вроде `aq-jaiyq.vercel.app`.
|
||||
|
||||
### Залить в GitHub:
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit"
|
||||
# Создай репозиторий на github.com, потом:
|
||||
git remote add origin https://github.com/ТВОЙ_ЛОГИН/aq-jaiyq.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
### Свой домен (aqjaiyq.kz):
|
||||
Купи домен на любом регистраторе (`hoster.kz`, `ps.kz` — ~5000 ₸/год)
|
||||
и привяжи к Vercel через настройки → Domains.
|
||||
|
||||
## ✏️ Как менять контент
|
||||
|
||||
### Поменять контакты, адрес, часы работы
|
||||
Файл `lib/config.ts` — там все основные данные бизнеса.
|
||||
|
||||
### Добавить товар в минимаркет
|
||||
Файл `data/market.json` — добавь новый объект в массив `products`:
|
||||
```json
|
||||
{
|
||||
"id": "mk-005",
|
||||
"category": "dairy",
|
||||
"name": "Молоко 1 л",
|
||||
"price": 450,
|
||||
"unit": "л",
|
||||
"available": true,
|
||||
"tags": []
|
||||
}
|
||||
```
|
||||
|
||||
### Добавить блюдо в кафе
|
||||
Файл `data/cafe.json` — аналогично, в массив `products`.
|
||||
|
||||
Доступные категории кафе:
|
||||
`breakfast, soups, main, salads, shashlik, steaks, cold_snacks, hot_snacks, pasta, pizza, doner, sides, sauces, company, sets`
|
||||
|
||||
Доступные категории минимаркета:
|
||||
`bread, dairy, drinks, vegetables, fruits, grocery, frozen, sweets, household`
|
||||
|
||||
### Метки (tags) для товаров и блюд
|
||||
- `hit` — Хит продаж
|
||||
- `spicy` — Острое
|
||||
- `preorder` — Предзаказ
|
||||
- `signature` — Фирменное блюдо
|
||||
- `new` — Новинка
|
||||
- `sale` — Акция (попадёт в блок «Акции дня»)
|
||||
- `vegan` — Вегетарианское
|
||||
|
||||
### Скрыть товар (нет в наличии)
|
||||
В файле каталога поменяй `"available": true` на `"available": false`.
|
||||
|
||||
### Добавить фото товаров
|
||||
Когда у тебя будут фото, добавь в каждый товар поле `"image": "/images/myfile.jpg"`,
|
||||
а сами файлы положи в папку `public/images/`.
|
||||
|
||||
## 💬 Как работает заказ через WhatsApp
|
||||
|
||||
1. Клиент заполняет форму на `/checkout`.
|
||||
2. По нажатию кнопки открывается WhatsApp с уже готовым текстом заказа.
|
||||
3. Текст приходит на номер из `lib/config.ts` → поле `ordersPhone`.
|
||||
4. Ты подтверждаешь заказ в чате с клиентом.
|
||||
|
||||
Никакая база данных не нужна — всё работает через стандартную ссылку `wa.me`.
|
||||
|
||||
## 🛠 Что добавить в следующих версиях
|
||||
|
||||
- 📷 Загрузка фото через админку (нужна Supabase)
|
||||
- 💳 Онлайн-оплата (Kaspi Pay, Halyk Epay)
|
||||
- 📦 Полноценная админка с базой данных
|
||||
- 🚴 Управление курьерами
|
||||
- 📊 Статистика продаж
|
||||
- 🇰🇿 Перевод на казахский
|
||||
|
||||
Эти задачи прописаны в `TZ.md`.
|
||||
|
||||
## 📞 Контакты
|
||||
|
||||
- 📱 Заказы: **+7 705 426 31 30**
|
||||
- 📱 Минимаркет: **+7 776 031 96 66**
|
||||
- 📍 ул. Ардагерлер 40, посёлок Нура, Карагандинская область
|
||||
87
app/about/page.tsx
Normal file
87
app/about/page.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { Phone, MapPin, Clock, MessageCircle } from 'lucide-react';
|
||||
import { BUSINESS, WHATSAPP_LINK } from '@/lib/config';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'О нас — AQ JAIYQ',
|
||||
description: 'Кафе и минимаркет AQ JAIYQ в посёлке Нура. Адрес, контакты, часы работы.'
|
||||
};
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-10">
|
||||
<h1 className="font-display text-3xl md:text-4xl font-bold text-charcoal-700 mb-3">
|
||||
О нас
|
||||
</h1>
|
||||
<p className="text-charcoal-500 mb-8">
|
||||
AQ JAIYQ — кафе и минимаркет в посёлке Нура. Мы готовим вкусную еду
|
||||
и привозим свежие продукты прямо к вам домой.
|
||||
</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Info icon={<MapPin />} title="Адрес">
|
||||
{BUSINESS.address}
|
||||
<br />
|
||||
{BUSINESS.city}
|
||||
<br />
|
||||
{BUSINESS.region}
|
||||
</Info>
|
||||
|
||||
<Info icon={<Clock />} title="Время работы">
|
||||
🍕 Кафе: {BUSINESS.cafeHours}
|
||||
<br />
|
||||
🛒 Минимаркет: {BUSINESS.marketHours}
|
||||
</Info>
|
||||
|
||||
<Info icon={<Phone />} title="Заказы">
|
||||
<a href={`tel:${BUSINESS.ordersPhone}`} className="text-gold-600 hover:underline">
|
||||
{BUSINESS.ordersPhone}
|
||||
</a>
|
||||
</Info>
|
||||
|
||||
<Info icon={<Phone />} title="Минимаркет (служебный)">
|
||||
<a href={`tel:${BUSINESS.servicePhone}`} className="text-gold-600 hover:underline">
|
||||
{BUSINESS.servicePhone}
|
||||
</a>
|
||||
</Info>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 bg-charcoal-600 text-cream rounded-3xl p-6 text-center">
|
||||
<h2 className="font-display text-xl font-bold mb-3 text-gold-300">
|
||||
Есть вопросы?
|
||||
</h2>
|
||||
<p className="text-sm text-charcoal-200 mb-4">
|
||||
Напишите нам в WhatsApp — отвечаем в течение нескольких минут.
|
||||
</p>
|
||||
<a
|
||||
href={WHATSAPP_LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 bg-green-500 hover:bg-green-600 text-white font-medium px-6 py-3 rounded-xl"
|
||||
>
|
||||
<MessageCircle size={20} />
|
||||
Открыть WhatsApp
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<Link href="/" className="text-sm text-charcoal-500 hover:underline">
|
||||
← На главную
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Info({ icon, title, children }: { icon: React.ReactNode; title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="bg-white rounded-2xl shadow-soft p-5">
|
||||
<div className="w-10 h-10 rounded-full bg-gold-100 text-gold-600 flex items-center justify-center mb-3">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="text-xs uppercase tracking-wider text-charcoal-400 mb-1">{title}</div>
|
||||
<div className="text-sm text-charcoal-700 leading-relaxed">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
app/cafe/page.tsx
Normal file
22
app/cafe/page.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { CatalogView } from '@/components/CatalogView';
|
||||
import cafeData from '@/data/cafe.json';
|
||||
import type { CatalogData } from '@/lib/types';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Кафе AQ JAIYQ — меню и доставка | Нура',
|
||||
description:
|
||||
'Меню кафе AQ JAIYQ: пицца, шашлык, донеры, плов, паста, салаты. Доставка по Нуре.'
|
||||
};
|
||||
|
||||
export default function CafePage() {
|
||||
return (
|
||||
<CatalogView
|
||||
section="cafe"
|
||||
data={cafeData as CatalogData}
|
||||
title="Кафе AQ JAIYQ"
|
||||
description="Авторская кухня, классика, фирменные блюда. Готовим из свежих продуктов."
|
||||
emoji="🍕"
|
||||
/>
|
||||
);
|
||||
}
|
||||
190
app/cart/page.tsx
Normal file
190
app/cart/page.tsx
Normal file
@ -0,0 +1,190 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { ChevronLeft, Minus, Plus, Trash2, ShoppingBag } from 'lucide-react';
|
||||
import { useCart } from '@/lib/cart-store';
|
||||
import { formatPrice } from '@/lib/catalog';
|
||||
import type { CartItem } from '@/lib/types';
|
||||
|
||||
export default function CartPage() {
|
||||
const cafeItems = useCart(s => s.getCafeItems());
|
||||
const marketItems = useCart(s => s.getMarketItems());
|
||||
const cafeTotal = useCart(s => s.getCafeTotal());
|
||||
const marketTotal = useCart(s => s.getMarketTotal());
|
||||
const total = useCart(s => s.getTotal());
|
||||
const update = useCart(s => s.updateQuantity);
|
||||
const remove = useCart(s => s.removeItem);
|
||||
|
||||
const hasItems = cafeItems.length > 0 || marketItems.length > 0;
|
||||
const hasBoth = cafeItems.length > 0 && marketItems.length > 0;
|
||||
|
||||
if (!hasItems) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto px-4 py-16 text-center">
|
||||
<ShoppingBag size={60} className="mx-auto text-charcoal-200 mb-4" />
|
||||
<h1 className="font-display text-2xl font-bold text-charcoal-700 mb-2">
|
||||
Корзина пуста
|
||||
</h1>
|
||||
<p className="text-charcoal-400 mb-6">
|
||||
Выберите что-нибудь в кафе или минимаркете
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<Link href="/cafe" className="btn-secondary">
|
||||
🍕 В кафе
|
||||
</Link>
|
||||
<Link href="/market" className="btn-primary">
|
||||
🛒 В минимаркет
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-6">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-1 text-sm text-charcoal-500 hover:text-charcoal-700 mb-4"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
Продолжить покупки
|
||||
</Link>
|
||||
|
||||
<h1 className="font-display text-2xl md:text-3xl font-bold text-charcoal-700 mb-6">
|
||||
Ваш заказ
|
||||
</h1>
|
||||
|
||||
{/* Подсказка при смешанной корзине */}
|
||||
{hasBoth && (
|
||||
<div className="bg-blue-50 border border-blue-100 text-blue-900 rounded-2xl p-4 mb-5 text-sm">
|
||||
💡 <strong>В корзине есть еда и продукты.</strong> Мы доставим их одним
|
||||
заказом — еда из кафе уедет горячей, а минимаркет соберём отдельной
|
||||
сумкой.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кафе */}
|
||||
{cafeItems.length > 0 && (
|
||||
<CartSection
|
||||
title="🍕 Кафе"
|
||||
items={cafeItems}
|
||||
subtotal={cafeTotal}
|
||||
onUpdate={update}
|
||||
onRemove={remove}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Минимаркет */}
|
||||
{marketItems.length > 0 && (
|
||||
<CartSection
|
||||
title="🛒 Минимаркет"
|
||||
items={marketItems}
|
||||
subtotal={marketTotal}
|
||||
onUpdate={update}
|
||||
onRemove={remove}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Итого */}
|
||||
<div className="bg-white rounded-2xl shadow-card p-5 mt-6">
|
||||
<div className="flex items-baseline justify-between mb-1">
|
||||
<span className="text-charcoal-500">Сумма заказа</span>
|
||||
<span className="font-medium">{formatPrice(total)}</span>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between mb-3 text-xs text-charcoal-400">
|
||||
<span>Стоимость доставки уточним при оформлении</span>
|
||||
</div>
|
||||
<div className="h-px bg-charcoal-100 mb-3" />
|
||||
<div className="flex items-baseline justify-between mb-4">
|
||||
<span className="text-lg font-bold text-charcoal-700">Итого</span>
|
||||
<span className="text-2xl font-display font-bold text-charcoal-700">
|
||||
{formatPrice(total)}
|
||||
</span>
|
||||
</div>
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="btn-primary w-full justify-center inline-flex items-center"
|
||||
>
|
||||
Оформить заказ →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CartSection({
|
||||
title,
|
||||
items,
|
||||
subtotal,
|
||||
onUpdate,
|
||||
onRemove
|
||||
}: {
|
||||
title: string;
|
||||
items: CartItem[];
|
||||
subtotal: number;
|
||||
onUpdate: (id: string, q: number) => void;
|
||||
onRemove: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white rounded-2xl shadow-soft p-4 mb-4">
|
||||
<div className="font-display text-lg font-bold text-charcoal-700 mb-3">
|
||||
{title}
|
||||
</div>
|
||||
<div className="divide-y divide-charcoal-100">
|
||||
{items.map(item => (
|
||||
<div key={item.productId} className="py-3 flex items-center gap-3">
|
||||
<div className="w-16 h-16 bg-sand rounded-xl flex items-center justify-center text-2xl text-gold-300 shrink-0">
|
||||
{item.section === 'cafe' ? '🍽️' : '🛒'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm text-charcoal-700 line-clamp-2">
|
||||
{item.name}
|
||||
</div>
|
||||
<div className="text-xs text-charcoal-400 mt-0.5">
|
||||
{formatPrice(item.price)}
|
||||
{item.unit ? ` / ${item.unit}` : ''}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<div className="flex items-center bg-sand rounded-lg">
|
||||
<button
|
||||
onClick={() => onUpdate(item.productId, item.quantity - 1)}
|
||||
className="p-1.5 hover:text-gold-600"
|
||||
aria-label="Уменьшить"
|
||||
>
|
||||
<Minus size={14} />
|
||||
</button>
|
||||
<span className="w-7 text-center text-sm font-medium">
|
||||
{item.quantity}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => onUpdate(item.productId, item.quantity + 1)}
|
||||
className="p-1.5 hover:text-gold-600"
|
||||
aria-label="Увеличить"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onRemove(item.productId)}
|
||||
className="text-charcoal-300 hover:text-red-500 p-1"
|
||||
aria-label="Удалить"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<div className="font-bold text-sm text-charcoal-700">
|
||||
{formatPrice(item.price * item.quantity)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t border-charcoal-100 flex items-center justify-between text-sm">
|
||||
<span className="text-charcoal-500">Промежуточный итог</span>
|
||||
<span className="font-bold">{formatPrice(subtotal)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
472
app/checkout/page.tsx
Normal file
472
app/checkout/page.tsx
Normal file
@ -0,0 +1,472 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ChevronLeft, MessageCircle } from 'lucide-react';
|
||||
import { useCart } from '@/lib/cart-store';
|
||||
import { formatPrice } from '@/lib/catalog';
|
||||
import { BUSINESS, whatsappLink } from '@/lib/config';
|
||||
import type { CartItem } from '@/lib/types';
|
||||
|
||||
type DeliveryType = 'delivery' | 'pickup';
|
||||
type PaymentType = 'kaspi_qr' | 'kaspi_pay' | 'halyk' | 'transfer' | 'cash';
|
||||
type WhenType = 'asap' | 'time';
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const router = useRouter();
|
||||
const items = useCart(s => s.items);
|
||||
const cafeItems = useCart(s => s.getCafeItems());
|
||||
const marketItems = useCart(s => s.getMarketItems());
|
||||
const subtotal = useCart(s => s.getTotal());
|
||||
const clearCart = useCart(s => s.clearCart);
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [delivery, setDelivery] = useState<DeliveryType>('delivery');
|
||||
const [street, setStreet] = useState('');
|
||||
const [landmark, setLandmark] = useState('');
|
||||
const [comment, setComment] = useState('');
|
||||
const [when, setWhen] = useState<WhenType>('asap');
|
||||
const [time, setTime] = useState('');
|
||||
const [payment, setPayment] = useState<PaymentType>('cash');
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
if (items.length === 0 && !submitted) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto px-4 py-16 text-center">
|
||||
<h1 className="font-display text-2xl font-bold text-charcoal-700 mb-3">
|
||||
Корзина пуста
|
||||
</h1>
|
||||
<Link href="/" className="btn-primary inline-block">
|
||||
На главную
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const deliveryFee = delivery === 'delivery' && subtotal < BUSINESS.freeDeliveryFrom
|
||||
? BUSINESS.deliveryFee
|
||||
: 0;
|
||||
const total = subtotal + deliveryFee;
|
||||
|
||||
const valid =
|
||||
name.trim().length >= 2 &&
|
||||
phone.replace(/\D/g, '').length >= 10 &&
|
||||
(delivery === 'pickup' || (street.trim() && landmark.trim())) &&
|
||||
(when === 'asap' || time);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!valid) return;
|
||||
|
||||
const orderText = buildOrderText({
|
||||
name,
|
||||
phone,
|
||||
delivery,
|
||||
street,
|
||||
landmark,
|
||||
comment,
|
||||
when,
|
||||
time,
|
||||
payment,
|
||||
cafeItems,
|
||||
marketItems,
|
||||
subtotal,
|
||||
deliveryFee,
|
||||
total
|
||||
});
|
||||
|
||||
// Открываем WhatsApp с заранее заполненным текстом
|
||||
const url = whatsappLink(orderText);
|
||||
window.open(url, '_blank');
|
||||
|
||||
// Очищаем корзину и показываем подтверждение
|
||||
clearCart();
|
||||
setSubmitted(true);
|
||||
};
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<div className="max-w-xl mx-auto px-4 py-16 text-center">
|
||||
<div className="text-6xl mb-4">✅</div>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-bold text-charcoal-700 mb-3">
|
||||
Заказ отправлен в WhatsApp
|
||||
</h1>
|
||||
<p className="text-charcoal-500 mb-6">
|
||||
Мы получим ваше сообщение и свяжемся в течение нескольких минут
|
||||
для подтверждения. Если WhatsApp не открылся — нажмите кнопку ниже.
|
||||
</p>
|
||||
<a
|
||||
href={whatsappLink('Здравствуйте! Я отправлял(а) заказ через сайт.')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-primary inline-flex items-center gap-2 mb-3"
|
||||
>
|
||||
<MessageCircle size={20} />
|
||||
Открыть WhatsApp
|
||||
</a>
|
||||
<div>
|
||||
<Link href="/" className="text-sm text-charcoal-500 hover:underline">
|
||||
Вернуться на главную
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-6">
|
||||
<Link
|
||||
href="/cart"
|
||||
className="inline-flex items-center gap-1 text-sm text-charcoal-500 hover:text-charcoal-700 mb-4"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
В корзину
|
||||
</Link>
|
||||
|
||||
<h1 className="font-display text-2xl md:text-3xl font-bold text-charcoal-700 mb-6">
|
||||
Оформление заказа
|
||||
</h1>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* 1. Имя */}
|
||||
<Field label="Как вас зовут?" required>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="Например: Айдана"
|
||||
className="input"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* 2. Телефон */}
|
||||
<Field label="Ваш телефон" required>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={e => setPhone(e.target.value)}
|
||||
placeholder="+7 (___) ___-__-__"
|
||||
className="input"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-charcoal-400 mt-1">
|
||||
Свяжемся для подтверждения заказа
|
||||
</p>
|
||||
</Field>
|
||||
|
||||
{/* 3. Как получить */}
|
||||
<Field label="Как получить заказ?" required>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<ChoiceTile
|
||||
active={delivery === 'delivery'}
|
||||
onClick={() => setDelivery('delivery')}
|
||||
emoji="🚚"
|
||||
label="Доставка"
|
||||
hint="Привезём к вам"
|
||||
/>
|
||||
<ChoiceTile
|
||||
active={delivery === 'pickup'}
|
||||
onClick={() => setDelivery('pickup')}
|
||||
emoji="🏃"
|
||||
label="Самовывоз"
|
||||
hint="Заберу сам"
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
{/* 4. Адрес — только для доставки */}
|
||||
{delivery === 'delivery' && (
|
||||
<div className="space-y-3 bg-sand rounded-2xl p-4">
|
||||
<Field label="Улица" required>
|
||||
<input
|
||||
type="text"
|
||||
value={street}
|
||||
onChange={e => setStreet(e.target.value)}
|
||||
placeholder="например: ул. Ардагерлер"
|
||||
className="input"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Дом / ориентир" required>
|
||||
<input
|
||||
type="text"
|
||||
value={landmark}
|
||||
onChange={e => setLandmark(e.target.value)}
|
||||
placeholder="дом 12, синие ворота / возле мечети"
|
||||
className="input"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-charcoal-500 mt-1">
|
||||
💡 Можно указать ориентир, если нет точного номера дома —
|
||||
например <em>«рядом со школой»</em> или <em>«синие ворота»</em>
|
||||
</p>
|
||||
</Field>
|
||||
<Field label="Комментарий курьеру (необязательно)">
|
||||
<input
|
||||
type="text"
|
||||
value={comment}
|
||||
onChange={e => setComment(e.target.value)}
|
||||
placeholder="например: позвонить за 10 минут"
|
||||
className="input"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 5. Когда */}
|
||||
<Field label="Когда нужно?" required>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<ChoiceTile
|
||||
active={when === 'asap'}
|
||||
onClick={() => setWhen('asap')}
|
||||
emoji="⚡"
|
||||
label="Как можно быстрее"
|
||||
hint=""
|
||||
/>
|
||||
<ChoiceTile
|
||||
active={when === 'time'}
|
||||
onClick={() => setWhen('time')}
|
||||
emoji="🕐"
|
||||
label="К определённому времени"
|
||||
hint=""
|
||||
/>
|
||||
</div>
|
||||
{when === 'time' && (
|
||||
<input
|
||||
type="time"
|
||||
value={time}
|
||||
onChange={e => setTime(e.target.value)}
|
||||
className="input mt-2"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
{/* 6. Оплата */}
|
||||
<Field label="Способ оплаты" required>
|
||||
<div className="grid gap-2">
|
||||
<PayChoice
|
||||
active={payment === 'cash'}
|
||||
onClick={() => setPayment('cash')}
|
||||
label="💵 Наличными при получении"
|
||||
hint="Курьер привезёт сдачу"
|
||||
/>
|
||||
<PayChoice
|
||||
active={payment === 'kaspi_qr'}
|
||||
onClick={() => setPayment('kaspi_qr')}
|
||||
label="📱 Kaspi QR"
|
||||
hint="Сканируете QR-код в Kaspi.kz"
|
||||
/>
|
||||
<PayChoice
|
||||
active={payment === 'kaspi_pay'}
|
||||
onClick={() => setPayment('kaspi_pay')}
|
||||
label="💳 Kaspi Pay"
|
||||
hint="Оплата через приложение Kaspi"
|
||||
/>
|
||||
<PayChoice
|
||||
active={payment === 'halyk'}
|
||||
onClick={() => setPayment('halyk')}
|
||||
label="🏦 Halyk / Homebank"
|
||||
hint="Картой через Halyk"
|
||||
/>
|
||||
<PayChoice
|
||||
active={payment === 'transfer'}
|
||||
onClick={() => setPayment('transfer')}
|
||||
label="💸 Перевод на карту"
|
||||
hint="Реквизиты отправим в WhatsApp"
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
{/* Сумма */}
|
||||
<div className="bg-white rounded-2xl shadow-card p-5">
|
||||
<div className="flex items-baseline justify-between mb-1 text-sm">
|
||||
<span className="text-charcoal-500">Сумма заказа</span>
|
||||
<span>{formatPrice(subtotal)}</span>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between mb-3 text-sm">
|
||||
<span className="text-charcoal-500">Доставка</span>
|
||||
<span>
|
||||
{deliveryFee === 0
|
||||
? delivery === 'pickup'
|
||||
? '—'
|
||||
: 'Бесплатно'
|
||||
: formatPrice(deliveryFee)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-px bg-charcoal-100 mb-3" />
|
||||
<div className="flex items-baseline justify-between mb-4">
|
||||
<span className="text-lg font-bold">Итого</span>
|
||||
<span className="text-2xl font-display font-bold text-charcoal-700">
|
||||
{formatPrice(total)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!valid}
|
||||
className={`w-full h-14 rounded-xl font-medium text-base flex items-center justify-center gap-2 shadow-soft transition-all ${
|
||||
valid
|
||||
? 'bg-green-500 hover:bg-green-600 text-white hover:shadow-gold'
|
||||
: 'bg-charcoal-100 text-charcoal-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<MessageCircle size={20} />
|
||||
Отправить заказ в WhatsApp
|
||||
</button>
|
||||
|
||||
<p className="text-xs text-center text-charcoal-400 mt-3">
|
||||
Нажимая кнопку, вы откроете WhatsApp с уже готовым заказом —
|
||||
нужно только нажать «Отправить».
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
required,
|
||||
children
|
||||
}: {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-charcoal-700 mb-2">
|
||||
{label}
|
||||
{required && <span className="text-red-500"> *</span>}
|
||||
</label>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChoiceTile({
|
||||
active,
|
||||
onClick,
|
||||
emoji,
|
||||
label,
|
||||
hint
|
||||
}: {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
emoji: string;
|
||||
label: string;
|
||||
hint: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
active
|
||||
? 'border-gold-400 bg-gold-50'
|
||||
: 'border-charcoal-100 bg-white hover:border-charcoal-200'
|
||||
}`}
|
||||
>
|
||||
<div className="text-2xl mb-1">{emoji}</div>
|
||||
<div className="text-sm font-medium text-charcoal-700">{label}</div>
|
||||
{hint && <div className="text-xs text-charcoal-400">{hint}</div>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function PayChoice({
|
||||
active,
|
||||
onClick,
|
||||
label,
|
||||
hint
|
||||
}: {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
hint: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
active
|
||||
? 'border-gold-400 bg-gold-50'
|
||||
: 'border-charcoal-100 bg-white hover:border-charcoal-200'
|
||||
}`}
|
||||
>
|
||||
<div className="text-sm font-medium text-charcoal-700">{label}</div>
|
||||
<div className="text-xs text-charcoal-400 mt-0.5">{hint}</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Билдер текста заказа для WhatsApp
|
||||
function buildOrderText(o: {
|
||||
name: string;
|
||||
phone: string;
|
||||
delivery: DeliveryType;
|
||||
street: string;
|
||||
landmark: string;
|
||||
comment: string;
|
||||
when: WhenType;
|
||||
time: string;
|
||||
payment: PaymentType;
|
||||
cafeItems: CartItem[];
|
||||
marketItems: CartItem[];
|
||||
subtotal: number;
|
||||
deliveryFee: number;
|
||||
total: number;
|
||||
}): string {
|
||||
const payLabel: Record<PaymentType, string> = {
|
||||
cash: '💵 Наличными при получении',
|
||||
kaspi_qr: '📱 Kaspi QR',
|
||||
kaspi_pay: '💳 Kaspi Pay',
|
||||
halyk: '🏦 Halyk / Homebank',
|
||||
transfer: '💸 Перевод на карту'
|
||||
};
|
||||
|
||||
let text = `🛍️ *Новый заказ с сайта AQ JAIYQ*\n\n`;
|
||||
text += `👤 ${o.name}\n`;
|
||||
text += `📞 ${o.phone}\n\n`;
|
||||
|
||||
if (o.delivery === 'delivery') {
|
||||
text += `🚚 *Доставка*\n`;
|
||||
text += `📍 ${o.street}, ${o.landmark}\n`;
|
||||
if (o.comment) text += `💬 ${o.comment}\n`;
|
||||
} else {
|
||||
text += `🏃 *Самовывоз* (${BUSINESS.address}, ${BUSINESS.city})\n`;
|
||||
}
|
||||
|
||||
text += `🕐 ${o.when === 'asap' ? 'Как можно быстрее' : `К ${o.time}`}\n`;
|
||||
text += `💰 ${payLabel[o.payment]}\n\n`;
|
||||
|
||||
if (o.cafeItems.length > 0) {
|
||||
text += `🍕 *КАФЕ*\n`;
|
||||
for (const item of o.cafeItems) {
|
||||
text += `• ${item.name} × ${item.quantity} = ${item.price * item.quantity} ₸\n`;
|
||||
}
|
||||
text += `\n`;
|
||||
}
|
||||
|
||||
if (o.marketItems.length > 0) {
|
||||
text += `🛒 *МИНИМАРКЕТ*\n`;
|
||||
for (const item of o.marketItems) {
|
||||
text += `• ${item.name} × ${item.quantity} = ${item.price * item.quantity} ₸\n`;
|
||||
}
|
||||
text += `\n`;
|
||||
}
|
||||
|
||||
text += `━━━━━━━━━━━━━━━\n`;
|
||||
text += `Сумма: ${o.subtotal.toLocaleString('ru-RU')} ₸\n`;
|
||||
if (o.deliveryFee > 0) {
|
||||
text += `Доставка: ${o.deliveryFee.toLocaleString('ru-RU')} ₸\n`;
|
||||
}
|
||||
text += `*Итого: ${o.total.toLocaleString('ru-RU')} ₸*`;
|
||||
|
||||
return text;
|
||||
}
|
||||
59
app/globals.css
Normal file
59
app/globals.css
Normal file
@ -0,0 +1,59 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--brand-gold: #C8A876;
|
||||
--brand-charcoal: #3D3A3A;
|
||||
--brand-cream: #FAF8F4;
|
||||
}
|
||||
|
||||
html, body {
|
||||
background: var(--brand-cream);
|
||||
color: var(--brand-charcoal);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* Плавные переходы для всех кнопок */
|
||||
button, a {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
/* Скрываем скроллбар на горизонтальных лентах категорий, но оставляем функционал */
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
/* Анимация для пульсации хитов */
|
||||
@keyframes pulse-gold {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(200, 168, 118, 0.4); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(200, 168, 118, 0); }
|
||||
}
|
||||
|
||||
.animate-pulse-gold {
|
||||
animation: pulse-gold 2s infinite;
|
||||
}
|
||||
|
||||
/* Утилитарные классы для бренда */
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
@apply bg-gold-400 hover:bg-gold-500 text-white font-medium px-5 py-3 rounded-xl shadow-soft hover:shadow-gold transition-all;
|
||||
}
|
||||
.btn-secondary {
|
||||
@apply bg-charcoal-600 hover:bg-charcoal-700 text-cream font-medium px-5 py-3 rounded-xl shadow-soft transition-all;
|
||||
}
|
||||
.btn-ghost {
|
||||
@apply bg-white hover:bg-sand text-charcoal-600 font-medium px-5 py-3 rounded-xl border border-charcoal-100 transition-all;
|
||||
}
|
||||
.card {
|
||||
@apply bg-white rounded-2xl shadow-soft hover:shadow-card transition-shadow overflow-hidden;
|
||||
}
|
||||
.input {
|
||||
@apply w-full h-12 px-4 rounded-xl border border-charcoal-100 bg-white text-charcoal-700 placeholder:text-charcoal-300 focus:outline-none focus:border-gold-400 focus:ring-2 focus:ring-gold-200 transition-all;
|
||||
}
|
||||
}
|
||||
40
app/layout.tsx
Normal file
40
app/layout.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { WhatsAppFloat } from '@/components/WhatsAppFloat';
|
||||
import { CartBar } from '@/components/CartBar';
|
||||
import { BUSINESS } from '@/lib/config';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `${BUSINESS.name} — кафе и минимаркет в Нуре`,
|
||||
description:
|
||||
'AQ JAIYQ — доставка еды из кафе и продуктов из минимаркета в посёлке Нура. Быстрый заказ через WhatsApp, оплата картой или наличными.',
|
||||
keywords: ['Нура', 'кафе', 'минимаркет', 'доставка', 'AQ JAIYQ', 'АҚ ЖАЙЫҚ'],
|
||||
openGraph: {
|
||||
title: `${BUSINESS.name} — кафе и минимаркет в Нуре`,
|
||||
description: 'Доставка еды и продуктов в Нуре. Заказ за 2 минуты.',
|
||||
type: 'website',
|
||||
locale: 'ru_KZ'
|
||||
}
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
themeColor: '#3D3A3A'
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="ru">
|
||||
<body className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1 pb-24 md:pb-0">{children}</main>
|
||||
<Footer />
|
||||
<WhatsAppFloat />
|
||||
<CartBar />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
22
app/market/page.tsx
Normal file
22
app/market/page.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { CatalogView } from '@/components/CatalogView';
|
||||
import marketData from '@/data/market.json';
|
||||
import type { CatalogData } from '@/lib/types';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Минимаркет AQ JAIYQ — продукты с доставкой | Нура',
|
||||
description:
|
||||
'Хлеб, молочка, овощи и бытовые товары. Доставка по посёлку Нура из минимаркета AQ JAIYQ (24/7).'
|
||||
};
|
||||
|
||||
export default function MarketPage() {
|
||||
return (
|
||||
<CatalogView
|
||||
section="market"
|
||||
data={marketData as CatalogData}
|
||||
title="Минимаркет AQ JAIYQ"
|
||||
description="Свежие продукты и товары для дома. Работаем круглосуточно — закажете в любое время."
|
||||
emoji="🛒"
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
app/not-found.tsx
Normal file
26
app/not-found.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="max-w-xl mx-auto px-4 py-20 text-center">
|
||||
<div className="text-7xl mb-4">🍽️</div>
|
||||
<h1 className="font-display text-3xl font-bold text-charcoal-700 mb-2">
|
||||
Страница не найдена
|
||||
</h1>
|
||||
<p className="text-charcoal-500 mb-6">
|
||||
Кажется, такой страницы нет. Может быть, вы искали что-то из меню?
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<Link href="/" className="btn-ghost">
|
||||
На главную
|
||||
</Link>
|
||||
<Link href="/cafe" className="btn-secondary">
|
||||
🍕 В кафе
|
||||
</Link>
|
||||
<Link href="/market" className="btn-primary">
|
||||
🛒 В минимаркет
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
196
app/page.tsx
Normal file
196
app/page.tsx
Normal file
@ -0,0 +1,196 @@
|
||||
import Link from 'next/link';
|
||||
import { Search, ShoppingBag, Smile, Truck } from 'lucide-react';
|
||||
import { SectionTile } from '@/components/SectionTile';
|
||||
import { PopularRow } from '@/components/PopularRow';
|
||||
import { getPopularCafe, getPopularMarket, getSaleItems } from '@/lib/catalog';
|
||||
import { BUSINESS } from '@/lib/config';
|
||||
|
||||
export default function HomePage() {
|
||||
const popularCafe = getPopularCafe(8);
|
||||
const popularMarket = getPopularMarket(8);
|
||||
const sales = getSaleItems(6);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* HERO */}
|
||||
<section className="bg-gradient-to-br from-charcoal-600 via-charcoal-700 to-charcoal-600 text-cream">
|
||||
<div className="max-w-7xl mx-auto px-4 py-10 md:py-16">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="font-display text-3xl md:text-5xl font-bold mb-3 text-gold-300">
|
||||
AQ JAIYQ
|
||||
</h1>
|
||||
<p className="text-sm md:text-base text-charcoal-200 max-w-xl mx-auto">
|
||||
Кафе и минимаркет в посёлке Нура.<br />
|
||||
Закажи за 2 минуты — доставим домой или забери сам.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Большой поиск */}
|
||||
<Link
|
||||
href="/search"
|
||||
className="max-w-xl mx-auto bg-white rounded-2xl shadow-card flex items-center gap-3 px-4 py-3 hover:scale-[1.02] transition-transform"
|
||||
>
|
||||
<Search size={20} className="text-charcoal-400 shrink-0" />
|
||||
<span className="text-charcoal-400 text-sm">
|
||||
Найти товар или блюдо…
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Подсказки запросов */}
|
||||
<div className="flex flex-wrap justify-center gap-2 mt-3 text-xs text-charcoal-300">
|
||||
<span className="opacity-60">Популярное:</span>
|
||||
{['пицца', 'шашлык', 'донер', 'хлеб', 'сахар'].map(q => (
|
||||
<Link
|
||||
key={q}
|
||||
href={`/search?q=${encodeURIComponent(q)}`}
|
||||
className="hover:text-gold-300"
|
||||
>
|
||||
{q}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Две главные плитки */}
|
||||
<section className="py-8 md:py-12">
|
||||
<div className="max-w-7xl mx-auto px-4 grid md:grid-cols-2 gap-4 md:gap-6">
|
||||
<SectionTile
|
||||
href="/cafe"
|
||||
title="Кафе"
|
||||
subtitle="Еда на заказ — пицца, шашлык, донеры, плов и многое другое"
|
||||
emoji="🍕"
|
||||
variant="cafe"
|
||||
/>
|
||||
<SectionTile
|
||||
href="/market"
|
||||
title="Минимаркет"
|
||||
subtitle="Продукты с доставкой — хлеб, молочка, овощи, бытовое"
|
||||
emoji="🛒"
|
||||
variant="market"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Акции дня — если есть */}
|
||||
{sales.length > 0 && (
|
||||
<section className="py-8 bg-gradient-to-br from-gold-50 to-gold-100">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<h2 className="font-display text-2xl md:text-3xl font-bold text-charcoal-700 mb-5">
|
||||
🔥 Акции дня
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4">
|
||||
{sales.map(item => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={`/product/${item.section}/${item.id}`}
|
||||
className="card group flex flex-col"
|
||||
>
|
||||
<div className="aspect-square bg-sand flex items-center justify-center text-5xl text-gold-300">
|
||||
{item.section === 'cafe' ? '🍽️' : '🛒'}
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<h3 className="font-medium text-sm">{item.name}</h3>
|
||||
<div className="mt-2 font-bold text-charcoal-700">
|
||||
{item.price.toLocaleString('ru-RU')} ₸
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Популярные блюда */}
|
||||
<PopularRow
|
||||
title="🏆 Популярные блюда"
|
||||
subtitle="Что чаще всего заказывают в нашем кафе"
|
||||
products={popularCafe}
|
||||
section="cafe"
|
||||
/>
|
||||
|
||||
{/* Популярные товары */}
|
||||
<PopularRow
|
||||
title="⭐ Популярные товары"
|
||||
subtitle="Самое нужное из минимаркета"
|
||||
products={popularMarket}
|
||||
section="market"
|
||||
/>
|
||||
|
||||
{/* Как это работает */}
|
||||
<section className="py-10 md:py-14 bg-charcoal-50">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<h2 className="font-display text-2xl md:text-3xl font-bold text-center text-charcoal-700 mb-8">
|
||||
Как это работает
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
<Step
|
||||
icon={<Search size={28} />}
|
||||
num={1}
|
||||
title="Выбери"
|
||||
text="Зайди в кафе или минимаркет, выбери блюда и товары"
|
||||
/>
|
||||
<Step
|
||||
icon={<ShoppingBag size={28} />}
|
||||
num={2}
|
||||
title="Оформи"
|
||||
text="Укажи имя, адрес и удобный способ оплаты"
|
||||
/>
|
||||
<Step
|
||||
icon={<Truck size={28} />}
|
||||
num={3}
|
||||
title="Получи"
|
||||
text="Привезём домой по Нуре или забери сам"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Информационная плашка */}
|
||||
<section className="py-8">
|
||||
<div className="max-w-3xl mx-auto px-4">
|
||||
<div className="bg-gradient-to-r from-gold-400 to-gold-500 rounded-2xl p-6 text-charcoal-700 text-center shadow-card">
|
||||
<Smile size={32} className="mx-auto mb-2" />
|
||||
<h3 className="font-display text-xl font-bold mb-2">
|
||||
Заказ за 2 минуты — без регистрации
|
||||
</h3>
|
||||
<p className="text-sm opacity-90">
|
||||
Достаточно имени и номера телефона. Отправка заказа — прямо в наш WhatsApp.
|
||||
</p>
|
||||
<div className="mt-3 text-xs opacity-75">
|
||||
{BUSINESS.address}, {BUSINESS.city}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Step({
|
||||
icon,
|
||||
num,
|
||||
title,
|
||||
text
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
num: number;
|
||||
title: string;
|
||||
text: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white rounded-2xl p-6 shadow-soft relative">
|
||||
<div className="absolute top-4 right-4 text-5xl font-display font-bold text-gold-100">
|
||||
{num}
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-full bg-gold-100 text-gold-600 flex items-center justify-center mb-4">
|
||||
{icon}
|
||||
</div>
|
||||
<h3 className="font-display text-lg font-bold mb-1 text-charcoal-700">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm text-charcoal-400">{text}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
123
app/product/[section]/[id]/page.tsx
Normal file
123
app/product/[section]/[id]/page.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { Badge } from '@/components/Badge';
|
||||
import { AddToCartButton } from '@/components/AddToCartButton';
|
||||
import {
|
||||
getProductById,
|
||||
getCategoryName,
|
||||
formatPrice
|
||||
} from '@/lib/catalog';
|
||||
import type { Section } from '@/lib/types';
|
||||
|
||||
interface PageProps {
|
||||
params: { section: string; id: string };
|
||||
}
|
||||
|
||||
export default function ProductPage({ params }: PageProps) {
|
||||
const sec = params.section as Section;
|
||||
if (sec !== 'cafe' && sec !== 'market') notFound();
|
||||
|
||||
const product = getProductById(sec, params.id);
|
||||
if (!product) notFound();
|
||||
|
||||
const categoryName = getCategoryName(sec, product.category);
|
||||
const backHref = sec === 'cafe' ? '/cafe' : '/market';
|
||||
const backLabel = sec === 'cafe' ? 'Меню кафе' : 'Минимаркет';
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto px-4 py-6">
|
||||
{/* Назад */}
|
||||
<Link
|
||||
href={backHref}
|
||||
className="inline-flex items-center gap-1 text-sm text-charcoal-500 hover:text-charcoal-700 mb-4"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
{backLabel}
|
||||
</Link>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6 md:gap-10">
|
||||
{/* Фото */}
|
||||
<div className="aspect-square bg-sand rounded-3xl overflow-hidden flex items-center justify-center">
|
||||
{product.image ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-8xl text-gold-300">
|
||||
{sec === 'cafe' ? '🍽️' : '🛒'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Информация */}
|
||||
<div className="flex flex-col">
|
||||
<div className="text-xs uppercase tracking-wider text-charcoal-400 mb-2">
|
||||
{categoryName}
|
||||
</div>
|
||||
|
||||
<h1 className="font-display text-3xl md:text-4xl font-bold text-charcoal-700 mb-3">
|
||||
{product.name}
|
||||
</h1>
|
||||
|
||||
{/* Метки */}
|
||||
{product.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{product.tags.map(t => (
|
||||
<Badge key={t} tag={t} size="md" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Единица */}
|
||||
{product.unit && (
|
||||
<div className="text-sm text-charcoal-500 mb-3">{product.unit}</div>
|
||||
)}
|
||||
|
||||
{/* Состав */}
|
||||
{product.ingredients && (
|
||||
<div className="bg-sand rounded-2xl p-4 mb-4">
|
||||
<div className="text-xs uppercase tracking-wider text-charcoal-400 mb-1">
|
||||
Состав
|
||||
</div>
|
||||
<div className="text-sm text-charcoal-700 leading-relaxed">
|
||||
{product.ingredients}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Описание */}
|
||||
{product.description && (
|
||||
<p className="text-sm text-charcoal-600 leading-relaxed mb-4">
|
||||
{product.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Предупреждение про предзаказ */}
|
||||
{product.tags.includes('preorder') && (
|
||||
<div className="bg-blue-50 text-blue-800 text-sm rounded-2xl p-4 mb-4 border border-blue-100">
|
||||
📅 <strong>Этот товар — по предзаказу.</strong> После оформления
|
||||
мы свяжемся с вами в WhatsApp и уточним сроки готовности.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Цена и кнопка */}
|
||||
<div className="mt-auto bg-white rounded-2xl p-4 shadow-soft">
|
||||
<div className="flex items-baseline justify-between mb-3">
|
||||
<div className="text-3xl font-display font-bold text-charcoal-700">
|
||||
{formatPrice(product.price)}
|
||||
</div>
|
||||
{product.unit && (
|
||||
<div className="text-sm text-charcoal-400">за {product.unit}</div>
|
||||
)}
|
||||
</div>
|
||||
<AddToCartButton product={product} section={sec} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
157
app/search/page.tsx
Normal file
157
app/search/page.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useEffect, Suspense } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { Search, ChevronLeft, X, MessageCircle } from 'lucide-react';
|
||||
import { searchProducts } from '@/lib/catalog';
|
||||
import { ProductCard } from '@/components/ProductCard';
|
||||
import { WHATSAPP_LINK } from '@/lib/config';
|
||||
|
||||
function SearchInner() {
|
||||
const params = useSearchParams();
|
||||
const router = useRouter();
|
||||
const initialQ = params.get('q') || '';
|
||||
const [q, setQ] = useState(initialQ);
|
||||
|
||||
// Обновляем URL без перезагрузки при наборе
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
const url = q ? `/search?q=${encodeURIComponent(q)}` : '/search';
|
||||
router.replace(url, { scroll: false });
|
||||
}, 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [q, router]);
|
||||
|
||||
const results = useMemo(() => searchProducts(q), [q]);
|
||||
const total = results.cafe.length + results.market.length;
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto px-4 py-6">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-1 text-sm text-charcoal-500 hover:text-charcoal-700 mb-4"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
На главную
|
||||
</Link>
|
||||
|
||||
<h1 className="font-display text-2xl md:text-3xl font-bold text-charcoal-700 mb-4">
|
||||
Поиск
|
||||
</h1>
|
||||
|
||||
{/* Поле поиска */}
|
||||
<div className="relative">
|
||||
<Search
|
||||
size={20}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 text-charcoal-400 pointer-events-none"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={q}
|
||||
onChange={e => setQ(e.target.value)}
|
||||
placeholder="Найти товар или блюдо... (например: сахар, донер)"
|
||||
className="w-full h-14 pl-12 pr-12 rounded-2xl border border-charcoal-100 bg-white text-base focus:outline-none focus:border-gold-400 focus:ring-2 focus:ring-gold-200"
|
||||
/>
|
||||
{q && (
|
||||
<button
|
||||
onClick={() => setQ('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 p-2 text-charcoal-400 hover:text-charcoal-700"
|
||||
aria-label="Очистить"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Подсказки */}
|
||||
{q.length < 2 && (
|
||||
<div className="mt-6">
|
||||
<div className="text-sm text-charcoal-400 mb-3">Популярные запросы:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['пицца', 'шашлык', 'донер', 'плов', 'хлеб', 'сахар', 'картофель', 'салат'].map(
|
||||
w => (
|
||||
<button
|
||||
key={w}
|
||||
onClick={() => setQ(w)}
|
||||
className="bg-white border border-charcoal-100 px-4 py-2 rounded-full text-sm hover:bg-sand"
|
||||
>
|
||||
{w}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Результаты */}
|
||||
{q.length >= 2 && (
|
||||
<div className="mt-6">
|
||||
{total === 0 ? (
|
||||
<div className="py-12 text-center">
|
||||
<div className="text-6xl mb-3">🔍</div>
|
||||
<h2 className="font-display text-xl text-charcoal-700 mb-1">
|
||||
Ничего не найдено
|
||||
</h2>
|
||||
<p className="text-sm text-charcoal-400 mb-5">
|
||||
По запросу «{q}» — ничего нет. Попробуйте по-другому.
|
||||
</p>
|
||||
<a
|
||||
href={`${WHATSAPP_LINK}?text=${encodeURIComponent(
|
||||
`Здравствуйте! Ищу: "${q}". Подскажите, есть ли у вас?`
|
||||
)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-primary inline-flex items-center gap-2"
|
||||
>
|
||||
<MessageCircle size={18} />
|
||||
Спросить в WhatsApp
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-sm text-charcoal-400 mb-4">
|
||||
Найдено: <strong>{total}</strong>
|
||||
</div>
|
||||
|
||||
{results.cafe.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h2 className="font-display text-lg font-bold text-charcoal-700 mb-3">
|
||||
🍕 Кафе ({results.cafe.length})
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{results.cafe.map(p => (
|
||||
<ProductCard key={p.id} product={p} section="cafe" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{results.market.length > 0 && (
|
||||
<div>
|
||||
<h2 className="font-display text-lg font-bold text-charcoal-700 mb-3">
|
||||
🛒 Минимаркет ({results.market.length})
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{results.market.map(p => (
|
||||
<ProductCard key={p.id} product={p} section="market" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SearchPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="p-8 text-center">Загрузка…</div>}>
|
||||
<SearchInner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
75
components/AddToCartButton.tsx
Normal file
75
components/AddToCartButton.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Plus, Minus, Check } from 'lucide-react';
|
||||
import type { Product, Section } from '@/lib/types';
|
||||
import { useCart } from '@/lib/cart-store';
|
||||
|
||||
export function AddToCartButton({
|
||||
product,
|
||||
section,
|
||||
size = 'lg'
|
||||
}: {
|
||||
product: Product;
|
||||
section: Section;
|
||||
size?: 'md' | 'lg';
|
||||
}) {
|
||||
const [qty, setQty] = useState(1);
|
||||
const [justAdded, setJustAdded] = useState(false);
|
||||
const addItem = useCart(s => s.addItem);
|
||||
|
||||
const handleAdd = () => {
|
||||
for (let i = 0; i < qty; i++) {
|
||||
addItem(product, section);
|
||||
}
|
||||
setJustAdded(true);
|
||||
setTimeout(() => setJustAdded(false), 1500);
|
||||
};
|
||||
|
||||
const btnSize = size === 'lg' ? 'h-14 text-base' : 'h-12 text-sm';
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Кол-во */}
|
||||
<div className="flex items-center gap-1 bg-white rounded-xl border border-charcoal-100 px-2 h-12">
|
||||
<button
|
||||
onClick={() => setQty(Math.max(1, qty - 1))}
|
||||
className="p-2 hover:bg-sand rounded-lg"
|
||||
aria-label="Уменьшить"
|
||||
>
|
||||
<Minus size={16} />
|
||||
</button>
|
||||
<span className="w-8 text-center font-bold">{qty}</span>
|
||||
<button
|
||||
onClick={() => setQty(qty + 1)}
|
||||
className="p-2 hover:bg-sand rounded-lg"
|
||||
aria-label="Увеличить"
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* В корзину */}
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
className={`flex-1 ${btnSize} font-medium rounded-xl shadow-soft transition-all flex items-center justify-center gap-2 ${
|
||||
justAdded
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gold-400 hover:bg-gold-500 text-white hover:shadow-gold'
|
||||
}`}
|
||||
>
|
||||
{justAdded ? (
|
||||
<>
|
||||
<Check size={20} />
|
||||
Добавлено!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus size={20} />
|
||||
В корзину
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
components/Badge.tsx
Normal file
55
components/Badge.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import type { ProductTag } from '@/lib/types';
|
||||
|
||||
const TAG_CONFIG: Record<ProductTag, { label: string; icon: string; className: string }> = {
|
||||
hit: {
|
||||
label: 'Хит',
|
||||
icon: '🏆',
|
||||
className: 'bg-gold-100 text-gold-700 border-gold-300'
|
||||
},
|
||||
spicy: {
|
||||
label: 'Острое',
|
||||
icon: '🌶️',
|
||||
className: 'bg-red-50 text-red-700 border-red-200'
|
||||
},
|
||||
preorder: {
|
||||
label: 'Предзаказ',
|
||||
icon: '📅',
|
||||
className: 'bg-blue-50 text-blue-700 border-blue-200'
|
||||
},
|
||||
signature: {
|
||||
label: 'Фирменное',
|
||||
icon: '⭐',
|
||||
className: 'bg-charcoal-600 text-gold-200 border-charcoal-700'
|
||||
},
|
||||
new: {
|
||||
label: 'Новинка',
|
||||
icon: '🆕',
|
||||
className: 'bg-green-50 text-green-700 border-green-200'
|
||||
},
|
||||
sale: {
|
||||
label: 'Акция',
|
||||
icon: '🔥',
|
||||
className: 'bg-orange-50 text-orange-700 border-orange-200'
|
||||
},
|
||||
vegan: {
|
||||
label: 'Веган',
|
||||
icon: '🌱',
|
||||
className: 'bg-green-50 text-green-700 border-green-200'
|
||||
}
|
||||
};
|
||||
|
||||
export function Badge({ tag, size = 'sm' }: { tag: ProductTag; size?: 'sm' | 'md' }) {
|
||||
const cfg = TAG_CONFIG[tag];
|
||||
if (!cfg) return null;
|
||||
|
||||
const sizeClass = size === 'sm' ? 'text-[10px] px-1.5 py-0.5' : 'text-xs px-2 py-1';
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 rounded-full border font-medium ${cfg.className} ${sizeClass}`}
|
||||
>
|
||||
<span>{cfg.icon}</span>
|
||||
<span>{cfg.label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
50
components/CartBar.tsx
Normal file
50
components/CartBar.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ShoppingBag } from 'lucide-react';
|
||||
import { useCart } from '@/lib/cart-store';
|
||||
import { formatPrice } from '@/lib/catalog';
|
||||
|
||||
/**
|
||||
* Плавающая полоса корзины внизу экрана.
|
||||
* Показывается, когда в корзине есть товары, и НЕ на страницах корзины/оформления.
|
||||
*/
|
||||
export function CartBar() {
|
||||
const pathname = usePathname();
|
||||
const items = useCart(s => s.items);
|
||||
const count = useCart(s => s.getCount());
|
||||
const total = useCart(s => s.getTotal());
|
||||
|
||||
const hideOn = ['/cart', '/checkout'];
|
||||
if (items.length === 0) return null;
|
||||
if (hideOn.some(p => pathname?.startsWith(p))) return null;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href="/cart"
|
||||
className="fixed bottom-4 left-4 right-4 md:left-auto md:right-6 md:bottom-6 md:w-80 z-20 bg-gold-400 hover:bg-gold-500 text-charcoal-700 rounded-2xl shadow-card px-4 py-3 flex items-center justify-between gap-3 transition-all hover:scale-[1.02]"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-white rounded-full p-2">
|
||||
<ShoppingBag size={18} />
|
||||
</div>
|
||||
<div className="leading-tight">
|
||||
<div className="text-xs font-medium opacity-80">
|
||||
{count} {plural(count, ['товар', 'товара', 'товаров'])}
|
||||
</div>
|
||||
<div className="font-bold">{formatPrice(total)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-medium text-sm">Оформить →</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function plural(n: number, forms: [string, string, string]): string {
|
||||
const mod10 = n % 10;
|
||||
const mod100 = n % 100;
|
||||
if (mod10 === 1 && mod100 !== 11) return forms[0];
|
||||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return forms[1];
|
||||
return forms[2];
|
||||
}
|
||||
129
components/CatalogView.tsx
Normal file
129
components/CatalogView.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { CatalogData, Section } from '@/lib/types';
|
||||
import { ProductCard } from './ProductCard';
|
||||
import { CategoryNav } from './CategoryNav';
|
||||
|
||||
interface CatalogViewProps {
|
||||
section: Section;
|
||||
data: CatalogData;
|
||||
title: string;
|
||||
description: string;
|
||||
emoji: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Универсальный экран каталога. Используется и для Кафе, и для Минимаркета.
|
||||
* Категории — сверху, товары — сеткой ниже.
|
||||
*/
|
||||
export function CatalogView({ section, data, title, description, emoji }: CatalogViewProps) {
|
||||
const [activeCategory, setActiveCategory] = useState<string>('all');
|
||||
|
||||
const available = useMemo(
|
||||
() => data.products.filter(p => p.available),
|
||||
[data]
|
||||
);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (activeCategory === 'all') return available;
|
||||
return available.filter(p => p.category === activeCategory);
|
||||
}, [available, activeCategory]);
|
||||
|
||||
// Группируем по категориям при просмотре "Все"
|
||||
const grouped = useMemo(() => {
|
||||
if (activeCategory !== 'all') return null;
|
||||
const groups: Record<string, typeof filtered> = {};
|
||||
for (const p of filtered) {
|
||||
if (!groups[p.category]) groups[p.category] = [];
|
||||
groups[p.category].push(p);
|
||||
}
|
||||
return groups;
|
||||
}, [filtered, activeCategory]);
|
||||
|
||||
const activeCategoryObj = data.categories.find(c => c.id === activeCategory);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Хедер раздела */}
|
||||
<section
|
||||
className={`py-8 md:py-12 ${
|
||||
section === 'cafe'
|
||||
? 'bg-gradient-to-br from-charcoal-600 to-charcoal-700 text-cream'
|
||||
: 'bg-gradient-to-br from-gold-400 to-gold-500 text-charcoal-700'
|
||||
}`}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="text-5xl mb-2">{emoji}</div>
|
||||
<h1 className="font-display text-3xl md:text-4xl font-bold">{title}</h1>
|
||||
<p className="opacity-80 mt-2 max-w-xl">{description}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Навигация по категориям */}
|
||||
<CategoryNav
|
||||
categories={data.categories}
|
||||
activeId={activeCategory}
|
||||
onChange={setActiveCategory}
|
||||
/>
|
||||
|
||||
{/* Контент */}
|
||||
<section className="py-6">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="py-16 text-center">
|
||||
<div className="text-5xl mb-3">📦</div>
|
||||
<h2 className="font-display text-lg text-charcoal-600">
|
||||
Здесь пока пусто
|
||||
</h2>
|
||||
<p className="text-sm text-charcoal-400 mt-1">
|
||||
Товары появятся скоро. Загляните позже!
|
||||
</p>
|
||||
</div>
|
||||
) : grouped ? (
|
||||
// Режим "Все": показываем по категориям
|
||||
<div className="space-y-10">
|
||||
{[...data.categories]
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.filter(cat => grouped[cat.id]?.length)
|
||||
.map(cat => (
|
||||
<div key={cat.id}>
|
||||
<h2 className="font-display text-xl md:text-2xl font-bold text-charcoal-700 mb-4 flex items-center gap-2">
|
||||
<span>{cat.icon}</span>
|
||||
<span>{cat.name}</span>
|
||||
<span className="text-sm text-charcoal-400 font-normal">
|
||||
({grouped[cat.id].length})
|
||||
</span>
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 md:gap-4">
|
||||
{grouped[cat.id].map(p => (
|
||||
<ProductCard key={p.id} product={p} section={section} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// Режим конкретной категории
|
||||
<>
|
||||
{activeCategoryObj && (
|
||||
<h2 className="font-display text-xl md:text-2xl font-bold text-charcoal-700 mb-4 flex items-center gap-2">
|
||||
<span>{activeCategoryObj.icon}</span>
|
||||
<span>{activeCategoryObj.name}</span>
|
||||
<span className="text-sm text-charcoal-400 font-normal">
|
||||
({filtered.length})
|
||||
</span>
|
||||
</h2>
|
||||
)}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 md:gap-4">
|
||||
{filtered.map(p => (
|
||||
<ProductCard key={p.id} product={p} section={section} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
50
components/CategoryNav.tsx
Normal file
50
components/CategoryNav.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import type { Category } from '@/lib/types';
|
||||
|
||||
interface CategoryNavProps {
|
||||
categories: Category[];
|
||||
activeId: string;
|
||||
onChange: (id: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Горизонтальная навигация по категориям.
|
||||
* Скроллится на телефоне, помещается в ряд на десктопе.
|
||||
*/
|
||||
export function CategoryNav({ categories, activeId, onChange }: CategoryNavProps) {
|
||||
return (
|
||||
<div className="sticky top-[60px] z-20 bg-cream/95 backdrop-blur-sm border-b border-charcoal-100">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="flex gap-2 overflow-x-auto scrollbar-hide py-3">
|
||||
<button
|
||||
onClick={() => onChange('all')}
|
||||
className={`shrink-0 px-4 py-2 rounded-full text-sm font-medium transition-colors whitespace-nowrap ${
|
||||
activeId === 'all'
|
||||
? 'bg-charcoal-600 text-cream'
|
||||
: 'bg-white text-charcoal-600 hover:bg-sand'
|
||||
}`}
|
||||
>
|
||||
Все
|
||||
</button>
|
||||
{[...categories]
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map(cat => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => onChange(cat.id)}
|
||||
className={`shrink-0 px-4 py-2 rounded-full text-sm font-medium transition-colors whitespace-nowrap ${
|
||||
activeId === cat.id
|
||||
? 'bg-charcoal-600 text-cream'
|
||||
: 'bg-white text-charcoal-600 hover:bg-sand'
|
||||
}`}
|
||||
>
|
||||
<span className="mr-1">{cat.icon}</span>
|
||||
{cat.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
87
components/Footer.tsx
Normal file
87
components/Footer.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import { Phone, MapPin, Clock, MessageCircle } from 'lucide-react';
|
||||
import { BUSINESS, WHATSAPP_LINK } from '@/lib/config';
|
||||
import { Logo } from './Logo';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="bg-charcoal-700 text-cream mt-16">
|
||||
<div className="max-w-7xl mx-auto px-4 py-10">
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{/* Бренд */}
|
||||
<div>
|
||||
<Logo variant="light" size="lg" />
|
||||
<p className="mt-4 text-sm text-charcoal-200 max-w-xs">
|
||||
Кафе и минимаркет в посёлке Нура. Доставка еды и продуктов
|
||||
по селу с заказом за 2 минуты.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Контакты */}
|
||||
<div>
|
||||
<h3 className="text-gold-300 font-display text-lg mb-4 tracking-wide">
|
||||
Контакты
|
||||
</h3>
|
||||
<ul className="space-y-3 text-sm">
|
||||
<li className="flex items-start gap-2">
|
||||
<MapPin size={16} className="text-gold-400 mt-0.5 shrink-0" />
|
||||
<span>
|
||||
{BUSINESS.address}<br />
|
||||
{BUSINESS.city}, {BUSINESS.region}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Phone size={16} className="text-gold-400 mt-0.5 shrink-0" />
|
||||
<a href={`tel:${BUSINESS.ordersPhone}`} className="hover:text-gold-300">
|
||||
{BUSINESS.ordersPhone}
|
||||
<span className="block text-xs text-charcoal-300">для заказов</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Phone size={16} className="text-gold-400 mt-0.5 shrink-0" />
|
||||
<a href={`tel:${BUSINESS.servicePhone}`} className="hover:text-gold-300">
|
||||
{BUSINESS.servicePhone}
|
||||
<span className="block text-xs text-charcoal-300">минимаркет (служебный)</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Время работы */}
|
||||
<div>
|
||||
<h3 className="text-gold-300 font-display text-lg mb-4 tracking-wide">
|
||||
Время работы
|
||||
</h3>
|
||||
<ul className="space-y-3 text-sm">
|
||||
<li className="flex items-start gap-2">
|
||||
<Clock size={16} className="text-gold-400 mt-0.5 shrink-0" />
|
||||
<span>
|
||||
<strong>🍕 Кафе:</strong> {BUSINESS.cafeHours}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Clock size={16} className="text-gold-400 mt-0.5 shrink-0" />
|
||||
<span>
|
||||
<strong>🛒 Минимаркет:</strong> {BUSINESS.marketHours}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<a
|
||||
href={WHATSAPP_LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-6 inline-flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white font-medium px-5 py-3 rounded-xl transition-colors"
|
||||
>
|
||||
<MessageCircle size={18} />
|
||||
Написать в WhatsApp
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 pt-6 border-t border-charcoal-500 text-center text-xs text-charcoal-300">
|
||||
© {new Date().getFullYear()} {BUSINESS.name}. Все права защищены.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
91
components/Header.tsx
Normal file
91
components/Header.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { ShoppingBag, Search, Menu, X } from 'lucide-react';
|
||||
import { Logo } from './Logo';
|
||||
import { useCart } from '@/lib/cart-store';
|
||||
|
||||
export function Header() {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const count = useCart(s => s.getCount());
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-40 bg-charcoal-600 text-cream shadow-soft">
|
||||
<div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between gap-4">
|
||||
<Logo variant="light" size="md" />
|
||||
|
||||
{/* Десктоп: меню */}
|
||||
<nav className="hidden md:flex items-center gap-6 text-sm font-medium">
|
||||
<Link href="/cafe" className="hover:text-gold-300 transition-colors">
|
||||
🍕 Кафе
|
||||
</Link>
|
||||
<Link href="/market" className="hover:text-gold-300 transition-colors">
|
||||
🛒 Минимаркет
|
||||
</Link>
|
||||
<Link href="/about" className="hover:text-gold-300 transition-colors">
|
||||
О нас
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href="/search"
|
||||
aria-label="Поиск"
|
||||
className="p-2 hover:bg-charcoal-500 rounded-lg transition-colors"
|
||||
>
|
||||
<Search size={20} />
|
||||
</Link>
|
||||
<Link
|
||||
href="/cart"
|
||||
aria-label="Корзина"
|
||||
className="relative p-2 hover:bg-charcoal-500 rounded-lg transition-colors"
|
||||
>
|
||||
<ShoppingBag size={20} />
|
||||
{count > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-gold-400 text-charcoal-700 text-[10px] font-bold rounded-full min-w-[18px] h-[18px] flex items-center justify-center px-1">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
aria-label="Меню"
|
||||
className="md:hidden p-2 hover:bg-charcoal-500 rounded-lg transition-colors"
|
||||
>
|
||||
{menuOpen ? <X size={20} /> : <Menu size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Мобильное меню */}
|
||||
{menuOpen && (
|
||||
<nav className="md:hidden border-t border-charcoal-500 bg-charcoal-600">
|
||||
<div className="max-w-7xl mx-auto px-4 py-2 flex flex-col">
|
||||
<Link
|
||||
href="/cafe"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="py-3 hover:text-gold-300 border-b border-charcoal-500"
|
||||
>
|
||||
🍕 Кафе
|
||||
</Link>
|
||||
<Link
|
||||
href="/market"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="py-3 hover:text-gold-300 border-b border-charcoal-500"
|
||||
>
|
||||
🛒 Минимаркет
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="py-3 hover:text-gold-300"
|
||||
>
|
||||
О нас
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
75
components/Logo.tsx
Normal file
75
components/Logo.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import Link from 'next/link';
|
||||
import { BUSINESS } from '@/lib/config';
|
||||
|
||||
interface LogoProps {
|
||||
variant?: 'light' | 'dark';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
showText?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Логотип AQ JAIYQ.
|
||||
* Стилизованная версия — золотой полумесяц со стрелой (как на исходном логотипе).
|
||||
* Можно потом заменить на загруженное изображение через /public/logo.png.
|
||||
*/
|
||||
export function Logo({ variant = 'dark', size = 'md', showText = true }: LogoProps) {
|
||||
const sizes = {
|
||||
sm: { circle: 36, text: 'text-base' },
|
||||
md: { circle: 44, text: 'text-lg' },
|
||||
lg: { circle: 64, text: 'text-2xl' }
|
||||
};
|
||||
|
||||
const s = sizes[size];
|
||||
const textColor = variant === 'light' ? 'text-cream' : 'text-charcoal-600';
|
||||
|
||||
return (
|
||||
<Link href="/" className="flex items-center gap-3 group">
|
||||
<div
|
||||
className="rounded-full bg-charcoal-600 flex items-center justify-center shadow-soft group-hover:scale-105 transition-transform"
|
||||
style={{ width: s.circle, height: s.circle }}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
className="w-3/4 h-3/4"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="goldGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#D7BB78" />
|
||||
<stop offset="100%" stopColor="#9A7B54" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* Полумесяц */}
|
||||
<path
|
||||
d="M 50 15 A 35 35 0 1 0 50 85 A 27 35 0 1 1 50 15 Z"
|
||||
fill="url(#goldGrad)"
|
||||
/>
|
||||
{/* Стрела */}
|
||||
<path
|
||||
d="M 30 50 L 80 50 L 75 45 M 80 50 L 75 55"
|
||||
stroke="url(#goldGrad)"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{showText && (
|
||||
<div className="flex flex-col leading-none">
|
||||
<span className={`font-display font-bold tracking-wider ${s.text} ${textColor}`}>
|
||||
AQ JAIYQ
|
||||
</span>
|
||||
<span
|
||||
className={`text-[10px] tracking-[0.2em] ${
|
||||
variant === 'light' ? 'text-gold-200' : 'text-gold-500'
|
||||
}`}
|
||||
>
|
||||
АҚ ЖАЙЫҚ
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
41
components/PopularRow.tsx
Normal file
41
components/PopularRow.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import type { Product, Section } from '@/lib/types';
|
||||
import { ProductCard } from './ProductCard';
|
||||
|
||||
interface PopularRowProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
products: Product[];
|
||||
section: Section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Лента популярных товаров/блюд.
|
||||
* На мобильных — горизонтальный скролл, на десктопе — сетка.
|
||||
*/
|
||||
export function PopularRow({ title, subtitle, products, section }: PopularRowProps) {
|
||||
if (products.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="py-8">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="flex items-end justify-between mb-5">
|
||||
<div>
|
||||
<h2 className="font-display text-2xl md:text-3xl font-bold text-charcoal-700">
|
||||
{title}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p className="text-sm text-charcoal-400 mt-1">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Мобильная горизонтальная лента / десктопная сетка */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4">
|
||||
{products.map(p => (
|
||||
<ProductCard key={p.id} product={p} section={section} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
88
components/ProductCard.tsx
Normal file
88
components/ProductCard.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Plus, Check } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { Product, Section } from '@/lib/types';
|
||||
import { formatPrice } from '@/lib/catalog';
|
||||
import { useCart } from '@/lib/cart-store';
|
||||
import { Badge } from './Badge';
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
section: Section;
|
||||
}
|
||||
|
||||
export function ProductCard({ product, section }: ProductCardProps) {
|
||||
const addItem = useCart(s => s.addItem);
|
||||
const [justAdded, setJustAdded] = useState(false);
|
||||
|
||||
const handleAdd = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
addItem(product, section);
|
||||
setJustAdded(true);
|
||||
setTimeout(() => setJustAdded(false), 1200);
|
||||
};
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/product/${section}/${product.id}`}
|
||||
className="card group flex flex-col"
|
||||
>
|
||||
{/* Фото */}
|
||||
<div className="relative aspect-square bg-sand overflow-hidden">
|
||||
{product.image ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gold-300 text-5xl">
|
||||
{section === 'cafe' ? '🍽️' : '🛒'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Метки в углу */}
|
||||
{product.tags.length > 0 && (
|
||||
<div className="absolute top-2 left-2 flex flex-col gap-1">
|
||||
{product.tags.slice(0, 2).map(t => (
|
||||
<Badge key={t} tag={t} size="sm" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Контент */}
|
||||
<div className="p-3 flex flex-col flex-1 gap-2">
|
||||
<h3 className="font-medium text-sm leading-snug line-clamp-2 text-charcoal-700">
|
||||
{product.name}
|
||||
</h3>
|
||||
|
||||
{product.unit && (
|
||||
<div className="text-xs text-charcoal-400">{product.unit}</div>
|
||||
)}
|
||||
|
||||
<div className="mt-auto flex items-center justify-between gap-2 pt-2">
|
||||
<div className="font-bold text-charcoal-700">
|
||||
{formatPrice(product.price)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
aria-label="Добавить в корзину"
|
||||
className={`rounded-full p-2 shadow-soft transition-all ${
|
||||
justAdded
|
||||
? 'bg-green-500 text-white scale-110'
|
||||
: 'bg-gold-400 hover:bg-gold-500 text-white hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
{justAdded ? <Check size={18} /> : <Plus size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
42
components/SectionTile.tsx
Normal file
42
components/SectionTile.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import Link from 'next/link';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
interface SectionTileProps {
|
||||
href: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
emoji: string;
|
||||
variant: 'cafe' | 'market';
|
||||
}
|
||||
|
||||
export function SectionTile({ href, title, subtitle, emoji, variant }: SectionTileProps) {
|
||||
const styles =
|
||||
variant === 'cafe'
|
||||
? 'from-charcoal-600 to-charcoal-700 text-cream'
|
||||
: 'from-gold-400 to-gold-500 text-charcoal-700';
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={`relative group bg-gradient-to-br ${styles} rounded-3xl p-6 md:p-8 shadow-card overflow-hidden hover:scale-[1.02] transition-transform`}
|
||||
>
|
||||
{/* Декоративный круг */}
|
||||
<div className="absolute -right-12 -top-12 w-48 h-48 rounded-full bg-white/5 group-hover:scale-110 transition-transform" />
|
||||
<div className="absolute -right-6 -bottom-6 w-32 h-32 rounded-full bg-white/5" />
|
||||
|
||||
<div className="relative">
|
||||
<div className="text-5xl md:text-6xl mb-4">{emoji}</div>
|
||||
<h2 className="font-display text-2xl md:text-3xl font-bold mb-2">{title}</h2>
|
||||
<p className="text-sm md:text-base opacity-80 mb-6">{subtitle}</p>
|
||||
<div
|
||||
className={`inline-flex items-center gap-2 font-medium text-sm ${
|
||||
variant === 'cafe' ? 'text-gold-300' : 'text-charcoal-700'
|
||||
}`}
|
||||
>
|
||||
Перейти к меню
|
||||
<ArrowRight size={16} className="group-hover:translate-x-1 transition-transform" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
27
components/WhatsAppFloat.tsx
Normal file
27
components/WhatsAppFloat.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { MessageCircle } from 'lucide-react';
|
||||
import { WHATSAPP_LINK } from '@/lib/config';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
/**
|
||||
* Плавающая кнопка WhatsApp, видна на всех страницах кроме корзины и оформления.
|
||||
* (На странице оформления уже есть собственная WhatsApp-кнопка для отправки заказа.)
|
||||
*/
|
||||
export function WhatsAppFloat() {
|
||||
const pathname = usePathname();
|
||||
const hideOn = ['/cart', '/checkout'];
|
||||
if (hideOn.some(p => pathname?.startsWith(p))) return null;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={WHATSAPP_LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Написать в WhatsApp"
|
||||
className="fixed bottom-24 md:bottom-6 right-4 z-30 bg-green-500 hover:bg-green-600 text-white rounded-full p-4 shadow-card hover:scale-105 transition-all"
|
||||
>
|
||||
<MessageCircle size={26} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
149
data/cafe.json
Normal file
149
data/cafe.json
Normal file
@ -0,0 +1,149 @@
|
||||
{
|
||||
"categories": [
|
||||
{ "id": "breakfast", "name": "Завтраки", "icon": "🌅", "order": 1 },
|
||||
{ "id": "soups", "name": "Первые блюда", "icon": "🍲", "order": 2 },
|
||||
{ "id": "main", "name": "Вторые блюда", "icon": "🍖", "order": 3 },
|
||||
{ "id": "salads", "name": "Салаты", "icon": "🥗", "order": 4 },
|
||||
{ "id": "shashlik", "name": "Шашлык", "icon": "🍢", "order": 5 },
|
||||
{ "id": "steaks", "name": "Стейки", "icon": "🥩", "order": 6 },
|
||||
{ "id": "cold_snacks", "name": "Холодные закуски", "icon": "❄️", "order": 7 },
|
||||
{ "id": "hot_snacks", "name": "Горячие закуски", "icon": "🔥", "order": 8 },
|
||||
{ "id": "pasta", "name": "Паста", "icon": "🍝", "order": 9 },
|
||||
{ "id": "pizza", "name": "Пицца", "icon": "🍕", "order": 10 },
|
||||
{ "id": "doner", "name": "Донеры", "icon": "🌯", "order": 11 },
|
||||
{ "id": "sides", "name": "Гарниры", "icon": "🍚", "order": 12 },
|
||||
{ "id": "sauces", "name": "Соусы", "icon": "🥫", "order": 13 },
|
||||
{ "id": "company", "name": "Блюда на компанию", "icon": "🎉", "order": 14 },
|
||||
{ "id": "sets", "name": "Сеты на компанию", "icon": "🎁", "order": 15 }
|
||||
],
|
||||
"products": [
|
||||
{ "id": "cf-001", "category": "breakfast", "name": "Блины", "price": 450, "unit": "2 шт", "available": true, "tags": [] },
|
||||
{ "id": "cf-002", "category": "breakfast", "name": "Сырники", "price": 800, "unit": "2 шт", "available": true, "tags": [] },
|
||||
{ "id": "cf-003", "category": "breakfast", "name": "Завтрак классический", "price": 1000, "ingredients": "глазунья, сосиски, обжаренный хлеб", "available": true, "tags": [] },
|
||||
{ "id": "cf-004", "category": "breakfast", "name": "Мини-чебуреки", "price": 800, "unit": "6 шт", "available": true, "tags": [] },
|
||||
{ "id": "cf-005", "category": "breakfast", "name": "Мини-самса", "price": 900, "unit": "6 шт", "available": true, "tags": [] },
|
||||
{ "id": "cf-006", "category": "breakfast", "name": "Омлет", "price": 600, "ingredients": "яйцо, молоко, зелень", "available": true, "tags": [] },
|
||||
{ "id": "cf-007", "category": "breakfast", "name": "Хлебная корзина", "price": 800, "available": true, "tags": [] },
|
||||
{ "id": "cf-008", "category": "breakfast", "name": "Хлеб белый", "price": 20, "available": true, "tags": [] },
|
||||
{ "id": "cf-009", "category": "breakfast", "name": "Хлеб чёрный", "price": 30, "available": true, "tags": [] },
|
||||
{ "id": "cf-010", "category": "breakfast", "name": "Лепёшка", "price": 350, "available": true, "tags": [] },
|
||||
{ "id": "cf-011", "category": "breakfast", "name": "Бауырсаки", "price": 600, "unit": "0,5 кг", "available": true, "tags": ["preorder"] },
|
||||
|
||||
{ "id": "cf-012", "category": "soups", "name": "Грибной суп-пюре", "price": 1200, "available": true, "tags": [] },
|
||||
{ "id": "cf-013", "category": "soups", "name": "Чечевичный суп", "price": 1200, "available": true, "tags": [] },
|
||||
{ "id": "cf-014", "category": "soups", "name": "Шурпа", "price": 1500, "ingredients": "баранина, морковь, картофель, болг. перец, лук, зелень", "available": true, "tags": [] },
|
||||
{ "id": "cf-015", "category": "soups", "name": "Борщ", "price": 1200, "available": true, "tags": [] },
|
||||
{ "id": "cf-016", "category": "soups", "name": "Том-ям", "price": 2500, "ingredients": "сёмга, креветки, шампиньоны, чеснок, имбирь, лимон, перец чили, рис", "available": true, "tags": ["spicy"] },
|
||||
{ "id": "cf-017", "category": "soups", "name": "Пельмени с бульоном", "price": 1000, "available": true, "tags": [] },
|
||||
{ "id": "cf-018", "category": "soups", "name": "Куриный суп-лапша", "price": 1200, "available": true, "tags": [] },
|
||||
{ "id": "cf-019", "category": "soups", "name": "Солянка", "price": 1500, "available": true, "tags": [] },
|
||||
|
||||
{ "id": "cf-020", "category": "main", "name": "Котлеты", "price": 600, "ingredients": "+ гарнир на выбор", "available": true, "tags": [] },
|
||||
{ "id": "cf-021", "category": "main", "name": "Плов", "price": 1200, "available": true, "tags": ["hit"] },
|
||||
{ "id": "cf-022", "category": "main", "name": "Манты с картофелем", "price": 1200, "ingredients": "фарш из говядины, картофель, лук, перец, соль", "available": true, "tags": ["preorder"] },
|
||||
{ "id": "cf-023", "category": "main", "name": "Требуха с овощами", "price": 1200, "available": true, "tags": [] },
|
||||
{ "id": "cf-024", "category": "main", "name": "Манты", "price": 1500, "ingredients": "фарш из говядины, лук, перец, соль", "available": true, "tags": [] },
|
||||
{ "id": "cf-025", "category": "main", "name": "Куырдак из баранины", "price": 1500, "available": true, "tags": ["hit"] },
|
||||
{ "id": "cf-026", "category": "main", "name": "Картошка с грибами", "price": 1800, "available": true, "tags": [] },
|
||||
{ "id": "cf-027", "category": "main", "name": "Гуйру лагман", "price": 2200, "ingredients": "говядина, болг. перец, пекин. капуста, лук, чеснок, морковь, лапша", "available": true, "tags": [] },
|
||||
{ "id": "cf-028", "category": "main", "name": "Джигяр", "price": 2200, "ingredients": "жареная печень, курдюк, овощи", "available": true, "tags": [] },
|
||||
{ "id": "cf-029", "category": "main", "name": "Бараньи рёбра", "price": 2300, "available": true, "tags": [] },
|
||||
{ "id": "cf-030", "category": "main", "name": "Филе судака с овощами", "price": 2300, "available": true, "tags": ["preorder"] },
|
||||
{ "id": "cf-031", "category": "main", "name": "Цомян лагман", "price": 2500, "ingredients": "говядина, болг. перец, пекин. капуста, чеснок, лук, морковь, лапша обжаренные в томате или на сое", "available": true, "tags": [] },
|
||||
{ "id": "cf-032", "category": "main", "name": "Мясо по-тайски", "price": 2500, "available": true, "tags": ["spicy"] },
|
||||
{ "id": "cf-033", "category": "main", "name": "Курица с грибами в сливочном соусе", "price": 2500, "ingredients": "+ гарнир на выбор", "available": true, "tags": [] },
|
||||
|
||||
{ "id": "cf-034", "category": "salads", "name": "Морковный", "price": 900, "ingredients": "морковь, лук, зелень", "available": true, "tags": [] },
|
||||
{ "id": "cf-035", "category": "salads", "name": "Свежий", "price": 1000, "ingredients": "огурец, помидоры, редис, лук, зелень", "available": true, "tags": [] },
|
||||
{ "id": "cf-036", "category": "salads", "name": "Ачучук", "price": 1200, "ingredients": "помидоры, лук, ост. перец", "available": true, "tags": [] },
|
||||
{ "id": "cf-037", "category": "salads", "name": "Под шубой", "price": 1500, "ingredients": "картофель, морковь, свёкла, сельдь, лук, яйца", "available": true, "tags": [] },
|
||||
{ "id": "cf-038", "category": "salads", "name": "Оливье", "price": 1500, "ingredients": "картофель, морковь, сол. огурцы, горох, колбаса, яйца", "available": true, "tags": [] },
|
||||
{ "id": "cf-039", "category": "salads", "name": "Винегрет", "price": 1500, "ingredients": "картофель, кваш. капуста, лук, морковь, горох, свёкла", "available": true, "tags": [] },
|
||||
{ "id": "cf-040", "category": "salads", "name": "Греческий", "price": 1800, "ingredients": "огурцы, помидоры, болг. перец, лук, фирм. соус, сыр фета", "available": true, "tags": [] },
|
||||
{ "id": "cf-041", "category": "salads", "name": "Тайский", "price": 2000, "ingredients": "огурцы, помидоры, болг. перец, говядина, соя, зелень", "available": true, "tags": [] },
|
||||
{ "id": "cf-042", "category": "salads", "name": "Нежность", "price": 2000, "ingredients": "гов. язык, кукуруза, огурцы, яйца, картофель пай", "available": true, "tags": [] },
|
||||
{ "id": "cf-043", "category": "salads", "name": "Малибу", "price": 2000, "ingredients": "филе курицы, огурцы, помидоры, сыр, сухарики", "available": true, "tags": [] },
|
||||
{ "id": "cf-044", "category": "salads", "name": "Тбилиси", "price": 2500, "ingredients": "говядина, конс. фасоль, грец. орехи, болг. перец, чеснок, лук", "available": true, "tags": [] },
|
||||
{ "id": "cf-045", "category": "salads", "name": "Баварский", "price": 2500, "ingredients": "картофель, яйцо, марин. огурцы, опята, шампиньоны, лук", "available": true, "tags": [] },
|
||||
{ "id": "cf-046", "category": "salads", "name": "Гнездо глухаря", "price": 2500, "ingredients": "филе курицы копч., огурец, яйцо, перепел. яйца, пай", "available": true, "tags": [] },
|
||||
{ "id": "cf-047", "category": "salads", "name": "Цезарь классический", "price": 2500, "ingredients": "капуста, фирменный соус, филе курицы, переп. яйца, сухари", "available": true, "tags": [] },
|
||||
{ "id": "cf-048", "category": "salads", "name": "Цезарь с креветками", "price": 2800, "ingredients": "капуста, фирменный соус, креветки, переп. яйца, сухари", "available": true, "tags": [] },
|
||||
|
||||
{ "id": "cf-049", "category": "shashlik", "name": "Шашлык из курицы", "price": 1500, "available": true, "tags": ["hit"] },
|
||||
{ "id": "cf-050", "category": "shashlik", "name": "Шашлык из филе утки", "price": 1500, "available": true, "tags": [] },
|
||||
{ "id": "cf-051", "category": "shashlik", "name": "Шашлык из баранины", "price": 2000, "available": true, "tags": ["hit"] },
|
||||
|
||||
{ "id": "cf-052", "category": "steaks", "name": "Стейк T-bone", "price": 2500, "available": true, "tags": [] },
|
||||
{ "id": "cf-053", "category": "steaks", "name": "Стейк сёмги", "price": 4500, "available": true, "tags": [] },
|
||||
|
||||
{ "id": "cf-054", "category": "cold_snacks", "name": "Кавказская нарезка", "price": 2500, "ingredients": "огурцы, помидоры, болг. перец, зелень, брынза", "available": true, "tags": [] },
|
||||
{ "id": "cf-055", "category": "cold_snacks", "name": "Закуска по-русски", "price": 2500, "ingredients": "сельдь, кваш. капуста, марин. огурцы, грибы, лук, картофель", "available": true, "tags": [] },
|
||||
{ "id": "cf-056", "category": "cold_snacks", "name": "Казахские деликатесы", "price": 5500, "ingredients": "қазы, шұжық, кесек ет", "available": true, "tags": [] },
|
||||
{ "id": "cf-057", "category": "cold_snacks", "name": "Рыбное плато", "price": 5500, "available": true, "tags": [] },
|
||||
|
||||
{ "id": "cf-058", "category": "hot_snacks", "name": "Куриные нагетсы", "price": 650, "unit": "6 шт", "available": true, "tags": [] },
|
||||
{ "id": "cf-059", "category": "hot_snacks", "name": "Чесночные гренки", "price": 800, "available": true, "tags": [] },
|
||||
{ "id": "cf-060", "category": "hot_snacks", "name": "Луковые кольца", "price": 900, "available": true, "tags": [] },
|
||||
{ "id": "cf-061", "category": "hot_snacks", "name": "Сырные палочки", "price": 1200, "unit": "6 шт", "available": true, "tags": [] },
|
||||
{ "id": "cf-062", "category": "hot_snacks", "name": "Баклажановые роллы", "price": 1500, "unit": "8 шт", "available": true, "tags": ["preorder"] },
|
||||
{ "id": "cf-063", "category": "hot_snacks", "name": "Кабачковые роллы", "price": 1500, "unit": "8 шт", "available": true, "tags": ["preorder"] },
|
||||
{ "id": "cf-064", "category": "hot_snacks", "name": "Хрустящие крылышки", "price": 1800, "available": true, "tags": [] },
|
||||
{ "id": "cf-065", "category": "hot_snacks", "name": "Креветки к пиву", "price": 2500, "available": true, "tags": [] },
|
||||
{ "id": "cf-066", "category": "hot_snacks", "name": "Спринг-роллы с курицей", "price": 2500, "available": true, "tags": ["preorder"] },
|
||||
{ "id": "cf-067", "category": "hot_snacks", "name": "Фирменная закуска «АҚ ЖАЙЫҚ»", "price": 2500, "ingredients": "фарш из брынзы и шпината в тесте, жаренные во фритюре", "available": true, "tags": ["signature", "preorder"] },
|
||||
{ "id": "cf-068", "category": "hot_snacks", "name": "Колбаски куриные", "price": 3000, "unit": "5 шт", "available": true, "tags": [] },
|
||||
{ "id": "cf-069", "category": "hot_snacks", "name": "Колбаски говяжьи", "price": 3500, "unit": "5 шт", "available": true, "tags": [] },
|
||||
{ "id": "cf-070", "category": "hot_snacks", "name": "Колбаски бараньи", "price": 3500, "unit": "5 шт", "available": true, "tags": [] },
|
||||
|
||||
{ "id": "cf-071", "category": "pasta", "name": "Паста Альфредо", "price": 2500, "ingredients": "паста феттучини, куриное филе, чеснок, сливки, тёртый пармезан, петрушка", "available": true, "tags": [] },
|
||||
{ "id": "cf-072", "category": "pasta", "name": "Паста Болоньезе", "price": 2500, "ingredients": "говяжий фарш, томат, специи, паста на выбор (феттучини/спагетти), пармезан", "available": true, "tags": [] },
|
||||
{ "id": "cf-073", "category": "pasta", "name": "Паста с креветками в сливочно-чесночном соусе", "price": 3000, "available": true, "tags": [] },
|
||||
{ "id": "cf-074", "category": "pasta", "name": "Том-ям паста", "price": 3000, "ingredients": "шампиньоны, креветки, черри, паста Том-ям, спагетти", "available": true, "tags": ["spicy"] },
|
||||
|
||||
{ "id": "cf-075", "category": "pizza", "name": "Маргарита", "price": 1800, "ingredients": "моцарелла, помидоры", "available": true, "tags": [] },
|
||||
{ "id": "cf-076", "category": "pizza", "name": "Сырная", "price": 2500, "ingredients": "моцарелла", "available": true, "tags": [] },
|
||||
{ "id": "cf-077", "category": "pizza", "name": "Пепперони", "price": 2500, "ingredients": "моцарелла, колбаса", "available": true, "tags": [] },
|
||||
{ "id": "cf-078", "category": "pizza", "name": "Охотничья", "price": 3000, "ingredients": "моцарелла, охотничьи колбаски", "available": true, "tags": [] },
|
||||
{ "id": "cf-079", "category": "pizza", "name": "Bon dior", "price": 3000, "ingredients": "моцарелла, колбаса, брокколи", "available": true, "tags": [] },
|
||||
{ "id": "cf-080", "category": "pizza", "name": "4 чезона", "price": 3000, "ingredients": "моцарелла, колбаса, грибы, курица, помидоры", "available": true, "tags": [] },
|
||||
{ "id": "cf-081", "category": "pizza", "name": "Pollo e funghi", "price": 3200, "ingredients": "курица с грибами, моцарелла", "available": true, "tags": [] },
|
||||
{ "id": "cf-082", "category": "pizza", "name": "Mexicano", "price": 3200, "ingredients": "моцарелла, курица, помидоры, кукуруза, маслины, халапенью", "available": true, "tags": ["spicy"] },
|
||||
{ "id": "cf-083", "category": "pizza", "name": "Мясная", "price": 3200, "ingredients": "моцарелла, говядина, копч. колбаса, курица, помидоры, маслины", "available": true, "tags": [] },
|
||||
{ "id": "cf-084", "category": "pizza", "name": "Болоньезе пицца", "price": 3200, "ingredients": "моцарелла, помидоры, фарш", "available": true, "tags": [] },
|
||||
{ "id": "cf-085", "category": "pizza", "name": "Сырный лосось", "price": 3500, "ingredients": "моцарелла, лосось, маслины, творожный сыр", "available": true, "tags": [] },
|
||||
|
||||
{ "id": "cf-086", "category": "doner", "name": "Mini Классика", "price": 1200, "available": true, "tags": [] },
|
||||
{ "id": "cf-087", "category": "doner", "name": "Mini Чесночный", "price": 1200, "available": true, "tags": [] },
|
||||
{ "id": "cf-088", "category": "doner", "name": "Mini Острый", "price": 1300, "available": true, "tags": ["spicy"] },
|
||||
{ "id": "cf-089", "category": "doner", "name": "Mini Сырный", "price": 1450, "available": true, "tags": [] },
|
||||
{ "id": "cf-090", "category": "doner", "name": "Mini Сырно-острый", "price": 1500, "available": true, "tags": ["spicy"] },
|
||||
{ "id": "cf-091", "category": "doner", "name": "Max Классика", "price": 1500, "available": true, "tags": ["hit"] },
|
||||
{ "id": "cf-092", "category": "doner", "name": "Max Чесночный", "price": 1500, "available": true, "tags": [] },
|
||||
{ "id": "cf-093", "category": "doner", "name": "Max Острый", "price": 1550, "available": true, "tags": ["spicy"] },
|
||||
{ "id": "cf-094", "category": "doner", "name": "Max Сырный", "price": 1650, "available": true, "tags": [] },
|
||||
{ "id": "cf-095", "category": "doner", "name": "Max Сырно-острый", "price": 1700, "available": true, "tags": ["spicy"] },
|
||||
|
||||
{ "id": "cf-096", "category": "sides", "name": "Рис", "price": 500, "available": true, "tags": [] },
|
||||
{ "id": "cf-097", "category": "sides", "name": "Картофель запечённый", "price": 600, "available": true, "tags": [] },
|
||||
{ "id": "cf-098", "category": "sides", "name": "Картофель фри", "price": 600, "available": true, "tags": [] },
|
||||
{ "id": "cf-099", "category": "sides", "name": "Овощи гриль", "price": 1000, "available": true, "tags": [] },
|
||||
|
||||
{ "id": "cf-100", "category": "sauces", "name": "Майонез", "price": 100, "available": true, "tags": [] },
|
||||
{ "id": "cf-101", "category": "sauces", "name": "Кетчуп", "price": 100, "available": true, "tags": [] },
|
||||
{ "id": "cf-102", "category": "sauces", "name": "Сметана", "price": 100, "available": true, "tags": [] },
|
||||
|
||||
{ "id": "cf-103", "category": "company", "name": "Манты на компанию", "price": 12000, "ingredients": "фарш из говядины, лук, специи. 10-12 человек", "available": true, "tags": [] },
|
||||
{ "id": "cf-104", "category": "company", "name": "Цыплёнок Табака", "price": 12000, "unit": "2 шт", "ingredients": "+ гарнир на выбор. 10-12 человек", "available": true, "tags": [] },
|
||||
{ "id": "cf-105", "category": "company", "name": "Курица Галантин", "price": 15000, "ingredients": "тушка курицы, начинённая марин. курин. бедром, + гарнир. 10-12 человек", "available": true, "tags": [] },
|
||||
{ "id": "cf-106", "category": "company", "name": "Шашлычки на шпажках", "price": 15000, "unit": "15 шт", "ingredients": "говядина, курица, баранина-люля. 10-12 человек", "available": true, "tags": [] },
|
||||
{ "id": "cf-107", "category": "company", "name": "Томлёные говяжьи рёбра в тесте", "price": 20000, "ingredients": "говядина, слоёное тесто, картофель. 10-12 человек", "available": true, "tags": [] },
|
||||
{ "id": "cf-108", "category": "company", "name": "Балғын Ет", "price": 20000, "ingredients": "томлёная конина, картофель. 10-12 человек", "available": true, "tags": [] },
|
||||
{ "id": "cf-109", "category": "company", "name": "Запечённая баранья лопатка", "price": 25000, "ingredients": "+ гарнир на выбор. 10-12 человек", "available": true, "tags": [] },
|
||||
{ "id": "cf-110", "category": "company", "name": "Казан-кебаб", "price": 30000, "ingredients": "обжаренный картофель, говядина, розочки из теста. 10-12 человек", "available": true, "tags": ["signature"] },
|
||||
{ "id": "cf-111", "category": "company", "name": "Сёмга запечённая с овощами", "price": 40000, "ingredients": "+ гарнир на выбор. 10-12 человек", "available": true, "tags": [] },
|
||||
|
||||
{ "id": "cf-112", "category": "sets", "name": "Пивной сет", "price": 5500, "ingredients": "креветки, чечел жарен., чипсы, чесночные гренки", "available": true, "tags": [] },
|
||||
{ "id": "cf-113", "category": "sets", "name": "Ассорти из колбасок", "price": 11000, "ingredients": "говядина, баранина, курица", "available": true, "tags": [] },
|
||||
{ "id": "cf-114", "category": "sets", "name": "Мясной сет", "price": 13000, "ingredients": "шашлык, баранина, говядина, курица", "available": true, "tags": [] }
|
||||
]
|
||||
}
|
||||
19
data/market.json
Normal file
19
data/market.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"categories": [
|
||||
{ "id": "bread", "name": "Хлеб", "icon": "🍞", "order": 1 },
|
||||
{ "id": "dairy", "name": "Молочные продукты", "icon": "🥛", "order": 2 },
|
||||
{ "id": "drinks", "name": "Напитки", "icon": "🥤", "order": 3 },
|
||||
{ "id": "vegetables", "name": "Овощи", "icon": "🥬", "order": 4 },
|
||||
{ "id": "fruits", "name": "Фрукты", "icon": "🍎", "order": 5 },
|
||||
{ "id": "grocery", "name": "Бакалея", "icon": "🧂", "order": 6 },
|
||||
{ "id": "frozen", "name": "Заморозка", "icon": "🧊", "order": 7 },
|
||||
{ "id": "sweets", "name": "Сладости", "icon": "🍫", "order": 8 },
|
||||
{ "id": "household", "name": "Бытовые товары", "icon": "🧼", "order": 9 }
|
||||
],
|
||||
"products": [
|
||||
{ "id": "mk-001", "category": "bread", "name": "Хлеб домашний", "price": 165, "unit": "шт", "available": true, "tags": ["hit"] },
|
||||
{ "id": "mk-002", "category": "bread", "name": "Хлеб на сыворотке", "price": 180, "unit": "шт", "available": true, "tags": [] },
|
||||
{ "id": "mk-003", "category": "grocery", "name": "Сахар", "price": 569, "unit": "кг", "available": true, "tags": [] },
|
||||
{ "id": "mk-004", "category": "vegetables", "name": "Картофель", "price": 319, "unit": "кг", "available": true, "tags": [] }
|
||||
]
|
||||
}
|
||||
88
lib/cart-store.ts
Normal file
88
lib/cart-store.ts
Normal file
@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { CartItem, Product, Section } from './types';
|
||||
|
||||
interface CartState {
|
||||
items: CartItem[];
|
||||
addItem: (product: Product, section: Section) => void;
|
||||
removeItem: (productId: string) => void;
|
||||
updateQuantity: (productId: string, quantity: number) => void;
|
||||
clearCart: () => void;
|
||||
// Геттеры
|
||||
getCount: () => number;
|
||||
getTotal: () => number;
|
||||
getCafeItems: () => CartItem[];
|
||||
getMarketItems: () => CartItem[];
|
||||
getCafeTotal: () => number;
|
||||
getMarketTotal: () => number;
|
||||
}
|
||||
|
||||
export const useCart = create<CartState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
items: [],
|
||||
|
||||
addItem: (product, section) => {
|
||||
const items = get().items;
|
||||
const existing = items.find(i => i.productId === product.id);
|
||||
|
||||
if (existing) {
|
||||
set({
|
||||
items: items.map(i =>
|
||||
i.productId === product.id
|
||||
? { ...i, quantity: i.quantity + 1 }
|
||||
: i
|
||||
)
|
||||
});
|
||||
} else {
|
||||
set({
|
||||
items: [
|
||||
...items,
|
||||
{
|
||||
productId: product.id,
|
||||
section,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
quantity: 1,
|
||||
unit: product.unit,
|
||||
image: product.image
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeItem: (productId) => {
|
||||
set({ items: get().items.filter(i => i.productId !== productId) });
|
||||
},
|
||||
|
||||
updateQuantity: (productId, quantity) => {
|
||||
if (quantity <= 0) {
|
||||
get().removeItem(productId);
|
||||
return;
|
||||
}
|
||||
set({
|
||||
items: get().items.map(i =>
|
||||
i.productId === productId ? { ...i, quantity } : i
|
||||
)
|
||||
});
|
||||
},
|
||||
|
||||
clearCart: () => set({ items: [] }),
|
||||
|
||||
getCount: () => get().items.reduce((sum, i) => sum + i.quantity, 0),
|
||||
getTotal: () => get().items.reduce((sum, i) => sum + i.quantity * i.price, 0),
|
||||
getCafeItems: () => get().items.filter(i => i.section === 'cafe'),
|
||||
getMarketItems: () => get().items.filter(i => i.section === 'market'),
|
||||
getCafeTotal: () =>
|
||||
get().getCafeItems().reduce((sum, i) => sum + i.quantity * i.price, 0),
|
||||
getMarketTotal: () =>
|
||||
get().getMarketItems().reduce((sum, i) => sum + i.quantity * i.price, 0)
|
||||
}),
|
||||
{
|
||||
name: 'aqjaiyq-cart'
|
||||
}
|
||||
)
|
||||
);
|
||||
109
lib/catalog.ts
Normal file
109
lib/catalog.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import cafeData from '@/data/cafe.json';
|
||||
import marketData from '@/data/market.json';
|
||||
import type { CatalogData, Product, Section, ProductTag } from './types';
|
||||
|
||||
const cafe = cafeData as CatalogData;
|
||||
const market = marketData as CatalogData;
|
||||
|
||||
export function getCatalog(section: Section): CatalogData {
|
||||
return section === 'cafe' ? cafe : market;
|
||||
}
|
||||
|
||||
export function getAllProducts(section: Section): Product[] {
|
||||
return getCatalog(section).products.filter(p => p.available);
|
||||
}
|
||||
|
||||
export function getProductById(section: Section, id: string): Product | undefined {
|
||||
return getCatalog(section).products.find(p => p.id === id);
|
||||
}
|
||||
|
||||
export function findProductAcrossSections(id: string): { product: Product; section: Section } | undefined {
|
||||
const inCafe = cafe.products.find(p => p.id === id);
|
||||
if (inCafe) return { product: inCafe, section: 'cafe' };
|
||||
const inMarket = market.products.find(p => p.id === id);
|
||||
if (inMarket) return { product: inMarket, section: 'market' };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getProductsByCategory(section: Section, categoryId: string): Product[] {
|
||||
return getAllProducts(section).filter(p => p.category === categoryId);
|
||||
}
|
||||
|
||||
export function getProductsByTag(section: Section, tag: ProductTag): Product[] {
|
||||
return getAllProducts(section).filter(p => p.tags.includes(tag));
|
||||
}
|
||||
|
||||
export function getCategoryName(section: Section, categoryId: string): string {
|
||||
const cat = getCatalog(section).categories.find(c => c.id === categoryId);
|
||||
return cat?.name || categoryId;
|
||||
}
|
||||
|
||||
export function getCategoryIcon(section: Section, categoryId: string): string {
|
||||
const cat = getCatalog(section).categories.find(c => c.id === categoryId);
|
||||
return cat?.icon || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск товаров по строке. Терпит регистр, ищет по имени и составу.
|
||||
* Возвращает результаты сгруппированные по разделу.
|
||||
*/
|
||||
export function searchProducts(query: string): {
|
||||
cafe: Product[];
|
||||
market: Product[];
|
||||
} {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (q.length < 2) return { cafe: [], market: [] };
|
||||
|
||||
const match = (p: Product) =>
|
||||
p.available && (
|
||||
p.name.toLowerCase().includes(q) ||
|
||||
(p.ingredients?.toLowerCase().includes(q) ?? false) ||
|
||||
(p.description?.toLowerCase().includes(q) ?? false)
|
||||
);
|
||||
|
||||
return {
|
||||
cafe: cafe.products.filter(match),
|
||||
market: market.products.filter(match)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Популярные блюда из кафе (с меткой "hit" или "signature").
|
||||
*/
|
||||
export function getPopularCafe(limit = 8): Product[] {
|
||||
const hits = getAllProducts('cafe').filter(
|
||||
p => p.tags.includes('hit') || p.tags.includes('signature')
|
||||
);
|
||||
return hits.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Популярные товары минимаркета.
|
||||
*/
|
||||
export function getPopularMarket(limit = 8): Product[] {
|
||||
const all = getAllProducts('market');
|
||||
const hits = all.filter(p => p.tags.includes('hit'));
|
||||
// Если хитов мало — добавим остальные товары
|
||||
if (hits.length < limit) {
|
||||
const rest = all.filter(p => !p.tags.includes('hit'));
|
||||
return [...hits, ...rest].slice(0, limit);
|
||||
}
|
||||
return hits.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Товары/блюда со скидкой (с меткой "sale").
|
||||
*/
|
||||
export function getSaleItems(limit = 6): Array<Product & { section: Section }> {
|
||||
const cafeItems = getAllProducts('cafe')
|
||||
.filter(p => p.tags.includes('sale'))
|
||||
.map(p => ({ ...p, section: 'cafe' as Section }));
|
||||
const marketItems = getAllProducts('market')
|
||||
.filter(p => p.tags.includes('sale'))
|
||||
.map(p => ({ ...p, section: 'market' as Section }));
|
||||
return [...cafeItems, ...marketItems].slice(0, limit);
|
||||
}
|
||||
|
||||
export function formatPrice(price: number): string {
|
||||
return `${price.toLocaleString('ru-RU')} ₸`;
|
||||
}
|
||||
38
lib/config.ts
Normal file
38
lib/config.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Конфигурация бизнеса AQ JAIYQ.
|
||||
* Все основные данные собраны в одном месте,
|
||||
* чтобы их легко было поменять без поиска по коду.
|
||||
*/
|
||||
|
||||
export const BUSINESS = {
|
||||
name: 'AQ JAIYQ',
|
||||
nameKz: 'АҚ ЖАЙЫҚ',
|
||||
tagline: 'Кафе и минимаркет в Нуре',
|
||||
|
||||
// Контакты
|
||||
ordersPhone: '+7 705 426 31 30', // Номер для заказов с сайта
|
||||
ordersPhoneRaw: '77054263130', // Тот же номер для WhatsApp API (без +)
|
||||
servicePhone: '+7 776 031 96 66', // Служебный телефон минимаркета
|
||||
servicePhoneRaw: '77760319666',
|
||||
|
||||
// Адрес
|
||||
address: 'ул. Ардагерлер 40',
|
||||
city: 'поселок Нура',
|
||||
region: 'Карагандинская область',
|
||||
country: 'Казахстан',
|
||||
|
||||
// Время работы
|
||||
cafeHours: '10:00 – 23:00',
|
||||
marketHours: 'Круглосуточно (24 часа)',
|
||||
|
||||
// Доставка
|
||||
deliveryFee: 500, // Стоимость доставки по селу
|
||||
freeDeliveryFrom: 5000, // Бесплатная доставка от X тенге
|
||||
minOrderAmount: 1000 // Минимальная сумма заказа
|
||||
} as const;
|
||||
|
||||
export const WHATSAPP_LINK = `https://wa.me/${BUSINESS.ordersPhoneRaw}`;
|
||||
|
||||
export function whatsappLink(text: string) {
|
||||
return `${WHATSAPP_LINK}?text=${encodeURIComponent(text)}`;
|
||||
}
|
||||
50
lib/types.ts
Normal file
50
lib/types.ts
Normal file
@ -0,0 +1,50 @@
|
||||
export type Section = 'cafe' | 'market';
|
||||
|
||||
export type ProductTag = 'hit' | 'spicy' | 'preorder' | 'signature' | 'new' | 'sale' | 'vegan';
|
||||
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
category: string;
|
||||
name: string;
|
||||
price: number;
|
||||
unit?: string;
|
||||
ingredients?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
available: boolean;
|
||||
tags: ProductTag[];
|
||||
}
|
||||
|
||||
export interface CatalogData {
|
||||
categories: Category[];
|
||||
products: Product[];
|
||||
}
|
||||
|
||||
export interface CartItem {
|
||||
productId: string;
|
||||
section: Section;
|
||||
name: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
unit?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface CheckoutForm {
|
||||
name: string;
|
||||
phone: string;
|
||||
deliveryType: 'delivery' | 'pickup';
|
||||
street: string;
|
||||
landmark: string;
|
||||
comment: string;
|
||||
payment: 'kaspi_qr' | 'kaspi_pay' | 'halyk' | 'transfer' | 'cash';
|
||||
whenDelivery: 'asap' | 'time';
|
||||
deliveryTime?: string;
|
||||
}
|
||||
13
next.config.js
Normal file
13
next.config.js
Normal file
@ -0,0 +1,13 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
images: {
|
||||
formats: ['image/webp'],
|
||||
remotePatterns: [
|
||||
{ protocol: 'https', hostname: 'images.unsplash.com' },
|
||||
{ protocol: 'https', hostname: 'res.cloudinary.com' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "aq-jaiyq",
|
||||
"version": "0.1.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",
|
||||
"zustand": "^4.5.4",
|
||||
"lucide-react": "^0.408.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
55
tailwind.config.ts
Normal file
55
tailwind.config.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}'
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Фирменные цвета AQ JAIYQ (по логотипу)
|
||||
gold: {
|
||||
DEFAULT: '#C8A876',
|
||||
50: '#FAF6EE',
|
||||
100: '#F3E9D2',
|
||||
200: '#E5D2A4',
|
||||
300: '#D7BB78',
|
||||
400: '#C8A876',
|
||||
500: '#B8956A',
|
||||
600: '#9A7B54',
|
||||
700: '#7B6243',
|
||||
800: '#5C4932',
|
||||
900: '#3D3022'
|
||||
},
|
||||
charcoal: {
|
||||
DEFAULT: '#3D3A3A',
|
||||
50: '#F5F4F4',
|
||||
100: '#E5E3E3',
|
||||
200: '#C7C3C3',
|
||||
300: '#9F9999',
|
||||
400: '#6F6A6A',
|
||||
500: '#4F4B4B',
|
||||
600: '#3D3A3A',
|
||||
700: '#2C2A2A',
|
||||
800: '#1E1D1D',
|
||||
900: '#121111'
|
||||
},
|
||||
cream: '#FAF8F4',
|
||||
sand: '#F5F0E8'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['system-ui', '-apple-system', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||||
display: ['Georgia', 'serif']
|
||||
},
|
||||
boxShadow: {
|
||||
soft: '0 2px 12px -2px rgba(61, 58, 58, 0.08)',
|
||||
card: '0 4px 20px -4px rgba(61, 58, 58, 0.12)',
|
||||
gold: '0 4px 16px -4px rgba(200, 168, 118, 0.4)'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
};
|
||||
|
||||
export default config;
|
||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"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": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user