Replace entire repo content with kottgard-production-v1.1.zip (for shahikitchen-prod repo)

This commit is contained in:
root
2026-06-29 15:04:08 +00:00
parent 6cd5aaa048
commit 57cc67d2d3
850 changed files with 26149 additions and 15588 deletions
-28
View File
@@ -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);
}
-122
View File
@@ -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';
-45
View File
@@ -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)
);
});
}
-13
View File
@@ -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';
-4
View File
@@ -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(),
};
}