lots of changes
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -41,7 +41,7 @@ class UserConductor extends Conductor
|
|||||||
$data = $model->toArray();
|
$data = $model->toArray();
|
||||||
|
|
||||||
if ($user === null || ($user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0)) {
|
if ($user === null || ($user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0)) {
|
||||||
$fields = ['id', 'username'];
|
$fields = ['id', 'username', 'display_name'];
|
||||||
$data = arrayLimitKeys($data, $fields);
|
$data = arrayLimitKeys($data, $fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
public/274jCYSAlHm9P8vTE4CBwZAGgBycxorBHxbyvHNe.jpg
Normal file
BIN
public/274jCYSAlHm9P8vTE4CBwZAGgBycxorBHxbyvHNe.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 406 KiB |
@@ -1,6 +1,5 @@
|
|||||||
@import "variables.scss";
|
@import "variables.scss";
|
||||||
@import "utils.scss";
|
@import "utils.scss";
|
||||||
@import "data-table.scss";
|
|
||||||
@import "tinymce.scss";
|
@import "tinymce.scss";
|
||||||
@import "prism.css";
|
@import "prism.css";
|
||||||
|
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
@import "vue3-easy-data-table/dist/style.css";
|
|
||||||
.vue3-easy-data-table {
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
.vue3-easy-data-table__header tr {
|
|
||||||
background-color: var(--easy-table-header-background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue3-easy-data-table__header th {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue3-easy-data-table__body td {
|
|
||||||
white-space: nowrap;
|
|
||||||
// overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue3-easy-data-table__main {
|
|
||||||
border-top-left-radius: 12px;
|
|
||||||
border-top-right-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue3-easy-data-table__footer {
|
|
||||||
border-bottom-left-radius: 12px;
|
|
||||||
border-bottom-right-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.easy-data-table-cell-center {
|
|
||||||
text-align: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
th.easy-data-table-cell-center .header {
|
|
||||||
justify-content: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-wrapper {
|
|
||||||
a {
|
|
||||||
color: $font-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--easy-table-border: #{1px solid $border-color};
|
|
||||||
--easy-table-row-border: #{1px solid $border-color};
|
|
||||||
|
|
||||||
--easy-table-header-font-size: #{calc($font-size / 1.1)};
|
|
||||||
--easy-table-header-background-color: #{$secondary-background-color};
|
|
||||||
--easy-table-header-item-padding: 20px 20px;
|
|
||||||
|
|
||||||
--easy-table-body-row-font-size: #{calc($font-size / 1.2)};
|
|
||||||
--easy-table-body-item-padding: 20px 20px;
|
|
||||||
--easy-table-body-row-hover-background-color: #e5f3fd;
|
|
||||||
|
|
||||||
--easy-table-footer-font-size: #{calc($font-size / 1.2)};
|
|
||||||
--easy-table-footer-background-color: #{$secondary-background-color};
|
|
||||||
--easy-table-footer-padding: 20px 20px;
|
|
||||||
--easy-table-footer-height: auto;
|
|
||||||
|
|
||||||
--easy-table-message-font-size: #{calc($font-size / 1.2)};
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
@import "@vuepic/vue-datepicker/dist/main.css";
|
|
||||||
|
|
||||||
.dp__menu {
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 1rem;
|
|
||||||
box-shadow: 4px 4px 12px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dp__input {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: map-get($spacer, 2) map-get($spacer, 3) map-get($spacer, 2) #{calc(
|
|
||||||
map-get($spacer, 4) * 1.2
|
|
||||||
)};
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dp__action_row {
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.dp__selection_preview {
|
|
||||||
width: auto;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dp__action_buttons {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: map-get($spacer, 2);
|
|
||||||
|
|
||||||
.dp__action {
|
|
||||||
padding: map-get($spacer, 1) map-get($spacer, 3);
|
|
||||||
font-size: 1rem;
|
|
||||||
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;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
color: $secondary-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dp__select {
|
|
||||||
background-color: $primary-color;
|
|
||||||
border-color: $primary-color;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #fff;
|
|
||||||
color: $primary-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dp__theme_light {
|
|
||||||
--dp-success-color: #{$primary-color};
|
|
||||||
}
|
|
||||||
@@ -78,6 +78,10 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-grow-1 {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-0 {
|
.flex-0 {
|
||||||
flex: 0 !important;
|
flex: 0 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
import { defineStore } from "pinia";
|
|
||||||
import { clamp } from "../helpers/utils";
|
|
||||||
|
|
||||||
export interface ProgressStore {
|
|
||||||
spinner: number;
|
|
||||||
status: number;
|
|
||||||
opacity: number;
|
|
||||||
queue: number;
|
|
||||||
timeoutID: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useProgressStore = defineStore({
|
|
||||||
id: "progress",
|
|
||||||
state: (): ProgressStore => ({
|
|
||||||
spinner: 0,
|
|
||||||
status: 0,
|
|
||||||
opacity: 0,
|
|
||||||
queue: 0,
|
|
||||||
timeoutID: null,
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
start() {
|
|
||||||
if (this.queue == 0 && this.opacity == 0) {
|
|
||||||
this.set(0);
|
|
||||||
|
|
||||||
const work = () => {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
if (this.status < 1) {
|
|
||||||
this._trickle();
|
|
||||||
work();
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
|
|
||||||
work();
|
|
||||||
|
|
||||||
if (this.opacity == 0) {
|
|
||||||
if (this.timeoutID != null) {
|
|
||||||
window.clearTimeout(this.timeoutID);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timeoutID = window.setTimeout(() => {
|
|
||||||
this._show();
|
|
||||||
this.timeoutID = null;
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.spinner == 0) {
|
|
||||||
this.spinner = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
++this.queue;
|
|
||||||
},
|
|
||||||
|
|
||||||
set(number: number) {
|
|
||||||
const n = clamp(number, 0.08, 1);
|
|
||||||
this.status = n;
|
|
||||||
},
|
|
||||||
|
|
||||||
finish() {
|
|
||||||
if (this.queue > 0) {
|
|
||||||
--this.queue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_trickle() {
|
|
||||||
const n = this.status;
|
|
||||||
|
|
||||||
if (this.queue == 0) {
|
|
||||||
if (this.opacity == 0 && this.timeoutID != null) {
|
|
||||||
this._hide();
|
|
||||||
window.clearTimeout(this.timeoutID);
|
|
||||||
this.timeoutID = null;
|
|
||||||
} else if (this.timeoutID == null) {
|
|
||||||
this.timeoutID = window.setTimeout(() => {
|
|
||||||
this.set(1);
|
|
||||||
this.timeoutID = null;
|
|
||||||
|
|
||||||
this.timeoutID = window.setTimeout(() => {
|
|
||||||
this._hide();
|
|
||||||
this.timeoutID = null;
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
this.status = 0;
|
|
||||||
}, 150);
|
|
||||||
}, 500);
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n > 0 && n < 1) {
|
|
||||||
let amount = 0;
|
|
||||||
|
|
||||||
if (n >= 0 && n < 0.2) {
|
|
||||||
amount = 0.1;
|
|
||||||
} else if (n >= 0.2 && n < 0.5) {
|
|
||||||
amount = 0.04;
|
|
||||||
} else if (n >= 0.5 && n < 0.8) {
|
|
||||||
amount = 0.02;
|
|
||||||
} else if (n >= 0.8 && n < 0.99) {
|
|
||||||
amount = 0.005;
|
|
||||||
} else {
|
|
||||||
amount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set(clamp(n + amount, 0, 0.994));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_show() {
|
|
||||||
this.opacity = 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
_hide() {
|
|
||||||
this.opacity = 0;
|
|
||||||
|
|
||||||
if (this.spinner == 1) {
|
|
||||||
this.spinner = 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="sm-carousel"
|
|
||||||
@mouseover="handleMouseOver"
|
|
||||||
@mouseleave="handleMouseLeave">
|
|
||||||
<div ref="slides" class="sm-carousel-slides">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
<div class="sm-carousel-slide-prev" @click="handleClickSlidePrev">
|
|
||||||
<ion-icon name="chevron-back-outline" />
|
|
||||||
</div>
|
|
||||||
<div class="sm-carousel-slide-next" @click="handleClickSlideNext">
|
|
||||||
<ion-icon name="chevron-forward-outline" />
|
|
||||||
</div>
|
|
||||||
<div class="sm-carousel-slide-indicators">
|
|
||||||
<div
|
|
||||||
v-for="(indicator, index) in slideElements"
|
|
||||||
:key="index"
|
|
||||||
:class="[
|
|
||||||
'sm-carousel-slide-indicator-item',
|
|
||||||
{ highlighted: currentSlide == index },
|
|
||||||
]"
|
|
||||||
@click="handleClickIndicator(index)"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, onUnmounted, Ref, ref } from "vue";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The window interval reference to slide the carousel.
|
|
||||||
*/
|
|
||||||
let intervalRef: number | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the user clicking the previous slider indicator.
|
|
||||||
*/
|
|
||||||
const handleClickSlidePrev = () => {
|
|
||||||
if (currentSlide.value == 0) {
|
|
||||||
currentSlide.value = maxSlide.value;
|
|
||||||
} else {
|
|
||||||
currentSlide.value--;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSlidePositions();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the user clicking the next slider indicator.
|
|
||||||
*/
|
|
||||||
const handleClickSlideNext = () => {
|
|
||||||
if (currentSlide.value == maxSlide.value) {
|
|
||||||
currentSlide.value = 0;
|
|
||||||
} else {
|
|
||||||
currentSlide.value++;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSlidePositions();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the user clicking a slider indicator.
|
|
||||||
*
|
|
||||||
* @param {number} index The slide to move to.
|
|
||||||
*/
|
|
||||||
const handleClickIndicator = (index: number) => {
|
|
||||||
currentSlide.value = index;
|
|
||||||
updateSlidePositions();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle slides added/removed from the carousel and update the data/indicators.
|
|
||||||
*/
|
|
||||||
const handleCarouselUpdate = () => {
|
|
||||||
if (slides.value != null) {
|
|
||||||
slideElements.value =
|
|
||||||
slides.value.querySelectorAll(".sm-carousel-slide");
|
|
||||||
maxSlide.value = slideElements.value.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSlidePositions();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the style transform of each slide.
|
|
||||||
*/
|
|
||||||
const updateSlidePositions = () => {
|
|
||||||
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(() => {
|
|
||||||
handleClickSlideNext();
|
|
||||||
}, 7000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the carousel slider.
|
|
||||||
*/
|
|
||||||
const stopAutoSlide = () => {
|
|
||||||
if (intervalRef != null) {
|
|
||||||
window.clearInterval(intervalRef);
|
|
||||||
intervalRef = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect the mutation observer to the slider.
|
|
||||||
*/
|
|
||||||
const connectMutationObserver = () => {
|
|
||||||
if (slides.value != null) {
|
|
||||||
mutationObserver.value = new MutationObserver(handleCarouselUpdate);
|
|
||||||
|
|
||||||
mutationObserver.value.observe(slides.value, {
|
|
||||||
attributes: false,
|
|
||||||
childList: true,
|
|
||||||
characterData: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnect the mutation observer from the slider.
|
|
||||||
*/
|
|
||||||
const disconnectMutationObserver = () => {
|
|
||||||
if (mutationObserver.value) {
|
|
||||||
mutationObserver.value.disconnect();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
connectMutationObserver();
|
|
||||||
handleCarouselUpdate();
|
|
||||||
startAutoSlide();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopAutoSlide();
|
|
||||||
disconnectMutationObserver();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.sm-carousel {
|
|
||||||
position: relative;
|
|
||||||
height: 28rem;
|
|
||||||
background: #eee;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.sm-carousel-slide-prev,
|
|
||||||
.sm-carousel-slide-next,
|
|
||||||
.sm-carousel-slide-indicators {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-carousel-slide-prev,
|
|
||||||
.sm-carousel-slide-next {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
font-size: 300%;
|
|
||||||
-webkit-transform: translateY(-50%) scale(1);
|
|
||||||
transform: translateY(-50%) scale(1);
|
|
||||||
cursor: pointer;
|
|
||||||
color: #fff;
|
|
||||||
transform-origin: center center;
|
|
||||||
transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out;
|
|
||||||
opacity: 0.75;
|
|
||||||
|
|
||||||
ion-icon {
|
|
||||||
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
-webkit-transform: translateY(-50%) scale(1.25);
|
|
||||||
transform: translateY(-50%) scale(1.25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-carousel-slide-prev {
|
|
||||||
left: 1rem;
|
|
||||||
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-carousel-slide-next {
|
|
||||||
right: 1rem;
|
|
||||||
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-carousel-slide-indicators {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
bottom: 0.25rem;
|
|
||||||
width: 100%;
|
|
||||||
height: 2rem;
|
|
||||||
opacity: 0.75;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
|
|
||||||
.sm-carousel-slide-indicator-item {
|
|
||||||
height: 12px;
|
|
||||||
width: 12px;
|
|
||||||
border: 1px solid white;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 80%;
|
|
||||||
margin: 0 calc(#{map-get($spacer, 1)} / 3);
|
|
||||||
color: #fff;
|
|
||||||
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
|
|
||||||
|
|
||||||
&.highlighted {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 400px) {
|
|
||||||
.sm-carousel {
|
|
||||||
.sm-carousel-slide-prev,
|
|
||||||
.sm-carousel-slide-next {
|
|
||||||
font-size: 150%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="sm-carousel-slide" :style="styleObject">
|
|
||||||
<div 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="sm-carousel-slide-body-buttons">
|
|
||||||
<SMButton
|
|
||||||
v-if="url"
|
|
||||||
:to="url"
|
|
||||||
:label="cta"
|
|
||||||
type="secondary-outline" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import SMButton from "./SMButton.vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
type: [String, Object],
|
|
||||||
default: "",
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
cta: {
|
|
||||||
type: String,
|
|
||||||
default: "View",
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Carousel slide styles.
|
|
||||||
*/
|
|
||||||
let styleObject = {
|
|
||||||
backgroundImage: `url('${props.image}')`,
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.sm-carousel-slide {
|
|
||||||
position: absolute;
|
|
||||||
transition: all 0.5s;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.sm-carousel-slide-loading {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-carousel-slide-body {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
.sm-carousel-slide-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba(0, 0, 0, 0.75);
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-width: 800px;
|
|
||||||
padding: 2rem 3rem 1.5rem 3rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
margin-left: 3rem;
|
|
||||||
margin-right: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 200%;
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
text-shadow: 0 0 8px rgba(0, 0, 0, 1);
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
display: inline-block;
|
|
||||||
color: #fff;
|
|
||||||
max-width: 600px;
|
|
||||||
text-shadow: 0 0 8px rgba(0, 0, 0, 1);
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-carousel-slide-body-buttons {
|
|
||||||
margin-top: 2rem;
|
|
||||||
text-align: right;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-outline {
|
|
||||||
border-color: #fff;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
|
||||||
.sm-carousel-slide {
|
|
||||||
.sm-carousel-slide-body {
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
.sm-carousel-slide-content {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 5rem;
|
|
||||||
padding-right: 5rem;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
.sm-carousel-slide-content-inner {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 120%;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-carousel-slide-body-buttons {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 640px) {
|
|
||||||
.sm-carousel-slide .sm-carousel-slide-body {
|
|
||||||
h3,
|
|
||||||
p {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 400px) {
|
|
||||||
.sm-carousel-slide .sm-carousel-slide-body {
|
|
||||||
.sm-carousel-slide-content {
|
|
||||||
padding-left: 3rem;
|
|
||||||
padding-right: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 175%;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="sm-progress-container"
|
|
||||||
:style="{ opacity: `${progressStore.opacity || 0}` }">
|
|
||||||
<div
|
|
||||||
class="sm-progress"
|
|
||||||
:style="{
|
|
||||||
width: `${(progressStore.status || 0) * 100}%`,
|
|
||||||
}"></div>
|
|
||||||
</div>
|
|
||||||
<div class="sm-spinner">
|
|
||||||
<div
|
|
||||||
class="sm-spinner-icon"
|
|
||||||
:style="{ opacity: `${progressStore.spinner || 0}` }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useProgressStore } from "../store/ProgressStore";
|
|
||||||
|
|
||||||
const progressStore = useProgressStore();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.sm-progress-container {
|
|
||||||
position: fixed;
|
|
||||||
background-color: $border-color;
|
|
||||||
height: 2px;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 2000;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
|
|
||||||
.sm-progress {
|
|
||||||
background-color: $primary-color-dark;
|
|
||||||
width: 0%;
|
|
||||||
height: 100%;
|
|
||||||
transition: width 0.2s ease-in-out;
|
|
||||||
box-shadow: 0 0 10px $primary-color-dark, 0 0 4px $primary-color-dark;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-spinner {
|
|
||||||
position: fixed;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
opacity: 0.5;
|
|
||||||
|
|
||||||
.sm-spinner-icon {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
border: solid 2px transparent;
|
|
||||||
border-top-color: #29d;
|
|
||||||
border-left-color: #29d;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
|
|
||||||
-webkit-animation: sm-progress-spinner 500ms linear infinite;
|
|
||||||
animation: sm-progress-spinner 500ms linear infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes sm-progress-spinner {
|
|
||||||
0% {
|
|
||||||
-webkit-transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
-webkit-transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes sm-progress-spinner {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
<template>
|
|
||||||
<SMContainer
|
|
||||||
:class="[
|
|
||||||
'flex-0',
|
|
||||||
'sm-breadcrumbs-container',
|
|
||||||
{ closed: computedRouteCrumbs.length == 0 },
|
|
||||||
]">
|
|
||||||
<ul class="sm-breadcrumbs">
|
|
||||||
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
|
|
||||||
<li
|
|
||||||
v-for="(routeItem, index) of computedRouteCrumbs"
|
|
||||||
:key="routeItem.name">
|
|
||||||
<router-link
|
|
||||||
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, ComputedRef } from "vue";
|
|
||||||
import { RouteRecordRaw, useRoute } from "vue-router";
|
|
||||||
import { routes } from "../router";
|
|
||||||
import { useApplicationStore } from "../store/ApplicationStore";
|
|
||||||
|
|
||||||
const applicationStore = useApplicationStore();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: RouteRecordRaw[]): RouteRecordRaw[] | null => {
|
|
||||||
let found: RouteRecordRaw[] | null = null;
|
|
||||||
let index: RouteRecordRaw | null = null;
|
|
||||||
let child: RouteRecordRaw[] | null = null;
|
|
||||||
|
|
||||||
list.every((entry: RouteRecordRaw) => {
|
|
||||||
if (index == null && "path" in entry && entry.path == "") {
|
|
||||||
index = entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (child == null && entry.children) {
|
|
||||||
child = findMatch(entry.children);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index != null && child != null) {
|
|
||||||
child.unshift(index);
|
|
||||||
found = child;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("name" in entry && entry.name == currentPageName) {
|
|
||||||
found = [entry];
|
|
||||||
if (entry.path == "") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found != null && index != null) {
|
|
||||||
found.unshift(index);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
return found || child;
|
|
||||||
};
|
|
||||||
|
|
||||||
let itemList = findMatch(routes);
|
|
||||||
if (itemList) {
|
|
||||||
if (applicationStore.dynamicTitle.length > 0) {
|
|
||||||
let meta = {};
|
|
||||||
|
|
||||||
if ("meta" in itemList[itemList.length - 1]) {
|
|
||||||
meta = itemList[itemList.length - 1]["meta"];
|
|
||||||
}
|
|
||||||
|
|
||||||
meta["title"] = applicationStore.dynamicTitle;
|
|
||||||
|
|
||||||
itemList[itemList.length - 1]["meta"] = meta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return itemList || [];
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.sm-breadcrumbs-container.closed .sm-breadcrumbs {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0s;
|
|
||||||
transition-delay: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-breadcrumbs {
|
|
||||||
height: 3.25rem;
|
|
||||||
display: flex;
|
|
||||||
max-width: 1200px;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 1rem 0 0;
|
|
||||||
list-style-type: none;
|
|
||||||
font-size: 75%;
|
|
||||||
color: $secondary-color-dark;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.25s ease-in-out;
|
|
||||||
transition-delay: 0.5s;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
span {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:last-child):after {
|
|
||||||
display: inline-block;
|
|
||||||
content: "";
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
border-top: 2px solid #000;
|
|
||||||
border-right: 2px solid #000;
|
|
||||||
margin: 0 0.6rem;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -42,7 +42,7 @@ const slots = useSlots();
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
// align-items: center;
|
align-items: center;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
&.full {
|
&.full {
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div :class="['form-group', { 'has-error': error }]">
|
|
||||||
<label v-if="label" :class="{ required: required }">{{ label }}</label>
|
|
||||||
<datepicker
|
|
||||||
v-model="date"
|
|
||||||
text-input
|
|
||||||
auto-apply
|
|
||||||
:is-24="false"
|
|
||||||
:month-change-on-scroll="false"
|
|
||||||
:preview-format="computedFormat"
|
|
||||||
:format="computedFormat"
|
|
||||||
:placeholder="props.placeholder"
|
|
||||||
:range="range"
|
|
||||||
:enable-time-picker="computedEnableTime"
|
|
||||||
@update:model-value="onUpdate"
|
|
||||||
@blur="onBlur"
|
|
||||||
@change="onChange" />
|
|
||||||
<div class="form-group-error">{{ error }}</div>
|
|
||||||
<div v-if="slots.default" class="form-group-info">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
<div v-if="help" class="form-group-help">
|
|
||||||
<!-- <font-awesome-icon v-if="helpIcon" :icon="helpIcon" /> -->
|
|
||||||
{{ help }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { watch, computed, useSlots, ref } from "vue";
|
|
||||||
import Datepicker from "@vuepic/vue-datepicker";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: [String, Array],
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
required: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: "text",
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
help: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
helpIcon: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
range: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
enableTime: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emits = defineEmits(["update:modelValue", "blur", "change"]);
|
|
||||||
const slots = useSlots();
|
|
||||||
|
|
||||||
const onUpdate = (modelData) => {
|
|
||||||
let emitResult = null;
|
|
||||||
|
|
||||||
if (Array.isArray(modelData) == false) {
|
|
||||||
emitResult = format(modelData, "yyyy-MM-dd HH:mm:ss");
|
|
||||||
} else {
|
|
||||||
emitResult = modelData.map((item, index) => {
|
|
||||||
if (index == 0) {
|
|
||||||
item.setHours(0, 0, 0, 0);
|
|
||||||
} else {
|
|
||||||
item.setHours(23, 59, 59, 999);
|
|
||||||
}
|
|
||||||
|
|
||||||
return format(item, "yyyy-MM-dd HH:mm:ss");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
emits("update:modelValue", emitResult);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onBlur = () => {
|
|
||||||
emits("blur");
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChange = () => {
|
|
||||||
emits("change");
|
|
||||||
};
|
|
||||||
|
|
||||||
let date = ref("");
|
|
||||||
|
|
||||||
const initialContent = computed(() => {
|
|
||||||
return props.modelValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
const computedFormat = computed(() => {
|
|
||||||
return props.enableTime == true && props.range == false
|
|
||||||
? "d/MM/yyyy h:mm aa"
|
|
||||||
: "d/MM/yyyy";
|
|
||||||
});
|
|
||||||
|
|
||||||
const computedEnableTime = computed(() => {
|
|
||||||
return props.enableTime == true && props.range == false;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(initialContent, (newContent) => {
|
|
||||||
if (
|
|
||||||
typeof date.value == "undefined" ||
|
|
||||||
(typeof date.value == "string" && date.value.length == 0)
|
|
||||||
) {
|
|
||||||
date.value = newContent === undefined ? "" : newContent;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
:type="props.type"
|
:type="props.type"
|
||||||
class="input-control"
|
class="input-control"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
v-bind="{ id: id }"
|
v-bind="{ id: id, autofocus: props.autofocus }"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
@focus="handleFocus"
|
@focus="handleFocus"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
@@ -101,6 +101,11 @@ const props = defineProps({
|
|||||||
default: "",
|
default: "",
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
autofocus: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
|
|||||||
26
resources/js/components/SMLoading.vue
Normal file
26
resources/js/components/SMLoading.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="loading-container">
|
||||||
|
<SMLoadingIcon v-bind="{ large: props.large }" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
large: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -21,7 +21,7 @@ const props = defineProps({
|
|||||||
.loading-icon-balls {
|
.loading-icon-balls {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 3em;
|
width: 2.5em;
|
||||||
height: 0.5em;
|
height: 0.5em;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
@@ -34,41 +34,22 @@ const props = defineProps({
|
|||||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||||
}
|
}
|
||||||
div:nth-child(1) {
|
div:nth-child(1) {
|
||||||
left: 0.3em;
|
left: 0em;
|
||||||
animation: sm-loading-icon1 0.6s infinite;
|
animation: sm-loading-icon1 0.6s infinite;
|
||||||
}
|
}
|
||||||
div:nth-child(2) {
|
div:nth-child(2) {
|
||||||
left: 0.3em;
|
left: 0em;
|
||||||
animation: sm-loading-icon2 0.6s infinite;
|
animation: sm-loading-icon2 0.6s infinite;
|
||||||
}
|
}
|
||||||
div:nth-child(3) {
|
div:nth-child(3) {
|
||||||
left: 1.2em;
|
left: 1em;
|
||||||
animation: sm-loading-icon2 0.6s infinite;
|
animation: sm-loading-icon2 0.6s infinite;
|
||||||
}
|
}
|
||||||
div:nth-child(4) {
|
div:nth-child(4) {
|
||||||
left: 2.1em;
|
left: 2em;
|
||||||
animation: sm-loading-icon3 0.6s infinite;
|
animation: sm-loading-icon3 0.6s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.large {
|
|
||||||
div {
|
|
||||||
width: 1.5em;
|
|
||||||
height: 1.5em;
|
|
||||||
}
|
|
||||||
div:nth-child(1) {
|
|
||||||
left: 0em;
|
|
||||||
}
|
|
||||||
div:nth-child(2) {
|
|
||||||
left: 0em;
|
|
||||||
}
|
|
||||||
div:nth-child(3) {
|
|
||||||
left: 3em;
|
|
||||||
}
|
|
||||||
div:nth-child(4) {
|
|
||||||
left: 6em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes sm-loading-icon1 {
|
@keyframes sm-loading-icon1 {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
@@ -86,6 +67,35 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@keyframes sm-loading-icon2 {
|
@keyframes sm-loading-icon2 {
|
||||||
|
0% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(1em, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
width: 7.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
|
||||||
|
div {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div:nth-child(2) {
|
||||||
|
animation: sm-loading-large-icon2 0.6s infinite;
|
||||||
|
}
|
||||||
|
div:nth-child(3) {
|
||||||
|
left: 3em;
|
||||||
|
animation: sm-loading-large-icon2 0.6s infinite;
|
||||||
|
}
|
||||||
|
div:nth-child(4) {
|
||||||
|
left: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sm-loading-large-icon2 {
|
||||||
0% {
|
0% {
|
||||||
transform: translate(0, 0);
|
transform: translate(0, 0);
|
||||||
}
|
}
|
||||||
@@ -93,5 +103,6 @@ const props = defineProps({
|
|||||||
transform: translate(3em, 0);
|
transform: translate(3em, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer
|
<SMContainer
|
||||||
:full="true"
|
:full="true"
|
||||||
:class="['sm-navbar-container', { 'sm-nav-active': showToggle }]"
|
:class="['navbar-container', { 'nav-active': showToggle }]"
|
||||||
@click="handleClickNavBar">
|
@click="handleClickNavBar">
|
||||||
<template #inner>
|
<template #inner>
|
||||||
<nav class="sm-navbar">
|
<nav class="navbar">
|
||||||
<div id="sm-nav-head">
|
<div id="nav-head">
|
||||||
<router-link :to="{ name: 'home' }" id="sm-logo-link">
|
<router-link :to="{ name: 'home' }" id="logo-link">
|
||||||
<img
|
<img
|
||||||
class="sm-nav-logo dark:d-none"
|
class="nav-logo dark:d-none"
|
||||||
src="/assets/logo.png"
|
src="/assets/logo.png"
|
||||||
width="270"
|
width="270"
|
||||||
height="40"
|
height="40"
|
||||||
alt="STEMMechanics" />
|
alt="STEMMechanics" />
|
||||||
<img
|
<img
|
||||||
class="sm-nav-logo light:d-none"
|
class="nav-logo light:d-none"
|
||||||
src="/assets/logo-dark.png"
|
src="/assets/logo-dark.png"
|
||||||
width="270"
|
width="270"
|
||||||
height="40"
|
height="40"
|
||||||
alt="STEMMechanics" />
|
alt="STEMMechanics" />
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="sm-nav-right">
|
<div class="nav-right">
|
||||||
<SMButton
|
<SMButton
|
||||||
type="primary"
|
type="primary"
|
||||||
size="medium"
|
size="medium"
|
||||||
:to="{ name: 'workshops' }"
|
:to="{ name: 'workshops' }"
|
||||||
label="Find Workshops" />
|
label="Find Workshops" />
|
||||||
<label
|
<label
|
||||||
id="sm-nav-toggle"
|
id="nav-toggle"
|
||||||
@click.stop="handleClickToggleMenu"
|
@click.stop="handleClickToggleMenu"
|
||||||
><img
|
><img
|
||||||
src="/assets/hamburger.svg"
|
src="/assets/hamburger.svg"
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
/></label>
|
/></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="sm-nav">
|
<div id="nav">
|
||||||
<ul>
|
<ul>
|
||||||
<template v-for="item in menuItems">
|
<template v-for="item in menuItems">
|
||||||
<li
|
<li
|
||||||
@@ -63,32 +63,21 @@ import SMButton from "../components/SMButton.vue";
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const showToggle = ref(false);
|
const showToggle = ref(false);
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
|
||||||
name: "workshops",
|
|
||||||
label: "Workshops",
|
|
||||||
to: { name: "workshops" },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "blog",
|
name: "blog",
|
||||||
label: "Blog",
|
label: "Blog",
|
||||||
to: { name: "blog" },
|
to: { name: "blog" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "workshops",
|
||||||
|
label: "Workshops",
|
||||||
|
to: { name: "workshops" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "community",
|
name: "community",
|
||||||
label: "Community",
|
label: "Community",
|
||||||
to: { name: "blog" },
|
to: { name: "blog" },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "about",
|
|
||||||
label: "About",
|
|
||||||
to: { name: "blog" },
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// name: "courses",
|
|
||||||
// label: "Courses",
|
|
||||||
// to: "/courses",
|
|
||||||
// icon: "briefcase-outline",
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
name: "contact",
|
name: "contact",
|
||||||
label: "Contact",
|
label: "Contact",
|
||||||
@@ -141,7 +130,7 @@ const handleClickNavBar = () => {
|
|||||||
|
|
||||||
const handleClickBody = (event: MouseEvent) => {
|
const handleClickBody = (event: MouseEvent) => {
|
||||||
const header = document.querySelector("header");
|
const header = document.querySelector("header");
|
||||||
const navbarContainer = document.querySelector(".sm-navbar-container");
|
const navbarContainer = document.querySelector(".navbar-container");
|
||||||
if (
|
if (
|
||||||
!header?.contains(event.target as Node) &&
|
!header?.contains(event.target as Node) &&
|
||||||
!navbarContainer?.contains(event.target as Node)
|
!navbarContainer?.contains(event.target as Node)
|
||||||
@@ -163,26 +152,26 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.page-home {
|
.page-home {
|
||||||
.sm-navbar-container {
|
.navbar-container {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
&:not(.sm-nav-active) {
|
&:not(.nav-active) {
|
||||||
.sm-nav-logo.dark\:d-none {
|
.nav-logo.dark\:d-none {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm-nav-logo.light\:d-none {
|
.nav-logo.light\:d-none {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm-navbar #sm-nav-head #sm-nav-toggle {
|
.navbar #nav-head #nav-toggle {
|
||||||
filter: invert(100%) saturate(0%) brightness(120%);
|
filter: invert(100%) saturate(0%) brightness(120%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm-navbar-container {
|
.navbar-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
-webkit-backdrop-filter: blur(4px);
|
-webkit-backdrop-filter: blur(4px);
|
||||||
@@ -190,32 +179,33 @@ onUnmounted(() => {
|
|||||||
background-color: var(--navbar-color);
|
background-color: var(--navbar-color);
|
||||||
box-shadow: var(--base-shadow);
|
box-shadow: var(--base-shadow);
|
||||||
|
|
||||||
&.sm-nav-active {
|
&.nav-active {
|
||||||
background-color: var(--navbar-color) !important;
|
background-color: var(--navbar-color) !important;
|
||||||
|
|
||||||
#sm-nav {
|
#nav {
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
|
transition: max-height 0.4s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sm-nav-toggle {
|
#nav-toggle {
|
||||||
background-color: hsla(0, 0%, 50%, 0.1);
|
background-color: hsla(0, 0%, 50%, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm-navbar {
|
.navbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sm-nav-head {
|
#nav-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
#sm-logo-link {
|
#logo-link {
|
||||||
padding-right: 18px;
|
padding-right: 18px;
|
||||||
margin-top: -10px;
|
margin-top: -10px;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
@@ -232,7 +222,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm-nav-right {
|
.nav-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
@@ -242,18 +232,18 @@ onUnmounted(() => {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sm-nav-toggle {
|
#nav-toggle {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#sm-nav {
|
#nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
transition: max-height 0.4s linear;
|
transition: max-height 0;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -290,13 +280,13 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.sm-navbar #sm-nav-head #sm-nav-toggle {
|
.navbar #nav-head #nav-toggle {
|
||||||
filter: invert(100%) saturate(0%) brightness(120%);
|
filter: invert(100%) saturate(0%) brightness(120%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 650px) {
|
@media screen and (max-width: 650px) {
|
||||||
.sm-nav-right {
|
.nav-right {
|
||||||
.button {
|
.button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,541 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
'sm-input-group',
|
|
||||||
{
|
|
||||||
'sm-input-active': inputActive,
|
|
||||||
'sm-feedback-invalid': feedbackInvalid,
|
|
||||||
'sm-input-small': small,
|
|
||||||
},
|
|
||||||
computedClassType,
|
|
||||||
]">
|
|
||||||
<label v-if="label">{{ label }}</label>
|
|
||||||
<ion-icon
|
|
||||||
class="sm-invalid-icon"
|
|
||||||
name="alert-circle-outline"></ion-icon>
|
|
||||||
<input
|
|
||||||
v-if="
|
|
||||||
type == 'text' ||
|
|
||||||
type == 'email' ||
|
|
||||||
type == 'password' ||
|
|
||||||
type == 'email' ||
|
|
||||||
type == 'url' ||
|
|
||||||
type == 'daterange' ||
|
|
||||||
type == 'datetime'
|
|
||||||
"
|
|
||||||
:type="type"
|
|
||||||
:value="value"
|
|
||||||
@input="handleInput"
|
|
||||||
@focus="handleFocus"
|
|
||||||
@blur="handleBlur"
|
|
||||||
@keydown="handleKeydown" />
|
|
||||||
<textarea
|
|
||||||
v-else-if="type == 'textarea'"
|
|
||||||
rows="5"
|
|
||||||
:value="value"
|
|
||||||
@input="handleInput"
|
|
||||||
@focus="handleFocus"
|
|
||||||
@blur="handleBlur"
|
|
||||||
@keydown="handleKeydown"></textarea>
|
|
||||||
<div v-else-if="type == 'file'" class="sm-input-file-group">
|
|
||||||
<input
|
|
||||||
id="file"
|
|
||||||
type="file"
|
|
||||||
class="sm-file"
|
|
||||||
:accept="props.accept"
|
|
||||||
@change="handleChange" />
|
|
||||||
<label class="sm-button" for="file">Select file</label>
|
|
||||||
<div class="sm-file-name">
|
|
||||||
{{ modelValue?.name ? modelValue.name : modelValue }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
v-else-if="type == 'select'"
|
|
||||||
:value="value"
|
|
||||||
@input="handleInput"
|
|
||||||
@focus="handleFocus"
|
|
||||||
@blur="handleBlur"
|
|
||||||
@keydown="handleKeydown">
|
|
||||||
<option
|
|
||||||
v-for="(optionValue, key) in options"
|
|
||||||
:key="key"
|
|
||||||
:value="key"
|
|
||||||
:selected="key == value">
|
|
||||||
{{ optionValue }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<div v-else-if="type == 'media'" class="sm-input-media">
|
|
||||||
<div class="sm-input-media-item">
|
|
||||||
<img v-if="mediaUrl.length > 0" :src="mediaUrl" />
|
|
||||||
<ion-icon v-else name="image-outline" />
|
|
||||||
</div>
|
|
||||||
<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">{{
|
|
||||||
feedbackInvalid
|
|
||||||
}}</span>
|
|
||||||
<span v-if="slots.default" class="sm-input-info">
|
|
||||||
<slot></slot>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, inject, ref, useSlots, watch } from "vue";
|
|
||||||
import { openDialog } from "../components/SMDialog";
|
|
||||||
import { api } from "../helpers/api";
|
|
||||||
import { MediaResponse } from "../helpers/api.types";
|
|
||||||
import { toTitleCase } from "../helpers/string";
|
|
||||||
import { isEmpty } from "../helpers/utils";
|
|
||||||
import { isUUID } from "../helpers/uuid";
|
|
||||||
import SMDialogMedia from "../components/dialogs/SMDialogMedia.vue";
|
|
||||||
import { mediaGetVariantUrl } from "../helpers/media";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: "text",
|
|
||||||
},
|
|
||||||
small: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
feedbackInvalid: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
accept: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
control: {
|
|
||||||
type: [String, Object],
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emits = defineEmits(["update:modelValue", "focus", "blur", "keydown"]);
|
|
||||||
const slots = useSlots();
|
|
||||||
const mediaUrl = ref("");
|
|
||||||
|
|
||||||
const objForm = inject("form", props.form);
|
|
||||||
const objControl =
|
|
||||||
typeof props.control == "object"
|
|
||||||
? props.control
|
|
||||||
: !isEmpty(objForm) &&
|
|
||||||
typeof props.control == "string" &&
|
|
||||||
props.control != ""
|
|
||||||
? objForm.controls[props.control]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const label = ref(props.label);
|
|
||||||
const feedbackInvalid = ref(props.feedbackInvalid);
|
|
||||||
const value = ref(props.modelValue);
|
|
||||||
const inputActive = ref(value.value.length > 0 || props.type == "select");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the classname based on type
|
|
||||||
*/
|
|
||||||
const computedClassType = computed(() => {
|
|
||||||
return `sm-input-type-${props.type}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.label,
|
|
||||||
(newValue) => {
|
|
||||||
label.value = newValue;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (objControl) {
|
|
||||||
if (value.value.length > 0) {
|
|
||||||
objControl.value = value.value;
|
|
||||||
} else {
|
|
||||||
value.value = objControl.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (label.value.length == 0 && typeof props.control == "string") {
|
|
||||||
label.value = toTitleCase(props.control);
|
|
||||||
}
|
|
||||||
|
|
||||||
inputActive.value = value.value?.length > 0 || props.type == "select";
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => objControl.validation.result.valid,
|
|
||||||
(newValue) => {
|
|
||||||
feedbackInvalid.value = newValue
|
|
||||||
? ""
|
|
||||||
: objControl.validation.result.invalidMessages[0];
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => objControl.value,
|
|
||||||
(newValue) => {
|
|
||||||
value.value = newValue;
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(newValue) => {
|
|
||||||
value.value = newValue;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.feedbackInvalid,
|
|
||||||
(newValue) => {
|
|
||||||
feedbackInvalid.value = newValue;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => value.value,
|
|
||||||
async (newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
inputActive.value = newValue.length > 0;
|
|
||||||
|
|
||||||
if (props.type == "media") {
|
|
||||||
if (isUUID(newValue)) {
|
|
||||||
try {
|
|
||||||
const result = await api.get({
|
|
||||||
url: "/media/{id}",
|
|
||||||
params: {
|
|
||||||
id: newValue,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = result.data as MediaResponse;
|
|
||||||
|
|
||||||
if (data && data.medium) {
|
|
||||||
mediaUrl.value = mediaGetVariantUrl(
|
|
||||||
data.medium,
|
|
||||||
"small"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChange = (event) => {
|
|
||||||
emits("update:modelValue", event.target.files[0]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInput = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
value.value = target.value;
|
|
||||||
emits("update:modelValue", target.value);
|
|
||||||
|
|
||||||
if (objControl) {
|
|
||||||
objControl.value = target.value;
|
|
||||||
feedbackInvalid.value = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFocus = (event: Event) => {
|
|
||||||
inputActive.value = true;
|
|
||||||
|
|
||||||
if (event instanceof KeyboardEvent) {
|
|
||||||
if (event.key === undefined || event.key === "Tab") {
|
|
||||||
emits("blur", event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emits("focus", event);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBlur = async (event: Event) => {
|
|
||||||
if (objControl) {
|
|
||||||
await objControl.validate();
|
|
||||||
objControl.isValid();
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
|
|
||||||
if (target.value.length == 0) {
|
|
||||||
inputActive.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
emits("blur", event);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeydown = (event: Event) => {
|
|
||||||
emits("keydown", event);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMediaSelect = async (event) => {
|
|
||||||
let result = await openDialog(SMDialogMedia);
|
|
||||||
if (result) {
|
|
||||||
mediaUrl.value = result.url;
|
|
||||||
emits("update:modelValue", result.id);
|
|
||||||
|
|
||||||
if (objControl) {
|
|
||||||
objControl.value = result.id;
|
|
||||||
feedbackInvalid.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.sm-column .sm-input-group {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-input-group {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: map-get($spacer, 4);
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&.sm-input-small {
|
|
||||||
font-size: 80%;
|
|
||||||
|
|
||||||
&.sm-input-active {
|
|
||||||
label {
|
|
||||||
transform: translate(6px, -3px) scale(0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
padding: calc(#{map-get($spacer, 1)} * 1.5) map-get($spacer, 2)
|
|
||||||
calc(#{map-get($spacer, 1)} / 2) map-get($spacer, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
label {
|
|
||||||
padding: map-get($spacer, 1) map-get($spacer, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
padding: calc(#{map-get($spacer, 2)} * 2) map-get($spacer, 3)
|
|
||||||
calc(#{map-get($spacer, 2)} / 2) map-get($spacer, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
padding: calc(#{map-get($spacer, 2)} * 2) map-get($spacer, 3)
|
|
||||||
calc(#{map-get($spacer, 2)} / 2) map-get($spacer, 3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sm-feedback-invalid {
|
|
||||||
input,
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
border: 2px solid $danger-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-invalid-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: $secondary-color-dark;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-invalid-icon {
|
|
||||||
position: absolute;
|
|
||||||
display: none;
|
|
||||||
right: 0;
|
|
||||||
top: 2px;
|
|
||||||
padding: map-get($spacer, 2) map-get($spacer, 3);
|
|
||||||
color: $danger-color;
|
|
||||||
font-size: 120%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sm-input-select {
|
|
||||||
.sm-invalid-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid var(--base-color-darker);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 14px 18px;
|
|
||||||
color: var(--base-color-text);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
background-color: var(--base-color-light);
|
|
||||||
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
padding-right: 2.5rem;
|
|
||||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 0.75rem center;
|
|
||||||
background-size: 24px 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sm-input-type-media {
|
|
||||||
label {
|
|
||||||
position: relative;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-input-help {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sm-feedback-invalid .sm-input-media .sm-input-media-item ion-icon {
|
|
||||||
border: 2px solid $danger-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sm-feedback-invalid .sm-invalid-icon {
|
|
||||||
// position: relative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-input-media {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: map-get($spacer, 2);
|
|
||||||
|
|
||||||
.sm-input-media-item {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-icon {
|
|
||||||
padding: 4rem;
|
|
||||||
font-size: 3rem;
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-input-help {
|
|
||||||
font-size: 75%;
|
|
||||||
margin: 0 map-get($spacer, 1);
|
|
||||||
color: $secondary-color-dark;
|
|
||||||
|
|
||||||
.sm-input-invalid {
|
|
||||||
color: $danger-color;
|
|
||||||
padding-right: map-get($spacer, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-input-file-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
input {
|
|
||||||
opacity: 0;
|
|
||||||
width: 0.1px;
|
|
||||||
height: 0.1px;
|
|
||||||
position: absolute;
|
|
||||||
margin-left: -9999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label.button {
|
|
||||||
margin-right: map-get($spacer, 4);
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
margin: 0;
|
|
||||||
height: 3rem;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm-file-name {
|
|
||||||
display: block;
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
border-top-right-radius: 12px;
|
|
||||||
border-bottom-right-radius: 12px;
|
|
||||||
flex: 1;
|
|
||||||
height: 3rem;
|
|
||||||
background-color: #fff;
|
|
||||||
line-height: 3rem;
|
|
||||||
padding: 0 1rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Directive } from "vue";
|
|
||||||
|
|
||||||
const bodyClass: Directive = {
|
|
||||||
mounted(el, binding) {
|
|
||||||
document.body.classList.add(binding.value as string);
|
|
||||||
},
|
|
||||||
unmounted(el, binding) {
|
|
||||||
document.body.classList.remove(binding.value as string);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default bodyClass;
|
|
||||||
@@ -90,6 +90,7 @@ export interface User {
|
|||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
display_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserResponse {
|
export interface UserResponse {
|
||||||
|
|||||||
@@ -497,6 +497,13 @@ router.afterEach((to, from) => {
|
|||||||
document.body.classList.remove(`page-${from.name}`);
|
document.body.classList.remove(`page-${from.name}`);
|
||||||
}
|
}
|
||||||
document.body.classList.add(`page-${to.name}`);
|
document.body.classList.add(`page-${to.name}`);
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
const autofocusElement = document.querySelector("[autofocus]");
|
||||||
|
if (autofocusElement) {
|
||||||
|
autofocusElement.focus();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMMastHead title="Blog" />
|
<SMMastHead title="Blog" />
|
||||||
<SMContainer>
|
<SMContainer class="flex-grow-1">
|
||||||
<SMInput
|
<SMInput
|
||||||
type="text"
|
type="text"
|
||||||
label="Search articles"
|
label="Search articles"
|
||||||
@@ -14,6 +14,10 @@
|
|||||||
@click="handleClickSearch"
|
@click="handleClickSearch"
|
||||||
/></template>
|
/></template>
|
||||||
</SMInput>
|
</SMInput>
|
||||||
|
<template v-if="pageLoading">
|
||||||
|
<SMLoading large />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<SMPagination
|
<SMPagination
|
||||||
v-if="postsTotal > postsPerPage"
|
v-if="postsTotal > postsPerPage"
|
||||||
v-model="postsPage"
|
v-model="postsPage"
|
||||||
@@ -34,7 +38,7 @@
|
|||||||
)})`,
|
)})`,
|
||||||
}"></div>
|
}"></div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
{{ post.user.username }} -
|
{{ post.user.display_name }} -
|
||||||
{{ computedDate(post.publish_at) }}
|
{{ computedDate(post.publish_at) }}
|
||||||
</div>
|
</div>
|
||||||
<h3 class="title">{{ post.title }}</h3>
|
<h3 class="title">{{ post.title }}</h3>
|
||||||
@@ -43,6 +47,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</SMContainer>
|
</SMContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -57,6 +62,7 @@ import SMMastHead from "../components/SMMastHead.vue";
|
|||||||
import SMInput from "../components/SMInput.vue";
|
import SMInput from "../components/SMInput.vue";
|
||||||
import SMButton from "../components/SMButton.vue";
|
import SMButton from "../components/SMButton.vue";
|
||||||
import { excerpt } from "../helpers/string";
|
import { excerpt } from "../helpers/string";
|
||||||
|
import SMLoading from "../components/SMLoading.vue";
|
||||||
|
|
||||||
const message = ref("");
|
const message = ref("");
|
||||||
const pageLoading = ref(true);
|
const pageLoading = ref(true);
|
||||||
@@ -159,7 +165,7 @@ handleLoad();
|
|||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
word-break: break-all;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template #body>
|
<template #body>
|
||||||
<SMInput control="username">
|
<SMInput control="username" autofocus>
|
||||||
<router-link to="/forgot-username"
|
<router-link to="/forgot-username"
|
||||||
>Forgot username?</router-link
|
>Forgot username?</router-link
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<template #body>
|
<template #body>
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput control="username" />
|
<SMInput control="username" autofocus />
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput
|
<SMInput
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMMastHead title="Workshops" />
|
<SMMastHead title="Workshops" />
|
||||||
<SMContainer>
|
<SMContainer class="flex-grow-1">
|
||||||
<SMToolbar class="align-items-start">
|
<SMToolbar class="align-items-start">
|
||||||
<SMInput
|
<SMInput
|
||||||
v-model="filterKeywords"
|
v-model="filterKeywords"
|
||||||
@@ -32,7 +32,11 @@
|
|||||||
:message="formMessage"
|
:message="formMessage"
|
||||||
class="mt-5" />
|
class="mt-5" />
|
||||||
|
|
||||||
<div v-if="postsTotal > 0" class="events">
|
<template v-if="pageLoading">
|
||||||
|
<SMLoading large />
|
||||||
|
</template>
|
||||||
|
<SMNoItems v-else-if="postsTotal == 0" />
|
||||||
|
<div v-else class="events">
|
||||||
<router-link
|
<router-link
|
||||||
class="event-card"
|
class="event-card"
|
||||||
v-for="event in events"
|
v-for="event in events"
|
||||||
@@ -76,7 +80,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<SMNoItems v-else />
|
|
||||||
</SMContainer>
|
</SMContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -92,6 +95,7 @@ import { SMDate } from "../helpers/datetime";
|
|||||||
import SMMastHead from "../components/SMMastHead.vue";
|
import SMMastHead from "../components/SMMastHead.vue";
|
||||||
import SMContainer from "../components/SMContainer.vue";
|
import SMContainer from "../components/SMContainer.vue";
|
||||||
import SMNoItems from "../components/SMNoItems.vue";
|
import SMNoItems from "../components/SMNoItems.vue";
|
||||||
|
import SMLoading from "../components/SMLoading.vue";
|
||||||
|
|
||||||
interface EventData {
|
interface EventData {
|
||||||
event: Event;
|
event: Event;
|
||||||
@@ -99,7 +103,7 @@ interface EventData {
|
|||||||
bannerType: string;
|
bannerType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loading = ref(true);
|
const pageLoading = ref(true);
|
||||||
let events: Event[] = reactive([]);
|
let events: Event[] = reactive([]);
|
||||||
const dateRangeError = ref("");
|
const dateRangeError = ref("");
|
||||||
|
|
||||||
@@ -166,7 +170,7 @@ const handleLoad = async () => {
|
|||||||
dateRangeError.value = "";
|
dateRangeError.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
pageLoading.value = true;
|
||||||
formMessage.value = "";
|
formMessage.value = "";
|
||||||
events = [];
|
events = [];
|
||||||
|
|
||||||
@@ -244,7 +248,7 @@ const handleLoad = async () => {
|
|||||||
"Could not load any events from the server.";
|
"Could not load any events from the server.";
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
pageLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user