Skip to content

Commit 7bbe009

Browse files
Merge branch 'main' of https://github.com/krishnaacharyaa/wanderlust into readme-file
2 parents b467151 + 954593b commit 7bbe009

16 files changed

+215
-78
lines changed

Diff for: .github/workflows/issue-assigned.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
script: |
1616
const issue_number = context.issue.number;
1717
const issue_assignee = context.payload.assignee.login;
18-
const contributing_guidelines_url = 'https://github.com/krishnaacharyaa/wanderlust/blob/main/.github/CONTRIBUTING.md';
18+
const contributing_guidelines_url = 'https://github.com/krishnaacharyaa/wanderlust/blob/main/.github/CONTRIBUTING.md#guidelines-for-contributions';
1919
2020
github.rest.issues.createComment({
2121
owner: context.repo.owner,

Diff for: backend/controllers/posts-controller.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,11 @@ export const updatePostHandler = async (req, res) => {
135135
if (!updatedPost) {
136136
return res.status(HTTP_STATUS.NOT_FOUND).json({ message: RESPONSE_MESSAGES.POSTS.NOT_FOUND });
137137
}
138-
139-
res.status(HTTP_STATUS.OK).json(updatedPost);
138+
// invalidate the redis cache
139+
await deleteDataFromCache(REDIS_KEYS.ALL_POSTS),
140+
await deleteDataFromCache(REDIS_KEYS.FEATURED_POSTS),
141+
await deleteDataFromCache(REDIS_KEYS.LATEST_POSTS),
142+
await res.status(HTTP_STATUS.OK).json(updatedPost);
140143
} catch (err) {
141144
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message });
142145
}
@@ -152,7 +155,11 @@ export const deletePostByIdHandler = async (req, res) => {
152155
}
153156
await User.findByIdAndUpdate(post.authorId, { $pull: { posts: req.params.id } });
154157

155-
res.status(HTTP_STATUS.OK).json({ message: RESPONSE_MESSAGES.POSTS.DELETED });
158+
// invalidate the redis cache
159+
await deleteDataFromCache(REDIS_KEYS.ALL_POSTS),
160+
await deleteDataFromCache(REDIS_KEYS.FEATURED_POSTS),
161+
await deleteDataFromCache(REDIS_KEYS.LATEST_POSTS),
162+
res.status(HTTP_STATUS.OK).json({ message: RESPONSE_MESSAGES.POSTS.DELETED });
156163
} catch (err) {
157164
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message });
158165
}

Diff for: backend/controllers/user-controller.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js';
22
import User from '../models/user.js';
3+
import { Role } from '../types/role-type.js';
34

45
export const getAllUserHandler = async (req, res) => {
56
try {
6-
const users = await User.find().select('_id name email');
7+
const users = await User.find().select('_id fullName role email');
78
return res.status(HTTP_STATUS.OK).json({ users });
89
} catch (error) {
9-
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: error.message });
10+
console.log(error);
11+
res
12+
.status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
13+
.json({ message: RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR });
1014
}
1115
};
1216

