Skip to content

Commit

Permalink
Added ingredients to edit page and made types more restrictive
Browse files Browse the repository at this point in the history
  • Loading branch information
VaiTon committed Jan 26, 2024
1 parent 578a140 commit b8808c1
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 80 deletions.
64 changes: 29 additions & 35 deletions src/lib/api/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,8 @@ export class ProductsApi {
}

async getProductReducedForCard(barcode: string): Promise<ProductState<ProductReduced>> {
const fields = ['product_name', 'code', 'image_front_small_url', 'brands'];

const params = new URLSearchParams({
fields: fields.join(','),
fields: REDUCED_FIELDS.join(','),
lc: get(preferences).lang
});

Expand Down Expand Up @@ -97,38 +95,25 @@ export type ProductStateBase = {
};
};

export type ProductStateFailure = ProductStateBase & {
status: 'failure';
errors: {
field: { id: string; value: string };
impact: { lc_name: string; name: string; id: string };
message: { lc_name: string; name: string; id: string };
}[];
};

export type ProductStateFound<T = Product> = ProductStateBase & {
product: T;
};

export type ProductStateSuccess<T = Product> = ProductStateFound<T> & {
status: 'success';
export type ProductStateError = {
field: { id: string; value: string };
impact: { lc_name: string; name: string; id: string };
message: { lc_name: string; name: string; id: string };
};

export type ProductStateSuccessWithWarnings<T = Product> = ProductStateFound<T> & {
status: 'success_with_warnings';
warnings: object[];
export type ProductStateFailure = ProductStateBase & {
status: 'failure';
errors: ProductStateError[];
};

export type ProductStateSuccessWithErrors<T = Product> = ProductStateFound<T> & {
status: 'success_with_errors';
errors: object[];
};
export type ProductStateFound<T = Product> = ProductStateBase & { product: T } & (
| { status: 'success' }
| { status: 'success_with_warnings'; warnings: object[] }
| { status: 'success_with_errors'; errors: ProductStateError[] }
);

export type ProductState<T = Product> =
| ProductStateSuccess<T>
| ProductStateSuccessWithWarnings<T>
| ProductStateSuccessWithErrors<T>
| ProductStateFailure;
export type ProductState<T = Product> = ProductStateBase &
(ProductStateFound<T> | ProductStateFailure);

export type ProductSearch<T = Product> = {
count: number;
Expand All @@ -139,6 +124,8 @@ export type ProductSearch<T = Product> = {
skip: number;
};

type LangIngredient = `ingredients_text_${string}`;

export type Product = {
knowledge_panels: Record<string, KnowledgePanel>;
product_name: string;
Expand All @@ -159,9 +146,11 @@ export type Product = {
}[];
additives_tags: string[];

ingredients_text: string;
[lang: LangIngredient]: string;

image_front_url: string;
image_front_small_url: string;
image_front_thumb_url: string;

image_ingredients_url: string;
image_ingredients_small_url: string;
Expand Down Expand Up @@ -192,10 +181,15 @@ export type Product = {
nutriments: Nutriments;
};

export type ProductReduced = Pick<
Product,
'image_front_small_url' | 'code' | 'product_name' | 'brands'
>;
const REDUCED_FIELDS = [
'image_front_small_url',
'code',
'product_name',
'brands',
'quantity'
] as const;

export type ProductReduced = Pick<Product, (typeof REDUCED_FIELDS)[number]>;

/** @deprecated */
export async function getProductReducedForCard(
Expand Down
2 changes: 1 addition & 1 deletion src/lib/knowledgepanels/KnowledgePanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
/>
{:else}
<img
class="mr-4 h-8 w-8 rounded-full dark:invert"
class="mr-4 h-8 w-8 rounded-md bg-white object-contain"
src={panel.title_element.icon_url}
alt={panel.title_element.title}
/>
Expand Down
7 changes: 6 additions & 1 deletion src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
export let data: PageData;
</script>

<svelte:head>
<!-- Preconnect to static assets -->
<link rel="preconnect" href="https://images.openfoodfacts.org" crossorigin="anonymous" />
</svelte:head>

<div class="container mx-auto my-4 flex flex-col items-center xl:max-w-6xl">
<Card>
<div class="card-body items-center text-center">
Expand Down Expand Up @@ -59,7 +64,7 @@
{state.product.product_name ?? state.product.code}
</p>
<p class="mt-2 text-sm font-light">
{state.product.brands}
{state.product.brands} - {state.product.quantity}
</p>
</div>
</div>
Expand Down
43 changes: 25 additions & 18 deletions src/routes/+page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
getProductReducedForCard,
ProductsApi,
type ProductReduced,
type ProductState,
type ProductStateFound
Expand All @@ -23,34 +23,41 @@ async function productsWithQuestions(
'https://robotoff.openfoodfacts.org/api/v1/questions?' + new URLSearchParams({ count: count })
).then((it) => it.json());

const productApi = new ProductsApi(fetch);

const productsPromises = response.questions.map((question) =>
getProductReducedForCard(question.barcode, fetch)
productApi.getProductReducedForCard(question.barcode)
);

return Promise.all(productsPromises);
}

export const load = (async ({ fetch }) => {
const products = productsWithQuestions(fetch);
function deduplicate<T>(array: T[], key: (el: T) => string): T[] {
const seen = new Set<string>();

return array.filter((el) => {
if (seen.has(key(el))) return false;
else {
seen.add(key(el));
return true;
}
});
}

export const load: PageLoad = async ({ fetch }) => {
const states = productsWithQuestions(fetch);

// filtering out failures
const filteredProducts = products.then((it) =>
it.filter((state) => state.status != 'failure')
) as Promise<ProductStateFound<ProductReduced>[]>;
const filteredProducts: ProductStateFound<ProductReduced>[] = await states.then((states) =>
states.filter(
(state): state is ProductStateFound<ProductReduced> => state.status != 'failure'
)
);

// deduping
const dedupedProducts = filteredProducts.then((it) => {
const seen = new Set<string>();
return it.filter((state) => {
if (seen.has(state.product.code)) return false;
else {
seen.add(state.product.code);
return true;
}
});
});
const dedupedProducts = deduplicate(await filteredProducts, (it) => it.product.code);

return {
streamed: { products: dedupedProducts }
};
}) satisfies PageLoad;
};
7 changes: 3 additions & 4 deletions src/routes/products/[barcode]/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import type { PageLoad } from './$types';
import { type Brand, type Label, getTaxo, type Store, type Category, ProductsApi } from '$lib/api';
import { error } from '@sveltejs/kit';
import { FolksonomyApi } from '$lib/api/folksonomy';
import { PricesApi } from '$lib/api/prices';
import { PricesApi, isConfigured as isPricesConfigured } from '$lib/api/prices';

export const ssr = false;

export const load: PageLoad = async ({ params, fetch }) => {
const productsApi = new ProductsApi(fetch);
const state = await productsApi.getProduct(params.barcode);
if (state.status === 'failure') {
state
error(404, { message: 'Failure to load product', errors: state.errors });
}

Expand All @@ -24,10 +25,8 @@ export const load: PageLoad = async ({ params, fetch }) => {

const pricesApi = new PricesApi(fetch);
let pricesResponse = null;
try {
if (isPricesConfigured()) {
pricesResponse = pricesApi.getPrices({ product_code: params.barcode });
} catch (e) {
console.error('Error fetching prices', e);
}

return {
Expand Down
62 changes: 41 additions & 21 deletions src/routes/products/[barcode]/edit/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<script lang="ts">
import { writable, get } from 'svelte/store';
import { getOrDefault, ProductsApi, type Taxonomy } from '$lib/api';
import { preferences } from '$lib/settings';
import Card from '$lib/ui/Card.svelte';
import type { PageData } from './$types';
import Tags from './Tags.svelte';
import TagsString from './TagsString.svelte';
export let data: PageData;
Expand All @@ -17,53 +20,70 @@
$: labelNames = getNames(data.labels);
$: brandNames = getNames(data.brands);
let newProduct = { ...data.state.product };
let productStore = writable(data.state.product);
async function submit() {
const product = get(productStore);
console.group('Product added/edited');
console.debug('Submitting', newProduct);
const ok = await new ProductsApi(fetch).addOrEditProductV2(newProduct);
console.debug('Submitting', product);
const ok = await new ProductsApi(fetch).addOrEditProductV2(product);
console.debug('Submitted', ok);
console.groupEnd();
if (ok) {
window.location.href = '/products/' + newProduct.code;
window.location.href = '/products/' + product.code;
}
}
$: {
productStore.subscribe((it) => {
console.debug('Product store changed', it);
});
}
</script>

<Card>
<div class="form-control mb-4">
<label for="">Name</label>
<input type="text" class="input input-bordered w-full" bind:value={newProduct.product_name} />
<input
type="text"
class="input input-bordered w-full"
bind:value={$productStore.product_name}
/>
</div>

<div class="form-control mb-4">
<label for="">Quantity</label>
<input type="text" class="input input-bordered w-full" bind:value={newProduct.quantity} />
<input type="text" class="input input-bordered w-full" bind:value={$productStore.quantity} />
</div>

<div class="form-control mb-4">
<label for="">Categories: </label>
<Tags
tags={data.state.product.categories.split(',').filter((c) => c !== '')}
autocomplete={categoryNames}
on:change={(e) => (newProduct.categories = e.detail.tags.join(','))}
/>
<TagsString bind:tagsString={$productStore.categories} autocomplete={categoryNames} />
</div>
<div class="mb-4">
<label for="">Labels</label>
<Tags
tags={data.state.product.labels.split(',').filter((l) => l !== '')}
autocomplete={labelNames}
on:change={(e) => (newProduct.labels = e.detail.tags.join(','))}
/>
<TagsString bind:tagsString={$productStore.labels} autocomplete={labelNames} />
</div>
<div class="mb-4">
<label for="">Brands</label>
<Tags
tags={data.state.product.brands.split(',').filter((l) => l !== '')}
autocomplete={brandNames}
on:change={(e) => (newProduct.brands = e.detail.tags.join(','))}
<TagsString bind:tagsString={$productStore.brands} autocomplete={brandNames} />
</div>
</Card>

<Card>
<h3 class="mb-4 text-3xl font-bold">Ingredients</h3>

{#if $productStore.image_ingredients_url}
<img src={$productStore.image_ingredients_url} alt="Ingredients" class="mb-4" />
{:else}
<p class="alert alert-warning mb-4">No ingredients image</p>
{/if}

<div class="form-control mb-4">
<textarea
class="textarea textarea-bordered h-40 w-full"
bind:value={$productStore.ingredients_text}
/>
</div>
</Card>
Expand Down
14 changes: 14 additions & 0 deletions src/routes/products/[barcode]/edit/TagsString.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts">
import Tags from './Tags.svelte';
export let tagsString: string;
export let separator = ',';
export let autocomplete: readonly string[] = [];
</script>

<Tags
{autocomplete}
tags={tagsString.split(separator).filter((str) => str !== '')}
on:change={(e) => (tagsString = e.detail.tags.join(separator))}
/>

0 comments on commit b8808c1

Please sign in to comment.