Skip to content

Commit

Permalink
feat-#178: added admin UI to frontend (#369)
Browse files Browse the repository at this point in the history
  • Loading branch information
krishnaacharyaa authored Jun 18, 2024
2 parents af322ce + bf99c48 commit d90c7eb
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 68 deletions.
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),
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),
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
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

0 comments on commit d90c7eb

Please sign in to comment.