1317
export const changeUserRoleHandler = async (req, res) => {
1418
try {
1519
const userId = req.params.userId;
1620
const { role } = req.body;
17-
if (role === 'user' || role === 'admin') {
21+
if (role === Role.User || role === Role.Admin) {
1822
const user = await User.findById(userId);
1923
if (!user)
2024
return res
@@ -29,6 +33,7 @@ export const changeUserRoleHandler = async (req, res) => {
2933
}
3034
return res.status(HTTP_STATUS.OK).json({ message: RESPONSE_MESSAGES.USERS.UPDATE });
3135
} catch (error) {
36+
console.log(error);
3237
res
3338
.status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
3439
.json({ message: RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR, error: error });
@@ -45,6 +50,7 @@ export const deleteUserHandler = async (req, res) => {
4550
.json({ message: RESPONSE_MESSAGES.USERS.USER_NOT_EXISTS });
4651
res.status(HTTP_STATUS.NO_CONTENT).json({ message: RESPONSE_MESSAGES.USERS.DELETED });
4752
} catch (error) {
53+
console.log(error);
4854
res
4955
.status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
5056
.json({ message: RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR, error: error });

Diff for: backend/middlewares/auth-middleware.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { JWT_SECRET } from '../config/utils.js';
22
import { ApiError } from '../utils/api-error.js';
33
import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js';
44
import jwt from 'jsonwebtoken';
5+
import { Role } from '../types/role-type.js';
56

67
export const authMiddleware = async (req, res, next) => {
78
const token = req.cookies?.access_token;
@@ -22,7 +23,7 @@ export const authMiddleware = async (req, res, next) => {
2223

2324
export const isAdminMiddleware = async (req, res, next) => {
2425
const role = req.user.role;
25-
if (role !== 'ADMIN') {
26+
if (role !== Role.Admin) {
2627
return new ApiError(HTTP_STATUS.UNAUTHORIZED, RESPONSE_MESSAGES.USERS.UNAUTHORIZED_USER);
2728
}
2829
next();

Diff for: backend/models/user.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import JWT from 'jsonwebtoken';
33
import bcrypt from 'bcryptjs';
44
import crypto from 'crypto';
55
import { ACCESS_TOKEN_EXPIRES_IN, JWT_SECRET, REFRESH_TOKEN_EXPIRES_IN } from '../config/utils.js';
6+
import { Role } from '../types/role-type.js';
67

78
const userSchema = new Schema(
89
{
@@ -47,8 +48,8 @@ const userSchema = new Schema(
4748
},
4849
role: {
4950
type: String,
50-
default: 'USER',
51-
enum: ['USER', 'ADMIN'],
51+
default: Role.User,
52+
enum: [Role.User, Role.Admin],
5253
},
5354
posts: [
5455
{

Diff for: backend/types/role-type.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const Role = {
2+
Admin: 'ADMIN',
3+
User: 'USER',
4+
};

Diff for: frontend/eslint.config.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ export default [
66
{ languageOptions: { globals: globals.browser } },
77
...tseslint.configs.recommended,
88
pluginReactConfig,
9-
{
10-
rules: {
11-
'react/react-in-jsx-scope': 'off' ,
12-
'react/no-unescaped-entities': 'off'
13-
}
14-
}
9+
{
10+
rules: {
11+
'react/react-in-jsx-scope': 'off',
12+
'react/no-unescaped-entities': 'off',
13+
},
14+
},
1515
];

Diff for: frontend/src/App.tsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import UnprotectedRoute from './components/unprotected-route';
1313
import { useLayoutEffect } from 'react';
1414
import RequireAuth from './components/require-auth';
1515
import useThemeClass from './utils/theme-changer';
16+
import AdminContainer from './components/admin-container';
17+
import { Role } from './types/role-type.tsx';
1618

1719
function App() {
1820
useLayoutEffect(() => {
@@ -30,12 +32,14 @@ function App() {
3032
<Route path="signin" element={<SignIn />} />
3133
<Route path="signup" element={<SignUp />} />
3234
</Route>
33-
<Route element={<RequireAuth allowedRole={['ADMIN', 'USER']} />}>
35+
<Route element={<RequireAuth allowedRole={[Role.Admin, Role.User]} />}>
3436
<Route path="add-blog" element={<AddBlog />} />
3537
</Route>
36-
<Route path="admin" element={<RequireAuth allowedRole={['ADMIN']} />}>
37-
<Route path="users" element={<AdminUsers />} />
38-
<Route path="blogs" element={<AdminBlogs />} />
38+
<Route path="admin" element={<RequireAuth allowedRole={[Role.Admin]} />}>
39+
<Route element={<AdminContainer />}>
40+
<Route path="users" element={<AdminUsers />} />
41+
<Route path="blogs" element={<AdminBlogs />} />
42+
</Route>
3943
</Route>
4044
</Route>
4145
<Route path="*" element={<NotFound />} />

Diff for: frontend/src/components/admin-sidebar.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NavLink } from 'react-router-dom';
1+
import { NavLink, useNavigate } from 'react-router-dom';
22
import UserIcon from '@/assets/svg/user-icon';
33
import BlogIcon from '@/assets/svg/blog-icon';
44
import BarIcons from '@/assets/svg/bars-icon';
@@ -8,6 +8,8 @@ import CloseIcon from '@/assets/svg/close-icon';
88
const AdminSidebar = () => {
99
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
1010

11+
const navigate = useNavigate();
12+
1113
return (
1214
<>
1315
<button
@@ -32,7 +34,12 @@ const AdminSidebar = () => {
3234
<CloseIcon />
3335
</button>
3436
<div className="border-b border-[#D9D9D9] bg-light px-6 py-3 dark:border-gray-700 dark:bg-dark sm:p-6 ">
35-
<h1 className="text-xl font-medium text-light-title dark:text-dark-title">WanderLust</h1>
37+
<h1
38+
onClick={() => navigate('/')}
39+
className="cursor-pointer text-xl font-medium text-light-title dark:text-dark-title"
40+
>
41+
WanderLust
42+
</h1>
3643
</div>
3744
<div className="flex flex-col gap-2 p-6">
3845
<NavLink

Diff for: frontend/src/components/skeletons/post-card-skeleton.tsx

+6-10
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,12 @@ export const PostCardSkeleton = () => {
66
<div className="mb-4 mr-8 mt-4 rounded-lg bg-light shadow-md dark:bg-dark-card">
77
<Skeleton className="h-48 w-full rounded-lg bg-slate-200 dark:bg-slate-700" />
88
<div className="p-4">
9-
<Skeleton className="mb-2 h-3 w-full pr-2 bg-slate-200 dark:bg-slate-700" />
10-
<Skeleton className="mb-2 h-6 w-full pr-2 bg-slate-200 dark:bg-slate-700" />
11-
<Skeleton className="mb-2 sm:mb-4 h-6 w-full pr-2 bg-slate-200 dark:bg-slate-700" />
12-
<div className="mt-1 sm:mt-2 flex flex-wrap gap-1 sm:gap-1.5">
13-
<Skeleton
14-
className={`h-6 w-16 rounded-full bg-slate-200 dark:bg-slate-700`}
15-
/>
16-
<Skeleton
17-
className={`h-6 w-16 rounded-full bg-slate-200 dark:bg-slate-700`}
18-
/>
9+
<Skeleton className="mb-2 h-3 w-full bg-slate-200 pr-2 dark:bg-slate-700" />
10+
<Skeleton className="mb-2 h-6 w-full bg-slate-200 pr-2 dark:bg-slate-700" />
11+
<Skeleton className="mb-2 h-6 w-full bg-slate-200 pr-2 dark:bg-slate-700 sm:mb-4" />
12+
<div className="mt-1 flex flex-wrap gap-1 sm:mt-2 sm:gap-1.5">
13+
<Skeleton className={`h-6 w-16 rounded-full bg-slate-200 dark:bg-slate-700`} />
14+
<Skeleton className={`h-6 w-16 rounded-full bg-slate-200 dark:bg-slate-700`} />
1915
</div>
2016
</div>
2117
</div>

Diff for: frontend/src/layouts/header-layout.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import Loader from '@/components/skeletons/loader';
1212
import useAuthData from '@/hooks/useAuthData';
1313
import userState from '@/utils/user-state';
1414
import { Link } from 'react-router-dom';
15+
import { Role } from '@/types/role-type.tsx';
1516

1617
function header() {
1718
const navigate = useNavigate();
1819
const { token, loading } = useAuthData();
20+
const user = userState.getUser();
1921

2022
const handleLogout = async () => {
2123
try {
@@ -73,6 +75,17 @@ function header() {
7375
<Loader />
7476
) : token ? (
7577
<div className="flex gap-2">
78+
{user?.role === Role.Admin && (
79+
<button
80+
className="active:scale-click hidden rounded border border-slate-50 px-4 py-2 hover:bg-slate-500/25 md:inline-block"
81+
onClick={() => {
82+
navigate('/admin/blogs');
83+
}}
84+
>
85+
Dashboard
86+
</button>
87+
)}
88+
7689
<button
7790
className="active:scale-click hidden rounded border border-slate-50 px-4 py-2 hover:bg-slate-500/25 md:inline-block"
7891
onClick={() => {

Diff for: frontend/src/pages/admin-blogs.tsx

+65-28
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,79 @@
11
import PenIcon from '@/assets/svg/pen-icon';
22
import TrasnIcon from '@/assets/svg/trash-icon';
3-
import { imageUrls } from '@/constants/images';
3+
import axiosInstance from '@/helpers/axios-instance';
4+
import formatPostTime from '@/utils/format-post-time';
5+
import { useEffect, useState } from 'react';
6+
import { toast } from 'react-toastify';
7+
import 'react-toastify/dist/ReactToastify.css';
8+
import Post from '@/types/post-type';
49

510
const AdminBlogs = () => {
11+
const [posts, setPosts] = useState<Post[]>([]);
12+
13+
const fetchData = async () => {
14+
try {
15+
const response = await axiosInstance.get('/api/posts');
16+
setPosts(response?.data);
17+
} catch (error) {
18+
toast.error('Something went wrong! Please try again.');
19+
}
20+
};
21+
22+
const handleDelete = async (postId: string) => {
23+
const response = await axiosInstance.delete('/api/posts/admin/' + postId);
24+
if (response.status === 200) {
25+
fetchData();
26+
toast.success('Post successfully deleted !');
27+
}
28+
};
29+
30+
useEffect(() => {
31+
fetchData();
32+
}, []);
33+
634
return (
735
<>
836
<div className="w-full p-3 px-5 sm:p-12">
937
<h1 className="absolute left-16 top-3 text-2xl font-bold text-light-title dark:text-dark-title sm:static">
1038
Blogs
1139
</h1>
1240
<div className="mt-2 flex flex-col sm:mt-12">
13-
<div className="flex flex-row items-center justify-between gap-2 rounded-lg bg-light px-3 py-3 shadow-md dark:bg-dark-card sm:gap-5">
14-
<img
15-
src={imageUrls[1]}
16-
className=" h-16 w-16 rounded-xl object-cover shadow-lg sm:h-24 sm:w-24"
17-
alt=""
18-
/>
19-
<div className="flex w-12 flex-1 grow flex-col justify-between gap-2">
20-
<h4 className="w-full truncate text-base font-semibold text-light-title dark:text-dark-title sm:text-xl">
21-
A Serene Escape to Bali's Hidden Beaches
22-
</h4>
23-
<p className="hidden w-full truncate text-sm text-light-description dark:text-dark-description sm:inline">
24-
Explore Bali's tranquil shores and discover the best-hidden beaches the island has
25-
to offer. Dive into the crystal-clear water
26-
</p>
27-
<p className="text-sm font-semibold text-[#6941C6] dark:text-dark-secondary">
28-
Drew Cano • 1 Jan 2023
29-
</p>
30-
</div>
31-
<div className="mt-2 flex flex-col gap-2 sm:mt-0 sm:flex-row ">
32-
<button className="h-fit rounded-xl border-0 text-base font-semibold text-light-title dark:text-dark-title sm:text-xl">
33-
<PenIcon />
34-
</button>
35-
<button className="h-fit rounded-xl border-0 text-base font-semibold text-light-title dark:text-dark-title sm:text-xl ">
36-
<TrasnIcon />
37-
</button>
38-
</div>
39-
</div>
41+
{posts?.map((post: Post) => {
42+
return (
43+
<div
44+
key={post?._id}
45+
className="mb-3 flex flex-row items-center justify-between gap-2 rounded-lg bg-light px-3 py-3 shadow-md dark:bg-dark-card sm:gap-5"
46+
>
47+
<img
48+
src={post?.imageLink}
49+
className=" h-16 w-16 rounded-xl object-cover shadow-lg sm:h-24 sm:w-24"
50+
alt=""
51+
/>
52+
<div className="flex w-12 flex-1 grow flex-col justify-between gap-2">
53+
<h4 className="w-full truncate text-base font-semibold text-light-title dark:text-dark-title sm:text-xl">
54+
{post?.title}
55+
</h4>
56+
<p className="hidden w-full truncate text-sm text-light-description dark:text-dark-description sm:inline">
57+
{post?.description}
58+
</p>
59+
<p className="text-sm font-semibold text-[#6941C6] dark:text-dark-secondary">
60+
{post?.authorName}{formatPostTime(post?.timeOfPost)}
61+
</p>
62+
</div>
63+
<div className="mt-2 flex flex-col gap-2 sm:mt-0 sm:flex-row ">
64+
<button className="h-fit rounded-xl border-0 text-base font-semibold text-light-title dark:text-dark-title sm:text-xl">
65+
<PenIcon />
66+
</button>
67+
<button
68+
onClick={() => handleDelete(post?._id)}
69+
className="h-fit rounded-xl border-0 text-base font-semibold text-light-title dark:text-dark-title sm:text-xl "
70+
>
71+
<TrasnIcon />
72+
</button>
73+
</div>
74+
</div>
75+
);
76+
})}
4077
</div>
4178
</div>
4279
</>

0 commit comments

Comments
 (0)