Skip to content
Open
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
11 changes: 10 additions & 1 deletion src/app.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { Auth as AuthType } from '$lib/server/auth';
import type { PageData as AccompanimentsPageData } from './routes/recipes/[slug]/accompaniments/$types';
import type { PageData as SimilarRecipesPageData } from './routes/recipes/[slug]/similar/$types';
import type { PrismaClient } from '@prisma/client';
import type { AuthRequest, Session } from 'lucia';

Expand All @@ -24,7 +26,14 @@ declare global {
prompt: string;
};
}
// interface PageState {}
interface PageState {
accompaniments?: Omit<AccompanimentsPageData, 'accompaniments'> & {
accompaniments: Awaited<AccompanimentsPageData['accompaniments']>;
};
similarRecipes?: Omit<SimilarRecipesPageData, 'similarRecipes'> & {
similarRecipes: Awaited<SimilarRecipesPageData['similarRecipes']>;
};
}
// interface Platform {}
}

Expand Down
5 changes: 5 additions & 0 deletions src/lib/types/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type PreloadedPageData<T> = {
type: string;
status: number;
data: T;
};
208 changes: 201 additions & 7 deletions src/routes/recipes/[slug]/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,41 +1,211 @@
<script lang="ts">
import { onMount } from 'svelte';
import { quintOut } from 'svelte/easing';
import { crossfade, fade } from 'svelte/transition';

import Modal from '$lib/components/Modal.svelte';
import Clipboard from '$lib/svg/Clipboard.svelte';
import Close from '$lib/svg/Close.svelte';
import Facebook from '$lib/svg/Facebook.svelte';
import Reddit from '$lib/svg/Reddit.svelte';
import Spinner from '$lib/svg/Spinner.svelte';
import StarEmpty from '$lib/svg/StarEmpty.svelte';
import StarFull from '$lib/svg/StarFull.svelte';
import Twitter from '$lib/svg/Twitter.svelte';
import Warning from '$lib/svg/Warning.svelte';
import { copyToClipboard } from '$lib/utils/clipboard';
import { prefersReducedMotion } from '$lib/utils/preferences';
import { toasts } from '$lib/utils/toats';

import AccompanimentsResult from './accompaniments/Result.svelte';
import SimilarRecipesResult from './similar/Result.svelte';

import type { PreloadedPageData } from '$lib/types/preload';
import type { PageData } from './$types';
import type { PageData as AccompanimentsPageData } from './accompaniments/$types';
import type { PageData as SimilarRecipesPageData } from './similar/$types';

import { browser } from '$app/environment';
import { enhance } from '$app/forms';
import { goto, preloadData, pushState } from '$app/navigation';
import { page } from '$app/stores';

export let data: PageData;

let isIngredientsWarningOpen = true;

// Constants
const [send, receive] = crossfade({
duration: 700,
easing: quintOut,
});
const facebook = encodeURI(`https://www.facebook.com/sharer/sharer.php`);
const reddit = encodeURI(`https://www.reddit.com/submit`);
const twitter = encodeURI(`https://twitter.com/intent/tweet`);
const facebook = encodeURI(
`https://www.facebook.com/sharer/sharer.php?u=\n\n${$page.url}&quote=${
data.recipe.dish
}\n\n* ${data.recipe.shoppingList.join('\n* ')}&hashtag=recette,listedecourse`,
);
const reddit = encodeURI(
`https://www.reddit.com/submit?url=${$page.url}&title=${
data.recipe.dish
}&text=* ${data.recipe.shoppingList.join('\n* ')}`,
);
const twitter = encodeURI(
`https://twitter.com/intent/tweet?url=\n\n${$page.url}&text=${
data.recipe.dish
}\n\n* ${data.recipe.shoppingList.join('\n* ')}&hashtags=recette,listedecourse`,
);

// Variables
let accompanimentsLoading = false;
let similarRecipesLoading = false;
let isIngredientsWarningOpen = true;
let accompanimentsButton: HTMLAnchorElement | null = null;
let similarRecipesButton: HTMLAnchorElement | null = null;

// Computed
$: starKey = data.isFavourite ? 'full' : 'empty';

// Functions
const closeIngredientsWarning = () => {
isIngredientsWarningOpen = false;
};

