♻️ Interactive v2 #1
| @@ -5,10 +5,8 @@ import "time" | ||||
| type Post struct { | ||||
| 	BaseModel | ||||
|  | ||||
| 	Alias            string        `json:"alias" gorm:"uniqueIndex"` | ||||
| 	Title            string        `json:"title"` | ||||
| 	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"` | ||||
| 	Attachments      []Attachment  `json:"attachments"` | ||||
| 	LikedAccounts    []PostLike    `json:"liked_accounts"` | ||||
|   | ||||
| @@ -12,12 +12,12 @@ import ( | ||||
| func getOwnPost(c *fiber.Ctx) error { | ||||
| 	user := c.Locals("principal").(models.Account) | ||||
|  | ||||
| 	id := c.Params("postId") | ||||
| 	id, _ := c.ParamsInt("postId", 0) | ||||
| 	take := c.QueryInt("take", 0) | ||||
| 	offset := c.QueryInt("offset", 0) | ||||
|  | ||||
| 	tx := database.C.Where(&models.Post{ | ||||
| 		Alias:    id, | ||||
| 		BaseModel: models.BaseModel{ID: uint(id)}, | ||||
| 		AuthorID:  user.ID, | ||||
| 	}) | ||||
|  | ||||
|   | ||||
| @@ -13,12 +13,12 @@ import ( | ||||
| ) | ||||
|  | ||||
| func getPost(c *fiber.Ctx) error { | ||||
| 	id := c.Params("postId") | ||||
| 	id, _ := c.ParamsInt("postId", 0) | ||||
| 	take := c.QueryInt("take", 0) | ||||
| 	offset := c.QueryInt("offset", 0) | ||||
|  | ||||
| 	tx := database.C.Where(&models.Post{ | ||||
| 		Alias: id, | ||||
| 		BaseModel: models.BaseModel{ID: uint(id)}, | ||||
| 	}).Where("published_at <= ? OR published_at IS NULL", time.Now()) | ||||
|  | ||||
| 	post, err := services.GetPost(tx) | ||||
| @@ -162,8 +162,6 @@ func createPost(c *fiber.Ctx) error { | ||||
| 	post, err := services.NewPost( | ||||
| 		user, | ||||
| 		realm, | ||||
| 		data.Alias, | ||||
| 		data.Title, | ||||
| 		data.Content, | ||||
| 		data.Attachments, | ||||
| 		data.Categories, | ||||
| @@ -207,8 +205,6 @@ func editPost(c *fiber.Ctx) error { | ||||
|  | ||||
| 	post, err := services.EditPost( | ||||
| 		post, | ||||
| 		data.Alias, | ||||
| 		data.Title, | ||||
| 		data.Content, | ||||
| 		data.PublishedAt, | ||||
| 		data.Categories, | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import ( | ||||
| 	"strings" | ||||
| 	"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/middleware/cache" | ||||
| 	"github.com/gofiber/fiber/v2/middleware/cors" | ||||
|   | ||||
| @@ -20,7 +20,7 @@ func PreloadRelatedPost(tx *gorm.DB) *gorm.DB { | ||||
| 		Preload("Author"). | ||||
| 		Preload("Attachments"). | ||||
| 		Preload("Categories"). | ||||
| 		Preload("Tags"). | ||||
| 		Preload("Hashtags"). | ||||
| 		Preload("RepostTo"). | ||||
| 		Preload("ReplyTo"). | ||||
| 		Preload("RepostTo.Author"). | ||||
| @@ -29,8 +29,8 @@ func PreloadRelatedPost(tx *gorm.DB) *gorm.DB { | ||||
| 		Preload("ReplyTo.Attachments"). | ||||
| 		Preload("RepostTo.Categories"). | ||||
| 		Preload("ReplyTo.Categories"). | ||||
| 		Preload("RepostTo.Tags"). | ||||
| 		Preload("ReplyTo.Tags") | ||||
| 		Preload("RepostTo.Hashtags"). | ||||
| 		Preload("ReplyTo.Hashtags") | ||||
| } | ||||
|  | ||||
| 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( | ||||
| 	user models.Account, | ||||
| 	realm *models.Realm, | ||||
| 	alias, title, content string, | ||||
| 	content string, | ||||
| 	attachments []models.Attachment, | ||||
| 	categories []models.Category, | ||||
| 	tags []models.Tag, | ||||
| @@ -202,11 +202,9 @@ func NewPost( | ||||
| 	} | ||||
|  | ||||
| 	post = models.Post{ | ||||
| 		Alias:       alias, | ||||
| 		Title:       title, | ||||
| 		Content:     content, | ||||
| 		Attachments: attachments, | ||||
| 		Tags:        tags, | ||||
| 		Hashtags:    tags, | ||||
| 		Categories:  categories, | ||||
| 		AuthorID:    user.ID, | ||||
| 		RealmID:     realmId, | ||||
| @@ -225,7 +223,7 @@ func NewPost( | ||||
| 			BaseModel: models.BaseModel{ID: *post.ReplyID}, | ||||
| 		}).Preload("Author").First(&op).Error; err == nil { | ||||
| 			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( | ||||
| 					op.Author, | ||||
| 					fmt.Sprintf("%s replied you", user.Name), | ||||
| @@ -252,7 +250,7 @@ func NewPost( | ||||
| 		}) | ||||
|  | ||||
| 		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( | ||||
| 				account, | ||||
| 				fmt.Sprintf("%s just posted a post", user.Name), | ||||
| @@ -270,7 +268,7 @@ func NewPost( | ||||
|  | ||||
| func EditPost( | ||||
| 	post models.Post, | ||||
| 	alias, title, content string, | ||||
| 	content string, | ||||
| 	publishedAt *time.Time, | ||||
| 	categories []models.Category, | ||||
| 	tags []models.Tag, | ||||
| @@ -294,11 +292,9 @@ func EditPost( | ||||
| 		publishedAt = lo.ToPtr(time.Now()) | ||||
| 	} | ||||
|  | ||||
| 	post.Alias = alias | ||||
| 	post.Title = title | ||||
| 	post.Content = content | ||||
| 	post.PublishedAt = *publishedAt | ||||
| 	post.Tags = tags | ||||
| 	post.Hashtags = tags | ||||
| 	post.Categories = categories | ||||
| 	post.Attachments = attachments | ||||
|  | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										6
									
								
								pkg/views/embed.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								pkg/views/embed.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| package views | ||||
|  | ||||
| import "embed" | ||||
|  | ||||
| //go:embed all:dist | ||||
| var FS embed.FS | ||||
| @@ -2,7 +2,7 @@ | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <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"> | ||||
|     <title>Goatplaza</title> | ||||
|   </head> | ||||
|   | ||||
| @@ -13,9 +13,11 @@ | ||||
|     "format": "prettier --write src/" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@fontsource/roboto": "^5.0.8", | ||||
|     "@mdi/font": "^7.4.47", | ||||
|     "@unocss/reset": "^0.58.5", | ||||
|     "pinia": "^2.1.7", | ||||
|     "universal-cookie": "^7.1.0", | ||||
|     "unocss": "^0.58.5", | ||||
|     "vue": "^3.4.15", | ||||
|     "vue-router": "^4.2.5", | ||||
|   | ||||
							
								
								
									
										21
									
								
								pkg/views/public/favicon.svg
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										21
									
								
								pkg/views/public/favicon.svg
									
									
									
									
									
										Executable 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 | 
							
								
								
									
										11
									
								
								pkg/views/src/assets/utils.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								pkg/views/src/assets/utils.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| html, body, #app, .v-application { | ||||
|     overflow: auto !important; | ||||
| } | ||||
|  | ||||
| .no-scrollbar { | ||||
|     scrollbar-width: none; | ||||
| } | ||||
|  | ||||
| .no-scrollbar::-webkit-scrollbar { | ||||
|     width: 0; | ||||
| } | ||||
							
								
								
									
										31
									
								
								pkg/views/src/components/posts/PostItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								pkg/views/src/components/posts/PostItem.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										21
									
								
								pkg/views/src/components/posts/PostList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								pkg/views/src/components/posts/PostList.vue
									
									
									
									
									
										Normal 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> | ||||
| @@ -1,13 +1,24 @@ | ||||
| <template> | ||||
|   <v-navigation-drawer v-model="drawerOpen" color="grey-lighten-5" floating> | ||||
|     <div class="d-flex text-center justify-center items-center h-[64px]"> | ||||
|       <h1>Goatplaza</h1> | ||||
|     </div> | ||||
|     <v-list density="compact" nav> | ||||
|     </v-list> | ||||
|   </v-navigation-drawer> | ||||
|  | ||||
|   <v-app-bar height="64" color="primary" scroll-behavior="elevate" flat> | ||||
|     <div class="container mx-auto px-5"> | ||||
|       <v-app-bar-nav-icon variant="text" @click.stop="toggleDrawer"></v-app-bar-nav-icon> | ||||
|     <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" /> | ||||
|  | ||||
|       <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> | ||||
|   </v-app-bar> | ||||
|  | ||||
| @@ -16,12 +27,16 @@ | ||||
|   </v-main> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { ref } from "vue" | ||||
| <script setup lang="ts"> | ||||
| import { ref } from "vue"; | ||||
|  | ||||
| const drawerOpen = ref(true) | ||||
| const navigationMenu = [ | ||||
|   { name: "Explore", icon: "mdi-compass", to: "explore" } | ||||
| ]; | ||||
|  | ||||
| const drawerOpen = ref(true); | ||||
|  | ||||
| function toggleDrawer() { | ||||
|   drawerOpen.value = !drawerOpen.value | ||||
|   drawerOpen.value = !drawerOpen.value; | ||||
| } | ||||
| </script> | ||||
|   | ||||
| @@ -1,27 +1,35 @@ | ||||
| import "virtual:uno.css" | ||||
| import "virtual:uno.css"; | ||||
|  | ||||
| import { createApp } from "vue" | ||||
| import { createPinia } from "pinia" | ||||
| import "./assets/utils.css"; | ||||
|  | ||||
| import "vuetify/styles" | ||||
| import { createVuetify } from "vuetify" | ||||
| import * as components from "vuetify/components" | ||||
| import * as directives from "vuetify/directives" | ||||
| import { createApp } from "vue"; | ||||
| import { createPinia } from "pinia"; | ||||
|  | ||||
| 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 router from "./router" | ||||
| import "@mdi/font/css/materialdesignicons.min.css"; | ||||
| 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( | ||||
|   createVuetify({ | ||||
|     components, | ||||
|     directives, | ||||
|     blueprint: md3, | ||||
|     theme: { | ||||
|       defaultTheme: "original", | ||||
|       themes: { | ||||
|         light: { | ||||
|         original: { | ||||
|           colors: { | ||||
|             primary: "#4a5099", | ||||
|             secondary: "#2196f3", | ||||
|             accent: "#009688", | ||||
| @@ -32,10 +40,11 @@ app.use( | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| ) | ||||
| ); | ||||
|  | ||||
| app.use(createPinia()) | ||||
| app.use(router) | ||||
| app.use(createPinia()); | ||||
| app.use(router); | ||||
|  | ||||
| app.mount("#app") | ||||
| app.mount("#app"); | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { createRouter, createWebHistory } from "vue-router" | ||||
| import MasterLayout from "@/layouts/master.vue" | ||||
| import LandingPage from "@/views/landing.vue" | ||||
|  | ||||
| const router = createRouter({ | ||||
|   history: createWebHistory(import.meta.env.BASE_URL), | ||||
| @@ -11,8 +10,8 @@ const router = createRouter({ | ||||
|       children: [ | ||||
|         { | ||||
|           path: "/", | ||||
|           name: "landing", | ||||
|           component: LandingPage | ||||
|           name: "explore", | ||||
|           component: () => import("@/views/explore.vue") | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   | ||||
							
								
								
									
										10
									
								
								pkg/views/src/scripts/request.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								pkg/views/src/scripts/request.ts
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										56
									
								
								pkg/views/src/stores/userinfo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								pkg/views/src/stores/userinfo.ts
									
									
									
									
									
										Normal 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 } | ||||
| }) | ||||
							
								
								
									
										14
									
								
								pkg/views/src/stores/wellKnown.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								pkg/views/src/stores/wellKnown.ts
									
									
									
									
									
										Normal 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 } | ||||
| }) | ||||
							
								
								
									
										59
									
								
								pkg/views/src/views/explore.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								pkg/views/src/views/explore.vue
									
									
									
									
									
										Normal 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> | ||||
| @@ -1,3 +0,0 @@ | ||||
| <template> | ||||
|     <div>Good morning!</div> | ||||
| </template> | ||||
| @@ -4,6 +4,8 @@ | ||||
|   "exclude": ["src/**/__tests__/*"], | ||||
|   "compilerOptions": { | ||||
|     "composite": true, | ||||
|     "allowJs": true, | ||||
|     "checkJs": true, | ||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", | ||||
|  | ||||
|     "baseUrl": ".", | ||||
|   | ||||
| @@ -12,5 +12,11 @@ export default defineConfig({ | ||||
|     alias: { | ||||
|       "@": fileURLToPath(new URL("./src", import.meta.url)) | ||||
|     } | ||||
|   }, | ||||
|   server: { | ||||
|     proxy: { | ||||
|       "/.well-known": "http://localhost:8445", | ||||
|       "/api": "http://localhost:8445" | ||||
|     } | ||||
|   } | ||||
| }) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user