♻️ 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) | ||||
|  | ||||
| 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 | ||||
|  | ||||
|   loading.value = true | ||||
| @@ -54,12 +55,13 @@ async function postComment(evt: SubmitEvent) { | ||||
|     body: data | ||||
|   }) | ||||
|   if (res.status === 200) { | ||||
|     form.reset() | ||||
|     success.value = true | ||||
|     editor.show.comment = false | ||||
|   } else { | ||||
|     error.value = await res.text() | ||||
|   } | ||||
|   loading.value = false | ||||
|   editor.show.comment = false | ||||
|   editor.done = true | ||||
| } | ||||
| </script> | ||||
|   | ||||
| @@ -85,7 +85,8 @@ const success = ref(false) | ||||
| const loading = ref(false) | ||||
|  | ||||
| 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 (!extras.publishedAt) data.set("published_at", new Date().toISOString()) | ||||
|   else data.set("published_at", extras.publishedAt) | ||||
| @@ -97,11 +98,12 @@ async function postMoment(evt: SubmitEvent) { | ||||
|     body: data | ||||
|   }) | ||||
|   if (res.status === 200) { | ||||
|     form.reset() | ||||
|     success.value = true | ||||
|     editor.show.moment = false | ||||
|   } else { | ||||
|     error.value = await res.text() | ||||
|   } | ||||
|   loading.value = false | ||||
|   editor.show.moment = false | ||||
| } | ||||
| </script> | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| <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 /> | ||||
|   </v-dialog> | ||||
|   <v-dialog v-model="editor.show.comment" class="max-w-[540px]"> | ||||
|     <comment-editor /> | ||||
|   <v-dialog v-model="editor.show.article" transition="dialog-bottom-transition" fullscreen eager> | ||||
|     <article-editor /> | ||||
|   </v-dialog> | ||||
| </template> | ||||
|  | ||||
| @@ -11,6 +14,7 @@ | ||||
| import { useEditor } from "@/stores/editor" | ||||
| import MomentEditor from "@/components/publish/MomentEditor.vue" | ||||
| import CommentEditor from "@/components/publish/CommentEditor.vue"; | ||||
| import ArticleEditor from "@/components/publish/ArticleEditor.vue"; | ||||
|  | ||||
| const editor = useEditor() | ||||
| </script> | ||||
|   | ||||
| @@ -3,10 +3,9 @@ | ||||
|     <div class="timeline flex-grow-1 max-w-[75ch]"> | ||||
|       <v-card :loading="loading"> | ||||
|         <article> | ||||
|           <v-card-title>{{ post?.title }}</v-card-title> | ||||
|  | ||||
|           <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" /> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user