Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 7 additions & 39 deletions app/api/menu_service.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useQuery, useMutation } from '@tanstack/react-query';
import { rootQueryUrl, menusEndpoint } from '~/root';
import type { DailyMenu, DailyMenuDTO } from '~/interfaces';
import { TAG_ID_MAP } from '~/interfaces';
import { getAccessToken } from '~/api/user_service';
import i18n from '~/i18n';

Expand Down Expand Up @@ -77,23 +76,8 @@ function upsertLocalDraft(
return stored;
}

function convertTagsToObjects(menu: any) {
return {
...menu,
dishes: (menu.dishes || []).map((dish: any) => ({
...dish,
tags: (dish.tags || [])
.map((tag: string) => ({
id: TAG_ID_MAP[tag as keyof typeof TAG_ID_MAP],
value: tag,
}))
.filter((t: any) => t.id),
})),
};
}

function getUiLanguageCode() {
return (i18n.language || 'en').split('-')[0].toUpperCase();
return (i18n.language || 'en').split('-')[0].toLowerCase();
}

function mapLocalizedDishes(rawDishes: any[]): DailyMenu['dishes'] {
Expand All @@ -106,7 +90,7 @@ function mapLocalizedDishes(rawDishes: any[]): DailyMenu['dishes'] {
image: dishData?.image || '',
category: dishData?.category || 'MAIN_COURSE',
price: String(item?.price ?? dishData?.price ?? '0'),
tags: Array.isArray(dishData?.tags) ? dishData.tags : [],
allergens: Array.isArray(dishData?.allergens) ? dishData.allergens : [],
} as any;
});
}
Expand Down Expand Up @@ -196,35 +180,19 @@ export const useUpdateDailyMenuWithToken = (token: string) =>
}

const currentLanguage = getUiLanguageCode();
const body = JSON.stringify(convertTagsToObjects(menu));
console.log('[updateMenu] sending body:', body);

// Try POST first (create), fall back to PUT (update) if 405
let response = await fetch(`${rootQueryUrl}/${menusEndpoint}/${restaurantId}`, {
method: 'POST',
const response = await fetch(`${rootQueryUrl}/${menusEndpoint}/${restaurantId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
'Accept-Language': currentLanguage,
},
body,
body: JSON.stringify(menu),
});

if (response.status === 405) {
response = await fetch(`${rootQueryUrl}/${menusEndpoint}/${restaurantId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
'Accept-Language': currentLanguage,
},
body,
});
}

if (!response.ok) {
const text = await response.text().catch(() => '');
console.log('[updateMenu] error response:', text);
throw new Error(`Failed to update menu (${response.status}): ${text}`);
}

Expand Down Expand Up @@ -403,7 +371,7 @@ export const menuService = {
name: dish.name,
category: dish.category ?? 'MAIN_COURSE',
price: typeof dish.price === 'number' ? dish.price : Number(dish.price ?? 0),
tags: (dish as any).tags?.map((t: any) => t.value ?? t) ?? [],
allergens: dish.allergens ?? [],
})),
};
} catch {
Expand Down Expand Up @@ -475,7 +443,7 @@ export const menuService = {
'Content-Type': 'application/json',
'Accept-Language': currentLanguage,
},
body: JSON.stringify(convertTagsToObjects(menu)),
body: JSON.stringify(menu),
});

