Skip to content

Commit dbfd9a1

Browse files
committed
refactor(blogs, posts): rename blog references to post in queries, actions, and components
1 parent df7b54c commit dbfd9a1

File tree

24 files changed

+874
-178
lines changed

24 files changed

+874
-178
lines changed

docs/etc/queries.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ Some examples of queries you can run in Supabase.
44

55
```sql
66
create type resource_status as enum ('pending', 'approved', 'rejected');
7-
create type blog_status as enum ('draft', 'published', 'archived');
7+
create type post_status as enum ('draft', 'published', 'archived');
88

99
---
1010

1111
alter table resources
1212
add column status resource_status not null default 'pending';
1313

14-
alter table blogs
15-
add column status blog_status not null default 'draft';
14+
alter table posts
15+
add column status post_status not null default 'draft';
1616
```
1717

1818
```sql

docs/resume.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Software developers of all levels looking to improve their stack, learn new tech
2121

2222
### Resource Navigation
2323

24-
- Feed-style views: explore, trending, blogs, etc.
24+
- Feed-style views: explore, trending, posts, etc.
2525
- Category-based feed views
2626
- Detail page for each resource (`/resources/[slug]`)
2727
- Redirection to the original URL of the resource

src/actions/blogs.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.

src/actions/posts.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"use server";
2+
3+
import type { Post, PostForm } from "@/types";
4+
5+
import { createClient } from "@/lib/supabase/server";
6+
7+
export const getPosts = async (): Promise<Post[]> => {
8+
const supabase = await createClient();
9+
const { data, error } = await supabase
10+
.from("posts")
11+
.select(`*, author:profiles (username, full_name, avatar_url)`);
12+
13+
if (error) throw new Error(error.message);
14+
return data;
15+
};
16+
17+
export const getPostsByAdmin = async (): Promise<Post[]> => {
18+
const supabase = await createClient();
19+
const { data, error } = await supabase
20+
.from("posts")
21+
.select(`*, author:profiles (id, username, full_name, avatar_url)`);
22+
23+
if (error) throw new Error(error.message);
24+
return data;
25+
};
26+
27+
export const getPostBySlug = async (slug: string): Promise<Post> => {
28+
const supabase = await createClient();
29+
const { data, error } = await supabase
30+
.from("posts")
31+
.select(`*, author:profiles (username, full_name, avatar_url)`)
32+
.eq("slug", slug)
33+
.single();
34+
35+
if (error) throw new Error(error.message);
36+
return data;
37+
};
38+
39+
export const createPostByAdmin = async (input: PostForm) => {
40+
const supabase = await createClient();
41+
const { data, error } = await supabase
42+
.from("posts")
43+
.insert(input)
44+
.select()
45+
.single();
46+
47+
if (error) throw new Error(`Error creating post: ${error.message}`);
48+
return data;
49+
};
50+
51+
export const updatePostByAdmin = async (id: string, input: PostForm) => {
52+
const supabase = await createClient();
53+
const { data, error } = await supabase
54+
.from("posts")
55+
.update(input)
56+
.eq("id", id)
57+
.select()
58+
.single();
59+
60+
if (error) throw new Error(`Error updating post: ${error.message}`);
61+
return data;
62+
};

src/actions/profiles.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"use server";
2+
3+
import type { Profile } from "@/types";
4+
5+
import { createClient } from "@/lib/supabase/server";
6+
7+
export const getProfilesByAdmin = async (): Promise<Profile[]> => {
8+
const supabase = await createClient();
9+
const { data, error } = await supabase.from("profiles").select("*");
10+
if (error) throw new Error(error.message);
11+
return data;
12+
};
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"use client";
2+
3+
import type { Post, PostForm, Profile } from "@/types";
4+
5+
import { useState } from "react";
6+
import { useRouter } from "next/navigation";
7+
import { useDisclosure, addToast } from "@heroui/react";
8+
import { useFieldArray, useForm } from "react-hook-form";
9+
import { zodResolver } from "@hookform/resolvers/zod";
10+
import { HeadingDashboard } from "@/components/ui";
11+
import { PostDrawer, PostTable } from "@/components/posts";
12+
import { createPostByAdmin, updatePostByAdmin } from "@/actions/posts";
13+
import { uploadImage } from "@/actions/storage";
14+
import { postsSchema } from "@/lib/zod";
15+
16+
type Props = {
17+
posts: Post[];
18+
authors: Profile[];
19+
};
20+
21+
export default function PostsPageClient({ posts, authors }: Props) {
22+
const router = useRouter();
23+
const { isOpen, onOpen, onClose } = useDisclosure();
24+
const [editingPost, setEditingPost] = useState<Post | null>(null);
25+
const [loading, setLoading] = useState<boolean>(false);
26+
27+
const {
28+
register,
29+
handleSubmit,
30+
control,
31+
reset,
32+
formState: { errors, isSubmitting },
33+
} = useForm<PostForm>({
34+
resolver: zodResolver(postsSchema),
35+
defaultValues: {
36+
name: "",
37+
content: "",
38+
cover_url: undefined,
39+
excerpt: "",
40+
tags: [""],
41+
author: "",
42+
status: "draft",
43+
},
44+
});
45+
46+
const { fields, replace, append, remove } = useFieldArray({
47+
control,
48+
// @ts-expect-error - name is a valid key for FieldArrayWithId
49+
name: "tags" as const,
50+
});
51+
52+
const openEditModal = (post: Post) => {
53+
setEditingPost(post);
54+
reset({
55+
name: post.name,
56+
content: post.content,
57+
cover_url: post.cover_url,
58+
excerpt: post.excerpt,
59+
tags: post.tags,
60+
author: post.author.id,
61+
status: post.status,
62+
});
63+
replace(post.tags);
64+
onOpen();
65+
};
66+
67+
const openAddModal = () => {
68+
setEditingPost(null);
69+
reset({
70+
name: "",
71+
content: "",
72+
cover_url: undefined,
73+
excerpt: "",
74+
tags: [""],
75+
author: "",
76+
status: "draft",
77+
});
78+
onOpen();
79+
};
80+
81+
const onSubmit = async (data: PostForm) => {
82+
setLoading(true);
83+
try {
84+
let coverURL = editingPost?.cover_url || "";
85+
86+
if (data.cover_url_file instanceof File) {
87+
coverURL = await uploadImage({
88+
file: data.cover_url_file,
89+
name: data.name,
90+
bucket: "code-atlas",
91+
folder: "posts",
92+
});
93+
} else if (data.cover_url) {
94+
coverURL = data.cover_url;
95+
}
96+
97+
const payload = {
98+
name: data.name,
99+
content: data.content,
100+
cover_url: coverURL,
101+
excerpt: data.excerpt,
102+
tags: data.tags.filter((tag) => tag.trim() !== ""),
103+
author: data.author,
104+
status: data.status,
105+
};
106+
107+
if (editingPost) await updatePostByAdmin(editingPost.id, payload);
108+
else await createPostByAdmin(payload);
109+
110+
router.refresh();
111+
setEditingPost(null);
112+
onClose();
113+
} catch (error) {
114+
console.error("Submit error:", error);
115+
addToast({ title: "Error", description: "Error submitting form." });
116+
} finally {
117+
setLoading(false);
118+
}
119+
};
120+
121+
return (
122+
<div className="flex flex-col gap-4">
123+
{/* Heading */}
124+
<HeadingDashboard
125+
title="Posts"
126+
count={posts.length}
127+
openAddModal={openAddModal}
128+
/>
129+
130+
{/* Table */}
131+
<PostTable posts={posts} openEditModal={openEditModal} />
132+
133+
{/* Drawer */}
134+
<PostDrawer
135+
editingPost={editingPost}
136+
control={control}
137+
authors={authors}
138+
fields={fields}
139+
errors={errors}
140+
onSubmit={onSubmit}
141+
handleSubmit={handleSubmit}
142+
register={register}
143+
append={append}
144+
remove={remove}
145+
onClose={onClose}
146+
isOpen={isOpen}
147+
loading={loading}
148+
isSubmitting={isSubmitting}
149+
/>
150+
</div>
151+
);
152+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Metadata } from "next";
2+
3+
import { PROJECT_NAME } from "@/config/constants";
4+
import { getPostsByAdmin } from "@/actions/posts";
5+
import { getProfilesByAdmin } from "@/actions/profiles";
6+
import PostsPageClient from "./client";
7+
8+
export const metadata: Metadata = {
9+
title: `Posts - ${PROJECT_NAME}`,
10+
description: "Manage all posts.",
11+
};
12+
13+
export default async function PostsPage() {
14+
const [posts, profiles] = await Promise.all([
15+
getPostsByAdmin(),
16+
getProfilesByAdmin(),
17+
]);
18+
19+
return (
20+
<main className="max-w-screen-xl mx-auto p-4">
21+
<PostsPageClient posts={posts} authors={profiles} />
22+
</main>
23+
);
24+
}

src/app/(public)/blog/[slug]/page.tsx

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/app/(public)/blog/page.tsx

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)