aboutsummaryrefslogtreecommitdiff
path: root/front-end/src/bootstrap
diff options
context:
space:
mode:
Diffstat (limited to 'front-end/src/bootstrap')
-rw-r--r--front-end/src/bootstrap/Environment.vue106
-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
-rw-r--r--front-end/src/bootstrap/index.ts113
-rw-r--r--front-end/src/bootstrap/style/all.scss14
-rw-r--r--front-end/src/bootstrap/style/buttons.scss44
-rw-r--r--front-end/src/bootstrap/style/etc.scss67
-rw-r--r--front-end/src/bootstrap/style/footer.scss67
-rw-r--r--front-end/src/bootstrap/style/header.scss40
-rw-r--r--front-end/src/bootstrap/style/headings.scss32
-rw-r--r--front-end/src/bootstrap/style/inputs.scss65
-rw-r--r--front-end/src/bootstrap/style/modals.scss29
-rw-r--r--front-end/src/bootstrap/style/toast.scss26
16 files changed, 1049 insertions, 0 deletions
diff --git a/front-end/src/bootstrap/Environment.vue b/front-end/src/bootstrap/Environment.vue
new file mode 100644
index 0000000..c077869
--- /dev/null
+++ b/front-end/src/bootstrap/Environment.vue
@@ -0,0 +1,106 @@
+<template>
+ <head>
+ <title>{{ metaTile }}</title>
+ </head>
+ <div id="env-entry" ref="content" class="absolute top-0 left-0 w-full min-h-screen env-bg">
+ <div class="absolute flex w-full">
+ <notifications
+ class="general-toast"
+ group="general"
+ position="top"
+ :style="generalToastStyle"
+ />
+ </div>
+ <div class="absolute flex w-full">
+ <notifications
+ class="form-toast"
+ group="form"
+ position="top"
+ :style="formToastStyle"
+ />
+ </div>
+
+ <site-header ref="header" :site-title="siteTitle" :routes="routes" >
+ <template #site_logo>
+ <!-- Use the global site-logo if enabled -->
+ <site-logo />
+ </template>
+ </site-header>
+
+ <div id="env-body" class="flex w-full" :style="bodyStyle">
+ <cookie-warning :hidden="showCookieWarning" />
+
+ <slot name="main" />
+ </div>
+
+ <!-- Setup footer with nav elements from global config -->
+ <site-footer ref="footer">
+ <template #footer-nav-1>
+ <footer-nav-1/>
+ </template>
+ <template #footer-nav-2>
+ <footer-nav-2/>
+ </template>
+ </site-footer>
+
+ <PasswordPrompt />
+ <ConfirmPrompt />
+ </div>
+</template>
+
+<script setup lang="ts">
+
+import { computed } from 'vue'
+import { RouteLocation, useRouter } from 'vue-router'
+import { filter, map, without, find, includes } from 'lodash'
+import { useEnvSize, useScrollOnRouteChange, useSession, useTitle } from '@vnuge/vnlib.browser'
+import CookieWarning from './components/CookieWarning.vue'
+import PasswordPrompt from './components/PasswordPrompt.vue'
+import siteHeader from './components/Header.vue'
+import siteFooter from './components/Footer.vue'
+import ConfirmPrompt from './components/ConfirmPrompt.vue'
+import { headerRoutes, authRoutes, siteTitle, showCookieWarning } from './index'
+
+const { loggedIn } = useSession();
+const { getRoutes } = useRouter();
+const { title } = useTitle(siteTitle.value);
+
+//Use the env size to calculate the header and footer heights for us
+const { header, footer, content, headerHeight, footerHeight } = useEnvSize(true)
+
+//setup autoscroll
+useScrollOnRouteChange();
+
+//Compute meta title from the default site title and the page title
+const metaTile = computed(() => title.value ? `${title.value} | ${siteTitle.value}` : siteTitle.value)
+
+const routes = computed<RouteLocation[]>(() => {
+ // Get routes that are defined above but only if they are defined in the router
+ // This is a computed property because loggedin is a reactive property
+ const routeNames = loggedIn.value ? authRoutes.value : headerRoutes.value
+
+ const routes = filter(getRoutes(), (pageName) => includes(routeNames, pageName.name))
+
+ const activeRoutes = map(routeNames, route => find(routes, { name: route }))
+
+ return without<RouteLocation>(activeRoutes, undefined)
+})
+
+
+//Forces the page content to be exactly the height of the viewport - header and footer sizes
+const bodyStyle = computed(() => {
+ return { 'min-height': `calc(100vh - ${headerHeight.value + footerHeight.value}px)` }
+})
+
+const generalToastStyle = computed(() => {
+ return { top: `${headerHeight.value + 5}px` }
+})
+
+const formToastStyle = computed(() => {
+ return { top: `${headerHeight.value}px` }
+})
+</script>
+
+<style>
+
+</style>
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
diff --git a/front-end/src/bootstrap/index.ts b/front-end/src/bootstrap/index.ts
new file mode 100644
index 0000000..ead41e1
--- /dev/null
+++ b/front-end/src/bootstrap/index.ts
@@ -0,0 +1,113 @@
+import App from '../App.vue'
+import Notifications, { notify } from '@kyvg/vue3-notification'
+import { configureNotifier } from '@vnuge/vnlib.browser'
+import { createApp as vueCreateApp, ref } from "vue";
+import { useDark } from "@vueuse/core";
+
+//Font awesome support
+import { Library } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
+import { faBars, faLightbulb, faTimes } from '@fortawesome/free-solid-svg-icons'
+
+
+//Required global state elements for app components
+export const headerRoutes = ref<string[]>([]);
+export const authRoutes = ref<string[]>([]);
+export const siteTitle = ref<string>("");
+export const showCookieWarning = ref<boolean>(false);
+
+
+export interface AppConfig {
+ /**
+ * Routes to be displayed in the header when the user is not logged in
+ */
+ headerRoutes: string[];
+
+ /**
+ * Routes to be displayed in the header when the user is logged in
+ */
+ authRoutes: string[];
+
+ /**
+ * The title of the site, used in the title bar
+ */
+ siteTitle: string;
+
+ /**
+ * Enables dark mode support
+ */
+ useDarkMode: boolean;
+
+ /**
+ * The element to mount the app to
+ */
+ mountElement: string;
+
+ /**
+ * library instance for adding required icons
+ */
+ faLibrary: Library;
+
+ /**
+ * If true, the cookie warning will not be displayed
+ */
+ hideCookieWarning?: boolean;
+
+ /**
+ * Called when the app is created for you to add custom elements
+ * and configure the app
+ * @param app The app instance
+ */
+ onCreate: (app: any) => void;
+}
+
+/**
+ * Creates a new vn-minimal vuejs web-app with the given configuration
+ * @param config The configuration for the app
+ * @returns The app instance
+ */
+export const createVnApp = (config: AppConfig, createApp?: (app: any) => any) => {
+ headerRoutes.value = config.headerRoutes;
+ authRoutes.value = config.authRoutes;
+ siteTitle.value = config.siteTitle;
+
+ //Allow the user to override the createApp function
+ createApp = createApp || vueCreateApp;
+
+ //Enable dark mode support
+ if (config.useDarkMode) {
+ useDark({
+ selector: 'html',
+ valueDark: 'dark',
+ valueLight: 'light',
+ });
+ }
+
+ if (!config.hideCookieWarning) {
+ //Configure the cookie warning to be displayed cookies are not enabled
+ showCookieWarning.value = navigator?.cookieEnabled === false;
+ }
+
+ //Add required icons to library
+ config.faLibrary.add(faBars, faLightbulb, faTimes);
+
+ //create the vue app
+ const app = createApp(App)
+
+ //Add the library to the app
+ app.component('fa-icon', FontAwesomeIcon)
+
+ //Add the notification and router to the app
+ app.use(Notifications);
+
+ //Call the onCreate callback
+ config.onCreate(app);
+
+ //Mount the app
+ app.mount(config.mountElement);
+
+ //Configure notification handler
+ configureNotifier({ notify, close: notify.close });
+
+ return app;
+} \ No newline at end of file
diff --git a/front-end/src/bootstrap/style/all.scss b/front-end/src/bootstrap/style/all.scss
new file mode 100644
index 0000000..ede0b30
--- /dev/null
+++ b/front-end/src/bootstrap/style/all.scss
@@ -0,0 +1,14 @@
+@tailwind base;
+
+@tailwind components;
+
+@tailwind utilities;
+
+@import "./buttons.scss";
+@import "./footer.scss";
+@import "./header.scss";
+@import "./inputs.scss";
+@import "./headings.scss";
+@import "./toast.scss";
+@import "./etc.scss";
+@import "./modals.scss"; \ No newline at end of file
diff --git a/front-end/src/bootstrap/style/buttons.scss b/front-end/src/bootstrap/style/buttons.scss
new file mode 100644
index 0000000..a440def
--- /dev/null
+++ b/front-end/src/bootstrap/style/buttons.scss
@@ -0,0 +1,44 @@
+.button-group {
+ @apply inline-flex -space-x-0 divide-x overflow-hidden rounded-lg border border-transparent shadow-sm;
+ @apply divide-gray-300 border-gray-300 dark:divide-dark-500 dark:border-dark-500;
+
+ & .btn {
+ @apply border-0 ring-0 focus:ring-0;
+ }
+}
+
+.btn {
+ @apply ease-in-out duration-100 border-2 px-4 py-2 text-center text-sm font-medium transition-all focus:ring-2;
+ @apply bg-white border-gray-300 text-gray-700 shadow-sm hover:bg-gray-100 focus:ring-gray-100;
+
+ .dark & {
+ @apply bg-transparent text-inherit border-dark-300 hover:bg-transparent hover:border-gray-400 focus:ring-gray-300;
+ }
+
+ &.borderless,
+ &.no-border,
+ &.b-0{
+ @apply bg-transparent border-0 shadow-none ring-0 focus:ring-0 active:ring-0;
+ }
+
+ &:disabled {
+ @apply cursor-not-allowed border-gray-100 bg-gray-50 text-gray-400;
+ @apply dark:bg-transparent dark:border-dark-400 dark:text-dark-300;
+ }
+
+ &.sm {
+ @apply px-3 py-1.5 text-sm;
+ }
+
+ &.xs {
+ @apply px-2 py-1;
+ }
+
+ &.lg {
+ @apply px-5 py-3 text-lg;
+ }
+
+ &.xl {
+ @apply px-6 py-4 text-xl;
+ }
+}
diff --git a/front-end/src/bootstrap/style/etc.scss b/front-end/src/bootstrap/style/etc.scss
new file mode 100644
index 0000000..5d65ff4
--- /dev/null
+++ b/front-end/src/bootstrap/style/etc.scss
@@ -0,0 +1,67 @@
+#header-mobile-nav a:hover,
+#header-desktop-nav a:hover,
+#header-mobile-nav .router-link-active,
+#header-desktop-nav .router-link-active,
+footer .footer-content .router-link-active {
+ @apply text-primary-500 dark:text-primary-600;
+}
+
+#header-mobile-nav a {
+ @apply text-2xl;
+}
+
+#site-title h3 {
+ @apply mb-0 text-xl;
+}
+
+
+.env-bg {
+ font-family: "Nunito";
+ background: #f7f7f7;
+ @apply dark:bg-dark-900;
+ @apply text-gray-700 dark:text-gray-300;
+}
+
+.env-bg-gradient {
+ background: #98E4C8;
+ background: -webkit-linear-gradient(bottom right, #98E4C8, #2C6BC3);
+ background: -moz-linear-gradient(bottom right, #98E4C8, #2C6BC3);
+ background: linear-gradient(to top left, #98E4C8, #2C6BC3);
+ @apply text-gray-700;
+}
+
+.app-component-entry {
+ @apply flex flex-col flex-auto mb-10;
+
+ @screen sm {
+ & {
+ min-height: 640px;
+ }
+ }
+}
+
+.vue-notification-group.general-toast {
+ @apply right-5;
+}
+
+.float-label {
+ @apply mt-5 relative;
+
+ &>label {
+ @apply absolute top-0 left-0 bottom-0 block pl-3 ease-linear duration-75 opacity-0;
+ }
+
+ &>input:focus+label {
+ @apply opacity-100 -mt-6;
+ }
+}
+
+.default-page-template {
+ min-height: 400px;
+ @apply container w-full max-w-4xl px-4 pt-3 mx-auto sm:pt-6 md:px-0;
+}
+
+a.link {
+ @apply duration-150 ease-in-out;
+ @apply text-primary-500 hover:text-primary-600;
+} \ No newline at end of file
diff --git a/front-end/src/bootstrap/style/footer.scss b/front-end/src/bootstrap/style/footer.scss
new file mode 100644
index 0000000..4d05928
--- /dev/null
+++ b/front-end/src/bootstrap/style/footer.scss
@@ -0,0 +1,67 @@
+footer{
+ @apply text-center shadow-md bg-white dark:bg-dark-800 dark:text-gray-500;
+
+ .footer-content{
+ @apply mx-auto max-w-7xl p-4;
+
+ nav,
+ .color-selector-container{
+ @apply col-span-4 sm:col-span-3 md:col-span-2 lg:col-span-2 ml-8 sm:ml-0;
+ }
+ .footer-link.router-link-active {
+ @apply text-primary-500 dark:text-primary-600;
+ }
+
+ .footer-main-container {
+ @apply grid mb-3 sm:pl-0;
+ @apply grid-cols-4 sm:grid-cols-6 md:grid-cols-6 lg:grid-cols-11 gap-10 lg:gap-20;
+ }
+
+ p.nav-title {
+ @apply mb-3 text-xs font-semibold tracking-wider uppercase text-left;
+ }
+
+ a.footer-link {
+ @apply flex mb-3 text-sm font-medium md:mb-2 text-center transform hover:scale-105 ease-in-out duration-75;
+ @apply hover:text-primary-500 dark:hover:text-primary-600;
+ }
+
+ button.bg-sel-btn {
+ @apply text-sm ease-in-out duration-100 transform hover:scale-105;
+ @apply hover:text-primary-500 dark:hover:text-primary-600;
+ }
+
+ .footer-lower {
+ @apply flex flex-col flex-wrap items-start justify-between pt-10 mt-10 border-t md:flex-row md:items-center dark:border-dark-500;
+ }
+ }
+
+ .env-bg-gradient &{
+ @apply text-current text-gray-700 bg-transparent;
+ .footer-content {
+ border-top: 1px solid;
+ @apply border-gray-200;
+
+ .nav-title {
+ @apply text-gray-700;
+ }
+
+ nav .footer-link,
+ button.bg-sel-btn{
+ @apply hover:text-white;
+
+ &.router-link-active {
+ @apply text-white;
+ }
+ }
+
+ button.bg-sel-btn{
+ @apply hover:text-white;
+ }
+ }
+
+ .footer-lower {
+ @apply border-t-0;
+ }
+ }
+} \ No newline at end of file
diff --git a/front-end/src/bootstrap/style/header.scss b/front-end/src/bootstrap/style/header.scss
new file mode 100644
index 0000000..bc5789d
--- /dev/null
+++ b/front-end/src/bootstrap/style/header.scss
@@ -0,0 +1,40 @@
+header{
+ .header-container{
+ @apply h-12;
+ }
+ .header-container,
+ .side-menu{
+ @apply shadow-md;
+ @apply bg-white dark:bg-dark-800 dark:text-gray-100;
+ }
+
+ .side-menu {
+ @apply absolute pt-2 ease-in-out duration-150;
+ }
+
+ .drop-controller {
+ min-width: 11rem;
+ @apply hidden md:flex mr-0 ml-auto my-0 relative;
+
+ .drop-menu{
+ @apply w-36 m-auto cursor-pointer text-left rounded-b-lg flex flex-col;
+
+ @apply text-gray-700 bg-white dark:bg-dark-800 dark:text-gray-300;
+
+ @apply bg-white border-b border-l border-r border-transparent;
+
+ a {
+ @apply px-3 py-1;
+ @apply text-gray-700 hover:text-black dark:text-gray-300 dark:hover:text-gray-100;
+ }
+ }
+
+ &.hovered .drop-menu {
+ @apply border-gray-200 shadow-md dark:border-dark-500;
+ }
+ }
+
+ .user-menu{
+ @apply m-auto cursor-default truncate whitespace-nowrap max-w-xs;
+ }
+} \ No newline at end of file
diff --git a/front-end/src/bootstrap/style/headings.scss b/front-end/src/bootstrap/style/headings.scss
new file mode 100644
index 0000000..326a7c2
--- /dev/null
+++ b/front-end/src/bootstrap/style/headings.scss
@@ -0,0 +1,32 @@
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ @apply font-medium leading-tight mt-0 mb-2;
+}
+
+h1 {
+ @apply sm:text-5xl text-4xl;
+}
+
+h2 {
+ @apply sm:text-4xl text-3xl;
+}
+
+h3 {
+ @apply sm:text-3xl text-2xl;
+}
+
+h4 {
+ @apply sm:text-2xl text-xl;
+}
+
+h5 {
+ @apply sm:text-xl text-lg;
+}
+
+h6 {
+ @apply sm:text-base text-sm;
+} \ No newline at end of file
diff --git a/front-end/src/bootstrap/style/inputs.scss b/front-end/src/bootstrap/style/inputs.scss
new file mode 100644
index 0000000..64f8901
--- /dev/null
+++ b/front-end/src/bootstrap/style/inputs.scss
@@ -0,0 +1,65 @@
+input.input,
+select.input,
+textarea.input {
+ @apply duration-100 ease-in-out outline-none border p-2;
+ @apply border-gray-200 bg-inherit dark:border-dark-400 dark:text-white hover:border-gray-300 hover:dark:border-dark-200;
+}
+
+
+/* CHECKBOXES */
+
+label.checkbox {
+ @apply flex items-center cursor-pointer;
+
+ input[type="checkbox"] {
+ @apply ease-in-out duration-100 w-5 h-5;
+ @apply border-2 rounded-sm border-gray-300 dark:border-dark-500;
+
+ &:checked {
+ @apply text-primary-500 dark:text-primary-600 border-primary-500 dark:border-primary-600;
+ }
+ }
+
+ &.primary {
+ input[type="checkbox"] {
+ @apply appearance-none;
+ @apply hover:border-primary-500 dark:hover:border-primary-600;
+
+ &:checked {
+ @apply bg-primary-500 dark:bg-primary-600 border-primary-500 dark:border-primary-600;
+ }
+
+ &+span.check {
+ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
+ @apply bg-white dark:bg-dark-500;
+ }
+ }
+
+ span.check {
+ margin: 0px 0px 1px 4px;
+ @apply absolute h-3 w-3;
+ }
+ }
+
+}
+
+/*Select */
+
+select.input.options {
+ @apply text-current;
+}
+
+select.input:disabled{
+ @apply appearance-none;
+}
+
+/*Validation inputs*/
+input.input.dirty.data-valid,
+select.input.dirty.data-valid {
+ @apply border-primary-500 dark:border-primary-600;
+}
+
+input.input.dirty.data-invalid,
+select.input.dirty.data-invalid {
+ @apply border-red-600 dark:border-red-500;
+}
diff --git a/front-end/src/bootstrap/style/modals.scss b/front-end/src/bootstrap/style/modals.scss
new file mode 100644
index 0000000..254b8e1
--- /dev/null
+++ b/front-end/src/bootstrap/style/modals.scss
@@ -0,0 +1,29 @@
+.modal-entry {
+ background: #00000077;
+ @apply fixed z-50 flex w-full px-6;
+
+ .modal-content-container {
+ @apply w-full max-w-md p-5 m-auto rounded-md shadow-2xl mt-44;
+ @apply bg-white border border-transparent dark:bg-dark-600 dark:border-primary-500 dark:text-white;
+
+ .modal-title {
+ @apply text-xl font-bold;
+ }
+
+ .modal-description {
+ @apply text-sm;
+ }
+ }
+
+ .modal-button-container {
+ @apply flex flex-row justify-end pt-3 gap-3;
+ }
+
+ .input-container {
+ @apply pt-5;
+
+ input {
+ @apply w-full;
+ }
+ }
+} \ No newline at end of file
diff --git a/front-end/src/bootstrap/style/toast.scss b/front-end/src/bootstrap/style/toast.scss
new file mode 100644
index 0000000..3ddc3ac
--- /dev/null
+++ b/front-end/src/bootstrap/style/toast.scss
@@ -0,0 +1,26 @@
+.general-toast {
+ .notification-title {
+ font-size: 16px;
+ }
+
+ .notification-content {
+ font-size: 14px;
+ }
+
+ .vue-notification {
+ @apply duration-200 ease-in-out shadow-md hover:shadow-lg;
+ }
+}
+
+.form-toast {
+ left: calc(50% - 150px);
+ @apply pt-2 mx-auto mb-3;
+
+ .notification-title {
+ font-size: 14px;
+ }
+
+ .notification-content {
+ font-size: 12px;
+ }
+}