if (!response.ok) {
Expand Down
21 changes: 1 addition & 20 deletions app/api/restaurant_service.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import { rootQueryUrl, allRestaurantsEndpoint } from '~/root';
import { apiGet, useApiPut } from '~/api/api';
import type {
Restaurant,
RankedRestaurant,
RestaurantDetailsDTO,
UpdateRestaurantRequest,
} from '~/interfaces';
import type { Restaurant, RestaurantDetailsDTO, UpdateRestaurantRequest } from '~/interfaces';

export let useGetAllRestaurants = () =>
apiGet<Restaurant[]>('restaurants', `${rootQueryUrl}/${allRestaurantsEndpoint}`);

export let useGetRestaurantRecommendations = (token: string | undefined) =>
useQuery<RankedRestaurant[]>({
queryKey: ['restaurantRecommendations', token],
queryFn: async () => {
const response = await fetch(`${rootQueryUrl}/${allRestaurantsEndpoint}/recommendation`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) throw new Error(`API Error: ${response.status}`);
return response.json() as Promise<RankedRestaurant[]>;
},
enabled: !!token,
});

export let useGetRestaurantDetails = (id: string) =>
apiGet<RestaurantDetailsDTO>(
`restaurant-${id}`,
Expand Down
40 changes: 0 additions & 40 deletions app/api/user_service.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { rootQueryUrl } from '~/root';
import type { TagValue, TagType } from '~/interfaces';

export interface RestaurantListDTO {
id: string;
Expand Down Expand Up @@ -105,42 +104,3 @@ export const useGetFirstOwnedRestaurant = (token: string | undefined) => {
enabled: isBrowser && !!token,
});
};

export type PreferenceType = 'INCLUDE' | 'EXCLUDE';

export interface UserPreferenceDTO {
tagValue: TagValue;
tagType: TagType;
preferenceType: PreferenceType;
}

export const useGetPreferences = (token: string | undefined) =>
useQuery<UserPreferenceDTO[]>({
queryKey: ['userPreferences'],
queryFn: async () => {
const response = await fetch(`${rootQueryUrl}/api/users`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) throw new Error('Failed to fetch preferences');
return response.json();
},
enabled: !!token,
});

export async function savePreferences(
token: string,
preferences: Array<{ tagValue: TagValue; preferenceType: PreferenceType }>
): Promise<void> {
const response = await fetch(`${rootQueryUrl}/api/users/preferences`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ preferences }),
});
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new Error(`Failed to save preferences (${response.status}): ${text}`);
}
}
28 changes: 17 additions & 11 deletions app/components/menu/dish/dish_info_box.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import type { Dish } from '~/interfaces';
import { Milk, Nut, Wheat, Leaf, Vegan } from 'lucide-react';
import { Beef, Milk, Nut, Wheat, CirclePile } from 'lucide-react';
import type { ComponentType } from 'react';

const TAG_ICON_MAP: Record<string, { icon: ComponentType<{ size?: number }>; label: string }> = {
GLUTEN: { icon: Wheat, label: 'Gluten' },
LACTOSE: { icon: Milk, label: 'Lactose' },
NUTS: { icon: Nut, label: 'Nuts' },
VEGAN: { icon: Vegan, label: 'Vegan' },
VEGETARIAN: { icon: Leaf, label: 'Vegetarian' },
};
const ALLERGEN_ICON_MAP: Record<string, { icon: ComponentType<{ size?: number }>; label: string }> =
{
GLUTEN: { icon: Wheat, label: 'Gluten' },
LACTOSE: { icon: Milk, label: 'Lactose' },
MEAT: { icon: Beef, label: 'Meat' },
NUTS: { icon: Nut, label: 'Nuts' },
SESAME: { icon: CirclePile, label: 'Sesame' },
};

