Skip to content

Commit f64e2cf

Browse files
committed
CRUD of blog is completed
1 parent 304d90d commit f64e2cf

40 files changed

+1892
-323
lines changed

Diff for: app/(auth)/login/page.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { startTransition } from "react";
1212
import { useGlobalContext } from '@/context/context';
1313

1414
const initialState: {
15-
message:string | null, errors?: Record<string, string[] | undefined>,
15+
message:string | null,
16+
errors?: Record<string, string[] | undefined>,
1617
isLoggedIn?:boolean,
1718
accessToken?: string | null,
1819
userId?: string | null
@@ -56,7 +57,7 @@ const Login = () => {
5657
},[state])
5758

5859
return (
59-
<div className="grid grid-cols-5 gap-8 mx-5 md:gap-12 max-w-screen" style={{ height: 'calc(100vh - 4rem)' }}>
60+
<div className="grid grid-cols-5 gap-8 mx-5 md:gap-12 max-w-screen mb-20" style={{ height: 'calc(100vh - 4rem)' }}>
6061
<form onSubmit={handleSubmit} method='POST' className="col-span-5 max-w-lg sm:max-w-4xl mt-8 sm:mt-16 sm:col-span-3 md:col-span-2">
6162
<h1 className="heading text-2xl font-bold">Welcome!</h1>
6263
<p className="para mb-5 mt-2">Log In your account.</p>

Diff for: app/(auth)/register/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const Register = () => {
3535
}
3636

3737
return (
38-
<div className="grid grid-cols-5 gap-8 mx-5 md:gap-12 max-w-screen" style={{ height: 'calc(100vh - 4rem)' }}>
38+
<div className="grid grid-cols-5 gap-8 mx-5 md:gap-12 max-w-screen h-auto mb-20">
3939
<form onSubmit={handleSubmit} action={formAction} method='POST' className="col-span-5 max-w-lg sm:max-w-4xl mt-8 sm:mt-16 sm:col-span-3 md:col-span-2">
4040
<h1 className="heading text-2xl font-bold">Register!</h1>
4141
<p className="para mb-5 mt-2">Create a new account.</p>

Diff for: app/(profile)/create-post/[[...blogid]]/page.tsx

+261
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
'use client'
2+
3+
import { CreatePost, UpdatePost } from '@/utils/profileActions';
4+
import {startTransition, useActionState, useEffect, useState} from 'react'
5+
import { FaImage } from "react-icons/fa";
6+
import { getBlog } from '@/utils/getBlog';
7+
import Image from 'next/image';
8+
9+
const initialState : {message: string | null, isCreated: boolean, errors?: Record<string, string[] | undefined>,} = {
10+
message: null,
11+
isCreated: false,
12+
errors:{},
13+
}
14+
15+
type placeholderValuesType = {
16+
id:string,
17+
blogCover: string,
18+
blogName: string,
19+
hook?: string,
20+
desc: string,
21+
blogTags: string[]
22+
}
23+
24+
const placeholderValues: placeholderValuesType = {
25+
id:'',
26+
blogCover: '',
27+
blogName: '',
28+
hook: '',
29+
desc: '',
30+
blogTags: []
31+
}
32+
33+
const CreateBlog = ({params}:{params:{blogid:string}}) => {
34+
const [loading, setLoading] = useState<boolean>(false);
35+
const [selectedTags, setSelectedTags] = useState<string[]>([]);
36+
const [action, setAction] = useState<string>('');
37+
const [previewImage, setPreviewImage] = useState<string | null>(null);
38+
const [placeholder, setPlaceholder] = useState<placeholderValuesType>(placeholderValues);
39+
40+
// Use useEffect to fetch the blog id asynchronously
41+
const [blogid, setBlogid] = useState<string | null>(null);
42+
43+
useEffect(() => {
44+
const fetchBlogId = async () => {
45+
46+
const resolvedBlogId = await params.blogid[0] ;
47+
console.log(resolvedBlogId);
48+
49+
setBlogid(resolvedBlogId);
50+
};
51+
52+
fetchBlogId();
53+
}, [params]);
54+
// const blogid = params.blogid?.[0];
55+
const [state, formAction] = useActionState(blogid ? UpdatePost : CreatePost, initialState);
56+
57+
useEffect(() => {
58+
const fetchBlog = async () => {
59+
if (blogid) {
60+
const blog = await getBlog(blogid);
61+
const { result } = blog;
62+
63+
if (result) {
64+
const { id, blogCover, blogName, hook, desc, blogTags} = result;
65+
66+
const tagNames = blogTags.map((tagObject: { tag: { name: string } }) => tagObject.tag.name);
67+
68+
setPlaceholder({ id, blogCover, blogName, hook, desc, blogTags: tagNames });
69+
setSelectedTags(tagNames);
70+
}
71+
}
72+
};
73+
fetchBlog();
74+
}, [blogid]);
75+
76+
77+
78+
const handleTagClick = (tag: string) => {
79+
setSelectedTags((prevTags) =>
80+
prevTags.includes(tag) ? prevTags.filter((t) => t !== tag) : [...prevTags, tag]
81+
);
82+
};
83+
84+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
85+
const file = e.target.files?.[0];
86+
if (file) {
87+
const reader = new FileReader();
88+
reader.onloadend = () => {
89+
setPreviewImage(reader.result as string);
90+
};
91+
reader.readAsDataURL(file);
92+
}
93+
};
94+
95+
const handleSubmit = async (e: React.FormEvent<HTMLElement>) => {
96+
e.preventDefault();
97+
98+
const formData = new FormData(e.currentTarget as HTMLFormElement);
99+
formData.append('tags', JSON.stringify(selectedTags));
100+
formData.append('action', action);
101+
102+
setLoading(true);
103+
startTransition(() => {
104+
formAction(formData);
105+
setLoading(false);
106+
});
107+
}
108+
109+
useEffect(()=>{
110+
if (state?.isCreated) {
111+
setPlaceholder(placeholderValues);
112+
setPreviewImage(null);
113+
setSelectedTags([]);
114+
}
115+
},[state])
116+
117+
return (
118+
<div className='background p-6 rounded-lg max-w-4xl mb-20'>
119+
<h1 className='text-xl font-bold mb-8'>Create a new blog</h1>
120+
{(state?.message) && <>
121+
<div className="flex gap-2">
122+
<p className="success-message text-green-500 mb-4 font-bold">
123+
{state?.message}
124+
</p>
125+
</div>
126+
</>}
127+
{(state?.errors?.root) && <>
128+
<div className="flex gap-2">
129+
<p className="error-message font-bold mb-4 text-sm">
130+
{state?.errors?.root}
131+
</p>
132+
</div>
133+
</>}
134+
<form onSubmit={handleSubmit} method='POST'>
135+
<p className='label mb-3'>Upload blog cover&nbsp;<span className='asterik'>*</span></p>
136+
<label htmlFor="blogCover" className='border-2 border-dashed rounded-lg h-40 flex flex-col gap-2 justify-center items-center cursor-pointer mb-5 secondaryBg'>
137+
<FaImage className='label text-3xl'/>
138+
<p className='text-xs'>Upload Blog Cover Image</p>
139+
<p className='text-xs'>click to browse</p>
140+
<input
141+
type='file'
142+
name='blogCover'
143+
id='blogCover'
144+
className='hidden'
145+
onChange={handleFileChange}
146+
/>
147+
</label>
148+
<input
149+
type="hidden"
150+
name="blogId"
151+
value={blogid || ''}
152+
/>
153+
{placeholder.blogCover || previewImage ? (
154+
<div className="w-40 h-40 bg-gray-200 mb-5">
155+
<Image
156+
src={placeholder.blogCover || previewImage || ''}
157+
alt="Blog Cover Preview"
158+
className="w-full h-full object-cover rounded-lg"
159+
width={160}
160+
height={160}
161+
priority
162+
/>
163+
</div>
164+
) : null}
165+
{state.errors && state.errors.blogCover && (
166+
<p className="error-message mb-2">{state.errors.blogCover}</p>
167+
)}
168+
<div className="flex flex-col mb-5">
169+
<label className="label" htmlFor="blogName">Blog Name <span className="asterik">*</span></label>
170+
<input
171+
type="text"
172+
name="blogName"
173+
className="input value w-full mt-2"
174+
id="blogName"
175+
required
176+
value={placeholder.blogName}
177+
placeholder='Eg. Next.js 15 is know released.'
178+
onChange={(e) => setPlaceholder((prev) => ({ ...prev, blogName: e.target.value }))}
179+
/>
180+
{state.errors && state.errors.blogName && (
181+
<p className="error-message">{state.errors.blogName}</p>
182+
)}
183+
</div>
184+
<div className="flex flex-col mb-5">
185+
<label className="label" htmlFor="hook">Hook <span className="asterik">*</span></label>
186+
<input
187+
type="text"
188+
name="hook"
189+
className="input value w-full mt-2"
190+
id="hook"
191+
placeholder='Eg. Did you know why next.js is so important? I tell you why'
192+
required
193+
value={placeholder.hook}
194+
onChange={(e) => setPlaceholder((prev) => ({ ...prev, hook: e.target.value }))}
195+
/>
196+
{state.errors && state.errors.hook && (
197+
<p className="error-message">{state.errors.hook}</p>
198+
)}
199+
</div>
200+
<div className="flex flex-col mb-5">
201+
<label className="label" htmlFor="desc">
202+
Description <span className="asterik">*</span>
203+
</label>
204+
<textarea
205+
name="desc"
206+
id="desc"
207+
className="input value w-full mt-2"
208+
rows={10}
209+
required
210+
placeholder='Eg. <h1>What is Next.js.</h1>'
211+
value={placeholder.desc}
212+
onChange={(e) => setPlaceholder((prev) => ({ ...prev, desc: e.target.value }))}
213+
/>
214+
{state.errors?.desc && (
215+
<p className="error-message">{state.errors.desc}</p>
216+
)}
217+
</div>
218+
<p className='label mb-3'>Tags&nbsp;<span className='asterik'>*</span></p>
219+
<div className="myBorder rounded-lg h-24 p-3 gap-2 items-center cursor-pointer mb-5 flex flex-wrap justify-start">
220+
{['Design', 'Research', 'Technology', 'Politics','Development'].map((tag) => (
221+
<span
222+
key={tag}
223+
className={`tag cursor-pointer ${
224+
selectedTags.includes(tag) ? 'tagSelected' : ''
225+
}`}
226+
onClick={() => handleTagClick(tag)}
227+
>
228+
{tag}
229+
</span>
230+
))}
231+
</div>
232+
{state.errors?.tags && (
233+
<p className="error-message">{state.errors.tags}</p>
234+
)}
235+
236+
{loading && (
237+
<div className="fixed top-0 left-0 w-full h-full inset-0 bg-black opacity-75 flex justify-center items-center z-10">
238+
<div className="spinner-border animate-spin inline-block w-16 h-16 border-4 border-t-4 border-blue-600 rounded-full"></div>
239+
</div>
240+
)}
241+
242+
<div className="flex justify-between mt-10">
243+
<button type="submit" onClick={() => setAction('cancel')} className="transparentBtn">
244+
Cancel
245+
</button>
246+
<div className="flex gap-7">
247+
<button type="submit" onClick={() => setAction('draft')} className="secondaryBtn">
248+
Save as Draft
249+
</button>
250+
<button type="submit" onClick={() => setAction('publish')} className="primaryBtn">
251+
{blogid ? 'Update' : 'Publish'}
252+
</button>
253+
</div>
254+
</div>
255+
256+
</form>
257+
</div>
258+
)
259+
}
260+
261+
export default CreateBlog

Diff for: app/(profile)/create-post/page.tsx

-80
This file was deleted.

0 commit comments

Comments
 (0)