Skip to content

Commit 773ec86

Browse files
committed
feat: add membership page and authorization logic
- use riddle to authorize - fix bug in the logic of post creation time - fix bug in the logic of getting post creator username - add group hove to post title
1 parent cc66100 commit 773ec86

12 files changed

+193
-41
lines changed

app.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const indexRouter = require('./routes/indexRouter.js');
88
const loginRouter = require('./routes/loginRouter.js');
99
const registerRouter = require('./routes/registerRouter.js');
1010
const postRouter = require('./routes/postRouter.js');
11+
const membershipRouter = require('./routes/membershipRouter.js');
1112
const customErrors = require('./errors/CustomErrors.js');
1213
const db = require('./db/query.js');
1314
const pool = require('./db/pool.js');
@@ -85,7 +86,8 @@ app.use('/', indexRouter);
8586
app.use('/login', loginRouter);
8687
app.use('/register', registerRouter);
8788
app.use('/post', postRouter);
88-
app.post('/logout', (req, res) => {
89+
app.use('/membership', membershipRouter);
90+
app.get('/logout', (req, res) => {
8991
req.logOut((err) => {
9092
if (err) {
9193
return next(err);

controllers/indexController.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ const indexController = {
77
...res.locals.currentUser
88
};
99
const posts = await db.getPosts();
10-
res.render('index', {user, posts});
10+
const postCreators = await Promise.all(
11+
posts.map(async (post) => await db.findUserById(post.user_id))
12+
);
13+
res.render('index', {user, posts, postCreators});
1114
}),
1215
};
1316

controllers/membershipController.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const asyncHandler = require('express-async-handler');
2+
const db = require('../db/query.js');
3+
const {body, validationResult} = require('express-validator');
4+
require('dotenv').config();
5+
6+
const validateRiddle = [
7+
body('secret_answer')
8+
.trim()
9+
.notEmpty()
10+
.withMessage('The answer is required'),
11+
body('secret_answer')
12+
.trim()
13+
.custom(value => value === process.env.MEMBERSHIP_SECRET)
14+
.withMessage('The answer is incorrect. Try again.')
15+
];
16+
17+
const membershipController = {
18+
renderForm: asyncHandler(async (req, res) => {
19+
const user = {
20+
...res.locals.currentUser
21+
};
22+
res.render('membership', {user});
23+
}),
24+
25+
addMembership: [
26+
validateRiddle,
27+
asyncHandler(async (req, res) => {
28+
const errors = validationResult(req);
29+
const user = {
30+
...res.locals.currentUser
31+
};
32+
if (!errors.isEmpty()) {
33+
res.render('membership', {user, errors: errors.array()});
34+
} else {
35+
await db.addMembership(user.id);
36+
res.redirect('/');
37+
}
38+
})
39+
],
40+
};
41+
42+
module.exports = membershipController;

db/query.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ const editPost = asyncHandler(async (id, title, content) => {
7676
);
7777
});
7878

79+
const addMembership = asyncHandler(async (id) => {
80+
await pool.query(
81+
'UPDATE users SET is_member = true WHERE id = $1', [id]
82+
);
83+
});
84+
7985
module.exports = {
8086
filterUsername,
8187
registerUser,
@@ -85,5 +91,6 @@ module.exports = {
8591
getPostById,
8692
createPost,
8793
editPost,
88-
deletePost
94+
deletePost,
95+
addMembership
8996
};

public/output.css

+14
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,11 @@ video {
835835
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
836836
}
837837

838+
.bg-gray-100 {
839+
--tw-bg-opacity: 1;
840+
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
841+
}
842+
838843
.bg-gray-50 {
839844
--tw-bg-opacity: 1;
840845
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
@@ -868,6 +873,10 @@ video {
868873
padding: 0.75rem;
869874
}
870875

876+
.p-4 {
877+
padding: 1rem;
878+
}
879+
871880
.p-6 {
872881
padding: 1.5rem;
873882
}
@@ -1167,6 +1176,11 @@ video {
11671176
--tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity));
11681177
}
11691178

1179+
.group:hover .group-hover\:text-blue-600 {
1180+
--tw-text-opacity: 1;
1181+
color: rgb(37 99 235 / var(--tw-text-opacity));
1182+
}
1183+
11701184
.group:hover .group-hover\:underline {
11711185
text-decoration-line: underline;
11721186
}

routes/loginRouter.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const loginController = require('../controllers/loginController.js');
44

55
loginRouter.get('/', (req, res) => {
66
if(req.isAuthenticated()) {
7-
res.render('login', {user: res.locals.currentUser});
7+
res.redirect('/');
88
} else {
99
loginController.renderForm(req, res);
1010
}

routes/membershipRouter.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const {Router} = require('express');
2+
const membershipRouter = Router();
3+
const membershipController = require('../controllers/membershipController.js');
4+
5+
membershipRouter.get('/', (req, res) => {
6+
if(req.isAuthenticated()) {
7+
membershipController.renderForm(req, res);
8+
} else {
9+
res.redirect('/login');
10+
}
11+
});
12+
membershipRouter.post('/', membershipController.addMembership);
13+
14+
module.exports = membershipRouter;

routes/postRouter.js

+21-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,28 @@ const {Router} = require('express');
22
const postRouter = Router();
33
const postController = require('../controllers/postController.js');
44

5-
postRouter.get('/create', postController.renderForm);
5+
postRouter.get('/create', (req, res) => {
6+
if(req.isAuthenticated()) {
7+
postController.renderForm(req, res);
8+
} else {
9+
res.redirect('/login');
10+
}
11+
});
612
postRouter.post('/create', postController.createPost);
7-
postRouter.get('/delete/:id', postController.deletePost);
8-
postRouter.get('/edit/:id', postController.renderEditForm);
13+
postRouter.get('/delete/:id', (req, res) => {
14+
if(req.isAuthenticated()) {
15+
postController.deletePost(req, res);
16+
} else {
17+
res.redirect('/login');
18+
}
19+
});
20+
postRouter.get('/edit/:id', (req, res) => {
21+
if(req.isAuthenticated()) {
22+
postController.renderEditForm(req, res);
23+
} else {
24+
res.redirect('/login');
25+
}
26+
});
927
postRouter.post('/edit/:id', postController.editPost);
1028

1129
module.exports = postRouter;

routes/registerRouter.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const registerController = require('../controllers/registerController.js');
44

55
registerRouter.get('/', (req, res) => {
66
if(req.isAuthenticated()) {
7-
res.render('register', {user: res.locals.currentUser, isAuthenticated: true});
7+
res.redirect('/');
88
} else {
99
registerController.renderForm(req, res);
1010
}

views/index.ejs

+44-32
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515
<h1 class="text-3xl md:text-4xl font-bold text-gray-900">
1616
Welcome, <%= locals.user.first_name %> <%= locals.user.last_name %>
1717
</h1>
18+
<a href="/logout" class="px-4 py-2 bg-black text-white rounded-md hover:bg-black/80">Logout</a>
1819
<div class="w-20 h-1 bg-blue-500 rounded-full"></div>
1920
</div>
2021
<div class="feed space-y-8 flex flex-col max-w-4xl mx-auto w-full">
2122
<% if(locals.posts.length > 0) { %>
22-
<% locals.posts.forEach(post => { %>
23-
<div class="post bg-white p-6 md:p-8 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100">
23+
<% locals.posts.forEach( (post, idx) => { %>
24+
<div class="post bg-white p-6 md:p-8 rounded-xl shadow-sm group hover:shadow-md transition-all duration-300 border border-gray-100">
2425
<article class="space-y-4">
25-
<h2 class="text-xl md:text-2xl font-bold text-gray-900 hover:text-blue-600 transition-colors">
26+
<h2 class="text-xl md:text-2xl font-bold text-gray-900 group-hover:text-blue-600 transition-colors">
2627
<%= post.title %>
2728
</h2>
2829
<p class="text-md md:text-lg text-gray-700 leading-relaxed">
@@ -35,7 +36,7 @@
3536
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
3637
</svg>
3738
<% if(locals.user.is_member || locals.user.id === post.user_id) { %>
38-
<time class="text-gray-500" id="createdAt" data-created-at="<%= post.created_at %>"></time>
39+
<time class="text-gray-500 created-at" id="createdAt" data-created-at="<%= post.created_at %>"></time>
3940
<% } else { %>
4041
<a href="/membership" class="text-blue-500 hover:text-blue-700 hover:underline font-medium">
4142
Membership Required
@@ -47,7 +48,7 @@
4748
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
4849
</svg>
4950
<% if(locals.user.is_member || locals.user.id === post.user_id) { %>
50-
<span class="text-gray-500"><%= user.username %></span>
51+
<span class="text-gray-500"><%= postCreators[idx].username %></span>
5152
<% } else { %>
5253
<a href="/membership" class="text-blue-500 hover:text-blue-700 hover:underline font-medium">
5354
Membership Required
@@ -93,34 +94,45 @@
9394
</body>
9495
<script>
9596
document.addEventListener('DOMContentLoaded', function() {
96-
const createdAtElement = document.getElementById('createdAt');
97-
const createdAt = new Date(createdAtElement.getAttribute('data-created-at'));
98-
const now = new Date();
99-
const diffInMs = now - createdAt;
100-
const diffInSecs = Math.floor(diffInMs / 1000);
101-
const diffInMins = Math.floor(diffInSecs / 60);
102-
const diffInHours = Math.floor(diffInMins / 60);
103-
const diffInDays = Math.floor(diffInHours / 24);
104-
const diffInMonths = Math.floor(diffInDays / 30);
105-
const diffInYears = Math.floor(diffInDays / 365);
106-
107-
let timeAgo = '';
108-
109-
if (diffInYears > 0) {
110-
timeAgo = diffInYears === 1 ? '1 year ago' : `${diffInYears} years ago`;
111-
} else if (diffInMonths > 0) {
112-
timeAgo = diffInMonths === 1 ? '1 month ago' : `${diffInMonths} months ago`;
113-
} else if (diffInDays > 0) {
114-
timeAgo = diffInDays === 1 ? '1 day ago' : `${diffInDays} days ago`;
115-
} else if (diffInHours > 0) {
116-
timeAgo = diffInHours === 1 ? '1 hour ago' : `${diffInHours} hours ago`;
117-
} else if (diffInMins > 0) {
118-
timeAgo = diffInMins === 1 ? '1 minute ago' : `${diffInMins} minutes ago`;
119-
} else {
120-
timeAgo = diffInSecs <= 5 ? 'Just now' : `${diffInSecs} seconds ago`;
97+
98+
// FUNCTIONS //
99+
100+
function updatePostTime(){
101+
const createdAtElement = document.querySelectorAll('.created-at');
102+
createdAtElement.forEach((element) => {
103+
const createdAt = new Date(element.getAttribute('data-created-at'));
104+
const now = new Date();
105+
const diffInMs = now - createdAt;
106+
const diffInSecs = Math.floor(diffInMs / 1000);
107+
const diffInMins = Math.floor(diffInSecs / 60);
108+
const diffInHours = Math.floor(diffInMins / 60);
109+
const diffInDays = Math.floor(diffInHours / 24);
110+
const diffInMonths = Math.floor(diffInDays / 30);
111+
const diffInYears = Math.floor(diffInDays / 365);
112+
113+
let timeAgo = '';
114+
115+
if (diffInYears > 0) {
116+
timeAgo = diffInYears === 1 ? '1 year ago' : `${diffInYears} years ago`;
117+
} else if (diffInMonths > 0) {
118+
timeAgo = diffInMonths === 1 ? '1 month ago' : `${diffInMonths} months ago`;
119+
} else if (diffInDays > 0) {
120+
timeAgo = diffInDays === 1 ? '1 day ago' : `${diffInDays} days ago`;
121+
} else if (diffInHours > 0) {
122+
timeAgo = diffInHours === 1 ? '1 hour ago' : `${diffInHours} hours ago`;
123+
} else if (diffInMins > 0) {
124+
timeAgo = diffInMins === 1 ? '1 minute ago' : `${diffInMins} minutes ago`;
125+
} else {
126+
timeAgo = diffInSecs <= 5 ? 'Just now' : `${diffInSecs} seconds ago`;
127+
}
128+
element.textContent = timeAgo;
129+
});
121130
}
122-
123-
createdAtElement.textContent = timeAgo;
131+
132+
// FUNCTION CALLS //
133+
134+
updatePostTime();
135+
124136
});
125137
</script>
126138
</html>

views/membership.ejs

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>WhisperSpace - Membership</title>
7+
<link rel="stylesheet" href="/output.css">
8+
</head>
9+
<body>
10+
<wrapper class="w-full flex flex-col items-center">
11+
<%- include('partials/header') %>
12+
<main class="px-4 py-20 w-full max-w-7xl flex flex-col items-center justify-center">
13+
<div class="text-center mb-12">
14+
<h1 class="text-4xl font-bold mb-4">Become a Member</h1>
15+
<p class="text-gray-600 max-w-md mx-auto">
16+
Solve the riddle to unlock your membership.
17+
</p>
18+
</div>
19+
<% if(locals.errors || locals.sessionErrors ) { %>
20+
<div class="w-full max-w-[400px] mb-8 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
21+
<%- include('partials/authErrors') %>
22+
</div>
23+
<% } %>
24+
<div class="border border-black px-8 pt-12 pb-6 rounded-md flex flex-col w-full max-w-[400px]">
25+
<div class="mb-6 bg-gray-100 p-4 rounded-md">
26+
<p class="text-gray-800 font-semibold">
27+
I speak without a mouth and hear without ears.
28+
I have no body, but I <br> come alive with wind.
29+
If you look at <br> me backward, I might help you understand what I am.
30+
</p>
31+
</div>
32+
<form action="/membership" method="post" class="w-full max-w-[400px] flex flex-col gap-2">
33+
<input class="p-2 rounded-md border border-gray-300" type="text" id="secret_answer" name="secret_answer" placeholder="Answer">
34+
<input type="submit" class="mt-5 cursor-pointer p-2 rounded-md bg-black text-white hover:bg-black/80" value="Login">
35+
</form>
36+
</div>
37+
</main>
38+
</wrapper>
39+
</body>
40+
</html>

views/partials/header.ejs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<a href="/membership" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Become a Member</a>
99
</div>
1010
<% } else { %>
11-
<a href="/login" class="hover:text-gray-600">Login</a>
11+
<a href="/login" class=" hover:text-gray-600">Login</a>
1212
<a href="/register" class="px-4 py-2 bg-black text-white rounded-md hover:bg-black/80">
1313
Register
1414
</a>

0 commit comments

Comments
 (0)