Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat-#178: added admin UI to frontend #369

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 10 additions & 3 deletions backend/controllers/posts-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,11 @@ export const updatePostHandler = async (req, res) => {
if (!updatedPost) {
return res.status(HTTP_STATUS.NOT_FOUND).json({ message: RESPONSE_MESSAGES.POSTS.NOT_FOUND });
}

res.status(HTTP_STATUS.OK).json(updatedPost);
// invalidate the redis cache
await deleteDataFromCache(REDIS_KEYS.ALL_POSTS),
await deleteDataFromCache(REDIS_KEYS.FEATURED_POSTS),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should ideally format, I'm confused, why isn't it working for you !!
we have a husky hook which actually formats the code,
Isn't it working for you?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess its not working for me.
i will manually do "npm run format" in both frontend and backend and push the code

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await deleteDataFromCache(REDIS_KEYS.LATEST_POSTS),
await res.status(HTTP_STATUS.OK).json(updatedPost);
} catch (err) {
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message });
}
Expand All @@ -152,7 +155,11 @@ export const deletePostByIdHandler = async (req, res) => {
}
await User.findByIdAndUpdate(post.authorId, { $pull: { posts: req.params.id } });

res.status(HTTP_STATUS.OK).json({ message: RESPONSE_MESSAGES.POSTS.DELETED });
// invalidate the redis cache
await deleteDataFromCache(REDIS_KEYS.ALL_POSTS),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

await deleteDataFromCache(REDIS_KEYS.FEATURED_POSTS),
await deleteDataFromCache(REDIS_KEYS.LATEST_POSTS),
res.status(HTTP_STATUS.OK).json({ message: RESPONSE_MESSAGES.POSTS.DELETED });
} catch (err) {
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message });
}
Expand Down
11 changes: 8 additions & 3 deletions backend/controllers/user-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ import User from '../models/user.js';

export const getAllUserHandler = async (req, res) => {
try {
const users = await User.find().select('_id name email');
const users = await User.find().select('_id fullName role email');
return res.status(HTTP_STATUS.OK).json({ users });
} catch (error) {
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: error.message });
console.log(error);
res
.status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
.json({ message: RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR });
}
};

