Skip to content

Commit 4bdeb70

Browse files
authored
Add new recipe (#34)
* Refactor data handling in Recipe, Login, and index components to simplify response processing * Add new recipe icon and update header for logged-in users * Implement useAuth composable for managing logged-in state and create new recipe page * Implement new recipe creation form with validation and submission handling * Prettify code * Prettify code (again) * Enhance recipe creation form with additional fields and improved submission handling * Update recipe form to use number inputs for cooking and preparation times with validation
1 parent cd7caa1 commit 4bdeb70

File tree

4 files changed

+209
-12
lines changed

4 files changed

+209
-12
lines changed

assets/icons/new-section.svg

Lines changed: 3 additions & 0 deletions
Loading

components/Header.vue

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
</nuxt-link>
1010
</li>
1111
<li v-else>
12-
<nuxt-link to="/login" title="Login"><IconAccount /></nuxt-link>
12+
<nuxt-link to="/login" title="Login">
13+
<IconAccount />
14+
</nuxt-link>
15+
</li>
16+
<li v-if="isLoggedIn">
17+
<nuxt-link to="/new-recipe" title="New Recipe">
18+
<IconNewRecipe />
19+
</nuxt-link>
1320
</li>
1421
<li>
1522
<nuxt-link to="/contact" title="Contact Us">
@@ -26,31 +33,25 @@
2633
</template>
2734

2835
<script setup>
29-
import { ref, watchEffect } from 'vue'
3036
import { useRouter } from 'vue-router'
31-
import { useCookie } from '#app'
37+
import { useAuth } from '@/composables/useAuth'
3238
import ButtonHome from './ButtonHome.vue'
3339
import IconAccount from '@/assets/icons/user.svg'
3440
import IconContact from '@/assets/icons/mail-question.svg'
41+
import IconNewRecipe from '@/assets/icons/new-section.svg'
3542
import IconPrivacy from '@/assets/icons/privacy-policy.svg'
3643
3744
const {
3845
public: { appTitle },
3946
} = useRuntimeConfig()
40-
const isLoggedIn = ref(false)
41-
42-
// Monitor the refresh_token cookie
43-
const userCookie = useCookie('refresh_token')
4447
45-
// Update isLoggedIn when the cookie changes
46-
watchEffect(() => {
47-
isLoggedIn.value = !!userCookie.value
48-
})
48+
// Use the composable for logged-in state
49+
const { isLoggedIn } = useAuth()
4950
5051
// Ensure the header reacts to route changes
5152
const router = useRouter()
5253
router.afterEach(() => {
53-
isLoggedIn.value = !!userCookie.value
54+
isLoggedIn.value = !!useCookie('refresh_token').value
5455
})
5556
</script>
5657

composables/useAuth.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ref, watchEffect } from 'vue'
2+
import { useCookie } from '#app'
3+
4+
export function useAuth() {
5+
const isLoggedIn = ref(false)
6+
const userCookie = useCookie('refresh_token')
7+
8+
// Update isLoggedIn when the cookie changes
9+
watchEffect(() => {
10+
isLoggedIn.value = !!userCookie.value
11+
})
12+
13+
return { isLoggedIn }
14+
}

