aboutsummaryrefslogtreecommitdiff
path: root/front-end/src/bootstrap/components
diff options
context:
space:
mode:
Diffstat (limited to 'front-end/src/bootstrap/components')
-rw-r--r--front-end/src/bootstrap/components/ConfirmPrompt.vue69
-rw-r--r--front-end/src/bootstrap/components/CookieWarning.vue36
-rw-r--r--front-end/src/bootstrap/components/Footer.vue69
-rw-r--r--front-end/src/bootstrap/components/Header.vue162
-rw-r--r--front-end/src/bootstrap/components/PasswordPrompt.vue110
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 &copy; 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