Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
fca7f3b
feat: add R01.test.js and unit-test-helpers.js
Carrot7712 Jan 13, 2022
96da4be
feat:add R02.test.js
Carrot7712 Jan 13, 2022
37322b4
feat:add R03.test.js
Carrot7712 Jan 13, 2022
ad796af
feat:add R04.test.js
Carrot7712 Jan 13, 2022
4bce0c5
add R05.test.js
AmberYen Mar 2, 2022
7f6510a
fix R01.test.js comment typo
tuterwell Aug 10, 2023
dfab553
add handlebars
Aug 17, 2023
ba46ef5
add index page
Aug 17, 2023
9b4968f
add admin index page
Aug 17, 2023
4fd03ba
add user model
Aug 22, 2023
b6fdd61
user signup
Aug 22, 2023
c1c6544
flash msg & signup verification
Aug 22, 2023
ff8e5e0
passport init & signin
Aug 22, 2023
be12f34
add header & footer
Aug 24, 2023
cd76f19
user model add is_admin
Aug 24, 2023
e5715d7
add restaurant model
Aug 25, 2023
9d8c64a
modify admin restaurants page
Aug 25, 2023
29ac0f5
create restaurant
Aug 25, 2023
dab6528
admin restaurant page
Aug 25, 2023
1319467
admin update restaurant
Aug 25, 2023
3e2be90
delete restaurant
Aug 25, 2023
ac5c095
add restaurant image
Aug 25, 2023
cd6c73d
add seed data
Aug 26, 2023
96a8c03
feat: modify config for heroku
Aug 26, 2023
040912b
feat: add imgur api
Aug 26, 2023
108843e
main merge with R01
Aug 27, 2023
ad88695
add category model
Aug 28, 2023
75db895
update seed files
Aug 28, 2023
3846eca
show category on admin restaurant pages
Aug 29, 2023
4bc10c0
add categories selector on admin create and edit page
Aug 29, 2023
2c3d7a8
add ifCond hbs helper & update category selector
Aug 29, 2023
0e7fe29
add admin categories page
Aug 30, 2023
bad2bc4
category create
Aug 30, 2023
40ef344
category update
Aug 30, 2023
1b4655e
category delete
Aug 30, 2023
bca0f17
add restaurants index page
Aug 30, 2023
9616cab
add restaurant page
Aug 30, 2023
aad534d
餐廳資訊整理:Dashboard
Aug 31, 2023
97cb74c
add categories navbar on restaurants index page
Sep 1, 2023
efc9314
add pagination on restaurants index page
Sep 1, 2023
8cdab6d
add comment model
Sep 2, 2023
b3a9e58
add post comment on restaurant page
Sep 2, 2023
d78e736
show comments on restaurant page
Sep 2, 2023
2f96422
add delete comment for admin
Sep 2, 2023
21ef382
merge R03 branch (user profile)
Sep 3, 2023
39df685
add imgur api to userController
Sep 3, 2023
488e15e
add table name to comment model
Sep 3, 2023
6789860
add feeds page
Sep 3, 2023
0e0e570
create favorite model
Sep 4, 2023
bc18859
add favorite/unfavorite button at index page
Sep 4, 2023
c7c779f
get user's favorited restaurants & switch button
Sep 4, 2023
e030110
add Like / Unlike function
Sep 4, 2023
1aea0b5
deal user's comment length in controller instead of view
Sep 6, 2023
b04c176
avoid other user to get in '/users/:id/edit'
Sep 6, 2023
e8b1d06
create followship model
Sep 6, 2023
2f9da4e
add topUser page
Sep 6, 2023
cc4b392
addFollowing & removeFollowing function
Sep 6, 2023
78d7cca
Merge remote-tracking branch 'origin/R05-test' into R05
Sep 6, 2023
c3d5b7a
addFollowing & removeFollowing restaurant function on 10-top restaura…
Sep 6, 2023
76ab9cb
modify "userToRender.commentsLength" for test
Sep 12, 2023
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
IMGUR_CLIENT_ID=
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: NODE_ENV=production node app.js
31 changes: 30 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
const path = require('path')
if (process.env.NODE_ENV !== 'production') {
require('dotenv').config()
}
const express = require('express')
const routes = require('./routes')