pages/new-recipe.vue

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<template>
2+
<div v-if="isLoggedIn">
3+
<h1>Create a New Recipe</h1>
4+
<form @submit.prevent="submitRecipe">
5+
<div>
6+
<label for="title">Title:</label>
7+
<input type="text" id="title" v-model="recipe.title" required />
8+
</div>
9+
<div>
10+
<label for="ingredients">Ingredients:</label>
11+
<textarea
12+
id="ingredients"
13+
v-model="recipe.ingredients"
14+
required
15+
></textarea>
16+
</div>
17+
<div>
18+
<label for="instructions">Instructions:</label>
19+
<textarea
20+
id="instructions"
21+
v-model="recipe.instructions"
22+
required
23+
></textarea>
24+
</div>
25+
<div>
26+
<label for="preparationTime">Preparation Time:</label>
27+
<input
28+
type="number"
29+
id="preparationTime"
30+
v-model="recipe.preparationTime"
31+
min="0"
32+
max="4320"
33+
/>
34+
</div>
35+
<div>
36+
<label for="cookingTime">Cooking Time:</label>
37+
<input
38+
type="number"
39+
id="cookingTime"
40+
v-model="recipe.cookingTime"
41+
min="1"
42+
max="4320"
43+
/>
44+
</div>
45+
<div>
46+
<label for="servings">Servings:</label>
47+
<input
48+
type="number"
49+
id="servings"
50+
v-model="recipe.servings"
51+
min="1"
52+
max="10"
53+
/>
54+
</div>
55+
<div>
56+
<label for="difficulty">Difficulty:</label>
57+
<select id="difficulty" v-model="recipe.difficulty">
58+
<option value="easy">Easy</option>
59+
<option value="medium">Medium</option>
60+
<option value="hard">Hard</option>
61+
</select>
62+
</div>
63+
<div>
64+
<label for="source">Source:</label>
65+
<input type="text" id="source" v-model="recipe.source" />
66+
</div>
67+
<div>
68+
<label for="language">Language:</label>
69+
<select id="language" v-model="recipe.language">
70+
<!-- TODO: Request languages list to API -->
71+
<option value="en">English</option>
72+
<option value="fr">Français</option>
73+
</select>
74+
</div>
75+
<div>
76+
<label for="nutritionalInfo">Nutritional Info:</label>
77+
<textarea
78+
id="nutritionalInfo"
79+
v-model="recipe.nutritionalInfo"
80+
></textarea>
81+
</div>
82+
83+
<button type="submit">Create Recipe</button>
84+
</form>
85+
</div>
86+
<div v-else>
87+
<p>Please log in to create a new recipe.</p>
88+
<nuxt-link to="/login">Login</nuxt-link>
89+
</div>
90+
</template>
91+
92+
<script setup>
93+
import { useAuth } from '@/composables/useAuth'
94+
95+
const {
96+
public: { apiBaseUrl },
97+
} = useRuntimeConfig()
98+
const { isLoggedIn } = useAuth()
99+
100+
const recipe = ref({
101+
title: '',
102+
ingredients: '',
103+
instructions: '',
104+
preparationTime: '',
105+
cookingTime: '',
106+
servings: 1,
107+
difficulty: 'easy',
108+
source: '',
109+
language: 'en',
110+
nutritionalInfo: '',
111+
})
112+
113+
const submitRecipe = async () => {
114+
if (!isLoggedIn.value) {
115+
console.error('User is not logged in')
116+
return
117+
}
118+
119+
const accessToken = useCookie('access_token')?.value
120+
121+
if (!accessToken) {
122+
console.error('Missing access token')
123+
return
124+
}
125+
126+
const payload = {
127+
author_id: 1, // TODO: Fix the API to use the session token
128+
picture_id: 1,
129+
cook_time:
130+
parseInt(recipe.value.preparationTime || '0') +
131+
parseInt(recipe.value.cookingTime || '0'),
132+
recipe_source: recipe.value.source,
133+
language_iso_code: recipe.value.language,
134+
title: recipe.value.title,
135+
details: recipe.value.ingredients,
136+
preparation: recipe.value.instructions,
137+
nutritional_information: recipe.value.nutritionalInfo,
138+
}
139+
140+
console.log('Payload:', payload)
141+
console.log('Access token:', accessToken)
142+
console.log('API Base URL:', apiBaseUrl)
143+
144+
try {
145+
const response = await fetch(`${apiBaseUrl}/recipe`, {
146+
method: 'POST',
147+
headers: {
148+
'Content-Type': 'application/json',
149+
Authorization: `Bearer ${accessToken}`,
150+
},
151+
body: JSON.stringify(payload),
152+
})
153+
154+
console.log('Response status:', response.status)
155+
const text = await response.text()
156+
console.log('Raw response text:', text)
157+
158+
if (!response.ok) {
159+
throw new Error(`Failed to create recipe: ${response.statusText}`)
160+
}
161+
162+
const data = await response.json()
163+
console.log('Recipe created:', data)
164+
// TODO: router.push(`/recipe/${data.recipe_id}`)
165+
} catch (error) {
166+
console.error('Error creating recipe:', error)
167+
}
168+
}
169+
</script>
170+
171+
<style scoped>
172+
div {
173+
align-items: center;
174+
display: flex;
175+
flex-direction: column;
176+
gap: var(--spacing-medium);
177+
justify-content: center;
178+
}
179+
</style>

0 commit comments

Comments
 (0)