Skip to content

Commit e298b45

Browse files
feat-krishnaacharyaa#419: Added post editing functionality (krishnaacharyaa#443)
2 parents f157e4a + 6b7161e commit e298b45

File tree

8 files changed

+403
-281
lines changed

8 files changed

+403
-281
lines changed

Diff for: frontend/src/App.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import RequireAuthBlog from './components/require-auth-blog';
1616
import useThemeClass from './utils/theme-changer';
1717
import AdminContainer from './components/admin-container';
1818
import { Role } from './types/role-type.tsx';
19+
import EditBlog from './pages/edit-blog.tsx';
1920

2021
function App() {
2122
useLayoutEffect(() => {
@@ -35,6 +36,7 @@ function App() {
3536
</Route>
3637
<Route element={<RequireAuthBlog allowedRole={[Role.Admin, Role.User]} />}>
3738
<Route path="add-blog" element={<AddBlog />} />
39+
<Route path="edit-blog/:postId" element={<EditBlog />} />
3840
</Route>
3941
<Route path="admin" element={<RequireAuth allowedRole={[Role.Admin]} />}>
4042
<Route element={<AdminContainer />}>

Diff for: frontend/src/components/form-blog.tsx

+300
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import { useState, useEffect } from 'react';
2+
import { useNavigate } from 'react-router-dom';
3+
import { toast } from 'react-toastify';
4+
import 'react-toastify/dist/ReactToastify.css';
5+
import navigateBackBlackIcon from '@/assets/svg/navigate-back-black.svg';
6+
import navigateBackWhiteIcon from '@/assets/svg/navigate-back-white.svg';
7+
import ModalComponent from '@/components/modal';
8+
import CategoryPill from '@/components/category-pill';
9+
import { categories } from '@/utils/category-colors';
10+
import userState from '@/utils/user-state';
11+
import axiosInstance from '@/helpers/axios-instance';
12+
import { AxiosError, isAxiosError } from 'axios';
13+
import { useForm } from 'react-hook-form';
14+
import { TFormBlogSchema, formBlogSchema } from '@/lib/types';
15+
import { zodResolver } from '@hookform/resolvers/zod';
16+
import Post from '@/types/post-type';
17+
import useAuthData from '@/hooks/useAuthData';
18+
19+
interface FormBlogPropType {
20+
type: 'new' | 'edit';
21+
postId?: string;
22+
post?: Post;
23+
}
24+
25+
function FormBlog({ type, postId, post }: FormBlogPropType) {
26+
const [selectedImage, setSelectedImage] = useState<string>('');
27+
const {
28+
register,
29+
handleSubmit,
30+
reset,
31+
setValue,
32+
trigger,
33+
formState: { errors },
34+
watch,
35+
} = useForm<TFormBlogSchema>({
36+
resolver: zodResolver(formBlogSchema),
37+
defaultValues: {
38+
title: post?.title || '',
39+
authorName: post?.authorName || '',
40+
imageLink: post?.imageLink || '',
41+
categories: post?.categories || [],
42+
description: post?.description || '',
43+
isFeaturedPost: false,
44+
},
45+
});
46+
const formData = watch();
47+
const handleImageSelect = (imageUrl: string) => {
48+
setSelectedImage(imageUrl);
49+
};
50+
51+
const [modal, setmodal] = useState(false);
52+
const userData = useAuthData();
53+
54+
//checks the length of the categories array and if the category is already selected
55+
const isValidCategory = (category: string): boolean => {
56+
return formData.categories.length >= 3 && !formData.categories.includes(category);
57+
};
58+
59+
const handleCategoryClick = (category: string) => {
60+
if (isValidCategory(category)) return;
61+
62+
if (formData.categories.includes(category)) {
63+
setValue(
64+
'categories',
65+
formData.categories.filter((cat) => cat !== category)
66+
);
67+
} else {
68+
setValue('categories', [...formData.categories, category]);
69+
}
70+
trigger('categories');
71+
};
72+
73+
const handleselector = () => {
74+
setValue('imageLink', selectedImage);
75+
setmodal(false);
76+
};
77+
const handleCheckboxChange = () => {
78+
setValue('isFeaturedPost', !formData.isFeaturedPost);
79+
};
80+
const onSumbit = async () => {
81+
try {
82+
let postPromise;
83+
if (type === 'new') {
84+
postPromise = axiosInstance.post('/api/posts/', formData);
85+
}
86+
87+
if (type === 'edit' && postId) {
88+
if (userData?.role === 'ADMIN') {
89+
postPromise = axiosInstance.patch(`/api/posts/admin/${postId}`, formData);
90+
} else {
91+
postPromise = axiosInstance.patch(`/api/posts/${postId}`, formData);
92+
}
93+
}
94+
if (postPromise)
95+
toast.promise(postPromise, {
96+
pending: 'Creating blog post...',
97+
success: {
98+
render() {
99+
reset();
100+
navigate('/');
101+
return 'Blog created successfully';
102+
},
103+
},
104+
error: {
105+
render({ data }) {
106+
if (data instanceof AxiosError) {
107+
if (data?.response?.data?.message) {
108+
return data?.response?.data?.message;
109+
}
110+
}
111+
return 'Blog creation failed';
112+
},
113+
},
114+
});
115+
if (postPromise) return (await postPromise).data;
116+
} catch (error: unknown) {
117+
if (isAxiosError(error)) {
118+
navigate('/');
119+
userState.removeUser();
120+
console.error(error.response?.data?.message);
121+
} else {
122+
console.log(error);
123+
}
124+
}
125+
};
126+
const navigate = useNavigate();
127+
const [isDarkMode, setIsDarkMode] = useState<boolean | null>(null);
128+
useEffect(() => {
129+
const storedTheme = localStorage.getItem('theme');
130+
setIsDarkMode(storedTheme === 'dark');
131+
}, []);
132+
133+
function Asterisk() {
134+
return <span className="dark:text-dark-tertiary">*</span>;
135+
}
136+
137+
return (
138+
<div className="flex-grow cursor-default bg-slate-50 px-6 py-8 dark:bg-dark-card">
139+
<div className="mb-4 flex justify-center">
140+
<div className="flex w-[32rem] items-center justify-start space-x-4 sm:w-5/6 lg:w-4/6 ">
141+
<div className="w-fit cursor-pointer">
142+
<img
143+
alt="theme"
144+
src={isDarkMode ? navigateBackWhiteIcon : navigateBackBlackIcon}
145+
onClick={() => navigate(-1)}
146+
className="active:scale-click h-5 w-10"
147+
/>
148+
</div>
149+
<h2 className="cursor-text text-lg font-semibold text-light-primary dark:text-dark-primary sm:text-xl lg:text-2xl">
150+
Create Blog
151+
</h2>
152+
</div>
153+
</div>
154+
<div className="flex justify-center">
155+
<form onSubmit={handleSubmit(onSumbit)} className="sm:w-5/6 lg:w-2/3">
156+
<div className="mb-2 flex items-center">
157+
<label className="flex items-center">
158+
<span className="px-2 text-base font-medium text-light-secondary dark:text-dark-secondary">
159+
Is this a featured blog?
160+
</span>
161+
<input
162+
{...register('isFeaturedPost')}
163+
type="checkbox"
164+
className="ml-2 h-5 w-5 cursor-pointer rounded-full accent-purple-400"
165+
checked={formData.isFeaturedPost}
166+
onChange={handleCheckboxChange}
167+
/>
168+
</label>
169+
</div>
170+
171+
<div className="mb-2">
172+
<div className="px-2 py-1 font-medium text-light-secondary dark:text-dark-secondary">
173+
Blog title <Asterisk />
174+
</div>
175+
<input
176+
{...register('title')}
177+
type="text"
178+
placeholder="Travel Bucket List for this Year"
179+
autoComplete="off"
180+
className="dark:text-textInField mb-1 w-full rounded-lg bg-slate-200 p-3 placeholder:text-sm placeholder:text-light-tertiary dark:bg-dark-field dark:text-dark-textColor dark:placeholder:text-dark-tertiary"
181+
value={formData.title}
182+
/>
183+
{errors.title && (
184+
<span className="p-2 text-sm text-red-500">{`${errors.title.message}`}</span>
185+
)}
186+
</div>
187+
188+
<div className="mb-1">
189+
<div className="px-2 py-1 font-medium text-light-secondary dark:text-dark-secondary">
190+
Blog content <Asterisk />
191+
</div>
192+
<textarea
193+
{...register('description')}
194+
placeholder="Start writing here&hellip;"
195+
rows={5}
196+
className="dark:text-textInField w-full rounded-lg bg-slate-200 p-3 placeholder:text-sm placeholder:text-light-tertiary dark:bg-dark-field dark:text-dark-textColor dark:placeholder:text-dark-tertiary"
197+
value={formData.description}
198+
/>
199+
{errors.description && (
200+
<span className="p-2 text-sm text-red-500">{`${errors.description.message}`}</span>
201+
)}
202+
</div>
203+
<div className="mb-2">
204+
<div className="px-2 py-1 font-medium text-light-secondary dark:text-dark-secondary">
205+
Author name <Asterisk />
206+
</div>
207+
<input
208+
{...register('authorName')}
209+
type="text"
210+
placeholder="Shree Sharma"
211+
className="dark:text-textInField mb-1 w-full rounded-lg bg-slate-200 p-3 placeholder:text-sm placeholder:text-light-tertiary dark:bg-dark-field dark:text-dark-textColor dark:placeholder:text-dark-tertiary"
212+
value={formData.authorName}
213+
/>
214+
{errors.authorName && (
215+
<span className="p-2 text-sm text-red-500">{`${errors.authorName.message}`}</span>
216+
)}
217+
</div>
218+
219+
<div className="px-2 py-1 font-medium text-light-secondary dark:text-dark-secondary">
220+
Blog cover image
221+
<span className="text-xs tracking-wide text-dark-tertiary">
222+
&nbsp;(jpg/png/webp)&nbsp;
223+
</span>
224+
<Asterisk />
225+
</div>
226+
<div>
227+
<div className="mb-1 flex justify-between gap-2 sm:gap-4">
228+
<input
229+
{...register('imageLink')}
230+
type="url"
231+
id="imagelink"
232+
name="imageLink"
233+
placeholder="https://&hellip;"
234+
autoComplete="off"
235+
className="dark:text-textInField w-3/4 rounded-lg bg-slate-200 p-3 placeholder:text-sm placeholder:text-light-tertiary dark:bg-dark-field dark:text-dark-textColor dark:placeholder:text-dark-tertiary lg:w-10/12"
236+
value={formData.imageLink}
237+
/>
238+
<button
239+
name="openModal"
240+
type="button"
241+
className="lg:text-md active:scale-click w-1/4 rounded-lg bg-light-primary text-xs text-slate-50 hover:bg-light-primary/80 dark:bg-dark-primary dark:text-dark-card dark:hover:bg-dark-secondary/80 sm:text-sm lg:w-2/12 lg:px-4 lg:py-3"
242+
onClick={() => {
243+
setmodal(true);
244+
}}
245+
>
246+
Pick image
247+
</button>
248+
</div>
249+
{errors.imageLink && (
250+
<span className="p-2 text-sm text-red-500">{`${errors.imageLink.message}`}</span>
251+
)}
252+
</div>
253+
254+
<div className="mb-4 flex flex-col">
255+
<label className="px-2 pb-1 font-medium text-light-secondary dark:text-dark-secondary sm:mr-4 sm:w-fit">
256+
Categories
257+
<span className="text-xs tracking-wide text-dark-tertiary">
258+
&nbsp;(max 3 categories)&nbsp;
259+
</span>
260+
<Asterisk />
261+
</label>
262+
<div>
263+
<div className="flex flex-wrap gap-3 rounded-lg p-2 dark:bg-dark-card dark:p-3">
264+
{categories.map((category, index) => (
265+
<span key={`${category}-${index}`} onClick={() => handleCategoryClick(category)}>
266+
<CategoryPill
267+
category={category}
268+
selected={formData.categories.includes(category)}
269+
disabled={isValidCategory(category)}
270+
/>
271+
</span>
272+
))}
273+
</div>
274+
{errors.categories && (
275+
<span className="p-2 text-sm text-red-500">{`${errors.categories.message}`}</span>
276+
)}
277+
</div>
278+
</div>
279+
280+
<button
281+
name="post-blog"
282+
type="submit"
283+
className="active:scale-click flex w-full items-center justify-center rounded-lg bg-light-primary px-12 py-3 text-base font-semibold text-light hover:bg-light-primary/80 dark:bg-dark-primary dark:text-dark-card dark:hover:bg-dark-secondary/80 sm:mx-1 sm:w-fit"
284+
>
285+
{type === 'new' ? 'Post Blog' : 'Update Blog'}
286+
</button>
287+
</form>
288+
<ModalComponent
289+
selectedImage={selectedImage}
290+
handleImageSelect={handleImageSelect}
291+
handleSelector={handleselector}
292+
setModal={setmodal}
293+
modal={modal}
294+
/>
295+
</div>
296+
</div>
297+
);
298+
}
299+
300+
export default FormBlog;

Diff for: frontend/src/lib/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const isValidImageLink = (value: string) => {
3232
const imageLinkRegex = /\.(jpg|jpeg|png|webp)$/i;
3333
return imageLinkRegex.test(value);
3434
};
35-
export const addBlogSchema = z.object({
35+
export const formBlogSchema = z.object({
3636
title: z.string().refine((value) => value.trim().split(/\s+/).length >= 3, {
3737
message: 'Oops! Title needs more spice. Give it at least 3 words.',
3838
}),
@@ -70,4 +70,4 @@ export interface AuthData {
7070

7171
export type TSignInSchema = z.infer<typeof signInSchema>;
7272
export type TSignUpSchema = z.infer<typeof signUpSchema>;
73-
export type TAddBlogScheme = z.infer<typeof addBlogSchema>;
73+
export type TFormBlogSchema = z.infer<typeof formBlogSchema>;

0 commit comments

Comments
 (0)