♻️ Interactive v2 #1
| @@ -26,7 +26,7 @@ func createMoment(c *fiber.Ctx) error { | ||||
|  | ||||
| 	var data struct { | ||||
| 		Alias       string              `json:"alias" form:"alias"` | ||||
| 		Content     string              `json:"content" form:"content" validate:"required"` | ||||
| 		Content     string              `json:"content" form:"content" validate:"required,max=1024"` | ||||
| 		Hashtags    []models.Tag        `json:"hashtags" form:"hashtags"` | ||||
| 		Categories  []models.Category   `json:"categories" form:"categories"` | ||||
| 		Attachments []models.Attachment `json:"attachments" form:"attachments"` | ||||
| @@ -89,7 +89,7 @@ func editMoment(c *fiber.Ctx) error { | ||||
|  | ||||
| 	var data struct { | ||||
| 		Alias       string              `json:"alias" form:"alias" validate:"required"` | ||||
| 		Content     string              `json:"content" form:"content" validate:"required"` | ||||
| 		Content     string              `json:"content" form:"content" validate:"required,max=1024"` | ||||
| 		PublishedAt *time.Time          `json:"published_at" form:"published_at"` | ||||
| 		Hashtags    []models.Tag        `json:"hashtags" form:"hashtags"` | ||||
| 		Categories  []models.Category   `json:"categories" form:"categories"` | ||||
|   | ||||
| @@ -83,6 +83,16 @@ func (v *PostTypeContext) GetViaAlias(alias string) (models.Feed, error) { | ||||
| 		return item, err | ||||
| 	} | ||||
|  | ||||
| 	var attachments []models.Attachment | ||||
| 	if err := database.C. | ||||
| 		Model(&models.Attachment{}). | ||||
| 		Where(v.ColumnName+"_id = ?", item.ID). | ||||
| 		Scan(&attachments).Error; err != nil { | ||||
| 		return item, err | ||||
| 	} else { | ||||
| 		item.Attachments = attachments | ||||
| 	} | ||||
|  | ||||
| 	return item, nil | ||||
| } | ||||
|  | ||||
| @@ -96,6 +106,16 @@ func (v *PostTypeContext) Get(id uint, noComments ...bool) (models.Feed, error) | ||||
| 		return item, err | ||||
| 	} | ||||
|  | ||||
| 	var attachments []models.Attachment | ||||
| 	if err := database.C. | ||||
| 		Model(&models.Attachment{}). | ||||
| 		Where(v.ColumnName+"_id = ?", id). | ||||
| 		Scan(&attachments).Error; err != nil { | ||||
| 		return item, err | ||||
| 	} else { | ||||
| 		item.Attachments = attachments | ||||
| 	} | ||||
|  | ||||
| 	return item, nil | ||||
| } | ||||
|  | ||||
| @@ -184,6 +204,41 @@ func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	{ | ||||
| 		var attachments []struct { | ||||
| 			models.Attachment | ||||
|  | ||||
| 			PostID uint `json:"post_id"` | ||||
| 		} | ||||
|  | ||||
| 		itemMap := lo.SliceToMap(items, func(item *models.Feed) (uint, *models.Feed) { | ||||
| 			return item.ID, item | ||||
| 		}) | ||||
|  | ||||
| 		idx := lo.Map(items, func(item *models.Feed, index int) uint { | ||||
| 			return item.ID | ||||
| 		}) | ||||
|  | ||||
| 		if err := database.C. | ||||
| 			Model(&models.Attachment{}). | ||||
| 			Select(v.ColumnName+"_id as post_id, *"). | ||||
| 			Where(v.ColumnName+"_id IN (?)", idx). | ||||
| 			Scan(&attachments).Error; err != nil { | ||||
| 			return items, err | ||||
| 		} | ||||
|  | ||||
| 		list := map[uint][]models.Attachment{} | ||||
| 		for _, info := range attachments { | ||||
| 			list[info.PostID] = append(list[info.PostID], info.Attachment) | ||||
| 		} | ||||
|  | ||||
| 		for k, v := range list { | ||||
| 			if post, ok := itemMap[k]; ok { | ||||
| 				post.Attachments = v | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return items, nil | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -22,6 +22,7 @@ | ||||
|     "universal-cookie": "^7.1.0", | ||||
|     "unocss": "^0.58.5", | ||||
|     "vue": "^3.4.15", | ||||
|     "vue-easy-lightbox": "next", | ||||
|     "vue-router": "^4.2.5", | ||||
|     "vuetify": "^3.5.7" | ||||
|   }, | ||||
|   | ||||
| @@ -9,7 +9,7 @@ | ||||
|       /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex-grow-1"> | ||||
|     <div class="flex-grow-1 relative"> | ||||
|       <div class="font-bold">{{ props.item?.author.nick }}</div> | ||||
|  | ||||
|       <div v-if="props.item?.model_type === 'article'" class="text-xs text-grey-darken-4 mb-2"> | ||||
| @@ -32,27 +32,61 @@ | ||||
|         :reactions="props.item?.reaction_list ?? {}" | ||||
|         @update="updateReactions" | ||||
|       /> | ||||
|  | ||||
|       <div class="mt-1 text-xs text-opacity-60 flex gap-2 items-center"> | ||||
|         <span>Posted at {{ new Date(props.item?.created_at).toLocaleString() }}</span> | ||||
|       </div> | ||||
|  | ||||
|       <v-menu> | ||||
|         <template #activator="{ props }"> | ||||
|           <div class="absolute right-0 top-0"> | ||||
|             <v-btn v-bind="props" icon="mdi-dots-vertical" variant="text" size="x-small" /> | ||||
|           </div> | ||||
|         </template> | ||||
|  | ||||
|         <v-list density="compact" lines="one"> | ||||
|           <v-list-item disabled append-icon="mdi-flag" title="Report" /> | ||||
|           <v-list-item v-if="isOwned" append-icon="mdi-pencil" title="Edit" @click="editPost" /> | ||||
|           <v-list-item v-if="isOwned" append-icon="mdi-delete" title="Delete" /> | ||||
|         </v-list> | ||||
|       </v-menu> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import type { Component } from "vue" | ||||
| import { computed, type Component } from "vue" | ||||
| import { useUserinfo } from "@/stores/userinfo" | ||||
| import { useEditor } from "@/stores/editor" | ||||
| import ArticleContent from "@/components/posts/ArticleContent.vue" | ||||
| import MomentContent from "@/components/posts/MomentContent.vue" | ||||
| import CommentContent from "@/components/posts/CommentContent.vue" | ||||
| import PostAttachment from "./PostAttachment.vue" | ||||
| import PostReaction from "@/components/posts/PostReaction.vue" | ||||
|  | ||||
| const id = useUserinfo() | ||||
|  | ||||
| const props = defineProps<{ item: any; brief?: boolean }>() | ||||
| const emits = defineEmits(["update:item"]) | ||||
|  | ||||
| const editor = useEditor() | ||||
|  | ||||
| const renderer: { [id: string]: Component } = { | ||||
|   article: ArticleContent, | ||||
|   moment: MomentContent, | ||||
|   comment: CommentContent | ||||
| } | ||||
|  | ||||
| const isOwned = computed(() => props.item?.author_id === id.userinfo.data.id) | ||||
|  | ||||
| function editPost() { | ||||
|   editor.related.edit_to = props.item | ||||
|   if(editor.show.hasOwnProperty(props.item.model_type)) { | ||||
|     // @ts-ignore | ||||
|     editor.show[props.item.model_type] = true | ||||
|   } | ||||
| } | ||||
|  | ||||
| function updateReactions(symbol: string, num: number) { | ||||
|   const item = JSON.parse(JSON.stringify(props.item)) | ||||
|   if (item.reaction_list == null) { | ||||
|   | ||||
| @@ -21,6 +21,10 @@ | ||||
|  | ||||
|       <v-card-text> | ||||
|         <v-container class="article-container"> | ||||
|           <v-alert v-if="editor.related.edit_to" class="mb-3" type="info" variant="tonal"> | ||||
|             You are editing a post with alias <b class="font-mono">{{ editor.related.edit_to?.alias }}</b> | ||||
|           </v-alert> | ||||
|  | ||||
|           <v-textarea | ||||
|             required | ||||
|             class="mb-3" | ||||
| @@ -40,6 +44,7 @@ | ||||
|                     variant="solo-filled" | ||||
|                     density="comfortable" | ||||
|                     label="Title" | ||||
|                     :loading="reverting" | ||||
|                     v-model="data.title" | ||||
|                   /> | ||||
|  | ||||
| @@ -103,7 +108,7 @@ | ||||
| import { request } from "@/scripts/request" | ||||
| import { useEditor } from "@/stores/editor" | ||||
| import { getAtk } from "@/stores/userinfo" | ||||
| import { reactive, ref } from "vue" | ||||
| import { reactive, ref, watch } from "vue" | ||||
| import { useRouter } from "vue-router" | ||||
| import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" | ||||
| import Media from "@/components/publish/parts/Media.vue" | ||||
| @@ -116,11 +121,11 @@ const dialogs = reactive({ | ||||
|   media: false | ||||
| }) | ||||
|  | ||||
| const data = reactive<any>({ | ||||
| const data = ref<any>({ | ||||
|   title: "", | ||||
|   content: "", | ||||
|   description: "", | ||||
|   publishedAt: null, | ||||
|   published_at: null, | ||||
|   attachments: [] | ||||
| }) | ||||
|  | ||||
| @@ -128,6 +133,7 @@ const router = useRouter() | ||||
|  | ||||
| const error = ref<string | null>(null) | ||||
| const success = ref(false) | ||||
| const reverting = ref(false) | ||||
| const loading = ref(false) | ||||
| const uploading = ref(false) | ||||
|  | ||||
| @@ -136,15 +142,20 @@ async function postArticle(evt: SubmitEvent) { | ||||
|  | ||||
|   if (uploading.value) return | ||||
|  | ||||
|   if (!data.content) return | ||||
|   if (!data.title || !data.description) return | ||||
|   if (!data.publishedAt) data.publishedAt = new Date().toISOString() | ||||
|   const payload = data.value | ||||
|   console.log(payload) | ||||
|   if (!payload.content) return | ||||
|   if (!payload.title || !payload.description) return | ||||
|   if (!payload.publishedAt) payload.publishedAt = new Date().toISOString() | ||||
|  | ||||
|   const url = editor.related.edit_to ? `/api/p/articles/${editor.related.edit_to?.id}` : "/api/p/articles" | ||||
|   const method = editor.related.edit_to ? "PUT" : "POST" | ||||
|  | ||||
|   loading.value = true | ||||
|   const res = await request("/api/p/articles", { | ||||
|     method: "POST", | ||||
|   const res = await request(url, { | ||||
|     method: method, | ||||
|     headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` }, | ||||
|     body: JSON.stringify(data) | ||||
|     body: JSON.stringify(payload) | ||||
|   }) | ||||
|   if (res.status === 200) { | ||||
|     const data = await res.json() | ||||
| @@ -166,13 +177,22 @@ function pasteMedia(evt: ClipboardEvent) { | ||||
|     Array.from(files).forEach((item) => { | ||||
|       media.value.upload(item).then((meta: any) => { | ||||
|         if (meta) { | ||||
|           data.content += `\n` | ||||
|           data.value.content += `\n` | ||||
|         } | ||||
|       }) | ||||
|     }) | ||||
|     evt.preventDefault() | ||||
|   } | ||||
| } | ||||
|  | ||||
| watch(editor.related, (val) => { | ||||
|   if (val.edit_to && val.edit_to.model_type === "article") { | ||||
|     request(`/api/p/articles/${val.edit_to.alias}`).then(async (res) => { | ||||
|       data.value = await res.json() | ||||
|       data.value.attachments = data.value.attachments ?? [] | ||||
|     }) | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
|   | ||||
| @@ -2,9 +2,20 @@ | ||||
|   <v-card title="Record a moment" :loading="loading"> | ||||
|     <v-form @submit.prevent="postMoment"> | ||||
|       <v-card-text> | ||||
|         <v-textarea required hide-details name="content" variant="outlined" label="What's happened?!" /> | ||||
|         <v-alert v-if="editor.related.edit_to" class="mb-3" type="info" variant="tonal"> | ||||
|           You are editing a post with alias <b class="font-mono">{{ editor.related.edit_to?.alias }}</b> | ||||
|         </v-alert> | ||||
|  | ||||
|         <div class="flex mt-1"> | ||||
|         <v-textarea | ||||
|           required | ||||
|           persistent-counter | ||||
|           variant="outlined" | ||||
|           label="What's happened?!" | ||||
|           counter="1024" | ||||
|           v-model="data.content" | ||||
|         /> | ||||
|  | ||||
|         <div class="flex mt-[-18px]"> | ||||
|           <v-tooltip text="Planned publish" location="start"> | ||||
|             <template #activator="{ props }"> | ||||
|               <v-btn | ||||
| @@ -17,18 +28,6 @@ | ||||
|               /> | ||||
|             </template> | ||||
|           </v-tooltip> | ||||
|           <v-tooltip text="Categories" location="start"> | ||||
|             <template #activator="{ props }"> | ||||
|               <v-btn | ||||
|                 v-bind="props" | ||||
|                 type="button" | ||||
|                 variant="text" | ||||
|                 icon="mdi-shape" | ||||
|                 size="small" | ||||
|                 @click="dialogs.categories = true" | ||||
|               /> | ||||
|             </template> | ||||
|           </v-tooltip> | ||||
|           <v-tooltip text="Media" location="start"> | ||||
|             <template #activator="{ props }"> | ||||
|               <v-btn | ||||
| @@ -53,8 +52,8 @@ | ||||
|     </v-form> | ||||
|   </v-card> | ||||
|  | ||||
|   <planned-publish v-model:show="dialogs.plan" v-model:value="extras.publishedAt" /> | ||||
|   <media v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="extras.attachments" /> | ||||
|   <planned-publish v-model:show="dialogs.plan" v-model:value="data.published_at" /> | ||||
|   <media v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" /> | ||||
|  | ||||
|   <v-snackbar v-model="success" :timeout="3000">Your post has been published.</v-snackbar> | ||||
|   <v-snackbar v-model="uploading" :timeout="-1"> | ||||
| @@ -70,7 +69,7 @@ | ||||
| import { request } from "@/scripts/request" | ||||
| import { useEditor } from "@/stores/editor" | ||||
| import { getAtk } from "@/stores/userinfo" | ||||
| import { reactive, ref } from "vue" | ||||
| import { reactive, ref, watch } from "vue" | ||||
| import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" | ||||
| import Media from "@/components/publish/parts/Media.vue" | ||||
|  | ||||
| @@ -78,12 +77,12 @@ const editor = useEditor() | ||||
|  | ||||
| const dialogs = reactive({ | ||||
|   plan: false, | ||||
|   categories: false, | ||||
|   media: false | ||||
| }) | ||||
|  | ||||
| const extras = reactive({ | ||||
|   publishedAt: null, | ||||
| const data = ref<any>({ | ||||
|   content: "", | ||||
|   published_at: null, | ||||
|   attachments: [] | ||||
| }) | ||||
|  | ||||
| @@ -94,18 +93,18 @@ const uploading = ref(false) | ||||
|  | ||||
| async function postMoment(evt: SubmitEvent) { | ||||
|   const form = evt.target as HTMLFormElement | ||||
|   const data: any = Object.fromEntries(new FormData(form)) | ||||
|   if (!data.hasOwnProperty("content")) return | ||||
|   if (!extras.publishedAt) data["published_at"] = new Date().toISOString() | ||||
|   else data["published_at"] = extras.publishedAt | ||||
|   const payload = data.value | ||||
|   if (!payload.content) return | ||||
|   if (!payload.published_at) payload.published_at = new Date().toISOString() | ||||
|  | ||||
|   data["attachments"] = extras.attachments | ||||
|   const url = editor.related.edit_to ? `/api/p/moments/${editor.related.edit_to?.id}` : "/api/p/moments" | ||||
|   const method = editor.related.edit_to ? "PUT" : "POST" | ||||
|  | ||||
|   loading.value = true | ||||
|   const res = await request("/api/p/moments", { | ||||
|     method: "POST", | ||||
|   const res = await request(url, { | ||||
|     method: method, | ||||
|     headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` }, | ||||
|     body: JSON.stringify(data) | ||||
|     body: JSON.stringify(payload) | ||||
|   }) | ||||
|   if (res.status === 200) { | ||||
|     form.reset() | ||||
| @@ -116,6 +115,12 @@ async function postMoment(evt: SubmitEvent) { | ||||
|   } | ||||
|   loading.value = false | ||||
| } | ||||
|  | ||||
| watch(editor.related, (val) => { | ||||
|   if (val.edit_to) { | ||||
|     data.value = val.edit_to | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   | ||||
| @@ -10,7 +10,8 @@ export const useEditor = defineStore("editor", () => { | ||||
|     comment: false | ||||
|   }) | ||||
|  | ||||
|   const related = reactive<{ comment_to: any; reply_to: any; repost_to: any }>({ | ||||
|   const related = reactive<{ edit_to: any; comment_to: any; reply_to: any; repost_to: any }>({ | ||||
|     edit_to: null, | ||||
|     comment_to: null, | ||||
|     reply_to: null, | ||||
|     repost_to: null | ||||
|   | ||||
| @@ -15,8 +15,8 @@ | ||||
|  | ||||
|             <div class="px-3"> | ||||
|               <post-reaction | ||||
|                 model="articles" | ||||
|                 :item="post" | ||||
|                 :model="route.params.postType" | ||||
|                 :reactions="post?.reaction_list ?? {}" | ||||
|                 @update="updateReactions" | ||||
|               /> | ||||
| @@ -32,9 +32,9 @@ | ||||
|           <comment-list | ||||
|             model="article" | ||||
|             dataset="articles" | ||||
|             v-model:comments="comments" | ||||
|             :item="post" | ||||
|             :alias="route.params.alias" | ||||
|             v-model:comments="comments" | ||||
|           /> | ||||
|         </div> | ||||
|       </v-card> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user