Skip to content

Commit 1fdadc0

Browse files
Merge pull request #17 from AliNikseresht/dev
add create page and add blogservice-by-id and change some login and c…
2 parents fbadb78 + e702656 commit 1fdadc0

File tree

9 files changed

+311
-29
lines changed

9 files changed

+311
-29
lines changed

src/layout/Layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const Layout = () => {
1414
<div>
1515
{!isAuthRoute && <Sidebar />}
1616
<div
17-
className={`transition-all w-[calc(100vw_-_140px)] ${
17+
className={`transition-all p-3.5 md:p-0 md:w-[calc(100vw_-_140px)] ${
1818
isAuthRoute ? "" : "ms-auto"
1919
}`}
2020
>

src/layout/Sidebar.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const Sidebar = () => {
1212
const location = useLocation();
1313
const navigate = useNavigate();
1414
const { user } = useAuth();
15-
15+
const MAX_AUTHOR_LENGTH = 11;
1616
const hideSidebarPaths = ["/login", "/register"];
1717
const shouldHideSidebar = hideSidebarPaths.includes(location.pathname);
1818

@@ -105,9 +105,13 @@ const Sidebar = () => {
105105
>
106106
<FaRegUser />
107107
</li>
108-
{user ? (
109-
<li className="text-xs mt-2">{user.displayName || "nickname"}</li>
110-
) : null}
108+
<li className="text-xs mt-2">
109+
{user?.email
110+
? user.email.length > MAX_AUTHOR_LENGTH
111+
? `${user.email.slice(0, MAX_AUTHOR_LENGTH)}...`
112+
: user.email
113+
: "Anonymous"}
114+
</li>
111115
</ul>
112116
</div>
113117
</div>

src/pages/Blogs/CreateBlog.tsx

Lines changed: 124 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,128 @@
1+
import { useState } from "react";
2+
import { toast } from "react-toastify";
3+
import { collection, addDoc, serverTimestamp } from "firebase/firestore";
4+
import { useAuth } from "../../hooks/AuthContext";
5+
import { db } from "../../config/firebaseConfig";
16

27
const CreateBlog = () => {
8+
const [title, setTitle] = useState("");
9+
const [content, setContent] = useState("");
10+
const [tags, setTags] = useState("");
11+
const { user } = useAuth();
12+
const [isPreview, setIsPreview] = useState(false);
13+
14+
const handleSubmit = async () => {
15+
if (!title || !content || !tags) {
16+
toast.error("All fields are required!");
17+
return;
18+
}
19+
20+
try {
21+
await addDoc(collection(db, "Blogs"), {
22+
title,
23+
content,
24+
tags: tags.split(",").map((tag) => tag.trim()),
25+
author: user?.displayName || user?.email || "Anonymous",
26+
createdAt: serverTimestamp(),
27+
});
28+
toast.success("Blog created successfully!");
29+
setTitle("");
30+
setContent("");
31+
setTags("");
32+
setIsPreview(false);
33+
} catch (error) {
34+
console.error("Error creating blog: ", error);
35+
toast.error("Failed to create blog. Please try again.");
36+
}
37+
};
38+
339
return (
4-
<div>CreateBlog</div>
5-
)
6-
}
40+
<div className="grid grid-cols-2 gap-8 p-8 h-screen">
41+
{/* Blog Form */}
42+
<form
43+
className="p-6 border rounded-lg bg-gray-900"
44+
onSubmit={(e) => {
45+
e.preventDefault();
46+
setIsPreview(true);
47+
}}
48+
>
49+
<h2 className="text-2xl font-bold mb-6 text-green-400">
50+
Create a New Blog
51+
</h2>
52+
<div className="mb-4">
53+
<label className="block text-white text-sm font-bold mb-2">
54+
Title
55+
</label>
56+
<input
57+
type="text"
58+
className="w-full px-3 py-2 border border-green-400 focus:outline-none bg-transparent text-white"
59+
value={title}
60+
onChange={(e) => setTitle(e.target.value)}
61+
placeholder="Enter blog title"
62+
/>
63+
</div>
64+
<div className="mb-4">
65+
<label className="block text-white text-sm font-bold mb-2">
66+
Content
67+
</label>
68+
<textarea
69+
className="w-full px-3 py-2 border border-green-400 focus:outline-none bg-transparent text-white"
70+
value={content}
71+
onChange={(e) => setContent(e.target.value)}
72+
placeholder="Write your blog content here..."
73+
rows={5}
74+
></textarea>
75+
</div>
76+
<div className="mb-4">
77+
<label className="block text-white text-sm font-bold mb-2">
78+
Tags (comma separated)
79+
</label>
80+
<input
81+
type="text"
82+
className="w-full px-3 py-2 border border-green-400 focus:outline-none bg-transparent text-white"
83+
value={tags}
84+
onChange={(e) => setTags(e.target.value)}
85+
placeholder="e.g., React, JavaScript, Firebase"
86+
/>
87+
</div>
88+
<button
89+
type="submit"
90+
className="border border-green-400 text-green-400 px-4 py-2"
91+
>
92+
Preview Blog
93+
</button>
94+
</form>
95+
96+
{/* Blog Preview */}
97+
{isPreview && (
98+
<div className="p-6 border rounded-lg bg-gray-800">
99+
<h2 className="text-2xl font-bold mb-6 text-green-400">Preview</h2>
100+
<h3 className="text-lg font-semibold mb-2 text-white">
101+
{title || "Untitled"}
102+
</h3>
103+
<p className="mb-4 text-white">{content || "No content provided."}</p>
104+
<div className="mb-4">
105+
<span className="text-sm font-bold text-green-400">Tags: </span>
106+
<span className="text-white">{tags || "No tags"}</span>
107+
</div>
108+
<div className="flex gap-4">
109+
<button
110+
onClick={() => setIsPreview(false)}
111+
className="border border-yellow-500 text-yellow-500 px-4 py-2"
112+
>
113+
Edit Blog
114+
</button>
115+
<button
116+
onClick={handleSubmit}
117+
className="border border-green-400 text-green-400 px-4 py-2"
118+
>
119+
Publish Blog
120+
</button>
121+
</div>
122+
</div>
123+
)}
124+
</div>
125+
);
126+
};
7127

8-
export default CreateBlog
128+
export default CreateBlog;

src/pages/Blogs/ViewBlog.tsx

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,69 @@
1+
import { useNavigate, useParams } from "react-router-dom";
2+
import { useEffect, useState } from "react";
3+
import { TBlog } from "../../types/home";
4+
import Loading from "../../components/ui/Loading";
5+
import { blogByIdService } from "../../services/blogByIdService";
6+
import formatDate from "../../utils/formatDate";
17

28
const ViewBlog = () => {
9+
const navigate = useNavigate();
10+
const { id } = useParams<{ id: string }>();
11+
const [blog, setBlog] = useState<TBlog | null>(null);
12+
const [loading, setLoading] = useState<boolean>(true);
13+
14+
useEffect(() => {
15+
const loadBlog = async () => {
16+
try {
17+
if (id) {
18+
const blogData = await blogByIdService(id);
19+
setBlog(blogData);
20+
}
21+
} catch (error) {
22+
console.error("Error loading blog:", error);
23+
} finally {
24+
setLoading(false);
25+
}
26+
};
27+
28+
loadBlog();
29+
}, [id]);
30+
31+
if (loading) {
32+
return <Loading />;
33+
}
34+
35+
if (!blog) {
36+
return <p>Blog not found.</p>;
37+
}
38+
339
return (
4-
<div>ViewBlog</div>
5-
)
6-
}
40+
<div className="max-w-4xl mx-auto p-4 h-screen flex flex-col gap-4 justify-center">
41+
<button
42+
onClick={() => navigate(-1)}
43+
className="c-green border border-green w-28 py-1 mb-[1.5em]"
44+
>
45+
Back
46+
</button>
47+
<div>
48+
<h1 className="text-4xl font-bold c-green">{blog.title}</h1>
49+
<div className="flex items-center gap-2 my-5">
50+
<p className="c-white text-lg">{blog.author}</p>
51+
<p className="c-green">{formatDate(blog.createdAt.seconds)}</p>
52+
</div>
53+
<div className="text-lg leading-relaxed">{blog.content}</div>
54+
<div className="flex flex-wrap gap-2 mt-6">
55+
{blog.tags.map((tagItem, index) => (
56+
<span
57+
key={index}
58+
className="border border-green px-3 py-1 text-sm rounded-full c-green"
59+
>
60+
#{tagItem}
61+
</span>
62+
))}
63+
</div>
64+
</div>
65+
</div>
66+
);
67+
};
768

8-
export default ViewBlog
69+
export default ViewBlog;

src/pages/Home/Home.tsx

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@ import { fetchBlogs } from "../../services/blogService";
77
import { TBlog } from "../../types/home";
88

99
const Home = () => {
10-
//state
10+
// state
1111
const [blogs, setBlogs] = useState<TBlog[]>([]);
1212
const [loading, setLoading] = useState(true);
13+
const [isLoadingMore, setIsLoadingMore] = useState(false);
14+
const [lastDoc, setLastDoc] = useState<any>(null);
1315

14-
//service
1516
useEffect(() => {
1617
const loadBlogs = async () => {
1718
try {
18-
const blogsData = await fetchBlogs();
19+
const { blogs: blogsData, lastDoc } = await fetchBlogs();
1920
setBlogs(blogsData);
20-
console.log(blogsData);
21+
setLastDoc(lastDoc);
2122
} catch (error) {
2223
console.error("Error loading blogs:", error);
2324
} finally {
@@ -28,7 +29,40 @@ const Home = () => {
2829
loadBlogs();
2930
}, []);
3031

31-
//loading
32+
const loadMoreBlogs = async () => {
33+
if (isLoadingMore || !lastDoc) return;
34+
35+
setIsLoadingMore(true);
36+
try {
37+
const { blogs: moreBlogs, lastDoc: newLastDoc } = await fetchBlogs(
38+
lastDoc
39+
);
40+
setBlogs((prevBlogs) => [...prevBlogs, ...moreBlogs]);
41+
setLastDoc(newLastDoc);
42+
} catch (error) {
43+
console.error("Error loading more blogs:", error);
44+
} finally {
45+
setIsLoadingMore(false);
46+
}
47+
};
48+
49+
useEffect(() => {
50+
const handleScroll = () => {
51+
if (
52+
window.innerHeight + window.scrollY >=
53+
document.documentElement.scrollHeight - 200
54+
) {
55+
loadMoreBlogs();
56+
}
57+
};
58+
59+
window.addEventListener("scroll", handleScroll);
60+
return () => {
61+
window.removeEventListener("scroll", handleScroll);
62+
};
63+
}, [lastDoc, isLoadingMore]);
64+
65+
// loading
3266
if (loading) {
3367
return <Loading />;
3468
}
@@ -44,6 +78,11 @@ const Home = () => {
4478
) : (
4579
blogs.map((blog) => <BlogCard key={blog.id} blog={blog} />)
4680
)}
81+
{isLoadingMore && (
82+
<div className="flex justify-center my-4">
83+
<Loading />
84+
</div>
85+
)}
4786
</div>
4887
);
4988
};

src/pages/Home/_components/BlogCard.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,41 @@
1+
import { Link } from "react-router-dom";
12
import { TBlog } from "../../../types/home";
23
import formatDate from "../../../utils/formatDate";
34

45
const BlogCard = ({ blog }: { blog: TBlog }) => {
6+
const MAX_CONTENT_LENGTH = 300;
7+
const MAX_AUTHOR_LENGTH = 14;
8+
59
return (
6-
<div key={blog.id} className="flex items-start md:h-[13rem] mb-4">
10+
<div key={blog.id} className="flex items-start lg:h-[13rem] mb-4">
711
<div className="hidden md:flex flex-col items-start justify-between relative h-44">
812
<p className="text-left text-3xl uppercase font-semibold w-20">
913
{formatDate(blog.createdAt.seconds)}
1014
</p>
11-
<p className="-rotate-90 absolute -left-[2.05em] bottom-5 text-xs w-36 text-right c-gray">
12-
{blog.author}
15+
<p className="-rotate-90 absolute -left-[2.05em] bottom-5 text-xs w-36 text-right c-green">
16+
{blog.author.length > MAX_AUTHOR_LENGTH
17+
? `${blog.author.slice(0, MAX_AUTHOR_LENGTH)}...`
18+
: blog.author}
1319
</p>
1420
</div>
1521
<div className="flex flex-col justify-between h-full w-full">
1622
<h2 className="c-green md:text-3xl font-semibold">{blog.title}</h2>
17-
<p className="text-xs md:text-sm my-3">{blog.content}</p>
23+
<p className="text-xs md:text-sm my-3">
24+
{blog.content.length > MAX_CONTENT_LENGTH
25+
? `${blog.content.slice(0, MAX_CONTENT_LENGTH)}...`
26+
: blog.content}
27+
<Link
28+
to={`/blog/${blog.id}`}
29+
className="self-start text-sm c-green hover:underline ml-1"
30+
>
31+
Read More →
32+
</Link>
33+
</p>
1834
<div className="flex md:hidden items-center justify-between mb-3">
1935
<p className="text-left text-sm uppercase font-semibold ">
2036
{formatDate(blog.createdAt.seconds)}
2137
</p>
22-
<p className="text-center text-sm c-gray">{blog.author}</p>
38+
<p className="text-center text-sm c-white">{blog.author}</p>
2339
</div>
2440
<div className="flex flex-wrap gap-2">
2541
{blog.tags.map((tagItem, index) => (

src/routes/router.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ export const privateMainRoutes: MainRoute[] = [
4949
title: "Create",
5050
},
5151
{
52-
path: "/blog/:blogid",
52+
path: "/blog/:id",
5353
element: <ViewBlog />,
5454
title: "View",
5555
},
5656
{
57-
path: "/blog/:blogid/edit",
57+
path: "/blog/:id/edit",
5858
element: <EditBlog />,
5959
title: "Edit",
6060
},

0 commit comments

Comments
 (0)