Replace entire repo content with correct code from /root/shahikitchen-website/
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import type { OrderLine } from '../shared/order-line';
|
||||
|
||||
export type BranchLocation = 'askim' | 'backaplan' | '';
|
||||
|
||||
export interface BookingDetails {
|
||||
location: BranchLocation;
|
||||
date: string;
|
||||
time: string;
|
||||
guests: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export type PreOrderLine = OrderLine;
|
||||
|
||||
export interface BookingInquiry {
|
||||
booking: BookingDetails;
|
||||
preOrder: PreOrderLine[];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { BookingDetails } from './entities';
|
||||
|
||||
export function isBookingComplete(booking: BookingDetails): boolean {
|
||||
return !!(
|
||||
booking.location &&
|
||||
booking.date &&
|
||||
booking.time &&
|
||||
booking.guests &&
|
||||
booking.name &&
|
||||
booking.phone
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
addOrderLine,
|
||||
calculateOrderItemCount,
|
||||
calculateOrderTotal,
|
||||
updateOrderLineQuantity,
|
||||
} from '../shared/order-line';
|
||||
import type { CartAction, CartState } from './entities';
|
||||
|
||||
/** Pure cart state machine — no React, no localStorage. */
|
||||
export function cartReducer(state: CartState, action: CartAction): CartState {
|
||||
switch (action.type) {
|
||||
case 'ADD_ITEM':
|
||||
return { ...state, items: addOrderLine(state.items, action.payload) };
|
||||
|
||||
case 'REMOVE_ITEM':
|
||||
return {
|
||||
...state,
|
||||
items: state.items.filter((item) => item.id !== action.payload),
|
||||
};
|
||||
|
||||
case 'UPDATE_QUANTITY':
|
||||
return {
|
||||
...state,
|
||||
items: updateOrderLineQuantity(
|
||||
state.items,
|
||||
action.payload.id,
|
||||
action.payload.quantity
|
||||
),
|
||||
};
|
||||
|
||||
case 'CLEAR_CART':
|
||||
return { ...state, items: [] };
|
||||
|
||||
case 'RESTORE_ITEMS':
|
||||
return { ...state, items: action.payload };
|
||||
|
||||
case 'TOGGLE_CART':
|
||||
return { ...state, isOpen: !state.isOpen };
|
||||
|
||||
case 'OPEN_CART':
|
||||
return { ...state, isOpen: true };
|
||||
|
||||
case 'CLOSE_CART':
|
||||
return { ...state, isOpen: false };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCartTotals(items: CartState['items']) {
|
||||
return {
|
||||
totalItems: calculateOrderItemCount(items),
|
||||
totalPrice: calculateOrderTotal(items),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { OrderLine, NewOrderLine } from '../shared/order-line';
|
||||
|
||||
export type CartItem = OrderLine;
|
||||
export type NewCartItem = NewOrderLine;
|
||||
|
||||
export interface CartState {
|
||||
items: CartItem[];
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export type CartAction =
|
||||
| { type: 'ADD_ITEM'; payload: NewCartItem }
|
||||
| { type: 'REMOVE_ITEM'; payload: string }
|
||||
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
|
||||
| { type: 'CLEAR_CART' }
|
||||
| { type: 'TOGGLE_CART' }
|
||||
| { type: 'OPEN_CART' }
|
||||
| { type: 'CLOSE_CART' }
|
||||
| { type: 'RESTORE_ITEMS'; payload: CartItem[] };
|
||||
@@ -0,0 +1,11 @@
|
||||
export type FulfillmentMode = 'pickup' | 'delivery';
|
||||
|
||||
export interface PickupInquiryDetails {
|
||||
name: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export interface DeliveryInquiryDetails extends PickupInquiryDetails {
|
||||
address: string;
|
||||
preferredTime: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { CartItem } from './entities';
|
||||
|
||||
/** Port: cart persistence (implemented by infrastructure). */
|
||||
export interface CartRepository {
|
||||
load(): CartItem[];
|
||||
save(items: CartItem[]): void;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { CateringPackage } from './entities';
|
||||
|
||||
/**
|
||||
* Catering packages — price per head in SEK (kr).
|
||||
* Dish IDs reference domain/catering/dish-catalog.ts.
|
||||
*/
|
||||
export const cateringPackages: CateringPackage[] = [
|
||||
{
|
||||
id: 'silver',
|
||||
name: 'Silver Package',
|
||||
description:
|
||||
'A refined introduction to Shahi catering — one signature curry, dal, rice, naan and fresh salad. Ideal for office lunches and intimate gatherings.',
|
||||
pricePerHead: 189,
|
||||
currency: 'SEK',
|
||||
includedDishes: [
|
||||
'butter-chicken',
|
||||
'dal',
|
||||
'plain-rice',
|
||||
'naan',
|
||||
'salad',
|
||||
],
|
||||
image: 'butter-chicken.jpg',
|
||||
},
|
||||
{
|
||||
id: 'gold',
|
||||
name: 'Gold Package',
|
||||
description:
|
||||
'Our most popular celebration menu — biryani, two curries, a vegetarian classic, rice, naan, raita and salad. Perfect for weddings and family events.',
|
||||
pricePerHead: 249,
|
||||
currency: 'SEK',
|
||||
includedDishes: [
|
||||
'chicken-biryani',
|
||||
'butter-chicken',
|
||||
'lamb-karahi',
|
||||
'palak-paneer',
|
||||
'plain-rice',
|
||||
'naan',
|
||||
'raita',
|
||||
'salad',
|
||||
],
|
||||
image: 'chicken-biryani.jpg',
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
id: 'royal',
|
||||
name: 'Royal Shahi Package',
|
||||
description:
|
||||
'The full Shahi experience — biryani, tikka masala, premium lamb, seekh kebab, paneer, dal, and complete sides. For grand occasions that demand royalty.',
|
||||
pricePerHead: 329,
|
||||
currency: 'SEK',
|
||||
includedDishes: [
|
||||
'chicken-biryani',
|
||||
'chicken-tikka-masala',
|
||||
'butter-chicken',
|
||||
'lamb-korma',
|
||||
'seekh-kebab',
|
||||
'shahi-paneer',
|
||||
'dal',
|
||||
'plain-rice',
|
||||
'naan',
|
||||
'raita',
|
||||
'salad',
|
||||
],
|
||||
image: 'lamm-karahi.jpg',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { CateringDish, CateringDishCategory } from './entities';
|
||||
|
||||
/**
|
||||
* Master catalog of dishes available for catering packages.
|
||||
* IDs match the catering brief exactly.
|
||||
*/
|
||||
export const CATERING_DISH_CATALOG: CateringDish[] = [
|
||||
// Lamb
|
||||
{ id: 'lamb-vindaloo', name: 'Lamb Vindaloo', category: 'lamb', image: 'lamm-vindaloo.jpg' },
|
||||
{ id: 'lamb-karahi', name: 'Lamb Karahi', category: 'lamb', image: 'lamm-karahi.jpg' },
|
||||
{ id: 'lamb-korma', name: 'Lamb Korma', category: 'lamb', image: 'lamm-rogan-josh.jpg' },
|
||||
{ id: 'lamb-achari', name: 'Lamb Achari', category: 'lamb', image: 'lamm-vindaloo.jpg' },
|
||||
|
||||
// Chicken
|
||||
{ id: 'chicken-tikka-masala', name: 'Chicken Tikka Masala', category: 'chicken', image: 'chicken-tikka.jpg' },
|
||||
{ id: 'butter-chicken', name: 'Butter Chicken', category: 'chicken', image: 'butter-chicken.jpg' },
|
||||
{ id: 'chicken-korma', name: 'Chicken Korma', category: 'chicken', image: 'butter-chicken.jpg' },
|
||||
{ id: 'chicken-karahi', name: 'Chicken Karahi', category: 'chicken', image: 'chicken-karahi.jpg' },
|
||||
{ id: 'chicken-achari', name: 'Chicken Achari', category: 'chicken', image: 'chicken-karahi.jpg' },
|
||||
{ id: 'chicken-biryani', name: 'Chicken Biryani', category: 'chicken', image: 'chicken-biryani.jpg' },
|
||||
|
||||
// Beef
|
||||
{ id: 'shami-kebab', name: 'Shami Kebab', category: 'beef', image: 'shami-sandwich.jpg' },
|
||||
{ id: 'seekh-kebab', name: 'Seekh Kebab', category: 'beef', image: 'kebab-roll.jpg' },
|
||||
{ id: 'beef-karahi', name: 'Beef Karahi', category: 'beef', image: 'bong-nihari.jpg' },
|
||||
|
||||
// Vegetarian
|
||||
{ id: 'dal', name: 'Dal', category: 'vegetarian', image: 'daal-makhani.jpg' },
|
||||
{ id: 'palak-paneer', name: 'Palak Paneer', category: 'vegetarian', image: 'palak-paneer.jpg' },
|
||||
{ id: 'shahi-paneer', name: 'Shahi Paneer', category: 'vegetarian', image: 'shahi-paneer.jpg' },
|
||||
{ id: 'sabzi', name: 'Sabzi', category: 'vegetarian', image: 'lahore-chana.jpg' },
|
||||
|
||||
// Sides
|
||||
{ id: 'plain-rice', name: 'Plain Rice', category: 'sides', image: 'chicken-biryani.jpg' },
|
||||
{ id: 'naan', name: 'Naan', category: 'sides', image: 'keema-naan.jpg' },
|
||||
{ id: 'salad', name: 'Salad', category: 'sides', image: 'chana-chaat.jpg' },
|
||||
{ id: 'raita', name: 'Raita', category: 'sides', image: 'mango-lassi.jpg' },
|
||||
];
|
||||
|
||||
export const CATERING_CATEGORY_ORDER: CateringDishCategory[] = [
|
||||
'lamb',
|
||||
'chicken',
|
||||
'beef',
|
||||
'vegetarian',
|
||||
'sides',
|
||||
];
|
||||
|
||||
export function getCateringDishById(id: string): CateringDish | undefined {
|
||||
return CATERING_DISH_CATALOG.find((d) => d.id === id);
|
||||
}
|
||||
|
||||
export function groupDishesByCategory(
|
||||
dishIds: string[]
|
||||
): Record<CateringDishCategory, CateringDish[]> {
|
||||
const grouped: Record<CateringDishCategory, CateringDish[]> = {
|
||||
lamb: [],
|
||||
chicken: [],
|
||||
beef: [],
|
||||
vegetarian: [],
|
||||
sides: [],
|
||||
};
|
||||
|
||||
for (const id of dishIds) {
|
||||
const dish = getCateringDishById(id);
|
||||
if (dish) grouped[dish.category].push(dish);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export type CateringDishCategory =
|
||||
| 'lamb'
|
||||
| 'chicken'
|
||||
| 'beef'
|
||||
| 'vegetarian'
|
||||
| 'sides';
|
||||
|
||||
export interface CateringDish {
|
||||
id: string;
|
||||
name: string;
|
||||
category: CateringDishCategory;
|
||||
/** Optional link to an existing menu poster in /public/images/dishes/ */
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface CateringPackage {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
pricePerHead: number;
|
||||
currency: 'SEK';
|
||||
includedDishes: string[];
|
||||
image?: string;
|
||||
highlight?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export interface KitchenOrderItem {
|
||||
name: string;
|
||||
qty: number;
|
||||
}
|
||||
|
||||
export type OrderSource = 'table' | 'foodora' | 'uber' | 'wolt' | 'pickup' | 'other';
|
||||
|
||||
export type OrderStatus = 'received' | 'review' | 'preparing' | 'packing' | 'ready';
|
||||
|
||||
export interface KitchenOrder {
|
||||
id: string;
|
||||
source: OrderSource;
|
||||
label: string;
|
||||
customer: string;
|
||||
items: KitchenOrderItem[];
|
||||
status: OrderStatus;
|
||||
receivedAt: Date;
|
||||
etaMinutes: number;
|
||||
}
|
||||
|
||||
export const ORDER_STATUS_PIPELINE: readonly OrderStatus[] = [
|
||||
'received',
|
||||
'review',
|
||||
'preparing',
|
||||
'packing',
|
||||
'ready',
|
||||
] as const;
|
||||
@@ -0,0 +1,29 @@
|
||||
export type Language = 'en' | 'sv' | 'hi' | 'ur' | 'ar' | 'tr';
|
||||
|
||||
export const SUPPORTED_LANGUAGES: readonly Language[] = [
|
||||
'sv',
|
||||
'en',
|
||||
'ar',
|
||||
'tr',
|
||||
'hi',
|
||||
'ur',
|
||||
] as const;
|
||||
|
||||
export const DEFAULT_LANGUAGE: Language = 'sv';
|
||||
|
||||
export const RTL_LANGUAGES: readonly Language[] = ['ar', 'ur'] as const;
|
||||
|
||||
export function isLanguage(value: string): value is Language {
|
||||
return (SUPPORTED_LANGUAGES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function isRtlLanguage(language: Language): boolean {
|
||||
return (RTL_LANGUAGES as readonly string[]).includes(language);
|
||||
}
|
||||
|
||||
export interface LanguageOption {
|
||||
code: Language;
|
||||
name: string;
|
||||
native: string;
|
||||
flag: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Language } from './entities';
|
||||
|
||||
/** Port: language preference persistence. */
|
||||
export interface LanguageRepository {
|
||||
load(): Language | null;
|
||||
save(language: Language): void;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Extended dish detail data for the menu detail modal.
|
||||
* All sweets category items use the detail modal on /menu.
|
||||
*/
|
||||
|
||||
/** Category id whose items show the detail modal instead of inline description */
|
||||
export const DETAIL_MODAL_CATEGORY_ID = 'sweets';
|
||||
|
||||
/** Per-dish ingredient lists shown in the detail modal (add as provided) */
|
||||
export const dishIngredients: Record<string, string[]> = {
|
||||
'badam-barfi': [
|
||||
'Almonds (badam)',
|
||||
'Full-cream milk',
|
||||
'Sugar',
|
||||
'Ghee',
|
||||
'Cardamom',
|
||||
],
|
||||
};
|
||||
|
||||
export function hasDishDetailModal(_dishId: string, categoryId?: string): boolean {
|
||||
return categoryId === DETAIL_MODAL_CATEGORY_ID;
|
||||
}
|
||||
|
||||
export function getDishIngredients(dishId: string): string[] {
|
||||
return dishIngredients[dishId] ?? [];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export type MenuPricingMode = 'standard' | 'weight';
|
||||
|
||||
export interface MenuItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
image?: string;
|
||||
video?: string;
|
||||
isVegetarian?: boolean;
|
||||
/** Weight-based sweets: 90 kr / ½ kg, 179 kr / kg (kulfi stays standard). */
|
||||
pricing?: MenuPricingMode;
|
||||
pricePerHalfKg?: number;
|
||||
pricePerKg?: number;
|
||||
}
|
||||
|
||||
export interface MenuCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
items: MenuItem[];
|
||||
}
|
||||
|
||||
export interface MenuFilter {
|
||||
categoryId?: string;
|
||||
searchQuery?: string;
|
||||
vegetarianOnly?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { MenuCategory, MenuItem } from './entities';
|
||||
|
||||
/** Port: menu data access (implemented by infrastructure). */
|
||||
export interface MenuRepository {
|
||||
getCategories(): MenuCategory[];
|
||||
getAllItems(): MenuItem[];
|
||||
findItemById(id: string): MenuItem | undefined;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/** Restaurant contact & persistence keys — domain constants (no framework deps). */
|
||||
|
||||
export const RESTAURANT_CONTACT = {
|
||||
whatsappNumber: '46739381089',
|
||||
phonePrimary: '031288910',
|
||||
phoneMobile: '0739381089',
|
||||
email: 'hello@shahikitchen.se',
|
||||
website: 'https://shahikitchen.se',
|
||||
} as const;
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
cart: 'shahi-kitchen-cart',
|
||||
wishlist: 'shahi-kitchen-wishlist',
|
||||
language: 'shahi-kitchen-language',
|
||||
} as const;
|
||||
|
||||
export const PICKUP_LEAD_TIME_MINUTES = 30;
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
calculateSweetsWeightTotal,
|
||||
formatSweetWeightLabel,
|
||||
} from '../sweets/pricing';
|
||||
|
||||
/** Shared order-line model used by cart, pre-order, and table orders. */
|
||||
|
||||
export type OrderPricingMode = 'standard' | 'weight';
|
||||
|
||||
export interface OrderLine {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
image?: string;
|
||||
pricingMode?: OrderPricingMode;
|
||||
pricePerKg?: number;
|
||||
pricePerHalfKg?: number;
|
||||
}
|
||||
|
||||
export type NewOrderLine = Omit<OrderLine, 'quantity'>;
|
||||
|
||||
export function addOrderLine(lines: OrderLine[], item: NewOrderLine): OrderLine[] {
|
||||
const existing = lines.find((line) => line.id === item.id);
|
||||
if (existing) {
|
||||
return lines.map((line) =>
|
||||
line.id === item.id ? { ...line, quantity: line.quantity + 1 } : line
|
||||
);
|
||||
}
|
||||
return [...lines, { ...item, quantity: 1 }];
|
||||
}
|
||||
|
||||
export function updateOrderLineQuantity(
|
||||
lines: OrderLine[],
|
||||
id: string,
|
||||
quantity: number
|
||||
): OrderLine[] {
|
||||
if (quantity <= 0) {
|
||||
return lines.filter((line) => line.id !== id);
|
||||
}
|
||||
return lines.map((line) => (line.id === id ? { ...line, quantity } : line));
|
||||
}
|
||||
|
||||
export function removeOrderLine(lines: OrderLine[], id: string): OrderLine[] {
|
||||
return lines.filter((line) => line.id !== id);
|
||||
}
|
||||
|
||||
export function calculateLineTotal(line: OrderLine): number {
|
||||
if (line.pricingMode === 'weight') {
|
||||
return calculateSweetsWeightTotal(line.quantity);
|
||||
}
|
||||
return line.price * line.quantity;
|
||||
}
|
||||
|
||||
export function calculateOrderTotal(lines: OrderLine[]): number {
|
||||
return lines.reduce((sum, line) => sum + calculateLineTotal(line), 0);
|
||||
}
|
||||
|
||||
export function calculateOrderItemCount(lines: OrderLine[]): number {
|
||||
return lines.reduce((sum, line) => sum + line.quantity, 0);
|
||||
}
|
||||
|
||||
export function formatOrderLinesForMessage(lines: OrderLine[]): string {
|
||||
return lines
|
||||
.map((line) => {
|
||||
const total = calculateLineTotal(line);
|
||||
if (line.pricingMode === 'weight') {
|
||||
const weight = formatSweetWeightLabel(line.quantity);
|
||||
return `${line.name} (${weight}) — ${total} kr`;
|
||||
}
|
||||
return `${line.quantity} × ${line.name} — ${total} kr`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/** Inquiry format: "Dish Name x Qty - Price kr" */
|
||||
export function formatOrderLinesForInquiry(lines: OrderLine[]): string {
|
||||
return lines
|
||||
.map((line) => {
|
||||
const total = calculateLineTotal(line);
|
||||
const qty =
|
||||
line.pricingMode === 'weight'
|
||||
? formatSweetWeightLabel(line.quantity)
|
||||
: String(line.quantity);
|
||||
return `${line.name} x ${qty} - ${total} kr`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function formatLineQuantityLabel(line: OrderLine): string {
|
||||
if (line.pricingMode === 'weight') {
|
||||
return formatSweetWeightLabel(line.quantity);
|
||||
}
|
||||
return String(line.quantity);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/** Weight-based mithai pricing (all sweets except kulfi). */
|
||||
|
||||
export const SWEETS_HALF_KG_PRICE = 90;
|
||||
export const SWEETS_KG_PRICE = 179;
|
||||
export const KULFI_MENU_ID = 'kulfi';
|
||||
|
||||
/** quantity = number of half-kg portions */
|
||||
export function calculateSweetsWeightTotal(halfKgUnits: number): number {
|
||||
const fullKg = Math.floor(halfKgUnits / 2);
|
||||
const halfRemainder = halfKgUnits % 2;
|
||||
return fullKg * SWEETS_KG_PRICE + halfRemainder * SWEETS_HALF_KG_PRICE;
|
||||
}
|
||||
|
||||
export function halfKgUnitsToKg(halfKgUnits: number): number {
|
||||
return halfKgUnits * 0.5;
|
||||
}
|
||||
|
||||
export function formatSweetWeightLabel(halfKgUnits: number): string {
|
||||
const kg = halfKgUnitsToKg(halfKgUnits);
|
||||
return kg === 0.5 ? '0.5 kg' : `${kg} kg`;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export type Branch = 'askim' | 'backaplan';
|
||||
|
||||
export interface ValidTableId {
|
||||
valid: true;
|
||||
branch: Branch;
|
||||
tableNumber: string;
|
||||
tableIdentifier: string;
|
||||
}
|
||||
|
||||
export interface InvalidTableId {
|
||||
valid: false;
|
||||
reason: 'missing' | 'format' | 'range';
|
||||
}
|
||||
|
||||
export type ParsedTableId = ValidTableId | InvalidTableId;
|
||||
|
||||
const TABLE_ID_PATTERN = /^(backaplan|askim)-([0-9]{3})$/;
|
||||
const MIN_TABLE = 1;
|
||||
const MAX_TABLE = 40;
|
||||
|
||||
/** Domain rule: parse and validate QR table identifiers. */
|
||||
export function parseTableId(raw: string | null | undefined): ParsedTableId {
|
||||
if (!raw) {
|
||||
return { valid: false, reason: 'missing' };
|
||||
}
|
||||
|
||||
const match = raw.match(TABLE_ID_PATTERN);
|
||||
if (!match) {
|
||||
return { valid: false, reason: 'format' };
|
||||
}
|
||||
|
||||
const branch = match[1] as Branch;
|
||||
const tableNum = parseInt(match[2], 10);
|
||||
|
||||
if (tableNum < MIN_TABLE || tableNum > MAX_TABLE) {
|
||||
return { valid: false, reason: 'range' };
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
branch,
|
||||
tableNumber: match[2],
|
||||
tableIdentifier: raw,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export interface WishlistItem {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export type NewWishlistItem = WishlistItem;
|
||||
|
||||
export interface WishlistState {
|
||||
items: WishlistItem[];
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export type WishlistAction =
|
||||
| { type: 'TOGGLE_ITEM'; payload: NewWishlistItem }
|
||||
| { type: 'REMOVE_ITEM'; payload: string }
|
||||
| { type: 'CLEAR_WISHLIST' }
|
||||
| { type: 'TOGGLE_DRAWER' }
|
||||
| { type: 'OPEN_DRAWER' }
|
||||
| { type: 'CLOSE_DRAWER' }
|
||||
| { type: 'RESTORE_ITEMS'; payload: WishlistItem[] };
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { WishlistItem } from './entities';
|
||||
|
||||
export interface WishlistRepository {
|
||||
load(): WishlistItem[];
|
||||
save(items: WishlistItem[]): void;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { WishlistAction, WishlistItem, WishlistState } from './entities';
|
||||
|
||||
export function wishlistReducer(state: WishlistState, action: WishlistAction): WishlistState {
|
||||
switch (action.type) {
|
||||
case 'TOGGLE_ITEM': {
|
||||
const exists = state.items.some((item) => item.id === action.payload.id);
|
||||
if (exists) {
|
||||
return {
|
||||
...state,
|
||||
items: state.items.filter((item) => item.id !== action.payload.id),
|
||||
};
|
||||
}
|
||||
return { ...state, items: [...state.items, action.payload] };
|
||||
}
|
||||
|
||||
case 'REMOVE_ITEM':
|
||||
return {
|
||||
...state,
|
||||
items: state.items.filter((item) => item.id !== action.payload),
|
||||
};
|
||||
|
||||
case 'CLEAR_WISHLIST':
|
||||
return { ...state, items: [] };
|
||||
|
||||
case 'RESTORE_ITEMS':
|
||||
return { ...state, items: action.payload };
|
||||
|
||||
case 'TOGGLE_DRAWER':
|
||||
return { ...state, isOpen: !state.isOpen };
|
||||
|
||||
case 'OPEN_DRAWER':
|
||||
return { ...state, isOpen: true };
|
||||
|
||||
case 'CLOSE_DRAWER':
|
||||
return { ...state, isOpen: false };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function isInWishlist(items: WishlistItem[], id: string): boolean {
|
||||
return items.some((item) => item.id === id);
|
||||
}
|
||||
|
||||
export function getWishlistCount(items: WishlistItem[]): number {
|
||||
return items.length;
|
||||
}
|
||||
Reference in New Issue
Block a user