♻️ Interactive v2 #1
| @@ -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"` | ||||||
|   | |||||||
| @@ -12,13 +12,13 @@ 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, | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	post, err := services.GetPost(tx) | 	post, err := services.GetPost(tx) | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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" | ||||||
|   | |||||||
| @@ -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
									
								
							
							
						
						
									
										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"> | <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> | ||||||
|   | |||||||
| @@ -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
									
								
							
							
						
						
									
										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> | <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> | ||||||
|   | |||||||
| @@ -1,41 +1,50 @@ | |||||||
| 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: { | ||||||
|           primary: "#4a5099", |           colors: { | ||||||
|           secondary: "#2196f3", |             primary: "#4a5099", | ||||||
|           accent: "#009688", |             secondary: "#2196f3", | ||||||
|           error: "#f44336", |             accent: "#009688", | ||||||
|           warning: "#ff9800", |             error: "#f44336", | ||||||
|           info: "#03a9f4", |             warning: "#ff9800", | ||||||
|           success: "#4caf50" |             info: "#03a9f4", | ||||||
|  |             success: "#4caf50" | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
| ) | ); | ||||||
|  |  | ||||||
| app.use(createPinia()) | app.use(createPinia()); | ||||||
| app.use(router) | app.use(router); | ||||||
|  |  | ||||||
| app.mount("#app") | app.mount("#app"); | ||||||
|   | |||||||
| @@ -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") | ||||||
|         } |         } | ||||||
|       ] |       ] | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										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__/*"], |   "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": ".", | ||||||
|   | |||||||
| @@ -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" | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user