Replace entire repo content with kottgard-production-v1.1.zip (for shahikitchen-prod repo)
This commit is contained in:
@@ -1,28 +0,0 @@
|
||||
import type { MenuItem } from '@/domain/menu/entities';
|
||||
import type { NewOrderLine } from '@/domain/shared/order-line';
|
||||
import {
|
||||
SWEETS_HALF_KG_PRICE,
|
||||
SWEETS_KG_PRICE,
|
||||
} from '@/domain/sweets/pricing';
|
||||
|
||||
export function buildCartLineFromMenuItem(item: MenuItem): NewOrderLine {
|
||||
if (item.pricing === 'weight') {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
price: item.pricePerHalfKg ?? SWEETS_HALF_KG_PRICE,
|
||||
image: item.image,
|
||||
pricingMode: 'weight',
|
||||
pricePerKg: item.pricePerKg ?? SWEETS_KG_PRICE,
|
||||
pricePerHalfKg: item.pricePerHalfKg ?? SWEETS_HALF_KG_PRICE,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
price: item.price,
|
||||
image: item.image,
|
||||
pricingMode: 'standard',
|
||||
};
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import {
|
||||
DEFAULT_LANGUAGE,
|
||||
isLanguage,
|
||||
type Language,
|
||||
} from '@/domain/language/entities';
|
||||
import type { LanguageRepository } from '@/domain/language/repository';
|
||||
|
||||
export function detectBrowserLanguage(): Language {
|
||||
if (typeof navigator === 'undefined') return DEFAULT_LANGUAGE;
|
||||
|
||||
const browserLang = navigator.language.toLowerCase();
|
||||
if (browserLang.startsWith('sv')) return 'sv';
|
||||
if (browserLang.startsWith('ar')) return 'ar';
|
||||
if (browserLang.startsWith('tr')) return 'tr';
|
||||
if (browserLang.startsWith('hi')) return 'hi';
|
||||
if (browserLang.startsWith('ur')) return 'ur';
|
||||
if (browserLang.startsWith('en')) return 'en';
|
||||
return DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
export function resolveInitialLanguage(repository: LanguageRepository): Language {
|
||||
const saved = repository.load();
|
||||
if (saved && isLanguage(saved)) return saved;
|
||||
return detectBrowserLanguage();
|
||||
}
|
||||
|
||||
export function persistLanguage(repository: LanguageRepository, language: Language): void {
|
||||
repository.save(language);
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import type { MenuItem } from '@/domain/menu/entities';
|
||||
import {
|
||||
MEDIA_FOLDERS,
|
||||
SITE_ASSETS,
|
||||
mediaUrl,
|
||||
} from '@/infrastructure/assets/site-assets';
|
||||
|
||||
export type PosterVariant = 'standard' | 'optimized';
|
||||
|
||||
/** Strip .mp4 extension from a video filename */
|
||||
export function videoBaseName(videoFilename: string): string {
|
||||
return videoFilename.replace(/\.mp4$/i, '');
|
||||
}
|
||||
|
||||
/** Full URL for a dish image filename (e.g. "butter-chicken.jpg") */
|
||||
export function dishImageUrl(filename: string): string {
|
||||
return mediaUrl(MEDIA_FOLDERS.dishes, filename);
|
||||
}
|
||||
|
||||
/** Poster URL derived from a video filename */
|
||||
export function dishPosterFromVideo(
|
||||
videoFilename: string,
|
||||
variant: PosterVariant = 'standard'
|
||||
): string {
|
||||
const base = videoBaseName(videoFilename);
|
||||
const suffix = variant === 'optimized' ? '-optimized-poster.jpg' : '-poster.jpg';
|
||||
return mediaUrl(MEDIA_FOLDERS.dishes, `${base}${suffix}`);
|
||||
}
|
||||
|
||||
/** Primary logo used in navbar, footer, login, etc. */
|
||||
export function logoUrl(): string {
|
||||
return SITE_ASSETS.logo.primary;
|
||||
}
|
||||
|
||||
/** Hero banner video sources for homepage */
|
||||
export function heroBannerSources() {
|
||||
return {
|
||||
mobileWebm: SITE_ASSETS.banner.mobileWebm,
|
||||
mobileMp4: SITE_ASSETS.banner.mobileMp4,
|
||||
desktopMp4: SITE_ASSETS.banner.desktopMp4,
|
||||
};
|
||||
}
|
||||
|
||||
/** Chef expression images for PlayfulHeroScene */
|
||||
export function chefExpressionUrl(expression: 'wink' | 'smile' | 'normal'): string {
|
||||
const map = {
|
||||
wink: SITE_ASSETS.animation.chefWink,
|
||||
smile: SITE_ASSETS.animation.chefSmile,
|
||||
normal: SITE_ASSETS.animation.chefNormal,
|
||||
};
|
||||
return map[expression];
|
||||
}
|
||||
|
||||
/** Resolve primary poster path for a menu item */
|
||||
export function getMenuPosterSrc(
|
||||
item: MenuItem,
|
||||
variant: PosterVariant = 'standard'
|
||||
): string {
|
||||
if (item.video) {
|
||||
return dishPosterFromVideo(item.video, variant);
|
||||
}
|
||||
if (item.image) {
|
||||
return dishImageUrl(item.image);
|
||||
}
|
||||
return variant === 'optimized'
|
||||
? SITE_ASSETS.dishes.defaultPoster
|
||||
: SITE_ASSETS.fallbacks.dishPoster;
|
||||
}
|
||||
|
||||
/** Poster candidates for progressive fallback in UI `<img onError>` */
|
||||
export function getMenuPosterCandidates(item: MenuItem): string[] {
|
||||
if (!item.video) {
|
||||
return item.image
|
||||
? [dishImageUrl(item.image), SITE_ASSETS.fallbacks.logo]
|
||||
: [SITE_ASSETS.fallbacks.logo];
|
||||
}
|
||||
|
||||
const base = videoBaseName(item.video);
|
||||
return [
|
||||
dishPosterFromVideo(item.video, 'standard'),
|
||||
dishPosterFromVideo(item.video, 'optimized'),
|
||||
...(item.image ? [dishImageUrl(item.image)] : []),
|
||||
mediaUrl(MEDIA_FOLDERS.dishes, `${base}.jpg`),
|
||||
SITE_ASSETS.dishes.defaultPoster,
|
||||
SITE_ASSETS.fallbacks.dishPoster,
|
||||
];
|
||||
}
|
||||
|
||||
/** Full video URL for a menu item */
|
||||
export function getMenuVideoSrc(item: MenuItem): string | null {
|
||||
if (!item.video) return null;
|
||||
return mediaUrl(MEDIA_FOLDERS.videos, item.video);
|
||||
}
|
||||
|
||||
/** Optimized + fallback video sources for kiosk displays */
|
||||
export function getMenuVideoSources(videoFilename: string) {
|
||||
if (!videoFilename) {
|
||||
return { webm: '', mp4: '', fallback: '' };
|
||||
}
|
||||
const base = videoBaseName(videoFilename);
|
||||
return {
|
||||
webm: mediaUrl(MEDIA_FOLDERS.videos, `${base}-optimized.webm`),
|
||||
mp4: mediaUrl(MEDIA_FOLDERS.videos, `${base}-optimized.mp4`),
|
||||
fallback: mediaUrl(MEDIA_FOLDERS.videos, `${base}.mp4`),
|
||||
};
|
||||
}
|
||||
|
||||
/** Apply next fallback src in an img onError handler */
|
||||
export function applyNextImageFallback(
|
||||
target: HTMLImageElement,
|
||||
candidates: string[],
|
||||
currentSrc: string
|
||||
): void {
|
||||
const currentIndex = candidates.indexOf(currentSrc);
|
||||
const nextIndex = currentIndex >= 0 ? currentIndex + 1 : 0;
|
||||
if (nextIndex < candidates.length) {
|
||||
target.src = candidates[nextIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export registry for direct access when needed
|
||||
export { SITE_ASSETS, MEDIA_FOLDERS, mediaUrl } from '@/infrastructure/assets/site-assets';
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { MenuCategory, MenuFilter, MenuItem } from '@/domain/menu/entities';
|
||||
|
||||
export function filterMenuCategories(
|
||||
categories: MenuCategory[],
|
||||
filter: MenuFilter
|
||||
): MenuCategory[] {
|
||||
const query = filter.searchQuery?.toLowerCase().trim() ?? '';
|
||||
|
||||
return categories
|
||||
.map((category) => {
|
||||
if (filter.categoryId && filter.categoryId !== 'All' && category.id !== filter.categoryId) {
|
||||
return { ...category, items: [] };
|
||||
}
|
||||
|
||||
let items = category.items;
|
||||
|
||||
if (query) {
|
||||
items = items.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(query) ||
|
||||
(item.description?.toLowerCase().includes(query) ?? false)
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.vegetarianOnly) {
|
||||
items = items.filter((item) => item.isVegetarian);
|
||||
}
|
||||
|
||||
return { ...category, items };
|
||||
})
|
||||
.filter((category) => category.items.length > 0);
|
||||
}
|
||||
|
||||
export function filterMenuItems(items: MenuItem[], filter: MenuFilter): MenuItem[] {
|
||||
const query = filter.searchQuery?.toLowerCase().trim() ?? '';
|
||||
|
||||
return items.filter((item) => {
|
||||
if (filter.vegetarianOnly && !item.isVegetarian) return false;
|
||||
if (!query) return true;
|
||||
return (
|
||||
item.name.toLowerCase().includes(query) ||
|
||||
(item.description?.toLowerCase().includes(query) ?? false)
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* @deprecated Import from `@/application/media/asset-resolver` instead.
|
||||
* Kept for backward compatibility.
|
||||
*/
|
||||
export {
|
||||
getMenuPosterSrc,
|
||||
getMenuPosterCandidates,
|
||||
getMenuVideoSrc,
|
||||
getMenuVideoSources,
|
||||
dishImageUrl,
|
||||
dishPosterFromVideo,
|
||||
type PosterVariant,
|
||||
} from '@/application/media/asset-resolver';
|
||||
@@ -1,4 +0,0 @@
|
||||
/** Port: external messaging channel (WhatsApp, SMS, etc.). */
|
||||
export interface MessagingGateway {
|
||||
openWhatsApp(message: string): void;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import type { BookingDetails } from '@/domain/booking/entities';
|
||||
import { isBookingComplete } from '@/domain/booking/validation';
|
||||
import type { Language } from '@/domain/language/entities';
|
||||
import {
|
||||
calculateOrderTotal,
|
||||
formatOrderLinesForMessage,
|
||||
type OrderLine,
|
||||
} from '@/domain/shared/order-line';
|
||||
import { PICKUP_LEAD_TIME_MINUTES } from '@/domain/shared/constants';
|
||||
|
||||
export interface CartDrawerCopy {
|
||||
messageHello: string;
|
||||
messageIntro: string;
|
||||
messageConfirm: string;
|
||||
messageTotal: string;
|
||||
messageThanks: string;
|
||||
}
|
||||
|
||||
export interface BookingLocationCopy {
|
||||
askim: string;
|
||||
backaplan: string;
|
||||
}
|
||||
|
||||
export function getSuggestedPickupTime(language: Language, from: Date = new Date()): string {
|
||||
const suggested = new Date(from.getTime() + PICKUP_LEAD_TIME_MINUTES * 60 * 1000);
|
||||
const timeLocale =
|
||||
language === 'sv' ? 'sv-SE' :
|
||||
language === 'ar' ? 'ar-SA' :
|
||||
language === 'tr' ? 'tr-TR' :
|
||||
'en-GB';
|
||||
return suggested.toLocaleTimeString(timeLocale, { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
export function buildCartWhatsAppMessage(
|
||||
items: OrderLine[],
|
||||
copy: CartDrawerCopy,
|
||||
language: Language
|
||||
): string | null {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
const lines = formatOrderLinesForMessage(items);
|
||||
const total = calculateOrderTotal(items);
|
||||
const suggestedTime = getSuggestedPickupTime(language);
|
||||
|
||||
return `${copy.messageHello}
|
||||
|
||||
${copy.messageIntro}
|
||||
|
||||
${lines}
|
||||
${copy.messageConfirm.replace('{time}', suggestedTime)}
|
||||
|
||||
${copy.messageTotal}: ${total} kr
|
||||
|
||||
${copy.messageThanks}`;
|
||||
}
|
||||
|
||||
export function buildBookingWhatsAppMessage(
|
||||
booking: BookingDetails,
|
||||
preOrder: OrderLine[],
|
||||
locationCopy: BookingLocationCopy
|
||||
): string | null {
|
||||
if (!isBookingComplete(booking)) return null;
|
||||
|
||||
const locationLabel =
|
||||
booking.location === 'askim' ? locationCopy.askim : locationCopy.backaplan;
|
||||
|
||||
const itemsText =
|
||||
preOrder.length > 0
|
||||
? formatOrderLinesForMessage(preOrder).replace(/ — /g, ' — ')
|
||||
: 'None';
|
||||
|
||||
const totalText =
|
||||
preOrder.length > 0 ? `\nPre-order total: ${calculateOrderTotal(preOrder)} kr\n` : '';
|
||||
|
||||
return `Hello Shahi Kitchen 👋
|
||||
|
||||
Table Reservation Inquiry:
|
||||
|
||||
Name: ${booking.name}
|
||||
Phone: ${booking.phone}
|
||||
Email: ${booking.email || 'N/A'}
|
||||
Location: ${locationLabel}
|
||||
Date: ${booking.date}
|
||||
Time: ${booking.time}
|
||||
Guests: ${booking.guests}
|
||||
Special Requests: ${booking.notes || 'None'}
|
||||
|
||||
Pre-ordered items:
|
||||
${itemsText}${totalText}
|
||||
Please confirm table availability and pre-order.
|
||||
|
||||
Thank you!`;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { Branch } from '@/domain/table/table-id';
|
||||
import type { OrderLine } from '@/domain/shared/order-line';
|
||||
|
||||
export interface TableOrderPayload {
|
||||
branch: Branch;
|
||||
tableNumber: string;
|
||||
tableIdentifier: string;
|
||||
items: OrderLine[];
|
||||
notes?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export function buildTableOrderPayload(
|
||||
branch: Branch,
|
||||
tableNumber: string,
|
||||
tableIdentifier: string,
|
||||
items: OrderLine[],
|
||||
notes?: string
|
||||
): TableOrderPayload {
|
||||
return {
|
||||
branch,
|
||||
tableNumber,
|
||||
tableIdentifier,
|
||||
items,
|
||||
notes: notes?.trim() || undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user