diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c22897c Binary files /dev/null and b/.DS_Store differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..60dc64b --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..b6f0309 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/hy_interactive + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..8029dc3 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..ad50001 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pkg/.DS_Store b/pkg/.DS_Store new file mode 100644 index 0000000..7a08108 Binary files /dev/null and b/pkg/.DS_Store differ diff --git a/pkg/models/posts.go b/pkg/models/posts.go index 035dbbb..02cd576 100644 --- a/pkg/models/posts.go +++ b/pkg/models/posts.go @@ -5,6 +5,7 @@ import "time" type Post struct { BaseModel + // TODO Introduce thumbnail Alias string `json:"alias" gorm:"uniqueIndex"` Title string `json:"title"` Content string `json:"content"` diff --git a/pkg/server/creators_api.go b/pkg/server/creators_api.go new file mode 100644 index 0000000..47f2fbe --- /dev/null +++ b/pkg/server/creators_api.go @@ -0,0 +1,95 @@ +package server + +import ( + "code.smartsheep.studio/hydrogen/interactive/pkg/database" + "code.smartsheep.studio/hydrogen/interactive/pkg/models" + "code.smartsheep.studio/hydrogen/interactive/pkg/services" + "github.com/gofiber/fiber/v2" + "github.com/samber/lo" + "time" +) + +func getOwnPost(c *fiber.Ctx) error { + user := c.Locals("principal").(models.Account) + + id := c.Params("postId") + take := c.QueryInt("take", 0) + offset := c.QueryInt("offset", 0) + + tx := database.C.Where(&models.Post{ + Alias: id, + AuthorID: user.ID, + }) + + post, err := services.GetPost(tx) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + tx = database.C. + Where(&models.Post{ReplyID: &post.ID}). + Where("published_at <= ? OR published_at IS NULL", time.Now()). + Order("created_at desc") + + var count int64 + if err := tx. + Model(&models.Post{}). + Count(&count).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + posts, err := services.ListPost(tx, take, offset) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(fiber.Map{ + "data": post, + "count": count, + "related": posts, + }) +} + +func listOwnPost(c *fiber.Ctx) error { + take := c.QueryInt("take", 0) + offset := c.QueryInt("offset", 0) + realmId := c.QueryInt("realmId", 0) + + user := c.Locals("principal").(models.Account) + + tx := database.C. + Where(&models.Post{AuthorID: user.ID}). + Where("published_at <= ? OR published_at IS NULL", time.Now()). + Order("created_at desc") + + if realmId > 0 { + tx = tx.Where(&models.Post{RealmID: lo.ToPtr(uint(realmId))}) + } else { + tx = tx.Where("realm_id IS NULL") + } + + if len(c.Query("category")) > 0 { + tx = services.FilterPostWithCategory(tx, c.Query("category")) + } + + if len(c.Query("tag")) > 0 { + tx = services.FilterPostWithTag(tx, c.Query("tag")) + } + + var count int64 + if err := tx. + Model(&models.Post{}). + Count(&count).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + posts, err := services.ListPost(tx, take, offset) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(fiber.Map{ + "count": count, + "data": posts, + }) +} diff --git a/pkg/server/posts_api.go b/pkg/server/posts_api.go index 5759d2d..6b45ddf 100644 --- a/pkg/server/posts_api.go +++ b/pkg/server/posts_api.go @@ -19,7 +19,7 @@ func getPost(c *fiber.Ctx) error { tx := database.C.Where(&models.Post{ Alias: id, - }) + }).Where("published_at <= ? OR published_at IS NULL", time.Now()) post, err := services.GetPost(tx) if err != nil { diff --git a/pkg/server/realms_api.go b/pkg/server/realms_api.go index dee15ec..a269d23 100644 --- a/pkg/server/realms_api.go +++ b/pkg/server/realms_api.go @@ -40,6 +40,17 @@ func listOwnedRealm(c *fiber.Ctx) error { return c.JSON(realms) } +func listAvailableRealm(c *fiber.Ctx) error { + user := c.Locals("principal").(models.Account) + + realms, err := services.ListRealmIsAvailable(user) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(realms) +} + func createRealm(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) if user.PowerLevel < 10 { diff --git a/pkg/server/startup.go b/pkg/server/startup.go index 9d56f0f..800351e 100644 --- a/pkg/server/startup.go +++ b/pkg/server/startup.go @@ -76,8 +76,12 @@ func NewServer() { api.Put("/posts/:postId", auth, editPost) api.Delete("/posts/:postId", auth, deletePost) + api.Get("/creators/posts", auth, listOwnPost) + api.Get("/creators/posts/:postId", auth, getOwnPost) + api.Get("/realms", listRealm) api.Get("/realms/me", auth, listOwnedRealm) + api.Get("/realms/me/available", auth, listAvailableRealm) api.Get("/realms/:realmId", getRealm) api.Post("/realms", auth, createRealm) api.Post("/realms/:realmId/invite", auth, inviteRealm) diff --git a/pkg/services/realms.go b/pkg/services/realms.go index 43e2be7..3f74d2b 100644 --- a/pkg/services/realms.go +++ b/pkg/services/realms.go @@ -3,6 +3,7 @@ package services import ( "code.smartsheep.studio/hydrogen/interactive/pkg/database" "code.smartsheep.studio/hydrogen/interactive/pkg/models" + "github.com/samber/lo" ) func ListRealm() ([]models.Realm, error) { @@ -23,6 +24,28 @@ func ListRealmWithUser(user models.Account) ([]models.Realm, error) { return realms, nil } +func ListRealmIsAvailable(user models.Account) ([]models.Realm, error) { + var realms []models.Realm + var members []models.RealmMember + if err := database.C.Where(&models.RealmMember{ + AccountID: user.ID, + }).Find(&members).Error; err != nil { + return realms, err + } + + idx := lo.Map(members, func(item models.RealmMember, index int) uint { + return item.RealmID + }) + + if err := database.C.Where(&models.Realm{ + IsPublic: true, + }).Or("id IN ?", idx).Find(&realms).Error; err != nil { + return realms, err + } + + return realms, nil +} + func NewRealm(user models.Account, name, description string, isPublic bool) (models.Realm, error) { realm := models.Realm{ Name: name, diff --git a/pkg/view/package.json b/pkg/view/package.json index 62e620c..112e107 100644 --- a/pkg/view/package.json +++ b/pkg/view/package.json @@ -12,6 +12,7 @@ "@fortawesome/fontawesome-free": "^6.5.1", "@solidjs/router": "^0.10.10", "artplayer": "^5.1.1", + "cherry-markdown": "^0.8.38", "dompurify": "^3.0.8", "flv.js": "^1.6.2", "hls.js": "^1.5.3", diff --git a/pkg/view/src/components/LoadingAnimation.tsx b/pkg/view/src/components/LoadingAnimation.tsx new file mode 100644 index 0000000..ea2dd8f --- /dev/null +++ b/pkg/view/src/components/LoadingAnimation.tsx @@ -0,0 +1,8 @@ +export default function LoadingAnimation() { + return ( +
+

+

Listening to the latest news...

+
+ ) +} \ No newline at end of file diff --git a/pkg/view/src/components/PostAttachments.module.css b/pkg/view/src/components/posts/PostAttachments.module.css similarity index 100% rename from pkg/view/src/components/PostAttachments.module.css rename to pkg/view/src/components/posts/PostAttachments.module.css diff --git a/pkg/view/src/components/PostAttachments.tsx b/pkg/view/src/components/posts/PostAttachments.tsx similarity index 100% rename from pkg/view/src/components/PostAttachments.tsx rename to pkg/view/src/components/posts/PostAttachments.tsx diff --git a/pkg/view/src/components/PostPublish.tsx b/pkg/view/src/components/posts/PostEditActions.tsx similarity index 52% rename from pkg/view/src/components/PostPublish.tsx rename to pkg/view/src/components/posts/PostEditActions.tsx index 8ee090c..c914073 100644 --- a/pkg/view/src/components/PostPublish.tsx +++ b/pkg/view/src/components/posts/PostEditActions.tsx @@ -1,119 +1,28 @@ -import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js"; -import { getAtk, useUserinfo } from "../stores/userinfo.tsx"; +import { closeModel, openModel } from "../../scripts/modals.ts"; +import { createSignal, For, Match, Show, Switch } from "solid-js"; +import { getAtk, useUserinfo } from "../../stores/userinfo.tsx"; import styles from "./PostPublish.module.css"; -import { closeModel, openModel } from "../scripts/modals.ts"; -export default function PostPublish(props: { - replying?: any, - reposting?: any, +export default function PostEditActions(props: { editing?: any, - realmId?: number, - onReset: () => void, + onInputAlias: (value: string) => void, + onInputPublish: (value: string) => void, + onInputAttachments: (value: any[]) => void, + onInputCategories: (categories: any[]) => void, + onInputTags: (tags: any[]) => void, onError: (message: string | null) => void, - onPost: () => void }) { - const userinfo = useUserinfo(); + const userinfo = useUserinfo() - if (!userinfo?.isLoggedIn) { - return ( -
-
-