const showAccompaniments = async (e: MouseEvent & { currentTarget: HTMLAnchorElement }) => {
if (e.metaKey || e.ctrlKey || accompanimentsLoading) {
return;
}

e.preventDefault();

accompanimentsLoading = true;

try {
const { href } = e.currentTarget;
const result = (await preloadData(href)) as PreloadedPageData<AccompanimentsPageData>;

if (result.type === 'loaded' && result.status === 200) {
pushState(href, {
accompaniments: {
...result.data,
accompaniments: await result.data.accompaniments,
},
});
} else {
goto(href);
}
} catch (error) {
toasts.error('Impossible de charger les accompagnements personnalisés. Veuillez réessayer.');
}

accompanimentsLoading = false;
};

const showSimilarRecipes = async (e: MouseEvent & { currentTarget: HTMLAnchorElement }) => {
if (e.metaKey || e.ctrlKey || similarRecipesLoading) {
return;
}

e.preventDefault();

similarRecipesLoading = true;

try {
const { href } = e.currentTarget;
const result = (await preloadData(href)) as PreloadedPageData<SimilarRecipesPageData>;

if (result.type === 'loaded' && result.status === 200) {
pushState(href, {
similarRecipes: {
...result.data,
similarRecipes: await result.data.similarRecipes,
},
});
} else {
goto(href);
}
} catch (error) {
toasts.error('Impossible de charger les recettes similaires. Veuillez réessayer.');
}

similarRecipesLoading = false;
};

const closeCurrentModal = () => {
if (!browser) {
return;
}

window.history.back();
};

onMount(() => {
if (accompanimentsButton) {
accompanimentsButton.setAttribute('aria-haspopup', 'dialog');
}

if (similarRecipesButton) {
similarRecipesButton.setAttribute('aria-haspopup', 'dialog');
}
});
</script>

