From 89521c15af75523f337d8e6b84f70d5cbb62d443 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 2 Feb 2024 23:42:42 +0800 Subject: [PATCH] :sparkles: Posts --- go.mod | 1 - go.sum | 2 - pkg/database/migrator.go | 4 + pkg/models/accounts.go | 13 +- pkg/models/categories.go | 19 +++ pkg/models/posts.go | 17 +++ pkg/models/realms.go | 10 ++ pkg/server/auth.go | 2 +- pkg/server/posts_api.go | 59 ++++++++ pkg/server/startup.go | 3 + pkg/services/categories.go | 40 ++++++ pkg/services/posts.go | 59 ++++++++ pkg/view/index.html | 2 +- pkg/view/package.json | 1 + pkg/view/src/index.tsx | 4 +- pkg/view/src/layouts/RootLayout.tsx | 2 +- pkg/view/src/layouts/shared/Navbar.tsx | 8 +- pkg/view/src/pages/dashboard.tsx | 43 ------ pkg/view/src/pages/feed.module.css | 18 +++ pkg/view/src/pages/feed.tsx | 181 +++++++++++++++++++++++++ 20 files changed, 426 insertions(+), 62 deletions(-) create mode 100644 pkg/models/categories.go create mode 100644 pkg/models/posts.go create mode 100644 pkg/models/realms.go create mode 100644 pkg/server/posts_api.go create mode 100644 pkg/services/categories.go create mode 100644 pkg/services/posts.go delete mode 100644 pkg/view/src/pages/dashboard.tsx create mode 100644 pkg/view/src/pages/feed.module.css create mode 100644 pkg/view/src/pages/feed.tsx diff --git a/go.mod b/go.mod index 033af90..1cfe372 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module code.smartsheep.studio/hydrogen/interactive go 1.21.6 require ( - code.smartsheep.studio/hydrogen/passport v0.0.0-20240201075828-dbc09bd7af8a github.com/go-playground/validator/v10 v10.17.0 github.com/gofiber/fiber/v2 v2.52.0 github.com/golang-jwt/jwt/v5 v5.2.0 diff --git a/go.sum b/go.sum index aeb3b53..913c534 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -code.smartsheep.studio/hydrogen/passport v0.0.0-20240201075828-dbc09bd7af8a h1:66YEiBiB+S7QaUnNqQ/Q8zzgGNkQwcHlJdE2s/RdV6k= -code.smartsheep.studio/hydrogen/passport v0.0.0-20240201075828-dbc09bd7af8a/go.mod h1:LM5I1tdQLXY6n+hou3TPNV/v9hA/cD59zlYpspdzGks= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= diff --git a/pkg/database/migrator.go b/pkg/database/migrator.go index ed17371..4a1ac28 100644 --- a/pkg/database/migrator.go +++ b/pkg/database/migrator.go @@ -8,6 +8,10 @@ import ( func RunMigration(source *gorm.DB) error { if err := source.AutoMigrate( &models.Account{}, + &models.Realm{}, + &models.Category{}, + &models.Tag{}, + &models.Post{}, ); err != nil { return err } diff --git a/pkg/models/accounts.go b/pkg/models/accounts.go index a72340c..7e511e3 100644 --- a/pkg/models/accounts.go +++ b/pkg/models/accounts.go @@ -6,9 +6,12 @@ package models type Account struct { BaseModel - Name string `json:"name"` - Avatar string `json:"avatar"` - EmailAddress string `json:"email_address"` - PowerLevel int `json:"power_level"` - ExternalID uint `json:"external_id"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Description string `json:"description"` + EmailAddress string `json:"email_address"` + PowerLevel int `json:"power_level"` + Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"` + Realms []Realm `json:"realms"` + ExternalID uint `json:"external_id"` } diff --git a/pkg/models/categories.go b/pkg/models/categories.go new file mode 100644 index 0000000..20c232c --- /dev/null +++ b/pkg/models/categories.go @@ -0,0 +1,19 @@ +package models + +type Tag struct { + BaseModel + + Alias string `json:"alias" gorm:"uniqueIndex"` + Name string `json:"name"` + Description string `json:"description"` + Posts []Post `json:"posts" gorm:"many2many:post_tags"` +} + +type Category struct { + BaseModel + + Alias string `json:"alias" gorm:"uniqueIndex"` + Name string `json:"name"` + Description string `json:"description"` + Posts []Post `json:"categories" gorm:"many2many:post_categories"` +} diff --git a/pkg/models/posts.go b/pkg/models/posts.go new file mode 100644 index 0000000..a04dad9 --- /dev/null +++ b/pkg/models/posts.go @@ -0,0 +1,17 @@ +package models + +import "time" + +type Post struct { + BaseModel + + Alias string `json:"alias" gorm:"uniqueIndex"` + Title string `json:"title"` + Content string `json:"content"` + Tags []Tag `gorm:"many2many:post_tags"` + Categories []Category `gorm:"many2many:post_categories"` + PublishedAt time.Time `json:"published_at"` + RealmID *uint `json:"realm_id"` + AuthorID uint `json:"author_id"` + Author Account `json:"author"` +} diff --git a/pkg/models/realms.go b/pkg/models/realms.go new file mode 100644 index 0000000..370d87b --- /dev/null +++ b/pkg/models/realms.go @@ -0,0 +1,10 @@ +package models + +type Realm struct { + BaseModel + + Name string `json:"name"` + Description string `json:"description"` + Posts []Post `json:"posts"` + AccountID uint `json:"account_id"` +} diff --git a/pkg/server/auth.go b/pkg/server/auth.go index 07ea9ee..b221a4d 100644 --- a/pkg/server/auth.go +++ b/pkg/server/auth.go @@ -3,7 +3,7 @@ package server import ( "code.smartsheep.studio/hydrogen/interactive/pkg/database" "code.smartsheep.studio/hydrogen/interactive/pkg/models" - "code.smartsheep.studio/hydrogen/passport/pkg/security" + "code.smartsheep.studio/hydrogen/interactive/pkg/security" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/keyauth" "strconv" diff --git a/pkg/server/posts_api.go b/pkg/server/posts_api.go new file mode 100644 index 0000000..0d7fb2a --- /dev/null +++ b/pkg/server/posts_api.go @@ -0,0 +1,59 @@ +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" +) + +func listPost(c *fiber.Ctx) error { + take := c.QueryInt("take", 0) + offset := c.QueryInt("offset", 0) + + var count int64 + var posts []models.Post + if err := database.C. + Model(&models.Post{}). + Count(&count).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + if err := database.C. + Limit(take). + Offset(offset). + Order("created_at desc"). + Preload("Author"). + Find(&posts).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return c.JSON(fiber.Map{ + "count": count, + "data": posts, + }) + +} + +func createPost(c *fiber.Ctx) error { + user := c.Locals("principal").(models.Account) + + var data struct { + Alias string `json:"alias" validate:"required"` + Title string `json:"title" validate:"required"` + Content string `json:"content" validate:"required"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + } + + if err := BindAndValidate(c, &data); err != nil { + return err + } + + post, err := services.NewPost(user, data.Alias, data.Title, data.Content, data.Categories, data.Tags) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(post) +} diff --git a/pkg/server/startup.go b/pkg/server/startup.go index f81ace2..a41e4c5 100644 --- a/pkg/server/startup.go +++ b/pkg/server/startup.go @@ -61,6 +61,9 @@ func NewServer() { api.Get("/auth", doLogin) api.Get("/auth/callback", doPostLogin) api.Post("/auth/refresh", doRefreshToken) + + api.Get("/posts", listPost) + api.Post("/posts", auth, createPost) } A.Use("/", cache.New(cache.Config{ diff --git a/pkg/services/categories.go b/pkg/services/categories.go new file mode 100644 index 0000000..416d6fa --- /dev/null +++ b/pkg/services/categories.go @@ -0,0 +1,40 @@ +package services + +import ( + "code.smartsheep.studio/hydrogen/interactive/pkg/database" + "code.smartsheep.studio/hydrogen/interactive/pkg/models" + "errors" + "gorm.io/gorm" +) + +func GetCategory(alias, name string) (models.Category, error) { + var category models.Category + if err := database.C.Where(models.Category{Alias: alias}).First(&category).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + category = models.Category{ + Alias: alias, + Name: name, + } + err := database.C.Save(&category).Error + return category, err + } + return category, err + } + return category, nil +} + +func GetTag(alias, name string) (models.Tag, error) { + var tag models.Tag + if err := database.C.Where(models.Category{Alias: alias}).First(&tag).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + tag = models.Tag{ + Alias: alias, + Name: name, + } + err := database.C.Save(&tag).Error + return tag, err + } + return tag, err + } + return tag, nil +} diff --git a/pkg/services/posts.go b/pkg/services/posts.go new file mode 100644 index 0000000..5ad700e --- /dev/null +++ b/pkg/services/posts.go @@ -0,0 +1,59 @@ +package services + +import ( + "code.smartsheep.studio/hydrogen/interactive/pkg/database" + "code.smartsheep.studio/hydrogen/interactive/pkg/models" +) + +func NewPost( + user models.Account, + alias, title, content string, + categories []models.Category, + tags []models.Tag, +) (models.Post, error) { + return NewPostWithRealm(user, nil, alias, title, content, categories, tags) +} + +func NewPostWithRealm( + user models.Account, + realm *models.Realm, + alias, title, content string, + categories []models.Category, + tags []models.Tag, +) (models.Post, error) { + var err error + var post models.Post + for idx, category := range categories { + categories[idx], err = GetCategory(category.Alias, category.Name) + if err != nil { + return post, err + } + } + for idx, tag := range tags { + tags[idx], err = GetTag(tag.Alias, tag.Name) + if err != nil { + return post, err + } + } + + var realmId *uint + if realm != nil { + realmId = &realm.ID + } + + post = models.Post{ + Alias: alias, + Title: title, + Content: content, + Tags: tags, + Categories: categories, + AuthorID: user.ID, + RealmID: realmId, + } + + if err := database.C.Save(&post).Error; err != nil { + return post, err + } + + return post, nil +} diff --git a/pkg/view/index.html b/pkg/view/index.html index 3c940c2..731eaea 100644 --- a/pkg/view/index.html +++ b/pkg/view/index.html @@ -4,7 +4,7 @@ - Goatpass + Goatplaza
diff --git a/pkg/view/package.json b/pkg/view/package.json index e5a0dfa..7a1ab23 100644 --- a/pkg/view/package.json +++ b/pkg/view/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@fortawesome/fontawesome-free": "^6.5.1", "@solidjs/router": "^0.10.10", "solid-js": "^1.8.7", "universal-cookie": "^7.0.2" diff --git a/pkg/view/src/index.tsx b/pkg/view/src/index.tsx index 5f1d767..f45259b 100644 --- a/pkg/view/src/index.tsx +++ b/pkg/view/src/index.tsx @@ -8,6 +8,8 @@ import "./assets/fonts/fonts.css"; import { lazy } from "solid-js"; import { Route, Router } from "@solidjs/router"; +import "@fortawesome/fontawesome-free/css/all.css"; + import RootLayout from "./layouts/RootLayout.tsx"; import { UserinfoProvider } from "./stores/userinfo.tsx"; import { WellKnownProvider } from "./stores/wellKnown.tsx"; @@ -18,7 +20,7 @@ render(() => ( - import("./pages/dashboard.tsx"))} /> + import("./pages/feed.tsx"))} /> import("./pages/auth/callout.tsx"))} /> import("./pages/auth/callback.tsx"))} /> diff --git a/pkg/view/src/layouts/RootLayout.tsx b/pkg/view/src/layouts/RootLayout.tsx index 53f4efa..3b44a28 100644 --- a/pkg/view/src/layouts/RootLayout.tsx +++ b/pkg/view/src/layouts/RootLayout.tsx @@ -40,7 +40,7 @@ export default function RootLayout(props: any) { }> -
{props.children}
+
{props.children}
); } \ No newline at end of file diff --git a/pkg/view/src/layouts/shared/Navbar.tsx b/pkg/view/src/layouts/shared/Navbar.tsx index fb38ed4..7b238da 100644 --- a/pkg/view/src/layouts/shared/Navbar.tsx +++ b/pkg/view/src/layouts/shared/Navbar.tsx @@ -11,13 +11,7 @@ interface MenuItem { export default function Navbar() { const nav: MenuItem[] = [ - { - label: "You", children: [ - { label: "Dashboard", href: "/" }, - { label: "Security", href: "/security" }, - { label: "Personalise", href: "/personalise" } - ] - } + { label: "Feed", href: "/" } ]; const wellKnown = useWellKnown(); diff --git a/pkg/view/src/pages/dashboard.tsx b/pkg/view/src/pages/dashboard.tsx deleted file mode 100644 index c460663..0000000 --- a/pkg/view/src/pages/dashboard.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useUserinfo } from "../stores/userinfo.tsx"; -import { createSignal, Show } from "solid-js"; - -export default function DashboardPage() { - const userinfo = useUserinfo(); - - const [error, setError] = createSignal(null); - - function getGreeting() { - const currentHour = new Date().getHours(); - - if (currentHour >= 0 && currentHour < 12) { - return "Good morning! Wishing you a day filled with joy and success. ☀️"; - } else if (currentHour >= 12 && currentHour < 18) { - return "Afternoon! Hope you have a productive and joyful afternoon! ☀️"; - } else { - return "Good evening! Wishing you a relaxing and pleasant evening. 🌙"; - } - } - - return ( -
-
-

{userinfo?.displayName}

-

{getGreeting()}

-
- -
- - - -
- -
- ); -} \ No newline at end of file diff --git a/pkg/view/src/pages/feed.module.css b/pkg/view/src/pages/feed.module.css new file mode 100644 index 0000000..21584f0 --- /dev/null +++ b/pkg/view/src/pages/feed.module.css @@ -0,0 +1,18 @@ +.publishInput { + outline-style: none !important; + outline-width: 0 !important; +} + +.wrapper { + display: grid; + grid-template-columns: 1fr; + column-gap: 20px; + + max-height: calc(100vh - 64px); +} + +@media (min-width: 768px) { + .wrapper { + grid-template-columns: 1fr 2fr 1fr; + } +} \ No newline at end of file diff --git a/pkg/view/src/pages/feed.tsx b/pkg/view/src/pages/feed.tsx new file mode 100644 index 0000000..e76f151 --- /dev/null +++ b/pkg/view/src/pages/feed.tsx @@ -0,0 +1,181 @@ +import { getAtk, useUserinfo } from "../stores/userinfo.tsx"; +import { createSignal, For, Show } from "solid-js"; + +import styles from "./feed.module.css"; + +export default function DashboardPage() { + const userinfo = useUserinfo(); + + const [error, setError] = createSignal(null); + const [loading, setLoading] = createSignal(true); + const [submitting, setSubmitting] = createSignal(false); + + const [posts, setPosts] = createSignal([]); + const [postCount, setPostCount] = createSignal(0); + + const [page, setPage] = createSignal(1); + + async function readPosts() { + setLoading(true); + const res = await fetch("/api/posts?" + new URLSearchParams({ + take: (10).toString(), + skip: ((page() - 1) * 10).toString() + })); + if (res.status !== 200) { + setError(await res.text()); + } else { + const data = await res.json(); + setPosts(data["data"]); + setPostCount(data["count"]); + setError(null); + } + setLoading(false); + } + + 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 + }) + }); + if (res.status !== 200) { + setError(await res.text()); + } else { + await readPosts(); + form.reset(); + setError(null); + } + setSubmitting(false); + } + + readPosts(); + + return ( +
+ + +
+ +
+ + + +
+ +
+
+
+
+ {userinfo?.displayName.substring(0, 1)}}> + avatar + +
+
+
+ +
+
+ +