Fix Pages: build as IIFE with inlined CSS; strip type=module for Gitea MIME compatibility
This commit is contained in:
parent
94df4033ae
commit
9d4a9fd802
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
42
assets/index.js
Normal file
42
assets/index.js
Normal file
File diff suppressed because one or more lines are too long
@ -6,8 +6,7 @@
|
||||
<meta name="theme-color" content="#3D3A3A" />
|
||||
<title>AQ JAIYQ — кафе и минимаркет в Нуре</title>
|
||||
<meta name="description" content="AQ JAIYQ — доставка еды из кафе и продуктов из минимаркета в посёлке Нура. Быстрый заказ через WhatsApp." />
|
||||
<script type="module" crossorigin src="./assets/index-C3ZVCTBo.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BEYdkHi2.css">
|
||||
<script src="./assets/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
2719
package-lock.json
generated
Normal file
2719
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "aq-jaiyq",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1 --port 4321",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview --host 127.0.0.1 --port 4321"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.11"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
457
src/App.tsx
Normal file
457
src/App.tsx
Normal file
@ -0,0 +1,457 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCart, type CartItem } from "./cart";
|
||||
import { fmtTenge } from "./format";
|
||||
import {
|
||||
popularCafe,
|
||||
popularMarket,
|
||||
WHATSAPP_PHONE,
|
||||
PHONE_CAFE,
|
||||
PHONE_MARKET,
|
||||
ADDRESS,
|
||||
HOURS_CAFE,
|
||||
HOURS_MARKET,
|
||||
FULL_MENU_URL,
|
||||
type Product,
|
||||
} from "./products";
|
||||
|
||||
const TAG_LABEL: Record<string, string> = {
|
||||
hit: "Хит",
|
||||
signature: "Фирменное",
|
||||
preorder: "По предзаказу",
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const cart = useCart();
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header count={cart.count} onOpenCart={() => setDrawerOpen(true)} />
|
||||
<main className="flex-1">
|
||||
<Hero onOpenCart={() => setDrawerOpen(true)} />
|
||||
<Section
|
||||
id="cafe"
|
||||
title="Кафе"
|
||||
subtitle="Популярные позиции. Полное меню — на отдельной странице."
|
||||
items={popularCafe}
|
||||
onAdd={cart.add}
|
||||
fullMenuUrl={FULL_MENU_URL}
|
||||
/>
|
||||
<Section
|
||||
id="market"
|
||||
title="Минимаркет"
|
||||
subtitle="Хлеб, продукты первой необходимости. Круглосуточно."
|
||||
items={popularMarket}
|
||||
onAdd={cart.add}
|
||||
/>
|
||||
</main>
|
||||
<Footer />
|
||||
{cart.count > 0 && !drawerOpen && (
|
||||
<StickyCartFab
|
||||
count={cart.count}
|
||||
total={cart.total}
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
/>
|
||||
)}
|
||||
<CartDrawer
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
items={cart.items}
|
||||
total={cart.total}
|
||||
onAdd={cart.add}
|
||||
onDec={cart.dec}
|
||||
onRemove={cart.remove}
|
||||
onClear={cart.clear}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Header({ count, onOpenCart }: { count: number; onOpenCart: () => void }) {
|
||||
return (
|
||||
<header className="sticky top-0 z-30 bg-cream/90 backdrop-blur border-b border-charcoal-100">
|
||||
<div className="mx-auto max-w-5xl px-4 py-3 flex items-center justify-between gap-3">
|
||||
<a href="#top" className="font-display text-xl sm:text-2xl font-bold text-charcoal-700">
|
||||
АҚ ЖАЙЫҚ
|
||||
</a>
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href={`tel:${PHONE_CAFE.replace(/\s/g, "")}`}
|
||||
className="hidden sm:inline-flex btn-ghost !py-2 !px-3 text-sm"
|
||||
aria-label="Позвонить в кафе"
|
||||
>
|
||||
📞 {PHONE_CAFE}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenCart}
|
||||
className="btn-primary !py-2 !px-3 text-sm"
|
||||
aria-label={`Открыть корзину (${count})`}
|
||||
>
|
||||
🛒 Корзина{count > 0 ? ` · ${count}` : ""}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function Hero({ onOpenCart }: { onOpenCart: () => void }) {
|
||||
return (
|
||||
<section id="top" className="px-4 pt-10 pb-12 sm:pt-16 sm:pb-20">
|
||||
<div className="mx-auto max-w-5xl text-center">
|
||||
<p className="text-gold-500 uppercase tracking-widest text-xs sm:text-sm mb-3">
|
||||
Кафе и минимаркет · пос. Нура
|
||||
</p>
|
||||
<h1 className="font-display font-extrabold text-charcoal-700 text-4xl sm:text-6xl leading-tight">
|
||||
АҚ ЖАЙЫҚ
|
||||
</h1>
|
||||
<p className="mt-4 text-charcoal-500 text-base sm:text-lg max-w-xl mx-auto">
|
||||
Домашняя кухня, свежий хлеб и продукты под рукой. Соберите заказ и отправьте в WhatsApp одним кликом.
|
||||
</p>
|
||||
<div className="mt-7 flex flex-wrap items-center justify-center gap-3">
|
||||
<a href="#cafe" className="btn-primary">🍽️ Меню кафе</a>
|
||||
<a href="#market" className="btn-ghost">🛍️ Минимаркет</a>
|
||||
<button type="button" onClick={onOpenCart} className="btn-secondary">
|
||||
🛒 Открыть корзину
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({
|
||||
id,
|
||||
title,
|
||||
subtitle,
|
||||
items,
|
||||
onAdd,
|
||||
fullMenuUrl,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
items: Product[];
|
||||
onAdd: (p: Product) => void;
|
||||
fullMenuUrl?: string;
|
||||
}) {
|
||||
return (
|
||||
<section id={id} className="px-4 py-10 sm:py-14 border-t border-charcoal-100">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="flex items-end justify-between gap-3 mb-6">
|
||||
<div>
|
||||
<h2 className="font-display text-3xl sm:text-4xl font-bold text-charcoal-700">{title}</h2>
|
||||
{subtitle && <p className="text-charcoal-500 mt-1 text-sm sm:text-base">{subtitle}</p>}
|
||||
</div>
|
||||
{fullMenuUrl && (
|
||||
<a
|
||||
href={fullMenuUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gold-500 hover:text-gold-600 text-sm whitespace-nowrap"
|
||||
>
|
||||
Все позиции →
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{items.map((p) => (
|
||||
<ProductCard key={p.id} product={p} onAdd={() => onAdd(p)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ProductCard({ product, onAdd }: { product: Product; onAdd: () => void }) {
|
||||
return (
|
||||
<article className="bg-white rounded-2xl p-4 shadow-soft hover:shadow-card transition-shadow flex gap-3">
|
||||
<div className="text-4xl sm:text-5xl shrink-0" aria-hidden="true">
|
||||
{product.emoji}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 flex flex-col">
|
||||
<h3 className="font-medium text-charcoal-700 leading-snug">{product.name}</h3>
|
||||
{product.ingredients && (
|
||||
<p className="text-xs text-charcoal-400 mt-1 leading-snug">{product.ingredients}</p>
|
||||
)}
|
||||
{product.tags && product.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{product.tags.map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
className="text-[10px] uppercase tracking-wider bg-gold-100 text-gold-600 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
{TAG_LABEL[t] ?? t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 flex items-center justify-between gap-2">
|
||||
<div className="text-charcoal-700 font-semibold">
|
||||
{fmtTenge(product.price)}
|
||||
{product.unit && <span className="text-charcoal-400 text-xs ml-1">/ {product.unit}</span>}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
className="btn-primary !px-3 !py-2 text-sm"
|
||||
aria-label={`Добавить ${product.name}`}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function StickyCartFab({
|
||||
count,
|
||||
total,
|
||||
onClick,
|
||||
}: {
|
||||
count: number;
|
||||
total: number;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="sm:hidden fixed bottom-4 inset-x-4 z-20">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="btn-primary w-full shadow-card !py-4 text-base"
|
||||
>
|
||||
🛒 Корзина · {count} · {fmtTenge(total)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CartDrawer({
|
||||
open,
|
||||
onClose,
|
||||
items,
|
||||
total,
|
||||
onAdd,
|
||||
onDec,
|
||||
onRemove,
|
||||
onClear,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
items: CartItem[];
|
||||
total: number;
|
||||
onAdd: (p: Product) => void;
|
||||
onDec: (id: string) => void;
|
||||
onRemove: (id: string) => void;
|
||||
onClear: () => void;
|
||||
}) {
|
||||
const closeBtnRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
document.addEventListener("keydown", onKey);
|
||||
closeBtnRef.current?.focus();
|
||||
const prevOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKey);
|
||||
document.body.style.overflow = prevOverflow;
|
||||
};
|
||||
}, [open, onClose]);
|
||||
|
||||
const buildWhatsAppUrl = () => {
|
||||
const lines = items.map(
|
||||
(i) => `• ${i.product.name} × ${i.qty} — ${fmtTenge(i.product.price * i.qty)}`,
|
||||
);
|
||||
const text = [
|
||||
"Здравствуйте! Хочу заказать:",
|
||||
...lines,
|
||||
"",
|
||||
`Итого: ${fmtTenge(total)}`,
|
||||
"",
|
||||
"Имя:",
|
||||
"Адрес/самовывоз:",
|
||||
"Время:",
|
||||
].join("\n");
|
||||
return `https://wa.me/${WHATSAPP_PHONE}?text=${encodeURIComponent(text)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`fixed inset-0 z-40 bg-charcoal-700/40 transition-opacity ${
|
||||
open ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
|
||||
}`}
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<aside
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Корзина"
|
||||
className={`fixed top-0 right-0 z-50 h-full w-full sm:max-w-md bg-cream shadow-card flex flex-col transform transition-transform ${
|
||||
open ? "translate-x-0" : "translate-x-full"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-charcoal-100">
|
||||
<h2 className="font-display text-xl font-bold text-charcoal-700">Корзина</h2>
|
||||
<button
|
||||
ref={closeBtnRef}
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="btn-ghost !p-2 !px-3"
|
||||
aria-label="Закрыть корзину"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
{items.length === 0 ? (
|
||||
<p className="text-charcoal-400 text-center py-12">
|
||||
Корзина пуста. Добавьте позицию из меню.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
{items.map((i) => (
|
||||
<li key={i.product.id} className="bg-white rounded-xl p-3 flex gap-3 shadow-soft">
|
||||
<div className="text-3xl shrink-0" aria-hidden="true">
|
||||
{i.product.emoji}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="font-medium text-charcoal-700 leading-snug">{i.product.name}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemove(i.product.id)}
|
||||
className="text-charcoal-300 hover:text-charcoal-600 text-sm"
|
||||
aria-label={`Удалить ${i.product.name}`}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-charcoal-400 mt-0.5">
|
||||
{fmtTenge(i.product.price)}
|
||||
{i.product.unit && ` / ${i.product.unit}`}
|
||||
</p>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="inline-flex items-center gap-1 bg-charcoal-50 rounded-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDec(i.product.id)}
|
||||
className="px-3 py-1 text-charcoal-700 hover:bg-charcoal-100 rounded-l-lg"
|
||||
aria-label={`Уменьшить ${i.product.name}`}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span className="px-2 min-w-[2ch] text-center text-sm">{i.qty}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAdd(i.product)}
|
||||
className="px-3 py-1 text-charcoal-700 hover:bg-charcoal-100 rounded-r-lg"
|
||||
aria-label={`Увеличить ${i.product.name}`}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="font-semibold text-charcoal-700 text-sm">
|
||||
{fmtTenge(i.product.price * i.qty)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-charcoal-100 px-4 py-4 space-y-3 bg-cream">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-charcoal-500">Итого</span>
|
||||
<span className="font-display text-2xl font-bold text-charcoal-700">{fmtTenge(total)}</span>
|
||||
</div>
|
||||
<a
|
||||
href={items.length > 0 ? buildWhatsAppUrl() : undefined}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-disabled={items.length === 0}
|
||||
className={`btn-primary w-full !py-3 ${
|
||||
items.length === 0 ? "opacity-50 pointer-events-none" : ""
|
||||
}`}
|
||||
>
|
||||
💬 Заказать в WhatsApp{items.length > 0 ? ` · ${fmtTenge(total)}` : ""}
|
||||
</a>
|
||||
<a
|
||||
href={`tel:${PHONE_CAFE.replace(/\s/g, "")}`}
|
||||
className="btn-ghost w-full"
|
||||
>
|
||||
📞 Просто позвонить
|
||||
</a>
|
||||
{items.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClear}
|
||||
className="w-full text-charcoal-400 hover:text-charcoal-600 text-sm py-2"
|
||||
>
|
||||
Очистить корзину
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<footer className="bg-charcoal-700 text-cream px-4 py-10 mt-10">
|
||||
<div className="mx-auto max-w-5xl grid gap-6 sm:grid-cols-3">
|
||||
<div>
|
||||
<h3 className="font-display text-xl font-bold mb-2">АҚ ЖАЙЫҚ</h3>
|
||||
<p className="text-charcoal-200 text-sm">{ADDRESS}</p>
|
||||
<a
|
||||
href={FULL_MENU_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block mt-3 text-gold-300 hover:text-gold-200 text-sm"
|
||||
>
|
||||
Полное меню кафе →
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">Часы работы</h4>
|
||||
<p className="text-charcoal-200 text-sm">Кафе: {HOURS_CAFE}</p>
|
||||
<p className="text-charcoal-200 text-sm">Минимаркет: {HOURS_MARKET}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">Связь</h4>
|
||||
<p className="text-sm">
|
||||
<a
|
||||
href={`tel:${PHONE_CAFE.replace(/\s/g, "")}`}
|
||||
className="text-gold-300 hover:text-gold-200"
|
||||
>
|
||||
📞 {PHONE_CAFE}
|
||||
</a>
|
||||
<span className="text-charcoal-300"> · кафе</span>
|
||||
</p>
|
||||
<p className="text-sm mt-1">
|
||||
<a
|
||||
href={`tel:${PHONE_MARKET.replace(/\s/g, "")}`}
|
||||
className="text-gold-300 hover:text-gold-200"
|
||||
>
|
||||
📞 {PHONE_MARKET}
|
||||
</a>
|
||||
<span className="text-charcoal-300"> · минимаркет</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mx-auto max-w-5xl mt-8 text-charcoal-300 text-xs">© AQ JAIYQ · пос. Нура</p>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
52
src/cart.ts
Normal file
52
src/cart.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Product } from "./products";
|
||||
|
||||
const KEY = "aq-jaiyq-cart-v1";
|
||||
|
||||
export type CartItem = { product: Product; qty: number };
|
||||
|
||||
function read(): CartItem[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(KEY);
|
||||
return raw ? (JSON.parse(raw) as CartItem[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function write(items: CartItem[]) {
|
||||
localStorage.setItem(KEY, JSON.stringify(items));
|
||||
window.dispatchEvent(new CustomEvent("cart:update"));
|
||||
}
|
||||
|
||||
export function useCart() {
|
||||
const [items, setItems] = useState<CartItem[]>(() => read());
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => setItems(read());
|
||||
window.addEventListener("cart:update", handler);
|
||||
return () => window.removeEventListener("cart:update", handler);
|
||||
}, []);
|
||||
|
||||
const add = (product: Product) => {
|
||||
const cur = read();
|
||||
const idx = cur.findIndex((i) => i.product.id === product.id);
|
||||
if (idx >= 0) cur[idx].qty += 1;
|
||||
else cur.push({ product, qty: 1 });
|
||||
write(cur);
|
||||
};
|
||||
|
||||
const dec = (id: string) => {
|
||||
const cur = read()
|
||||
.map((i) => (i.product.id === id ? { ...i, qty: i.qty - 1 } : i))
|
||||
.filter((i) => i.qty > 0);
|
||||
write(cur);
|
||||
};
|
||||
|
||||
const remove = (id: string) => write(read().filter((i) => i.product.id !== id));
|
||||
const clear = () => write([]);
|
||||
const total = items.reduce((s, i) => s + i.product.price * i.qty, 0);
|
||||
const count = items.reduce((s, i) => s + i.qty, 0);
|
||||
|
||||
return { items, add, dec, remove, clear, total, count };
|
||||
}
|
||||
2
src/format.ts
Normal file
2
src/format.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const fmtTenge = (n: number) =>
|
||||
new Intl.NumberFormat("ru-KZ", { maximumFractionDigits: 0 }).format(n) + " ₸";
|
||||
26
src/index.css
Normal file
26
src/index.css
Normal file
@ -0,0 +1,26 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@600;700;800&display=swap");
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
@apply bg-cream text-charcoal-700 font-sans antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
@apply inline-flex items-center justify-center gap-2 bg-gold-400 hover:bg-gold-500 text-charcoal-700 font-medium px-5 py-3 rounded-xl transition-colors;
|
||||
}
|
||||
.btn-secondary {
|
||||
@apply inline-flex items-center justify-center gap-2 bg-charcoal-700 hover:bg-charcoal-600 text-cream font-medium px-5 py-3 rounded-xl transition-colors;
|
||||
}
|
||||
.btn-ghost {
|
||||
@apply inline-flex items-center justify-center gap-2 bg-white hover:bg-charcoal-50 text-charcoal-700 font-medium px-5 py-3 rounded-xl border border-charcoal-100 transition-colors;
|
||||
}
|
||||
}
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
51
src/products.ts
Normal file
51
src/products.ts
Normal file
@ -0,0 +1,51 @@
|
||||
export type Product = {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
unit?: string;
|
||||
ingredients?: string;
|
||||
emoji: string;
|
||||
tags?: string[];
|
||||
section: "cafe" | "market";
|
||||
};
|
||||
|
||||
export const popularCafe: Product[] = [
|
||||
{ id: "cf-021", name: "Плов", price: 1200, emoji: "🍚", tags: ["hit"], section: "cafe" },
|
||||
{ id: "cf-025", name: "Куырдак из баранины", price: 1500, emoji: "🥘", tags: ["hit"], section: "cafe" },
|
||||
{ id: "cf-049", name: "Шашлык из курицы", price: 1500, emoji: "🍢", tags: ["hit"], section: "cafe" },
|
||||
{ id: "cf-051", name: "Шашлык из баранины", price: 2000, emoji: "🍖", tags: ["hit"], section: "cafe" },
|
||||
{
|
||||
id: "cf-067",
|
||||
name: "Фирменная закуска «АҚ ЖАЙЫҚ»",
|
||||
price: 2500,
|
||||
ingredients: "фарш из брынзы и шпината в тесте, жаренные во фритюре",
|
||||
emoji: "🥟",
|
||||
tags: ["signature", "preorder"],
|
||||
section: "cafe",
|
||||
},
|
||||
{ id: "cf-091", name: "Max Классика", price: 1500, emoji: "🌯", tags: ["hit"], section: "cafe" },
|
||||
{
|
||||
id: "cf-110",
|
||||
name: "Казан-кебаб",
|
||||
price: 30000,
|
||||
ingredients: "обжаренный картофель, говядина, розочки из теста. 10–12 человек",
|
||||
emoji: "🍲",
|
||||
tags: ["signature"],
|
||||
section: "cafe",
|
||||
},
|
||||
];
|
||||
|
||||
export const popularMarket: Product[] = [
|
||||
{ id: "mk-001", name: "Хлеб домашний", price: 165, unit: "шт", emoji: "🍞", tags: ["hit"], section: "market" },
|
||||
{ id: "mk-002", name: "Хлеб на сыворотке", price: 180, unit: "шт", emoji: "🥖", section: "market" },
|
||||
{ id: "mk-003", name: "Сахар", price: 569, unit: "кг", emoji: "🧂", section: "market" },
|
||||
{ id: "mk-004", name: "Картофель", price: 319, unit: "кг", emoji: "🥔", section: "market" },
|
||||
];
|
||||
|
||||
export const WHATSAPP_PHONE = "77054263130";
|
||||
export const PHONE_CAFE = "+7 705 426 31 30";
|
||||
export const PHONE_MARKET = "+7 776 031 96 66";
|
||||
export const ADDRESS = "ул. Ардагерлер 40, поселок Нура, Карагандинская область";
|
||||
export const HOURS_CAFE = "10:00 – 23:00";
|
||||
export const HOURS_MARKET = "Круглосуточно";
|
||||
export const FULL_MENU_URL = "https://aq-jaiyq.kz/cafe";
|
||||
38
tailwind.config.js
Normal file
38
tailwind.config.js
Normal file
@ -0,0 +1,38 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{ts,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
cream: "#F5EFE6",
|
||||
charcoal: {
|
||||
50: "#F5F2EE",
|
||||
100: "#E6E1DA",
|
||||
200: "#C7C0B8",
|
||||
300: "#A39A91",
|
||||
400: "#7A726B",
|
||||
500: "#5A5450",
|
||||
600: "#4A4644",
|
||||
700: "#3D3A3A",
|
||||
},
|
||||
gold: {
|
||||
100: "#F2E9D2",
|
||||
200: "#E6D6A8",
|
||||
300: "#D7BB78",
|
||||
400: "#C9A961",
|
||||
500: "#B58F49",
|
||||
600: "#9A7B54",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
display: ['"Playfair Display"', "Georgia", "serif"],
|
||||
sans: ["Inter", "system-ui", "sans-serif"],
|
||||
},
|
||||
boxShadow: {
|
||||
card: "0 10px 30px -10px rgba(61, 58, 58, 0.25)",
|
||||
soft: "0 4px 12px -4px rgba(61, 58, 58, 0.12)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
27
vite.config.ts
Normal file
27
vite.config.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
const stripModuleType = {
|
||||
name: "strip-module-type",
|
||||
transformIndexHtml(html: string) {
|
||||
return html
|
||||
.replace(/<script type="module"/g, "<script")
|
||||
.replace(/\s*crossorigin/g, "");
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), stripModuleType],
|
||||
base: "./",
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
format: "iife",
|
||||
inlineDynamicImports: true,
|
||||
entryFileNames: "assets/index.js",
|
||||
chunkFileNames: "assets/[name].js",
|
||||
assetFileNames: "assets/[name].[ext]",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user