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:
AQ JAIYQ 2026-05-22 13:45:40 +05:00
commit a236dcf4cf
40 changed files with 5104 additions and 0 deletions

36
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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, посёлок Нура, Карагандинская область

1272
TZ.md Normal file

File diff suppressed because it is too large Load Diff

87
app/about/page.tsx Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>
);
}

View 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
View 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>
);
}

View 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
View 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
View 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
View 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>
</>
);
}

View 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
View 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
View 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
View 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
View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

55
tailwind.config.ts Normal file
View 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
View 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"]
}