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/bootstrap/components |
Initial commit
Diffstat (limited to 'front-end/src/bootstrap/components')
-rw-r--r-- | front-end/src/bootstrap/components/ConfirmPrompt.vue | 69 | ||||
-rw-r--r-- | front-end/src/bootstrap/components/CookieWarning.vue | 36 | ||||
-rw-r--r-- | front-end/src/bootstrap/components/Footer.vue | 69 | ||||
-rw-r--r-- | front-end/src/bootstrap/components/Header.vue | 162 | ||||
-rw-r--r-- | front-end/src/bootstrap/components/PasswordPrompt.vue | 110 |
5 files changed, 446 insertions, 0 deletions
diff --git a/front-end/src/bootstrap/components/ConfirmPrompt.vue b/front-end/src/bootstrap/components/ConfirmPrompt.vue new file mode 100644 index 0000000..8114387 --- /dev/null +++ b/front-end/src/bootstrap/components/ConfirmPrompt.vue @@ -0,0 +1,69 @@ + +<template> + <div id="confirm-prompt"> + <Dialog class="modal-entry" :style="style" :open="isRevealed" @close="cancel" > + <div class="modal-content-container"> + <DialogPanel> + <DialogTitle class="modal-title"> + {{ title }} + </DialogTitle> + + <DialogDescription class="modal-description"> + {{ message.text }} + </DialogDescription> + + <p class="modal-text-secondary"> + {{ message.subtext }} + </p> + + <div class="modal-button-container"> + <button class="rounded btn sm primary" @click="confirm"> + Confirm + </button> + <button class="rounded btn sm" @click="cancel"> + Close + </button> + </div> + </DialogPanel> + </div> + </Dialog> + </div> +</template> + +<script setup lang="ts"> +import { defaultTo } from 'lodash' +import { computed, ref } from 'vue' + +import { + Dialog, + DialogPanel, + DialogTitle, + DialogDescription, +} from '@headlessui/vue' + +import { onClickOutside } from '@vueuse/core' +import { useConfirm, useEnvSize } from '@vnuge/vnlib.browser' + +const { headerHeight } = useEnvSize() +//Use component side of confirm +const { isRevealed, confirm, cancel, onReveal } = useConfirm() + +const dialog = ref(null) +const message = ref({}) + +//Cancel prompt when user clicks outside of dialog, only when its open +onClickOutside(dialog, () => isRevealed.value ? cancel() : null) + +//Set message on reveal +onReveal(m => message.value = m); + +const title = computed(() => defaultTo(message.value.title, 'Confirm')) + +const style = computed(() => { + return { + 'height': `calc(100vh - ${headerHeight.value}px)`, + 'top': `${headerHeight.value}px` + } +}) + +</script>
\ No newline at end of file diff --git a/front-end/src/bootstrap/components/CookieWarning.vue b/front-end/src/bootstrap/components/CookieWarning.vue new file mode 100644 index 0000000..b5239f5 --- /dev/null +++ b/front-end/src/bootstrap/components/CookieWarning.vue @@ -0,0 +1,36 @@ +<template> + <div v-if="show" class="fixed top-0 left-0 z-10 w-full" :style="style"> + <div class="flex w-full p-2 text-center text-white bg-blue-600"> + <div class="m-auto text-sm font-semibold md:text-base"> + You must have cookies enabled for this site to work properly + </div> + </div> + </div> +</template> + +<script setup lang="ts"> + +import { computed, toRefs } from 'vue' +import { useEnvSize } from '@vnuge/vnlib.browser' + +const props = defineProps<{ + hidden?: boolean +}>() + +const { hidden } = toRefs(props) + +const { headerHeight } = useEnvSize() + +const show = computed(() => (!window.navigator.cookieEnabled) && !hidden.value) + +const style = computed(() => { + return { + top: headerHeight.value + 'px' + } +}) + +</script> + +<style> + +</style> diff --git a/front-end/src/bootstrap/components/Footer.vue b/front-end/src/bootstrap/components/Footer.vue new file mode 100644 index 0000000..98f6f55 --- /dev/null +++ b/front-end/src/bootstrap/components/Footer.vue @@ -0,0 +1,69 @@ +<template> + <footer class="bottom-0 left-0 z-10 w-full"> + <div id="footer-content" class="footer-content" > + <div class="footer-main-container"> + <div class="col-span-4 sm:col-span-6 lg:col-span-3"> + <p class="my-4 text-xs leading-normal"> + No tracking, no external dependencies, no ads. You shouldn't trust anyone with your data, + and I am no exception. + </p> + </div> + <nav> + <slot name="footer-nav-1" /> + </nav> + <nav> + <slot name="footer-nav-2" /> + </nav> + <nav> + <p class="nav-title"> + Built with + </p> + <a class="footer-link" href="https://www.vaughnnugent.com/resources/software/modules">VNLib HTTP v1.0.1</a> + <a class="footer-link" href="https://tailwindcss.com/">Tailwindcss</a> + <a class="footer-link" href="https://vuejs.org/">Vuejs v3</a> + <a class="footer-link" href="https://fontawesome.com/">Font Awesome</a> + </nav> + <div class="color-selector-container"> + <p class="nav-title"> + Color Scheme + </p> + <div class="flex flex-row gap-6 md:my-auto"> + <div class=""> + <button class="bg-sel-btn" @click.prevent="Dark" > + Dark + </button> + </div> + <div class=""> + <fa-icon icon="lightbulb" /> + </div> + <div class=""> + <button class="bg-sel-btn" @click.prevent="Light"> + Light + </button> + </div> + </div> + </div> + </div> + <div class="text-sm footer-lower"> + <div class="mb-6 md:mb-0"> + <p class="text-left"> + Highly angular trousers ~ Pete Jordanson + </p> + </div> + <div class="mb-6 text-left md:mb-0"> + Copyright © 2023 Vaughn Nugent. All Rights Reserved. + </div> + </div> + </div> + </footer> +</template> + +<script setup lang="ts"> +import { useDark } from '@vueuse/core' +import { debounce } from 'lodash' + +const isDark = useDark() + +const Dark = debounce(() => isDark.value = true, 50) +const Light = debounce(() => isDark.value = false, 50) +</script> diff --git a/front-end/src/bootstrap/components/Header.vue b/front-end/src/bootstrap/components/Header.vue new file mode 100644 index 0000000..f7481a3 --- /dev/null +++ b/front-end/src/bootstrap/components/Header.vue @@ -0,0 +1,162 @@ +<!-- eslint-disable vue/max-attributes-per-line --> +<template> + <header class="sticky top-0 left-0 z-40"> + <div class="flex header-container"> + <div id="header-mobile-menu" ref="sideMenu" class="side-menu" :style="sideMenuStyle"> + <div class="pt-4 pl-4 pr-6"> + <nav id="header-mobile-nav" class="relative flex flex-col pr-3"> + <div v-for="route in routes" :key="route.path" class="m-auto ml-0"> + <div class="my-1" @click="closeSideMenu"> + <router-link :to="route"> + {{ route.name }} + </router-link> + </div> + </div> + </nav> + </div> + </div> + <div class="flex flex-row w-full md:mx-3"> + <div class="hidden w-4 lg:block" /> + <div class="flex px-4 my-auto text-xl md:hidden"> + <div v-if="!sideMenuActive" class="w-7" @click.prevent="openSideMenu"> + <fa-icon icon="bars" /> + </div> + <div v-else class="text-2xl w-7"> + <fa-icon icon="times" /> + </div> + </div> + <div id="site-title-container" class="flex m-0 mr-3"> + <div class="inline-block px-1"> + <slot name="site_logo" /> + </div> + <div id="site-title" class="inline-block m-auto mx-1"> + <router-link to="/"> + <h3>{{ siteTitle }}</h3> + </router-link> + </div> + </div> + <div class="hidden w-4 lg:block" /> + <nav id="header-desktop-nav" class="flex-row hidden mr-2 md:flex"> + <span v-for="route in routes" :key="route.fullPath" class="flex px-1 lg:px-3"> + <div v-if="!route.hide" class="m-auto"> + <router-link :to="route" class="flex-auto"> + {{ route.name }} + </router-link> + </div> + </span> + </nav> + <div id="user-menu" ref="userMenu" class="drop-controller" :class="{ 'hovered': userMenuHovered }"> + <div class="user-menu"> + Hello <span class="font-semibold">{{ uname }}</span> + </div> + <div ref="userDrop" class="absolute top-0 right-0 duration-100 ease-in-out" style="z-index:-1" :style="dropStyle"> + <div class="drop-menu" @click.prevent="userMenuHovered = false"> + <span class="space-x-2" /> + <a v-if="!loggedIn" href="#" @click="gotoRoute('/register')"> + Register + </a> + <a v-else href="#" @click="gotoRoute('/account')"> + Account + </a> + <a v-if="!loggedIn" href="#" @click="gotoRoute('/login')"> + Login + </a> + <a v-else href="#" @click.prevent="OnLogout"> + Logout + </a> + </div> + </div> + </div> + <div class="hidden space-x-4 lg:block" /> + </div> + </div> + </header> +</template> + +<script setup lang="ts"> + +import { debounce, find } from 'lodash' +import { useElementSize, onClickOutside, useElementHover } from '@vueuse/core' +import { computed, ref, toRefs } from 'vue' +import { useSession, useUser, useEnvSize, apiCall } from '@vnuge/vnlib.browser' +import { RouteLocation, useRouter } from 'vue-router'; + +const props = defineProps<{ + siteTitle: string, + routes: RouteLocation[] +}>() + +const { siteTitle, routes } = toRefs(props) + +const { loggedIn } = useSession() +const { userName, logout } = useUser() +const { headerHeight } = useEnvSize() + +//Get the router for navigation +const router = useRouter() + +const sideMenuActive = ref(false) + +const userDrop = ref(null) +const sideMenu = ref(null) +const userMenu = ref(null) + +const dropMenuSize = useElementSize(userDrop) +const sideMenuSize = useElementSize(sideMenu) +const userMenuHovered = useElementHover(userMenu) + +const uname = computed(() => userName.value || 'Visitor') +const sideMenuStyle = computed(() => { + // Side menu should be the exact height of the page and under the header, + // So menu height is the height of the page minus the height of the header + return { + height: `calc(100vh - ${headerHeight.value}px)`, + left: sideMenuActive.value ? '0' : `-${sideMenuSize.width.value}px`, + top: `${headerHeight.value}px` + } +}) + +const dropStyle = computed(() => { + return { + 'margin-top': userMenuHovered.value ? `${headerHeight.value}px` : `-${(dropMenuSize.height.value - headerHeight.value)}px` + } +}) + +const closeSideMenu = debounce(() => sideMenuActive.value = false, 50) +const openSideMenu = debounce(() => sideMenuActive.value = true, 50) + +//Close side menu when clicking outside of it +onClickOutside(sideMenu, closeSideMenu) + +//Redirect to the route when clicking on it +const gotoRoute = (route : string) =>{ + + //Get all routes from the router + const allRoutes = router.getRoutes(); + + //Try to find the route by its path + const goto = find(allRoutes, { path: route }); + + if(goto){ + //navigate to the route manually + router.push(goto); + } + else{ + //Fallback to full navigation + window.location.assign(route); + } +} + +const OnLogout = () =>{ + apiCall(async ({ toaster }) => { + await logout() + toaster.general.success({ + id: 'logout-success', + title: 'Success', + text: 'You have been logged out', + duration: 5000 + }) + }) +} + +</script>
\ No newline at end of file diff --git a/front-end/src/bootstrap/components/PasswordPrompt.vue b/front-end/src/bootstrap/components/PasswordPrompt.vue new file mode 100644 index 0000000..ae29358 --- /dev/null +++ b/front-end/src/bootstrap/components/PasswordPrompt.vue @@ -0,0 +1,110 @@ +<template> + <div id="password-prompt"> + <Dialog + class="modal-entry" + :style="style" + :open="isRevealed" + @close="close" + > + <div ref="dialog" class="modal-content-container" > + <DialogPanel> + <DialogTitle class="modal-title"> + Enter your password + </DialogTitle> + + <DialogDescription class="modal-description"> + Please re-enter your password to continue. + </DialogDescription> + + <form id="password-form" @submit.prevent="formSubmitted" :disabled="waiting"> + <fieldset> + <div class="input-container"> + <input v-model="v$.password.$model" type="password" class="rounded input primary" placeholder="Password" @input="onInput"> + </div> + </fieldset> + </form> + + <div class="modal-button-container"> + <button class="rounded btn sm primary" form="password-form"> + Submit + </button> + <button class="rounded btn sm" @click="close" > + Close + </button> + </div> + </DialogPanel> + </div> + </Dialog> + </div> +</template> + +<script setup lang="ts"> +import { onClickOutside } from '@vueuse/core' +import useVuelidate from '@vuelidate/core' +import { reactive, ref, computed } from 'vue' +import { helpers, required, maxLength } from '@vuelidate/validators' +import { useWait, useMessage, usePassConfirm, useEnvSize, useVuelidateWrapper } from '@vnuge/vnlib.browser' +import { Dialog, DialogPanel, DialogTitle, DialogDescription } from '@headlessui/vue' + +const { headerHeight } = useEnvSize() + +//Use component side of pw prompt +const { isRevealed, confirm, cancel } = usePassConfirm() + +const { waiting } = useWait() +const { onInput } = useMessage() + +//Dialog html ref +const dialog = ref(null) + +const pwState = reactive({ password: '' }) + +const rules = { + password: { + required: helpers.withMessage('Please enter your password', required), + maxLength: helpers.withMessage('Password must be less than 100 characters', maxLength(100)) + } +} + +const v$ = useVuelidate(rules, pwState, { $lazy: true }) + +//Wrap validator so we an display error message on validation, defaults to the form toaster +const { validate } = useVuelidateWrapper(v$); + +const style = computed(() => { + return { + 'height': `calc(100vh - ${headerHeight.value}px)`, + 'top': `${headerHeight.value}px` + } +}) + +const formSubmitted = async function () { + //Calls validate on the vuelidate instance + if (!await validate()) { + return + } + + //Store pw copy + const password = v$.value.password.$model; + + //Clear the password form + v$.value.password.$model = ''; + v$.value.$reset(); + + //Pass the password to the confirm function + confirm({ password }); +} + +const close = function () { + // Clear the password form + v$.value.password.$model = ''; + v$.value.$reset(); + + //Close prompt + cancel(null); +} + +//Cancel prompt when user clicks outside of dialog, only when its open +onClickOutside(dialog, () => isRevealed.value ? cancel() : null) + +</script>
\ No newline at end of file |