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",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vuepic/vue-datepicker": "^3.6.4",
|
||||
"dompurify": "^3.0.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pinia": "^2.0.28",
|
||||
@@ -1721,9 +1722,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.4.tgz",
|
||||
"integrity": "sha512-1e2SpqHiRx4DPvmRuXU5J0di3iQACwJM+mFGE2HAkkK7Tbnfk9WcghcAmyWc9CRrjyRRUpmuhPUH6LphQQR3EQ=="
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.0.tgz",
|
||||
"integrity": "sha512-0g/yr2IJn4nTbxwL785YxS7/AvvgGFJw6LLWP+BzWzB1+BYOqPUT9Hy0rXrZh5HLdHnxH72aDdzvC9SdTjsuaA=="
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.0.3",
|
||||
@@ -4071,6 +4072,11 @@
|
||||
"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": {
|
||||
"version": "9.1.0",
|
||||
"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",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vuepic/vue-datepicker": "^3.6.4",
|
||||
"dompurify": "^3.0.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pinia": "^2.0.28",
|
||||
|
||||
@@ -124,11 +124,6 @@ select {
|
||||
}
|
||||
}
|
||||
|
||||
svg,
|
||||
button {
|
||||
@extend .prevent-select;
|
||||
}
|
||||
|
||||
code {
|
||||
display: block;
|
||||
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-error {
|
||||
display: flex;
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
/* Utility */
|
||||
.prevent-select {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
<div
|
||||
v-for="file of props.attachments"
|
||||
:key="file.id"
|
||||
class="attachment-row">
|
||||
<div class="attachment-file-icon">
|
||||
class="sm-attachment-row">
|
||||
<div class="sm-attachment-file-icon">
|
||||
<img
|
||||
:src="getFileIconImagePath(file.title || file.name)"
|
||||
height="48"
|
||||
width="48" />
|
||||
</div>
|
||||
<a :href="file.url" class="attachment-file-name">{{
|
||||
<a :href="file.url" class="sm-attachment-file-name">{{
|
||||
file.title || file.name
|
||||
}}</a>
|
||||
<div class="attachment-file-size">
|
||||
<div class="sm-attachment-file-size">
|
||||
({{ bytesReadable(file.size) }})
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { bytesReadable } from "../helpers/types";
|
||||
import { getFileIconImagePath } from "../helpers/utils";
|
||||
import SMContainer from "./SMContainer.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -33,11 +34,6 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getFileIconImagePath = (fileName: string): string => {
|
||||
const ext = fileName.split(".").pop();
|
||||
return `/img/fileicons/${ext}.png`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -46,7 +42,7 @@ const getFileIconImagePath = (fileName: string): string => {
|
||||
margin-top: map-get($spacer, 3);
|
||||
}
|
||||
|
||||
.attachment-row {
|
||||
.sm-attachment-row {
|
||||
border-bottom: 1px solid $secondary-background-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -56,13 +52,13 @@ const getFileIconImagePath = (fileName: string): string => {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.attachment-file-icon {
|
||||
.sm-attachment-file-icon {
|
||||
display: flex;
|
||||
width: 64px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.attachment-file-size {
|
||||
.sm-attachment-file-size {
|
||||
font-size: 75%;
|
||||
padding-left: 0.75rem;
|
||||
color: $secondary-color-dark;
|
||||
|
||||
@@ -1,51 +1,53 @@
|
||||
<template>
|
||||
<SMContainer
|
||||
v-if="showBreadcrumbs"
|
||||
:class="[
|
||||
'flex-0',
|
||||
'breadcrumbs-outer',
|
||||
{ closed: breadcrumbs.length == 0 },
|
||||
'sm-breadcrumbs-container',
|
||||
{ closed: computedRouteCrumbs.length == 0 },
|
||||
]">
|
||||
<ul class="breadcrumbs">
|
||||
<ul class="sm-breadcrumbs">
|
||||
<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
|
||||
v-if="idx != breadcrumbs.length - 1"
|
||||
:to="{ name: val.name }"
|
||||
>{{ val.meta?.title || val.name }}</router-link
|
||||
><span v-else>{{ val.meta?.title || val.name }}</span>
|
||||
v-if="index != computedRouteCrumbs.length - 1"
|
||||
:to="{ name: routeItem.name }"
|
||||
>{{ routeItem.meta?.title || routeItem.name }}</router-link
|
||||
><span v-else>{{
|
||||
routeItem.meta?.title || routeItem.name
|
||||
}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</SMContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { computed, ComputedRef } from "vue";
|
||||
import { RouteRecordRaw, useRoute } from "vue-router";
|
||||
import { routes } from "../router";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
|
||||
const applicationStore = useApplicationStore();
|
||||
const showBreadcrumbs = ref(true);
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
/**
|
||||
* Return a list of routes from the current page back to the root
|
||||
*/
|
||||
const computedRouteCrumbs: ComputedRef<RouteRecordRaw[]> = computed(() => {
|
||||
const currentPageName = useRoute().name;
|
||||
|
||||
if (currentPageName == "home") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const findMatch = (list) => {
|
||||
let found = null;
|
||||
let index = null;
|
||||
let child = null;
|
||||
const findMatch = (list: RouteRecordRaw[]): RouteRecordRaw[] | null => {
|
||||
let found: RouteRecordRaw[] | null = null;
|
||||
let index: RouteRecordRaw | null = null;
|
||||
let child: RouteRecordRaw[] | null = null;
|
||||
|
||||
list.every((entry) => {
|
||||
list.every((entry: RouteRecordRaw) => {
|
||||
if (index == null && "path" in entry && entry.path == "") {
|
||||
index = entry;
|
||||
}
|
||||
|
||||
if (child == null && "children" in entry) {
|
||||
if (child == null && entry.children) {
|
||||
child = findMatch(entry.children);
|
||||
}
|
||||
|
||||
@@ -74,32 +76,18 @@ const breadcrumbs = computed(() => {
|
||||
};
|
||||
|
||||
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 || [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.breadcrumbs-outer.closed .breadcrumbs {
|
||||
.sm-breadcrumbs-container.closed .sm-breadcrumbs {
|
||||
opacity: 0;
|
||||
transition: opacity 0s;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
.sm-breadcrumbs {
|
||||
height: 3.25rem;
|
||||
display: flex;
|
||||
max-width: 1200px;
|
||||
|
||||
@@ -1,36 +1,26 @@
|
||||
<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
|
||||
v-else-if="to == null"
|
||||
v-if="isEmpty(to)"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'button',
|
||||
'prevent-select',
|
||||
'sm-button',
|
||||
classType,
|
||||
{ 'button-block': block },
|
||||
{ 'dropdown-button': dropdown },
|
||||
{ 'sm-button-block': block },
|
||||
{ 'sm-dropdown-button': dropdown },
|
||||
]"
|
||||
:type="buttonType"
|
||||
@click="handleClick">
|
||||
<ion-icon
|
||||
v-if="icon && dropdown == null && iconLocation == 'before'"
|
||||
:icon="icon" />
|
||||
<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
|
||||
v-if="dropdown != null"
|
||||
name="caret-down-outline"
|
||||
@click.stop="handleToggleDropdown" />
|
||||
@click.stop="handleClickToggleDropdown" />
|
||||
<ul
|
||||
v-if="dropdown != null"
|
||||
ref="dropdownMenu"
|
||||
@@ -43,23 +33,29 @@
|
||||
</li>
|
||||
</ul>
|
||||
</button>
|
||||
<router-link
|
||||
v-else
|
||||
:to="to"
|
||||
<a
|
||||
v-else-if="!isEmpty(to) && typeof to == 'string'"
|
||||
:href="to"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'button',
|
||||
'prevent-select',
|
||||
classType,
|
||||
{ 'button-block': block },
|
||||
]">
|
||||
:class="['sm-button', classType, { 'sm-button-block': block }]"
|
||||
:type="buttonType">
|
||||
{{ label }}
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { Ref, ref } from "vue";
|
||||
import { isEmpty } from "../helpers/utils";
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: "Button", required: false },
|
||||
@@ -69,17 +65,20 @@ const props = defineProps({
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
iconLocation: {
|
||||
type: String,
|
||||
default: "before",
|
||||
required: false,
|
||||
validator: (value: string) => {
|
||||
return ["before", "after"].includes(value);
|
||||
},
|
||||
},
|
||||
to: {
|
||||
type: [String, Object],
|
||||
default: null,
|
||||
required: false,
|
||||
validator: (prop) => typeof prop === "object" || prop === null,
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
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 dropdownMenu = ref(null);
|
||||
const dropdownMenu: Ref<HTMLElement | null> = ref(null);
|
||||
|
||||
const emits = defineEmits(["click"]);
|
||||
const handleClick = () => {
|
||||
emits("click", "");
|
||||
};
|
||||
|
||||
const handleToggleDropdown = () => {
|
||||
dropdownMenu.value.style.display = "block";
|
||||
const handleClickToggleDropdown = () => {
|
||||
if (dropdownMenu.value) {
|
||||
dropdownMenu.value.style.display = "block";
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
dropdownMenu.value.style.display = "none";
|
||||
if (dropdownMenu.value) {
|
||||
dropdownMenu.value.style.display = "none";
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickItem = (item: string) => {
|
||||
@@ -121,16 +125,40 @@ const handleClickItem = (item: string) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.button {
|
||||
a.sm-button,
|
||||
.sm-button {
|
||||
cursor: pointer;
|
||||
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;
|
||||
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;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
@@ -146,7 +174,6 @@ const handleClickItem = (item: string) => {
|
||||
span {
|
||||
flex: 1;
|
||||
border-right: 1px solid $primary-color;
|
||||
padding: 0;
|
||||
padding-top: calc(#{map-get($spacer, 1)} / 1.5);
|
||||
padding-bottom: calc(#{map-get($spacer, 1)} / 1.5);
|
||||
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 {
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
@@ -195,7 +307,7 @@ const handleClickItem = (item: string) => {
|
||||
li {
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease-in-out;
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="captcha-notice">
|
||||
<div class="sm-captcha-notice">
|
||||
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/terms">Terms of Service</a> apply.
|
||||
@@ -7,7 +7,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.captcha-notice {
|
||||
.sm-captcha-notice {
|
||||
color: $secondary-color;
|
||||
font-size: 65%;
|
||||
line-height: 1.2rem;
|
||||
|
||||
@@ -1,60 +1,81 @@
|
||||
<template>
|
||||
<div
|
||||
class="carousel"
|
||||
class="sm-carousel"
|
||||
@mouseover="handleMouseOver"
|
||||
@mouseleave="handleMouseLeave">
|
||||
<div ref="slides" class="carousel-slides">
|
||||
<div ref="slides" class="sm-carousel-slides">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="carousel-slide-prev" @click="handleSlidePrev">
|
||||
<div class="sm-carousel-slide-prev" @click="handleClickSlidePrev">
|
||||
<ion-icon name="chevron-back-outline" />
|
||||
</div>
|
||||
<div class="carousel-slide-next" @click="handleSlideNext">
|
||||
<div class="sm-carousel-slide-next" @click="handleClickSlideNext">
|
||||
<ion-icon name="chevron-forward-outline" />
|
||||
</div>
|
||||
<div class="carousel-slide-indicators">
|
||||
<div class="sm-carousel-slide-indicators">
|
||||
<div
|
||||
v-for="(indicator, index) in slideElements"
|
||||
:key="index"
|
||||
:class="[
|
||||
'carousel-slide-indicator-item',
|
||||
'sm-carousel-slide-indicator-item',
|
||||
{ highlighted: currentSlide == index },
|
||||
]"
|
||||
@click="handleIndicator(index)"></div>
|
||||
@click="handleClickIndicator(index)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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);
|
||||
|
||||
/**
|
||||
* The maximum number of slides.
|
||||
*/
|
||||
let maxSlide = ref(0);
|
||||
let intervalRef = null;
|
||||
const mutationObserver = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
connectMutationObserver();
|
||||
handleUpdate();
|
||||
startAutoSlide();
|
||||
});
|
||||
/**
|
||||
* The window interval reference to slide the carousel.
|
||||
*/
|
||||
let intervalRef: number | null = null;
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoSlide();
|
||||
disconnectMutationObserver();
|
||||
});
|
||||
/**
|
||||
* The active mutation observer.
|
||||
*/
|
||||
const mutationObserver: Ref<MutationObserver | null> = ref(null);
|
||||
|
||||
/**
|
||||
* Handle the user moving the mouse over the carousel.
|
||||
*/
|
||||
const handleMouseOver = () => {
|
||||
stopAutoSlide();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the user moving the mouse leaving the carousel.
|
||||
*/
|
||||
const handleMouseLeave = () => {
|
||||
startAutoSlide();
|
||||
};
|
||||
|
||||
const handleSlidePrev = () => {
|
||||
/**
|
||||
* Handle the user clicking the previous slider indicator.
|
||||
*/
|
||||
const handleClickSlidePrev = () => {
|
||||
if (currentSlide.value == 0) {
|
||||
currentSlide.value = maxSlide.value;
|
||||
} else {
|
||||
@@ -64,7 +85,10 @@ const handleSlidePrev = () => {
|
||||
updateSlidePositions();
|
||||
};
|
||||
|
||||
const handleSlideNext = () => {
|
||||
/**
|
||||
* Handle the user clicking the next slider indicator.
|
||||
*/
|
||||
const handleClickSlideNext = () => {
|
||||
if (currentSlide.value == maxSlide.value) {
|
||||
currentSlide.value = 0;
|
||||
} else {
|
||||
@@ -74,34 +98,55 @@ const handleSlideNext = () => {
|
||||
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;
|
||||
updateSlidePositions();
|
||||
};
|
||||
|
||||
const handleUpdate = () => {
|
||||
slideElements.value = slides.value.querySelectorAll(".carousel-slide");
|
||||
maxSlide.value = slideElements.value.length - 1;
|
||||
/**
|
||||
* Handle slides added/removed from the carousel and update the data/indicators.
|
||||
*/
|
||||
const handleCarouselUpdate = () => {
|
||||
if (slides.value != null) {
|
||||
slideElements.value = slides.value.querySelectorAll(".carousel-slide");
|
||||
maxSlide.value = slideElements.value.length - 1;
|
||||
}
|
||||
|
||||
updateSlidePositions();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the style transform of each slide.
|
||||
*/
|
||||
const updateSlidePositions = () => {
|
||||
slideElements.value.forEach((slide, index) => {
|
||||
slide.style.transform = `translateX(${
|
||||
100 * (index - currentSlide.value)
|
||||
}%)`;
|
||||
});
|
||||
if (slideElements.value != null) {
|
||||
slideElements.value.forEach((slide, index) => {
|
||||
(slide as HTMLElement).style.transform = `translateX(${
|
||||
100 * (index - currentSlide.value)
|
||||
}%)`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the carousel slider.
|
||||
*/
|
||||
const startAutoSlide = () => {
|
||||
if (intervalRef == null) {
|
||||
intervalRef = window.setInterval(() => {
|
||||
handleSlideNext();
|
||||
handleClickSlideNext();
|
||||
}, 7000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop the carousel slider.
|
||||
*/
|
||||
const stopAutoSlide = () => {
|
||||
if (intervalRef != null) {
|
||||
window.clearInterval(intervalRef);
|
||||
@@ -109,39 +154,60 @@ const stopAutoSlide = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect the mutation observer to the slider.
|
||||
*/
|
||||
const connectMutationObserver = () => {
|
||||
mutationObserver.value = new MutationObserver(handleUpdate);
|
||||
if (slides.value != null) {
|
||||
mutationObserver.value = new MutationObserver(handleCarouselUpdate);
|
||||
|
||||
mutationObserver.value.observe(slides.value, {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
});
|
||||
mutationObserver.value.observe(slides.value, {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Disconnect the mutation observer from the slider.
|
||||
*/
|
||||
const disconnectMutationObserver = () => {
|
||||
mutationObserver.value.disconnect();
|
||||
if (mutationObserver.value) {
|
||||
mutationObserver.value.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
connectMutationObserver();
|
||||
handleCarouselUpdate();
|
||||
startAutoSlide();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoSlide();
|
||||
disconnectMutationObserver();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.carousel {
|
||||
.sm-carousel {
|
||||
position: relative;
|
||||
height: 28rem;
|
||||
background: #eee;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
.carousel-slide-prev,
|
||||
.carousel-slide-next,
|
||||
.carousel-slide-indicators {
|
||||
.sm-carousel-slide-prev,
|
||||
.sm-carousel-slide-next,
|
||||
.sm-carousel-slide-indicators {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-slide-prev,
|
||||
.carousel-slide-next {
|
||||
.sm-carousel-slide-prev,
|
||||
.sm-carousel-slide-next {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
font-size: 300%;
|
||||
@@ -153,7 +219,7 @@ const disconnectMutationObserver = () => {
|
||||
transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out;
|
||||
opacity: 0.75;
|
||||
|
||||
svg {
|
||||
ion-icon {
|
||||
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;
|
||||
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
|
||||
}
|
||||
|
||||
.carousel-slide-next {
|
||||
.sm-carousel-slide-next {
|
||||
right: 1rem;
|
||||
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
|
||||
}
|
||||
|
||||
.carousel-slide-indicators {
|
||||
.sm-carousel-slide-indicators {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -184,7 +250,7 @@ const disconnectMutationObserver = () => {
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
|
||||
.carousel-slide-indicator-item {
|
||||
.sm-carousel-slide-indicator-item {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border: 1px solid white;
|
||||
@@ -203,9 +269,9 @@ const disconnectMutationObserver = () => {
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 400px) {
|
||||
.carousel {
|
||||
.carousel-slide-prev,
|
||||
.carousel-slide-next {
|
||||
.sm-carousel {
|
||||
.sm-carousel-slide-prev,
|
||||
.sm-carousel-slide-next {
|
||||
font-size: 150%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
<template>
|
||||
<div
|
||||
class="carousel-slide"
|
||||
class="sm-carousel-slide"
|
||||
: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 />
|
||||
</div>
|
||||
<div v-else class="carousel-slide-body">
|
||||
<div class="carousel-slide-content">
|
||||
<div class="carousel-slide-content-inner">
|
||||
<div v-else class="sm-carousel-slide-body">
|
||||
<div class="sm-carousel-slide-content">
|
||||
<div class="sm-carousel-slide-content-inner">
|
||||
<h3>{{ title }}</h3>
|
||||
<p v-if="content">{{ content }}</p>
|
||||
<div class="carousel-slide-body-buttons">
|
||||
<SMButton v-if="url" :to="url" :label="cta" />
|
||||
<div class="sm-carousel-slide-body-buttons">
|
||||
<SMButton
|
||||
v-if="url"
|
||||
:to="url"
|
||||
:label="cta"
|
||||
type="secondary-outline" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -22,7 +26,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { ApiMedia } from "../helpers/api.types";
|
||||
import { MediaResponse } from "../helpers/api.types";
|
||||
import { imageLoad } from "../helpers/image";
|
||||
import SMButton from "./SMButton.vue";
|
||||
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||
@@ -57,25 +61,32 @@ const props = defineProps({
|
||||
|
||||
let imageUrl = ref("");
|
||||
|
||||
/**
|
||||
* Load the slider data.
|
||||
*/
|
||||
const handleLoad = () => {
|
||||
imageUrl.value = "";
|
||||
|
||||
api.get(`/media/${props.image}`).then((result) => {
|
||||
const data = result.data as ApiMedia;
|
||||
api.get({ url: "/media/{medium}", params: { medium: props.image } })
|
||||
.then((result) => {
|
||||
const data = result.data as MediaResponse;
|
||||
|
||||
if (data && data.medium) {
|
||||
imageLoad(data.medium.url, (url) => {
|
||||
imageUrl.value = url;
|
||||
});
|
||||
}
|
||||
});
|
||||
if (data && data.medium) {
|
||||
imageLoad(data.medium.url, (url) => {
|
||||
imageUrl.value = url;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
};
|
||||
|
||||
handleLoad();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.carousel-slide {
|
||||
.sm-carousel-slide {
|
||||
position: absolute;
|
||||
transition: all 0.5s;
|
||||
width: 100%;
|
||||
@@ -85,19 +96,14 @@ handleLoad();
|
||||
background-size: cover;
|
||||
overflow: hidden;
|
||||
|
||||
.carousel-slide-loading {
|
||||
.sm-carousel-slide-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
svg {
|
||||
color: rgba(0, 0, 0, 0.1);
|
||||
font-size: 300%;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-slide-body {
|
||||
.sm-carousel-slide-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
@@ -105,7 +111,7 @@ handleLoad();
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
|
||||
.carousel-slide-content {
|
||||
.sm-carousel-slide-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -136,17 +142,15 @@ handleLoad();
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.carousel-slide-body-buttons {
|
||||
.sm-carousel-slide-body-buttons {
|
||||
margin-top: 2rem;
|
||||
text-align: right;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 12px rgba(0, 0, 0, 0.5);
|
||||
background: transparent;
|
||||
.secondary-outline {
|
||||
border-color: #fff;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
|
||||
@@ -1,83 +1,27 @@
|
||||
<template>
|
||||
<div :class="['container', { full: isFull }]" :style="styleObject">
|
||||
<SMLoader :loading="loading">
|
||||
<d-error-forbidden
|
||||
v-if="pageError == 403 || !hasPermission()"></d-error-forbidden>
|
||||
<d-error-internal
|
||||
v-if="pageError >= 500 && hasPermission()"></d-error-internal>
|
||||
<d-error-not-found v-if="pageError == 404 && hasPermission()"
|
||||
>XX</d-error-not-found
|
||||
>
|
||||
<slot
|
||||
v-if="
|
||||
pageError < 300 && hasPermission() && slots.default
|
||||
"></slot>
|
||||
<div
|
||||
v-if="pageError < 300 && hasPermission() && slots.inner"
|
||||
class="container-inner">
|
||||
<slot name="inner"></slot>
|
||||
</div>
|
||||
</SMLoader>
|
||||
<div :class="['sm-container', { full: full }]">
|
||||
<slot v-if="slots.default"></slot>
|
||||
<div v-if="slots.inner" class="sm-container-inner">
|
||||
<slot name="inner"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMLoader from "./SMLoader.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";
|
||||
import { useSlots } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
pageError: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
required: false,
|
||||
},
|
||||
permission: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
defineProps({
|
||||
full: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
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>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
.sm-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
@@ -95,7 +39,7 @@ const isFull = computed(() => {
|
||||
padding-right: 0;
|
||||
max-width: 100%;
|
||||
|
||||
.container-inner {
|
||||
.sm-container-inner {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
width: 100%;
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<div
|
||||
:class="[
|
||||
'sm-dialog',
|
||||
{ 'dialog-narrow': narrow },
|
||||
{ 'dialog-full': full },
|
||||
{ 'dialog-noshadow': noShadow },
|
||||
{ 'sm-dialog-narrow': narrow },
|
||||
{ 'sm-dialog-full': full },
|
||||
{ 'sm-dialog-noshadow': noShadow },
|
||||
]">
|
||||
<transition name="fade">
|
||||
<div v-if="loading" class="dialog-loading-cover">
|
||||
<div class="dialog-loading">
|
||||
<div v-if="loading" class="sm-dialog-loading-cover">
|
||||
<div class="sm-dialog-loading">
|
||||
<SMLoadingIcon />
|
||||
<span>{{ loadingMessage }}</span>
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@ defineProps({
|
||||
min-width: map-get($spacer, 5) * 12;
|
||||
box-shadow: 4px 4px 20px rgba(0, 0, 0, 0.5);
|
||||
|
||||
&.dialog-noshadow {
|
||||
&.sm-dialog-noshadow {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
@@ -70,16 +70,16 @@ defineProps({
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
&.dialog-narrow {
|
||||
&.sm-dialog-narrow {
|
||||
min-width: auto;
|
||||
max-width: map-get($spacer, 5) * 10;
|
||||
}
|
||||
|
||||
&.dialog-full {
|
||||
&.sm-dialog-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-loading-cover {
|
||||
.sm-dialog-loading-cover {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -93,10 +93,11 @@ defineProps({
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
z-index: 19000;
|
||||
|
||||
.dialog-loading {
|
||||
.sm-dialog-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: map-get($spacer, 5) calc(map-get($spacer, 5) * 2);
|
||||
align-items: center;
|
||||
|
||||
border: 1px solid transparent;
|
||||
border-radius: 24px;
|
||||
@@ -119,7 +120,7 @@ defineProps({
|
||||
map-get($spacer, 4);
|
||||
min-width: auto;
|
||||
|
||||
.button {
|
||||
.sm-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div class="sm-editor">
|
||||
<Editor
|
||||
id="tinymce"
|
||||
ref="tinyeditor"
|
||||
v-model="editorContent"
|
||||
model-events="change blur focus"
|
||||
@@ -15,49 +14,44 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import "tinymce/tinymce";
|
||||
import Editor from "@tinymce/tinymce-vue";
|
||||
import "tinymce/themes/silver";
|
||||
import "tinymce/tinymce";
|
||||
|
||||
import "tinymce/icons/default";
|
||||
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/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/insertdatetime";
|
||||
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/autolink";
|
||||
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 { ref, watch, computed } from "vue";
|
||||
import { routes } from "../router";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { MediaCollection, MediaResponse } from "../helpers/api.types";
|
||||
import { routes } from "../router";
|
||||
|
||||
interface PageList {
|
||||
title: string;
|
||||
@@ -173,11 +167,11 @@ const initialContent = computed(() => {
|
||||
|
||||
watch(initialContent, handleInitialContentChange);
|
||||
|
||||
const handleBlur = (event, editor) => {
|
||||
const handleBlur = (event) => {
|
||||
emits("blur", event);
|
||||
};
|
||||
|
||||
const handleFocus = (event, editor) => {
|
||||
const handleFocus = (event) => {
|
||||
emits("focus", event);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<a :href="computedHref" :target="props.target" rel="noopener"
|
||||
<a :href="computedUrl" :target="props.target" rel="noopener"
|
||||
><slot></slot
|
||||
></a>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// import axios from 'axios'
|
||||
import { computed } from "vue";
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
|
||||
@@ -16,43 +15,28 @@ const props = defineProps({
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: "_self",
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
if (url.pathname.startsWith("/api/") && userStore.token) {
|
||||
return props.href + "?token=" + encodeURIComponent(userStore.token);
|
||||
const path = url.pathname;
|
||||
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;
|
||||
});
|
||||
|
||||
// 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>
|
||||
|
||||
@@ -24,6 +24,9 @@ const props = defineProps({
|
||||
});
|
||||
const emits = defineEmits(["submit"]);
|
||||
|
||||
/**
|
||||
* Handle the user submitting the form.
|
||||
*/
|
||||
const handleSubmit = function () {
|
||||
if (props.modelValue.validate()) {
|
||||
emits("submit");
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<component :is="parsedContent"></component>
|
||||
<component :is="computedContent"></component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from "dompurify";
|
||||
import { computed } from "vue";
|
||||
import "../../../import-meta";
|
||||
|
||||
const props = defineProps({
|
||||
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 = "";
|
||||
|
||||
const regex = new RegExp(
|
||||
`<a ([^>]*?)href="${import.meta.env.APP_URL}(.*?>.*?)</a>`,
|
||||
"ig"
|
||||
);
|
||||
|
||||
html = props.html.replace(regex, '<router-link $1to="$2</router-link>');
|
||||
html = DOMPurify.sanitize(html);
|
||||
|
||||
return {
|
||||
template: `<div class="content">${html}</div>`,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<template>
|
||||
<div class="heading">
|
||||
<router-link
|
||||
v-if="back != ''"
|
||||
:to="{ name: back }"
|
||||
class="heading-back">
|
||||
<div class="sm-heading">
|
||||
<router-link v-if="back != ''" :to="{ name: back }" class="sm-back">
|
||||
<ion-icon name="arrow-back-outline" />{{ backLabel }}
|
||||
</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" />
|
||||
</router-link>
|
||||
<span v-if="closeBack" class="close" @click="handleBack">
|
||||
<span v-if="closeBack" class="sm-close" @click="handleBack">
|
||||
<ion-icon name="close-outline" />
|
||||
</span>
|
||||
<h1>{{ heading }}</h1>
|
||||
@@ -50,20 +47,16 @@ const handleBack = () => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.heading {
|
||||
.sm-heading {
|
||||
position: relative;
|
||||
|
||||
.heading-back {
|
||||
.sm-back {
|
||||
position: absolute;
|
||||
padding-top: 2rem;
|
||||
font-size: 80%;
|
||||
|
||||
svg {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
.sm-close {
|
||||
right: -10px;
|
||||
top: -10px;
|
||||
position: absolute;
|
||||
@@ -76,10 +69,8 @@ const handleBack = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// @media screen and (max-width: 768px) {
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
.heading .close {
|
||||
.sm-heading .sm-close {
|
||||
right: 0;
|
||||
top: -20px;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
class="file"
|
||||
:accept="props.accept"
|
||||
@change="handleChange" />
|
||||
<label class="button" for="file">Select file</label>
|
||||
<label class="sm-button" for="file">Select file</label>
|
||||
<div class="file-name">
|
||||
{{ modelValue?.name ? modelValue.name : modelValue }}
|
||||
</div>
|
||||
@@ -68,7 +68,11 @@
|
||||
<img v-if="mediaUrl.length > 0" :src="mediaUrl" />
|
||||
<ion-icon v-else name="image-outline" />
|
||||
</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 v-if="slots.default || feedbackInvalid" class="sm-input-help">
|
||||
<span v-if="feedbackInvalid" class="sm-input-invalid">{{
|
||||
@@ -82,7 +86,7 @@
|
||||
</template>
|
||||
|
||||
<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 { toTitleCase } from "../helpers/string";
|
||||
import { isEmpty } from "../helpers/utils";
|
||||
@@ -141,7 +145,7 @@ const objControl =
|
||||
: !isEmpty(objForm) &&
|
||||
typeof props.control == "string" &&
|
||||
props.control != ""
|
||||
? objForm[props.control]
|
||||
? objForm.controls[props.control]
|
||||
: null;
|
||||
|
||||
const label = ref(props.label);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
border-radius: 50%;
|
||||
background: #000;
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, 1);
|
||||
}
|
||||
div:nth-child(1) {
|
||||
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>
|
||||
<div class="sm-message message-outer">
|
||||
<div :class="['message', type]">
|
||||
<div class="sm-message-container">
|
||||
<div :class="['sm-message', type]">
|
||||
<ion-icon v-if="icon" :name="icon"></ion-icon>
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
@@ -25,52 +25,50 @@ defineProps({
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-message {
|
||||
&.message-outer {
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
.sm-message-container {
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
|
||||
.message {
|
||||
display: inline-flex;
|
||||
padding: map-get($spacer, 2) map-get($spacer, 3);
|
||||
margin-bottom: map-get($spacer, 4);
|
||||
text-align: center;
|
||||
font-size: 90%;
|
||||
word-break: break-word;
|
||||
.sm-message {
|
||||
display: inline-flex;
|
||||
padding: map-get($spacer, 2) map-get($spacer, 3);
|
||||
margin-bottom: map-get($spacer, 4);
|
||||
text-align: center;
|
||||
font-size: 90%;
|
||||
word-break: break-word;
|
||||
|
||||
&.primary {
|
||||
background-color: $primary-color-lighter;
|
||||
color: $primary-color-darker;
|
||||
border: 1px solid $primary-color-lighter;
|
||||
border-radius: 12px;
|
||||
}
|
||||
&.primary {
|
||||
background-color: $primary-color-lighter;
|
||||
color: $primary-color-darker;
|
||||
border: 1px solid $primary-color-lighter;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: $success-color-lighter;
|
||||
color: $success-color-darker;
|
||||
border: 1px solid $success-color-lighter;
|
||||
border-radius: 12px;
|
||||
}
|
||||
&.success {
|
||||
background-color: $success-color-lighter;
|
||||
color: $success-color-darker;
|
||||
border: 1px solid $success-color-lighter;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: $danger-color-lighter;
|
||||
color: $danger-color-darker;
|
||||
border: 1px solid $danger-color-lighter;
|
||||
border-radius: 12px;
|
||||
}
|
||||
&.error {
|
||||
background-color: $danger-color-lighter;
|
||||
color: $danger-color-darker;
|
||||
border: 1px solid $danger-color-lighter;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
height: 1.3em;
|
||||
width: 1.3em;
|
||||
margin-right: map-get($spacer, 1);
|
||||
}
|
||||
ion-icon {
|
||||
height: 1.3em;
|
||||
width: 1.3em;
|
||||
margin-right: map-get($spacer, 1);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="modal">
|
||||
<div class="sm-modal">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<SMContainer
|
||||
:full="true"
|
||||
:class="['sm-navbar', { showDropdown: showToggle }]"
|
||||
@click="handleHideMenu">
|
||||
:class="['sm-navbar', { 'sm-show-dropdown': showToggle }]"
|
||||
@click="handleClickHideMenu">
|
||||
<template #inner>
|
||||
<div class="navbar-container">
|
||||
<router-link :to="{ name: 'home' }" class="brand"></router-link>
|
||||
<ul class="navmenu flex-fill">
|
||||
<div class="sm-navbar-container">
|
||||
<router-link
|
||||
:to="{ name: 'home' }"
|
||||
class="sm-brand"></router-link>
|
||||
<ul class="sm-navmenu flex-fill">
|
||||
<template v-for="item in menuItems">
|
||||
<li
|
||||
v-if="
|
||||
@@ -22,21 +24,23 @@
|
||||
</ul>
|
||||
<SMButton
|
||||
:to="{ name: 'workshop-list' }"
|
||||
class="navbar-cta"
|
||||
class="sm-navbar-cta"
|
||||
label="Find a workshop"
|
||||
icon="arrow-forward-outline" />
|
||||
<div class="menuButton" @click.stop="handleToggleMenu">
|
||||
<div
|
||||
class="sm-navbar-toggle-menu"
|
||||
@click.stop="handleClickToggleMenu">
|
||||
<span>Menu</span
|
||||
><ion-icon
|
||||
class="menuButtonIcon"
|
||||
name="reorder-three-outline"></ion-icon>
|
||||
><ion-icon name="reorder-three-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="navbar-dropdown-cover"></div>
|
||||
<ul class="navbar-dropdown">
|
||||
<div class="sm-navbar-dropdown-cover"></div>
|
||||
<ul class="sm-navbar-dropdown">
|
||||
<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>
|
||||
</div>
|
||||
</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;
|
||||
};
|
||||
|
||||
const handleHideMenu = () => {
|
||||
/**
|
||||
* Handle the user clicking an element to hide the dropdown menu.
|
||||
*/
|
||||
const handleClickHideMenu = () => {
|
||||
if (showToggle.value) {
|
||||
showToggle.value = false;
|
||||
}
|
||||
@@ -143,20 +153,20 @@ const handleHideMenu = () => {
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1000;
|
||||
|
||||
&.showDropdown {
|
||||
.navbar-dropdown-cover {
|
||||
&.sm-show-dropdown {
|
||||
.sm-navbar-dropdown-cover {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: visibility 0.3s linear, opacity 0.3s linear;
|
||||
}
|
||||
|
||||
.navbar-dropdown {
|
||||
.sm-navbar-dropdown {
|
||||
margin-top: 0;
|
||||
transition: margin 0.5s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-dropdown-cover {
|
||||
.sm-navbar-dropdown-cover {
|
||||
position: fixed;
|
||||
visibility: hidden;
|
||||
z-index: 2000;
|
||||
@@ -169,7 +179,7 @@ const handleHideMenu = () => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.navbar-dropdown {
|
||||
.sm-navbar-dropdown {
|
||||
position: fixed;
|
||||
z-index: 2001;
|
||||
top: 0;
|
||||
@@ -203,15 +213,12 @@ const handleHideMenu = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.navmenu,
|
||||
.navbar-dropdown {
|
||||
.sm-navmenu,
|
||||
.sm-navbar-dropdown {
|
||||
padding-top: map-get($spacer, 4);
|
||||
|
||||
li {
|
||||
// display: flex;
|
||||
// width: 100%;
|
||||
margin: 0 0.75rem;
|
||||
// justify-content: center;
|
||||
|
||||
a {
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
@@ -225,32 +232,33 @@ const handleHideMenu = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.menuClose ion-icon {
|
||||
.sm-navbar-close-menu ion-icon {
|
||||
cursor: pointer;
|
||||
font-size: map-get($spacer, 4);
|
||||
padding-left: map-get($spacer, 1);
|
||||
|
||||
&:hover {
|
||||
color: $danger-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
.sm-navbar-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
|
||||
.brand {
|
||||
.sm-brand {
|
||||
display: inline-block;
|
||||
background-image: url("/img/logo.png");
|
||||
background-position: left top;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
// width: 16.5rem;
|
||||
// height: 3rem;
|
||||
width: 13.5rem;
|
||||
height: 2rem;
|
||||
// margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.navmenu {
|
||||
.sm-navmenu {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
@@ -258,9 +266,8 @@ const handleHideMenu = () => {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
.sm-navbar-toggle-menu {
|
||||
cursor: pointer;
|
||||
// display: none;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
margin-left: 2rem;
|
||||
@@ -270,14 +277,14 @@ const handleHideMenu = () => {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menuButtonIcon {
|
||||
ion-icon {
|
||||
margin-left: 0.5rem;
|
||||
font-size: map-get($spacer, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-cta {
|
||||
.sm-navbar-cta {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.6rem 1.1rem;
|
||||
}
|
||||
@@ -285,16 +292,12 @@ const handleHideMenu = () => {
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.sm-navbar .navbar-container {
|
||||
.navmenu li {
|
||||
.sm-navmenu li {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
.sm-navbar-toggle-menu {
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
// display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,13 +311,13 @@ const handleHideMenu = () => {
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
.brand {
|
||||
.sm-brand {
|
||||
width: 13.5rem;
|
||||
height: 2rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.navbar-cta {
|
||||
.sm-navbar-cta {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
@@ -326,18 +329,18 @@ const handleHideMenu = () => {
|
||||
.sm-navbar {
|
||||
height: 4.5rem;
|
||||
|
||||
.navbar-dropdown-cover {
|
||||
.sm-navbar-dropdown-cover {
|
||||
margin-top: 4.5rem;
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
.brand {
|
||||
.sm-navbar-container {
|
||||
.sm-brand {
|
||||
background-image: url("/img/logo-small.png");
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.navbar-cta {
|
||||
.sm-navbar-cta {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
@@ -346,7 +349,7 @@ const handleHideMenu = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
.sm-menuButton {
|
||||
margin-left: 1rem;
|
||||
|
||||
span {
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
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 SMErrorInternal from "./errors/Internal.vue";
|
||||
import SMErrorNotFound from "./errors/NotFound.vue";
|
||||
import SMBreadcrumbs from "../components/SMBreadcrumbs.vue";
|
||||
import SMContainer from "./SMContainer.vue";
|
||||
import SMLoader from "./SMLoader.vue";
|
||||
|
||||
const props = defineProps({
|
||||
pageError: {
|
||||
@@ -66,7 +66,12 @@ if (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 (
|
||||
props.permission.length == 0 ||
|
||||
userStore.permissions.includes(props.permission)
|
||||
@@ -89,7 +94,6 @@ const hasPermission = () => {
|
||||
margin-bottom: 0;
|
||||
|
||||
.sm-page {
|
||||
// padding-top: 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>
|
||||
<router-link :to="to" class="panel">
|
||||
<div class="panel-image" :style="styleObject">
|
||||
<div v-if="dateInImage && date" class="panel-image-date">
|
||||
<div class="panel-image-date-day">
|
||||
<router-link :to="to" class="sm-panel">
|
||||
<div v-if="image" class="sm-panel-image" :style="styleObject">
|
||||
<div v-if="dateInImage && date" class="sm-panel-image-date">
|
||||
<div class="sm-panel-image-date-day">
|
||||
{{ new SMDate(date, { format: "yMd" }).format("dd") }}
|
||||
</div>
|
||||
<div class="panel-image-date-month">
|
||||
<div class="sm-panel-image-date-month">
|
||||
{{ new SMDate(date, { format: "yMd" }).format("MMM") }}
|
||||
</div>
|
||||
</div>
|
||||
<ion-icon
|
||||
v-if="hideImageLoader == false"
|
||||
class="panel-image-loader"
|
||||
v-if="imageUrl.length == 0"
|
||||
class="sm-panel-image-loader"
|
||||
name="image-outline" />
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<h3 class="panel-title">{{ title }}</h3>
|
||||
<div v-if="showDate && date" class="panel-date">
|
||||
<div class="sm-panel-body">
|
||||
<h3 class="sm-panel-title">{{ title }}</h3>
|
||||
<div v-if="showDate && date" class="sm-panel-date">
|
||||
<ion-icon
|
||||
v-if="showTime == false && endDate.length == 0"
|
||||
name="calendar-outline" />
|
||||
<ion-icon v-else name="time-outline" />
|
||||
<p>{{ panelDate }}</p>
|
||||
<p>{{ computedDate }}</p>
|
||||
</div>
|
||||
<div v-if="location" class="panel-location">
|
||||
<div v-if="location" class="sm-panel-location">
|
||||
<ion-icon name="location-outline" />
|
||||
<p>{{ location }}</p>
|
||||
</div>
|
||||
<div v-if="content" class="panel-content">{{ panelContent }}</div>
|
||||
<div v-if="button.length > 0" class="panel-button">
|
||||
<div v-if="content" class="sm-panel-content">
|
||||
{{ computedContent }}
|
||||
</div>
|
||||
<div v-if="button.length > 0" class="sm-panel-button">
|
||||
<SMButton :to="to" :type="buttonType" :label="button" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,13 +38,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed, ref, reactive, watch } from "vue";
|
||||
import { isUUID } from "../helpers/uuid";
|
||||
import { excerpt, replaceHtmlEntites, stripHtmlTags } from "../helpers/string";
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { imageLoad } from "../helpers/image";
|
||||
import { ApiMedia } from "../helpers/api.types";
|
||||
import { MediaResponse } from "../helpers/api.types";
|
||||
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";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -118,7 +120,10 @@ const props = defineProps({
|
||||
let styleObject = reactive({});
|
||||
let imageUrl = ref("");
|
||||
|
||||
const panelDate = computed(() => {
|
||||
/**
|
||||
* Return a human readable date based on props.date and props.endDate.
|
||||
*/
|
||||
const computedDate = computed(() => {
|
||||
let str = "";
|
||||
|
||||
if (props.date.length > 0) {
|
||||
@@ -149,29 +154,28 @@ const panelDate = computed(() => {
|
||||
return str;
|
||||
});
|
||||
|
||||
const panelContent = computed(() => {
|
||||
/**
|
||||
* Return the content string cleaned from HTML.
|
||||
*/
|
||||
const computedContent = computed(() => {
|
||||
return excerpt(replaceHtmlEntites(stripHtmlTags(props.content)), 200);
|
||||
});
|
||||
|
||||
const hideImageLoader = computed(() => {
|
||||
return (
|
||||
imageUrl.value &&
|
||||
imageUrl.value.length > 0 &&
|
||||
isUUID(imageUrl.value) == false
|
||||
);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.image && props.image.length > 0 && isUUID(props.image)) {
|
||||
api.get(`/media/${props.image}`).then((result) => {
|
||||
const data = result.data as ApiMedia;
|
||||
api.get({ url: "/media/{medium}", params: { medium: props.image } })
|
||||
.then((result) => {
|
||||
const data = result.data as MediaResponse;
|
||||
|
||||
if (data && data.medium) {
|
||||
imageLoad(data.medium.url, (url) => {
|
||||
imageUrl.value = url;
|
||||
});
|
||||
}
|
||||
});
|
||||
if (data && data.medium) {
|
||||
imageLoad(data.medium.url, (url) => {
|
||||
imageUrl.value = url;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -184,7 +188,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.panel {
|
||||
.sm-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid $border-color;
|
||||
@@ -203,7 +207,7 @@ watch(
|
||||
box-shadow: 0 0 14px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.panel-image {
|
||||
.sm-panel-image {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -216,12 +220,12 @@ watch(
|
||||
border-top-right-radius: 12px;
|
||||
background-color: #eee;
|
||||
|
||||
.panel-image-loader {
|
||||
.sm-panel-image-loader {
|
||||
font-size: 5rem;
|
||||
color: $secondary-color;
|
||||
}
|
||||
|
||||
.panel-image-date {
|
||||
.sm-panel-image-date {
|
||||
background-color: #fff;
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: center;
|
||||
@@ -232,19 +236,19 @@ watch(
|
||||
box-shadow: 4px 4px 15px rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
|
||||
.panel-image-date-day {
|
||||
.sm-panel-image-date-day {
|
||||
font-weight: bold;
|
||||
font-size: 130%;
|
||||
}
|
||||
|
||||
.panel-image-date-month {
|
||||
.sm-panel-image-date-month {
|
||||
text-transform: uppercase;
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
.sm-panel-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
@@ -252,12 +256,12 @@ watch(
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
.sm-panel-title {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.panel-date,
|
||||
.panel-location {
|
||||
.sm-panel-date,
|
||||
.sm-panel-location {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: top;
|
||||
@@ -279,17 +283,10 @@ watch(
|
||||
}
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
.sm-panel-content {
|
||||
margin-top: 1rem;
|
||||
line-height: 130%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.panel-button {
|
||||
.button {
|
||||
display: block;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="panel-list">
|
||||
<div v-if="loading" class="panel-list-loading">
|
||||
<div class="sm-panel-list">
|
||||
<div v-if="loading" class="sm-panel-list-loading">
|
||||
<SMLoadingIcon />
|
||||
</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" />
|
||||
<p>{{ notFoundText }}</p>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@ defineProps({
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.panel-list {
|
||||
.sm-panel-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
@@ -43,17 +43,13 @@ defineProps({
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
.panel-list-loading {
|
||||
.sm-panel-list-loading {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
font-size: 500%;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-list-not-found {
|
||||
.sm-panel-list-not-found {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-show="label == selectedLabel" class="tab-content">
|
||||
<div v-show="label == selectedLabel" class="sm-tab-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -7,7 +7,7 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -18,7 +18,7 @@ const selectedLabel = inject("selectedLabel");
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.tab-content {
|
||||
.sm-tab-content {
|
||||
padding: map-get($spacer, 3);
|
||||
background-color: #fff;
|
||||
border: 1px solid $border-color;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="tab-group">
|
||||
<ul class="tab-header">
|
||||
<div class="sm-tab-group">
|
||||
<ul class="sm-tab-header">
|
||||
<li
|
||||
v-for="label in tabLabels"
|
||||
:key="label"
|
||||
:class="['tab-item', { selected: selectedLabel == label }]"
|
||||
:class="['sm-tab-item', { selected: selectedLabel == label }]"
|
||||
@click="selectedLabel = label">
|
||||
{{ label }}
|
||||
</li>
|
||||
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, useSlots, provide } from "vue";
|
||||
import { provide, ref, useSlots } from "vue";
|
||||
|
||||
const slots = useSlots();
|
||||
const tabLabels = ref(slots.default().map((tab) => tab.props.label));
|
||||
@@ -24,17 +24,17 @@ provide("selectedLabel", selectedLabel);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.tab-group {
|
||||
.sm-tab-group {
|
||||
margin-bottom: map-get($spacer, 4);
|
||||
|
||||
.tab-header {
|
||||
.sm-tab-header {
|
||||
// border-bottom: 1px solid $border-color;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
.sm-tab-item {
|
||||
display: inline-block;
|
||||
padding: map-get($spacer, 2) map-get($spacer, 3);
|
||||
border: 1px solid transparent;
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<template>
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-column toolbar-column-left">
|
||||
<div class="sm-toolbar">
|
||||
<div class="sm-toolbar-column sm-toolbar-column-left">
|
||||
<slot name="left"></slot>
|
||||
</div>
|
||||
<div class="toolbar-column toolbar-column-right">
|
||||
<div class="sm-toolbar-column sm-toolbar-column-right">
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.toolbar {
|
||||
.sm-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: map-get($spacer, 2);
|
||||
|
||||
.toolbar-column {
|
||||
.sm-toolbar-column {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
&.toolbar-column-left {
|
||||
&.sm-toolbar-column-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
// }
|
||||
// }
|
||||
|
||||
&.toolbar-column-right {
|
||||
&.sm-toolbar-column-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
// }
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
<SMModal>
|
||||
<SMDialog>
|
||||
<h1>{{ props.title }}</h1>
|
||||
<p v-html="sanitizedHtml"></p>
|
||||
<p v-html="computedSanitizedText"></p>
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<SMButton
|
||||
:type="props.cancel.type"
|
||||
:label="props.cancel.label"
|
||||
@click="handleCancel()" />
|
||||
@click="handleClickCancel()" />
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton
|
||||
:type="props.confirm.type"
|
||||
:label="props.confirm.label"
|
||||
@click="handleConfirm()" />
|
||||
@click="handleClickConfirm()" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</SMDialog>
|
||||
@@ -22,13 +22,14 @@
|
||||
</template>
|
||||
|
||||
<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 { useApplicationStore } from "../../store/ApplicationStore";
|
||||
import SMButton from "../SMButton.vue";
|
||||
import SMDialog from "../SMDialog.vue";
|
||||
import SMFormFooter from "../SMFormFooter.vue";
|
||||
import SMModal from "../SMModal.vue";
|
||||
import SMDialog from "../SMDialog.vue";
|
||||
// import sanitizeHtml from "sanitize-html";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
@@ -59,30 +60,52 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const handleCancel = () => {
|
||||
const applicationStore = useApplicationStore();
|
||||
|
||||
/**
|
||||
* Handle the user clicking the cancel button.
|
||||
*/
|
||||
const handleClickCancel = () => {
|
||||
closeDialog(false);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
/**
|
||||
* Handle the user clicking the confirm button.
|
||||
*/
|
||||
const handleClickConfirm = () => {
|
||||
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") {
|
||||
handleCancel();
|
||||
handleClickCancel();
|
||||
return true;
|
||||
} else if (event.key === "Enter") {
|
||||
handleConfirm();
|
||||
handleClickConfirm();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("keyup", eventKeyUp);
|
||||
applicationStore.addKeyUpListener(eventKeyUp);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("keyup", eventKeyUp);
|
||||
applicationStore.removeKeyUpListener(eventKeyUp);
|
||||
});
|
||||
|
||||
// const sanitizedHtml = sanitizeHtml(props.text);
|
||||
const sanitizedHtml = props.text;
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<SMDialog
|
||||
:loading="dialogLoading"
|
||||
full
|
||||
:loading_message="dialogLoadingMessage"
|
||||
:loading-message="dialogLoadingMessage"
|
||||
class="sm-dialog-media">
|
||||
<h1>Insert Media</h1>
|
||||
<SMMessage
|
||||
@@ -101,18 +101,19 @@
|
||||
</template>
|
||||
|
||||
<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 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 { Media, MediaCollection, MediaResponse } from "../../helpers/api.types";
|
||||
import { bytesReadable } from "../../helpers/types";
|
||||
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 SMMessage from "../SMMessage.vue";
|
||||
import SMModal from "../SMModal.vue";
|
||||
|
||||
const props = defineProps({
|
||||
mime: {
|
||||
@@ -187,6 +188,8 @@ const selected = ref("");
|
||||
*/
|
||||
const perPage = ref(12);
|
||||
|
||||
const applicationStore = useApplicationStore();
|
||||
|
||||
/**
|
||||
* Returns the pagination info
|
||||
*/
|
||||
@@ -247,7 +250,6 @@ const getMediaItem = (item_id: string): Media | null => {
|
||||
let found: Media | null = null;
|
||||
|
||||
mediaItems.value.every((item) => {
|
||||
console.log(item.id, item_id);
|
||||
if (item.id == item_id) {
|
||||
found = item;
|
||||
return false;
|
||||
@@ -272,7 +274,6 @@ const handleClickCancel = () => {
|
||||
const handleClickInsert = () => {
|
||||
if (selected.value !== "") {
|
||||
const mediaItem = getMediaItem(selected.value);
|
||||
console.log(mediaItem, selected.value);
|
||||
if (mediaItem != null) {
|
||||
closeDialog(mediaItem);
|
||||
return;
|
||||
@@ -365,7 +366,6 @@ const handleClickUpload = () => {
|
||||
* Upload the file to the server.
|
||||
*/
|
||||
const handleChangeUpload = async () => {
|
||||
dialogLoading.value = true;
|
||||
formMessage.value = "";
|
||||
|
||||
if (refUploadInput.value != null && refUploadInput.value.files != null) {
|
||||
@@ -374,20 +374,24 @@ const handleChangeUpload = async () => {
|
||||
let submitFormData = new FormData();
|
||||
submitFormData.append("file", firstFile);
|
||||
|
||||
dialogLoading.value = true;
|
||||
dialogLoadingMessage.value = "Uploading file...";
|
||||
|
||||
api.post({
|
||||
url: "/media",
|
||||
body: submitFormData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
// progress: (progressData) =>
|
||||
// (dialogLoadingMessage.value = `Uploading Files ${Math.floor(
|
||||
// (progressData.loaded / progressData.total) * 100
|
||||
// )}%`),
|
||||
progress: (progressData) =>
|
||||
(dialogLoadingMessage.value = `Uploading Files ${Math.floor(
|
||||
(progressData.loaded / progressData.total) * 100
|
||||
)}%`),
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data) {
|
||||
const data = result.data as MediaResponse;
|
||||
|
||||
closeDialog(data.medium);
|
||||
} else {
|
||||
formMessage.value =
|
||||
@@ -398,6 +402,9 @@ const handleChangeUpload = async () => {
|
||||
formMessage.value =
|
||||
error.response?.data?.message ||
|
||||
"An unexpected error occurred";
|
||||
})
|
||||
.finally(() => {
|
||||
dialogLoading.value = false;
|
||||
});
|
||||
} else {
|
||||
formMessage.value = "No file was selected to upload";
|
||||
@@ -405,8 +412,6 @@ const handleChangeUpload = async () => {
|
||||
} else {
|
||||
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.
|
||||
* @returns {boolean} If the event was handled.
|
||||
*/
|
||||
const eventKeyUp = (event: KeyboardEvent) => {
|
||||
const eventKeyUp = (event: KeyboardEvent): boolean => {
|
||||
if (event.key === "Escape") {
|
||||
handleClickCancel();
|
||||
return true;
|
||||
} else if (event.key === "Enter") {
|
||||
if (selected.value.length > 0) {
|
||||
handleClickInsert();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("keyup", eventKeyUp);
|
||||
applicationStore.addKeyUpListener(eventKeyUp);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("keyup", eventKeyUp);
|
||||
applicationStore.removeKeyUpListener(eventKeyUp);
|
||||
});
|
||||
|
||||
watch(page, () => {
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPage from "../SMPage.vue";
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-error.forbidden .image {
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPage from "../SMPage.vue";
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-error.internal .image {
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPage from "../SMPage.vue";
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-error.not-found .image {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
import { useProgressStore } from "../store/ProgressStore";
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
interface ApiProgressData {
|
||||
loaded: number;
|
||||
total: number;
|
||||
@@ -21,6 +21,7 @@ export interface ApiResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
data: unknown;
|
||||
json?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const apiDefaultHeaders = {
|
||||
@@ -87,81 +88,22 @@ export const api = {
|
||||
signal: options.signal || null,
|
||||
};
|
||||
|
||||
let receivedData = false;
|
||||
|
||||
const progressStore = useProgressStore();
|
||||
progressStore.start();
|
||||
|
||||
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) => {
|
||||
let data: string | object = "";
|
||||
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 {
|
||||
data = response.json ? await response.json() : {};
|
||||
}
|
||||
|
||||
const result = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
|
||||
@@ -52,10 +52,10 @@ const defaultFormObject: FormObject = {
|
||||
});
|
||||
|
||||
return valid;
|
||||
}.bind(this),
|
||||
},
|
||||
loading: function (state = true) {
|
||||
this._loading = state;
|
||||
}.bind(this),
|
||||
},
|
||||
message: function (message = "", type = "", icon = "") {
|
||||
this._message = message;
|
||||
|
||||
@@ -65,14 +65,14 @@ const defaultFormObject: FormObject = {
|
||||
if (icon.length > 0) {
|
||||
this._messageIcon = icon;
|
||||
}
|
||||
}.bind(this),
|
||||
},
|
||||
error: function (message = "") {
|
||||
if (message == "") {
|
||||
this.message("");
|
||||
} else {
|
||||
this.message(message, "error", "alert-circle-outline");
|
||||
}
|
||||
}.bind(this),
|
||||
},
|
||||
apiErrors: function (apiResponse: ApiResponse) {
|
||||
let foundKeys = false;
|
||||
|
||||
@@ -102,7 +102,7 @@ const defaultFormObject: FormObject = {
|
||||
"An unknown server error occurred.\nPlease try again later."
|
||||
);
|
||||
}
|
||||
}.bind(this),
|
||||
},
|
||||
controls: {},
|
||||
|
||||
_loading: false,
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
* @param {object|string} objOrString The object or string.
|
||||
* @returns {boolean} If the object or string is empty.
|
||||
*/
|
||||
export const isEmpty = (objOrString: object | string): boolean => {
|
||||
if (objOrString) {
|
||||
if (typeof objOrString === "string") {
|
||||
return objOrString.length == 0;
|
||||
} else if (
|
||||
typeof objOrString == "object" &&
|
||||
Object.keys(objOrString).length === 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
export const isEmpty = (objOrString: unknown): boolean => {
|
||||
if (objOrString == null) {
|
||||
return true;
|
||||
} else if (typeof objOrString === "string") {
|
||||
return objOrString.length == 0;
|
||||
} else if (
|
||||
typeof objOrString == "object" &&
|
||||
Object.keys(objOrString).length === 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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 "normalize.css";
|
||||
import "../css/app.scss";
|
||||
import App from "./views/App.vue";
|
||||
import SMContainer from "./components/SMContainer.vue";
|
||||
import SMRow from "./components/SMRow.vue";
|
||||
import SMColumn from "./components/SMColumn.vue";
|
||||
import { PromiseDialog } from "vue3-promise-dialog";
|
||||
import { createPinia } from "pinia";
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
import { createApp } from "vue";
|
||||
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 App from "./views/App.vue";
|
||||
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
@@ -29,4 +30,5 @@ createApp(App)
|
||||
.component("SMContainer", SMContainer)
|
||||
.component("SMRow", SMRow)
|
||||
.component("SMColumn", SMColumn)
|
||||
.component("SMPage", SMPage)
|
||||
.mount("#app");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createWebHistory, createRouter } from "vue-router";
|
||||
import { useUserStore } from "@/store/UserStore";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { api } from "../helpers/api";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { useProgressStore } from "../store/ProgressStore";
|
||||
import { api } from "../helpers/api";
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
|
||||
@@ -80,18 +80,17 @@ import SMButton from "../components/SMButton.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
|
||||
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 { ref, reactive } from "vue";
|
||||
import { reactive, ref } from "vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
name: FormControl("", And([Required(), Min(4)])),
|
||||
email: FormControl("", And([Required(), Email()])),
|
||||
content: FormControl("", And([Required(), Min(8)])),
|
||||
|
||||
@@ -40,23 +40,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } 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 { reactive, ref } from "vue";
|
||||
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 { Form, FormControl } from "../helpers/form";
|
||||
import { And, Max, Min, Required } from "../helpers/validate";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const formDone = ref(false);
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
code: FormControl("", And([Required(), Min(6), Max(6)])),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -43,22 +43,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { api } from "../helpers/api";
|
||||
import { FormObject, FormControl } from "../helpers/form";
|
||||
import { And, Required, Min } from "../helpers/validate";
|
||||
import { ref, reactive } from "vue";
|
||||
import { reactive, ref } from "vue";
|
||||
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 SMDialog from "../components/SMDialog.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const formDone = ref(false);
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
username: FormControl("", And([Required(), Min(4)])),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -42,24 +42,23 @@
|
||||
</template>
|
||||
|
||||
<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, Email } from "../helpers/validate";
|
||||
import { Form, FormControl } from "../helpers/form";
|
||||
import { And, Email, Required } from "../helpers/validate";
|
||||
|
||||
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 SMPage from "../components/SMPage.vue";
|
||||
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const formDone = ref(false);
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
email: FormControl("", And([Required(), Email()])),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -124,24 +124,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { excerpt } from "../helpers/string";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMCarousel from "../components/SMCarousel.vue";
|
||||
import SMCarouselSlide from "../components/SMCarouselSlide.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import { FormObject, FormControl } from "../helpers/form";
|
||||
import { And, Email, Required } from "../helpers/validate";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
|
||||
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 { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
email: FormControl("", And([Required(), Email()])),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -34,22 +34,22 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from "vue";
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
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 SMFormFooter from "../components/SMFormFooter.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 { Form, FormControl } from "../helpers/form";
|
||||
import { And, Min, Required } from "../helpers/validate";
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
username: FormControl("", And([Required(), Min(4)])),
|
||||
password: FormControl("", Required()),
|
||||
})
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { api } from "../helpers/api";
|
||||
import { ref } from "vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const formLoading = ref(false);
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref } from "vue";
|
||||
import { api } from "../helpers/api";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import SMPanelList from "../components/SMPanelList.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 { PostCollection, Post } from "../helpers/api.types";
|
||||
|
||||
const message = ref("");
|
||||
const loading = ref(true);
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { api } from "../helpers/api";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { api } from "../helpers/api";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
|
||||
import SMAttachments from "../components/SMAttachments.vue";
|
||||
|
||||
const applicationStore = useApplicationStore();
|
||||
|
||||
@@ -70,15 +70,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import { reactive, ref } from "vue";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMDialog from "../components/SMDialog.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 { FormControl, FormObject } from "../helpers/form";
|
||||
import { Form, FormControl } from "../helpers/form";
|
||||
import {
|
||||
And,
|
||||
Custom,
|
||||
@@ -89,7 +89,6 @@ import {
|
||||
Required,
|
||||
} from "../helpers/validate";
|
||||
|
||||
import { debounce } from "../helpers/debounce";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
@@ -140,7 +139,7 @@ const checkUsername = async (value: string): boolean | string => {
|
||||
|
||||
const formDone = ref(false);
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
first_name: FormControl("", Required()),
|
||||
last_name: FormControl("", Required()),
|
||||
email: FormControl("", And([Required(), Email()])),
|
||||
|
||||
@@ -43,22 +43,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import { reactive, ref } from "vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMDialog from "../components/SMDialog.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 { Form, FormControl } from "../helpers/form";
|
||||
import { Required } from "../helpers/validate";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import { FormObject, FormControl } from "../helpers/form";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const formDone = ref(false);
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
username: FormControl("", Required()),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -42,23 +42,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { api } from "../helpers/api";
|
||||
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 { reactive, ref } from "vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
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 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 formDone = ref(false);
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
code: FormControl("", And([Required(), Min(6), Max(6)])),
|
||||
password: FormControl("", And([Required(), Password()])),
|
||||
})
|
||||
|
||||
@@ -75,9 +75,7 @@
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.rules {
|
||||
|
||||
@@ -563,6 +563,4 @@
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
@@ -34,23 +34,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { api } from "../helpers/api";
|
||||
import { FormObject, FormControl } from "../helpers/form";
|
||||
import { And, Email, Required } from "../helpers/validate";
|
||||
import { ref, reactive } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { reactive, ref } from "vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
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 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 formDone = ref(false);
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
email: FormControl("", And([Required(), Email()])),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -50,13 +50,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import SMPanelList from "../components/SMPanelList.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 events = reactive([]);
|
||||
|
||||
@@ -88,15 +88,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { api } from "../helpers/api";
|
||||
import { computed, ref, reactive } from "vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMHTML from "../components/SMHTML.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 { imageLoad } from "../helpers/image";
|
||||
|
||||
|
||||
@@ -19,19 +19,19 @@
|
||||
</template>
|
||||
|
||||
<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 { useRoute } from "vue-router";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMPage from "../../components/SMPage.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 form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
title: FormControl("", And([Required(), Min(2)])),
|
||||
content: FormControl("", Required()),
|
||||
})
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPage from "../../components/SMPage.vue";
|
||||
import { computed } from "vue";
|
||||
import { useUserStore } from "../../store/UserStore";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SMContainer :loading="formLoading" permission="logs/discord">
|
||||
<SMPage :loading="formLoading" permission="logs/discord">
|
||||
<h1>Discord Bot Logs</h1>
|
||||
<SMMessage
|
||||
v-if="message.message"
|
||||
@@ -22,15 +22,15 @@
|
||||
v-if="!message.message"
|
||||
label="Reload Logs"
|
||||
@click="loadData" />
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import { reactive, ref } from "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 SMTab from "../../components/SMTab.vue";
|
||||
import SMTabGroup from "../../components/SMTabGroup.vue";
|
||||
import { api } from "../../helpers/api";
|
||||
|
||||
let formLoading = ref(false);
|
||||
|
||||
@@ -105,27 +105,27 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import {
|
||||
And,
|
||||
Required,
|
||||
Min,
|
||||
DateTime,
|
||||
Custom,
|
||||
Email,
|
||||
Url,
|
||||
} from "../../helpers/validate";
|
||||
import { FormObject, FormControl } from "../../helpers/form";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
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 SMDialog from "../../components/SMDialog.vue";
|
||||
import SMDatepicker from "../../components/SMDatePicker.vue";
|
||||
import SMDialog from "../../components/SMDialog.vue";
|
||||
import SMEditor from "../../components/SMEditor.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";
|
||||
|
||||
const route = useRoute();
|
||||
@@ -169,7 +169,7 @@ const registration_data = computed(() => {
|
||||
});
|
||||
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
title: FormControl("", And([Required(), Min(6)])),
|
||||
location: FormControl("online"),
|
||||
address: FormControl(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SMContainer permission="admin/events">
|
||||
<SMPage permission="admin/events">
|
||||
<SMHeading heading="Events" />
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
@@ -38,23 +38,23 @@
|
||||
<div class="action-wrapper"></div>
|
||||
</template>
|
||||
</EasyDataTable>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<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 { 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 { 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 SMHeading from "../../components/SMHeading.vue";
|
||||
import SMMessage from "../../components/SMMessage.vue";
|
||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const search = ref("");
|
||||
|
||||
@@ -62,18 +62,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMDialog from "../../components/SMDialog.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 { FormObject, FormControl } from "../../helpers/form";
|
||||
import { And, Required, FileSize } from "../../helpers/validate";
|
||||
import { useRoute } from "vue-router";
|
||||
import { Form, FormControl } from "../../helpers/form";
|
||||
import { bytesReadable } from "../../helpers/types";
|
||||
import { useRouter } from "vue-router";
|
||||
import { And, FileSize, Required } from "../../helpers/validate";
|
||||
|
||||
const router = useRouter();
|
||||
const pageError = ref(200);
|
||||
@@ -83,7 +82,7 @@ const route = useRoute();
|
||||
const page_title = route.params.id ? "Edit Media" : "Upload Media";
|
||||
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
file: FormControl("", And([Required(), FileSize(5242880)])),
|
||||
permission: FormControl(),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SMContainer permission="admin/media">
|
||||
<SMPage permission="admin/media">
|
||||
<h1>Media</h1>
|
||||
|
||||
<SMMessage
|
||||
@@ -42,25 +42,25 @@
|
||||
</div>
|
||||
</template>
|
||||
</EasyDataTable>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<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 { 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 { 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 { bytesReadable } from "../../helpers/types";
|
||||
import SMMessage from "../../components/SMMessage.vue";
|
||||
import SMFileLink from "../../components/SMFileLink.vue";
|
||||
import { useUserStore } from "../../store/UserStore";
|
||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const search = ref("");
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMEditor v-model:model-value="form.content.value" />
|
||||
<SMEditor
|
||||
v-model:model-value="form.controls.content.value" />
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
@@ -61,22 +62,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } 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 { reactive, ref } from "vue";
|
||||
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 SMEditor from "../../components/SMEditor.vue";
|
||||
import SMPage from "../../components/SMPage.vue";
|
||||
import SMForm from "../../components/SMForm.vue";
|
||||
import SMFormFooter from "../../components/SMFormFooter.vue";
|
||||
import SMInput from "../../components/SMInput.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 userStore = useUserStore();
|
||||
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 form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
title: FormControl("", And([Required(), Min(8)])),
|
||||
slug: FormControl("", And([Required(), Min(6)])),
|
||||
publish_at: FormControl("", DateTime()),
|
||||
@@ -96,9 +97,9 @@ const form = reactive(
|
||||
);
|
||||
|
||||
const updateSlug = async () => {
|
||||
if (form.slug.value == "" && form.title.value != "") {
|
||||
if (form.controls.slug.value == "" && form.controls.title.value != "") {
|
||||
let idx = 0;
|
||||
let pre_slug = form.title.value
|
||||
let pre_slug = form.controls.title.value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/gim, "-")
|
||||
.replace(/-+/g, "-")
|
||||
@@ -122,8 +123,8 @@ const updateSlug = async () => {
|
||||
idx++;
|
||||
} catch (error) {
|
||||
if (error.status == 404) {
|
||||
if (form.slug.value == "") {
|
||||
form.slug.value = slug;
|
||||
if (form.controls.slug.value == "") {
|
||||
form.controls.slug.value = slug;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,18 +142,18 @@ const loadData = async () => {
|
||||
.then((result) => {
|
||||
const data = result.data as PostResponse;
|
||||
|
||||
form.title.value = data.post.title;
|
||||
form.slug.value = data.post.slug;
|
||||
form.user_id.value = data.post.user_id;
|
||||
form.content.value = data.post.content;
|
||||
form.publish_at.value = data.post.publish_at
|
||||
form.controls.title.value = data.post.title;
|
||||
form.controls.slug.value = data.post.slug;
|
||||
form.controls.user_id.value = data.post.user_id;
|
||||
form.controls.content.value = data.post.content;
|
||||
form.controls.publish_at.value = data.post.publish_at
|
||||
? new SMDate(data.post.publish_at, {
|
||||
format: "yMd",
|
||||
utc: true,
|
||||
}).format("dd/MM/yyyy HH:mm")
|
||||
: "";
|
||||
form.content.value = data.post.content;
|
||||
form.hero.value = data.post.hero;
|
||||
form.controls.content.value = data.post.content;
|
||||
form.controls.hero.value = data.post.hero;
|
||||
})
|
||||
.catch((error) => {
|
||||
pageError.value =
|
||||
@@ -166,15 +167,15 @@ const loadData = async () => {
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
let data = {
|
||||
title: form.title.value,
|
||||
slug: form.slug.value,
|
||||
publish_at: new SMDate(form.publish_at.value).format(
|
||||
title: form.controls.title.value,
|
||||
slug: form.controls.slug.value,
|
||||
publish_at: new SMDate(form.controls.publish_at.value).format(
|
||||
"yyyy/MM/dd HH:mm:ss",
|
||||
{ utc: true }
|
||||
),
|
||||
user_id: form.user_id.value,
|
||||
content: form.content.value,
|
||||
hero: form.hero.value,
|
||||
user_id: form.controls.user_id.value,
|
||||
content: form.controls.content.value,
|
||||
hero: form.controls.hero.value,
|
||||
};
|
||||
|
||||
if (route.params.id) {
|
||||
|
||||
@@ -51,20 +51,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, reactive } from "vue";
|
||||
import { SMDate } from "../../helpers/datetime";
|
||||
import { reactive, ref, watch } from "vue";
|
||||
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 { openDialog } from "vue3-promise-dialog";
|
||||
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
import SMToolbar from "../../components/SMToolbar.vue";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMHeading from "../../components/SMHeading.vue";
|
||||
import SMMessage from "../../components/SMMessage.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 search = ref("");
|
||||
|
||||
@@ -30,26 +30,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed } 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 { computed, reactive } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
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 SMForm from "../../components/SMForm.vue";
|
||||
import SMPage from "../../components/SMPage.vue";
|
||||
import SMHeading from "../../components/SMHeading.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 userStore = useUserStore();
|
||||
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
Form({
|
||||
first_name: FormControl("", And([Required()])),
|
||||
last_name: FormControl("", And([Required()])),
|
||||
email: FormControl("", And([Required(), Email()])),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SMContainer permission="admin/users">
|
||||
<SMPage permission="admin/users">
|
||||
<SMHeading heading="Users" />
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
@@ -22,20 +22,20 @@
|
||||
<div class="action-wrapper"></div>
|
||||
</template>
|
||||
</EasyDataTable>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<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 { 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 { 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 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 laravel from "laravel-vite-plugin";
|
||||
import analyzer from "rollup-plugin-analyzer";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
|
||||
Reference in New Issue
Block a user