export function DishInfo({ dish }: { dish: Dish }) {
const tagIcons = (dish.tags ?? []).map(tag => TAG_ICON_MAP[tag.value]).filter(Boolean);
const allergenIcons = (dish.allergens ?? [])
.map(allergen => {
const key = allergen.toUpperCase();
return ALLERGEN_ICON_MAP[key];
})
.filter(Boolean);

return (
<div className="flex w-full items-start justify-between gap-4 p-4 rounded-2xl border border-gray-200 dark:border-zinc-700 bg-white/95 dark:bg-zinc-900 shadow-sm hover:shadow-md transition-all">
Expand All @@ -22,8 +28,8 @@ export function DishInfo({ dish }: { dish: Dish }) {
</div>

<div className="mt-3 flex items-center gap-2 text-[#009DE0] dark:text-[#28b9f7]">
{tagIcons.length > 0 ? (
tagIcons.map(({ icon: Icon, label }) => (
{allergenIcons.length > 0 ? (
allergenIcons.map(({ icon: Icon, label }) => (
<span
key={`${dish.id}-${label}`}
title={label}
Expand Down
53 changes: 32 additions & 21 deletions app/components/menu/dish_list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,15 @@ export function DishListComponent({
{ value: 'glutenFree', label: t('filters.glutenFree'), icon: <WheatOff size={16} /> },
];

const dishFilterStrings = filters as string[];
// map UI filter values to the dish_filters expected strings
const filterValueMap: Record<FilterValue, string> = {
vegan: 'vegan',
vegetarian: 'vegetarian',
lactoseFree: 'lactose-free',
glutenFree: 'gluten-free',
};

const dishFilterStrings = filters.map(f => filterValueMap[f]);
const filteredByAllergens: Dish[] = filterDishes(dishes, dishFilterStrings);
const normalizedQuery = query.trim().toLowerCase();
const filteredDishes = filteredByAllergens.filter(dish => {
Expand Down Expand Up @@ -95,26 +103,29 @@ export function DishListComponent({
className="!bg-white dark:!bg-black border-gray-300 dark:border-zinc-700 shadow-sm"
/>

<div className="overflow-x-auto overflow-y-hidden">
<div className="flex gap-2 min-w-max pr-1">
{filterOptions.map(option => {
const active = filters.includes(option.value);
return (
<button
key={option.value}
type="button"
onClick={() => toggleFilter(option.value)}
className={`inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm whitespace-nowrap transition-colors ${
active
? 'border-[#009DE0] bg-sky-50 dark:bg-sky-900/20 text-[#009DE0]'
: 'border-gray-200 dark:border-zinc-700 text-gray-800 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-zinc-800'
}`}
>
<span className="shrink-0">{option.icon}</span>
<span>{option.label}</span>
</button>
);
})}
<div>
<div className="overflow-x-auto overflow-y-hidden">
<div className="flex gap-2 min-w-max pr-1">
{filterOptions.map(option => {
const active = filters.includes(option.value);

return (
<button
key={option.value}
type="button"
onClick={() => toggleFilter(option.value)}
className={`inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm whitespace-nowrap transition-colors ${
active
? 'border-[#009DE0] bg-sky-50 dark:bg-sky-900/20 text-[#009DE0]'
: 'border-gray-200 dark:border-zinc-700 text-gray-800 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-zinc-800'
}`}
>
<span className="shrink-0">{option.icon}</span>
<span>{option.label}</span>
</button>
);
})}
</div>
</div>
</div>
</div>
Expand Down
7 changes: 1 addition & 6 deletions app/components/menu/menu_translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@ type TranslateFn = (key: string) => string;
const ALLERGEN_KEY_MAP: Record<string, string> = {
GLUTEN: 'menuForm.allergenGluten',
LACTOSE: 'menuForm.allergenLactose',
MEAT: 'menuForm.allergenMeat',
NUTS: 'menuForm.allergenNuts',
SESAME: 'menuForm.allergenSesame',
VEGAN: 'filters.vegan',
VEGETARIAN: 'filters.vegetarian',
ITALIAN: 'filters.italian',
POLISH: 'filters.polish',
ASIAN: 'filters.asian',
FAST_FOOD: 'filters.fastFood',
};

const CATEGORY_KEY_MAP: Record<string, string> = {
Expand Down
12 changes: 1 addition & 11 deletions app/components/overview/filter_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,7 @@ import { useTranslation } from 'react-i18next';

import { ToggleGroup, ToggleGroupItem } from '~/shadcn/components/ui/toggle-group';

export type FilterValue =
| 'vegan'
| 'vegetarian'
| 'lactoseFree'
| 'glutenFree'
| 'nutsFree'
| 'hasMenuToday'
| 'italian'
| 'polish'
| 'asian'
| 'fastFood';
export type FilterValue = 'vegan' | 'vegetarian' | 'lactoseFree' | 'glutenFree' | 'hasMenuToday';

export interface FilterBarProps {
value: FilterValue[];
Expand Down
Loading
Loading