Login!

-

Or keep silent.

-
-
- ); - } - - const [submitting, setSubmitting] = createSignal(false); const [uploading, setUploading] = createSignal(false); - const [attachments, setAttachments] = createSignal([]); - const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>([]); - const [tags, setTags] = createSignal<{ alias: string, name: string }[]>([]); + const [attachments, setAttachments] = createSignal(props.editing?.attachments ?? []); + const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>(props.editing?.categories ?? []); + const [tags, setTags] = createSignal<{ alias: string, name: string }[]>(props.editing?.tags ?? []); const [attachmentMode, setAttachmentMode] = createSignal(0); - createEffect(() => { - setAttachments(props.editing?.attachments ?? []); - setCategories(props.editing?.categories ?? []); - setTags(props.editing?.tags ?? []); - }, [props.editing]); - - async function doPost(evt: SubmitEvent) { - evt.preventDefault(); - - const form = evt.target as HTMLFormElement; - const data = Object.fromEntries(new FormData(form)); - if (!data.content) return; - - setSubmitting(true); - const res = await fetch("/api/posts", { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${getAtk()}` - }, - body: JSON.stringify({ - alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""), - title: data.title, - content: data.content, - attachments: attachments(), - categories: categories(), - tags: tags(), - realm_id: data.publish_in_realm ? props.realmId : undefined, - published_at: data.published_at ? new Date(data.published_at as string) : new Date(), - repost_to: props.reposting?.id, - reply_to: props.replying?.id - }) - }); - if (res.status !== 200) { - props.onError(await res.text()); - } else { - form.reset(); - props.onError(null); - props.onPost(); - } - setSubmitting(false); - } - - async function doEdit(evt: SubmitEvent) { - evt.preventDefault(); - - const form = evt.target as HTMLFormElement; - const data = Object.fromEntries(new FormData(form)); - if (!data.content) return; - if (uploading()) return; - - setSubmitting(true); - const res = await fetch(`/api/posts/${props.editing?.id}`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${getAtk()}` - }, - body: JSON.stringify({ - alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""), - title: data.title, - content: data.content, - attachments: attachments(), - categories: categories(), - tags: tags(), - realm_id: props.realmId, - published_at: data.published_at ? new Date(data.published_at as string) : new Date() - }) - }); - if (res.status !== 200) { - props.onError(await res.text()); - } else { - form.reset(); - props.onError(null); - props.onPost(); - } - setSubmitting(false); - } - async function uploadAttachment(evt: SubmitEvent) { evt.preventDefault(); @@ -148,6 +57,7 @@ export default function PostPublish(props: { ...data, author_id: userinfo?.profiles?.id }])); + props.onInputCategories(categories()) form.reset(); } @@ -160,6 +70,7 @@ export default function PostPublish(props: { if (!data.name) return; setCategories(categories().concat([data as any])); + props.onInputCategories(categories()) form.reset(); } @@ -176,156 +87,83 @@ export default function PostPublish(props: { if (!data.name) return; setTags(tags().concat([data as any])); + props.onInputTags(tags()) form.reset(); } function removeTag(target: any) { setTags(tags().filter(item => item.alias !== target.alias)); - } - - function resetForm() { - setAttachments([]); - setCategories([]); - setTags([]); - props.onReset(); + props.onInputTags(tags()) } return ( <> -
(props.editing ? doEdit : doPost)(evt)} onReset={() => resetForm()}> -
-
-
- {userinfo?.displayName.substring(0, 1)}}> - avatar - +
+ + + + +
+ + + -
- -
-
- - - - - - - - - - - - -
-
- -
-
-
- -