diff options
author | vnugent <public@vaughnnugent.com> | 2023-07-12 01:28:23 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-07-12 01:28:23 -0400 |
commit | f64955c69d91e578e580b409ba31ac4b3477da96 (patch) | |
tree | 16f01392ddf1abfea13d7d1ede3bfb0459fe8f0d /front-end/src/views/Blog/components/Content |
Initial commit
Diffstat (limited to 'front-end/src/views/Blog/components/Content')
-rw-r--r-- | front-end/src/views/Blog/components/Content/ContentEditor.vue | 225 | ||||
-rw-r--r-- | front-end/src/views/Blog/components/Content/ContentTable.vue | 75 |
2 files changed, 300 insertions, 0 deletions
diff --git a/front-end/src/views/Blog/components/Content/ContentEditor.vue b/front-end/src/views/Blog/components/Content/ContentEditor.vue new file mode 100644 index 0000000..4de7f8a --- /dev/null +++ b/front-end/src/views/Blog/components/Content/ContentEditor.vue @@ -0,0 +1,225 @@ +<template> + <div id="content-editor" class="flex flex-col w-full"> + <div class="my-4 ml-auto"> + <div class="button-group"> + <!-- Submit the post form --> + <button :disabled="waiting" class="btn primary" form="content-upload-form"> + <fa-icon icon="spinner" v-if="waiting" class="animate-spin" /> + <span v-else>Save</span> + </button> + <button class="btn" @click="onClose">Cancel</button> + </div> + </div> + <div class="mx-auto sm:min-w-[20rem]"> + <h4 class="text-center">Edit Content</h4> + <p> + Add or edit content file + </p> + <div class="mt-3 text-sm"> + Publishing to: + </div> + <div class="p-2 px-3 bg-gray-200 border border-gray-300 rounded-md dark:bg-transparent"> + {{ selectedChannelName }} + </div> + </div> + <div id="content-edit-body" class="min-h-[24rem] my-10"> + <form id="content-upload-form" class="flex" @submit.prevent="onSubmit"> + <fieldset class="mx-auto flex flex-col gap-10 w-[32rem]"> + <div class="flex flex-col"> + <div class="p-3 py-0.5"> + <label class="">File name</label> + <input + type="text" + class="w-full input primary" + placeholder="Title" + v-model="v$.name.$model" + :class="{'invalid':v$.name.$invalid && v$.name.$dirty}" + /> + </div> + <div v-if="editFile?.id" class="mt-3"> + <div class="p-3 py-0.5"> + <label>Content Id</label> + <input type="text" class="w-full input primary" :value="editFile.id" readonly /> + </div> + </div> + + <div v-if="uploadedFile.name" class="border border-gray-300 p-4 w-[24rem] mx-auto rounded-sm relative mt-5"> + <div class="absolute top-0 text-right -right-12"> + <button class="rounded-sm btn sm red" @click.prevent="removeNewFile"> + <fa-icon :icon="['fas', 'trash']" /> + </button> + </div> + <div class=""> + Name: + <span class="border-b border-coolGray-400"> + {{ getFileName(uploadedFile) }} + </span> + </div> + <div class="mt-3"> + Size: {{ getFileSize(uploadedFile) }} + </div> + <div class="mt-3"> + Content-Type: {{ getContentType(uploadedFile) }} + </div> + </div> + <div v-else-if="editFile?.id" > + <div class="border border-gray-300 p-4 min-w-[24rem] mx-auto rounded-sm relative mt-5"> + <div class=""> + Name: {{ getFileName(editFile) }} + </div> + <div class="mt-3"> + Size: {{ getSizeinKb(editFile?.length) }} + </div> + <div class="mt-3"> + File Path: {{ editFile.path }} + </div> + <div class="mt-3"> + Content-Type: {{ editFile.content_type }} + </div> + </div> + </div> + <div v-if="!uploadedFile.name" class="m-auto mt-5 w-fit"> + <button class="btn" @click.prevent="open()"> + {{ editFile?.id ? 'Overwrite file' : 'Select File' }} + </button> + </div> + </div> + </fieldset> + </form> + </div> + <div class="mt-4"> + <div class="mx-auto w-fit"> + <button class="btn red" @click="onDelete">Delete Forever</button> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import { reactiveComputed, useFileDialog } from '@vueuse/core'; +import { ContentMeta } from '@vnuge/cmnext-admin'; +import { useConfirm, useVuelidateWrapper, useFormToaster, useWait } from '@vnuge/vnlib.browser'; +import { defaultTo, first, isEmpty, round, truncate } from 'lodash'; +import { required, helpers, maxLength } from '@vuelidate/validators' +import useVuelidate from '@vuelidate/core'; +import { BlogState } from '../../blog-api'; + +const emit = defineEmits(['close', 'submit', 'delete']); +const props = defineProps<{ + blog: BlogState +}>(); + +const { reveal } = useConfirm(); +const { waiting } = useWait(); + +const { content, channels } = props.blog; + +const selectedId = computed(() => content.selectedId.value); +const selectedContent = computed<ContentMeta>(() => defaultTo(content.selectedItem.value, {} as ContentMeta)); +const metaBuffer = reactiveComputed<ContentMeta>(() => ({ ...selectedContent.value})); +const selectedChannelName = computed(() => defaultTo(channels.selectedItem.value?.name, 'No channel selected')); + +const v$ = useVuelidate({ + name: { + required, + maxLen:maxLength(50), + reg: helpers.withMessage('The file name contains invalid characters', helpers.regex(/^[a-zA-Z0-9 \-\.]*$/)) + }, +}, metaBuffer) + +const { validate } = useVuelidateWrapper(v$); + +const file = ref<File | undefined>(); +const { files, open, reset, onChange } = useFileDialog({ accept: '*' }) +//update the file buffer when a user selects a file to upload +onChange(() => { + file.value = first(files.value) + v$.value.name.$model = file.value?.name; +}) + +const editFile = computed<ContentMeta | undefined>(() => selectedContent.value); +const uploadedFile = computed<File>(() => defaultTo(file.value, {} as File)); + +const getFileName = (file : File | ContentMeta) => truncate(file.name, { length: 20 }); +const getFileSize = (file : File) => { + const size = round(file.size > 1024 ? file.size / 1024 : file.size, 2); + return `${size} ${file.size > 1024 ? 'KB' : 'B'}`; +} +const getContentType = (file : File) => file.type; +const getSizeinKb = (value : number | undefined) => { + value = defaultTo(value, 0); + const size = round(value > 1024 ? value / 1024 : value, 2); + return `${size} ${value > 1024 ? 'KB' : 'B'}`; +} + +const onSubmit = async () => { + + const { error } = useFormToaster() + const hasFile = !isEmpty(file.value?.name); + + //Validate the form + if(!await validate()){ + return; + } + + //Check if in edit mode + if(selectedId.value === 'new'){ + //New file upload + if(!hasFile){ + error({ title: 'No file selected' }) + return; + } + } + //Edit mode + else{ + //If a new file has been attached, then we should prompt for an overwrite + if(hasFile){ + //Confirm overwrite + const { isCanceled } = await reveal({ + title: 'Overwrite file?', + text: 'Are you sure you want to overwrite the file? This action cannot be undone.', + }) + if (isCanceled) { + return; + } + } + } + emit('submit', { item: metaBuffer, file: file.value }); +} + +const onClose = () => emit('close'); + +const onDelete = async () => { + //Show confirm + const { isCanceled } = await reveal({ + title: 'Delete File?', + text: 'Are you sure you want to delete this file? This action cannot be undone.', + }) + if (isCanceled) { + return; + } + + if (!confirm('Are you sure you want to delete this file forever?')) { + return; + } + + //Emit the delete event with the original post + emit('delete', metaBuffer) +} + +const removeNewFile = () =>{ + file.value = undefined; + v$.value.name.$model = editFile.value?.name ?? ''; + reset(); +} + +</script> + +<style lang="scss"> +#content-upload-form{ + input.primary.invalid{ + @apply border-red-500; + } +} +</style>
\ No newline at end of file diff --git a/front-end/src/views/Blog/components/Content/ContentTable.vue b/front-end/src/views/Blog/components/Content/ContentTable.vue new file mode 100644 index 0000000..c47a063 --- /dev/null +++ b/front-end/src/views/Blog/components/Content/ContentTable.vue @@ -0,0 +1,75 @@ +<template> + <thead> + <tr> + <th>File Name</th> + <th>Id</th> + <th>Date</th> + <th>Content Type</th> + <th>Length</th> + <th></th> + </tr> + </thead> + <tbody> + <tr v-for="item in content" :key="item.id" class="table-row"> + <td> + {{ getItemName(item) }} + </td> + <td> + {{ getItemId(item) }} + </td> + <td> + {{ getDateString(item.date) }} + </td> + <td> + {{ item.content_type }} + </td> + <td> + {{ getItemLength(item) }} + </td> + <td class="w-24"> + <fieldset :disabled="waiting"> + <button class="btn xs no-border" @click="copyLink(item)"> + <fa-icon icon="link" /> + </button> + <button class="btn xs no-border" @click="copy(item.id)"> + <fa-icon icon="copy" /> + </button> + <button class="btn xs no-border" @click="openEdit(item)"> + <fa-icon icon="pencil" /> + </button> + </fieldset> + </td> + </tr> + </tbody> +</template> + +<script setup lang="ts"> +import { toRefs } from 'vue'; +import { filter as _filter, truncate } from 'lodash'; +import { useClipboard } from '@vueuse/core'; +import { useWait } from '@vnuge/vnlib.browser'; +import { ContentMeta } from '@vnuge/cmnext-admin'; + +const emit = defineEmits(['open-edit', 'copy-link']) + +const props = defineProps<{ + content: ContentMeta[] +}>() + +const { content } = toRefs(props) + +const { waiting } = useWait() +const { copy } = useClipboard() + +const getDateString = (time?: number) => new Date((time || 0) * 1000).toLocaleString(); +const getItemLength = (item: ContentMeta) : string =>{ + const length = item.length || 0; + return length > 1024 ? `${(length / 1024).toFixed(2)} KB` : `${length} B` +} +const getItemId = (item: ContentMeta) => truncate(item.id || '', { length: 20 }) +const getItemName = (item : ContentMeta) => truncate(item.name || '', { length: 30 }) + +const openEdit = async (item: ContentMeta) => emit('open-edit', item) +const copyLink = (item : ContentMeta) => emit('copy-link', item) + +</script>
\ No newline at end of file |