Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Video 4 #31

Open
wants to merge 1 commit into
base: video/3-create-routes
Choose a base branch
from
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
41 changes: 41 additions & 0 deletions src/lib/components/Search.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script lang="ts">
import Search from '$lib/svg/Search.svelte';

export let value = '';
export let disabled = false;
</script>

<form
action="/search"
method="GET"
class="form"
on:submit={(e) => {
if (disabled) {
e.preventDefault();
}
}}
>
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<Search class="h-4 w-4 text-gray-500" aria-hidden="true" />
</div>
<input
type="search"
id="q"
name="q"
placeholder="Poulet-frites, Raclette, Pizza, Repas de Noël..."
required
aria-label="Rechercher un plat"
aria-describedby="search-help"
class="!p-4 !pl-10 !pr-32"
{value}
/>
<p class="sr-only" id="search-help">
Votre recherche peut également contenir une demande, comme par exemple "Repas de Noël sans
gluten".
</p>
<button type="submit" class="btn | absolute right-2.5 bottom-2.5 px-4 py-2 w-fit" {disabled}>
Rechercher
</button>
</div>
</form>
7 changes: 7 additions & 0 deletions src/lib/server/GPT.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import OpenAI from 'openai';

import { OPENAI_API_KEY } from '$env/static/private';

export const openai = new OpenAI({
apiKey: OPENAI_API_KEY,
});
11 changes: 5 additions & 6 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<script lang="ts">
import Search from '$lib/components/Search.svelte';
</script>

<h1 class="h1">CookConnect</h1>

<div class="max-w-xl mx-auto w-full">
<input
type="search"
class="w-full border border-gray-300 rounded-lg px-4 py-2.5 focus:outline-none"
aria-label="Rechercher une recette"
placeholder="Rechercher une recette"
/>
<Search />
</div>
38 changes: 24 additions & 14 deletions src/routes/recipes/[slug]/+layout.server.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import { error } from '@sveltejs/kit';

import { RECIPES } from '$lib/server/db';
import { db } from '$lib/server/db';

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

export const load = (async ({ params }) => {
const recipe = RECIPES.find((definedRecipe) => definedRecipe === params.slug);
try {
const recipe = await db.recipe.findUniqueOrThrow({
where: {
slug: params.slug,
},
});

if (!recipe) {
error(404, "Cette recette n'existe pas.");
}
return {
recipe,
};
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e.code === 'P2025') {
error(404, {
message: "Cette recette n'existe pas.",
});
}
}

// eslint-disable-next-line no-console
console.error('Error while loading recipe page:', e);

return {
recipe: {
dish: recipe,
slug: recipe,
ingredients: [],
steps: [],
shoppingList: [],
},
};
throw e;
}
}) satisfies LayoutServerLoad;
9 changes: 8 additions & 1 deletion src/routes/recipes/[slug]/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { jsonValueToArray } from '$lib/utils/json';

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

export const load = (async ({ parent }) => {
const { recipe } = await parent();

return {
recipe,
recipe: {
...recipe,
ingredients: jsonValueToArray(recipe.ingredients),
steps: jsonValueToArray(recipe.steps),
shoppingList: jsonValueToArray(recipe.shoppingList),
},
isFavourite: false,
};
}) satisfies PageServerLoad;
2 changes: 1 addition & 1 deletion src/routes/recipes/[slug]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
</div>

<section class="container mx-auto space-y-4">
<p class="mb-8 text-lg text-gray-500 text-center md:text-xl">Description...</p>
<p class="mb-8 text-lg text-gray-500 text-center md:text-xl">{data.recipe.description}</p>
<div class="grid gap-6 md:grid-cols-3">
<div>
<div class="mb-2 text-lg font-semibold text-gray-900 flex gap-1 items-center">
Expand Down
23 changes: 22 additions & 1 deletion src/routes/recipes/[slug]/accompaniments/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
import { openai } from '$lib/server/GPT';
import { jsonValueToArray } from '$lib/utils/json';

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

export const load = (async ({ parent }) => {
const { recipe } = await parent();

const getAccompaniments = async () => {
const prompt = `
Tu es Carlos, un assistant culinaire du site "CookConnect".
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"].
Tu peux me donner au maximum 10 accompagnements.
`;

const result = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [{ role: 'system', content: prompt }],
stream: false,
});

return jsonValueToArray(JSON.parse(result.choices[0].message.content ?? '')).slice(0, 10);
};

return {
accompaniments: getAccompaniments(),
recipe,
accompaniments: [] as string[],
};
}) satisfies PageServerLoad;
57 changes: 39 additions & 18 deletions src/routes/recipes/[slug]/accompaniments/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,31 +1,52 @@
<script lang="ts">
import { cubicOut } from 'svelte/easing';
import { fade, type FadeParams } from 'svelte/transition';

import Loader from '$lib/components/Loader.svelte';
import { prefersReducedMotion } from '$lib/utils/preferences';

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

export let data: PageData;

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

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

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