export const changeUserRoleHandler = async (req, res) => {
try {
const userId = req.params.userId;
const { role } = req.body;
if (role === 'user' || role === 'admin') {
if (role === 'USER' || role === 'ADMIN') {
const user = await User.findById(userId);
if (!user)
return res
Expand All @@ -29,6 +32,7 @@ export const changeUserRoleHandler = async (req, res) => {
}
return res.status(HTTP_STATUS.OK).json({ message: RESPONSE_MESSAGES.USERS.UPDATE });
} catch (error) {
console.log(error);
res
.status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
.json({ message: RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR, error: error });
Expand All @@ -45,6 +49,7 @@ export const deleteUserHandler = async (req, res) => {
.json({ message: RESPONSE_MESSAGES.USERS.USER_NOT_EXISTS });
res.status(HTTP_STATUS.NO_CONTENT).json({ message: RESPONSE_MESSAGES.USERS.DELETED });
} catch (error) {
console.log(error);
res
.status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
.json({ message: RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR, error: error });
Expand Down
12 changes: 6 additions & 6 deletions frontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ export default [
{ languageOptions: { globals: globals.browser } },
...tseslint.configs.recommended,
pluginReactConfig,
{
rules: {
'react/react-in-jsx-scope': 'off' ,
'react/no-unescaped-entities': 'off'
}
}
{
rules: {
'react/react-in-jsx-scope': 'off',
'react/no-unescaped-entities': 'off',
},
},
];
7 changes: 5 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import UnprotectedRoute from './components/unprotected-route';
import { useLayoutEffect } from 'react';
import RequireAuth from './components/require-auth';
import useThemeClass from './utils/theme-changer';
import AdminContainer from './components/admin-container';

function App() {
useLayoutEffect(() => {
Expand All @@ -34,8 +35,10 @@ function App() {
<Route path="add-blog" element={<AddBlog />} />
</Route>
<Route path="admin" element={<RequireAuth allowedRole={['ADMIN']} />}>
<Route path="users" element={<AdminUsers />} />
<Route path="blogs" element={<AdminBlogs />} />
<Route element={<AdminContainer />}>
<Route path="users" element={<AdminUsers />} />
<Route path="blogs" element={<AdminBlogs />} />
</Route>
</Route>
</Route>
<Route path="*" element={<NotFound />} />
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/components/admin-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NavLink } from 'react-router-dom';
import { NavLink, useNavigate } from 'react-router-dom';
import UserIcon from '@/assets/svg/user-icon';
import BlogIcon from '@/assets/svg/blog-icon';
import BarIcons from '@/assets/svg/bars-icon';
Expand All @@ -8,6 +8,8 @@ import CloseIcon from '@/assets/svg/close-icon';
const AdminSidebar = () => {
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);

const navigate = useNavigate();

return (
<>
<button
Expand All @@ -32,7 +34,12 @@ const AdminSidebar = () => {
<CloseIcon />
</button>
<div className="border-b border-[#D9D9D9] bg-light px-6 py-3 dark:border-gray-700 dark:bg-dark sm:p-6 ">
<h1 className="text-xl font-medium text-light-title dark:text-dark-title">WanderLust</h1>
<h1
onClick={() => navigate('/')}
className="cursor-pointer text-xl font-medium text-light-title dark:text-dark-title"
>
WanderLust
</h1>
</div>
<div className="flex flex-col gap-2 p-6">
<NavLink
Expand Down
16 changes: 6 additions & 10 deletions frontend/src/components/skeletons/post-card-skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,12 @@ export const PostCardSkeleton = () => {
<div className="mb-4 mr-8 mt-4 rounded-lg bg-light shadow-md dark:bg-dark-card">
<Skeleton className="h-48 w-full rounded-lg bg-slate-200 dark:bg-slate-700" />
<div className="p-4">
<Skeleton className="mb-2 h-3 w-full pr-2 bg-slate-200 dark:bg-slate-700" />
<Skeleton className="mb-2 h-6 w-full pr-2 bg-slate-200 dark:bg-slate-700" />
<Skeleton className="mb-2 sm:mb-4 h-6 w-full pr-2 bg-slate-200 dark:bg-slate-700" />
<div className="mt-1 sm:mt-2 flex flex-wrap gap-1 sm:gap-1.5">
<Skeleton
className={`h-6 w-16 rounded-full bg-slate-200 dark:bg-slate-700`}
/>
<Skeleton
className={`h-6 w-16 rounded-full bg-slate-200 dark:bg-slate-700`}
/>
<Skeleton className="mb-2 h-3 w-full bg-slate-200 pr-2 dark:bg-slate-700" />
<Skeleton className="mb-2 h-6 w-full bg-slate-200 pr-2 dark:bg-slate-700" />
<Skeleton className="mb-2 h-6 w-full bg-slate-200 pr-2 dark:bg-slate-700 sm:mb-4" />
<div className="mt-1 flex flex-wrap gap-1 sm:mt-2 sm:gap-1.5">
<Skeleton className={`h-6 w-16 rounded-full bg-slate-200 dark:bg-slate-700`} />
<Skeleton className={`h-6 w-16 rounded-full bg-slate-200 dark:bg-slate-700`} />
</div>
</div>
</div>
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/layouts/header-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Link } from 'react-router-dom';
function header() {
const navigate = useNavigate();
const { token, loading } = useAuthData();
const user = userState.getUser();

const handleLogout = async () => {
try {
Expand Down Expand Up @@ -73,6 +74,17 @@ function header() {
<Loader />
) : token ? (
<div className="flex gap-2">
{user?.role === 'ADMIN' && (
<button
className="active:scale-click hidden rounded border border-slate-50 px-4 py-2 hover:bg-slate-500/25 md:inline-block"
onClick={() => {
navigate('/admin/blogs');
}}
>
Dashboard
</button>
)}

<button
className="active:scale-click hidden rounded border border-slate-50 px-4 py-2 hover:bg-slate-500/25 md:inline-block"
onClick={() => {
Expand Down
93 changes: 65 additions & 28 deletions frontend/src/pages/admin-blogs.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,79 @@
import PenIcon from '@/assets/svg/pen-icon';
import TrasnIcon from '@/assets/svg/trash-icon';
import { imageUrls } from '@/constants/images';
import axiosInstance from '@/helpers/axios-instance';
import formatPostTime from '@/utils/format-post-time';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import Post from '@/types/post-type';

const AdminBlogs = () => {
const [posts, setPosts] = useState<Post[]>([]);

const fetchData = async () => {
try {
const response = await axiosInstance.get('/api/posts');
setPosts(response?.data);
} catch (error) {
toast.error('Something went wrong! Please try again.');
}
};

const handleDelete = async (postId: string) => {
const response = await axiosInstance.delete('/api/posts/admin/' + postId);
if (response.status === 200) {
fetchData();
toast.success('Post successfully deleted !');
}
};

useEffect(() => {
fetchData();
}, []);

return (
<>
<div className="w-full p-3 px-5 sm:p-12">
<h1 className="absolute left-16 top-3 text-2xl font-bold text-light-title dark:text-dark-title sm:static">
Blogs
</h1>
<div className="mt-2 flex flex-col sm:mt-12">
<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">
<img
src={imageUrls[1]}
className=" h-16 w-16 rounded-xl object-cover shadow-lg sm:h-24 sm:w-24"
alt=""
/>
<div className="flex w-12 flex-1 grow flex-col justify-between gap-2">
<h4 className="w-full truncate text-base font-semibold text-light-title dark:text-dark-title sm:text-xl">
A Serene Escape to Bali's Hidden Beaches
</h4>
<p className="hidden w-full truncate text-sm text-light-description dark:text-dark-description sm:inline">
Explore Bali's tranquil shores and discover the best-hidden beaches the island has
to offer. Dive into the crystal-clear water
</p>
<p className="text-sm font-semibold text-[#6941C6] dark:text-dark-secondary">
Drew Cano • 1 Jan 2023
</p>
</div>
<div className="mt-2 flex flex-col gap-2 sm:mt-0 sm:flex-row ">
<button className="h-fit rounded-xl border-0 text-base font-semibold text-light-title dark:text-dark-title sm:text-xl">
<PenIcon />
</button>
<button className="h-fit rounded-xl border-0 text-base font-semibold text-light-title dark:text-dark-title sm:text-xl ">
<TrasnIcon />
</button>
</div>
</div>
{posts?.map((post: Post) => {
return (
<div
key={post?._id}
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"
>
<img
src={post?.imageLink}
className=" h-16 w-16 rounded-xl object-cover shadow-lg sm:h-24 sm:w-24"
alt=""
/>
<div className="flex w-12 flex-1 grow flex-col justify-between gap-2">
<h4 className="w-full truncate text-base font-semibold text-light-title dark:text-dark-title sm:text-xl">
{post?.title}
</h4>
<p className="hidden w-full truncate text-sm text-light-description dark:text-dark-description sm:inline">
{post?.description}
</p>
<p className="text-sm font-semibold text-[#6941C6] dark:text-dark-secondary">
{post?.authorName} • {formatPostTime(post?.timeOfPost)}
</p>
</div>
<div className="mt-2 flex flex-col gap-2 sm:mt-0 sm:flex-row ">
<button className="h-fit rounded-xl border-0 text-base font-semibold text-light-title dark:text-dark-title sm:text-xl">
<PenIcon />
</button>
<button
onClick={() => handleDelete(post?._id)}
className="h-fit rounded-xl border-0 text-base font-semibold text-light-title dark:text-dark-title sm:text-xl "
>
<TrasnIcon />
</button>
</div>
</div>
);
})}
</div>
</div>
</>
Expand Down
88 changes: 77 additions & 11 deletions frontend/src/pages/admin-users.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,88 @@
import axiosInstance from '@/helpers/axios-instance';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

enum role {
admin = 'ADMIN',
user = 'USER',
}

type User = {
_id: string;
fullName: string;
role: role;
email: string;
};

const AdminUsers = () => {
const [users, setUsers] = useState<User[]>([]);

const fetchData = async () => {
try {
const response = await axiosInstance.get('/api/user');
setUsers(response?.data?.users);
} catch (error) {
toast.error('Something went wrong! Please try again!');
}
};

const handleClick = async (userId: string, role: role) => {
try {
const response = await axiosInstance.patch('/api/user/' + userId, { role: role });
if (response.status === 200) {
fetchData();
toast.success('User updated successfully!');
}
} catch (error) {
toast.error('Something went wrong! Please try again later.');
}
};

useEffect(() => {
fetchData();
}, []);

return (
<>
<div className="w-full p-3 px-5 sm:p-12">
<h1 className="absolute left-16 top-3 text-2xl font-bold text-light-title dark:text-dark-title sm:static">
Users
</h1>
<div className="mt-2 sm:mt-12">
<div className="flex w-full flex-row items-center justify-between gap-5 rounded-lg border-b border-gray-300 bg-light px-3 py-4 shadow-md dark:border-gray-700 dark:bg-dark-card">
<div className="flex flex-col gap-[10px] ">
<p className="text-base font-medium text-light-title dark:text-dark-title">Hemant</p>
<p className="text-base font-medium text-light-description dark:text-dark-description">
hemant412@gmail.com
</p>
</div>
<button className="h-fit rounded-xl border border-black bg-black px-4 py-2 text-sm font-semibold text-white">
Admin
</button>
</div>
{users?.map((user: User) => {
return (
<div
key={user?._id}
className="mb-3 flex w-full flex-row items-center justify-between gap-5 rounded-lg border-b border-gray-300 bg-light px-3 py-4 shadow-md dark:border-gray-700 dark:bg-dark-card"
>
<div className="flex flex-col gap-[10px] ">
<p className="text-base font-medium text-light-title dark:text-dark-title">
{user?.fullName}
</p>
<p className="text-base font-medium text-light-description dark:text-dark-description">
{user?.email}
</p>
</div>
{user.role === role.admin && (
<button
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use enum instead, kindly replace all the places this is used to use enum

onClick={() => handleClick(user._id, role.user)}
className="h-fit rounded-xl border border-black bg-black px-4 py-2 text-sm font-semibold text-white"
>
Admin
</button>
)}
{user.role === role.user && (
<button
onClick={() => handleClick(user._id, role.admin)}
className="h-fit rounded-xl border border-black bg-transparent px-4 py-2 text-sm font-semibold text-white"
>
Admin
</button>
)}
</div>
);
})}
</div>
</div>
</>
Expand Down
Loading