♻️ Brand new post list

This commit is contained in:
LittleSheep 2024-03-02 20:01:59 +08:00
parent 178f80c707
commit 3ae72cd9e0
23 changed files with 314 additions and 65 deletions

View File

@ -5,10 +5,8 @@ import "time"
type Post struct { type Post struct {
BaseModel BaseModel
Alias string `json:"alias" gorm:"uniqueIndex"`
Title string `json:"title"`
Content string `json:"content"` Content string `json:"content"`
Tags []Tag `json:"tags" gorm:"many2many:post_tags"` Hashtags []Tag `json:"tags" gorm:"many2many:post_tags"`
Categories []Category `json:"categories" gorm:"many2many:post_categories"` Categories []Category `json:"categories" gorm:"many2many:post_categories"`
Attachments []Attachment `json:"attachments"` Attachments []Attachment `json:"attachments"`
LikedAccounts []PostLike `json:"liked_accounts"` LikedAccounts []PostLike `json:"liked_accounts"`

View File

@ -12,12 +12,12 @@ import (
func getOwnPost(c *fiber.Ctx) error { func getOwnPost(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account) user := c.Locals("principal").(models.Account)
id := c.Params("postId") id, _ := c.ParamsInt("postId", 0)
take := c.QueryInt("take", 0) take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0) offset := c.QueryInt("offset", 0)
tx := database.C.Where(&models.Post{ tx := database.C.Where(&models.Post{
Alias: id, BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID, AuthorID: user.ID,
}) })

View File

@ -13,12 +13,12 @@ import (
) )
func getPost(c *fiber.Ctx) error { func getPost(c *fiber.Ctx) error {
id := c.Params("postId") id, _ := c.ParamsInt("postId", 0)
take := c.QueryInt("take", 0) take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0) offset := c.QueryInt("offset", 0)
tx := database.C.Where(&models.Post{ tx := database.C.Where(&models.Post{
Alias: id, BaseModel: models.BaseModel{ID: uint(id)},
}).Where("published_at <= ? OR published_at IS NULL", time.Now()) }).Where("published_at <= ? OR published_at IS NULL", time.Now())
post, err := services.GetPost(tx) post, err := services.GetPost(tx)
@ -162,8 +162,6 @@ func createPost(c *fiber.Ctx) error {
post, err := services.NewPost( post, err := services.NewPost(
user, user,
realm, realm,
data.Alias,
data.Title,
data.Content, data.Content,
data.Attachments, data.Attachments,
data.Categories, data.Categories,
@ -207,8 +205,6 @@ func editPost(c *fiber.Ctx) error {
post, err := services.EditPost( post, err := services.EditPost(
post, post,
data.Alias,
data.Title,
data.Content, data.Content,
data.PublishedAt, data.PublishedAt,
data.Categories, data.Categories,

View File

@ -5,7 +5,7 @@ import (
"strings" "strings"
"time" "time"
"code.smartsheep.studio/hydrogen/identity/pkg/views" "code.smartsheep.studio/hydrogen/interactive/pkg/views"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cache" "github.com/gofiber/fiber/v2/middleware/cache"
"github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/cors"

View File

@ -20,7 +20,7 @@ func PreloadRelatedPost(tx *gorm.DB) *gorm.DB {
Preload("Author"). Preload("Author").
Preload("Attachments"). Preload("Attachments").
Preload("Categories"). Preload("Categories").
Preload("Tags"). Preload("Hashtags").
Preload("RepostTo"). Preload("RepostTo").
Preload("ReplyTo"). Preload("ReplyTo").
Preload("RepostTo.Author"). Preload("RepostTo.Author").
@ -29,8 +29,8 @@ func PreloadRelatedPost(tx *gorm.DB) *gorm.DB {
Preload("ReplyTo.Attachments"). Preload("ReplyTo.Attachments").
Preload("RepostTo.Categories"). Preload("RepostTo.Categories").
Preload("ReplyTo.Categories"). Preload("ReplyTo.Categories").
Preload("RepostTo.Tags"). Preload("RepostTo.Hashtags").
Preload("ReplyTo.Tags") Preload("ReplyTo.Hashtags")
} }
func FilterPostWithCategory(tx *gorm.DB, alias string) *gorm.DB { func FilterPostWithCategory(tx *gorm.DB, alias string) *gorm.DB {
@ -161,7 +161,7 @@ WHERE t.id IN ?`, prefix, prefix, prefix, prefix, prefix), postIds).Scan(&reactI
func NewPost( func NewPost(
user models.Account, user models.Account,
realm *models.Realm, realm *models.Realm,
alias, title, content string, content string,
attachments []models.Attachment, attachments []models.Attachment,
categories []models.Category, categories []models.Category,
tags []models.Tag, tags []models.Tag,
@ -202,11 +202,9 @@ func NewPost(
} }
post = models.Post{ post = models.Post{
Alias: alias,
Title: title,
Content: content, Content: content,
Attachments: attachments, Attachments: attachments,
Tags: tags, Hashtags: tags,
Categories: categories, Categories: categories,
AuthorID: user.ID, AuthorID: user.ID,
RealmID: realmId, RealmID: realmId,
@ -225,7 +223,7 @@ func NewPost(
BaseModel: models.BaseModel{ID: *post.ReplyID}, BaseModel: models.BaseModel{ID: *post.ReplyID},
}).Preload("Author").First(&op).Error; err == nil { }).Preload("Author").First(&op).Error; err == nil {
if op.Author.ID != user.ID { if op.Author.ID != user.ID {
postUrl := fmt.Sprintf("https://%s/posts/%s", viper.GetString("domain"), post.Alias) postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), post.ID)
err := NotifyAccount( err := NotifyAccount(
op.Author, op.Author,
fmt.Sprintf("%s replied you", user.Name), fmt.Sprintf("%s replied you", user.Name),
@ -252,7 +250,7 @@ func NewPost(
}) })
for _, account := range accounts { for _, account := range accounts {
postUrl := fmt.Sprintf("https://%s/posts/%s", viper.GetString("domain"), post.Alias) postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), post.ID)
err := NotifyAccount( err := NotifyAccount(
account, account,
fmt.Sprintf("%s just posted a post", user.Name), fmt.Sprintf("%s just posted a post", user.Name),
@ -270,7 +268,7 @@ func NewPost(
func EditPost( func EditPost(
post models.Post, post models.Post,
alias, title, content string, content string,
publishedAt *time.Time, publishedAt *time.Time,
categories []models.Category, categories []models.Category,
tags []models.Tag, tags []models.Tag,
@ -294,11 +292,9 @@ func EditPost(
publishedAt = lo.ToPtr(time.Now()) publishedAt = lo.ToPtr(time.Now())
} }
post.Alias = alias
post.Title = title
post.Content = content post.Content = content
post.PublishedAt = *publishedAt post.PublishedAt = *publishedAt
post.Tags = tags post.Hashtags = tags
post.Categories = categories post.Categories = categories
post.Attachments = attachments post.Attachments = attachments

Binary file not shown.

6
pkg/views/embed.go Normal file
View File

@ -0,0 +1,6 @@
package views
import "embed"
//go:embed all:dist
var FS embed.FS

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.svg">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Goatplaza</title> <title>Goatplaza</title>
</head> </head>

View File

@ -13,9 +13,11 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@fontsource/roboto": "^5.0.8",
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",
"@unocss/reset": "^0.58.5", "@unocss/reset": "^0.58.5",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"universal-cookie": "^7.1.0",
"unocss": "^0.58.5", "unocss": "^0.58.5",
"vue": "^3.4.15", "vue": "^3.4.15",
"vue-router": "^4.2.5", "vue-router": "^4.2.5",

21
pkg/views/public/favicon.svg Executable file
View File

@ -0,0 +1,21 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
<title>SmartSheep Logo</title>
<defs>
<image width="124" height="198" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHwAAADGCAMAAAAnkRSfAAAAAXNSR0IB2cksfwAAAq9QTFRFAAAA////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////J9QBRAAAAOV0Uk5TAA0WEiFfjMLMr40/BRdmtuz//vbQYAQmpfLdQAGovyURj/iOD0r07loDGJfbPDTf/bhd94QLdPrrVQLWcPmtHU31egknzehRg9Ev1xnzbgYHhfzmTMorSOSbFJloKsniReXDKG/7kQ6m8WMw3ue7I4cKNdTZOFTqsR5/Hzra0zZY7al4Ir5SPWHvnpbG4MRrlKJs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"/>
<image width="122" height="142" id="img2" href="data:image/png;base64,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"/>
</defs>
<style>
.s0 { fill: #ffffff;stroke: #000000;stroke-miterlimit:100;stroke-width: 56 }
.s1 { fill: #4750a3;stroke: #000000;stroke-miterlimit:100;stroke-width: 56 }
</style>
<path id="Wool" fill-rule="evenodd" class="s0" d="m128 608.4c0 95.9 77.4 173.6 172.8 173.6h441.6c84.8 0 153.6-69.1 153.6-154.3 0-74.6-52.8-136.9-122.9-151.1 4.9-12.9 7.7-27 7.7-41.7 0-63.9-51.6-115.8-115.2-115.8-23.6 0-45.7 7.3-64 19.6-33.2-57.9-95.2-96.7-166.4-96.7-106.1 0-192 86.3-192 192.9 0 3.2 0.1 6.5 0.2 9.7-67.2 23.8-115.4 88.1-115.4 163.8z"/>
<g id="Crystal">
<path id="Crystal" class="s1" d="m699 224l138.6 80v160l-138.6 80-138.6-80v-160z"/>
<use id="Highlight" href="#img1" x="688" y="255"/>
</g>
<g id="Horn">
</g>
<g id="Face">
<use id="Slime" href="#img2" x="233" y="538"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -0,0 +1,11 @@
html, body, #app, .v-application {
overflow: auto !important;
}
.no-scrollbar {
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
width: 0;
}

View File

@ -0,0 +1,31 @@
<template>
<v-card>
<template #text>
<div class="flex gap-3">
<div>
<v-avatar
color="grey-lighten-2"
icon="mdi-account-circle"
class="rounded-card"
:src="props.item?.author.avatar"
/>
</div>
<div>
<div class="font-bold">{{ props.item?.author.nick }}</div>
{{ props.item?.content }}
</div>
</div>
</template>
</v-card>
</template>
<script setup lang="ts">
const props = defineProps<{ item: any }>();
</script>
<style scoped>
.rounded-card {
border-radius: 8px;
}
</style>

View File

@ -0,0 +1,21 @@
<template>
<div class="post-list">
<div v-if="props.loading" class="text-center py-8">
<v-progress-circular indeterminate />
</div>
<v-infinite-scroll :items="props.posts" :onLoad="props.loader">
<template v-for="(item, index) in props.posts" :key="item">
<div class="mb-3 px-1">
<post-item :item="item" />
</div>
</template>
</v-infinite-scroll>
</div>
</template>
<script setup lang="ts">
import PostItem from "@/components/posts/PostItem.vue";
const props = defineProps<{ loading: boolean, posts: any[], loader: (opts: any) => Promise<any> }>();
</script>

View File

@ -1,13 +1,24 @@
<template> <template>
<v-navigation-drawer v-model="drawerOpen" color="grey-lighten-5" floating> <v-navigation-drawer v-model="drawerOpen" color="grey-lighten-5" floating>
<div class="d-flex text-center justify-center items-center h-[64px]"> <v-list density="compact" nav>
<h1>Goatplaza</h1> </v-list>
</div>
</v-navigation-drawer> </v-navigation-drawer>
<v-app-bar height="64" color="primary" scroll-behavior="elevate" flat> <v-app-bar height="64" color="primary" scroll-behavior="elevate" flat>
<div class="container mx-auto px-5"> <div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center">
<v-app-bar-nav-icon variant="text" @click.stop="toggleDrawer"></v-app-bar-nav-icon> <v-app-bar-nav-icon variant="text" @click.stop="toggleDrawer" />
<router-link :to="{ name: 'explore' }">
<h2 class="ml-2 text-lg font-500">Goatplaza</h2>
</router-link>
<v-spacer />
<v-tooltip v-for="item in navigationMenu" :text="item.name" location="bottom">
<template #activator="{ props }">
<v-btn flat v-bind="props" :to="{ name: item.to }" size="small" :icon="item.icon" />
</template>
</v-tooltip>
</div> </div>
</v-app-bar> </v-app-bar>
@ -16,12 +27,16 @@
</v-main> </v-main>
</template> </template>
<script setup> <script setup lang="ts">
import { ref } from "vue" import { ref } from "vue";
const drawerOpen = ref(true) const navigationMenu = [
{ name: "Explore", icon: "mdi-compass", to: "explore" }
];
const drawerOpen = ref(true);
function toggleDrawer() { function toggleDrawer() {
drawerOpen.value = !drawerOpen.value drawerOpen.value = !drawerOpen.value;
} }
</script> </script>

View File

@ -1,27 +1,35 @@
import "virtual:uno.css" import "virtual:uno.css";
import { createApp } from "vue" import "./assets/utils.css";
import { createPinia } from "pinia"
import "vuetify/styles" import { createApp } from "vue";
import { createVuetify } from "vuetify" import { createPinia } from "pinia";
import * as components from "vuetify/components"
import * as directives from "vuetify/directives"
import "@mdi/font/css/materialdesignicons.min.css" import "vuetify/styles";
import { createVuetify } from "vuetify";
import { md3 } from "vuetify/blueprints";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
import index from "./index.vue" import "@mdi/font/css/materialdesignicons.min.css";
import router from "./router" import "@fontsource/roboto/latin.css";
import "@unocss/reset/tailwind.css";
const app = createApp(index) import index from "./index.vue";
import router from "./router";
const app = createApp(index);
app.use( app.use(
createVuetify({ createVuetify({
components, components,
directives, directives,
blueprint: md3,
theme: { theme: {
defaultTheme: "original",
themes: { themes: {
light: { original: {
colors: {
primary: "#4a5099", primary: "#4a5099",
secondary: "#2196f3", secondary: "#2196f3",
accent: "#009688", accent: "#009688",
@ -32,10 +40,11 @@ app.use(
} }
} }
} }
}
}) })
) );
app.use(createPinia()) app.use(createPinia());
app.use(router) app.use(router);
app.mount("#app") app.mount("#app");

View File

@ -1,6 +1,5 @@
import { createRouter, createWebHistory } from "vue-router" import { createRouter, createWebHistory } from "vue-router"
import MasterLayout from "@/layouts/master.vue" import MasterLayout from "@/layouts/master.vue"
import LandingPage from "@/views/landing.vue"
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -11,8 +10,8 @@ const router = createRouter({
children: [ children: [
{ {
path: "/", path: "/",
name: "landing", name: "explore",
component: LandingPage component: () => import("@/views/explore.vue")
} }
] ]
} }

View File

@ -0,0 +1,10 @@
declare global {
interface Window {
__LAUNCHPAD_TARGET__?: string
}
}
export async function request(input: string, init?: RequestInit) {
const prefix = window.__LAUNCHPAD_TARGET__ ?? ""
return await fetch(prefix + input, init)
}

View File

@ -0,0 +1,56 @@
import Cookie from "universal-cookie"
import { defineStore } from "pinia"
import { ref } from "vue"
import { request } from "@/scripts/request"
export interface Userinfo {
isReady: boolean
isLoggedIn: boolean
displayName: string
data: any
}
const defaultUserinfo: Userinfo = {
isReady: false,
isLoggedIn: false,
displayName: "Citizen",
data: null
}
export function getAtk(): string {
return new Cookie().get("identity_auth_key")
}
export function checkLoggedIn(): boolean {
return new Cookie().get("identity_auth_key")
}
export const useUserinfo = defineStore("userinfo", () => {
const userinfo = ref(defaultUserinfo)
const isReady = ref(false)
async function readProfiles() {
if (!checkLoggedIn()) {
isReady.value = true;
}
const res = await request("/api/users/me", {
headers: { "Authorization": `Bearer ${getAtk()}` }
});
if (res.status !== 200) {
return;
}
const data = await res.json();
userinfo.value = {
isReady: true,
isLoggedIn: true,
displayName: data["nick"],
data: data
};
}
return { userinfo, isReady, readProfiles }
})

View File

@ -0,0 +1,14 @@
import { request } from "@/scripts/request"
import { defineStore } from "pinia"
import { ref } from "vue"
export const useWellKnown = defineStore("well-known", () => {
const wellKnown = ref({})
async function readWellKnown() {
const res = await request("/.well-known")
wellKnown.value = await res.json()
}
return { wellKnown, readWellKnown }
})

View File

@ -0,0 +1,59 @@
<template>
<v-container class="flex max-md:flex-col gap-3 overflow-auto max-h-[calc(100vh-72px)] no-scrollbar">
<div class="timeline flex-grow-1 mt-[-16px]">
<post-list :loading="loading" :posts="posts" :loader="readMore" />
</div>
<div class="aside sticky top-0 w-full h-fit md:min-w-[280px] md:max-w-[320px]">
<v-card title="Categories">
<v-list density="compact">
</v-list>
</v-card>
</div>
</v-container>
</template>
<script setup lang="ts">
import PostList from "@/components/posts/PostList.vue";
import { reactive, ref } from "vue";
import { request } from "@/scripts/request";
const error = ref<string | null>(null);
const loading = ref(false);
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
const posts = ref<any[]>([]);
async function readPosts() {
loading.value = true;
const res = await request(`/api/posts?` + new URLSearchParams({
take: pagination.pageSize.toString(),
offset: ((pagination.page - 1) * pagination.pageSize).toString()
}));
if (res.status !== 200) {
loading.value = false;
error.value = await res.text();
} else {
error.value = null;
loading.value = false;
const data = await res.json();
pagination.total = data["count"];
posts.value.push(...data["data"]);
}
}
async function readMore({ done }: any) {
// Reach the end of data
if (pagination.total <= pagination.page * pagination.pageSize) {
done("empty");
return;
}
pagination.page++;
await readPosts();
done("ok");
}
readPosts();
</script>

View File

@ -1,3 +0,0 @@
<template>
<div>Good morning!</div>
</template>

View File

@ -4,6 +4,8 @@
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"allowJs": true,
"checkJs": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".", "baseUrl": ".",

View File

@ -12,5 +12,11 @@ export default defineConfig({
alias: { alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)) "@": fileURLToPath(new URL("./src", import.meta.url))
} }
},
server: {
proxy: {
"/.well-known": "http://localhost:8445",
"/api": "http://localhost:8445"
}
} }
}) })