const app = express()
const port = process.env.PORT || 3000
const db = require('./models')
const handlebars = require('express-handlebars')
const flash = require('connect-flash')
const methodOverride = require('method-override')
const session = require('express-session')
const passport = require('./config/passport')
const handlebarsHelpers = require('./helpers/handlebars-helpers') // 引入 handlebars-helpers
const { getUser } = require('./helpers/auth-helpers') // 引入自定義的 auth-helpers
const SESSION_SECRET = 'secret'

// 註冊 Handlebars 樣板引擎,並指定副檔名為 .hbs
app.engine('hbs', handlebars({ extname: '.hbs', helpers: handlebarsHelpers }))
// 設定使用 Handlebars 做為樣板引擎
app.set('view engine', 'hbs')
app.use(express.urlencoded({ extended: true }))
app.use(session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false }))
app.use(passport.initialize()) // 初始化 Passport
app.use(passport.session()) // 啟動 session 功能
app.use(flash()) // 掛載套件
app.use(methodOverride('_method'))
app.use('/upload', express.static(path.join(__dirname, 'upload')))
app.use((req, res, next) => {
res.locals.success_messages = req.flash('success_messages') // 設定 success_msg 訊息
res.locals.error_messages = req.flash('error_messages') // 設定 warning_msg 訊息
res.locals.user = getUser(req)
next()
})
app.use(routes)

