diff options
Diffstat (limited to 'front-end/src/views/Blog/index.vue')
-rw-r--r-- | front-end/src/views/Blog/index.vue | 324 |
1 files changed, 324 insertions, 0 deletions
diff --git a/front-end/src/views/Blog/index.vue b/front-end/src/views/Blog/index.vue new file mode 100644 index 0000000..6bfcb6e --- /dev/null +++ b/front-end/src/views/Blog/index.vue @@ -0,0 +1,324 @@ +<template> + <div class="container mx-auto mt-10 mb-[10rem]"> + <div id="blog-admin-template" class=""> + + <TabGroup vertical :selected-index="tabId" @change="onTabChange"> + <div class="menu"> + <TabList> + <div class="inline-flex items-center justify-center w-16 h-16"> + <span class="username-box"> + {{ firstLetter }} + </span> + </div> + + <div class="border-t border-gray-100 dark:border-dark-500"> + <div class="px-2"> + + <Tab v-slot="{ selected }" as="div" class="py-4"> + <div class="t group menu-item" :class="{'active':selected}"> + + <fa-icon icon="bullhorn" size="lg" /> + + <span class="opacity-0 tooltip group-hover:opacity-100"> + Channel + </span> + </div> + </Tab> + + <ul class="flex flex-col pt-4 space-y-1 border-t border-gray-100 dark:border-dark-500"> + <Tab v-slot="{ selected }" as="li"> + <div class="group menu-item" :class="{'active':selected}"> + + <fa-icon icon="comment" size="xl" /> + + <span class="opacity-0 tooltip group-hover:opacity-100"> + Posts + </span> + </div> + </Tab> + <Tab v-slot="{ selected }" as="li"> + <div class="group menu-item" :class="{'active':selected}"> + + <fa-icon icon="folder-open" size="lg" /> + + <span class="opacity-0 tooltip group-hover:opacity-100"> + Content + </span> + </div> + </Tab> + + </ul> + </div> + </div> + </TabList> + </div> + + <TabPanels class="tab-container"> + <div class="flex flex-row h-12 px-4 pb-2"> + + <div class="inline-flex flex-row gap-3"> + <div class="my-auto"> + <fa-icon icon="bullhorn" /> + </div> + + <select id="channel-select" class="" v-model="channel"> + <option value="">Select Channel</option> + <option v-for="c in channels.items.value" :value="c.id"> + {{ c.name }} + </option> + </select> + </div> + + <div class="flex flex-row w-full max-w-md gap-4 ml-auto mr-4 filter"> + <div class="my-auto">Filter</div> + <input class="w-full rounded input primary" v-model="search"/> + </div> + + <div class="flex flex-row py-2 mr-auto"> + <Switch v-model="lastModified" + :class="lastModified ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-500'" + class="relative inline-flex items-center w-10 h-5 my-auto duration-75 rounded-full"> + <span class="sr-only">Last modified</span> + <span :class="lastModified ? 'translate-x-6' : 'translate-x-1'" + class="inline-block w-3 h-3 transition transform bg-white rounded-full" /> + </Switch> + <div class="my-auto ml-3"> + Last Modifed + </div> + + </div> + </div> + + <TabPanel> + <Channels :blog="blogState" /> + </TabPanel> + + <TabPanel> + <Posts :blog="blogState" /> + </TabPanel> + + <TabPanel> + <Content :blog="blogState" /> + </TabPanel> + + </TabPanels> + </TabGroup> + </div> + </div> +</template> + +<script setup lang="ts"> +import { computed } from 'vue'; +import { useScriptTag } from '@vueuse/core'; +import { useRouteQuery } from '@vueuse/router'; +import { TabGroup, TabList, Tab, TabPanels, TabPanel, Switch } from '@headlessui/vue' +import { first } from 'lodash'; +import { usePageGuard, useUser, useTitle } from '@vnuge/vnlib.browser'; +import { createBlogContext, useComputedChannels, useComputedPosts, useComputedContent, SortType } from '@vnuge/cmnext-admin'; +import { BlogState } from './blog-api'; +import Channels from './components/Channels.vue'; +import Posts from './components/Posts.vue'; +import Content from './components/Content.vue'; + +//Protect page +usePageGuard(); +useTitle('CMNext Admin') + +//Load scripts +const ckEditorTag = useScriptTag("https://cdn.ckeditor.com/ckeditor5/35.4.0/super-build/ckeditor.js") +//Store the wait result on the window for the editor script to wait +window.editorLoadResult = ckEditorTag.load(true); + +const { userName, getProfile } = useUser() + +//Load user profile and forget if not set +if(!userName.value){ + getProfile() +} + +const firstLetter = computed(() => first(userName.value)) + +const tabIdQ = useRouteQuery<string>('tabid', '', { mode: 'push' }) + +const context = createBlogContext({ + channelUrl: '/blog/channels', + postUrl: '/blog/posts', + contentUrl: '/blog/content' +}) + +const { search, sort, channel } = context.getQuery(); + +const channels = useComputedChannels(context) +const posts = useComputedPosts(context) +const content = useComputedContent(context) + +const blogState = { channels, posts, content } as BlogState + +//Map queries to their respective computed values +const tabId = computed(() => tabIdQ.value ? parseInt(tabIdQ.value) : 0); +const lastModified = computed({ + get :() => sort.value === SortType.ModifiedTime, + set: (value:boolean) => { + sort.value = value ? SortType.ModifiedTime : SortType.CreatedTime + } +}) + +const onTabChange = (id:number) => tabIdQ.value = id.toString(10) + +</script> + +<style lang="scss"> + +#blog-admin-template{ + @apply flex flex-row flex-auto min-h-[50rem] border rounded-sm max-w-[82rem] mx-auto; + @apply dark:border-dark-600 dark:text-gray-300 border-gray-200; + + .username-box{ + @apply grid w-10 h-10 text-sm rounded-lg place-content-center; + @apply text-gray-600 bg-gray-100 dark:text-gray-300 dark:bg-dark-600; + } + + .menu-item{ + @apply relative flex justify-center rounded px-2 py-2 cursor-pointer; + @apply text-gray-500 hover:bg-gray-50 hover:text-gray-700 dark:hover:bg-dark-700 dark:hover:text-gray-300; + + &.active{ + @apply text-primary-600; + } + + .tooltip{ + @apply absolute start-full -translate-y-1/2 top-1/2 ms-4 rounded px-2 py-1.5 text-xs font-medium; + @apply text-white bg-gray-900 dark:bg-dark-600; + } + } + + .menu{ + @apply flex flex-col justify-between w-16 border-e; + @apply bg-white dark:bg-dark-800 dark:border-dark-500; + } + + #channel-select{ + @apply w-full p-1 px-2 border rounded-sm sm:text-sm min-w-[13rem]; + @apply border-gray-300 text-gray-700 bg-white; + @apply dark:bg-dark-800 dark:border-dark-500 focus:dark:border-dark-400 hover:dark:border-dark-400 dark:text-inherit; + + option{ + @apply text-base; + } + } + + .tab-container{ + @apply flex-1 py-4 rounded-r-sm dark:bg-dark-800 bg-white text-gray-700 dark:text-inherit; + } + + // Rules for dynamic forms in edit panes + .dynamic-form.form{ + @apply w-full mt-4 md:px-12; + + .dynamic-form.input-group{ + @apply grid grid-flow-row grid-cols-2; + } + + .dynamic-form.input-group{ + @apply gap-x-16; + + .dynamic-form.input-container{ + + .dynamic-form.dynamic-input{ + @apply border rounded-sm p-2 bg-transparent w-full dark:border-dark-600; + @apply dark:bg-dark-800 focus:border-primary-500; + + &.input-textarea{ + @apply h-40 outline-none; + } + + &::placeholder{ + @apply dark:text-gray-500; + } + + &:disabled{ + @apply text-rose-400 border-transparent; + } + } + + &.dirty.data-invalid .dynamic-form.dynamic-input{ + @apply border-red-500 focus:border-red-500; + } + + .dynamic-form.field-description{ + @apply pt-1 p-2 pb-4 text-sm; + } + + } + + .dynamic-form.input-label{ + @apply col-span-2 text-right m-auto mr-2; + } + } + } + + table.edit-table { + @apply w-full divide-y-2 divide-gray-200 bg-white text-sm dark:divide-dark-500 dark:bg-dark-800; + + thead{ + @apply text-left text-lg; + } + + tbody{ + @apply divide-y divide-gray-200 dark:divide-dark-500; + } + + thead th, + tr td{ + @apply whitespace-nowrap px-4 py-2 font-medium; + } + } + + .ck.ck-editor{ + @apply border dark:border-coolGray-600; + } + + .ck-editor .ck-content, + .ck-editor .ck-source-editing-area{ + @apply min-h-[32rem] resize-y dark:bg-dark-800; + + a { + @apply text-blue-500; + } + + p{ + @apply my-2; + } + + pre{ + @apply p-2 dark:text-gray-200; + } + + h1, h2{ + @apply border-b pb-3 mb-4; + } + } + + .ck-source-editing-area textarea{ + @apply dark:bg-transparent; + } + + .ck.ck-toolbar, + .ck.ck-reset + { + @apply dark:bg-dark-800 dark:text-gray-300; + + .ck-button, + .ck-dropdown + { + @apply dark:text-gray-300; + + &:hover, + &.ck-on + { + @apply dark:bg-dark-600; + } + } + } +} +</style>
\ No newline at end of file |