Skip to content

Commit 2d74552

Browse files
authored
Merge pull request #150 from luannguyenQV/feat/refactor-post
Feat/refactor post
2 parents 3e954e5 + b0be71a commit 2d74552

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1187
-1367
lines changed

README.md

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
# About next-forum
99

10+
next-forum is a next of forum with newest technology
11+
1012
# Installation
1113

1214
Install
@@ -33,10 +35,10 @@ turbo dev
3335

3436
# Libraries
3537

36-
- ReactJS
38+
- ReactJS - 19.
3739
- TypeScript
38-
- NextJS 14 - App router and server actions
39-
- next-auth
40+
- NextJS 15. - App router and server actions
41+
- next-auth 5.
4042
- Prisma ORM
4143
- Postgres
4244
- Turborepo
@@ -55,36 +57,8 @@ turbo dev
5557
- Husky
5658
- Prettier
5759

58-
# Functions
59-
6060
# Folder structure
6161

62-
│─── src
63-
│────── actions
64-
│────── app
65-
│────── configs
66-
│────── constants
67-
│────── emails
68-
│────── font
69-
│────── hooks
70-
│────── i18n.ts
71-
│────── libs
72-
│────── messages
73-
│────── middleware.ts
74-
│────── molecules
75-
│────── providers
76-
│────── types
77-
└── types
78-
├────── next-auth.d.ts
79-
├── package.json
80-
├── postcss.config.js
81-
├── tailwind.config.js
82-
├── tsconfig.json
83-
├── components.json
84-
├── env.example
85-
├── next-env.d.ts
86-
├── next.config.mjs
87-
8862
## Front side functions
8963

9064
- [x] Register by email or github
@@ -93,7 +67,7 @@ turbo dev
9367
- [x] CRUD post
9468
- [x] List post: Search & filter by top or hot week, month, year, infinity
9569
- [x] Like post
96-
- [x] Comment on post
70+
- [ ] Comment on post
9771
- [x] Share post
9872
- [x] Manage tag
9973
- [x] Follow user
@@ -111,9 +85,6 @@ turbo dev
11185
- [ ] Manage posts
11286
- [ ] Manage images
11387
- [ ] Settings: Header/Menu
88+
- [ ] Manage roles and permission
11489

11590
# DEV NOTES
116-
117-
[[1][DEV NOTE] Initial turbo project and add tailwindcss library](https://dev.to/codeforstartup/dev-note-initial-turbo-project-and-add-tailwindcss-library-4iae)
118-
[[2][DEV NOTE] Integrate prisma and postgres database](https://dev.to/codeforstartup/2dev-note-add-prisma-and-postgres-database-2m84)
119-
[[3][DEV NOTE] Create a form with tiptap and react-textarea-autosize](https://dev.to/codeforstartup/3dev-note-create-a-form-with-tiptap-and-react-textarea-autosize-1cgn)

apps/web/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@
4545
"html-react-parser": "^5.0.11",
4646
"isomorphic-dompurify": "^2.14.0",
4747
"lowlight": "^3.1.0",
48-
"lucide-react": "^0.427.0",
48+
"lucide-react": "^0.469.0",
4949
"negotiator": "^0.6.3",
50-
"next": "15.0.4",
50+
"next": "^15.0.4",
5151
"next-auth": "5.0.0-beta.25",
5252
"next-intl": "^3.26.0",
5353
"next-themes": "^0.2.1",
@@ -56,10 +56,10 @@
5656
"postcss": "^8.4.28",
5757
"prismjs": "^1.29.0",
5858
"qs": "^6.11.2",
59-
"react": "19.0.0",
59+
"react": "^19.0.0-rc.1",
6060
"react-dnd": "^16.0.1",
6161
"react-dnd-html5-backend": "^16.0.1",
62-
"react-dom": "19.0.0",
62+
"react-dom": "^19.0.0-rc.1",
6363
"react-dropzone": "^14.2.3",
6464
"react-editor-js": "^2.1.0",
6565
"react-email": "^3.0.1",
@@ -88,8 +88,8 @@
8888
"devDependencies": {
8989
"@svgr/webpack": "^8.1.0",
9090
"@types/node": "^17.0.12",
91-
"@types/react": "19.0.1",
92-
"@types/react-dom": "19.0.1",
91+
"@types/react": "^19.0.1",
92+
"@types/react-dom": "^19.0.1",
9393
"database": "workspace:^",
9494
"eslint-config-custom": "workspace:*",
9595
"postcss-import": "^15.1.0",
@@ -100,8 +100,8 @@
100100
},
101101
"pnpm": {
102102
"overrides": {
103-
"@types/react": "19.0.1",
104-
"@types/react-dom": "19.0.1"
103+
"@types/react": "^19.0.1",
104+
"@types/react-dom": "^19.0.1"
105105
}
106106
}
107107
}

apps/web/src/actions/protect/postAction.ts

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ import prisma, {
99
createPost,
1010
PostOnUserType,
1111
PostStatus,
12+
Session,
1213
TCreatePostInput,
1314
TPostItem,
1415
updatePost,
1516
updatePostStatus,
1617
} from "database"
18+
import { ActionState, validatedActionWithUser } from "libs/validationAction"
1719
import { toast } from "react-toastify"
20+
import { TUserItem, userSelect } from "types/users"
21+
import * as z from "zod"
1822

19-
import { TUserItem, userSelect } from "@/types/users"
20-
21-
// TODO: move to database package
22-
// Get total actions (like, bookmark) for a post
2323
export const getTotalActions = async ({
2424
postId,
2525
actionType,
@@ -141,8 +141,6 @@ export const removeRelation = async ({
141141
},
142142
}),
143143
])
144-
145-
revalidatePath(`/post/${postSlug}`)
146144
} catch (error) {
147145
throw error
148146
}
@@ -171,17 +169,22 @@ export const getLikers = async ({ postId }: { postId: string }) => {
171169
}
172170
}
173171

