aboutsummaryrefslogtreecommitdiff
path: root/front-end/src/views/Blog/components/Content
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-07-12 01:28:23 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-07-12 01:28:23 -0400
commitf64955c69d91e578e580b409ba31ac4b3477da96 (patch)
tree16f01392ddf1abfea13d7d1ede3bfb0459fe8f0d /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.vue225
-rw-r--r--front-end/src/views/Blog/components/Content/ContentTable.vue75
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