<Modal
id="accompaniments"
title="Accompagnements"
description="Liste des accompagnements personnalisés pour cette recette."
open={!!$page.state.accompaniments}
on:close={closeCurrentModal}
>
{#if !!$page.state.accompaniments}
<AccompanimentsResult
dish={data.recipe.dish}
accompaniments={$page.state.accompaniments.accompaniments}
/>

<div class="flex gap-2 justify-end mt-4">
{#if $page.state.accompaniments.accompaniments.length > 0}
<button
type="button"
class="btn mx-0"
on:click={() => {
if (!$page.state.accompaniments) {
return;
}

copyToClipboard($page.state.accompaniments.accompaniments.join('\n'), {
successMessage: 'Liste des accompagnements copiée dans le presse-papier.',
failureMessage:
'Impossible de copier la liste des accompagnements dans le presse-papier.',
accessDeniedMessage:
"Vous devez autoriser l'accès au presse-papier pour copier la liste des accompagnements.",
});
}}
>
Copier la liste
</button>
{/if}
<button type="button" class="btn mx-0" on:click={closeCurrentModal}>Fermer</button>
</div>
{/if}
</Modal>

<Modal
id="similar"
title="Recettes similaires"
description="Liste des recettes similaires à celle-ci."
open={!!$page.state.similarRecipes}
on:close={closeCurrentModal}
>
{#if !!$page.state.similarRecipes}
<SimilarRecipesResult
dish={data.recipe.dish}
similarRecipes={$page.state.similarRecipes.similarRecipes}
/>

<div class="flex gap-2 justify-end mt-4">
<button type="button" class="btn mx-0" on:click={closeCurrentModal}>Fermer</button>
</div>
{/if}
</Modal>

<div class="mb-4" role="alert">
{#await data.disallowedIngredients then disallowedIngredients}
{#if disallowedIngredients && isIngredientsWarningOpen}
Expand Down Expand Up @@ -248,10 +418,34 @@
<h2 class="h2 text-center">Aller plus loin</h2>

<div class="gap-2.5 grid items-center justify-items-center">
<a href="/recipes/{data.recipe.slug}/accompaniments" class="btn | mx-0 text-center">
<a
href="/recipes/{data.recipe.slug}/accompaniments"
class="btn | mx-0 text-center"
class:opacity-50={accompanimentsLoading}
class:cursor-not-allowed={accompanimentsLoading}
aria-disabled={accompanimentsLoading ? 'true' : 'false'}
bind:this={accompanimentsButton}
on:click={showAccompaniments}
>
{#if accompanimentsLoading}
<Spinner />
{/if}

Demander des accompagnements personnalisés
</a>
<a href="/recipes/{data.recipe.slug}/similar" class="btn | mx-0 text-center">
<a
href="/recipes/{data.recipe.slug}/similar"
class="btn | mx-0 text-center"
class:opacity-50={similarRecipesLoading}
class:cursor-not-allowed={similarRecipesLoading}
aria-disabled={similarRecipesLoading ? 'true' : 'false'}
bind:this={similarRecipesButton}
on:click={showSimilarRecipes}
>
{#if similarRecipesLoading}
<Spinner />
{/if}

Trouver des recettes similaires
</a>
</div>
Expand Down
13 changes: 9 additions & 4 deletions src/routes/recipes/[slug]/accompaniments/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { redirect } from '@sveltejs/kit';

import { openai } from '$lib/server/GPT';
import { jsonValueToArray } from '$lib/utils/json';
import { openai } from '$lib/server/GPT.js';
import { jsonValueToArray } from '$lib/utils/json.js';

import type { PageServerLoad } from './$types';
import type { PageServerLoad } from './$types.js';

export const load = (async ({ parent, locals }) => {
export const load = (async ({ locals, parent }) => {
const { session } = locals;

if (!session) {
Expand All @@ -20,6 +20,11 @@ export const load = (async ({ parent, locals }) => {
La recette actuellement consultée est "${recipe.dish}".
Ton travail consiste à me donner une liste d'accompagnements pour cette recette.
Je veux le résultat au format JSON, comme suit : ["accompagnement1", "accompagnement2", "accompagnement3"].
${
session.user.disallowedIngredients
? `Attention, tu dois me donner des accompagnements qui ne contiennent pas ce genre d'ingrédients : ${session.user.disallowedIngredients}.`
: ''
}
Tu peux me donner au maximum 10 accompagnements.
`;

Expand Down
24 changes: 4 additions & 20 deletions src/routes/recipes/[slug]/accompaniments/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import Loader from '$lib/components/Loader.svelte';
import { prefersReducedMotion } from '$lib/utils/preferences';

import Result from './Result.svelte';

import type { PageData } from './$types';

export let data: PageData;
Expand All @@ -17,31 +19,13 @@

<h1 class="h1">{data.recipe.dish}</h1>

<section class="container mx-auto space-y-4">
<section class="container mx-auto space-y-4" aria-live="polite">
<h2 class="h2 text-center">Accompagnements personnalisés</h2>

{#await data.accompaniments}
<Loader message="Chargement des accompagnements..." />
{:then accompaniments}
{#if accompaniments.length > 0}
<p class="sr-only" role="status">
{accompaniments.length} accompagnement{accompaniments.length > 1 ? 's' : ''} trouvé{accompaniments.length >
1
? 's'
: ''} pour "{data.recipe.dish}".
</p>
<ol class="space-y-1 text-gray-500 list-decimal list-inside w-fit mx-auto">
{#each accompaniments as accompaniment}
<li>
<span class="text-gray-900">{accompaniment}</span>
</li>
{/each}
</ol>
{:else}
<p class="text-gray-500 text-center" role="status">
Aucun accompagnement trouvé pour "{data.recipe.dish}".
</p>
{/if}
<Result dish={data.recipe.dish} {accompaniments} />
{:catch}
<p class="text-red-500 text-center" role="status" transition:fade={fadeParams}>
Une erreur est survenue lors du chargement des accompagnements. Veuillez réessayer plus tard.
Expand Down
37 changes: 37 additions & 0 deletions src/routes/recipes/[slug]/accompaniments/Result.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script lang="ts">
import { cubicOut } from 'svelte/easing';
import { fade, type FadeParams } from 'svelte/transition';

import { prefersReducedMotion } from '$lib/utils/preferences';

export let dish: string;
export let accompaniments: string[] = [];

const fadeParams: FadeParams = {
duration: prefersReducedMotion() ? 0 : 300,
easing: cubicOut,
};
</script>

{#if accompaniments.length > 0}
<p class="sr-only" role="status">
{accompaniments.length} accompagnement{accompaniments.length > 1 ? 's' : ''} trouvé{accompaniments.length >
1
? 's'
: ''} pour "{dish}".
</p>
<ol
class="space-y-1 text-gray-500 list-decimal list-inside w-fit mx-auto"
transition:fade={fadeParams}
>
{#each accompaniments as accompaniment}
<li>
<span class="text-gray-900">{accompaniment}</span>
</li>
{/each}
</ol>
{:else}
<p class="text-gray-500 text-center" role="status" transition:fade={fadeParams}>
Aucun accompagnement trouvé pour "{dish}".
</p>
{/if}
Loading