174-
export const onTogglePost = async ({ post }: { post: TPostItem }) => {
172+
export async function onTogglePost(
173+
prevState: { post: TPostItem },
174+
_
175+
): Promise<{ post: TPostItem }> {
175176
try {
176-
await updatePostStatus(
177-
post.id,
178-
post.postStatus === PostStatus.DRAFT ? PostStatus.PUBLISHED : PostStatus.DRAFT,
179-
post?.author?.id
177+
const { data } = await updatePostStatus(
178+
prevState.post.id,
179+
prevState.post.postStatus === PostStatus.DRAFT ? PostStatus.PUBLISHED : PostStatus.DRAFT,
180+
prevState.post?.author?.id
180181
)
182+
183+
return { post: data }
181184
} catch (error) {
182185
toast.error(error)
183186
} finally {
184-
revalidatePath(`/post/${post.slug}`)
187+
revalidatePath(`/post/${prevState.post.slug}`)
185188
}
186189
}
187190

@@ -209,3 +212,54 @@ export const handleCreateUpdatePost = async ({
209212
redirect(APP_ROUTES.POST.replace(":postId", newPostId))
210213
}
211214
}
215+
216+
const toggleLikePostSchema = z.object({
217+
postId: z.string(),
218+
postSlug: z.string(),
219+
isLiked: z.boolean(),
220+
})
221+
222+
export type ToggleLikePostSchemaType = ActionState & z.infer<typeof toggleLikePostSchema>
223+
224+
export const onToggleLikePostWithUser = async (
225+
data: ToggleLikePostSchemaType,
226+
formData: FormData
227+
) => {
228+
try {
229+
await (data.isLiked ? removeRelation : addRelation)({
230+
postId: data.postId,
231+
postSlug: data.postSlug,
232+
action: PostOnUserType.LIKE,
233+
})
234+
return {
235+
postId: data.postId,
236+
postSlug: data.postSlug,
237+
isLiked: !data.isLiked,
238+
success: "Success",
239+
}
240+
} catch (error) {
241+
return { error: error instanceof Error ? error.message : "Unknown error" }
242+
} finally {
243+
revalidatePath(`/post/${data.postSlug}`)
244+
}
245+
}
246+
247+
export const handleBookmark = async (prevState: ActionState, _: FormData) => {
248+
try {
249+
await (prevState.isBookmarked ? removeRelation : addRelation)({
250+
postId: prevState.postId,
251+
postSlug: prevState.postSlug,
252+
action: PostOnUserType.BOOKMARK,
253+
})
254+
return {
255+
postId: prevState.postId,
256+
postSlug: prevState.postSlug,
257+
isBookmarked: !prevState.isBookmarked,
258+
success: "Success",
259+
}
260+
} catch (error) {
261+
return { error: error instanceof Error ? error.message : "Unknown error" }
262+
} finally {
263+
revalidatePath(`/post/${prevState.postSlug}`)
264+
}
265+
}

apps/web/src/app/[lang]/(public-fullwidth)/posts/[postId]/page.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ import LikeButton from "molecules/posts/post-detail/like-button"
77
import TableOfContents from "molecules/posts/post-detail/table-of-contents"
88
import BookmarkButton from "molecules/posts/post-item/bookmark-button"
99

10-
import { TSearchParams } from "@/types"
11-
1210
import "./tocbot.css"
1311

1412
import { auth } from "configs/auth"
1513
import { getPost, PostStatus } from "database"
14+
import { TSearchParams } from "types"
1615

1716
export async function generateMetadata(props): Promise<Metadata> {
1817
const params = await props.params
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Based on https://next-saas-start.vercel.app/
2+
import { auth } from "configs/auth"
3+
import { Session } from "next-auth"
4+
import { z } from "zod"
5+
6+
export type ActionState = {
7+
error?: string
8+
success?: string
9+
[key: string]: any // This allows for additional properties
10+
}
11+
12+
type ValidatedActionFunction<S extends z.ZodType<any, any>, T> = (
13+
data: z.infer<S>,
14+
formData: FormData
15+
) => Promise<T>
16+
17+
export function validatedAction<S extends z.ZodType<any, any>, T>(
18+
schema: S,
19+
action: ValidatedActionFunction<S, T>
20+
) {
21+
return async (prevState: ActionState, formData: FormData): Promise<T> => {
22+
const result = schema.safeParse(Object.fromEntries(formData))
23+
if (!result.success) {
24+
return { error: result.error.errors[0].message } as T
25+
}
26+
27+
return action(result.data, formData)
28+
}
29+
}
30+
31+
type ValidatedActionWithUserFunction<S extends z.ZodType<any, any>, T> = (
32+
data: z.infer<S>,
33+
formData: FormData,
34+
session: Session
35+
) => Promise<T>
36+
37+
type ValidatedActionWithUserProps<S extends z.ZodType<any, any>, T> = {
38+
schema?: S
39+
action: ValidatedActionWithUserFunction<S, T>
40+
}
41+
42+
export function validatedActionWithUser<S extends z.ZodType<any, any>, T>({
43+
schema,
44+
action,
45+
}: ValidatedActionWithUserProps<S, T>) {
46+
return async (prevState: ActionState, formData: FormData): Promise<T> => {
47+
const session = await auth()
48+
49+
if (!session) {
50+
throw new Error("User is not authenticated")
51+
}
52+
53+
if (!schema) {
54+
return action(prevState, formData, session)
55+
}
56+
57+
const result = schema.safeParse(Object.fromEntries(formData))
58+
59+
if (!result.success) {
60+
return { error: result.error.errors[0].message } as T
61+
}
62+
63+
return action(result.data, formData, session)
64+
}
65+
}
66+
67+
export function withUser<T>(action: (prevState: ActionState, formData: FormData) => Promise<T>) {
68+
return async (prevState: ActionState, formData: FormData): Promise<T> => {
69+
const session = await auth()
70+
71+
if (!session) {
72+
throw new Error("User is not authenticated")
73+
}
74+
75+
return action(prevState, formData)
76+
}
77+
}

apps/web/src/molecules/posts/post-detail/comments/comment-header.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import React from "react"
44
import { usePathname, useRouter, useSearchParams } from "next/navigation"
55

6+
import { TPostItem } from "database"
7+
import { GetDataSuccessType } from "types"
8+
import { TCommentItem } from "types/comment"
69
import {
710
Select,
811
SelectContent,
@@ -13,10 +16,6 @@ import {
1316
Typography,
1417
} from "ui"
1518

16-
import { GetDataSuccessType } from "@/types"
17-
import { TCommentItem } from "@/types/comment"
18-
import { TPostItem } from "@/types/posts"
19-
2019
type CommentHeaderProps = {
2120
post: TPostItem
2221
comments: GetDataSuccessType<TCommentItem[]>

apps/web/src/molecules/posts/post-detail/edit-post-button/index.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,10 @@ import Link from "next/link"
33

44
import { auth } from "configs/auth"
55
import APP_ROUTES from "constants/routes"
6-
import { PostStatus } from "database"
7-
import { updatePostStatus } from "database/src/posts/queries"
6+
import { TPostItem } from "database"
87
import { LucideEdit } from "lucide-react"
98
import { getTranslations } from "next-intl/server"
10-
import { Button, buttonVariants, cn, toast } from "ui"
11-
12-
import { onTogglePost } from "@/actions/protect/postAction"
13-
import { TPostItem } from "@/types/posts"
9+
import { buttonVariants, cn } from "ui"
1410

1511
import TogglePost from "./toggle-post"
1612

0 commit comments

Comments
 (0)