♻️ Interactive v2 #1
							
								
								
									
										152
									
								
								pkg/views/src/components/publish/ArticleEditor.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								pkg/views/src/components/publish/ArticleEditor.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | |||||||
|  | <template> | ||||||
|  |   <v-card :rounded="false"> | ||||||
|  |     <v-form @submit.prevent="postArticle"> | ||||||
|  |       <v-toolbar> | ||||||
|  |         <div class="article-toolbar"> | ||||||
|  |           <div class="flex"> | ||||||
|  |             <v-btn type="button" icon="mdi-close" @click="editor.show.article = false"></v-btn> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|  |           <v-toolbar-title>Write an article</v-toolbar-title> | ||||||
|  |  | ||||||
|  |           <div class="flex justify-end items-center"> | ||||||
|  |             <v-tooltip text="Publish"> | ||||||
|  |               <template #activator="{ props: binding }"> | ||||||
|  |                 <v-btn type="submit" icon="mdi-publish" v-bind="binding" :loading="loading" /> | ||||||
|  |               </template> | ||||||
|  |             </v-tooltip> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </v-toolbar> | ||||||
|  |  | ||||||
|  |       <v-card-text> | ||||||
|  |         <v-container class="article-container"> | ||||||
|  |           <v-textarea | ||||||
|  |             required | ||||||
|  |             class="mb-3" | ||||||
|  |             variant="outlined" | ||||||
|  |             label="Content" | ||||||
|  |             hint="The content supports markdown syntax" | ||||||
|  |             v-model="data.content" | ||||||
|  |           /> | ||||||
|  |  | ||||||
|  |           <v-expansion-panels> | ||||||
|  |             <v-expansion-panel title="Brief describe"> | ||||||
|  |               <template #text> | ||||||
|  |                 <div class="mt-1"> | ||||||
|  |                   <v-text-field | ||||||
|  |                     required | ||||||
|  |                     variant="solo-filled" | ||||||
|  |                     density="comfortable" | ||||||
|  |                     label="Title" | ||||||
|  |                     v-model="data.title" | ||||||
|  |                   /> | ||||||
|  |  | ||||||
|  |                   <v-textarea | ||||||
|  |                     required | ||||||
|  |                     auto-grow | ||||||
|  |                     variant="solo-filled" | ||||||
|  |                     density="comfortable" | ||||||
|  |                     label="Description" | ||||||
|  |                     v-model="data.description" | ||||||
|  |                   /> | ||||||
|  |                 </div> | ||||||
|  |               </template> | ||||||
|  |             </v-expansion-panel> | ||||||
|  |  | ||||||
|  |             <v-expansion-panel title="Planned publish"> | ||||||
|  |               <template #text> | ||||||
|  |                 <div class="flex justify-between items-center"> | ||||||
|  |                   <div> | ||||||
|  |                     <p class="text-xs">Your content will visible for public at</p> | ||||||
|  |                     <p class="text-lg font-medium"> | ||||||
|  |                       {{ data.publishedAt ? new Date(data.publishedAt).toLocaleString() : new Date().toLocaleString() }} | ||||||
|  |                     </p> | ||||||
|  |                   </div> | ||||||
|  |                   <v-btn size="small" icon="mdi-pencil" variant="text" @click="dialogs.plan = true" /> | ||||||
|  |                 </div> | ||||||
|  |               </template> | ||||||
|  |             </v-expansion-panel> | ||||||
|  |           </v-expansion-panels> | ||||||
|  |         </v-container> | ||||||
|  |       </v-card-text> | ||||||
|  |     </v-form> | ||||||
|  |   </v-card> | ||||||
|  |  | ||||||
|  |   <planned-publish v-model:show="dialogs.plan" v-model:value="data.publishedAt" /> | ||||||
|  |  | ||||||
|  |   <v-snackbar v-model="success" :timeout="3000">Your article has been published.</v-snackbar> | ||||||
|  |  | ||||||
|  |   <!-- @vue-ignore --> | ||||||
|  |   <v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { request } from "@/scripts/request" | ||||||
|  | import { useEditor } from "@/stores/editor" | ||||||
|  | import { getAtk } from "@/stores/userinfo" | ||||||
|  | import { reactive, ref } from "vue" | ||||||
|  | import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" | ||||||
|  | import { useRouter } from "vue-router" | ||||||
|  |  | ||||||
|  | const editor = useEditor() | ||||||
|  |  | ||||||
|  | const dialogs = reactive({ | ||||||
|  |   plan: false, | ||||||
|  |   categories: false, | ||||||
|  |   media: false | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const data = reactive<any>({ | ||||||
|  |   title: "", | ||||||
|  |   content: "", | ||||||
|  |   description: "", | ||||||
|  |   publishedAt: null | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const router = useRouter() | ||||||
|  |  | ||||||
|  | const error = ref<string | null>(null) | ||||||
|  | const success = ref(false) | ||||||
|  | const loading = ref(false) | ||||||
|  |  | ||||||
|  | async function postArticle(evt: SubmitEvent) { | ||||||
|  |   const form = evt.target as HTMLFormElement | ||||||
|  |  | ||||||
|  |   if (!data.content) return | ||||||
|  |   if (!data.title || !data.description) return | ||||||
|  |   if (!data.publishedAt) data.publishedAt = new Date().toISOString() | ||||||
|  |  | ||||||
|  |   loading.value = true | ||||||
|  |   const res = await request("/api/p/articles", { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json",  Authorization: `Bearer ${getAtk()}` }, | ||||||
|  |     body: JSON.stringify(data) | ||||||
|  |   }) | ||||||
|  |   if (res.status === 200) { | ||||||
|  |     const data = await res.json() | ||||||
|  |     form.reset() | ||||||
|  |     router.push({ name: "posts.details", params: { postType: "articles", alias: data.alias } }) | ||||||
|  |     success.value = true | ||||||
|  |     editor.show.article = false | ||||||
|  |   } else { | ||||||
|  |     error.value = await res.text() | ||||||
|  |   } | ||||||
|  |   loading.value = false | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .article-toolbar { | ||||||
|  |   display: grid; | ||||||
|  |   flex-grow: 1; | ||||||
|  |   align-items: center; | ||||||
|  |   margin: 0 16px; | ||||||
|  |  | ||||||
|  |   grid-template-columns: 1fr auto 1fr; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .article-container { | ||||||
|  |   max-width: 720px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -44,7 +44,8 @@ const success = ref(false) | |||||||
| const loading = ref(false) | const loading = ref(false) | ||||||
|  |  | ||||||
| async function postComment(evt: SubmitEvent) { | async function postComment(evt: SubmitEvent) { | ||||||
|   const data = new FormData(evt.target as HTMLFormElement) |   const form = evt.target as HTMLFormElement | ||||||
|  |   const data = new FormData(form) | ||||||
|   if (!data.has("content")) return |   if (!data.has("content")) return | ||||||
|  |  | ||||||
|   loading.value = true |   loading.value = true | ||||||
| @@ -54,12 +55,13 @@ async function postComment(evt: SubmitEvent) { | |||||||
|     body: data |     body: data | ||||||
|   }) |   }) | ||||||
|   if (res.status === 200) { |   if (res.status === 200) { | ||||||
|  |     form.reset() | ||||||
|     success.value = true |     success.value = true | ||||||
|  |     editor.show.comment = false | ||||||
|   } else { |   } else { | ||||||
|     error.value = await res.text() |     error.value = await res.text() | ||||||
|   } |   } | ||||||
|   loading.value = false |   loading.value = false | ||||||
|   editor.show.comment = false |  | ||||||
|   editor.done = true |   editor.done = true | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -85,7 +85,8 @@ const success = ref(false) | |||||||
| const loading = ref(false) | const loading = ref(false) | ||||||
|  |  | ||||||
| async function postMoment(evt: SubmitEvent) { | async function postMoment(evt: SubmitEvent) { | ||||||
|   const data = new FormData(evt.target as HTMLFormElement) |   const form = evt.target as HTMLFormElement | ||||||
|  |   const data = new FormData(form) | ||||||
|   if (!data.has("content")) return |   if (!data.has("content")) return | ||||||
|   if (!extras.publishedAt) data.set("published_at", new Date().toISOString()) |   if (!extras.publishedAt) data.set("published_at", new Date().toISOString()) | ||||||
|   else data.set("published_at", extras.publishedAt) |   else data.set("published_at", extras.publishedAt) | ||||||
| @@ -97,11 +98,12 @@ async function postMoment(evt: SubmitEvent) { | |||||||
|     body: data |     body: data | ||||||
|   }) |   }) | ||||||
|   if (res.status === 200) { |   if (res.status === 200) { | ||||||
|  |     form.reset() | ||||||
|     success.value = true |     success.value = true | ||||||
|  |     editor.show.moment = false | ||||||
|   } else { |   } else { | ||||||
|     error.value = await res.text() |     error.value = await res.text() | ||||||
|   } |   } | ||||||
|   loading.value = false |   loading.value = false | ||||||
|   editor.show.moment = false |  | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -1,9 +1,12 @@ | |||||||
| <template> | <template> | ||||||
|   <v-dialog v-model="editor.show.moment" class="max-w-[540px]"> |   <v-dialog v-model="editor.show.comment" class="max-w-[540px]" eager> | ||||||
|  |     <comment-editor /> | ||||||
|  |   </v-dialog> | ||||||
|  |   <v-dialog v-model="editor.show.moment" class="max-w-[540px]" eager> | ||||||
|     <moment-editor /> |     <moment-editor /> | ||||||
|   </v-dialog> |   </v-dialog> | ||||||
|   <v-dialog v-model="editor.show.comment" class="max-w-[540px]"> |   <v-dialog v-model="editor.show.article" transition="dialog-bottom-transition" fullscreen eager> | ||||||
|     <comment-editor /> |     <article-editor /> | ||||||
|   </v-dialog> |   </v-dialog> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -11,6 +14,7 @@ | |||||||
| import { useEditor } from "@/stores/editor" | import { useEditor } from "@/stores/editor" | ||||||
| import MomentEditor from "@/components/publish/MomentEditor.vue" | import MomentEditor from "@/components/publish/MomentEditor.vue" | ||||||
| import CommentEditor from "@/components/publish/CommentEditor.vue"; | import CommentEditor from "@/components/publish/CommentEditor.vue"; | ||||||
|  | import ArticleEditor from "@/components/publish/ArticleEditor.vue"; | ||||||
|  |  | ||||||
| const editor = useEditor() | const editor = useEditor() | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -3,10 +3,9 @@ | |||||||
|     <div class="timeline flex-grow-1 max-w-[75ch]"> |     <div class="timeline flex-grow-1 max-w-[75ch]"> | ||||||
|       <v-card :loading="loading"> |       <v-card :loading="loading"> | ||||||
|         <article> |         <article> | ||||||
|           <v-card-title>{{ post?.title }}</v-card-title> |  | ||||||
|  |  | ||||||
|           <v-card-text> |           <v-card-text> | ||||||
|             <div class="text-sm">{{ post?.description }}</div> |             <h1 class="text-lg font-medium">{{ post?.title }}</h1> | ||||||
|  |             <p class="text-sm">{{ post?.description }}</p> | ||||||
|  |  | ||||||
|             <v-divider class="mt-5 mx-[-16px] border-opacity-50" /> |             <v-divider class="mt-5 mx-[-16px] border-opacity-50" /> | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user