cleanup
This commit is contained in:
6
import-meta.d.ts
vendored
Normal file
6
import-meta.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
interface ImportMeta {
|
||||||
|
env: {
|
||||||
|
APP_URL: string;
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
dropdownMenu.value.style.display = "block";
|
if (dropdownMenu.value) {
|
||||||
|
dropdownMenu.value.style.display = "block";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
dropdownMenu.value.style.display = "none";
|
if (dropdownMenu.value) {
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = () => {
|
/**
|
||||||
slideElements.value = slides.value.querySelectorAll(".carousel-slide");
|
* Handle slides added/removed from the carousel and update the data/indicators.
|
||||||
maxSlide.value = slideElements.value.length - 1;
|
*/
|
||||||
|
const handleCarouselUpdate = () => {
|
||||||
|
if (slides.value != null) {
|
||||||
|
slideElements.value = slides.value.querySelectorAll(".carousel-slide");
|
||||||
|
maxSlide.value = slideElements.value.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
updateSlidePositions();
|
updateSlidePositions();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the style transform of each slide.
|
||||||
|
*/
|
||||||
const updateSlidePositions = () => {
|
const updateSlidePositions = () => {
|
||||||
slideElements.value.forEach((slide, index) => {
|
if (slideElements.value != null) {
|
||||||
slide.style.transform = `translateX(${
|
slideElements.value.forEach((slide, index) => {
|
||||||
100 * (index - currentSlide.value)
|
(slide as HTMLElement).style.transform = `translateX(${
|
||||||
}%)`;
|
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,39 +154,60 @@ 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,
|
||||||
childList: true,
|
childList: true,
|
||||||
characterData: true,
|
characterData: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect the mutation observer from the slider.
|
||||||
|
*/
|
||||||
const disconnectMutationObserver = () => {
|
const disconnectMutationObserver = () => {
|
||||||
mutationObserver.value.disconnect();
|
if (mutationObserver.value) {
|
||||||
|
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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,25 +61,32 @@ 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 */
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleLoad();
|
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;
|
||||||
|
|||||||
@@ -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>
|
<slot name="inner"></slot>
|
||||||
<d-error-internal
|
</div>
|
||||||
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>
|
|
||||||
</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%;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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>`,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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,52 +25,50 @@ 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);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
background-color: $primary-color-lighter;
|
background-color: $primary-color-lighter;
|
||||||
color: $primary-color-darker;
|
color: $primary-color-darker;
|
||||||
border: 1px solid $primary-color-lighter;
|
border: 1px solid $primary-color-lighter;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
background-color: $success-color-lighter;
|
background-color: $success-color-lighter;
|
||||||
color: $success-color-darker;
|
color: $success-color-darker;
|
||||||
border: 1px solid $success-color-lighter;
|
border: 1px solid $success-color-lighter;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
background-color: $danger-color-lighter;
|
background-color: $danger-color-lighter;
|
||||||
color: $danger-color-darker;
|
color: $danger-color-darker;
|
||||||
border: 1px solid $danger-color-lighter;
|
border: 1px solid $danger-color-lighter;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-icon {
|
ion-icon {
|
||||||
height: 1.3em;
|
height: 1.3em;
|
||||||
width: 1.3em;
|
width: 1.3em;
|
||||||
margin-right: map-get($spacer, 1);
|
margin-right: map-get($spacer, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal">
|
<div class="sm-modal">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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,29 +154,28 @@ 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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
// }
|
// }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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, () => {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
data = response.text ? await response.text() : "";
|
try {
|
||||||
|
data = response.json ? await response.json() : {};
|
||||||
|
} catch (error) {
|
||||||
|
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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
* @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;
|
||||||
return objOrString.length == 0;
|
} else if (typeof objOrString === "string") {
|
||||||
} else if (
|
return objOrString.length == 0;
|
||||||
typeof objOrString == "object" &&
|
} else if (
|
||||||
Object.keys(objOrString).length === 0
|
typeof objOrString == "object" &&
|
||||||
) {
|
Object.keys(objOrString).length === 0
|
||||||
return true;
|
) {
|
||||||
}
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)])),
|
||||||
|
|||||||
@@ -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)])),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)])),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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()])),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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()])),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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()),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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()])),
|
||||||
|
|||||||
@@ -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()),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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()])),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -563,6 +563,4 @@
|
|||||||
</SMPage>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts"></script>
|
||||||
import SMPage from "../components/SMPage.vue";
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -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()])),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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([]);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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()),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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("");
|
||||||
|
|||||||
@@ -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(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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("");
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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("");
|
||||||
|
|||||||
@@ -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()])),
|
||||||
|
|||||||
@@ -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
13
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
@@ -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: [
|
||||||
|
|||||||
Reference in New Issue
Block a user