{#if data.accompaniments.length > 0}
<p class="sr-only" role="status">
{data.accompaniments.length} accompagnement{data.accompaniments.length > 1 ? 's' : ''} trouvé{data
.accompaniments.length > 1
? 's'
: ''} pour "{data.recipe.dish}".
{#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}
{: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.
</p>
<ol class="space-y-1 text-gray-500 list-decimal list-inside w-fit mx-auto">
{#each data.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}
{/await}

<a href="/recipes/{data.recipe.slug}" class="btn">Retour à la recette</a>
</section>
43 changes: 41 additions & 2 deletions src/routes/recipes/[slug]/similar/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,50 @@
import { openai } from '$lib/server/GPT';
import { db } from '$lib/server/db';
import { jsonValueToArray } from '$lib/utils/json';

import type { PageServerLoad } from './$types';
import type { Recipe } from '@prisma/client';

export const load = (async ({ parent }) => {
const { recipe } = await parent();

const getSimilarRecipes = async () => {
const recipes = await db.recipe.findMany({
select: {
dish: true,
slug: true,
description: true,
},
where: {
slug: {
not: {
equals: recipe.slug,
},
},
},
});

const prompt = `
Tu es Carlos, un assistant culinaire du site "CookConnect".
La recette que tu consultes actuellement est "${recipe.dish}".
Je vais te donner une liste de recettes et ton travail est de me donner les recettes qui sont les plus adaptées pour être recommandées en tant que "recettes similaires".
Tu peux me donner entre 0 et 3 recettes.
Je veux le résultat au format JSON, comme suit : ["slug1", "slug2", "slug3"].
Voici la liste des recettes : ${JSON.stringify(recipes)}
`;

const result = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [{ role: 'system', content: prompt }],
stream: false,
});

const slugs = jsonValueToArray(JSON.parse(result.choices[0].message.content ?? ''));

return recipes.filter((similarRecipe) => slugs.includes(similarRecipe.slug)).slice(0, 3);
};

return {
similarRecipes: getSimilarRecipes(),
recipe,
similarRecipes: [] as Recipe[],
};
}) satisfies PageServerLoad;
81 changes: 51 additions & 30 deletions src/routes/recipes/[slug]/similar/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,46 +1,67 @@
<script lang="ts">
import { cubicOut } from 'svelte/easing';
import { fade, type FadeParams } from 'svelte/transition';

import Card from '$lib/components/Card.svelte';
import Loader from '$lib/components/Loader.svelte';
import { prefersReducedMotion } from '$lib/utils/preferences';
import { truncate } from '$lib/utils/string';

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

export let data: PageData;

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

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

<section class="container mx-auto space-y-4">
<h2 class="h2 text-center">Recettes similaires</h2>

{#if data.similarRecipes.length > 0}
<p class="sr-only" role="status">
{data.similarRecipes.length} recette{data.similarRecipes.length > 1 ? 's' : ''} similaire{data
.similarRecipes.length > 1
? 's'
: ''} trouvée{data.similarRecipes.length > 1 ? 's' : ''} pour "{data.recipe.dish}".
{#await data.similarRecipes}
<Loader message="Chargement des recettes similaires..." />
{:then similarRecipes}
{#if similarRecipes.length > 0}
<p class="sr-only" role="status">
{similarRecipes.length} recette{similarRecipes.length > 1 ? 's' : ''} similaire{similarRecipes.length >
1
? 's'
: ''} trouvée{similarRecipes.length > 1 ? 's' : ''} pour "{data.recipe.dish}".
</p>
<ul class="flex flex-col justify-center mx-auto gap-2">
{#each similarRecipes as similarRecipe}
<li>
<Card>
<div class="flex-grow flex flex-col gap-1">
<a
href="/recipes/{similarRecipe.slug}"
class="hover:text-primary-600 focus-visible:text-primary-600 transition-colors motion-reduce:transition-none"
>
<h3 class="h3">
{similarRecipe.dish}
</h3>
</a>
<p class="text-gray-600">{truncate(similarRecipe.description, 100)}</p>
</div>
</Card>
</li>
{/each}
</ul>
{:else}
<p class="text-gray-500 text-center" role="status">
Aucune recette similaire trouvée pour "{data.recipe.dish}".
</p>
{/if}
{:catch}
<p class="text-red-500 text-center" role="status" transition:fade={fadeParams}>
Une erreur est survenue lors du chargement des recettes similaires. Veuillez réessayer plus
tard.
</p>
<ul class="flex flex-col justify-center mx-auto gap-2">
{#each data.similarRecipes as similarRecipe}
<li>
<Card>
<div class="flex-grow flex flex-col gap-1">
<a
href="/recipes/{similarRecipe.slug}"
class="hover:text-primary-600 focus-visible:text-primary-600 transition-colors motion-reduce:transition-none"
>
<h3 class="h3">
{similarRecipe.dish}
</h3>
</a>
<p class="text-gray-600">{truncate(similarRecipe.description, 100)}</p>
</div>
</Card>
</li>
{/each}
</ul>
{:else}
<p class="text-gray-500 text-center" role="status">
Aucune recette similaire trouvée pour "{data.recipe.dish}".
</p>
{/if}
{/await}

<a href="/recipes/{data.recipe.slug}" class="btn">Retour à la recette</a>
</section>
Loading