This commit is contained in:
2023-02-27 14:52:01 +10:00
parent aeb7939c6e
commit c8e49ba49c
71 changed files with 958 additions and 1358 deletions

6
import-meta.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
interface ImportMeta {
env: {
APP_URL: string;
[key: string]: string;
};
}

12
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"@tinymce/tinymce-vue": "^4.0.7", "@tinymce/tinymce-vue": "^4.0.7",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
"@vuepic/vue-datepicker": "^3.6.4", "@vuepic/vue-datepicker": "^3.6.4",
"dompurify": "^3.0.0",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pinia": "^2.0.28", "pinia": "^2.0.28",
@@ -1721,9 +1722,9 @@
} }
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "2.4.4", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.4.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.0.tgz",
"integrity": "sha512-1e2SpqHiRx4DPvmRuXU5J0di3iQACwJM+mFGE2HAkkK7Tbnfk9WcghcAmyWc9CRrjyRRUpmuhPUH6LphQQR3EQ==" "integrity": "sha512-0g/yr2IJn4nTbxwL785YxS7/AvvgGFJw6LLWP+BzWzB1+BYOqPUT9Hy0rXrZh5HLdHnxH72aDdzvC9SdTjsuaA=="
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.0.3", "version": "16.0.3",
@@ -4071,6 +4072,11 @@
"vue": "^2.7.0 || ^3.0.0" "vue": "^2.7.0 || ^3.0.0"
} }
}, },
"node_modules/vue-dompurify-html/node_modules/dompurify": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.4.tgz",
"integrity": "sha512-1e2SpqHiRx4DPvmRuXU5J0di3iQACwJM+mFGE2HAkkK7Tbnfk9WcghcAmyWc9CRrjyRRUpmuhPUH6LphQQR3EQ=="
},
"node_modules/vue-eslint-parser": { "node_modules/vue-eslint-parser": {
"version": "9.1.0", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz",

View File

@@ -29,6 +29,7 @@
"@tinymce/tinymce-vue": "^4.0.7", "@tinymce/tinymce-vue": "^4.0.7",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
"@vuepic/vue-datepicker": "^3.6.4", "@vuepic/vue-datepicker": "^3.6.4",
"dompurify": "^3.0.0",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pinia": "^2.0.28", "pinia": "^2.0.28",

View File

@@ -124,11 +124,6 @@ select {
} }
} }
svg,
button {
@extend .prevent-select;
}
code { code {
display: block; display: block;
font-size: 0.8rem; font-size: 0.8rem;
@@ -165,96 +160,6 @@ code {
} }
} }
/* Button */
button.button,
a.button,
label.button {
padding: map-get($spacer, 2) map-get($spacer, 4);
color: white;
font-weight: 800;
border-width: 2px;
border-style: solid;
border-radius: 24px;
transition: background-color 0.1s, color 0.1s;
cursor: pointer;
background-color: $secondary-color;
border-color: $secondary-color;
min-width: 7rem;
text-align: center;
&:disabled {
cursor: not-allowed;
background-color: $secondary-color !important;
border-color: $secondary-color !important;
opacity: 0.5;
}
&:hover:not(:disabled) {
text-decoration: none;
color: $secondary-color;
}
&.primary {
background-color: $primary-color;
border-color: $primary-color;
&:hover:not(:disabled) {
color: $primary-color;
}
}
&.secondary {
background-color: $secondary-color;
border-color: $secondary-color;
&:hover:not(:disabled) {
color: $secondary-color;
}
}
&.danger {
background-color: $danger-color;
border-color: $danger-color;
&:hover:not(:disabled) {
color: $danger-color;
}
}
&.outline {
background-color: transparent;
border-color: $outline-color;
color: $outline-color;
&:hover:not(:disabled) {
background-color: $outline-color;
border-color: $outline-color;
color: $outline-hover-color;
}
}
&:hover:not(:disabled) {
background-color: #fff;
}
svg {
padding-left: 0.5rem;
vertical-align: middle !important;
}
}
.button + .button {
margin: 0 map-get($spacer, 2);
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
/* Page Errors */ /* Page Errors */
.page-error { .page-error {
display: flex; display: flex;

View File

@@ -193,6 +193,7 @@
/* Utility */ /* Utility */
.prevent-select { .prevent-select {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
} }

View File

@@ -6,17 +6,17 @@
<div <div
v-for="file of props.attachments" v-for="file of props.attachments"
:key="file.id" :key="file.id"
class="attachment-row"> class="sm-attachment-row">
<div class="attachment-file-icon"> <div class="sm-attachment-file-icon">
<img <img
:src="getFileIconImagePath(file.title || file.name)" :src="getFileIconImagePath(file.title || file.name)"
height="48" height="48"
width="48" /> width="48" />
</div> </div>
<a :href="file.url" class="attachment-file-name">{{ <a :href="file.url" class="sm-attachment-file-name">{{
file.title || file.name file.title || file.name
}}</a> }}</a>
<div class="attachment-file-size"> <div class="sm-attachment-file-size">
({{ bytesReadable(file.size) }}) ({{ bytesReadable(file.size) }})
</div> </div>
</div> </div>
@@ -25,6 +25,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { bytesReadable } from "../helpers/types"; import { bytesReadable } from "../helpers/types";
import { getFileIconImagePath } from "../helpers/utils";
import SMContainer from "./SMContainer.vue"; import SMContainer from "./SMContainer.vue";
const props = defineProps({ const props = defineProps({
@@ -33,11 +34,6 @@ const props = defineProps({
required: true, required: true,
}, },
}); });
const getFileIconImagePath = (fileName: string): string => {
const ext = fileName.split(".").pop();
return `/img/fileicons/${ext}.png`;
};
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -46,7 +42,7 @@ const getFileIconImagePath = (fileName: string): string => {
margin-top: map-get($spacer, 3); margin-top: map-get($spacer, 3);
} }
.attachment-row { .sm-attachment-row {
border-bottom: 1px solid $secondary-background-color; border-bottom: 1px solid $secondary-background-color;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -56,13 +52,13 @@ const getFileIconImagePath = (fileName: string): string => {
border-bottom: 0; border-bottom: 0;
} }
.attachment-file-icon { .sm-attachment-file-icon {
display: flex; display: flex;
width: 64px; width: 64px;
justify-content: center; justify-content: center;
} }
.attachment-file-size { .sm-attachment-file-size {
font-size: 75%; font-size: 75%;
padding-left: 0.75rem; padding-left: 0.75rem;
color: $secondary-color-dark; color: $secondary-color-dark;

View File

@@ -1,51 +1,53 @@
<template> <template>
<SMContainer <SMContainer
v-if="showBreadcrumbs"
:class="[ :class="[
'flex-0', 'flex-0',
'breadcrumbs-outer', 'sm-breadcrumbs-container',
{ closed: breadcrumbs.length == 0 }, { closed: computedRouteCrumbs.length == 0 },
]"> ]">
<ul class="breadcrumbs"> <ul class="sm-breadcrumbs">
<li><router-link :to="{ name: 'home' }">Home</router-link></li> <li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li v-for="(val, idx) of breadcrumbs" :key="val.name"> <li
v-for="(routeItem, index) of computedRouteCrumbs"
:key="routeItem.name">
<router-link <router-link
v-if="idx != breadcrumbs.length - 1" v-if="index != computedRouteCrumbs.length - 1"
:to="{ name: val.name }" :to="{ name: routeItem.name }"
>{{ val.meta?.title || val.name }}</router-link >{{ routeItem.meta?.title || routeItem.name }}</router-link
><span v-else>{{ val.meta?.title || val.name }}</span> ><span v-else>{{
routeItem.meta?.title || routeItem.name
}}</span>
</li> </li>
</ul> </ul>
</SMContainer> </SMContainer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue"; import { computed, ComputedRef } from "vue";
import { useRoute } from "vue-router"; import { RouteRecordRaw, useRoute } from "vue-router";
import { routes } from "../router"; import { routes } from "../router";
import { useApplicationStore } from "../store/ApplicationStore";
const applicationStore = useApplicationStore(); /**
const showBreadcrumbs = ref(true); * Return a list of routes from the current page back to the root
*/
const breadcrumbs = computed(() => { const computedRouteCrumbs: ComputedRef<RouteRecordRaw[]> = computed(() => {
const currentPageName = useRoute().name; const currentPageName = useRoute().name;
if (currentPageName == "home") { if (currentPageName == "home") {
return []; return [];
} }
const findMatch = (list) => { const findMatch = (list: RouteRecordRaw[]): RouteRecordRaw[] | null => {
let found = null; let found: RouteRecordRaw[] | null = null;
let index = null; let index: RouteRecordRaw | null = null;
let child = null; let child: RouteRecordRaw[] | null = null;
list.every((entry) => { list.every((entry: RouteRecordRaw) => {
if (index == null && "path" in entry && entry.path == "") { if (index == null && "path" in entry && entry.path == "") {
index = entry; index = entry;
} }
if (child == null && "children" in entry) { if (child == null && entry.children) {
child = findMatch(entry.children); child = findMatch(entry.children);
} }
@@ -74,32 +76,18 @@ const breadcrumbs = computed(() => {
}; };
let itemList = findMatch(routes); let itemList = findMatch(routes);
if (itemList) {
if (applicationStore.dynamicTitle.length > 0) {
let meta = [];
if ("meta" in itemList) {
meta = itemList[itemList.length - 1];
}
meta["title"] = applicationStore.dynamicTitle;
itemList[itemList.length - 1]["meta"] = meta;
}
}
return itemList || []; return itemList || [];
}); });
</script> </script>
<style lang="scss"> <style lang="scss">
.breadcrumbs-outer.closed .breadcrumbs { .sm-breadcrumbs-container.closed .sm-breadcrumbs {
opacity: 0; opacity: 0;
transition: opacity 0s; transition: opacity 0s;
transition-delay: 0s; transition-delay: 0s;
} }
.breadcrumbs { .sm-breadcrumbs {
height: 3.25rem; height: 3.25rem;
display: flex; display: flex;
max-width: 1200px; max-width: 1200px;

View File

@@ -1,36 +1,26 @@
<template> <template>
<a
v-if="href.length > 0 || typeof to == 'string'"
:href="href"
:disabled="disabled"
:class="[
'button',
'prevent-select',
classType,
{ 'button-block': block },
]"
:type="buttonType">
{{ label }}
<ion-icon v-if="icon" :icon="icon" />
</a>
<button <button
v-else-if="to == null" v-if="isEmpty(to)"
:disabled="disabled" :disabled="disabled"
:class="[ :class="[
'button', 'sm-button',
'prevent-select',
classType, classType,
{ 'button-block': block }, { 'sm-button-block': block },
{ 'dropdown-button': dropdown }, { 'sm-dropdown-button': dropdown },
]" ]"
:type="buttonType" :type="buttonType"
@click="handleClick"> @click="handleClick">
<ion-icon
v-if="icon && dropdown == null && iconLocation == 'before'"
:icon="icon" />
<span>{{ label }}</span> <span>{{ label }}</span>
<ion-icon v-if="icon && dropdown == null" :icon="icon" /> <ion-icon
v-if="icon && dropdown == null && iconLocation == 'after'"
:icon="icon" />
<ion-icon <ion-icon
v-if="dropdown != null" v-if="dropdown != null"
name="caret-down-outline" name="caret-down-outline"
@click.stop="handleToggleDropdown" /> @click.stop="handleClickToggleDropdown" />
<ul <ul
v-if="dropdown != null" v-if="dropdown != null"
ref="dropdownMenu" ref="dropdownMenu"
@@ -43,23 +33,29 @@
</li> </li>
</ul> </ul>
</button> </button>
<router-link <a
v-else v-else-if="!isEmpty(to) && typeof to == 'string'"
:to="to" :href="to"
:disabled="disabled" :disabled="disabled"
:class="[ :class="['sm-button', classType, { 'sm-button-block': block }]"
'button', :type="buttonType">
'prevent-select',
classType,
{ 'button-block': block },
]">
{{ label }} {{ label }}
<ion-icon v-if="icon" :icon="icon" /> <ion-icon v-if="icon" :icon="icon" />
</a>
<router-link
v-else-if="!isEmpty(to) && typeof to == 'object'"
:to="to"
:disabled="disabled"
:class="['sm-button', classType, { 'sm-button-block': block }]">
<ion-icon v-if="icon && iconLocation == 'before'" :icon="icon" />
{{ label }}
<ion-icon v-if="icon && iconLocation == 'after'" :icon="icon" />
</router-link> </router-link>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { Ref, ref } from "vue";
import { isEmpty } from "../helpers/utils";
const props = defineProps({ const props = defineProps({
label: { type: String, default: "Button", required: false }, label: { type: String, default: "Button", required: false },
@@ -69,17 +65,20 @@ const props = defineProps({
default: "", default: "",
required: false, required: false,
}, },
iconLocation: {
type: String,
default: "before",
required: false,
validator: (value: string) => {
return ["before", "after"].includes(value);
},
},
to: { to: {
type: [String, Object], type: [String, Object],
default: null, default: null,
required: false, required: false,
validator: (prop) => typeof prop === "object" || prop === null, validator: (prop) => typeof prop === "object" || prop === null,
}, },
href: {
type: String,
default: "",
required: false,
},
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false, default: false,
@@ -98,21 +97,26 @@ const props = defineProps({
}, },
}); });
const buttonType = props.type == "submit" ? "submit" : "button"; const buttonType: "submit" | "button" =
props.type == "submit" ? "submit" : "button";
const classType = props.type == "submit" ? "primary" : props.type; const classType = props.type == "submit" ? "primary" : props.type;
const dropdownMenu = ref(null); const dropdownMenu: Ref<HTMLElement | null> = ref(null);
const emits = defineEmits(["click"]); const emits = defineEmits(["click"]);
const handleClick = () => { const handleClick = () => {
emits("click", ""); emits("click", "");
}; };
const handleToggleDropdown = () => { const handleClickToggleDropdown = () => {
if (dropdownMenu.value) {
dropdownMenu.value.style.display = "block"; dropdownMenu.value.style.display = "block";
}
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
if (dropdownMenu.value) {
dropdownMenu.value.style.display = "none"; dropdownMenu.value.style.display = "none";
}
}; };
const handleClickItem = (item: string) => { const handleClickItem = (item: string) => {
@@ -121,16 +125,40 @@ const handleClickItem = (item: string) => {
</script> </script>
<style lang="scss"> <style lang="scss">
.button { a.sm-button,
.sm-button {
cursor: pointer; cursor: pointer;
position: relative; position: relative;
padding: map-get($spacer, 2) map-get($spacer, 4);
color: white;
font-weight: 800;
border-width: 2px;
border-style: solid;
border-radius: 24px;
transition: background-color 0.1s, color 0.1s;
background-color: $secondary-color;
border-color: $secondary-color;
min-width: 7rem;
text-align: center;
display: inline-block;
&.button-block { -webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
&.sm-button-block {
display: block; display: block;
width: 100%; width: 100%;
} }
&.dropdown-button { &.sm-button-small {
font-size: 85%;
font-weight: normal;
padding: map-get($spacer, 1) map-get($spacer, 3);
}
&.sm-dropdown-button {
padding: 0; padding: 0;
white-space: nowrap; white-space: nowrap;
display: flex; display: flex;
@@ -146,7 +174,6 @@ const handleClickItem = (item: string) => {
span { span {
flex: 1; flex: 1;
border-right: 1px solid $primary-color; border-right: 1px solid $primary-color;
padding: 0;
padding-top: calc(#{map-get($spacer, 1)} / 1.5); padding-top: calc(#{map-get($spacer, 1)} / 1.5);
padding-bottom: calc(#{map-get($spacer, 1)} / 1.5); padding-bottom: calc(#{map-get($spacer, 1)} / 1.5);
padding-left: map-get($spacer, 3); padding-left: map-get($spacer, 3);
@@ -169,6 +196,91 @@ const handleClickItem = (item: string) => {
} }
} }
&:disabled {
cursor: not-allowed;
background-color: $secondary-color !important;
border-color: $secondary-color !important;
opacity: 0.5;
}
&:hover:not(:disabled) {
text-decoration: none;
color: $secondary-color;
}
&.primary {
background-color: $primary-color;
border-color: $primary-color;
&:hover:not(:disabled) {
color: $primary-color;
}
}
&.primary-outline {
background-color: transparent;
border-color: $primary-color;
color: $primary-color;
&:hover:not(:disabled) {
color: $primary-color;
}
}
&.secondary {
background-color: $secondary-color;
border-color: $secondary-color;
&:hover:not(:disabled) {
color: $secondary-color;
}
}
&.secondary-outline {
background-color: transparent;
border-color: $secondary-color;
color: $secondary-color;
&:hover:not(:disabled) {
color: $secondary-color;
}
}
&.danger {
background-color: $danger-color;
border-color: $danger-color;
&:hover:not(:disabled) {
color: $danger-color;
}
}
&.danger-outline {
background-color: transparent;
border-color: $danger-color;
color: $danger-color;
&:hover:not(:disabled) {
color: $danger-color;
}
}
&.outline {
background-color: transparent;
border-color: $outline-color;
color: $outline-color;
&:hover:not(:disabled) {
background-color: $outline-color;
border-color: $outline-color;
color: $outline-hover-color;
}
}
&:hover:not(:disabled) {
background-color: #fff;
}
ion-icon { ion-icon {
height: 1.2rem; height: 1.2rem;
width: 1.2rem; width: 1.2rem;
@@ -195,7 +307,7 @@ const handleClickItem = (item: string) => {
li { li {
padding: 12px 16px; padding: 12px 16px;
cursor: pointer; cursor: pointer;
transition: background 0.1s ease-in-out; transition: background-color 0.1s ease-in-out;
} }
li:hover { li:hover {

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="captcha-notice"> <div class="sm-captcha-notice">
This site is protected by reCAPTCHA and the Google This site is protected by reCAPTCHA and the Google
<a href="https://policies.google.com/privacy">Privacy Policy</a> and <a href="https://policies.google.com/privacy">Privacy Policy</a> and
<a href="https://policies.google.com/terms">Terms of Service</a> apply. <a href="https://policies.google.com/terms">Terms of Service</a> apply.
@@ -7,7 +7,7 @@
</template> </template>
<style lang="scss"> <style lang="scss">
.captcha-notice { .sm-captcha-notice {
color: $secondary-color; color: $secondary-color;
font-size: 65%; font-size: 65%;
line-height: 1.2rem; line-height: 1.2rem;

View File

@@ -1,60 +1,81 @@
<template> <template>
<div <div
class="carousel" class="sm-carousel"
@mouseover="handleMouseOver" @mouseover="handleMouseOver"
@mouseleave="handleMouseLeave"> @mouseleave="handleMouseLeave">
<div ref="slides" class="carousel-slides"> <div ref="slides" class="sm-carousel-slides">
<slot></slot> <slot></slot>
</div> </div>
<div class="carousel-slide-prev" @click="handleSlidePrev"> <div class="sm-carousel-slide-prev" @click="handleClickSlidePrev">
<ion-icon name="chevron-back-outline" /> <ion-icon name="chevron-back-outline" />
</div> </div>
<div class="carousel-slide-next" @click="handleSlideNext"> <div class="sm-carousel-slide-next" @click="handleClickSlideNext">
<ion-icon name="chevron-forward-outline" /> <ion-icon name="chevron-forward-outline" />
</div> </div>
<div class="carousel-slide-indicators"> <div class="sm-carousel-slide-indicators">
<div <div
v-for="(indicator, index) in slideElements" v-for="(indicator, index) in slideElements"
:key="index" :key="index"
:class="[ :class="[
'carousel-slide-indicator-item', 'sm-carousel-slide-indicator-item',
{ highlighted: currentSlide == index }, { highlighted: currentSlide == index },
]" ]"
@click="handleIndicator(index)"></div> @click="handleClickIndicator(index)"></div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue"; import { onMounted, onUnmounted, Ref, ref } from "vue";
const slides = ref(null); /**
let slideElements = ref([]); * Reference to slides element.
*/
const slides: Ref<HTMLElement | null> = ref(null);
/**
* The list of slide elements.
*/
let slideElements: Ref<NodeList | null> = ref(null);
/**
* Index of the current slide.
*/
let currentSlide = ref(0); let currentSlide = ref(0);
/**
* The maximum number of slides.
*/
let maxSlide = ref(0); let maxSlide = ref(0);
let intervalRef = null;
const mutationObserver = ref(null);
onMounted(() => { /**
connectMutationObserver(); * The window interval reference to slide the carousel.
handleUpdate(); */
startAutoSlide(); let intervalRef: number | null = null;
});
onUnmounted(() => { /**
stopAutoSlide(); * The active mutation observer.
disconnectMutationObserver(); */
}); const mutationObserver: Ref<MutationObserver | null> = ref(null);
/**
* Handle the user moving the mouse over the carousel.
*/
const handleMouseOver = () => { const handleMouseOver = () => {
stopAutoSlide(); stopAutoSlide();
}; };
/**
* Handle the user moving the mouse leaving the carousel.
*/
const handleMouseLeave = () => { const handleMouseLeave = () => {
startAutoSlide(); startAutoSlide();
}; };
const handleSlidePrev = () => { /**
* Handle the user clicking the previous slider indicator.
*/
const handleClickSlidePrev = () => {
if (currentSlide.value == 0) { if (currentSlide.value == 0) {
currentSlide.value = maxSlide.value; currentSlide.value = maxSlide.value;
} else { } else {
@@ -64,7 +85,10 @@ const handleSlidePrev = () => {
updateSlidePositions(); updateSlidePositions();
}; };
const handleSlideNext = () => { /**
* Handle the user clicking the next slider indicator.
*/
const handleClickSlideNext = () => {
if (currentSlide.value == maxSlide.value) { if (currentSlide.value == maxSlide.value) {
currentSlide.value = 0; currentSlide.value = 0;
} else { } else {
@@ -74,34 +98,55 @@ const handleSlideNext = () => {
updateSlidePositions(); updateSlidePositions();
}; };
const handleIndicator = (index) => { /**
* Handle the user clicking a slider indicator.
*
* @param {number} index The slide to move to.
*/
const handleClickIndicator = (index: number) => {
currentSlide.value = index; currentSlide.value = index;
updateSlidePositions(); updateSlidePositions();
}; };
const handleUpdate = () => { /**
* Handle slides added/removed from the carousel and update the data/indicators.
*/
const handleCarouselUpdate = () => {
if (slides.value != null) {
slideElements.value = slides.value.querySelectorAll(".carousel-slide"); slideElements.value = slides.value.querySelectorAll(".carousel-slide");
maxSlide.value = slideElements.value.length - 1; maxSlide.value = slideElements.value.length - 1;
}
updateSlidePositions(); updateSlidePositions();
}; };
/**
* Update the style transform of each slide.
*/
const updateSlidePositions = () => { const updateSlidePositions = () => {
if (slideElements.value != null) {
slideElements.value.forEach((slide, index) => { slideElements.value.forEach((slide, index) => {
slide.style.transform = `translateX(${ (slide as HTMLElement).style.transform = `translateX(${
100 * (index - currentSlide.value) 100 * (index - currentSlide.value)
}%)`; }%)`;
}); });
}
}; };
/**
* Start the carousel slider.
*/
const startAutoSlide = () => { const startAutoSlide = () => {
if (intervalRef == null) { if (intervalRef == null) {
intervalRef = window.setInterval(() => { intervalRef = window.setInterval(() => {
handleSlideNext(); handleClickSlideNext();
}, 7000); }, 7000);
} }
}; };
/**
* Stop the carousel slider.
*/
const stopAutoSlide = () => { const stopAutoSlide = () => {
if (intervalRef != null) { if (intervalRef != null) {
window.clearInterval(intervalRef); window.clearInterval(intervalRef);
@@ -109,8 +154,12 @@ const stopAutoSlide = () => {
} }
}; };
/**
* Connect the mutation observer to the slider.
*/
const connectMutationObserver = () => { const connectMutationObserver = () => {
mutationObserver.value = new MutationObserver(handleUpdate); if (slides.value != null) {
mutationObserver.value = new MutationObserver(handleCarouselUpdate);
mutationObserver.value.observe(slides.value, { mutationObserver.value.observe(slides.value, {
attributes: false, attributes: false,
@@ -118,30 +167,47 @@ const connectMutationObserver = () => {
characterData: true, characterData: true,
subtree: true, subtree: true,
}); });
}
}; };
/**
* Disconnect the mutation observer from the slider.
*/
const disconnectMutationObserver = () => { const disconnectMutationObserver = () => {
if (mutationObserver.value) {
mutationObserver.value.disconnect(); mutationObserver.value.disconnect();
}
}; };
onMounted(() => {
connectMutationObserver();
handleCarouselUpdate();
startAutoSlide();
});
onUnmounted(() => {
stopAutoSlide();
disconnectMutationObserver();
});
</script> </script>
<style lang="scss"> <style lang="scss">
.carousel { .sm-carousel {
position: relative; position: relative;
height: 28rem; height: 28rem;
background: #eee; background: #eee;
overflow: hidden; overflow: hidden;
&:hover { &:hover {
.carousel-slide-prev, .sm-carousel-slide-prev,
.carousel-slide-next, .sm-carousel-slide-next,
.carousel-slide-indicators { .sm-carousel-slide-indicators {
opacity: 1; opacity: 1;
} }
} }
.carousel-slide-prev, .sm-carousel-slide-prev,
.carousel-slide-next { .sm-carousel-slide-next {
position: absolute; position: absolute;
top: 50%; top: 50%;
font-size: 300%; font-size: 300%;
@@ -153,7 +219,7 @@ const disconnectMutationObserver = () => {
transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out; transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out;
opacity: 0.75; opacity: 0.75;
svg { ion-icon {
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1)); filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
} }
@@ -163,17 +229,17 @@ const disconnectMutationObserver = () => {
} }
} }
.carousel-slide-prev { .sm-carousel-slide-prev {
left: 1rem; left: 1rem;
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1)); filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
} }
.carousel-slide-next { .sm-carousel-slide-next {
right: 1rem; right: 1rem;
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1)); filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
} }
.carousel-slide-indicators { .sm-carousel-slide-indicators {
position: absolute; position: absolute;
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -184,7 +250,7 @@ const disconnectMutationObserver = () => {
opacity: 0.75; opacity: 0.75;
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;
.carousel-slide-indicator-item { .sm-carousel-slide-indicator-item {
height: 12px; height: 12px;
width: 12px; width: 12px;
border: 1px solid white; border: 1px solid white;
@@ -203,9 +269,9 @@ const disconnectMutationObserver = () => {
} }
@media only screen and (max-width: 400px) { @media only screen and (max-width: 400px) {
.carousel { .sm-carousel {
.carousel-slide-prev, .sm-carousel-slide-prev,
.carousel-slide-next { .sm-carousel-slide-next {
font-size: 150%; font-size: 150%;
} }
} }

View File

@@ -1,17 +1,21 @@
<template> <template>
<div <div
class="carousel-slide" class="sm-carousel-slide"
:style="{ backgroundImage: `url('${imageUrl}')` }"> :style="{ backgroundImage: `url('${imageUrl}')` }">
<div v-if="imageUrl.length == 0" class="carousel-slide-loading"> <div v-if="imageUrl.length == 0" class="sm-carousel-slide-loading">
<SMLoadingIcon /> <SMLoadingIcon />
</div> </div>
<div v-else class="carousel-slide-body"> <div v-else class="sm-carousel-slide-body">
<div class="carousel-slide-content"> <div class="sm-carousel-slide-content">
<div class="carousel-slide-content-inner"> <div class="sm-carousel-slide-content-inner">
<h3>{{ title }}</h3> <h3>{{ title }}</h3>
<p v-if="content">{{ content }}</p> <p v-if="content">{{ content }}</p>
<div class="carousel-slide-body-buttons"> <div class="sm-carousel-slide-body-buttons">
<SMButton v-if="url" :to="url" :label="cta" /> <SMButton
v-if="url"
:to="url"
:label="cta"
type="secondary-outline" />
</div> </div>
</div> </div>
</div> </div>
@@ -22,7 +26,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { api } from "../helpers/api"; import { api } from "../helpers/api";
import { ApiMedia } from "../helpers/api.types"; import { MediaResponse } from "../helpers/api.types";
import { imageLoad } from "../helpers/image"; import { imageLoad } from "../helpers/image";
import SMButton from "./SMButton.vue"; import SMButton from "./SMButton.vue";
import SMLoadingIcon from "./SMLoadingIcon.vue"; import SMLoadingIcon from "./SMLoadingIcon.vue";
@@ -57,17 +61,24 @@ const props = defineProps({
let imageUrl = ref(""); let imageUrl = ref("");
/**
* Load the slider data.
*/
const handleLoad = () => { const handleLoad = () => {
imageUrl.value = ""; imageUrl.value = "";
api.get(`/media/${props.image}`).then((result) => { api.get({ url: "/media/{medium}", params: { medium: props.image } })
const data = result.data as ApiMedia; .then((result) => {
const data = result.data as MediaResponse;
if (data && data.medium) { if (data && data.medium) {
imageLoad(data.medium.url, (url) => { imageLoad(data.medium.url, (url) => {
imageUrl.value = url; imageUrl.value = url;
}); });
} }
})
.catch(() => {
/* empty */
}); });
}; };
@@ -75,7 +86,7 @@ handleLoad();
</script> </script>
<style lang="scss"> <style lang="scss">
.carousel-slide { .sm-carousel-slide {
position: absolute; position: absolute;
transition: all 0.5s; transition: all 0.5s;
width: 100%; width: 100%;
@@ -85,19 +96,14 @@ handleLoad();
background-size: cover; background-size: cover;
overflow: hidden; overflow: hidden;
.carousel-slide-loading { .sm-carousel-slide-loading {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100%; height: 100%;
svg {
color: rgba(0, 0, 0, 0.1);
font-size: 300%;
}
} }
.carousel-slide-body { .sm-carousel-slide-body {
display: flex; display: flex;
align-items: center; align-items: center;
height: 100%; height: 100%;
@@ -105,7 +111,7 @@ handleLoad();
margin: 0 auto; margin: 0 auto;
padding: 1rem; padding: 1rem;
.carousel-slide-content { .sm-carousel-slide-content {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -136,17 +142,15 @@ handleLoad();
text-align: left; text-align: left;
} }
.carousel-slide-body-buttons { .sm-carousel-slide-body-buttons {
margin-top: 2rem; margin-top: 2rem;
text-align: right; text-align: right;
max-width: 600px; max-width: 600px;
} }
.button { .secondary-outline {
display: inline-block;
box-shadow: 0 0 12px rgba(0, 0, 0, 0.5);
background: transparent;
border-color: #fff; border-color: #fff;
color: #fff;
&:hover { &:hover {
color: #333; color: #333;

View File

@@ -1,83 +1,27 @@
<template> <template>
<div :class="['container', { full: isFull }]" :style="styleObject"> <div :class="['sm-container', { full: full }]">
<SMLoader :loading="loading"> <slot v-if="slots.default"></slot>
<d-error-forbidden <div v-if="slots.inner" class="sm-container-inner">
v-if="pageError == 403 || !hasPermission()"></d-error-forbidden>
<d-error-internal
v-if="pageError >= 500 && hasPermission()"></d-error-internal>
<d-error-not-found v-if="pageError == 404 && hasPermission()"
>XX</d-error-not-found
>
<slot
v-if="
pageError < 300 && hasPermission() && slots.default
"></slot>
<div
v-if="pageError < 300 && hasPermission() && slots.inner"
class="container-inner">
<slot name="inner"></slot> <slot name="inner"></slot>
</div> </div>
</SMLoader>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import SMLoader from "./SMLoader.vue"; import { useSlots } from "vue";
import DErrorForbidden from "./errors/Forbidden.vue";
import DErrorInternal from "./errors/Internal.vue";
import DErrorNotFound from "./errors/NotFound.vue";
import { useUserStore } from "../store/UserStore";
import { computed, useSlots } from "vue";
const props = defineProps({ defineProps({
pageError: {
type: Number,
default: 200,
required: false,
},
permission: {
type: String,
default: "",
required: false,
},
loading: {
type: Boolean,
default: false,
required: false,
},
full: { full: {
type: Boolean, type: Boolean,
default: false, default: false,
required: false, required: false,
}, },
background: {
type: String,
default: "",
required: false,
},
}); });
const slots = useSlots(); const slots = useSlots();
const userStore = useUserStore();
let styleObject = {};
if (props.background != "") {
styleObject["backgroundImage"] = `url('${props.background}')`;
}
const hasPermission = () => {
return (
props.permission.length == 0 ||
userStore.permissions.includes(props.permission)
);
};
const isFull = computed(() => {
return props.pageError == 200 ? props.full : false;
});
</script> </script>
<style lang="scss"> <style lang="scss">
.container { .sm-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
@@ -95,7 +39,7 @@ const isFull = computed(() => {
padding-right: 0; padding-right: 0;
max-width: 100%; max-width: 100%;
.container-inner { .sm-container-inner {
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;
width: 100%; width: 100%;

View File

@@ -2,13 +2,13 @@
<div <div
:class="[ :class="[
'sm-dialog', 'sm-dialog',
{ 'dialog-narrow': narrow }, { 'sm-dialog-narrow': narrow },
{ 'dialog-full': full }, { 'sm-dialog-full': full },
{ 'dialog-noshadow': noShadow }, { 'sm-dialog-noshadow': noShadow },
]"> ]">
<transition name="fade"> <transition name="fade">
<div v-if="loading" class="dialog-loading-cover"> <div v-if="loading" class="sm-dialog-loading-cover">
<div class="dialog-loading"> <div class="sm-dialog-loading">
<SMLoadingIcon /> <SMLoadingIcon />
<span>{{ loadingMessage }}</span> <span>{{ loadingMessage }}</span>
</div> </div>
@@ -58,7 +58,7 @@ defineProps({
min-width: map-get($spacer, 5) * 12; min-width: map-get($spacer, 5) * 12;
box-shadow: 4px 4px 20px rgba(0, 0, 0, 0.5); box-shadow: 4px 4px 20px rgba(0, 0, 0, 0.5);
&.dialog-noshadow { &.sm-dialog-noshadow {
box-shadow: none !important; box-shadow: none !important;
} }
@@ -70,16 +70,16 @@ defineProps({
font-size: 90%; font-size: 90%;
} }
&.dialog-narrow { &.sm-dialog-narrow {
min-width: auto; min-width: auto;
max-width: map-get($spacer, 5) * 10; max-width: map-get($spacer, 5) * 10;
} }
&.dialog-full { &.sm-dialog-full {
width: 100%; width: 100%;
} }
.dialog-loading-cover { .sm-dialog-loading-cover {
position: fixed; position: fixed;
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -93,10 +93,11 @@ defineProps({
background-color: rgba(255, 255, 255, 0.5); background-color: rgba(255, 255, 255, 0.5);
z-index: 19000; z-index: 19000;
.dialog-loading { .sm-dialog-loading {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: map-get($spacer, 5) calc(map-get($spacer, 5) * 2); padding: map-get($spacer, 5) calc(map-get($spacer, 5) * 2);
align-items: center;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 24px; border-radius: 24px;
@@ -119,7 +120,7 @@ defineProps({
map-get($spacer, 4); map-get($spacer, 4);
min-width: auto; min-width: auto;
.button { .sm-button {
display: block; display: block;
width: 100%; width: 100%;
text-align: center; text-align: center;

View File

@@ -1,7 +1,6 @@
<template> <template>
<div class="sm-editor"> <div class="sm-editor">
<Editor <Editor
id="tinymce"
ref="tinyeditor" ref="tinyeditor"
v-model="editorContent" v-model="editorContent"
model-events="change blur focus" model-events="change blur focus"
@@ -15,49 +14,44 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import "tinymce/tinymce";
import Editor from "@tinymce/tinymce-vue"; import Editor from "@tinymce/tinymce-vue";
import "tinymce/themes/silver"; import "tinymce/themes/silver";
import "tinymce/tinymce";
import "tinymce/icons/default"; import "tinymce/icons/default";
import "tinymce/models/dom"; import "tinymce/models/dom";
import "tinymce/plugins/image";
import "tinymce/plugins/media";
import "tinymce/plugins/table";
import "tinymce/plugins/lists";
import "tinymce/plugins/advlist"; import "tinymce/plugins/advlist";
import "tinymce/plugins/link";
import "tinymce/plugins/autolink";
import "tinymce/plugins/lists";
import "tinymce/plugins/link";
import "tinymce/plugins/image";
import "tinymce/plugins/charmap";
import "tinymce/plugins/searchreplace";
import "tinymce/plugins/visualblocks";
import "tinymce/plugins/code";
import "tinymce/plugins/fullscreen";
import "tinymce/plugins/preview";
import "tinymce/plugins/anchor"; import "tinymce/plugins/anchor";
import "tinymce/plugins/insertdatetime"; import "tinymce/plugins/autolink";
import "tinymce/plugins/media";
import "tinymce/plugins/help";
import "tinymce/plugins/table";
import "tinymce/plugins/importcss";
import "tinymce/plugins/directionality";
import "tinymce/plugins/visualchars";
import "tinymce/plugins/template";
import "tinymce/plugins/codesample";
import "tinymce/plugins/pagebreak";
import "tinymce/plugins/nonbreaking";
import "tinymce/plugins/emoticons";
import "tinymce/plugins/autosave"; import "tinymce/plugins/autosave";
import "tinymce/plugins/charmap";
import "tinymce/plugins/code";
import "tinymce/plugins/codesample";
import "tinymce/plugins/directionality";
import "tinymce/plugins/emoticons";
import "tinymce/plugins/fullscreen";
import "tinymce/plugins/help";
import "tinymce/plugins/image";
import "tinymce/plugins/importcss";
import "tinymce/plugins/insertdatetime";
import "tinymce/plugins/link";
import "tinymce/plugins/lists";
import "tinymce/plugins/media";
import "tinymce/plugins/nonbreaking";
import "tinymce/plugins/pagebreak";
import "tinymce/plugins/preview";
import "tinymce/plugins/searchreplace";
import "tinymce/plugins/table";
import "tinymce/plugins/template";
import "tinymce/plugins/visualblocks";
import "tinymce/plugins/visualchars";
import "tinymce/plugins/wordcount"; import "tinymce/plugins/wordcount";
import { ref, watch, computed } from "vue"; import { computed, ref, watch } from "vue";
import { routes } from "../router";
import { api } from "../helpers/api"; import { api } from "../helpers/api";
import { MediaCollection, MediaResponse } from "../helpers/api.types"; import { MediaCollection, MediaResponse } from "../helpers/api.types";
import { routes } from "../router";
interface PageList { interface PageList {
title: string; title: string;
@@ -173,11 +167,11 @@ const initialContent = computed(() => {
watch(initialContent, handleInitialContentChange); watch(initialContent, handleInitialContentChange);
const handleBlur = (event, editor) => { const handleBlur = (event) => {
emits("blur", event); emits("blur", event);
}; };
const handleFocus = (event, editor) => { const handleFocus = (event) => {
emits("focus", event); emits("focus", event);
}; };

View File

@@ -1,11 +1,10 @@
<template> <template>
<a :href="computedHref" :target="props.target" rel="noopener" <a :href="computedUrl" :target="props.target" rel="noopener"
><slot></slot ><slot></slot
></a> ></a>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// import axios from 'axios'
import { computed } from "vue"; import { computed } from "vue";
import { useUserStore } from "../store/UserStore"; import { useUserStore } from "../store/UserStore";
@@ -16,43 +15,28 @@ const props = defineProps({
}, },
target: { target: {
type: String, type: String,
default: "", default: "_self",
}, },
}); });
const userStore = useUserStore(); const userStore = useUserStore();
const computedHref = computed(() => { /**
* Return the URL with a token param attached if the user is logged in and its a api media download request.
*/
const computedUrl = computed(() => {
const url = new URL(props.href); const url = new URL(props.href);
if (url.pathname.startsWith("/api/") && userStore.token) { const path = url.pathname;
return props.href + "?token=" + encodeURIComponent(userStore.token); const mediumRegex = /^\/media\/[a-zA-Z0-9]+\/download$/;
if (mediumRegex.test(path) && userStore.token) {
if (url.search) {
return `${props.href}&token=${encodeURIComponent(userStore.token)}`;
} else {
return `${props.href}?token=${encodeURIComponent(userStore.token)}`;
}
} }
return props.href; return props.href;
}); });
// const handleClick = async (event) => {
// const url = new URL(props.href)
// if(url.pathname.startsWith('/api/')) {
// console.log('api')
// event.preventDefault()
// axios.get(props.href, {responseType: 'blob'})
// .then(response => {
// const blob = new Blob([response.data], { type: response.data.type })
// const href = URL.createObjectURL(blob)
// const link = document.createElement('a')
// link.setAttribute('href', href)
// link.setAttribute('target', props.target)
// document.body.appendChild(link)
// link.click()
// document.body.removeChild(link)
// URL.revokeObjectURL(href)
// }).catch(e => {
// console.log(e)
// })
// }
// console.log('finish')
// }
</script> </script>

View File

@@ -24,6 +24,9 @@ const props = defineProps({
}); });
const emits = defineEmits(["submit"]); const emits = defineEmits(["submit"]);
/**
* Handle the user submitting the form.
*/
const handleSubmit = function () { const handleSubmit = function () {
if (props.modelValue.validate()) { if (props.modelValue.validate()) {
emits("submit"); emits("submit");

View File

@@ -1,9 +1,11 @@
<template> <template>
<component :is="parsedContent"></component> <component :is="computedContent"></component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import DOMPurify from "dompurify";
import { computed } from "vue"; import { computed } from "vue";
import "../../../import-meta";
const props = defineProps({ const props = defineProps({
html: { html: {
@@ -13,14 +15,19 @@ const props = defineProps({
}, },
}); });
const parsedContent = computed(() => { /**
* Return the html as a component, relative links as router-link and sanitized.
*/
const computedContent = computed(() => {
let html = ""; let html = "";
const regex = new RegExp( const regex = new RegExp(
`<a ([^>]*?)href="${import.meta.env.APP_URL}(.*?>.*?)</a>`, `<a ([^>]*?)href="${import.meta.env.APP_URL}(.*?>.*?)</a>`,
"ig" "ig"
); );
html = props.html.replace(regex, '<router-link $1to="$2</router-link>'); html = props.html.replace(regex, '<router-link $1to="$2</router-link>');
html = DOMPurify.sanitize(html);
return { return {
template: `<div class="content">${html}</div>`, template: `<div class="content">${html}</div>`,

View File

@@ -1,15 +1,12 @@
<template> <template>
<div class="heading"> <div class="sm-heading">
<router-link <router-link v-if="back != ''" :to="{ name: back }" class="sm-back">
v-if="back != ''"
:to="{ name: back }"
class="heading-back">
<ion-icon name="arrow-back-outline" />{{ backLabel }} <ion-icon name="arrow-back-outline" />{{ backLabel }}
</router-link> </router-link>
<router-link v-if="close != ''" :to="{ name: close }" class="close"> <router-link v-if="close != ''" :to="{ name: close }" class="sm-close">
<ion-icon name="close-outline" /> <ion-icon name="close-outline" />
</router-link> </router-link>
<span v-if="closeBack" class="close" @click="handleBack"> <span v-if="closeBack" class="sm-close" @click="handleBack">
<ion-icon name="close-outline" /> <ion-icon name="close-outline" />
</span> </span>
<h1>{{ heading }}</h1> <h1>{{ heading }}</h1>
@@ -50,20 +47,16 @@ const handleBack = () => {
</script> </script>
<style lang="scss"> <style lang="scss">
.heading { .sm-heading {
position: relative; position: relative;
.heading-back { .sm-back {
position: absolute; position: absolute;
padding-top: 2rem; padding-top: 2rem;
font-size: 80%; font-size: 80%;
svg {
margin-right: 0.5rem;
}
} }
.close { .sm-close {
right: -10px; right: -10px;
top: -10px; top: -10px;
position: absolute; position: absolute;
@@ -76,10 +69,8 @@ const handleBack = () => {
} }
} }
// @media screen and (max-width: 768px) {
@media only screen and (max-width: 640px) { @media only screen and (max-width: 640px) {
.heading .close { .sm-heading .sm-close {
right: 0; right: 0;
top: -20px; top: -20px;
} }

View File

@@ -43,7 +43,7 @@
class="file" class="file"
:accept="props.accept" :accept="props.accept"
@change="handleChange" /> @change="handleChange" />
<label class="button" for="file">Select file</label> <label class="sm-button" for="file">Select file</label>
<div class="file-name"> <div class="file-name">
{{ modelValue?.name ? modelValue.name : modelValue }} {{ modelValue?.name ? modelValue.name : modelValue }}
</div> </div>
@@ -68,7 +68,11 @@
<img v-if="mediaUrl.length > 0" :src="mediaUrl" /> <img v-if="mediaUrl.length > 0" :src="mediaUrl" />
<ion-icon v-else name="image-outline" /> <ion-icon v-else name="image-outline" />
</div> </div>
<a class="button" @click.prevent="handleMediaSelect">Select file</a> <a
class="sm-button sm-button-small"
@click.prevent="handleMediaSelect"
>Select file</a
>
</div> </div>
<div v-if="slots.default || feedbackInvalid" class="sm-input-help"> <div v-if="slots.default || feedbackInvalid" class="sm-input-help">
<span v-if="feedbackInvalid" class="sm-input-invalid">{{ <span v-if="feedbackInvalid" class="sm-input-invalid">{{
@@ -82,7 +86,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { watch, computed, useSlots, ref, inject } from "vue"; import { computed, inject, ref, useSlots, watch } from "vue";
import { openDialog } from "vue3-promise-dialog"; import { openDialog } from "vue3-promise-dialog";
import { toTitleCase } from "../helpers/string"; import { toTitleCase } from "../helpers/string";
import { isEmpty } from "../helpers/utils"; import { isEmpty } from "../helpers/utils";
@@ -141,7 +145,7 @@ const objControl =
: !isEmpty(objForm) && : !isEmpty(objForm) &&
typeof props.control == "string" && typeof props.control == "string" &&
props.control != "" props.control != ""
? objForm[props.control] ? objForm.controls[props.control]
: null; : null;
const label = ref(props.label); const label = ref(props.label);

View File

@@ -22,6 +22,7 @@
border-radius: 50%; border-radius: 50%;
background: #000; background: #000;
animation-timing-function: cubic-bezier(0, 1, 1, 0); animation-timing-function: cubic-bezier(0, 1, 1, 0);
box-shadow: 0 0 1px rgba(0, 0, 0, 1);
} }
div:nth-child(1) { div:nth-child(1) {
left: 8px; left: 8px;

View File

@@ -1,360 +0,0 @@
<template>
<div
:class="[
'sm-input-group',
{ 'sm-input-active': inputActive, 'sm-has-error': error },
]">
<label v-if="label" :class="{ required: required, inline: inline }">{{
label
}}</label>
<ion-icon class="sm-error-icon" name="alert-circle-outline"></ion-icon>
<input
v-if="
type == 'text' ||
type == 'email' ||
type == 'password' ||
type == 'email' ||
type == 'url'
"
:type="type"
:value="modelValue"
:placeholder="placeholder"
@input="input"
@focus="handleFocus"
@blur="handleBlur"
@keydown="handleKeydown" />
<textarea
v-if="type == 'textarea'"
rows="5"
:value="modelValue"
:placeholder="placeholder"
@input="input"
@blur="handleBlur"
@keydown="handleBlur"></textarea>
<div v-if="type == 'file'" class="input-file-group">
<input
id="file"
type="file"
class="file"
:accept="props.accept"
@change="handleChange" />
<label class="button" for="file">Select file</label>
<div class="file-name">
{{ modelValue?.name ? modelValue.name : modelValue }}
</div>
</div>
<a v-if="type == 'link'" :href="href" target="_blank">{{
props.modelValue
}}</a>
<span v-if="type == 'static'">{{ props.modelValue }}</span>
<div v-if="type == 'media'" class="input-media-group">
<div class="input-media-display">
<img v-if="mediaUrl.length > 0" :src="mediaUrl" />
<ion-icon v-else name="image-outline" />
</div>
<div v-if="type == 'media'" class="form-group-error">
{{ error }}
</div>
<a class="button" @click.prevent="handleMediaSelect">Select file</a>
</div>
<div v-if="slots.default || error" class="sm-input-help">
<span v-if="type != 'media'" class="sm-input-error">{{
error
}}</span>
<span v-if="slots.default" class="sm-input-info">
<slot></slot>
</span>
</div>
<div v-if="help" class="form-group-help">
<ion-icon v-if="helpIcon" name="information-circle-outline" />
{{ help }}
</div>
</div>
</template>
<script setup lang="ts">
import { computed, useSlots, ref, watch } from "vue";
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
import { openDialog } from "vue3-promise-dialog";
const props = defineProps({
modelValue: {
type: String,
default: "",
},
label: {
type: String,
default: "",
required: false,
},
placeholder: {
type: String,
default: "",
required: false,
},
required: {
type: Boolean,
default: false,
},
type: {
type: String,
default: "text",
},
error: {
type: String,
default: "",
},
help: {
type: String,
default: "",
},
helpIcon: {
type: String,
default: "",
},
accept: {
type: String,
default: "",
},
href: {
type: String,
default: "",
},
});
const emits = defineEmits(["update:modelValue", "blur"]);
const slots = useSlots();
const mediaUrl = ref("");
let inputActive = ref(false);
const handleChange = (event) => {
emits("update:modelValue", event.target.files[0]);
emits("blur", event);
};
const input = (event) => {
emits("update:modelValue", event.target.value);
};
const handleBlur = (event) => {
if (props.modelValue.length == 0) {
inputActive.value = false;
}
if (event.keyCode == undefined || event.keyCode == 9) {
emits("blur", event);
}
};
const handleFocus = (event) => {
inputActive.value = true;
if (event.keyCode == undefined || event.keyCode == 9) {
emits("blur", event);
}
};
const handleKeydown = (event) => {};
const handleMediaSelect = async (event) => {
let result = await openDialog(SMDialogMedia);
if (result) {
mediaUrl.value = result.url;
emits("update:modelValue", result.id);
}
};
const inline = computed(() => {
return ["static", "link"].includes(props.type);
});
const handleLoad = async () => {
if (props.type == "media" && props.modelValue.length > 0) {
try {
let result = await api.get(`/media/${props.modelValue}`);
mediaUrl.value = result.json.medium.url;
} catch (error) {
/* empty */
}
}
};
watch(
() => props.modelValue,
() => {
handleLoad();
}
);
</script>
<style lang="scss">
.sm-input-group {
position: relative;
display: flex;
flex-direction: column;
margin-bottom: map-get($spacer, 4);
&.sm-input-active {
label {
transform: translate(8px, -3px) scale(0.7);
color: $secondary-color-dark;
}
input {
padding: calc(#{map-get($spacer, 2)} * 1.5) map-get($spacer, 3)
calc(#{map-get($spacer, 2)} / 2) map-get($spacer, 3);
}
}
&.sm-has-error {
input,
select,
textarea {
border: 2px solid $danger-color;
}
.sm-error-icon {
display: block;
}
}
label {
position: absolute;
display: block;
padding: map-get($spacer, 2) map-get($spacer, 3);
line-height: 1.5;
transform-origin: top left;
transform: translate(0, 1px) scale(1);
transition: all 0.1s ease-in-out;
color: $font-color;
}
.sm-error-icon {
position: absolute;
display: none;
right: 0;
top: 2px;
padding: map-get($spacer, 2) map-get($spacer, 3);
color: $danger-color;
font-size: 120%;
}
input,
select,
textarea {
box-sizing: border-box;
display: block;
width: 100%;
border: 1px solid $border-color;
border-radius: 12px;
padding: map-get($spacer, 2) map-get($spacer, 3);
color: $font-color;
margin-bottom: map-get($spacer, 1);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
textarea {
resize: none;
}
.sm-input-help {
font-size: 75%;
margin: 0 map-get($spacer, 1);
.sm-input-error {
color: $danger-color;
padding-right: map-get($spacer, 1);
}
}
}
// .form-group {
// margin-bottom: map-get($spacer, 3);
// padding: 0 4px;
// flex: 1;
// input,
// textarea {
// margin-bottom: map-get($spacer, 1);
// }
// label {
// position: absolute;
// }
// .form-group-info {
// font-size: 85%;
// margin-bottom: map-get($spacer, 1);
// }
// .form-group-error {
// // display: none;
// font-size: 85%;
// margin-bottom: map-get($spacer, 1);
// color: $danger-color;
// }
// .form-group-help {
// font-size: 85%;
// margin-bottom: map-get($spacer, 1);
// color: $secondary-color;
// svg {
// vertical-align: middle !important;
// }
// }
// &.has-error {
// input,
// textarea,
// .input-file-group,
// .input-media-group .input-media-display {
// border: 2px solid $danger-color;
// }
// .form-group-error {
// display: block;
// }
// }
// }
// .input-media-group {
// display: flex;
// margin: 0 auto;
// max-width: 26rem;
// flex-direction: column;
// align-items: center;
// .input-media-display {
// display: flex;
// margin-bottom: 1rem;
// border: 1px solid $border-color;
// background-color: #fff;
// img {
// max-width: 100%;
// max-height: 100%;
// }
// svg {
// padding: 4rem;
// }
// }
// .button {
// max-width: 13rem;
// }
// }
// .input-media-group + .form-group-error {
// text-align: center;
// }
// @media screen and (max-width: 768px) {
// .input-media-group {
// max-width: 13rem;
// }
// }
</style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="sm-message message-outer"> <div class="sm-message-container">
<div :class="['message', type]"> <div :class="['sm-message', type]">
<ion-icon v-if="icon" :name="icon"></ion-icon> <ion-icon v-if="icon" :name="icon"></ion-icon>
<p>{{ message }}</p> <p>{{ message }}</p>
</div> </div>
@@ -25,12 +25,11 @@ defineProps({
</script> </script>
<style lang="scss"> <style lang="scss">
.sm-message { .sm-message-container {
&.message-outer {
justify-content: center; justify-content: center;
align-self: center; align-self: center;
.message { .sm-message {
display: inline-flex; display: inline-flex;
padding: map-get($spacer, 2) map-get($spacer, 3); padding: map-get($spacer, 2) map-get($spacer, 3);
margin-bottom: map-get($spacer, 4); margin-bottom: map-get($spacer, 4);
@@ -73,5 +72,4 @@ defineProps({
} }
} }
} }
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="modal"> <div class="sm-modal">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>

View File

@@ -1,12 +1,14 @@
<template> <template>
<SMContainer <SMContainer
:full="true" :full="true"
:class="['sm-navbar', { showDropdown: showToggle }]" :class="['sm-navbar', { 'sm-show-dropdown': showToggle }]"
@click="handleHideMenu"> @click="handleClickHideMenu">
<template #inner> <template #inner>
<div class="navbar-container"> <div class="sm-navbar-container">
<router-link :to="{ name: 'home' }" class="brand"></router-link> <router-link
<ul class="navmenu flex-fill"> :to="{ name: 'home' }"
class="sm-brand"></router-link>
<ul class="sm-navmenu flex-fill">
<template v-for="item in menuItems"> <template v-for="item in menuItems">
<li <li
v-if=" v-if="
@@ -22,21 +24,23 @@
</ul> </ul>
<SMButton <SMButton
:to="{ name: 'workshop-list' }" :to="{ name: 'workshop-list' }"
class="navbar-cta" class="sm-navbar-cta"
label="Find a workshop" label="Find a workshop"
icon="arrow-forward-outline" /> icon="arrow-forward-outline" />
<div class="menuButton" @click.stop="handleToggleMenu"> <div
class="sm-navbar-toggle-menu"
@click.stop="handleClickToggleMenu">
<span>Menu</span <span>Menu</span
><ion-icon ><ion-icon name="reorder-three-outline"></ion-icon>
class="menuButtonIcon"
name="reorder-three-outline"></ion-icon>
</div> </div>
</div> </div>
</template> </template>
<div class="navbar-dropdown-cover"></div> <div class="sm-navbar-dropdown-cover"></div>
<ul class="navbar-dropdown"> <ul class="sm-navbar-dropdown">
<li class="ml-auto"> <li class="ml-auto">
<div class="menuClose" @click.stop="handleToggleMenu"> <div
class="sm-navbar-close-menu"
@click.stop="handleClickToggleMenu">
<ion-icon name="close-outline"></ion-icon> <ion-icon name="close-outline"></ion-icon>
</div> </div>
</li> </li>
@@ -121,11 +125,17 @@ const menuItems = [
}, },
]; ];
const handleToggleMenu = () => { /**
* Hanfle the user clicking an element to toggle the dropdown menu.
*/
const handleClickToggleMenu = () => {
showToggle.value = !showToggle.value; showToggle.value = !showToggle.value;
}; };
const handleHideMenu = () => { /**
* Handle the user clicking an element to hide the dropdown menu.
*/
const handleClickHideMenu = () => {
if (showToggle.value) { if (showToggle.value) {
showToggle.value = false; showToggle.value = false;
} }
@@ -143,20 +153,20 @@ const handleHideMenu = () => {
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
z-index: 1000; z-index: 1000;
&.showDropdown { &.sm-show-dropdown {
.navbar-dropdown-cover { .sm-navbar-dropdown-cover {
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
transition: visibility 0.3s linear, opacity 0.3s linear; transition: visibility 0.3s linear, opacity 0.3s linear;
} }
.navbar-dropdown { .sm-navbar-dropdown {
margin-top: 0; margin-top: 0;
transition: margin 0.5s ease-in-out; transition: margin 0.5s ease-in-out;
} }
} }
.navbar-dropdown-cover { .sm-navbar-dropdown-cover {
position: fixed; position: fixed;
visibility: hidden; visibility: hidden;
z-index: 2000; z-index: 2000;
@@ -169,7 +179,7 @@ const handleHideMenu = () => {
overflow: hidden; overflow: hidden;
} }
.navbar-dropdown { .sm-navbar-dropdown {
position: fixed; position: fixed;
z-index: 2001; z-index: 2001;
top: 0; top: 0;
@@ -203,15 +213,12 @@ const handleHideMenu = () => {
} }
} }
.navmenu, .sm-navmenu,
.navbar-dropdown { .sm-navbar-dropdown {
padding-top: map-get($spacer, 4); padding-top: map-get($spacer, 4);
li { li {
// display: flex;
// width: 100%;
margin: 0 0.75rem; margin: 0 0.75rem;
// justify-content: center;
a { a {
color: rgba(0, 0, 0, 0.8); color: rgba(0, 0, 0, 0.8);
@@ -225,32 +232,33 @@ const handleHideMenu = () => {
} }
} }
.menuClose ion-icon { .sm-navbar-close-menu ion-icon {
cursor: pointer; cursor: pointer;
font-size: map-get($spacer, 4); font-size: map-get($spacer, 4);
padding-left: map-get($spacer, 1); padding-left: map-get($spacer, 1);
&:hover {
color: $danger-color;
}
} }
} }
.navbar-container { .sm-navbar-container {
display: flex; display: flex;
flex: 1; flex: 1;
align-items: center; align-items: center;
.brand { .sm-brand {
display: inline-block; display: inline-block;
background-image: url("/img/logo.png"); background-image: url("/img/logo.png");
background-position: left top; background-position: left top;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: contain;
// width: 16.5rem;
// height: 3rem;
width: 13.5rem; width: 13.5rem;
height: 2rem; height: 2rem;
// margin-bottom: 1rem;
} }
.navmenu { .sm-navmenu {
flex: 1; flex: 1;
display: flex; display: flex;
justify-content: end; justify-content: end;
@@ -258,9 +266,8 @@ const handleHideMenu = () => {
padding: 0 1rem; padding: 0 1rem;
} }
.menuButton { .sm-navbar-toggle-menu {
cursor: pointer; cursor: pointer;
// display: none;
align-items: center; align-items: center;
font-size: 0.9rem; font-size: 0.9rem;
margin-left: 2rem; margin-left: 2rem;
@@ -270,14 +277,14 @@ const handleHideMenu = () => {
display: none; display: none;
} }
.menuButtonIcon { ion-icon {
margin-left: 0.5rem; margin-left: 0.5rem;
font-size: map-get($spacer, 4); font-size: map-get($spacer, 4);
} }
} }
} }
.navbar-cta { .sm-navbar-cta {
font-size: 0.9rem; font-size: 0.9rem;
padding: 0.6rem 1.1rem; padding: 0.6rem 1.1rem;
} }
@@ -285,16 +292,12 @@ const handleHideMenu = () => {
@media only screen and (max-width: 1200px) { @media only screen and (max-width: 1200px) {
.sm-navbar .navbar-container { .sm-navbar .navbar-container {
.navmenu li { .sm-navmenu li {
display: none; display: none;
} }
.menuButton { .sm-navbar-toggle-menu {
display: flex; display: flex;
span {
// display: block;
}
} }
} }
} }
@@ -308,13 +311,13 @@ const handleHideMenu = () => {
} }
.navbar-container { .navbar-container {
.brand { .sm-brand {
width: 13.5rem; width: 13.5rem;
height: 2rem; height: 2rem;
margin-bottom: 0; margin-bottom: 0;
} }
.navbar-cta { .sm-navbar-cta {
font-size: 0.9rem; font-size: 0.9rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
} }
@@ -326,18 +329,18 @@ const handleHideMenu = () => {
.sm-navbar { .sm-navbar {
height: 4.5rem; height: 4.5rem;
.navbar-dropdown-cover { .sm-navbar-dropdown-cover {
margin-top: 4.5rem; margin-top: 4.5rem;
} }
.navbar-container { .sm-navbar-container {
.brand { .sm-brand {
background-image: url("/img/logo-small.png"); background-image: url("/img/logo-small.png");
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
} }
.navbar-cta { .sm-navbar-cta {
font-size: 0.9rem; font-size: 0.9rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
@@ -346,7 +349,7 @@ const handleHideMenu = () => {
} }
} }
.menuButton { .sm-menuButton {
margin-left: 1rem; margin-left: 1rem;
span { span {

View File

@@ -21,14 +21,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from "../store/UserStore";
import { useSlots } from "vue"; import { useSlots } from "vue";
import SMLoader from "./SMLoader.vue"; import SMBreadcrumbs from "../components/SMBreadcrumbs.vue";
import { useUserStore } from "../store/UserStore";
import SMErrorForbidden from "./errors/Forbidden.vue"; import SMErrorForbidden from "./errors/Forbidden.vue";
import SMErrorInternal from "./errors/Internal.vue"; import SMErrorInternal from "./errors/Internal.vue";
import SMErrorNotFound from "./errors/NotFound.vue"; import SMErrorNotFound from "./errors/NotFound.vue";
import SMBreadcrumbs from "../components/SMBreadcrumbs.vue";
import SMContainer from "./SMContainer.vue"; import SMContainer from "./SMContainer.vue";
import SMLoader from "./SMLoader.vue";
const props = defineProps({ const props = defineProps({
pageError: { pageError: {
@@ -66,7 +66,12 @@ if (props.background != "") {
styleObject["backgroundImage"] = `url('${props.background}')`; styleObject["backgroundImage"] = `url('${props.background}')`;
} }
const hasPermission = () => { /**
* Return if the current user has the props.permission to view this page.
*
* @returns {boolean} If the user has the permission.
*/
const hasPermission = (): boolean => {
return ( return (
props.permission.length == 0 || props.permission.length == 0 ||
userStore.permissions.includes(props.permission) userStore.permissions.includes(props.permission)
@@ -89,7 +94,6 @@ const hasPermission = () => {
margin-bottom: 0; margin-bottom: 0;
.sm-page { .sm-page {
// padding-top: calc(map-get($spacer, 5) * 2);
padding-bottom: calc(map-get($spacer, 5) * 2); padding-bottom: calc(map-get($spacer, 5) * 2);
} }
} }

View File

@@ -1,25 +0,0 @@
<template v-if="error >= 300 || slots.default">
<d-error-forbidden v-if="error == 403"></d-error-forbidden>
<d-error-internal v-if="error >= 500"></d-error-internal>
<d-error-not-found v-if="error == 404"></d-error-not-found>
<template v-if="slots.default && error < 300">
<slot></slot>
</template>
</template>
<script setup lang="ts">
import { useSlots } from "vue";
import DErrorForbidden from "./errors/Forbidden.vue";
import DErrorInternal from "./errors/Internal.vue";
import DErrorNotFound from "./errors/NotFound.vue";
defineProps({
error: {
type: Number,
required: true,
default: 200,
},
});
const slots = useSlots();
</script>

View File

@@ -1,34 +1,36 @@
<template> <template>
<router-link :to="to" class="panel"> <router-link :to="to" class="sm-panel">
<div class="panel-image" :style="styleObject"> <div v-if="image" class="sm-panel-image" :style="styleObject">
<div v-if="dateInImage && date" class="panel-image-date"> <div v-if="dateInImage && date" class="sm-panel-image-date">
<div class="panel-image-date-day"> <div class="sm-panel-image-date-day">
{{ new SMDate(date, { format: "yMd" }).format("dd") }} {{ new SMDate(date, { format: "yMd" }).format("dd") }}
</div> </div>
<div class="panel-image-date-month"> <div class="sm-panel-image-date-month">
{{ new SMDate(date, { format: "yMd" }).format("MMM") }} {{ new SMDate(date, { format: "yMd" }).format("MMM") }}
</div> </div>
</div> </div>
<ion-icon <ion-icon
v-if="hideImageLoader == false" v-if="imageUrl.length == 0"
class="panel-image-loader" class="sm-panel-image-loader"
name="image-outline" /> name="image-outline" />
</div> </div>
<div class="panel-body"> <div class="sm-panel-body">
<h3 class="panel-title">{{ title }}</h3> <h3 class="sm-panel-title">{{ title }}</h3>
<div v-if="showDate && date" class="panel-date"> <div v-if="showDate && date" class="sm-panel-date">
<ion-icon <ion-icon
v-if="showTime == false && endDate.length == 0" v-if="showTime == false && endDate.length == 0"
name="calendar-outline" /> name="calendar-outline" />
<ion-icon v-else name="time-outline" /> <ion-icon v-else name="time-outline" />
<p>{{ panelDate }}</p> <p>{{ computedDate }}</p>
</div> </div>
<div v-if="location" class="panel-location"> <div v-if="location" class="sm-panel-location">
<ion-icon name="location-outline" /> <ion-icon name="location-outline" />
<p>{{ location }}</p> <p>{{ location }}</p>
</div> </div>
<div v-if="content" class="panel-content">{{ panelContent }}</div> <div v-if="content" class="sm-panel-content">
<div v-if="button.length > 0" class="panel-button"> {{ computedContent }}
</div>
<div v-if="button.length > 0" class="sm-panel-button">
<SMButton :to="to" :type="buttonType" :label="button" /> <SMButton :to="to" :type="buttonType" :label="button" />
</div> </div>
</div> </div>
@@ -36,13 +38,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, computed, ref, reactive, watch } from "vue"; import { computed, onMounted, reactive, ref, watch } from "vue";
import { isUUID } from "../helpers/uuid";
import { excerpt, replaceHtmlEntites, stripHtmlTags } from "../helpers/string";
import { api } from "../helpers/api"; import { api } from "../helpers/api";
import { imageLoad } from "../helpers/image"; import { MediaResponse } from "../helpers/api.types";
import { ApiMedia } from "../helpers/api.types";
import { SMDate } from "../helpers/datetime"; import { SMDate } from "../helpers/datetime";
import { imageLoad } from "../helpers/image";
import { excerpt, replaceHtmlEntites, stripHtmlTags } from "../helpers/string";
import { isUUID } from "../helpers/uuid";
import SMButton from "./SMButton.vue"; import SMButton from "./SMButton.vue";
const props = defineProps({ const props = defineProps({
@@ -118,7 +120,10 @@ const props = defineProps({
let styleObject = reactive({}); let styleObject = reactive({});
let imageUrl = ref(""); let imageUrl = ref("");
const panelDate = computed(() => { /**
* Return a human readable date based on props.date and props.endDate.
*/
const computedDate = computed(() => {
let str = ""; let str = "";
if (props.date.length > 0) { if (props.date.length > 0) {
@@ -149,28 +154,27 @@ const panelDate = computed(() => {
return str; return str;
}); });
const panelContent = computed(() => { /**
* Return the content string cleaned from HTML.
*/
const computedContent = computed(() => {
return excerpt(replaceHtmlEntites(stripHtmlTags(props.content)), 200); return excerpt(replaceHtmlEntites(stripHtmlTags(props.content)), 200);
}); });
const hideImageLoader = computed(() => {
return (
imageUrl.value &&
imageUrl.value.length > 0 &&
isUUID(imageUrl.value) == false
);
});
onMounted(async () => { onMounted(async () => {
if (props.image && props.image.length > 0 && isUUID(props.image)) { if (props.image && props.image.length > 0 && isUUID(props.image)) {
api.get(`/media/${props.image}`).then((result) => { api.get({ url: "/media/{medium}", params: { medium: props.image } })
const data = result.data as ApiMedia; .then((result) => {
const data = result.data as MediaResponse;
if (data && data.medium) { if (data && data.medium) {
imageLoad(data.medium.url, (url) => { imageLoad(data.medium.url, (url) => {
imageUrl.value = url; imageUrl.value = url;
}); });
} }
})
.catch(() => {
/* empty */
}); });
} }
}); });
@@ -184,7 +188,7 @@ watch(
</script> </script>
<style lang="scss"> <style lang="scss">
.panel { .sm-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid $border-color; border: 1px solid $border-color;
@@ -203,7 +207,7 @@ watch(
box-shadow: 0 0 14px rgba(0, 0, 0, 0.25); box-shadow: 0 0 14px rgba(0, 0, 0, 0.25);
} }
.panel-image { .sm-panel-image {
position: relative; position: relative;
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -216,12 +220,12 @@ watch(
border-top-right-radius: 12px; border-top-right-radius: 12px;
background-color: #eee; background-color: #eee;
.panel-image-loader { .sm-panel-image-loader {
font-size: 5rem; font-size: 5rem;
color: $secondary-color; color: $secondary-color;
} }
.panel-image-date { .sm-panel-image-date {
background-color: #fff; background-color: #fff;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
text-align: center; text-align: center;
@@ -232,19 +236,19 @@ watch(
box-shadow: 4px 4px 15px rgba(0, 0, 0, 0.2); box-shadow: 4px 4px 15px rgba(0, 0, 0, 0.2);
text-align: center; text-align: center;
.panel-image-date-day { .sm-panel-image-date-day {
font-weight: bold; font-weight: bold;
font-size: 130%; font-size: 130%;
} }
.panel-image-date-month { .sm-panel-image-date-month {
text-transform: uppercase; text-transform: uppercase;
font-size: 80%; font-size: 80%;
} }
} }
} }
.panel-body { .sm-panel-body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
@@ -252,12 +256,12 @@ watch(
background-color: #fff; background-color: #fff;
} }
.panel-title { .sm-panel-title {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.panel-date, .sm-panel-date,
.panel-location { .sm-panel-location {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: top; align-items: top;
@@ -279,17 +283,10 @@ watch(
} }
} }
.panel-content { .sm-panel-content {
margin-top: 1rem; margin-top: 1rem;
line-height: 130%; line-height: 130%;
flex: 1; flex: 1;
} }
.panel-button {
.button {
display: block;
margin-top: 1.5rem;
}
}
} }
</style> </style>

View File

@@ -1,9 +1,9 @@
<template> <template>
<div class="panel-list"> <div class="sm-panel-list">
<div v-if="loading" class="panel-list-loading"> <div v-if="loading" class="sm-panel-list-loading">
<SMLoadingIcon /> <SMLoadingIcon />
</div> </div>
<div v-else-if="notFound" class="panel-list-not-found"> <div v-else-if="notFound" class="sm-panel-list-not-found">
<ion-icon name="alert-circle-outline" /> <ion-icon name="alert-circle-outline" />
<p>{{ notFoundText }}</p> <p>{{ notFoundText }}</p>
</div> </div>
@@ -34,7 +34,7 @@ defineProps({
</script> </script>
<style lang="scss"> <style lang="scss">
.panel-list { .sm-panel-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
@@ -43,17 +43,13 @@ defineProps({
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
.panel-list-loading { .sm-panel-list-loading {
display: flex; display: flex;
flex: 1; flex: 1;
justify-content: center; justify-content: center;
svg {
font-size: 500%;
}
} }
.panel-list-not-found { .sm-panel-list-not-found {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;

View File

@@ -1,5 +1,5 @@
<template> <template>
<div v-show="label == selectedLabel" class="tab-content"> <div v-show="label == selectedLabel" class="sm-tab-content">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
@@ -7,7 +7,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject } from "vue"; import { inject } from "vue";
const props = defineProps({ defineProps({
label: { label: {
type: String, type: String,
required: true, required: true,
@@ -18,7 +18,7 @@ const selectedLabel = inject("selectedLabel");
</script> </script>
<style lang="scss"> <style lang="scss">
.tab-content { .sm-tab-content {
padding: map-get($spacer, 3); padding: map-get($spacer, 3);
background-color: #fff; background-color: #fff;
border: 1px solid $border-color; border: 1px solid $border-color;

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="tab-group"> <div class="sm-tab-group">
<ul class="tab-header"> <ul class="sm-tab-header">
<li <li
v-for="label in tabLabels" v-for="label in tabLabels"
:key="label" :key="label"
:class="['tab-item', { selected: selectedLabel == label }]" :class="['sm-tab-item', { selected: selectedLabel == label }]"
@click="selectedLabel = label"> @click="selectedLabel = label">
{{ label }} {{ label }}
</li> </li>
@@ -14,7 +14,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, useSlots, provide } from "vue"; import { provide, ref, useSlots } from "vue";
const slots = useSlots(); const slots = useSlots();
const tabLabels = ref(slots.default().map((tab) => tab.props.label)); const tabLabels = ref(slots.default().map((tab) => tab.props.label));
@@ -24,17 +24,17 @@ provide("selectedLabel", selectedLabel);
</script> </script>
<style lang="scss"> <style lang="scss">
.tab-group { .sm-tab-group {
margin-bottom: map-get($spacer, 4); margin-bottom: map-get($spacer, 4);
.tab-header { .sm-tab-header {
// border-bottom: 1px solid $border-color; // border-bottom: 1px solid $border-color;
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.tab-item { .sm-tab-item {
display: inline-block; display: inline-block;
padding: map-get($spacer, 2) map-get($spacer, 3); padding: map-get($spacer, 2) map-get($spacer, 3);
border: 1px solid transparent; border: 1px solid transparent;

View File

@@ -1,26 +1,26 @@
<template> <template>
<div class="toolbar"> <div class="sm-toolbar">
<div class="toolbar-column toolbar-column-left"> <div class="sm-toolbar-column sm-toolbar-column-left">
<slot name="left"></slot> <slot name="left"></slot>
</div> </div>
<div class="toolbar-column toolbar-column-right"> <div class="sm-toolbar-column sm-toolbar-column-right">
<slot name="right"></slot> <slot name="right"></slot>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss">
.toolbar { .sm-toolbar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: map-get($spacer, 2); margin-bottom: map-get($spacer, 2);
.toolbar-column { .sm-toolbar-column {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
&.toolbar-column-left { &.sm-toolbar-column-left {
justify-content: flex-start; justify-content: flex-start;
} }
@@ -43,7 +43,7 @@
// } // }
// } // }
&.toolbar-column-right { &.sm-toolbar-column-right {
justify-content: flex-end; justify-content: flex-end;
} }
// } // }

View File

@@ -2,19 +2,19 @@
<SMModal> <SMModal>
<SMDialog> <SMDialog>
<h1>{{ props.title }}</h1> <h1>{{ props.title }}</h1>
<p v-html="sanitizedHtml"></p> <p v-html="computedSanitizedText"></p>
<SMFormFooter> <SMFormFooter>
<template #left> <template #left>
<SMButton <SMButton
:type="props.cancel.type" :type="props.cancel.type"
:label="props.cancel.label" :label="props.cancel.label"
@click="handleCancel()" /> @click="handleClickCancel()" />
</template> </template>
<template #right> <template #right>
<SMButton <SMButton
:type="props.confirm.type" :type="props.confirm.type"
:label="props.confirm.label" :label="props.confirm.label"
@click="handleConfirm()" /> @click="handleClickConfirm()" />
</template> </template>
</SMFormFooter> </SMFormFooter>
</SMDialog> </SMDialog>
@@ -22,13 +22,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted } from "vue"; import DOMPurify from "dompurify";
import { computed, onMounted, onUnmounted } from "vue";
import { closeDialog } from "vue3-promise-dialog"; import { closeDialog } from "vue3-promise-dialog";
import { useApplicationStore } from "../../store/ApplicationStore";
import SMButton from "../SMButton.vue"; import SMButton from "../SMButton.vue";
import SMDialog from "../SMDialog.vue";
import SMFormFooter from "../SMFormFooter.vue"; import SMFormFooter from "../SMFormFooter.vue";
import SMModal from "../SMModal.vue"; import SMModal from "../SMModal.vue";
import SMDialog from "../SMDialog.vue";
// import sanitizeHtml from "sanitize-html";
const props = defineProps({ const props = defineProps({
title: { title: {
@@ -59,30 +60,52 @@ const props = defineProps({
}, },
}); });
const handleCancel = () => { const applicationStore = useApplicationStore();
/**
* Handle the user clicking the cancel button.
*/
const handleClickCancel = () => {
closeDialog(false); closeDialog(false);
}; };
const handleConfirm = () => { /**
* Handle the user clicking the confirm button.
*/
const handleClickConfirm = () => {
closeDialog(true); closeDialog(true);
}; };
const eventKeyUp = (event: KeyboardEvent) => { /**
* Sanitize the text property from XSS attacks.
*/
const computedSanitizedText = computed(() => {
return DOMPurify.sanitize(props.text);
});
/**
* Handle a keyboard event in this component.
*
* @param {KeyboardEvent} event The keyboard event.
* @returns {boolean} If the event was handled.
*/
const eventKeyUp = (event: KeyboardEvent): boolean => {
if (event.key === "Escape") { if (event.key === "Escape") {
handleCancel(); handleClickCancel();
return true;
} else if (event.key === "Enter") { } else if (event.key === "Enter") {
handleConfirm(); handleClickConfirm();
return true;
} }
return false;
}; };
onMounted(() => { onMounted(() => {
document.addEventListener("keyup", eventKeyUp); applicationStore.addKeyUpListener(eventKeyUp);
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener("keyup", eventKeyUp); applicationStore.removeKeyUpListener(eventKeyUp);
}); });
// const sanitizedHtml = sanitizeHtml(props.text);
const sanitizedHtml = props.text;
</script> </script>

View File

@@ -3,7 +3,7 @@
<SMDialog <SMDialog
:loading="dialogLoading" :loading="dialogLoading"
full full
:loading_message="dialogLoadingMessage" :loading-message="dialogLoadingMessage"
class="sm-dialog-media"> class="sm-dialog-media">
<h1>Insert Media</h1> <h1>Insert Media</h1>
<SMMessage <SMMessage
@@ -101,18 +101,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, watch, ref, onMounted, onUnmounted, Ref } from "vue"; import { computed, onMounted, onUnmounted, ref, Ref, watch } from "vue";
import { closeDialog } from "vue3-promise-dialog"; import { closeDialog } from "vue3-promise-dialog";
import SMButton from "../SMButton.vue";
import SMFormFooter from "../SMFormFooter.vue";
import SMDialog from "../SMDialog.vue";
import SMMessage from "../SMMessage.vue";
import SMModal from "../SMModal.vue";
import { api } from "../../helpers/api"; import { api } from "../../helpers/api";
import { Media, MediaCollection, MediaResponse } from "../../helpers/api.types";
import { bytesReadable } from "../../helpers/types"; import { bytesReadable } from "../../helpers/types";
import { getFilePreview } from "../../helpers/utils"; import { getFilePreview } from "../../helpers/utils";
import { Media, MediaCollection, MediaResponse } from "../../helpers/api.types"; import { useApplicationStore } from "../../store/ApplicationStore";
import SMButton from "../SMButton.vue";
import SMDialog from "../SMDialog.vue";
import SMFormFooter from "../SMFormFooter.vue";
import SMLoadingIcon from "../SMLoadingIcon.vue"; import SMLoadingIcon from "../SMLoadingIcon.vue";
import SMMessage from "../SMMessage.vue";
import SMModal from "../SMModal.vue";
const props = defineProps({ const props = defineProps({
mime: { mime: {
@@ -187,6 +188,8 @@ const selected = ref("");
*/ */
const perPage = ref(12); const perPage = ref(12);
const applicationStore = useApplicationStore();
/** /**
* Returns the pagination info * Returns the pagination info
*/ */
@@ -247,7 +250,6 @@ const getMediaItem = (item_id: string): Media | null => {
let found: Media | null = null; let found: Media | null = null;
mediaItems.value.every((item) => { mediaItems.value.every((item) => {
console.log(item.id, item_id);
if (item.id == item_id) { if (item.id == item_id) {
found = item; found = item;
return false; return false;
@@ -272,7 +274,6 @@ const handleClickCancel = () => {
const handleClickInsert = () => { const handleClickInsert = () => {
if (selected.value !== "") { if (selected.value !== "") {
const mediaItem = getMediaItem(selected.value); const mediaItem = getMediaItem(selected.value);
console.log(mediaItem, selected.value);
if (mediaItem != null) { if (mediaItem != null) {
closeDialog(mediaItem); closeDialog(mediaItem);
return; return;
@@ -365,7 +366,6 @@ const handleClickUpload = () => {
* Upload the file to the server. * Upload the file to the server.
*/ */
const handleChangeUpload = async () => { const handleChangeUpload = async () => {
dialogLoading.value = true;
formMessage.value = ""; formMessage.value = "";
if (refUploadInput.value != null && refUploadInput.value.files != null) { if (refUploadInput.value != null && refUploadInput.value.files != null) {
@@ -374,20 +374,24 @@ const handleChangeUpload = async () => {
let submitFormData = new FormData(); let submitFormData = new FormData();
submitFormData.append("file", firstFile); submitFormData.append("file", firstFile);
dialogLoading.value = true;
dialogLoadingMessage.value = "Uploading file...";
api.post({ api.post({
url: "/media", url: "/media",
body: submitFormData, body: submitFormData,
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
}, },
// progress: (progressData) => progress: (progressData) =>
// (dialogLoadingMessage.value = `Uploading Files ${Math.floor( (dialogLoadingMessage.value = `Uploading Files ${Math.floor(
// (progressData.loaded / progressData.total) * 100 (progressData.loaded / progressData.total) * 100
// )}%`), )}%`),
}) })
.then((result) => { .then((result) => {
if (result.data) { if (result.data) {
const data = result.data as MediaResponse; const data = result.data as MediaResponse;
closeDialog(data.medium); closeDialog(data.medium);
} else { } else {
formMessage.value = formMessage.value =
@@ -398,6 +402,9 @@ const handleChangeUpload = async () => {
formMessage.value = formMessage.value =
error.response?.data?.message || error.response?.data?.message ||
"An unexpected error occurred"; "An unexpected error occurred";
})
.finally(() => {
dialogLoading.value = false;
}); });
} else { } else {
formMessage.value = "No file was selected to upload"; formMessage.value = "No file was selected to upload";
@@ -405,8 +412,6 @@ const handleChangeUpload = async () => {
} else { } else {
formMessage.value = "No file was selected to upload"; formMessage.value = "No file was selected to upload";
} }
dialogLoading.value = false;
}; };
/** /**
@@ -440,26 +445,32 @@ const handleLoad = async () => {
}; };
/** /**
* Handle the user pressing keyboard keys. * Handle a keyboard event in this component.
* *
* @param {KeyboardEvent} event The keyboard event. * @param {KeyboardEvent} event The keyboard event.
* @returns {boolean} If the event was handled.
*/ */
const eventKeyUp = (event: KeyboardEvent) => { const eventKeyUp = (event: KeyboardEvent): boolean => {
if (event.key === "Escape") { if (event.key === "Escape") {
handleClickCancel(); handleClickCancel();
return true;
} else if (event.key === "Enter") { } else if (event.key === "Enter") {
if (selected.value.length > 0) { if (selected.value.length > 0) {
handleClickInsert(); handleClickInsert();
} }
return true;
} }
return false;
}; };
onMounted(() => { onMounted(() => {
document.addEventListener("keyup", eventKeyUp); applicationStore.addKeyUpListener(eventKeyUp);
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener("keyup", eventKeyUp); applicationStore.removeKeyUpListener(eventKeyUp);
}); });
watch(page, () => { watch(page, () => {

View File

@@ -10,9 +10,7 @@
</SMPage> </SMPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts"></script>
import SMPage from "../SMPage.vue";
</script>
<style lang="scss"> <style lang="scss">
.page-error.forbidden .image { .page-error.forbidden .image {

View File

@@ -13,9 +13,7 @@
</SMPage> </SMPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts"></script>
import SMPage from "../SMPage.vue";
</script>
<style lang="scss"> <style lang="scss">
.page-error.internal .image { .page-error.internal .image {

View File

@@ -10,9 +10,7 @@
</SMPage> </SMPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts"></script>
import SMPage from "../SMPage.vue";
</script>
<style lang="scss"> <style lang="scss">
.page-error.not-found .image { .page-error.not-found .image {

View File

@@ -1,5 +1,5 @@
import { useUserStore } from "../store/UserStore";
import { useProgressStore } from "../store/ProgressStore"; import { useProgressStore } from "../store/ProgressStore";
import { useUserStore } from "../store/UserStore";
interface ApiProgressData { interface ApiProgressData {
loaded: number; loaded: number;
total: number; total: number;
@@ -21,6 +21,7 @@ export interface ApiResponse {
status: number; status: number;
message: string; message: string;
data: unknown; data: unknown;
json?: Record<string, unknown>;
} }
const apiDefaultHeaders = { const apiDefaultHeaders = {
@@ -87,81 +88,22 @@ export const api = {
signal: options.signal || null, signal: options.signal || null,
}; };
let receivedData = false;
const progressStore = useProgressStore(); const progressStore = useProgressStore();
progressStore.start(); progressStore.start();
fetch(url, fetchOptions) fetch(url, fetchOptions)
.then((response) => {
receivedData = true;
if (options.progress) {
if (!response.ok) {
return response;
}
if (!response.body) {
return response;
}
let contentLength =
response.headers.get("content-length");
if (!contentLength) {
contentLength = -1;
}
// parse the integer into a base-10 number
const total = parseInt(contentLength, 10);
let loaded = 0;
return new Response(
// create and return a readable stream
new ReadableStream({
start(controller) {
const reader = response.body.getReader();
read();
/**
*
*/
function read() {
reader
.read()
.then(({ done, value }) => {
if (done) {
controller.close();
return;
}
loaded += value.byteLength;
options.progress({
loaded,
total,
});
controller.enqueue(value);
read();
})
.catch((error) => {
controller.error(error);
reject({
status: 0,
message: "controller error",
data: null,
});
});
}
},
})
);
}
return response;
})
.then(async (response) => { .then(async (response) => {
let data: string | object = ""; let data: string | object = "";
if (response.headers.get("content-type") == null) { if (response.headers.get("content-type") == null) {
try {
data = response.json ? await response.json() : {};
} catch (error) {
data = response.text ? await response.text() : ""; data = response.text ? await response.text() : "";
}
} else { } else {
data = response.json ? await response.json() : {}; data = response.json ? await response.json() : {};
} }
const result = { const result = {
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,

View File

@@ -52,10 +52,10 @@ const defaultFormObject: FormObject = {
}); });
return valid; return valid;
}.bind(this), },
loading: function (state = true) { loading: function (state = true) {
this._loading = state; this._loading = state;
}.bind(this), },
message: function (message = "", type = "", icon = "") { message: function (message = "", type = "", icon = "") {
this._message = message; this._message = message;
@@ -65,14 +65,14 @@ const defaultFormObject: FormObject = {
if (icon.length > 0) { if (icon.length > 0) {
this._messageIcon = icon; this._messageIcon = icon;
} }
}.bind(this), },
error: function (message = "") { error: function (message = "") {
if (message == "") { if (message == "") {
this.message(""); this.message("");
} else { } else {
this.message(message, "error", "alert-circle-outline"); this.message(message, "error", "alert-circle-outline");
} }
}.bind(this), },
apiErrors: function (apiResponse: ApiResponse) { apiErrors: function (apiResponse: ApiResponse) {
let foundKeys = false; let foundKeys = false;
@@ -102,7 +102,7 @@ const defaultFormObject: FormObject = {
"An unknown server error occurred.\nPlease try again later." "An unknown server error occurred.\nPlease try again later."
); );
} }
}.bind(this), },
controls: {}, controls: {},
_loading: false, _loading: false,

View File

@@ -4,9 +4,10 @@
* @param {object|string} objOrString The object or string. * @param {object|string} objOrString The object or string.
* @returns {boolean} If the object or string is empty. * @returns {boolean} If the object or string is empty.
*/ */
export const isEmpty = (objOrString: object | string): boolean => { export const isEmpty = (objOrString: unknown): boolean => {
if (objOrString) { if (objOrString == null) {
if (typeof objOrString === "string") { return true;
} else if (typeof objOrString === "string") {
return objOrString.length == 0; return objOrString.length == 0;
} else if ( } else if (
typeof objOrString == "object" && typeof objOrString == "object" &&
@@ -14,7 +15,6 @@ export const isEmpty = (objOrString: object | string): boolean => {
) { ) {
return true; return true;
} }
}
return false; return false;
}; };

View File

@@ -1,17 +1,18 @@
import "./bootstrap";
import { createApp } from "vue";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import Router from "@/router"; import Router from "@/router";
import "normalize.css"; import "normalize.css";
import "../css/app.scss"; import { createPinia } from "pinia";
import App from "./views/App.vue"; import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import SMContainer from "./components/SMContainer.vue"; import { createApp } from "vue";
import SMRow from "./components/SMRow.vue";
import SMColumn from "./components/SMColumn.vue";
import { PromiseDialog } from "vue3-promise-dialog";
import { VueReCaptcha } from "vue-recaptcha-v3"; import { VueReCaptcha } from "vue-recaptcha-v3";
import { PromiseDialog } from "vue3-promise-dialog";
import "../css/app.scss";
import "./bootstrap";
import SMColumn from "./components/SMColumn.vue";
import SMContainer from "./components/SMContainer.vue";
import SMPage from "./components/SMPage.vue";
import SMRow from "./components/SMRow.vue";
import "./lib/prism"; import "./lib/prism";
import App from "./views/App.vue";
const pinia = createPinia(); const pinia = createPinia();
pinia.use(piniaPluginPersistedstate); pinia.use(piniaPluginPersistedstate);
@@ -29,4 +30,5 @@ createApp(App)
.component("SMContainer", SMContainer) .component("SMContainer", SMContainer)
.component("SMRow", SMRow) .component("SMRow", SMRow)
.component("SMColumn", SMColumn) .component("SMColumn", SMColumn)
.component("SMPage", SMPage)
.mount("#app"); .mount("#app");

View File

@@ -1,8 +1,8 @@
import { createWebHistory, createRouter } from "vue-router";
import { useUserStore } from "@/store/UserStore"; import { useUserStore } from "@/store/UserStore";
import { createRouter, createWebHistory } from "vue-router";
import { api } from "../helpers/api";
import { useApplicationStore } from "../store/ApplicationStore"; import { useApplicationStore } from "../store/ApplicationStore";
import { useProgressStore } from "../store/ProgressStore"; import { useProgressStore } from "../store/ProgressStore";
import { api } from "../helpers/api";
export const routes = [ export const routes = [
{ {

View File

@@ -80,18 +80,17 @@ import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue"; import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue"; import SMForm from "../components/SMForm.vue";
import SMInput from "../components/SMInput.vue"; import SMInput from "../components/SMInput.vue";
import SMPage from "../components/SMPage.vue";
import { api } from "../helpers/api"; import { api } from "../helpers/api";
import { FormObject, FormControl } from "../helpers/form"; import { Form, FormControl } from "../helpers/form";
import { And, Email, Min, Required } from "../helpers/validate"; import { And, Email, Min, Required } from "../helpers/validate";
import { ref, reactive } from "vue"; import { reactive, ref } from "vue";
import { useReCaptcha } from "vue-recaptcha-v3"; import { useReCaptcha } from "vue-recaptcha-v3";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha(); const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const form = reactive( const form = reactive(
FormObject({ Form({
name: FormControl("", And([Required(), Min(4)])), name: FormControl("", And([Required(), Min(4)])),
email: FormControl("", And([Required(), Email()])), email: FormControl("", And([Required(), Email()])),
content: FormControl("", And([Required(), Min(8)])), content: FormControl("", And([Required(), Min(8)])),

View File

@@ -40,23 +40,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from "vue"; import { reactive, ref } from "vue";
import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMDialog from "../components/SMDialog.vue";
import SMPage from "../components/SMPage.vue";
import SMForm from "../components/SMForm.vue";
import { useRoute } from "vue-router";
import { And, Max, Min, Required } from "../helpers/validate";
import { useReCaptcha } from "vue-recaptcha-v3"; import { useReCaptcha } from "vue-recaptcha-v3";
import { FormControl, FormObject } from "../helpers/form"; import { useRoute } from "vue-router";
import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api"; import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Max, Min, Required } from "../helpers/validate";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha(); const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formDone = ref(false); const formDone = ref(false);
const form = reactive( const form = reactive(
FormObject({ Form({
code: FormControl("", And([Required(), Min(6), Max(6)])), code: FormControl("", And([Required(), Min(6), Max(6)])),
}) })
); );

View File

@@ -43,22 +43,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { api } from "../helpers/api"; import { reactive, ref } from "vue";
import { FormObject, FormControl } from "../helpers/form";
import { And, Required, Min } from "../helpers/validate";
import { ref, reactive } from "vue";
import { useReCaptcha } from "vue-recaptcha-v3"; import { useReCaptcha } from "vue-recaptcha-v3";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Min, Required } from "../helpers/validate";
import SMButton from "../components/SMButton.vue"; import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue"; import SMDialog from "../components/SMDialog.vue";
import SMFormFooter from "../components/SMFormFooter.vue"; import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue"; import SMInput from "../components/SMInput.vue";
import SMPage from "../components/SMPage.vue";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha(); const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formDone = ref(false); const formDone = ref(false);
const form = reactive( const form = reactive(
FormObject({ Form({
username: FormControl("", And([Required(), Min(4)])), username: FormControl("", And([Required(), Min(4)])),
}) })
); );

View File

@@ -42,24 +42,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from "vue"; import { reactive, ref } from "vue";
import { api } from "../helpers/api"; import { api } from "../helpers/api";
import { FormObject, FormControl } from "../helpers/form"; import { Form, FormControl } from "../helpers/form";
import { And, Required, Email } from "../helpers/validate"; import { And, Email, Required } from "../helpers/validate";
import SMButton from "../components/SMButton.vue"; import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue"; import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue"; import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue"; import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue"; import SMInput from "../components/SMInput.vue";
import SMPage from "../components/SMPage.vue";
import { useReCaptcha } from "vue-recaptcha-v3"; import { useReCaptcha } from "vue-recaptcha-v3";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha(); const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formDone = ref(false); const formDone = ref(false);
const form = reactive( const form = reactive(
FormObject({ Form({
email: FormControl("", And([Required(), Email()])), email: FormControl("", And([Required(), Email()])),
}) })
); );

View File

@@ -124,24 +124,24 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import { excerpt } from "../helpers/string"; import { useReCaptcha } from "vue-recaptcha-v3";
import { SMDate } from "../helpers/datetime";
import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue"; import SMButton from "../components/SMButton.vue";
import SMCarousel from "../components/SMCarousel.vue"; import SMCarousel from "../components/SMCarousel.vue";
import SMCarouselSlide from "../components/SMCarouselSlide.vue"; import SMCarouselSlide from "../components/SMCarouselSlide.vue";
import SMForm from "../components/SMForm.vue";
import SMDialog from "../components/SMDialog.vue"; import SMDialog from "../components/SMDialog.vue";
import SMPage from "../components/SMPage.vue"; import SMForm from "../components/SMForm.vue";
import { useReCaptcha } from "vue-recaptcha-v3"; import SMInput from "../components/SMInput.vue";
import { FormObject, FormControl } from "../helpers/form";
import { And, Email, Required } from "../helpers/validate";
import { api } from "../helpers/api"; import { api } from "../helpers/api";
import { SMDate } from "../helpers/datetime";
import { Form, FormControl } from "../helpers/form";
import { excerpt } from "../helpers/string";
import { And, Email, Required } from "../helpers/validate";
const slides = ref([]); const slides = ref([]);
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha(); const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const form = reactive( const form = reactive(
FormObject({ Form({
email: FormControl("", And([Required(), Email()])), email: FormControl("", And([Required(), Email()])),
}) })
); );

View File

@@ -34,22 +34,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive } from "vue"; import { reactive } from "vue";
import { useUserStore } from "../store/UserStore";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { api } from "../helpers/api";
import { FormObject, FormControl } from "../helpers/form";
import { And, Min, Required } from "../helpers/validate";
import SMPage from "../components/SMPage.vue";
import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue"; import SMButton from "../components/SMButton.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMDialog from "../components/SMDialog.vue"; import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue"; import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Min, Required } from "../helpers/validate";
import { useUserStore } from "../store/UserStore";
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
const form = reactive( const form = reactive(
FormObject({ Form({
username: FormControl("", And([Required(), Min(4)])), username: FormControl("", And([Required(), Min(4)])),
password: FormControl("", Required()), password: FormControl("", Required()),
}) })

View File

@@ -21,13 +21,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { api } from "../helpers/api";
import { ref } from "vue"; import { ref } from "vue";
import { api } from "../helpers/api";
import { useUserStore } from "../store/UserStore"; import { useUserStore } from "../store/UserStore";
import SMButton from "../components/SMButton.vue"; import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue"; import SMDialog from "../components/SMDialog.vue";
import SMPage from "../components/SMPage.vue";
const userStore = useUserStore(); const userStore = useUserStore();
const formLoading = ref(false); const formLoading = ref(false);

View File

@@ -29,13 +29,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { Ref, ref } from "vue"; import { Ref, ref } from "vue";
import { api } from "../helpers/api";
import SMMessage from "../components/SMMessage.vue"; import SMMessage from "../components/SMMessage.vue";
import SMPanelList from "../components/SMPanelList.vue";
import SMPanel from "../components/SMPanel.vue"; import SMPanel from "../components/SMPanel.vue";
import SMPage from "../components/SMPage.vue"; import SMPanelList from "../components/SMPanelList.vue";
import { api } from "../helpers/api";
import { Post, PostCollection } from "../helpers/api.types";
import { SMDate } from "../helpers/datetime"; import { SMDate } from "../helpers/datetime";
import { PostCollection, Post } from "../helpers/api.types";
const message = ref(""); const message = ref("");
const loading = ref(true); const loading = ref(true);

View File

@@ -25,12 +25,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue"; import { computed, ref } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { api } from "../helpers/api";
import { SMDate } from "../helpers/datetime"; import { SMDate } from "../helpers/datetime";
import { useApplicationStore } from "../store/ApplicationStore"; import { useApplicationStore } from "../store/ApplicationStore";
import { api } from "../helpers/api";
import SMPage from "../components/SMPage.vue";
import SMAttachments from "../components/SMAttachments.vue"; import SMAttachments from "../components/SMAttachments.vue";
const applicationStore = useApplicationStore(); const applicationStore = useApplicationStore();

View File

@@ -70,15 +70,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, watch } from "vue"; import { reactive, ref } from "vue";
import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue"; import SMButton from "../components/SMButton.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMDialog from "../components/SMDialog.vue"; import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue"; import SMForm from "../components/SMForm.vue";
import SMPage from "../components/SMPage.vue"; import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api"; import { api } from "../helpers/api";
import { FormControl, FormObject } from "../helpers/form"; import { Form, FormControl } from "../helpers/form";
import { import {
And, And,
Custom, Custom,
@@ -89,7 +89,6 @@ import {
Required, Required,
} from "../helpers/validate"; } from "../helpers/validate";
import { debounce } from "../helpers/debounce";
import { useReCaptcha } from "vue-recaptcha-v3"; import { useReCaptcha } from "vue-recaptcha-v3";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha(); const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
@@ -140,7 +139,7 @@ const checkUsername = async (value: string): boolean | string => {
const formDone = ref(false); const formDone = ref(false);
const form = reactive( const form = reactive(
FormObject({ Form({
first_name: FormControl("", Required()), first_name: FormControl("", Required()),
last_name: FormControl("", Required()), last_name: FormControl("", Required()),
email: FormControl("", And([Required(), Email()])), email: FormControl("", And([Required(), Email()])),

View File

@@ -43,22 +43,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from "vue"; import { reactive, ref } from "vue";
import SMInput from "../components/SMInput.vue"; import { useReCaptcha } from "vue-recaptcha-v3";
import SMButton from "../components/SMButton.vue"; import SMButton from "../components/SMButton.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMDialog from "../components/SMDialog.vue"; import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue"; import SMForm from "../components/SMForm.vue";
import SMPage from "../components/SMPage.vue"; import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api"; import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { Required } from "../helpers/validate"; import { Required } from "../helpers/validate";
import { useReCaptcha } from "vue-recaptcha-v3";
import { FormObject, FormControl } from "../helpers/form";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha(); const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formDone = ref(false); const formDone = ref(false);
const form = reactive( const form = reactive(
FormObject({ Form({
username: FormControl("", Required()), username: FormControl("", Required()),
}) })
); );

View File

@@ -42,23 +42,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { api } from "../helpers/api"; import { reactive, ref } from "vue";
import { FormObject, FormControl } from "../helpers/form";
import { And, Required, Min, Max, Password } from "../helpers/validate";
import { ref, reactive } from "vue";
import { useRoute } from "vue-router";
import { useReCaptcha } from "vue-recaptcha-v3"; import { useReCaptcha } from "vue-recaptcha-v3";
import { useRoute } from "vue-router";
import SMButton from "../components/SMButton.vue"; import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue"; import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue"; import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue"; import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue"; import SMInput from "../components/SMInput.vue";
import SMPage from "../components/SMPage.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Max, Min, Password, Required } from "../helpers/validate";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha(); const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formDone = ref(false); const formDone = ref(false);
const form = reactive( const form = reactive(
FormObject({ Form({
code: FormControl("", And([Required(), Min(6), Max(6)])), code: FormControl("", And([Required(), Min(6), Max(6)])),
password: FormControl("", And([Required(), Password()])), password: FormControl("", And([Required(), Password()])),
}) })

View File

@@ -75,9 +75,7 @@
</SMPage> </SMPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts"></script>
import SMPage from "../components/SMPage.vue";
</script>
<style lang="scss"> <style lang="scss">
.rules { .rules {

View File

@@ -563,6 +563,4 @@
</SMPage> </SMPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts"></script>
import SMPage from "../components/SMPage.vue";
</script>

View File

@@ -34,23 +34,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { api } from "../helpers/api"; import { reactive, ref } from "vue";
import { FormObject, FormControl } from "../helpers/form";
import { And, Email, Required } from "../helpers/validate";
import { ref, reactive } from "vue";
import { useRoute } from "vue-router";
import { useReCaptcha } from "vue-recaptcha-v3"; import { useReCaptcha } from "vue-recaptcha-v3";
import { useRoute } from "vue-router";
import SMButton from "../components/SMButton.vue"; import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue"; import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue"; import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue"; import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue"; import SMInput from "../components/SMInput.vue";
import SMPage from "../components/SMPage.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Email, Required } from "../helpers/validate";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha(); const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formDone = ref(false); const formDone = ref(false);
const form = reactive( const form = reactive(
FormObject({ Form({
email: FormControl("", And([Required(), Email()])), email: FormControl("", And([Required(), Email()])),
}) })
); );

View File

@@ -50,13 +50,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import { api } from "../helpers/api";
import { SMDate } from "../helpers/datetime";
import SMInput from "../components/SMInput.vue"; import SMInput from "../components/SMInput.vue";
import SMMessage from "../components/SMMessage.vue"; import SMMessage from "../components/SMMessage.vue";
import SMPanelList from "../components/SMPanelList.vue";
import SMPanel from "../components/SMPanel.vue"; import SMPanel from "../components/SMPanel.vue";
import SMPage from "../components/SMPage.vue"; import SMPanelList from "../components/SMPanelList.vue";
import { api } from "../helpers/api";
import { SMDate } from "../helpers/datetime";
const loading = ref(true); const loading = ref(true);
const events = reactive([]); const events = reactive([]);

View File

@@ -88,15 +88,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { api } from "../helpers/api"; import { computed, reactive, ref } from "vue";
import { computed, ref, reactive } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { useApplicationStore } from "../store/ApplicationStore";
import { SMDate } from "../helpers/datetime";
import SMButton from "../components/SMButton.vue"; import SMButton from "../components/SMButton.vue";
import SMHTML from "../components/SMHTML.vue"; import SMHTML from "../components/SMHTML.vue";
import SMMessage from "../components/SMMessage.vue"; import SMMessage from "../components/SMMessage.vue";
import SMPage from "../components/SMPage.vue"; import { api } from "../helpers/api";
import { SMDate } from "../helpers/datetime";
import { useApplicationStore } from "../store/ApplicationStore";
import { ApiEvent, ApiMedia } from "../helpers/api.types"; import { ApiEvent, ApiMedia } from "../helpers/api.types";
import { imageLoad } from "../helpers/image"; import { imageLoad } from "../helpers/image";

View File

@@ -19,19 +19,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { api } from "../../helpers/api";
import { FormObject, FormControl } from "../../helpers/form";
import { And, Required, Min } from "../../helpers/validate";
import { reactive } from "vue"; import { reactive } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import SMInput from "../../components/SMInput.vue";
import SMButton from "../../components/SMButton.vue"; import SMButton from "../../components/SMButton.vue";
import SMPage from "../../components/SMPage.vue";
import SMForm from "../../components/SMForm.vue"; import SMForm from "../../components/SMForm.vue";
import SMInput from "../../components/SMInput.vue";
import { api } from "../../helpers/api";
import { Form, FormControl } from "../../helpers/form";
import { And, Min, Required } from "../../helpers/validate";
const route = useRoute(); const route = useRoute();
const form = reactive( const form = reactive(
FormObject({ Form({
title: FormControl("", And([Required(), Min(2)])), title: FormControl("", And([Required(), Min(2)])),
content: FormControl("", Required()), content: FormControl("", Required()),
}) })

View File

@@ -55,7 +55,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import SMPage from "../../components/SMPage.vue";
import { computed } from "vue"; import { computed } from "vue";
import { useUserStore } from "../../store/UserStore"; import { useUserStore } from "../../store/UserStore";

View File

@@ -1,5 +1,5 @@
<template> <template>
<SMContainer :loading="formLoading" permission="logs/discord"> <SMPage :loading="formLoading" permission="logs/discord">
<h1>Discord Bot Logs</h1> <h1>Discord Bot Logs</h1>
<SMMessage <SMMessage
v-if="message.message" v-if="message.message"
@@ -22,15 +22,15 @@
v-if="!message.message" v-if="!message.message"
label="Reload Logs" label="Reload Logs"
@click="loadData" /> @click="loadData" />
</SMContainer> </SMPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from "vue"; import { reactive, ref } from "vue";
import SMButton from "../../components/SMButton.vue"; import SMButton from "../../components/SMButton.vue";
import SMTabGroup from "../../components/SMTabGroup.vue";
import SMTab from "../../components/SMTab.vue";
import SMMessage from "../../components/SMMessage.vue"; import SMMessage from "../../components/SMMessage.vue";
import SMTab from "../../components/SMTab.vue";
import SMTabGroup from "../../components/SMTabGroup.vue";
import { api } from "../../helpers/api"; import { api } from "../../helpers/api";
let formLoading = ref(false); let formLoading = ref(false);

View File

@@ -105,27 +105,27 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed } from "vue"; import { computed, reactive, ref } from "vue";
import {
And,
Required,
Min,
DateTime,
Custom,
Email,
Url,
} from "../../helpers/validate";
import { FormObject, FormControl } from "../../helpers/form";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { SMDate } from "../../helpers/datetime";
import { api } from "../../helpers/api";
import SMInput from "../../components/SMInput.vue";
import SMButton from "../../components/SMButton.vue"; import SMButton from "../../components/SMButton.vue";
import SMDialog from "../../components/SMDialog.vue";
import SMDatepicker from "../../components/SMDatePicker.vue"; import SMDatepicker from "../../components/SMDatePicker.vue";
import SMDialog from "../../components/SMDialog.vue";
import SMEditor from "../../components/SMEditor.vue"; import SMEditor from "../../components/SMEditor.vue";
import SMFormFooter from "../../components/SMFormFooter.vue"; import SMFormFooter from "../../components/SMFormFooter.vue";
import SMPage from "../../components/SMPage.vue"; import SMInput from "../../components/SMInput.vue";
import { api } from "../../helpers/api";
import { SMDate } from "../../helpers/datetime";
import { FormControl } from "../../helpers/form";
import {
And,
Custom,
DateTime,
Email,
Min,
Required,
Url,
} from "../../helpers/validate";
import SMForm from "../../components/SMForm.vue"; import SMForm from "../../components/SMForm.vue";
const route = useRoute(); const route = useRoute();
@@ -169,7 +169,7 @@ const registration_data = computed(() => {
}); });
const form = reactive( const form = reactive(
FormObject({ Form({
title: FormControl("", And([Required(), Min(6)])), title: FormControl("", And([Required(), Min(6)])),
location: FormControl("online"), location: FormControl("online"),
address: FormControl( address: FormControl(

View File

@@ -1,5 +1,5 @@
<template> <template>
<SMContainer permission="admin/events"> <SMPage permission="admin/events">
<SMHeading heading="Events" /> <SMHeading heading="Events" />
<SMMessage <SMMessage
v-if="formMessage.message" v-if="formMessage.message"
@@ -38,23 +38,23 @@
<div class="action-wrapper"></div> <div class="action-wrapper"></div>
</template> </template>
</EasyDataTable> </EasyDataTable>
</SMContainer> </SMPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, reactive } from "vue"; import { reactive, ref, watch } from "vue";
import { useRouter } from "vue-router";
import EasyDataTable from "vue3-easy-data-table"; import EasyDataTable from "vue3-easy-data-table";
import { openDialog } from "vue3-promise-dialog";
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import SMButton from "../../components/SMButton.vue";
import SMHeading from "../../components/SMHeading.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
import SMMessage from "../../components/SMMessage.vue";
import SMToolbar from "../../components/SMToolbar.vue";
import { api } from "../../helpers/api"; import { api } from "../../helpers/api";
import { SMDate } from "../../helpers/datetime"; import { SMDate } from "../../helpers/datetime";
import { useRouter } from "vue-router";
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import { openDialog } from "vue3-promise-dialog";
import SMToolbar from "../../components/SMToolbar.vue";
import SMButton from "../../components/SMButton.vue";
import { debounce } from "../../helpers/debounce"; import { debounce } from "../../helpers/debounce";
import SMHeading from "../../components/SMHeading.vue";
import SMMessage from "../../components/SMMessage.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
const router = useRouter(); const router = useRouter();
const search = ref(""); const search = ref("");

View File

@@ -62,18 +62,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed } from "vue"; import { computed, reactive, ref } from "vue";
import SMInput from "../../components/SMInput.vue"; import { useRoute, useRouter } from "vue-router";
import SMButton from "../../components/SMButton.vue"; import SMButton from "../../components/SMButton.vue";
import SMDialog from "../../components/SMDialog.vue"; import SMDialog from "../../components/SMDialog.vue";
import SMForm from "../../components/SMForm.vue"; import SMForm from "../../components/SMForm.vue";
import SMPage from "../../components/SMPage.vue"; import SMInput from "../../components/SMInput.vue";
import { api } from "../../helpers/api"; import { api } from "../../helpers/api";
import { FormObject, FormControl } from "../../helpers/form"; import { Form, FormControl } from "../../helpers/form";
import { And, Required, FileSize } from "../../helpers/validate";
import { useRoute } from "vue-router";
import { bytesReadable } from "../../helpers/types"; import { bytesReadable } from "../../helpers/types";
import { useRouter } from "vue-router"; import { And, FileSize, Required } from "../../helpers/validate";
const router = useRouter(); const router = useRouter();
const pageError = ref(200); const pageError = ref(200);
@@ -83,7 +82,7 @@ const route = useRoute();
const page_title = route.params.id ? "Edit Media" : "Upload Media"; const page_title = route.params.id ? "Edit Media" : "Upload Media";
const form = reactive( const form = reactive(
FormObject({ Form({
file: FormControl("", And([Required(), FileSize(5242880)])), file: FormControl("", And([Required(), FileSize(5242880)])),
permission: FormControl(), permission: FormControl(),
}) })

View File

@@ -1,5 +1,5 @@
<template> <template>
<SMContainer permission="admin/media"> <SMPage permission="admin/media">
<h1>Media</h1> <h1>Media</h1>
<SMMessage <SMMessage
@@ -42,25 +42,25 @@
</div> </div>
</template> </template>
</EasyDataTable> </EasyDataTable>
</SMContainer> </SMPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, watch } from "vue"; import { reactive, ref, watch } from "vue";
import { useRouter } from "vue-router";
import EasyDataTable from "vue3-easy-data-table"; import EasyDataTable from "vue3-easy-data-table";
import { openDialog } from "vue3-promise-dialog";
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import SMButton from "../../components/SMButton.vue";
import SMFileLink from "../../components/SMFileLink.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
import SMMessage from "../../components/SMMessage.vue";
import SMToolbar from "../../components/SMToolbar.vue";
import { api } from "../../helpers/api"; import { api } from "../../helpers/api";
import { SMDate } from "../../helpers/datetime"; import { SMDate } from "../../helpers/datetime";
import { useRouter } from "vue-router";
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import { openDialog } from "vue3-promise-dialog";
import SMToolbar from "../../components/SMToolbar.vue";
import SMButton from "../../components/SMButton.vue";
import { debounce } from "../../helpers/debounce"; import { debounce } from "../../helpers/debounce";
import { bytesReadable } from "../../helpers/types"; import { bytesReadable } from "../../helpers/types";
import SMMessage from "../../components/SMMessage.vue";
import SMFileLink from "../../components/SMFileLink.vue";
import { useUserStore } from "../../store/UserStore"; import { useUserStore } from "../../store/UserStore";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
const router = useRouter(); const router = useRouter();
const search = ref(""); const search = ref("");

View File

@@ -40,7 +40,8 @@
</SMRow> </SMRow>
<SMRow> <SMRow>
<SMColumn> <SMColumn>
<SMEditor v-model:model-value="form.content.value" /> <SMEditor
v-model:model-value="form.controls.content.value" />
</SMColumn> </SMColumn>
</SMRow> </SMRow>
<SMRow> <SMRow>
@@ -61,22 +62,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from "vue"; import { reactive, ref } from "vue";
import { api } from "../../helpers/api";
import { FormObject, FormControl } from "../../helpers/form";
import { And, Required, Min, DateTime } from "../../helpers/validate";
import { SMDate } from "../../helpers/datetime";
import { useUserStore } from "../../store/UserStore";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { PostResponse, UserCollection } from "../../helpers/api.types";
import SMInput from "../../components/SMInput.vue";
import SMButton from "../../components/SMButton.vue"; import SMButton from "../../components/SMButton.vue";
import SMEditor from "../../components/SMEditor.vue"; import SMEditor from "../../components/SMEditor.vue";
import SMPage from "../../components/SMPage.vue";
import SMForm from "../../components/SMForm.vue"; import SMForm from "../../components/SMForm.vue";
import SMFormFooter from "../../components/SMFormFooter.vue"; import SMFormFooter from "../../components/SMFormFooter.vue";
import SMInput from "../../components/SMInput.vue";
import SMInputAttachments from "../../components/SMInputAttachments.vue"; import SMInputAttachments from "../../components/SMInputAttachments.vue";
import { api } from "../../helpers/api";
import { PostResponse, UserCollection } from "../../helpers/api.types";
import { SMDate } from "../../helpers/datetime";
import { Form, FormControl } from "../../helpers/form";
import { And, DateTime, Min, Required } from "../../helpers/validate";
import { useUserStore } from "../../store/UserStore";
const route = useRoute(); const route = useRoute();
const userStore = useUserStore(); const userStore = useUserStore();
const page_title = route.params.id ? "Edit Post" : "Create New Post"; const page_title = route.params.id ? "Edit Post" : "Create New Post";
@@ -85,7 +86,7 @@ const authors = ref({});
const attachments = ref(["4687166e-7f9e-4394-abdf-2d254c8bb087"]); const attachments = ref(["4687166e-7f9e-4394-abdf-2d254c8bb087"]);
const form = reactive( const form = reactive(
FormObject({ Form({
title: FormControl("", And([Required(), Min(8)])), title: FormControl("", And([Required(), Min(8)])),
slug: FormControl("", And([Required(), Min(6)])), slug: FormControl("", And([Required(), Min(6)])),
publish_at: FormControl("", DateTime()), publish_at: FormControl("", DateTime()),
@@ -96,9 +97,9 @@ const form = reactive(
); );
const updateSlug = async () => { const updateSlug = async () => {
if (form.slug.value == "" && form.title.value != "") { if (form.controls.slug.value == "" && form.controls.title.value != "") {
let idx = 0; let idx = 0;
let pre_slug = form.title.value let pre_slug = form.controls.title.value
.toLowerCase() .toLowerCase()
.replace(/[^a-z0-9]/gim, "-") .replace(/[^a-z0-9]/gim, "-")
.replace(/-+/g, "-") .replace(/-+/g, "-")
@@ -122,8 +123,8 @@ const updateSlug = async () => {
idx++; idx++;
} catch (error) { } catch (error) {
if (error.status == 404) { if (error.status == 404) {
if (form.slug.value == "") { if (form.controls.slug.value == "") {
form.slug.value = slug; form.controls.slug.value = slug;
} }
} }
@@ -141,18 +142,18 @@ const loadData = async () => {
.then((result) => { .then((result) => {
const data = result.data as PostResponse; const data = result.data as PostResponse;
form.title.value = data.post.title; form.controls.title.value = data.post.title;
form.slug.value = data.post.slug; form.controls.slug.value = data.post.slug;
form.user_id.value = data.post.user_id; form.controls.user_id.value = data.post.user_id;
form.content.value = data.post.content; form.controls.content.value = data.post.content;
form.publish_at.value = data.post.publish_at form.controls.publish_at.value = data.post.publish_at
? new SMDate(data.post.publish_at, { ? new SMDate(data.post.publish_at, {
format: "yMd", format: "yMd",
utc: true, utc: true,
}).format("dd/MM/yyyy HH:mm") }).format("dd/MM/yyyy HH:mm")
: ""; : "";
form.content.value = data.post.content; form.controls.content.value = data.post.content;
form.hero.value = data.post.hero; form.controls.hero.value = data.post.hero;
}) })
.catch((error) => { .catch((error) => {
pageError.value = pageError.value =
@@ -166,15 +167,15 @@ const loadData = async () => {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
let data = { let data = {
title: form.title.value, title: form.controls.title.value,
slug: form.slug.value, slug: form.controls.slug.value,
publish_at: new SMDate(form.publish_at.value).format( publish_at: new SMDate(form.controls.publish_at.value).format(
"yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm:ss",
{ utc: true } { utc: true }
), ),
user_id: form.user_id.value, user_id: form.controls.user_id.value,
content: form.content.value, content: form.controls.content.value,
hero: form.hero.value, hero: form.controls.hero.value,
}; };
if (route.params.id) { if (route.params.id) {

View File

@@ -51,20 +51,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, reactive } from "vue"; import { reactive, ref, watch } from "vue";
import { SMDate } from "../../helpers/datetime";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { openDialog } from "vue3-promise-dialog";
import { api } from "../../helpers/api";
import { debounce } from "../../helpers/debounce";
import EasyDataTable from "vue3-easy-data-table"; import EasyDataTable from "vue3-easy-data-table";
import { openDialog } from "vue3-promise-dialog";
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue"; import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import SMToolbar from "../../components/SMToolbar.vue";
import SMButton from "../../components/SMButton.vue"; import SMButton from "../../components/SMButton.vue";
import SMHeading from "../../components/SMHeading.vue"; import SMHeading from "../../components/SMHeading.vue";
import SMMessage from "../../components/SMMessage.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue"; import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
import SMPage from "../../components/SMPage.vue"; import SMMessage from "../../components/SMMessage.vue";
import SMToolbar from "../../components/SMToolbar.vue";
import { api } from "../../helpers/api";
import { SMDate } from "../../helpers/datetime";
import { debounce } from "../../helpers/debounce";
const router = useRouter(); const router = useRouter();
const search = ref(""); const search = ref("");

View File

@@ -30,26 +30,26 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive, computed } from "vue"; import { computed, reactive } from "vue";
import { api } from "../../helpers/api";
import { FormObject, FormControl } from "../../helpers/form";
import { And, Required, Email, Phone } from "../../helpers/validate";
import { useUserStore } from "../../store/UserStore";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { openDialog } from "vue3-promise-dialog"; import { openDialog } from "vue3-promise-dialog";
import SMInput from "../../components/SMInput.vue"; import SMDialogChangePassword from "../../components/dialogs/SMDialogChangePassword.vue";
import SMButton from "../../components/SMButton.vue"; import SMButton from "../../components/SMButton.vue";
import SMForm from "../../components/SMForm.vue"; import SMForm from "../../components/SMForm.vue";
import SMPage from "../../components/SMPage.vue";
import SMHeading from "../../components/SMHeading.vue";
import SMFormFooter from "../../components/SMFormFooter.vue"; import SMFormFooter from "../../components/SMFormFooter.vue";
import SMDialogChangePassword from "../../components/dialogs/SMDialogChangePassword.vue"; import SMHeading from "../../components/SMHeading.vue";
import SMInput from "../../components/SMInput.vue";
import { api } from "../../helpers/api";
import { Form, FormControl } from "../../helpers/form";
import { And, Email, Phone, Required } from "../../helpers/validate";
import { useUserStore } from "../../store/UserStore";
const route = useRoute(); const route = useRoute();
const userStore = useUserStore(); const userStore = useUserStore();
const form = reactive( const form = reactive(
FormObject({ Form({
first_name: FormControl("", And([Required()])), first_name: FormControl("", And([Required()])),
last_name: FormControl("", And([Required()])), last_name: FormControl("", And([Required()])),
email: FormControl("", And([Required(), Email()])), email: FormControl("", And([Required(), Email()])),

View File

@@ -1,5 +1,5 @@
<template> <template>
<SMContainer permission="admin/users"> <SMPage permission="admin/users">
<SMHeading heading="Users" /> <SMHeading heading="Users" />
<SMMessage <SMMessage
v-if="formMessage.message" v-if="formMessage.message"
@@ -22,20 +22,20 @@
<div class="action-wrapper"></div> <div class="action-wrapper"></div>
</template> </template>
</EasyDataTable> </EasyDataTable>
</SMContainer> </SMPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, watch } from "vue"; import { reactive, ref, watch } from "vue";
import { useRouter } from "vue-router";
import EasyDataTable from "vue3-easy-data-table"; import EasyDataTable from "vue3-easy-data-table";
import { openDialog } from "vue3-promise-dialog";
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import SMHeading from "../../components/SMHeading.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
import SMMessage from "../../components/SMMessage.vue";
import { api } from "../../helpers/api"; import { api } from "../../helpers/api";
import { SMDate } from "../../helpers/datetime"; import { SMDate } from "../../helpers/datetime";
import { useRouter } from "vue-router";
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import { openDialog } from "vue3-promise-dialog";
import SMHeading from "../../components/SMHeading.vue";
import SMMessage from "../../components/SMMessage.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
const router = useRouter(); const router = useRouter();
const searchValue = ref(""); const searchValue = ref("");

13
tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
// "strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["resources/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,7 +1,7 @@
import { defineConfig } from "vite";
import laravel from "laravel-vite-plugin";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import laravel from "laravel-vite-plugin";
import analyzer from "rollup-plugin-analyzer"; import analyzer from "rollup-plugin-analyzer";
import { defineConfig } from "vite";
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [