♻️ Interactive v2 #1
| @@ -14,9 +14,19 @@ | |||||||
|         <div class="flex-grow-1"> |         <div class="flex-grow-1"> | ||||||
|           <div class="font-bold">{{ props.item?.author.nick }}</div> |           <div class="font-bold">{{ props.item?.author.nick }}</div> | ||||||
|  |  | ||||||
|           <div v-if="props.item?.modal_type === 'article'" class="text-xs text-grey-darken-4 mb-2">Published an article</div> |           <div v-if="props.item?.model_type === 'article'" class="text-xs text-grey-darken-4 mb-2">Published an | ||||||
|  |             article | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|           <component :is="renderer[props.item?.model_type]" v-bind="props" /> |           <component :is="renderer[props.item?.model_type]" v-bind="props" /> | ||||||
|  |  | ||||||
|  |           <post-reaction | ||||||
|  |             size="small" | ||||||
|  |             :item="props.item" | ||||||
|  |             :model="props.item?.model_type ? props.item?.model_type + 's' : 'articles'" | ||||||
|  |             :reactions="props.item?.reaction_list ?? {}" | ||||||
|  |             @update="updateReactions" | ||||||
|  |           /> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </template> |     </template> | ||||||
| @@ -27,13 +37,25 @@ | |||||||
| import type { Component } from "vue"; | import type { Component } from "vue"; | ||||||
| import ArticleContent from "@/components/posts/ArticleContent.vue"; | import ArticleContent from "@/components/posts/ArticleContent.vue"; | ||||||
| import MomentContent from "@/components/posts/MomentContent.vue"; | import MomentContent from "@/components/posts/MomentContent.vue"; | ||||||
|  | import PostReaction from "@/components/posts/PostReaction.vue"; | ||||||
|  |  | ||||||
| const props = defineProps<{ item: any, brief?: boolean, loading?: boolean }>(); | const props = defineProps<{ item: any, brief?: boolean, loading?: boolean }>(); | ||||||
|  | const emits = defineEmits(["update:item"]); | ||||||
|  |  | ||||||
| const renderer: { [id: string]: Component } = { | const renderer: { [id: string]: Component } = { | ||||||
|   article: ArticleContent, |   article: ArticleContent, | ||||||
|   moment: MomentContent |   moment: MomentContent | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | function updateReactions(symbol: string, num: number) { | ||||||
|  |   const item = JSON.parse(JSON.stringify(props.item)); | ||||||
|  |   if (item.reaction_list.hasOwnProperty(symbol)) { | ||||||
|  |     item.reaction_list[symbol] += num; | ||||||
|  |   } else { | ||||||
|  |     item.reaction_list[symbol] = num; | ||||||
|  |   } | ||||||
|  |   emits("update:item", item); | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style scoped> | <style scoped> | ||||||
|   | |||||||
| @@ -5,9 +5,9 @@ | |||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <v-infinite-scroll :items="props.posts" :onLoad="props.loader"> |     <v-infinite-scroll :items="props.posts" :onLoad="props.loader"> | ||||||
|       <template v-for="item in props.posts" :key="item"> |       <template v-for="(item, idx) in props.posts" :key="item"> | ||||||
|         <div class="mb-3 px-1"> |         <div class="mb-3 px-1"> | ||||||
|           <post-item :item="item" brief /> |           <post-item brief :item="item" @update:item="val => updateItem(idx, val)" /> | ||||||
|         </div> |         </div> | ||||||
|       </template> |       </template> | ||||||
|     </v-infinite-scroll> |     </v-infinite-scroll> | ||||||
| @@ -18,4 +18,11 @@ | |||||||
| import PostItem from "@/components/posts/PostItem.vue"; | import PostItem from "@/components/posts/PostItem.vue"; | ||||||
|  |  | ||||||
| const props = defineProps<{ loading: boolean, posts: any[], loader: (opts: any) => Promise<any> }>(); | const props = defineProps<{ loading: boolean, posts: any[], loader: (opts: any) => Promise<any> }>(); | ||||||
|  | const emits = defineEmits(["update:posts"]); | ||||||
|  |  | ||||||
|  | function updateItem(idx: number, data: any) { | ||||||
|  |   const posts = JSON.parse(JSON.stringify(props.posts)); | ||||||
|  |   posts[idx] = data; | ||||||
|  |   emits("update:posts", posts); | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
|   | |||||||
							
								
								
									
										81
									
								
								pkg/views/src/components/posts/PostReaction.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								pkg/views/src/components/posts/PostReaction.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="flex gap-[8px] my-[8px]"> | ||||||
|  |     <v-chip | ||||||
|  |       v-for="[k, v] in Object.entries(props.reactions)" | ||||||
|  |       :color="pickColor()" | ||||||
|  |       :size="props.size" | ||||||
|  |       @click="reactPost(k, emojis[k].attitude)" | ||||||
|  |     > | ||||||
|  |       <div class="ms-2">{{ v }}</div> | ||||||
|  |       <template #prepend>{{ emojis[k].icon }}</template> | ||||||
|  |     </v-chip> | ||||||
|  |  | ||||||
|  |     <v-menu v-if="!props.readonly" location="bottom center"> | ||||||
|  |       <template v-slot:activator="{ props: binding }"> | ||||||
|  |         <v-chip v-bind="binding" :size="props.size" prepend-icon="mdi-emoticon-plus"> | ||||||
|  |           React | ||||||
|  |         </v-chip> | ||||||
|  |       </template> | ||||||
|  |  | ||||||
|  |       <v-list density="compact" lines="one"> | ||||||
|  |         <v-list-item v-for="[k, v] in Object.entries(emojis)" @click="reactPost(k, v.attitude)"> | ||||||
|  |           <v-list-item-title class="font-mono">:{{ k }}:</v-list-item-title> | ||||||
|  |           <template #prepend> | ||||||
|  |             <div class="me-3">{{ v.icon }}</div> | ||||||
|  |           </template> | ||||||
|  |         </v-list-item> | ||||||
|  |       </v-list> | ||||||
|  |     </v-menu> | ||||||
|  |  | ||||||
|  |     <v-snackbar v-model="status.added" :timeout="3000">Your react has been added into post.</v-snackbar> | ||||||
|  |     <v-snackbar v-model="status.removed" :timeout="3000">Your react has been removed from post.</v-snackbar> | ||||||
|  |  | ||||||
|  |     <v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { request } from "@/scripts/request"; | ||||||
|  | import { getAtk } from "@/stores/userinfo"; | ||||||
|  | import { reactive, ref } from "vue"; | ||||||
|  |  | ||||||
|  | const emits = defineEmits(["update"]); | ||||||
|  | const props = defineProps<{ | ||||||
|  |   size?: string, | ||||||
|  |   readonly?: boolean, | ||||||
|  |   model: string, | ||||||
|  |   item: any, | ||||||
|  |   reactions: { [id: string]: number } | ||||||
|  | }>(); | ||||||
|  |  | ||||||
|  | const emojis: { [id: string]: { icon: string, attitude: number } } = { | ||||||
|  |   thumb_up: { icon: "👍", attitude: 1 }, | ||||||
|  |   clap: { icon: "👏", attitude: 1 } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function pickColor(): string { | ||||||
|  |   const colors = ["blue", "green", "purple"]; | ||||||
|  |   const randomIndex = Math.floor(Math.random() * colors.length); | ||||||
|  |   return colors[randomIndex]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const status = reactive({ added: false, removed: false }); | ||||||
|  | const error = ref<string | null>(null); | ||||||
|  |  | ||||||
|  | async function reactPost(symbol: string, attitude: number) { | ||||||
|  |   const res = await request(`/api/${props.model}/${props.item?.id}/react`, { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Authorization": `Bearer ${getAtk()}`, "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ symbol, attitude }) | ||||||
|  |   }); | ||||||
|  |   if (res.status === 201) { | ||||||
|  |     status.added = true; | ||||||
|  |     emits("update", symbol, 1); | ||||||
|  |   } else if (res.status === 204) { | ||||||
|  |     status.removed = true; | ||||||
|  |     emits("update", symbol, -1); | ||||||
|  |   } else { | ||||||
|  |     error.value = await res.text(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
|   <v-container class="flex max-md:flex-col gap-3 overflow-auto max-h-[calc(100vh-64px)] no-scrollbar"> |   <v-container class="flex max-md:flex-col gap-3 overflow-auto max-h-[calc(100vh-64px)] no-scrollbar"> | ||||||
|     <div class="timeline flex-grow-1 mt-[-16px]"> |     <div class="timeline flex-grow-1 mt-[-16px]"> | ||||||
|       <post-list :loading="loading" :posts="posts" :loader="readMore" /> |       <post-list v-model:posts="posts" :loading="loading" :loader="readMore" /> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="aside sticky top-0 w-full h-fit md:min-w-[280px] md:max-w-[320px] max-md:order-first"> |     <div class="aside sticky top-0 w-full h-fit md:min-w-[280px] md:max-w-[320px] max-md:order-first"> | ||||||
|   | |||||||
| @@ -23,8 +23,14 @@ | |||||||
|       </v-card> |       </v-card> | ||||||
|  |  | ||||||
|       <v-card title="Reactions" class="mt-3"> |       <v-card title="Reactions" class="mt-3"> | ||||||
|         <v-list density="compact"> |         <div class="px-[1rem] pb-[0.825rem] mt-[-12px]"> | ||||||
|         </v-list> |           <post-reaction | ||||||
|  |             :item="post" | ||||||
|  |             :model="route.params.postType" | ||||||
|  |             :reactions="post?.reaction_list ?? {}" | ||||||
|  |             @update="updateReactions" | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|       </v-card> |       </v-card> | ||||||
|     </div> |     </div> | ||||||
|   </v-container> |   </v-container> | ||||||
| @@ -35,6 +41,7 @@ import { ref } from "vue"; | |||||||
| import { request } from "@/scripts/request"; | import { request } from "@/scripts/request"; | ||||||
| import { useRoute } from "vue-router"; | import { useRoute } from "vue-router"; | ||||||
| import ArticleContent from "@/components/posts/ArticleContent.vue"; | import ArticleContent from "@/components/posts/ArticleContent.vue"; | ||||||
|  | import PostReaction from "@/components/posts/PostReaction.vue"; | ||||||
|  |  | ||||||
| const loading = ref(false); | const loading = ref(false); | ||||||
| const error = ref<string | null>(null); | const error = ref<string | null>(null); | ||||||
| @@ -55,4 +62,12 @@ async function readPost() { | |||||||
| } | } | ||||||
|  |  | ||||||
| readPost(); | readPost(); | ||||||
|  |  | ||||||
|  | function updateReactions(symbol: string, num: number) { | ||||||
|  |   if (post.value.reaction_list.hasOwnProperty(symbol)) { | ||||||
|  |     post.value.reaction_list[symbol] += num; | ||||||
|  |   } else { | ||||||
|  |     post.value.reaction_list[symbol] = num; | ||||||
|  |   } | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
		Reference in New Issue
	
	Block a user