Skip to content

Commit e79bc41

Browse files
committed
fix ui bugs, scale ingredients
1 parent d11fa22 commit e79bc41

File tree

8 files changed

+495
-328
lines changed

8 files changed

+495
-328
lines changed

src/lib/components/RecipeCard.svelte

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import type { IRecipe } from '$lib/types';
33
import RecipeCategoryTag from '$lib/components/RecipeCategoryTag.svelte';
4+
import Icon from '@iconify/svelte';
45
56
interface Props {
67
recipe: IRecipe;
@@ -24,7 +25,23 @@
2425
class="bg-center bg-no-repeat bg-cover w-full h-48"
2526
></div>
2627
<div class="p-3">
27-
<h3>{recipe.data.title}</h3>
28+
<h3 class="mb-2">{recipe.data.title}</h3>
29+
{#if recipe.data.total_time || recipe.data.nutrients?.calories}
30+
<div class="flex flex-wrap gap-x-3 gap-y-1 text-xs text-gray-500 dark:text-gray-400">
31+
{#if recipe.data.total_time}
32+
<div class="flex items-center gap-1">
33+
<Icon icon="ph:clock" width="0.9rem" />
34+
<span>{recipe.data.total_time} min</span>
35+
</div>
36+
{/if}
37+
{#if recipe.data.nutrients?.calories}
38+
<div class="flex items-center gap-1">
39+
<Icon icon="ph:fire" width="0.9rem" />
40+
<span>{recipe.data.nutrients.calories}</span>
41+
</div>
42+
{/if}
43+
</div>
44+
{/if}
2845
</div>
2946
</a>
3047
{#if !hideCategoriesTags}

src/lib/components/Toggle.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
interface Props {
33
label: string;
44
checked?: boolean;
5+
title?: string;
56
}
67
7-
let { label, checked = $bindable(false) }: Props = $props();
8+
let { label, checked = $bindable(false), title }: Props = $props();
89
</script>
910

10-
<label class="inline-flex items-center cursor-pointer">
11+
<label class="inline-flex items-center cursor-pointer" {title}>
1112
<input type="checkbox" value="" class="sr-only peer" bind:checked />
1213
<div
1314
class={`

src/routes/(app)/categories/+page.svelte

Lines changed: 169 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
2020
let showCategoriesDropdown = $state(false);
2121
let showTagsDropdown = $state(false);
22+
let showNutritionFilters = $state(false);
23+
24+
let activeNutritionCount = $derived(
25+
[data.minCalories, data.maxCalories, data.minProtein, data.maxProtein].filter(Boolean).length
26+
);
2227
2328
const sortOptions = [
2429
{ label: 'Name (A-Z)', value: '' },
@@ -68,161 +73,178 @@
6873
<div class="text-sm text-gray-500 dark:text-gray-400">{data.recipes.total} recipes</div>
6974
</div>
7075

71-
<div class="flex flex-col md:flex-row md:flex-wrap gap-3">
72-
<div class="relative">
73-
<button
74-
bind:this={categoriesBtnEl}
75-
onclick={() => (showCategoriesDropdown = !showCategoriesDropdown)}
76-
class="p-2 border rounded-sm hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2 justify-between w-full md:w-auto"
77-
>
78-
<div class="whitespace-nowrap overflow-hidden text-left w-full md:w-60 2xl:w-72">
79-
{data.selectedCategories.join(', ')}
80-
</div>
81-
<Icon icon="ph:caret-down" color="#000" width="1.2rem" />
82-
</button>
83-
{#if categoriesBtnEl}
84-
<div
85-
class="absolute left-0 right-0 w-full top-full bg-white dark:bg-gray-900 shadow-lg flex flex-col rounded-sm border z-10 max-h-96 overflow-auto"
86-
class:hidden={!showCategoriesDropdown}
87-
use:clickOutside={[categoriesBtnEl]}
88-
onclickoutside={() => {
89-
showCategoriesDropdown = false;
90-
}}
76+
<div class="flex flex-col gap-3">
77+
<div class="flex flex-col md:flex-row md:flex-wrap gap-3">
78+
<div class="relative">
79+
<button
80+
bind:this={categoriesBtnEl}
81+
onclick={() => (showCategoriesDropdown = !showCategoriesDropdown)}
82+
class="p-2 border rounded-sm hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2 justify-between w-full md:w-auto"
9183
>
92-
{#each data.categories as category}
93-
<a
94-
href={`?${new URLSearchParams({
95-
...Object.fromEntries($storePage.url.searchParams),
96-
page: '1',
97-
categories: ($storePage.url.searchParams.get('categories') || '')
98-
.split(',')
99-
.includes(category.slug)
100-
? ($storePage.url.searchParams.get('categories') || '')
101-
.split(',')
102-
.filter((c) => c !== category.slug)
103-
.join(',')
104-
: [
105-
...($storePage.url.searchParams.get('categories') || '').split(','),
106-
category.slug
107-
]
108-
.filter((c) => c)
109-
.join(',')
110-
})
111-
.toString()
112-
.trim()}`}
113-
class={`flex justify-between items-center w-full p-3 hover:bg-gray-100 dark:hover:bg-gray-800 first:rounded-t last:rounded-b ${data.selectedCategoriesSlugs.includes(category.slug) ? ' bg-gray-100 dark:bg-gray-800' : ''}`}
114-
>
115-
<span>{category.name}</span>
116-
<input
117-
type="checkbox"
118-
class="w-4 h-4 hover:cursor-pointer pointer-events-none"
119-
checked={data.selectedCategoriesSlugs.includes(category.slug)}
120-
/>
121-
</a>
122-
{/each}
123-
</div>
124-
{/if}
125-
</div>
84+
<div class="whitespace-nowrap overflow-hidden text-left w-full md:w-60 2xl:w-72">
85+
{data.selectedCategories.join(', ')}
86+
</div>
87+
<Icon icon="ph:caret-down" color="#000" width="1.2rem" />
88+
</button>
89+
{#if categoriesBtnEl}
90+
<div
91+
class="absolute left-0 right-0 w-full top-full bg-white dark:bg-gray-900 shadow-lg flex flex-col rounded-sm border z-10 max-h-96 overflow-auto"
92+
class:hidden={!showCategoriesDropdown}
93+
use:clickOutside={[categoriesBtnEl]}
94+
onclickoutside={() => {
95+
showCategoriesDropdown = false;
96+
}}
97+
>
98+
{#each data.categories as category}
99+
<a
100+
href={`?${new URLSearchParams({
101+
...Object.fromEntries($storePage.url.searchParams),
102+
page: '1',
103+
categories: ($storePage.url.searchParams.get('categories') || '')
104+
.split(',')
105+
.includes(category.slug)
106+
? ($storePage.url.searchParams.get('categories') || '')
107+
.split(',')
108+
.filter((c) => c !== category.slug)
109+
.join(',')
110+
: [
111+
...($storePage.url.searchParams.get('categories') || '').split(','),
112+
category.slug
113+
]
114+
.filter((c) => c)
115+
.join(',')
116+
})
117+
.toString()
118+
.trim()}`}
119+
class={`flex justify-between items-center w-full p-3 hover:bg-gray-100 dark:hover:bg-gray-800 first:rounded-t last:rounded-b ${data.selectedCategoriesSlugs.includes(category.slug) ? ' bg-gray-100 dark:bg-gray-800' : ''}`}
120+
>
121+
<span>{category.name}</span>
122+
<input
123+
type="checkbox"
124+
class="w-4 h-4 hover:cursor-pointer pointer-events-none"
125+
checked={data.selectedCategoriesSlugs.includes(category.slug)}
126+
/>
127+
</a>
128+
{/each}
129+
</div>
130+
{/if}
131+
</div>
132+
133+
<div class="relative">
134+
<button
135+
bind:this={tagsBtnEl}
136+
onclick={() => (showTagsDropdown = !showTagsDropdown)}
137+
class="p-2 border rounded-sm hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2 justify-between w-full md:w-auto"
138+
>
139+
<div class="whitespace-nowrap overflow-hidden text-left w-full md:w-60 2xl:w-72">
140+
{data.selectedTags.join(', ')}
141+
</div>
142+
<Icon icon="ph:caret-down" color="#000" width="1.2rem" />
143+
</button>
144+
{#if tagsBtnEl}
145+
<div
146+
class="absolute left-0 right-0 w-full top-full bg-white dark:bg-gray-900 shadow-lg flex flex-col rounded-sm border z-10 max-h-96 overflow-auto"
147+
class:hidden={!showTagsDropdown}
148+
use:clickOutside={[tagsBtnEl]}
149+
onclickoutside={() => {
150+
showTagsDropdown = false;
151+
}}
152+
>
153+
{#each data.tags as tag}
154+
<a
155+
href={`?${new URLSearchParams({
156+
...Object.fromEntries($storePage.url.searchParams),
157+
page: '1',
158+
tags: ($storePage.url.searchParams.get('tags') || '')
159+
.split(',')
160+
.includes(tag.slug)
161+
? ($storePage.url.searchParams.get('tags') || '')
162+
.split(',')
163+
.filter((c) => c !== tag.slug)
164+
.join(',')
165+
: [...($storePage.url.searchParams.get('tags') || '').split(','), tag.slug]
166+
.filter((c) => c)
167+
.join(',')
168+
})
169+
.toString()
170+
.trim()}`}
171+
class={`flex justify-between items-center w-full p-3 hover:bg-gray-100 dark:hover:bg-gray-800 first:rounded-t last:rounded-b ${data.selectedTagsSlugs.includes(tag.slug) ? ' bg-gray-100 dark:bg-gray-800' : ''}`}
172+
>
173+
<span>{tag.name}</span>
174+
<input
175+
type="checkbox"
176+
class="w-4 h-4 hover:cursor-pointer pointer-events-none"
177+
checked={data.selectedTagsSlugs.includes(tag.slug)}
178+
/>
179+
</a>
180+
{/each}
181+
</div>
182+
{/if}
183+
</div>
184+
185+
<select
186+
value={data.selectedSort}
187+
onchange={onSortChange}
188+
class="p-2 border rounded-sm hover:bg-gray-100 dark:hover:bg-gray-800 dark:bg-gray-900 bg-white"
189+
>
190+
{#each sortOptions as opt}
191+
<option value={opt.value}>{opt.label}</option>
192+
{/each}
193+
</select>
126194

127-
<div class="relative">
128195
<button
129-
bind:this={tagsBtnEl}
130-
onclick={() => (showTagsDropdown = !showTagsDropdown)}
131-
class="p-2 border rounded-sm hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2 justify-between w-full md:w-auto"
196+
onclick={() => (showNutritionFilters = !showNutritionFilters)}
197+
class="p-2 border rounded-sm hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2 whitespace-nowrap"
132198
>
133-
<div class="whitespace-nowrap overflow-hidden text-left w-full md:w-60 2xl:w-72">
134-
{data.selectedTags.join(', ')}
135-
</div>
136-
<Icon icon="ph:caret-down" color="#000" width="1.2rem" />
199+
<Icon icon="ph:funnel" width="1.2rem" />
200+
Filters{#if activeNutritionCount > 0}&nbsp;<span
201+
class="bg-blue-500 text-white text-xs rounded-full px-1.5 py-0.5"
202+
>{activeNutritionCount}</span
203+
>{/if}
137204
</button>
138-
{#if tagsBtnEl}
139-
<div
140-
class="absolute left-0 right-0 w-full top-full bg-white dark:bg-gray-900 shadow-lg flex flex-col rounded-sm border z-10 max-h-96 overflow-auto"
141-
class:hidden={!showTagsDropdown}
142-
use:clickOutside={[tagsBtnEl]}
143-
onclickoutside={() => {
144-
showTagsDropdown = false;
145-
}}
146-
>
147-
{#each data.tags as tag}
148-
<a
149-
href={`?${new URLSearchParams({
150-
...Object.fromEntries($storePage.url.searchParams),
151-
page: '1',
152-
tags: ($storePage.url.searchParams.get('tags') || '')
153-
.split(',')
154-
.includes(tag.slug)
155-
? ($storePage.url.searchParams.get('tags') || '')
156-
.split(',')
157-
.filter((c) => c !== tag.slug)
158-
.join(',')
159-
: [...($storePage.url.searchParams.get('tags') || '').split(','), tag.slug]
160-
.filter((c) => c)
161-
.join(',')
162-
})
163-
.toString()
164-
.trim()}`}
165-
class={`flex justify-between items-center w-full p-3 hover:bg-gray-100 dark:hover:bg-gray-800 first:rounded-t last:rounded-b ${data.selectedTagsSlugs.includes(tag.slug) ? ' bg-gray-100 dark:bg-gray-800' : ''}`}
166-
>
167-
<span>{tag.name}</span>
168-
<input
169-
type="checkbox"
170-
class="w-4 h-4 hover:cursor-pointer pointer-events-none"
171-
checked={data.selectedTagsSlugs.includes(tag.slug)}
172-
/>
173-
</a>
174-
{/each}
175-
</div>
176-
{/if}
177205
</div>
178206

179-
<select
180-
value={data.selectedSort}
181-
onchange={onSortChange}
182-
class="p-2 border rounded-sm hover:bg-gray-100 dark:hover:bg-gray-800 dark:bg-gray-900 bg-white"
183-
>
184-
{#each sortOptions as opt}
185-
<option value={opt.value}>{opt.label}</option>
186-
{/each}
187-
</select>
188-
189-
<div class="flex items-center gap-2">
190-
<input
191-
type="number"
192-
placeholder="Min cal"
193-
value={data.minCalories}
194-
onchange={(e) => onNutritionChange('minCalories', e)}
195-
class="p-2 border rounded-sm w-24 dark:bg-gray-900 bg-white"
196-
min="0"
197-
/>
198-
<input
199-
type="number"
200-
placeholder="Max cal"
201-
value={data.maxCalories}
202-
onchange={(e) => onNutritionChange('maxCalories', e)}
203-
class="p-2 border rounded-sm w-24 dark:bg-gray-900 bg-white"
204-
min="0"
205-
/>
206-
</div>
207+
{#if showNutritionFilters}
208+
<div class="flex flex-wrap gap-3">
209+
<div class="flex items-center gap-2">
210+
<input
211+
type="number"
212+
placeholder="Min cal"
213+
value={data.minCalories}
214+
onchange={(e) => onNutritionChange('minCalories', e)}
215+
class="p-2 border rounded-sm w-24 dark:bg-gray-900 bg-white"
216+
min="0"
217+
/>
218+
<input
219+
type="number"
220+
placeholder="Max cal"
221+
value={data.maxCalories}
222+
onchange={(e) => onNutritionChange('maxCalories', e)}
223+
class="p-2 border rounded-sm w-24 dark:bg-gray-900 bg-white"
224+
min="0"
225+
/>
226+
</div>
207227

208-
<div class="flex items-center gap-2">
209-
<input
210-
type="number"
211-
placeholder="Min prot"
212-
value={data.minProtein}
213-
onchange={(e) => onNutritionChange('minProtein', e)}
214-
class="p-2 border rounded-sm w-24 dark:bg-gray-900 bg-white"
215-
min="0"
216-
/>
217-
<input
218-
type="number"
219-
placeholder="Max prot"
220-
value={data.maxProtein}
221-
onchange={(e) => onNutritionChange('maxProtein', e)}
222-
class="p-2 border rounded-sm w-24 dark:bg-gray-900 bg-white"
223-
min="0"
224-
/>
225-
</div>
228+
<div class="flex items-center gap-2">
229+
<input
230+
type="number"
231+
placeholder="Min prot"
232+
value={data.minProtein}
233+
onchange={(e) => onNutritionChange('minProtein', e)}
234+
class="p-2 border rounded-sm w-24 dark:bg-gray-900 bg-white"
235+
min="0"
236+
/>
237+
<input
238+
type="number"
239+
placeholder="Max prot"
240+
value={data.maxProtein}
241+
onchange={(e) => onNutritionChange('maxProtein', e)}
242+
class="p-2 border rounded-sm w-24 dark:bg-gray-900 bg-white"
243+
min="0"
244+
/>
245+
</div>
246+
</div>
247+
{/if}
226248
</div>
227249
</div>
228250

0 commit comments

Comments
 (0)