app.listen(port, () => {
Expand Down
6 changes: 1 addition & 5 deletions config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
"dialect": "mysql"
},
"production": {
"username": "root",
"password": null,
"database": "database_production",
"host": "127.0.0.1",
"dialect": "mysql"
"use_env_variable": "MYSQL_DATABASE_URL"
},
"travis": {
"username": "travis",
Expand Down
41 changes: 41 additions & 0 deletions config/passport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const passport = require('passport')
const LocalStrategy = require('passport-local')
const bcrypt = require('bcryptjs')
const { User, Restaurant } = require('../models')
// set up Passport strategy
passport.use(new LocalStrategy(
// customize user field
{
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
},
// authenticate user
(req, email, password, cb) => {
User.findOne({ where: { email } })
.then(user => {
if (!user) return cb(null, false, req.flash('error_messages', '帳號或密碼輸入錯誤!'))
bcrypt.compare(password, user.password).then(res => {
if (!res) return cb(null, false, req.flash('error_messages', '帳號或密碼輸入錯誤!'))
return cb(null, user)
})
})
}
))
// serialize and deserialize user
passport.serializeUser((user, cb) => {
cb(null, user.id)
})
passport.deserializeUser((id, cb) => {
return User.findByPk(id, {
include: [
{ model: Restaurant, as: 'FavoritedRestaurants' },
{ model: Restaurant, as: 'LikedRestaurants' },
{ model: User, as: 'Followers' },
{ model: User, as: 'Followings' }
]
})
.then(user => cb(null, user.toJSON()))
.catch(err => cb(err))
})
module.exports = passport
165 changes: 165 additions & 0 deletions controllers/admin-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
const { Restaurant, User, Category } = require('../models')
const { imgurFileHandler } = require('../helpers/file-helpers')
const adminController = {
getRestaurants: (req, res, next) => {
Restaurant.findAll({
raw: true,
nest: true,
include: [Category]
})
.then(restaurants => res.render('admin/restaurants', { restaurants }))
.catch(err => next(err))
},
createRestaurant: (req, res, next) => {
return Category.findAll({
raw: true
})
.then(categories => res.render('admin/create-restaurant', { categories }))
.catch(err => next(err))
},
postRestaurant: (req, res, next) => {
const { name, tel, address, openingHours, description, categoryId } = req.body // 從 req.body 拿出表單裡的資料
if (!name) throw new Error('Restaurant name is required!') // name 是必填,若發先是空值就會終止程式碼,並在畫面顯示錯誤提示
const { file } = req // 把檔案取出來,也可以寫成 const file = req.file
return imgurFileHandler(file)// 把取出的檔案傳給 file-helper 處理後
.then(filePath => Restaurant.create({ // 再 create 這筆餐廳資料
name,
tel,
address,
openingHours,
description,
categoryId,
image: filePath || null
}))
.then(() => {
req.flash('success_messages', 'restaurant was successfully created')
res.redirect('/admin/restaurants')
})
.catch(err => next(err))
},
getRestaurant: (req, res, next) => {
Restaurant.findByPk(req.params.id, { // 去資料庫用 id 找一筆資料
raw: true, // 找到以後整理格式再回傳
nest: true,
include: [Category]
})
.then(restaurant => {
if (!restaurant) throw new Error("Restaurant didn't exist!") // 如果找不到,回傳錯誤訊息,後面不執行
res.render('admin/restaurant', { restaurant })
})
.catch(err => next(err))
},
editRestaurant: (req, res, next) => {
return Promise.all([
Restaurant.findByPk(req.params.id, { raw: true }),
Category.findAll({ raw: true })
])
.then(([restaurant, categories]) => {
if (!restaurant) throw new Error("Restaurant doesn't exist!")
res.render('admin/edit-restaurant', { restaurant, categories })
})
.catch(err => next(err))
},
putRestaurant: (req, res, next) => {
const { name, tel, address, openingHours, description, categoryId } = req.body
if (!name) throw new Error('Restaurant name is required!')
const { file } = req // 把檔案取出來
Promise.all([ // 非同步處理
Restaurant.findByPk(req.params.id), // 去資料庫查有沒有這間餐廳
imgurFileHandler(file) // 把檔案傳到 file-helper 處理
])
.then(([restaurant, filePath]) => { // 以上兩樣事都做完以後
if (!restaurant) throw new Error("Restaurant didn't exist!")
return restaurant.update({ // 修改這筆資料
name,
tel,
address,
openingHours,
description,
categoryId,
image: filePath || restaurant.image // 如果 filePath 是 Truthy (使用者有上傳新照片) 就用 filePath,是 Falsy (使用者沒有上傳新照片) 就沿用原本資料庫內的值
})
})
.then(() => {
req.flash('success_messages', 'restaurant was successfully to update')
res.redirect('/admin/restaurants')
})
.catch(err => next(err))
},
deleteRestaurant: (req, res, next) => {
return Restaurant.findByPk(req.params.id)
.then(restaurant => {
if (!restaurant) throw new Error("Restaurant didn't exist!")
return restaurant.destroy()
})
.then(() => res.redirect('/admin/restaurants'))
.catch(err => next(err))
},
getUsers: (req, res, next) => {
return User.findAll({ raw: true })
.then(users => res.render('admin/users', { users }))
.catch(err => next(err))
},
patchUser: (req, res, next) => {
return User.findByPk(req.params.id)
.then(user => {
if (user.email === 'root@example.com') {
req.flash('error_messages', '禁止變更 root 權限')
return res.redirect('back')
}
return user.update({ isAdmin: !user.isAdmin })
})
.then(() => {
req.flash('success_messages', '使用者權限變更成功')
res.redirect('/admin/users')
})
.catch(err => next(err))
},
getCategories: (req, res, next) => {
return Promise.all([
Category.findAll({ raw: true }),
req.params.id ? Category.findByPk(req.params.id, { raw: true }) : null
])
.then(([categories, category]) => {
res.render('admin/categories', {
categories,
category
})
})
.catch(err => next(err))
},
postCategory: (req, res, next) => {
const name = req.body.category
if (!name) throw new Error('category name is required!')
Category.create({ name })
.then(() => {
req.flash('success_messages', 'category was successfully created')
res.redirect('/admin/categories')
})
.catch(err => next(err))
},
putCategory: (req, res, next) => {
const name = req.body.category
if (!name) throw new Error('category name is required!')
return Category.findByPk(req.params.id)
.then(category => {
return category.update({ name })
})
.then(() => {
req.flash('success_messages', 'category was successfully to update')
res.redirect('/admin/categories')
})
.catch(err => next(err))
},
deleteCategory: (req, res, next) => {
return Category.findByPk(req.params.id)
.then(category => {
if (!category) throw new Error("Category didn't exist!")
return category.destroy()
})
.then(() => res.redirect('/admin/categories'))
.catch(err => next(err))
}
}

module.exports = adminController
122 changes: 122 additions & 0 deletions controllers/restaurant-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
const { Restaurant, Category, Comment, User } = require('../models')
const { getOffset, getPagination } = require('../helpers/pagination-helper')
const restaurantController = {
getRestaurants: (req, res) => {
const DEFAULT_LIMIT = 9
const page = Number(req.query.page) || 1
const limit = Number(req.query.limit) || DEFAULT_LIMIT
const offset = getOffset(limit, page)

const categoryId = Number(req.query.categoryId) || ''
const where = {}
if (categoryId) { where.categoryId = categoryId }
Promise.all([
Restaurant.findAndCountAll({
include: Category,
where,
limit,
offset,
nest: true,
raw: true
}),
Category.findAll({ raw: true })
])
.then(([restaurants, categories]) => {
const favoritedRestaurantsId = req.user && req.user.FavoritedRestaurants.map(fr => fr.id)
const likedRestaurantsId = req.user && req.user.LikedRestaurants.map(lr => lr.id)
const data = restaurants.rows.map(r => ({
...r,
description: r.description.substring(0, 50),
isFavorited: favoritedRestaurantsId.includes(r.id),
isLiked: likedRestaurantsId.includes(r.id)
}))
return res.render('restaurants', {
restaurants: data,
categories,
categoryId,
pagination: getPagination(limit, page, restaurants.count)
})
})
},
getRestaurant: (req, res, next) => {
return Restaurant.findByPk(req.params.id, {
include: [Category,
{ model: Comment, include: User },
{ model: User, as: 'FavoritedUsers' },
{ model: User, as: 'LikedUsers' }
],
nest: true,
raw: false
})
.then(restaurant => {
const isFavorited = restaurant.FavoritedUsers.some(f => f.id === req.user.id)
const isLiked = restaurant.LikedUsers.some(l => l.id === req.user.id)
if (!restaurant) throw new Error("Restaurant didn't exist!")
restaurant.increment('viewCounts')
res.render('restaurant', {
restaurant: restaurant.toJSON(),
isFavorited,
isLiked
})
})
.catch(err => next(err))
},
getDashboard: (req, res, next) => {
return Restaurant.findByPk(req.params.id, {
include: Category,
nest: true,
raw: false
})
.then(restaurant => {
if (!restaurant) throw new Error("Restaurant didn't exist!")
res.render('dashboard', {
restaurant: restaurant.toJSON()
})
})
.catch(err => next(err))
},
getFeeds: (req, res, next) => {
return Promise.all([
Restaurant.findAll({
limit: 10,
order: [['createdAt', 'DESC']],
include: [Category],
raw: true,
nest: true
}),
Comment.findAll({
limit: 10,
order: [['createdAt', 'DESC']],
include: [User, Restaurant],
raw: true,
nest: true
})
])
.then(([restaurants, comments]) => {
res.render('feeds', {
restaurants,
comments
})
})
.catch(err => next(err))
},
getTopRestaurants: (req, res, next) => {
const dataLengthLimit = 10
return Restaurant.findAll({
include: [{ model: User, as: 'FavoritedUsers' }]
})
.then((restaurants=>{
const result = restaurants.map(restaurant=>({
...restaurant.toJSON(),
description: restaurant.description.substring(0, 50),
favoritedCount: restaurant.FavoritedUsers.length,
isFavorited: req.user&&req.user.FavoritedRestaurants.some(f => f.id === restaurant.id)
}))
.sort((a, b) => b.favoritedCount - a.favoritedCount)
const restaurantsToRender = result.slice(0, dataLengthLimit)
res.render('top-restaurants', { restaurants: restaurantsToRender })
}))
.catch(err => next(err))
}
}
module.exports = restaurantController
Loading