Skip to content

Commit be6dd66

Browse files
authored
Merge pull request #2 from gdjy098/feature/favorites
Add recipe favorites
2 parents 8058c7b + 029972a commit be6dd66

6 files changed

Lines changed: 137 additions & 3 deletions

File tree

__tests__/routes.test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,56 @@ describe('Routes', () => {
106106
expect(response.headers.location).toBe('/login');
107107
});
108108

109+
test('POST /recipes/:id/favorite should save a recipe for the user', async () => {
110+
const agent = request.agent(app);
111+
await agent
112+
.post('/register')
113+
.send({ username: 'favuser', password: 'password123' });
114+
115+
await agent
116+
.post('/recipes')
117+
.send({ title: 'Favorite Recipe', ingredients: 'Test', method: 'Test' });
118+
119+
const recipe = await db.get('SELECT * FROM recipes WHERE title = ?', ['Favorite Recipe']);
120+
expect(recipe).toBeDefined();
121+
122+
const response = await agent
123+
.post(`/recipes/${recipe.id}/favorite`)
124+
.send();
125+
126+
expect(response.status).toBe(302);
127+
expect(response.headers.location).toBe(`/recipes/${recipe.id}`);
128+
129+
const favorite = await db.get('SELECT * FROM favorites WHERE recipe_id = ?', [recipe.id]);
130+
expect(favorite).toBeDefined();
131+
});
132+
133+
test('POST /recipes/:id/unfavorite should remove a saved recipe', async () => {
134+
const agent = request.agent(app);
135+
await agent
136+
.post('/register')
137+
.send({ username: 'unfavuser', password: 'password123' });
138+
139+
await agent
140+
.post('/recipes')
141+
.send({ title: 'Unfavorite Recipe', ingredients: 'Test', method: 'Test' });
142+
143+
const recipe = await db.get('SELECT * FROM recipes WHERE title = ?', ['Unfavorite Recipe']);
144+
expect(recipe).toBeDefined();
145+
146+
await agent.post(`/recipes/${recipe.id}/favorite`).send();
147+
148+
const response = await agent
149+
.post(`/recipes/${recipe.id}/unfavorite`)
150+
.send();
151+
152+
expect(response.status).toBe(302);
153+
expect(response.headers.location).toBe(`/recipes/${recipe.id}`);
154+
155+
const favorite = await db.get('SELECT * FROM favorites WHERE recipe_id = ?', [recipe.id]);
156+
expect(favorite).toBeUndefined();
157+
});
158+
109159
test('POST /recipes/:id/delete should delete a recipe', async () => {
110160
// 先插入一条菜谱
111161
const recipe = {

__tests__/test-database.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ async function createTables(db) {
3030
username TEXT NOT NULL UNIQUE,
3131
password_hash TEXT NOT NULL
3232
)`)
33+
34+
// Create the 'favorites' table to link users to recipes
35+
await db.exec(`CREATE TABLE IF NOT EXISTS favorites (
36+
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
37+
user_id INTEGER NOT NULL,
38+
recipe_id INTEGER NOT NULL,
39+
UNIQUE(user_id, recipe_id)
40+
)`)
3341
}
3442

3543
module.exports = { getTestDbConnection, initializeTestDb }

src/database/schema.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ async function createTables(db) {
1414
username TEXT NOT NULL UNIQUE,
1515
password_hash TEXT NOT NULL
1616
)`)
17+
18+
// Create the 'favorites' table to link users to recipes
19+
await db.exec(`CREATE TABLE IF NOT EXISTS favorites (
20+
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
21+
user_id INTEGER NOT NULL,
22+
recipe_id INTEGER NOT NULL,
23+
UNIQUE(user_id, recipe_id)
24+
)`)
1725
}
1826

1927
module.exports = { createTables }

src/routes.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,17 @@ router.get('/recipes/:id', async (req, res) => {
2525
const db = await getDbConnection()
2626
const recipeId = req.params.id
2727
const recipe = await db.get('SELECT * FROM recipes WHERE id = ?', [recipeId])
28-
res.render('recipe', { recipe })
28+
let isFavorited = false
29+
30+
if (req.session.user && recipe) {
31+
const favorite = await db.get(
32+
'SELECT id FROM favorites WHERE user_id = ? AND recipe_id = ?',
33+
[req.session.user.id, recipeId]
34+
)
35+
isFavorited = Boolean(favorite)
36+
}
37+
38+
res.render('recipe', { recipe, isFavorited })
2939
})
3040

3141
router.get('/register', (req, res) => {
@@ -85,7 +95,40 @@ router.post('/logout', (req, res) => {
8595
})
8696

8797
router.get('/profile', requireAuth, async (req, res) => {
88-
res.render('profile', { user: req.session.user })
98+
const db = await getDbConnection()
99+
const favorites = await db.all(
100+
`SELECT recipes.*
101+
FROM recipes
102+
INNER JOIN favorites ON favorites.recipe_id = recipes.id
103+
WHERE favorites.user_id = ?
104+
ORDER BY recipes.title ASC`,
105+
[req.session.user.id]
106+
)
107+
res.render('profile', {
108+
user: req.session.user,
109+
favorites,
110+
hasFavorites: favorites.length > 0,
111+
})
112+
})
113+
114+
router.post('/recipes/:id/favorite', requireAuth, async (req, res) => {
115+
const db = await getDbConnection()
116+
const recipeId = req.params.id
117+
await db.run('INSERT OR IGNORE INTO favorites (user_id, recipe_id) VALUES (?, ?)', [
118+
req.session.user.id,
119+
recipeId,
120+
])
121+
res.redirect(`/recipes/${recipeId}`)
122+
})
123+
124+
router.post('/recipes/:id/unfavorite', requireAuth, async (req, res) => {
125+
const db = await getDbConnection()
126+
const recipeId = req.params.id
127+
await db.run('DELETE FROM favorites WHERE user_id = ? AND recipe_id = ?', [
128+
req.session.user.id,
129+
recipeId,
130+
])
131+
res.redirect(`/recipes/${recipeId}`)
89132
})
90133

91134
router.post('/recipes', async (req, res) => {

views/profile.hbs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,21 @@
33

44
{{#if user}}
55
<p>Signed in as <strong>{{user.username}}</strong>.</p>
6-
<p>Your saved recipes will appear here once the favorites feature is added.</p>
6+
<h2>Saved Recipes</h2>
7+
{{#if hasFavorites}}
8+
<ul class="favorites-list">
9+
{{#each favorites}}
10+
<li class="favorite-item">
11+
<a href="/recipes/{{id}}">{{title}}</a>
12+
<form action="/recipes/{{id}}/unfavorite" method="POST" style="display:inline; margin-left: 10px;">
13+
<button type="submit" class="btn btn-secondary">Remove</button>
14+
</form>
15+
</li>
16+
{{/each}}
17+
</ul>
18+
{{else}}
19+
<p>You have not saved any recipes yet.</p>
20+
{{/if}}
721
{{else}}
822
<p>Please <a href="/login">login</a> to view your profile.</p>
923
{{/if}}

views/recipe.hbs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@
55
<div class="recipe-actions">
66
<button class="btn btn-primary" onclick="showEditForm()">Edit Recipe</button>
77
<a href="/recipes" class="btn btn-secondary">Back to All Recipes</a>
8+
{{#if currentUser}}
9+
{{#if isFavorited}}
10+
<form action="/recipes/{{recipe.id}}/unfavorite" method="POST" style="display:inline;">
11+
<button type="submit" class="btn btn-secondary">Remove Favorite</button>
12+
</form>
13+
{{else}}
14+
<form action="/recipes/{{recipe.id}}/favorite" method="POST" style="display:inline;">
15+
<button type="submit" class="btn btn-primary">Save Favorite</button>
16+
</form>
17+
{{/if}}
18+
{{/if}}
819
<form action="/recipes/{{recipe.id}}/delete" method="POST" style="display:inline;">
920
<button type="submit" class="btn btn-danger" onclick="return confirm('确定要删除该菜谱吗?')">Delete</button>
1021
</form>

0 commit comments

Comments
 (0)