bug fixes and updates
This commit is contained in:
@@ -36,11 +36,16 @@ h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: var(--header-font-family);
|
||||
font-weight: 800;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 42px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 36px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
a,
|
||||
@@ -70,37 +75,6 @@ small,
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--card-color);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 11px 10px -10px rgba(0, 0, 0, 0.2);
|
||||
margin: 48px auto 48px auto;
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
padding: 24px 48px 0;
|
||||
|
||||
p {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 48px 24px 48px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--base-color-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
--base-color-text: rgba(0, 0, 0, 0.8);
|
||||
--base-color-light: #fff;
|
||||
--base-color-dark: #ddd;
|
||||
--base-color-darker: #999;
|
||||
--base-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
||||
|
||||
--default-font-size: 18px;
|
||||
@@ -19,10 +20,16 @@
|
||||
--primary-color-dark: #0e80ce;
|
||||
--primary-color-darker: #095589;
|
||||
|
||||
--danger-color-lighter: #f3c8c8;
|
||||
--danger-color-light: #db5c5a;
|
||||
--danger-color: #c82e2b;
|
||||
--danger-color-dark: #952220;
|
||||
--danger-color-darker: #641715;
|
||||
|
||||
--header-font-family: "Montserrat", "Montserrat override", "Arial",
|
||||
"Helvetica", sans-serif;
|
||||
|
||||
--navbar-color: rgba(255, 255, 255, 0.1);
|
||||
--navbar-color: var(--base-color-light);
|
||||
--navbar-color-dropdown: var(--base-color-light);
|
||||
|
||||
--card-color: var(--base-color-light);
|
||||
@@ -38,7 +45,7 @@
|
||||
--base-color-text: rgba(255, 255, 255, 0.9);
|
||||
--base-color-light: #333;
|
||||
--base-color-dark: #444;
|
||||
--base-color-darker: #555;
|
||||
--base-color-darker: #999;
|
||||
|
||||
--footer-color-text: #999;
|
||||
--footer-color-border: rgba(255, 255, 255, 0.1);
|
||||
@@ -48,6 +55,12 @@
|
||||
--primary-color: #35a5f1;
|
||||
--primary-color-dark: #67bbf4;
|
||||
--primary-color-darker: #cce8fb;
|
||||
|
||||
--danger-color-lighter: #f3c8c8;
|
||||
--danger-color-light: #db5c5a;
|
||||
--danger-color: #db5c5a;
|
||||
--danger-color-dark: #952220;
|
||||
--danger-color-darker: #641715;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
124
resources/depreciated/ProgressStore.ts
Normal file
124
resources/depreciated/ProgressStore.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
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;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
280
resources/depreciated/SMCarousel.vue
Normal file
280
resources/depreciated/SMCarousel.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<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>
|
||||
194
resources/depreciated/SMCarouselSlide.vue
Normal file
194
resources/depreciated/SMCarouselSlide.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<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>
|
||||
86
resources/depreciated/SMProgress.vue
Normal file
86
resources/depreciated/SMProgress.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<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>
|
||||
59
resources/js/components/SMArticleCard.vue
Normal file
59
resources/js/components/SMArticleCard.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<router-link :to="props.to" :class="['sm-article-card', `${props.type}`]">
|
||||
<div
|
||||
class="thumbnail"
|
||||
:style="[{ backgroundImage: `url('${props.image}')` }]"></div>
|
||||
<div class="content">
|
||||
<h3>{{ props.title }}</h3>
|
||||
<p class="excerpt">{{ props.excerpt }}</p>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
to: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
excerpt: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-article-card {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
.thumbnail {
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: 7px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-color: var(--card-background-color);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
&.row {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
369
resources/js/components/SMButton-copy.vue
Normal file
369
resources/js/components/SMButton-copy.vue
Normal file
@@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<button
|
||||
v-if="isEmpty(to)"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'sm-button',
|
||||
classType,
|
||||
{ 'sm-button-small': small },
|
||||
{ 'sm-button-block': block },
|
||||
{ 'sm-dropdown-button': dropdown },
|
||||
]"
|
||||
:type="buttonType"
|
||||
@click="handleClick">
|
||||
<ion-icon
|
||||
v-if="icon && dropdown == null && iconLocation == 'before'"
|
||||
:icon="icon"
|
||||
class="sm-button-icon-before" />
|
||||
<span class="sm-button-label">{{ label }}</span>
|
||||
<ion-icon
|
||||
v-if="icon && dropdown == null && iconLocation == 'after'"
|
||||
:icon="icon"
|
||||
class="sm-button-icon-after" />
|
||||
<ion-icon
|
||||
v-if="dropdown != null"
|
||||
name="caret-down-outline"
|
||||
class="sm-button-icon-dropdown"
|
||||
@click.stop="handleClickToggleDropdown" />
|
||||
<ul
|
||||
v-if="dropdown != null"
|
||||
ref="dropdownMenu"
|
||||
@mouseleave="handleMouseLeave">
|
||||
<li
|
||||
v-for="(dropdownLabel, dropdownItem) in dropdown"
|
||||
:key="dropdownItem"
|
||||
@click.stop="handleClickItem(dropdownItem)">
|
||||
{{ dropdownLabel }}
|
||||
</li>
|
||||
</ul>
|
||||
</button>
|
||||
<a
|
||||
v-else-if="!isEmpty(to) && typeof to == 'string'"
|
||||
:href="to"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'sm-button',
|
||||
classType,
|
||||
{ 'sm-button-small': small },
|
||||
{ 'sm-button-block': block },
|
||||
]"
|
||||
:type="buttonType">
|
||||
<span class="sm-button-label">{{ label }}</span>
|
||||
<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-small': small },
|
||||
{ '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, ref } from "vue";
|
||||
import { isEmpty } from "../helpers/utils";
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: "Button", required: false },
|
||||
type: { type: String, default: "primary", required: false },
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
iconLocation: {
|
||||
type: String,
|
||||
default: "after",
|
||||
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,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
dropdown: {
|
||||
type: Object,
|
||||
default: null,
|
||||
required: false,
|
||||
validator: (prop) => typeof prop === "object" || prop === null,
|
||||
},
|
||||
});
|
||||
|
||||
const buttonType: "submit" | "button" =
|
||||
props.type == "submit" ? "submit" : "button";
|
||||
const classType = props.type == "submit" ? "primary" : props.type;
|
||||
const dropdownMenu: Ref<HTMLElement | null> = ref(null);
|
||||
|
||||
const emits = defineEmits(["click"]);
|
||||
const handleClick = () => {
|
||||
emits("click", "");
|
||||
};
|
||||
|
||||
const handleClickToggleDropdown = () => {
|
||||
if (dropdownMenu.value) {
|
||||
dropdownMenu.value.style.display = "block";
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (dropdownMenu.value) {
|
||||
dropdownMenu.value.style.display = "none";
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickItem = (item: string) => {
|
||||
emits("click", item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
a.sm-button,
|
||||
a:visited.sm-button,
|
||||
.sm-button {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
font-family: var(--header-font-family);
|
||||
font-weight: 800;
|
||||
margin: 16px 32px 16px 32px;
|
||||
padding: 16px 32px 16px 32px;
|
||||
border: 0;
|
||||
background-color: var(--base-color-light);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
color: var(--link-color);
|
||||
|
||||
// position: relative;
|
||||
// padding: map-get($spacer, 2) map-get($spacer, 4);
|
||||
// color: white;
|
||||
// font-weight: 800;
|
||||
// border-width: 2px;
|
||||
// border-style: solid;
|
||||
// min-width: 7rem;
|
||||
// text-align: center;
|
||||
// display: inline-flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
|
||||
// background-color: rgba(255, 255, 255, 0.2);
|
||||
// border-color: transparent;
|
||||
// box-shadow: 0 0 8px 8px rgba(0, 0, 0, 0.1);
|
||||
// transition: all 0.1s ease-in-out;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
&.sm-button-block {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.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;
|
||||
align-items: center;
|
||||
font-weight: normal;
|
||||
background: #fff !important;
|
||||
color: $primary-color !important;
|
||||
border-radius: 12px;
|
||||
border-width: 1px;
|
||||
font-size: 0.8rem;
|
||||
min-width: auto;
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
border-right: 1px solid $primary-color-lighter;
|
||||
padding-top: calc(#{map-get($spacer, 1)} / 1.5);
|
||||
padding-bottom: calc(#{map-get($spacer, 1)} / 1.5);
|
||||
padding-left: map-get($spacer, 3);
|
||||
padding-right: map-get($spacer, 3);
|
||||
}
|
||||
|
||||
.sm-button-icon-dropdown {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
padding: 0 0.3rem 0 0.2rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $primary-color !important;
|
||||
color: #fff !important;
|
||||
|
||||
span {
|
||||
border-right: 1px solid $primary-color-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: $secondary-color !important;
|
||||
border-color: $secondary-color !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
text-decoration: none;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
box-shadow: 0 0 8px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul {
|
||||
position: absolute;
|
||||
display: none;
|
||||
z-index: 100;
|
||||
top: 20%;
|
||||
right: 0;
|
||||
min-width: 100%;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f8f8f8;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 8px;
|
||||
color: $primary-color;
|
||||
box-shadow: 0 0 14px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
li {
|
||||
padding: map-get($spacer, 1);
|
||||
font-size: 100%;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background-color: $primary-color;
|
||||
color: #f8f8f8;
|
||||
}
|
||||
|
||||
.sm-button-icon-before {
|
||||
margin-right: map-get($spacer, 1);
|
||||
}
|
||||
|
||||
.sm-button-icon-after {
|
||||
margin-left: map-get($spacer, 1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,27 +3,20 @@
|
||||
v-if="isEmpty(to)"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'sm-button',
|
||||
'button',
|
||||
classType,
|
||||
{ 'sm-button-small': small },
|
||||
{ 'sm-button-block': block },
|
||||
{ 'sm-dropdown-button': dropdown },
|
||||
{ 'button-small': small },
|
||||
{ 'button-block': block },
|
||||
{ 'dropdown-button': dropdown },
|
||||
]"
|
||||
ref="buttonRef"
|
||||
:style="{ minWidth: minWidth }"
|
||||
:type="buttonType"
|
||||
@click="handleClick">
|
||||
<ion-icon
|
||||
v-if="icon && dropdown == null && iconLocation == 'before'"
|
||||
:icon="icon"
|
||||
class="sm-button-icon-before" />
|
||||
<span class="sm-button-label">{{ label }}</span>
|
||||
<ion-icon
|
||||
v-if="icon && dropdown == null && iconLocation == 'after'"
|
||||
:icon="icon"
|
||||
class="sm-button-icon-after" />
|
||||
<ion-icon
|
||||
v-if="dropdown != null"
|
||||
name="caret-down-outline"
|
||||
class="sm-button-icon-dropdown"
|
||||
class="button-icon-dropdown"
|
||||
@click.stop="handleClickToggleDropdown" />
|
||||
<ul
|
||||
v-if="dropdown != null"
|
||||
@@ -36,30 +29,31 @@
|
||||
{{ dropdownLabel }}
|
||||
</li>
|
||||
</ul>
|
||||
<span v-if="!loading">{{ props.label }}</span>
|
||||
<SMLoadingIcon v-else class="button-icon-loading" />
|
||||
</button>
|
||||
<a
|
||||
v-else-if="!isEmpty(to) && typeof to == 'string'"
|
||||
:href="to"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'sm-button',
|
||||
'button',
|
||||
classType,
|
||||
{ 'sm-button-small': small },
|
||||
{ 'sm-button-block': block },
|
||||
{ 'button-small': small },
|
||||
{ 'button-block': block },
|
||||
]"
|
||||
:type="buttonType">
|
||||
<span class="sm-button-label">{{ label }}</span>
|
||||
<ion-icon v-if="icon" :icon="icon" />
|
||||
<span class="button-label">{{ label }}</span>
|
||||
</a>
|
||||
<router-link
|
||||
v-else-if="!isEmpty(to) && typeof to == 'object'"
|
||||
:to="to"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'sm-button',
|
||||
'button',
|
||||
classType,
|
||||
{ 'sm-button-small': small },
|
||||
{ 'sm-button-block': block },
|
||||
{ 'button-small': small },
|
||||
{ 'button-block': block },
|
||||
]">
|
||||
<ion-icon v-if="icon && iconLocation == 'before'" :icon="icon" />
|
||||
{{ label }}
|
||||
@@ -68,12 +62,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref } from "vue";
|
||||
import { Ref, onMounted, ref, watch } from "vue";
|
||||
import { isEmpty } from "../helpers/utils";
|
||||
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: "Button", required: false },
|
||||
type: { type: String, default: "primary", required: false },
|
||||
type: { type: String, default: "secondary", required: false },
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
@@ -114,14 +109,40 @@ const props = defineProps({
|
||||
required: false,
|
||||
validator: (prop) => typeof prop === "object" || prop === null,
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const buttonType: "submit" | "button" =
|
||||
props.type == "submit" ? "submit" : "button";
|
||||
const classType = props.type == "submit" ? "primary" : props.type;
|
||||
const disabled = ref(props.disabled);
|
||||
const dropdownMenu: Ref<HTMLElement | null> = ref(null);
|
||||
const loading = ref(false);
|
||||
const minWidth = ref("");
|
||||
const buttonRef = ref(null);
|
||||
|
||||
const emits = defineEmits(["click"]);
|
||||
|
||||
if (props.form !== undefined) {
|
||||
watch(
|
||||
() => props.form.loading(),
|
||||
(newValue) => {
|
||||
loading.value = newValue;
|
||||
disabled.value = newValue;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (buttonRef.value) {
|
||||
minWidth.value = `${buttonRef.value.clientWidth}px`;
|
||||
}
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
emits("click", "");
|
||||
};
|
||||
@@ -144,226 +165,35 @@ const handleClickItem = (item: string) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
a.sm-button,
|
||||
a:visited.sm-button,
|
||||
.sm-button {
|
||||
cursor: pointer;
|
||||
.button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-family: var(--header-font-family);
|
||||
font-weight: 800;
|
||||
margin: 16px 32px 16px 32px;
|
||||
padding: 16px 32px 16px 32px;
|
||||
border: 0;
|
||||
background-color: var(--base-color-light);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
color: var(--link-color);
|
||||
|
||||
// position: relative;
|
||||
// padding: map-get($spacer, 2) map-get($spacer, 4);
|
||||
// color: white;
|
||||
// font-weight: 800;
|
||||
// border-width: 2px;
|
||||
// border-style: solid;
|
||||
// min-width: 7rem;
|
||||
// text-align: center;
|
||||
// display: inline-flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
|
||||
// background-color: rgba(255, 255, 255, 0.2);
|
||||
// border-color: transparent;
|
||||
// box-shadow: 0 0 8px 8px rgba(0, 0, 0, 0.1);
|
||||
// transition: all 0.1s ease-in-out;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
&.sm-button-block {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.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;
|
||||
align-items: center;
|
||||
font-weight: normal;
|
||||
background: #fff !important;
|
||||
color: $primary-color !important;
|
||||
border-radius: 12px;
|
||||
border-width: 1px;
|
||||
font-size: 0.8rem;
|
||||
min-width: auto;
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
border-right: 1px solid $primary-color-lighter;
|
||||
padding-top: calc(#{map-get($spacer, 1)} / 1.5);
|
||||
padding-bottom: calc(#{map-get($spacer, 1)} / 1.5);
|
||||
padding-left: map-get($spacer, 3);
|
||||
padding-right: map-get($spacer, 3);
|
||||
}
|
||||
|
||||
.sm-button-icon-dropdown {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
padding: 0 0.3rem 0 0.2rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $primary-color !important;
|
||||
color: #fff !important;
|
||||
|
||||
span {
|
||||
border-right: 1px solid $primary-color-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: $secondary-color !important;
|
||||
border-color: $secondary-color !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
text-decoration: none;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
box-shadow: 0 0 8px 8px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
|
||||
filter: brightness(115%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background-color: #eee;
|
||||
color: #095589;
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul {
|
||||
position: absolute;
|
||||
display: none;
|
||||
z-index: 100;
|
||||
top: 20%;
|
||||
right: 0;
|
||||
min-width: 100%;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f8f8f8;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 8px;
|
||||
color: $primary-color;
|
||||
box-shadow: 0 0 14px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
li {
|
||||
padding: map-get($spacer, 1);
|
||||
font-size: 100%;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background-color: $primary-color;
|
||||
color: #f8f8f8;
|
||||
}
|
||||
|
||||
.sm-button-icon-before {
|
||||
margin-right: map-get($spacer, 1);
|
||||
}
|
||||
|
||||
.sm-button-icon-after {
|
||||
margin-left: map-get($spacer, 1);
|
||||
background-color: var(--primary-color);
|
||||
color: var(--base-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
22
resources/js/components/SMCard.vue
Normal file
22
resources/js/components/SMCard.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div v-if="slots.header" class="card-header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div v-if="slots.body || slots.default``" class="card-body">
|
||||
<slot name="body"></slot>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-if="slots.footer" class="card-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from "vue";
|
||||
|
||||
const slots = useSlots();
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -10,13 +10,14 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from "vue";
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
full: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
</script>
|
||||
|
||||
@@ -24,7 +25,7 @@ const slots = useSlots();
|
||||
.sm-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
// align-items: center;
|
||||
flex-direction: column;
|
||||
padding: 0 16px 0 16px;
|
||||
margin: auto;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<form class="SMForm" @submit.prevent="handleSubmit">
|
||||
<SMLoader :loading="props.modelValue._loading"></SMLoader>
|
||||
<SMMessage
|
||||
v-if="props.modelValue._message.length > 0"
|
||||
@@ -37,3 +37,9 @@ const handleSubmit = async function () {
|
||||
|
||||
provide("form", props.modelValue);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.SMForm {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,35 +1,22 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'sm-form-card',
|
||||
{ 'sm-form-card-narrow': narrow },
|
||||
{ 'sm-form-card-full': full },
|
||||
{ 'sm-form-card-noshadow': noShadow },
|
||||
]">
|
||||
<transition name="fade">
|
||||
<div v-if="loading" class="sm-form-card-loading-cover">
|
||||
<div class="sm-form-card-loading">
|
||||
<SMLoadingIcon />
|
||||
<span>{{ loadingMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<slot></slot>
|
||||
<div :class="['form-card', { 'form-card-full': full }]">
|
||||
<div v-if="slots.header" class="form-card-header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div v-if="slots.body || slots.default``" class="form-card-body">
|
||||
<slot name="body"></slot>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-if="slots.footer" class="form-card-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||
import { useSlots } from "vue";
|
||||
|
||||
defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loadingMessage: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -38,108 +25,33 @@ defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
noShadow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-form-card {
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
background-color: #eee;
|
||||
padding: map-get($spacer, 5) map-get($spacer, 5)
|
||||
calc(map-get($spacer, 5) / 1.5) map-get($spacer, 5);
|
||||
border: 1px solid #eee;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
min-width: map-get($spacer, 5) * 12;
|
||||
box-shadow: 4px 4px 20px rgba(0, 0, 0, 0.5);
|
||||
.form-card {
|
||||
max-width: 640px;
|
||||
margin: 64px auto;
|
||||
padding: 32px 48px;
|
||||
background-color: var(--base-color-light);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--base-shadow);
|
||||
|
||||
&.sm-form-card-noshadow {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
& > h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
& > p {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
&.sm-form-card-narrow {
|
||||
min-width: auto;
|
||||
max-width: map-get($spacer, 5) * 10;
|
||||
}
|
||||
|
||||
&.sm-form-card-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sm-form-card-loading-cover {
|
||||
position: fixed;
|
||||
.form-card-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
z-index: 19000;
|
||||
|
||||
.sm-form-card-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;
|
||||
|
||||
ion-icon {
|
||||
font-size: calc(map-get($spacer, 5) * 1.5);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: map-get($spacer, 4);
|
||||
padding-top: map-get($spacer, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
.sm-container .sm-form-card {
|
||||
margin: 0 -1rem;
|
||||
|
||||
&.sm-form-card-full {
|
||||
width: auto;
|
||||
}
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sm-form-card {
|
||||
padding: map-get($spacer, 5) map-get($spacer, 4) map-get($spacer, 4)
|
||||
map-get($spacer, 4);
|
||||
min-width: auto;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
|
||||
.sm-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
margin-top: map-get($spacer, 1);
|
||||
margin-bottom: map-get($spacer, 1);
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
<template>
|
||||
<section class="sm-hero" v-if="loaded">
|
||||
<section class="sm-hero">
|
||||
<div class="sm-hero-background" :style="heroStyles"></div>
|
||||
<div class="sm-hero-content">
|
||||
<h1>{{ heroTitle }}</h1>
|
||||
<p>{{ heroExcerpt }}</p>
|
||||
<div class="sm-hero-buttons">
|
||||
<SMButton
|
||||
:to="{ name: 'post-view', params: { slug: heroSlug } }"
|
||||
label="Read More" />
|
||||
<SMContainer>
|
||||
<div class="sm-hero-content">
|
||||
<h1>{{ heroTitle }}</h1>
|
||||
<p>{{ heroExcerpt }}</p>
|
||||
<div class="sm-hero-buttons">
|
||||
<SMButton
|
||||
v-if="loaded"
|
||||
:to="{ name: 'article', params: { slug: heroSlug } }"
|
||||
label="Read More" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SMContainer>
|
||||
<div class="sm-hero-caption">
|
||||
<router-link
|
||||
:to="{ name: 'post-view', params: { slug: heroSlug } }"
|
||||
v-if="loaded"
|
||||
:to="{ name: 'article', params: { slug: heroSlug } }"
|
||||
>{{ heroImageTitle }}</router-link
|
||||
>
|
||||
</div>
|
||||
@@ -28,11 +32,11 @@ import { excerpt } from "../helpers/string";
|
||||
import SMButton from "./SMButton.vue";
|
||||
|
||||
const loaded = ref(false);
|
||||
let heroTitle = "";
|
||||
let heroExcerpt = "";
|
||||
let heroImageUrl = "";
|
||||
let heroTitle = ref("");
|
||||
let heroExcerpt = ref("");
|
||||
let heroImageUrl = ref("");
|
||||
let heroImageTitle = "";
|
||||
let heroSlug = "";
|
||||
let heroSlug = ref("");
|
||||
const translateY = ref(0);
|
||||
const heroStyles = ref({
|
||||
backgroundImage: "none",
|
||||
@@ -71,15 +75,18 @@ const handleLoad = async () => {
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * postsData.posts.length
|
||||
);
|
||||
heroTitle = postsData.posts[randomIndex].title;
|
||||
heroExcerpt = excerpt(postsData.posts[randomIndex].content, 200);
|
||||
heroImageUrl = mediaGetVariantUrl(
|
||||
heroTitle.value = postsData.posts[randomIndex].title;
|
||||
heroExcerpt.value = excerpt(
|
||||
postsData.posts[randomIndex].content,
|
||||
200
|
||||
);
|
||||
heroImageUrl.value = mediaGetVariantUrl(
|
||||
postsData.posts[randomIndex].hero
|
||||
);
|
||||
heroImageTitle = postsData.posts[randomIndex].hero.title;
|
||||
heroSlug = postsData.posts[randomIndex].slug;
|
||||
heroSlug.value = postsData.posts[randomIndex].slug;
|
||||
|
||||
heroStyles.value.backgroundImage = `linear-gradient(rgba(0, 0, 0, 0.5),rgba(0, 0, 0, 0.5)),url('${heroImageUrl}')`;
|
||||
heroStyles.value.backgroundImage = `linear-gradient(to right, rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2)),url('${heroImageUrl.value}')`;
|
||||
|
||||
loaded.value = true;
|
||||
}
|
||||
@@ -109,9 +116,8 @@ handleLoad();
|
||||
|
||||
.sm-hero-content {
|
||||
position: relative;
|
||||
padding: 150px 32px 120px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
margin: 150px 32px 120px;
|
||||
max-width: 640px;
|
||||
|
||||
h1 {
|
||||
font-size: 300%;
|
||||
@@ -134,18 +140,19 @@ handleLoad();
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
right: 30px;
|
||||
color: #999;
|
||||
color: #ccc;
|
||||
font-size: 80%;
|
||||
padding: 2px 10px;
|
||||
padding: 6px 12px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
transition: color 0.1s ease-in-out;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: #ccc;
|
||||
color: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,208 +1,127 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'sm-input-group',
|
||||
{
|
||||
'sm-input-active': inputActive,
|
||||
'sm-feedback-invalid': feedbackInvalid,
|
||||
'sm-input-small': small,
|
||||
},
|
||||
computedClassType,
|
||||
'input-control-item',
|
||||
{ 'input-active': active, 'input-invalid': feedbackInvalid },
|
||||
]">
|
||||
<label v-if="label">{{ label }}</label>
|
||||
<ion-icon
|
||||
class="sm-invalid-icon"
|
||||
name="alert-circle-outline"></ion-icon>
|
||||
<label class="input-label" v-bind="{ for: id }">{{ label }}</label>
|
||||
<ion-icon class="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"
|
||||
:type="props.type"
|
||||
class="input-control"
|
||||
:disabled="disabled"
|
||||
v-bind="{ id: id }"
|
||||
v-model="value"
|
||||
@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>
|
||||
@input="handleInput" />
|
||||
<div v-if="slots.default || feedbackInvalid" class="input-help">
|
||||
<span v-if="feedbackInvalid" class="input-invalid">
|
||||
{{ feedbackInvalid }}
|
||||
</span>
|
||||
<span v-if="slots.default"><slot></slot></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, useSlots, watch } from "vue";
|
||||
import { openDialog } from "./SMDialog";
|
||||
import { api } from "../helpers/api";
|
||||
import { MediaResponse } from "../helpers/api.types";
|
||||
import { imageMedium } from "../helpers/image";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
import { inject, watch, ref, useSlots } from "vue";
|
||||
import { isEmpty } from "../helpers/utils";
|
||||
import { isUUID } from "../helpers/uuid";
|
||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
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: {
|
||||
form: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
control: {
|
||||
type: [String, Object],
|
||||
default: "",
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
required: false,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
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"
|
||||
const form = inject("form", props.form);
|
||||
const control =
|
||||
typeof props.control === "object"
|
||||
? props.control
|
||||
: !isEmpty(objForm) &&
|
||||
typeof props.control == "string" &&
|
||||
props.control != ""
|
||||
? objForm.controls[props.control]
|
||||
: !isEmpty(form) &&
|
||||
typeof props.control === "string" &&
|
||||
props.control !== "" &&
|
||||
Object.prototype.hasOwnProperty.call(form.controls, props.control)
|
||||
? form.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");
|
||||
const label = ref(
|
||||
props.label != undefined
|
||||
? props.label
|
||||
: typeof props.control == "string"
|
||||
? toTitleCase(props.control)
|
||||
: ""
|
||||
);
|
||||
const value = ref(
|
||||
props.modelValue != undefined
|
||||
? props.modelValue
|
||||
: control != null
|
||||
? control.value
|
||||
: ""
|
||||
);
|
||||
const id = ref(
|
||||
props.id != undefined
|
||||
? props.id
|
||||
: typeof props.control == "string"
|
||||
? props.control
|
||||
: ""
|
||||
);
|
||||
const feedbackInvalid = ref("");
|
||||
const active = ref(value.value != "");
|
||||
const disabled = ref(props.disabled);
|
||||
|
||||
/**
|
||||
* Return the classname based on type
|
||||
*/
|
||||
const computedClassType = computed(() => {
|
||||
return `sm-input-type-${props.type}`;
|
||||
watch(value, (newValue) => {
|
||||
active.value = newValue.value != "";
|
||||
});
|
||||
|
||||
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";
|
||||
|
||||
if (typeof control === "object") {
|
||||
watch(
|
||||
() => objControl.validation.result.valid,
|
||||
() => control.validation.result.valid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue
|
||||
? ""
|
||||
: objControl.validation.result.invalidMessages[0];
|
||||
: control.validation.result.invalidMessages[0];
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => objControl.value,
|
||||
() => control.value,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
@@ -210,55 +129,26 @@ if (objControl) {
|
||||
);
|
||||
}
|
||||
|
||||
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 */
|
||||
}
|
||||
}
|
||||
}
|
||||
if (form) {
|
||||
watch(
|
||||
() => form.loading(),
|
||||
(newValue) => {
|
||||
disabled.value = newValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
const handleChange = (event) => {
|
||||
emits("update:modelValue", event.target.files[0]);
|
||||
const handleFocus = () => {
|
||||
active.value = true;
|
||||
};
|
||||
|
||||
const handleBlur = async () => {
|
||||
active.value = value.value != "";
|
||||
|
||||
if (control) {
|
||||
await control.validate();
|
||||
control.isValid();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
@@ -266,276 +156,76 @@ const handleInput = (event: Event) => {
|
||||
value.value = target.value;
|
||||
emits("update:modelValue", target.value);
|
||||
|
||||
if (objControl) {
|
||||
objControl.value = target.value;
|
||||
if (control) {
|
||||
control.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 {
|
||||
.input-control-item {
|
||||
margin-bottom: 16px;
|
||||
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 {
|
||||
.input-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);
|
||||
transform: translate(16px, 16px) scale(1);
|
||||
transition: all 0.1s ease-in-out;
|
||||
color: $secondary-color-dark;
|
||||
color: var(--base-color-darker);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sm-invalid-icon {
|
||||
.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%;
|
||||
right: 10px;
|
||||
top: 14px;
|
||||
color: var(--danger-color);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
&.sm-input-select {
|
||||
.sm-invalid-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
box-sizing: border-box;
|
||||
.input-control {
|
||||
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;
|
||||
padding: 20px 16px 8px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color-light);
|
||||
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
color: var(--base-color-text);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
.input-help {
|
||||
display: block;
|
||||
font-size: 85%;
|
||||
margin-bottom: 8px;
|
||||
|
||||
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;
|
||||
.input-invalid {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.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;
|
||||
span + span:before {
|
||||
content: "-";
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.sm-input-media {
|
||||
text-align: center;
|
||||
margin-bottom: map-get($spacer, 2);
|
||||
&.input-active {
|
||||
.input-label {
|
||||
transform: translate(16px, 6px) scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.sm-input-media-item {
|
||||
&.input-invalid {
|
||||
.invalid-icon {
|
||||
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;
|
||||
.input-control {
|
||||
border: 2px solid var(--danger-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
resources/js/components/SMInputGroup.vue
Normal file
11
resources/js/components/SMInputGroup.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="input-group">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.input-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -11,33 +11,32 @@
|
||||
.sm-loading-icon-balls {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
width: 3em;
|
||||
height: 0.5em;
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
top: 33px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
top: 0em;
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
border-radius: 50%;
|
||||
background: #000;
|
||||
background: var(--base-color-light);
|
||||
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;
|
||||
left: 0.3em;
|
||||
animation: sm-loading-icon1 0.6s infinite;
|
||||
}
|
||||
div:nth-child(2) {
|
||||
left: 8px;
|
||||
left: 0.3em;
|
||||
animation: sm-loading-icon2 0.6s infinite;
|
||||
}
|
||||
div:nth-child(3) {
|
||||
left: 32px;
|
||||
left: 1.2em;
|
||||
animation: sm-loading-icon2 0.6s infinite;
|
||||
}
|
||||
div:nth-child(4) {
|
||||
left: 56px;
|
||||
left: 2.1em;
|
||||
animation: sm-loading-icon3 0.6s infinite;
|
||||
}
|
||||
@keyframes sm-loading-icon1 {
|
||||
@@ -61,7 +60,7 @@
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(24px, 0);
|
||||
transform: translate(0.9em, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
import SMButton from "./SMButton.vue";
|
||||
import SMInput from "./SMInput.vue";
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
@@ -54,6 +56,7 @@ const tabs = () => {
|
||||
.masthead {
|
||||
background-color: var(--primary-color);
|
||||
width: 100%;
|
||||
margin-bottom: 32px;
|
||||
|
||||
.main {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,46 +1,49 @@
|
||||
<template>
|
||||
<SMContainer
|
||||
:full="true"
|
||||
:class="['sm-navbar', { 'sm-show-nav': showToggle }]"
|
||||
:class="['sm-navbar-container', { 'sm-show-nav': showToggle }]"
|
||||
@click="handleClickNavBar">
|
||||
<template #default>
|
||||
<div id="sm-nav-head">
|
||||
<router-link :to="{ name: 'home' }" id="sm-logo-link">
|
||||
<img
|
||||
class="sm-nav-logo dark:d-none"
|
||||
src="/assets/logo.png"
|
||||
width="270"
|
||||
height="40"
|
||||
alt="STEMMechanics" />
|
||||
<img
|
||||
class="sm-nav-logo light:d-none"
|
||||
src="/assets/logo-dark.png"
|
||||
width="270"
|
||||
height="40"
|
||||
alt="STEMMechanics" />
|
||||
</router-link>
|
||||
<label id="sm-nav-toggle" @click.stop="handleClickToggleMenu"
|
||||
><img
|
||||
src="/assets/hamburger.svg"
|
||||
width="24"
|
||||
height="24"
|
||||
alt="Navbar Toggle"
|
||||
/></label>
|
||||
</div>
|
||||
<div id="sm-nav">
|
||||
<ul class="left">
|
||||
<template v-for="item in menuItems">
|
||||
<li
|
||||
v-if="item.show == undefined || item.show()"
|
||||
:key="item.name">
|
||||
<router-link :to="item.to">{{
|
||||
item.label
|
||||
}}</router-link>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<ul class="right"></ul>
|
||||
</div>
|
||||
<template #inner>
|
||||
<nav class="sm-navbar">
|
||||
<div id="sm-nav-head">
|
||||
<router-link :to="{ name: 'home' }" id="sm-logo-link">
|
||||
<img
|
||||
class="sm-nav-logo dark:d-none"
|
||||
src="/assets/logo.png"
|
||||
width="270"
|
||||
height="40"
|
||||
alt="STEMMechanics" />
|
||||
<img
|
||||
class="sm-nav-logo light:d-none"
|
||||
src="/assets/logo-dark.png"
|
||||
width="270"
|
||||
height="40"
|
||||
alt="STEMMechanics" />
|
||||
</router-link>
|
||||
<label
|
||||
id="sm-nav-toggle"
|
||||
@click.stop="handleClickToggleMenu"
|
||||
><img
|
||||
src="/assets/hamburger.svg"
|
||||
width="24"
|
||||
height="24"
|
||||
alt="Navbar Toggle"
|
||||
/></label>
|
||||
</div>
|
||||
<div id="sm-nav">
|
||||
<ul>
|
||||
<template v-for="item in menuItems">
|
||||
<li
|
||||
v-if="item.show == undefined || item.show()"
|
||||
:key="item.name">
|
||||
<router-link :to="item.to"
|
||||
><span>{{ item.label }}</span></router-link
|
||||
>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
</SMContainer>
|
||||
</template>
|
||||
@@ -52,17 +55,25 @@ import { useUserStore } from "../store/UserStore";
|
||||
const userStore = useUserStore();
|
||||
const showToggle = ref(false);
|
||||
const menuItems = [
|
||||
{
|
||||
name: "news",
|
||||
label: "News",
|
||||
to: { name: "post-list" },
|
||||
icon: "newspaper-outline",
|
||||
},
|
||||
{
|
||||
name: "workshops",
|
||||
label: "Workshops",
|
||||
to: { name: "event-list" },
|
||||
icon: "library-outline",
|
||||
},
|
||||
{
|
||||
name: "blog",
|
||||
label: "Blog",
|
||||
to: { name: "blog" },
|
||||
},
|
||||
{
|
||||
name: "community",
|
||||
label: "Community",
|
||||
to: { name: "blog" },
|
||||
},
|
||||
{
|
||||
name: "about",
|
||||
label: "About",
|
||||
to: { name: "blog" },
|
||||
},
|
||||
// {
|
||||
// name: "courses",
|
||||
@@ -74,7 +85,6 @@ const menuItems = [
|
||||
name: "contact",
|
||||
label: "Contact",
|
||||
to: { name: "contact" },
|
||||
icon: "mail-outline",
|
||||
},
|
||||
{
|
||||
name: "register",
|
||||
@@ -82,15 +92,12 @@ const menuItems = [
|
||||
to: { name: "register" },
|
||||
icon: "person-add-outline",
|
||||
show: () => !userStore.id,
|
||||
inNav: false,
|
||||
},
|
||||
{
|
||||
name: "login",
|
||||
label: "Log in",
|
||||
to: { name: "login" },
|
||||
icon: "log-in-outline",
|
||||
show: () => !userStore.id,
|
||||
inNav: false,
|
||||
},
|
||||
{
|
||||
name: "dashboard",
|
||||
@@ -98,7 +105,6 @@ const menuItems = [
|
||||
to: { name: "dashboard" },
|
||||
icon: "grid-outline",
|
||||
show: () => userStore.id,
|
||||
inNav: false,
|
||||
},
|
||||
{
|
||||
name: "logout",
|
||||
@@ -106,7 +112,6 @@ const menuItems = [
|
||||
to: { name: "logout" },
|
||||
icon: "log-out-outline",
|
||||
show: () => userStore.id,
|
||||
inNav: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -128,7 +133,13 @@ const handleClickNavBar = () => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-navbar {
|
||||
body[data-route-name="page-home"] {
|
||||
.sm-navbar-container {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.sm-navbar-container {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
@@ -137,7 +148,7 @@ const handleClickNavBar = () => {
|
||||
box-shadow: var(--base-shadow);
|
||||
|
||||
&.sm-show-nav {
|
||||
background-color: var(--navbar-color-dropdown);
|
||||
background-color: var(--navbar-color) !important;
|
||||
|
||||
#sm-nav {
|
||||
display: flex;
|
||||
@@ -148,15 +159,22 @@ const handleClickNavBar = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.sm-navbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#sm-nav-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
|
||||
#sm-logo-link {
|
||||
padding-left: 23px;
|
||||
padding-right: 18px;
|
||||
margin-top: -10px;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
@@ -172,19 +190,7 @@ const handleClickNavBar = () => {
|
||||
}
|
||||
|
||||
#sm-nav-toggle {
|
||||
padding: 23px;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: hsla(0, 0%, 50%, 0.1);
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +211,7 @@ const handleClickNavBar = () => {
|
||||
li a {
|
||||
color: var(--base-color-text);
|
||||
display: block;
|
||||
padding: 12px 24px;
|
||||
padding: 12px 0;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -213,6 +219,10 @@ const handleClickNavBar = () => {
|
||||
text-decoration: none;
|
||||
background-color: hsla(0, 0%, 50%, 0.1);
|
||||
}
|
||||
|
||||
span {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,4 +233,22 @@ const handleClickNavBar = () => {
|
||||
filter: invert(100%) saturate(0%) brightness(120%);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
// #sm-nav-toggle {
|
||||
// padding: 23px;
|
||||
// -webkit-user-select: none;
|
||||
// -moz-user-select: none;
|
||||
// -ms-user-select: none;
|
||||
// user-select: none;
|
||||
|
||||
// &:hover {
|
||||
// background-color: hsla(0, 0%, 50%, 0.1);
|
||||
// }
|
||||
|
||||
// img {
|
||||
// display: block;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
<template>
|
||||
<div class="sm-pagination">
|
||||
<ion-icon
|
||||
name="chevron-back-outline"
|
||||
:class="[{ disabled: computedDisablePrevButton }]"
|
||||
@click="handleClickPrev" />
|
||||
<span class="sm-pagination-info">{{ computedPaginationInfo }}</span>
|
||||
<ion-icon
|
||||
name="chevron-forward-outline"
|
||||
:class="[{ disabled: computedDisableNextButton }]"
|
||||
@click="handleClickNext" />
|
||||
<div class="pagination">
|
||||
<div
|
||||
v-if="props.modelValue > 1"
|
||||
:class="[
|
||||
'item',
|
||||
'previous',
|
||||
{ disabled: computedDisablePrevButton },
|
||||
]"
|
||||
@click="handleClickPrev">
|
||||
<ion-icon name="chevron-back-outline" />
|
||||
Previous
|
||||
</div>
|
||||
<div
|
||||
:class="['item', { active: page == props.modelValue }]"
|
||||
v-for="(page, idx) of computedPages"
|
||||
:key="idx"
|
||||
@click="handleClickPage(page)">
|
||||
{{ page }}
|
||||
</div>
|
||||
<div
|
||||
v-if="(props.modelValue + 3) * props.perPage <= props.total"
|
||||
:class="['item', 'next', { disabled: computedDisableNextButton }]"
|
||||
@click="handleClickNext">
|
||||
Next
|
||||
<ion-icon name="chevron-forward-outline" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -35,15 +51,28 @@ const emits = defineEmits(["update:modelValue"]);
|
||||
/**
|
||||
* Returns the pagination info
|
||||
*/
|
||||
const computedPaginationInfo = computed(() => {
|
||||
if (props.total == 0) {
|
||||
return "0 - 0 of 0";
|
||||
const computedPages = computed(() => {
|
||||
let pages = [];
|
||||
|
||||
if (props.modelValue - 2 > 0) {
|
||||
pages.push(props.modelValue - 2);
|
||||
}
|
||||
|
||||
const start = (props.modelValue - 1) * props.perPage + 1;
|
||||
const end = Math.min(start + props.perPage - 1, props.total);
|
||||
if (props.modelValue - 1 > 0) {
|
||||
pages.push(props.modelValue - 1);
|
||||
}
|
||||
|
||||
return `${start} - ${end} of ${props.total}`;
|
||||
pages.push(props.modelValue);
|
||||
|
||||
if (props.perPage * (props.modelValue + 1) <= props.total) {
|
||||
pages.push(props.modelValue + 1);
|
||||
}
|
||||
|
||||
if (props.perPage * (props.modelValue + 2) <= props.total) {
|
||||
pages.push(props.modelValue + 2);
|
||||
}
|
||||
|
||||
return pages;
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -69,67 +98,76 @@ const computedDisableNextButton = computed(() => {
|
||||
|
||||
/**
|
||||
* Handle click on previous button
|
||||
*
|
||||
* @param {MouseEvent} $event The mouse event.
|
||||
*/
|
||||
const handleClickPrev = ($event: MouseEvent): void => {
|
||||
if (
|
||||
$event.target &&
|
||||
($event.target as HTMLElement).classList.contains("disabled") ==
|
||||
false &&
|
||||
props.modelValue > 1
|
||||
) {
|
||||
emits("update:modelValue", props.modelValue - 1);
|
||||
}
|
||||
const handleClickPrev = (): void => {
|
||||
emits("update:modelValue", props.modelValue - 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle click on next button
|
||||
*
|
||||
* @param {MouseEvent} $event The mouse event.
|
||||
*/
|
||||
const handleClickNext = ($event: MouseEvent): void => {
|
||||
if (
|
||||
$event.target &&
|
||||
($event.target as HTMLElement).classList.contains("disabled") ==
|
||||
false &&
|
||||
props.modelValue < computedTotalPages.value
|
||||
) {
|
||||
emits("update:modelValue", props.modelValue + 1);
|
||||
}
|
||||
const handleClickNext = (): void => {
|
||||
emits("update:modelValue", props.modelValue + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle click on page button
|
||||
*
|
||||
* @param {number} page The page number to display.
|
||||
*/
|
||||
const handleClickPage = (page: number): void => {
|
||||
emits("update:modelValue", page);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-pagination {
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: var(--header-font-family);
|
||||
font-size: 90%;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
|
||||
ion-icon {
|
||||
border: 1px solid $secondary-color;
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
transition: color 0.1s ease-in-out, background-color 0.1s ease-in-out;
|
||||
color: $font-color;
|
||||
background-color: var(--base-color-light);
|
||||
padding: 12px 16px;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: var(--base-shadow);
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: $secondary-color;
|
||||
&.active {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:not(.disabled) {
|
||||
&:hover {
|
||||
background-color: $secondary-color;
|
||||
color: #eee;
|
||||
}
|
||||
&:first-of-type {
|
||||
border-left-width: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-right-width: 0;
|
||||
}
|
||||
|
||||
&.previous ion-icon {
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
&.next ion-icon {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(115%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sm-pagination-info {
|
||||
margin: 0 map-get($spacer, 3);
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.pagination .item.active {
|
||||
background-color: var(--primary-color-light);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,7 +32,7 @@ import SMButton from "../SMButton.vue";
|
||||
import SMFormCard from "../SMFormCard.vue";
|
||||
import SMForm from "../SMForm.vue";
|
||||
import SMFormFooter from "../SMFormFooter.vue";
|
||||
import SMInput from "../SMInput.vue";
|
||||
import SMInput from "../../depreciated/SMInput-old.vue";
|
||||
|
||||
const form: FormObject = reactive(
|
||||
Form({
|
||||
|
||||
541
resources/js/depreciated/SMInput-old.vue
Normal file
541
resources/js/depreciated/SMInput-old.vue
Normal file
@@ -0,0 +1,541 @@
|
||||
<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>
|
||||
12
resources/js/directives/body-class.ts
Normal file
12
resources/js/directives/body-class.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
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;
|
||||
@@ -50,6 +50,18 @@ export interface MediaCollection {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface Article {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
user_id: string;
|
||||
user: User;
|
||||
content: string;
|
||||
publish_at: string;
|
||||
hero: Media;
|
||||
attachments: Array<Media>;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
@@ -54,8 +54,12 @@ const defaultFormObject: FormObject = {
|
||||
|
||||
return valid;
|
||||
},
|
||||
loading: function (state = true) {
|
||||
this._loading = state;
|
||||
loading: function (state = undefined) {
|
||||
if (state !== undefined) {
|
||||
this._loading = state;
|
||||
}
|
||||
|
||||
return this._loading;
|
||||
},
|
||||
message: function (message = "", type = "", icon = "") {
|
||||
this._message = message;
|
||||
|
||||
@@ -15,6 +15,25 @@ export const routes = [
|
||||
},
|
||||
component: () => import("@/views/Home.vue"),
|
||||
},
|
||||
{
|
||||
path: "/blog",
|
||||
name: "blog",
|
||||
meta: {
|
||||
title: "Blog",
|
||||
},
|
||||
component: () => import("@/views/Blog.vue"),
|
||||
},
|
||||
{
|
||||
path: "/article",
|
||||
redirect: "/blog",
|
||||
children: [
|
||||
{
|
||||
path: ":slug",
|
||||
name: "article",
|
||||
component: () => import("@/views/Article.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/verify-email",
|
||||
name: "verify-email",
|
||||
@@ -130,24 +149,6 @@ export const routes = [
|
||||
},
|
||||
component: () => import("@/views/Register.vue"),
|
||||
},
|
||||
{
|
||||
path: "/news",
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "post-list",
|
||||
meta: {
|
||||
title: "News",
|
||||
},
|
||||
component: () => import("@/views/PostList.vue"),
|
||||
},
|
||||
{
|
||||
path: ":slug",
|
||||
name: "post-view",
|
||||
component: () => import("@/views/PostView.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/dashboard",
|
||||
children: [
|
||||
@@ -475,7 +476,8 @@ router.beforeEach(async (to, from, next) => {
|
||||
});
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
// empty
|
||||
const routeName = `page-${to.name}`;
|
||||
document.body.dataset.routeName = routeName;
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
</header>
|
||||
<main>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition>
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
<component :is="Component" />
|
||||
</router-view>
|
||||
</main>
|
||||
<footer>
|
||||
@@ -26,6 +24,7 @@ import SMDialogList from "../components/SMDialog";
|
||||
<style lang="scss">
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
142
resources/js/views/Blog.vue
Normal file
142
resources/js/views/Blog.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<SMMastHead title="Blog" />
|
||||
<SMContainer>
|
||||
<SMInputGroup>
|
||||
<SMInput
|
||||
type="text"
|
||||
label="Search articles"
|
||||
v-model="searchInput" />
|
||||
<SMButton type="submit" label="Search" @click="handeClickSearch" />
|
||||
</SMInputGroup>
|
||||
<SMPagination
|
||||
v-model="postsPage"
|
||||
:total="postsTotal"
|
||||
:per-page="postsPerPage" />
|
||||
<div class="posts">
|
||||
<article
|
||||
class="article-card"
|
||||
v-for="(post, idx) in posts"
|
||||
:key="idx">
|
||||
<div
|
||||
class="thumbnail"
|
||||
:style="{
|
||||
backgroundImage: `url(${mediaGetVariantUrl(
|
||||
post.hero,
|
||||
'medium'
|
||||
)})`,
|
||||
}"></div>
|
||||
<div class="content">
|
||||
{{ post.content }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</SMContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref, watch } from "vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import SMPagination from "../components/SMPagination.vue";
|
||||
import SMPanel from "../components/SMPanel.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 { mediaGetVariantUrl } from "../helpers/media";
|
||||
import SMMastHead from "../components/SMMastHead.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMInputGroup from "../components/SMInputGroup.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
|
||||
const message = ref("");
|
||||
const pageLoading = ref(true);
|
||||
const posts: Ref<Post[]> = ref([]);
|
||||
|
||||
const postsPerPage = 24;
|
||||
let postsPage = ref(1);
|
||||
let postsTotal = ref(0);
|
||||
|
||||
let searchInput = ref("");
|
||||
|
||||
const handeClickSearch = () => {
|
||||
alert(searchInput.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the page data.
|
||||
*/
|
||||
const handleLoad = () => {
|
||||
message.value = "";
|
||||
pageLoading.value = true;
|
||||
|
||||
api.get({
|
||||
url: "/posts",
|
||||
params: {
|
||||
limit: postsPerPage,
|
||||
page: postsPage.value,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
const data = result.data as PostCollection;
|
||||
|
||||
posts.value = data.posts;
|
||||
postsTotal.value = data.total;
|
||||
posts.value.forEach((post) => {
|
||||
post.publish_at = new SMDate(post.publish_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status != 404) {
|
||||
message.value =
|
||||
error.data?.message ||
|
||||
"The server is currently not available";
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
pageLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => postsPage.value,
|
||||
() => {
|
||||
handleLoad();
|
||||
}
|
||||
);
|
||||
|
||||
handleLoad();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.posts {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
|
||||
.article-card {
|
||||
.thumbnail {
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: 7px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-color: var(--card-background-color);
|
||||
box-shadow: 0 5px 10px -3px #00000078;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.posts {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.posts {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -125,7 +125,7 @@ import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormCard from "../components/SMFormCard.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMInput from "../depreciated/SMInput-old.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Form, FormControl } from "../helpers/form";
|
||||
import { And, Email, Min, Required } from "../helpers/validate";
|
||||
|
||||
@@ -47,7 +47,7 @@ import SMButton from "../components/SMButton.vue";
|
||||
import SMFormCard from "../components/SMFormCard.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMInput from "../depreciated/SMInput-old.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Form, FormControl } from "../helpers/form";
|
||||
import { And, Max, Min, Required } from "../helpers/validate";
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from "vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMInput from "../depreciated/SMInput-old.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import SMPagination from "../components/SMPagination.vue";
|
||||
import SMPanel from "../components/SMPanel.vue";
|
||||
|
||||
@@ -53,7 +53,7 @@ import SMButton from "../components/SMButton.vue";
|
||||
import SMFormCard from "../components/SMFormCard.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMInput from "../depreciated/SMInput-old.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Form, FormControl } from "../helpers/form";
|
||||
import { And, Min, Required } from "../helpers/validate";
|
||||
|
||||
@@ -52,7 +52,7 @@ import SMButton from "../components/SMButton.vue";
|
||||
import SMFormCard from "../components/SMFormCard.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMInput from "../depreciated/SMInput-old.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Form, FormControl } from "../helpers/form";
|
||||
import { And, Email, Required } from "../helpers/validate";
|
||||
|
||||
@@ -1,130 +1,164 @@
|
||||
<template>
|
||||
<SMPage full class="sm-page-home">
|
||||
<SMHero />
|
||||
<SMContainer class="about">
|
||||
<h2>Join the Fun!</h2>
|
||||
<p></p>
|
||||
<p>
|
||||
To meet the demands of a constantly evolving world, it is
|
||||
essential to nurture a new generation of scientists, engineers,
|
||||
and leaders who are skilled in problem-solving. Science and
|
||||
technology offer endless possibilities for innovation and
|
||||
progress, and it is through STEM education that we can equip the
|
||||
next generation with the tools they need to tackle these
|
||||
challenges.
|
||||
</p>
|
||||
<p>
|
||||
STEMMechanics is a family-run business that is committed to
|
||||
providing accessible and inclusive STEM education to all. We
|
||||
offer a wide range of STEM courses, after-school clubs, and
|
||||
themed workshops across Queensland, both to the general public
|
||||
and to private groups.
|
||||
</p>
|
||||
</SMContainer>
|
||||
<SMContainer class="workshops">
|
||||
<SMRow>
|
||||
<SMColumn class="align-items-center flex-basis-55">
|
||||
<h2>Build skills while having a great time</h2>
|
||||
<p>
|
||||
Our online and in-person workshops are filled with
|
||||
engaging and exciting activities that kids will love.
|
||||
They will have fun, make new friends, and gain valuable
|
||||
skills that they can use throughout their lives.
|
||||
</p>
|
||||
<SMButton
|
||||
:to="{ name: 'event-list' }"
|
||||
label="Explore Workshops" />
|
||||
</SMColumn>
|
||||
<SMColumn
|
||||
class="align-items-center justify-content-center flex-basis-45">
|
||||
<img src="/img/green-screen.jpg" />
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
<SMContainer class="support">
|
||||
<h2>And the support doesn't stop!</h2>
|
||||
<SMRow>
|
||||
<SMColumn
|
||||
class="align-items-center justify-content-center flex-basis-45">
|
||||
<img src="/img/discord.jpg" />
|
||||
</SMColumn>
|
||||
<SMColumn class="align-items-center flex-basis-55">
|
||||
<p>
|
||||
Though the workshop has come to a close, we remain
|
||||
available to assist you via email and Discord with any
|
||||
projects you undertake at home. We are always happy to
|
||||
help.
|
||||
</p>
|
||||
<div class="button-row">
|
||||
<a href="https://discord.gg/yNzk4x7mpD">Join Discord</a>
|
||||
<router-link :to="{ name: 'contact' }"
|
||||
>Contact Us</router-link
|
||||
>
|
||||
</div>
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
<SMContainer full class="minecraft">
|
||||
<SMContainer>
|
||||
<h2>Play Minecraft with us</h2>
|
||||
<SMHero />
|
||||
|
||||
<section class="container">
|
||||
<h2>Latest Articles</h2>
|
||||
<div class="d-flex" style="gap: 30px">
|
||||
<SMArticleCard
|
||||
:image="articles[0].hero.url"
|
||||
:title="articles[0].title"
|
||||
:excerpt="excerpt(articles[0].content)"
|
||||
:to="{
|
||||
name: 'article',
|
||||
params: { slug: articles[0].slug },
|
||||
}"
|
||||
class="flex-fill"></SMArticleCard>
|
||||
<div class="article-list">
|
||||
<SMArticleCard
|
||||
:image="articles[1].hero.url"
|
||||
:title="articles[1].title"
|
||||
:excerpt="excerpt(articles[1].content)"
|
||||
type="row"
|
||||
:to="{
|
||||
name: 'article',
|
||||
params: { slug: articles[1].slug },
|
||||
}"></SMArticleCard>
|
||||
<SMArticleCard
|
||||
:image="articles[2].hero.url"
|
||||
:title="articles[2].title"
|
||||
:excerpt="excerpt(articles[2].content)"
|
||||
type="row"
|
||||
:to="{
|
||||
name: 'article',
|
||||
params: { slug: articles[2].slug },
|
||||
}"></SMArticleCard>
|
||||
<SMArticleCard
|
||||
:image="articles[3].hero.url"
|
||||
:title="articles[3].title"
|
||||
:excerpt="excerpt(articles[3].content)"
|
||||
type="row"
|
||||
:to="{
|
||||
name: 'article',
|
||||
params: { slug: articles[3].slug },
|
||||
}"></SMArticleCard>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SMContainer class="about">
|
||||
<h2>Join the Fun!</h2>
|
||||
<p></p>
|
||||
<p>
|
||||
To meet the demands of a constantly evolving world, it is essential
|
||||
to nurture a new generation of scientists, engineers, and leaders
|
||||
who are skilled in problem-solving. Science and technology offer
|
||||
endless possibilities for innovation and progress, and it is through
|
||||
STEM education that we can equip the next generation with the tools
|
||||
they need to tackle these challenges.
|
||||
</p>
|
||||
<p>
|
||||
STEMMechanics is a family-run business that is committed to
|
||||
providing accessible and inclusive STEM education to all. We offer a
|
||||
wide range of STEM courses, after-school clubs, and themed workshops
|
||||
across Queensland, both to the general public and to private groups.
|
||||
</p>
|
||||
</SMContainer>
|
||||
<SMContainer class="workshops">
|
||||
<SMRow>
|
||||
<SMColumn class="align-items-center flex-basis-55">
|
||||
<h2>Build skills while having a great time</h2>
|
||||
<p>
|
||||
We invite you to join us on our Minecraft servers,
|
||||
supporting both Bedrock and Java clients, where you can
|
||||
participate in weekly challenges and mini-games.
|
||||
Our online and in-person workshops are filled with engaging
|
||||
and exciting activities that kids will love. They will have
|
||||
fun, make new friends, and gain valuable skills that they
|
||||
can use throughout their lives.
|
||||
</p>
|
||||
<p class="minecraft-education">
|
||||
<img
|
||||
src="/img/minecraft-edu.png"
|
||||
height="96"
|
||||
width="96"
|
||||
class="minecraft-image" />
|
||||
We also offer workshops for
|
||||
<a
|
||||
href="https://education.minecraft.net/en-us/discover/what-is-minecraft"
|
||||
target="_blank"
|
||||
>Minecraft Education</a
|
||||
>, where you can learn to make it rain rabbits or grow
|
||||
flowers wherever you walk, all without the need for a school
|
||||
account.
|
||||
<SMButton
|
||||
:to="{ name: 'event-list' }"
|
||||
label="Explore Workshops" />
|
||||
</SMColumn>
|
||||
<SMColumn
|
||||
class="align-items-center justify-content-center flex-basis-45">
|
||||
<img src="/img/green-screen.jpg" />
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
<SMContainer class="support">
|
||||
<h2>And the support doesn't stop!</h2>
|
||||
<SMRow>
|
||||
<SMColumn
|
||||
class="align-items-center justify-content-center flex-basis-45">
|
||||
<img src="/img/discord.jpg" />
|
||||
</SMColumn>
|
||||
<SMColumn class="align-items-center flex-basis-55">
|
||||
<p>
|
||||
Though the workshop has come to a close, we remain available
|
||||
to assist you via email and Discord with any projects you
|
||||
undertake at home. We are always happy to help.
|
||||
</p>
|
||||
<p class="pt-5">
|
||||
<img
|
||||
src="/img/minecraft-address.png"
|
||||
height="70"
|
||||
class="minecraft-address" />
|
||||
</p>
|
||||
</SMContainer>
|
||||
</SMContainer>
|
||||
<SMContainer class="subscribe">
|
||||
<h2>Be the first to know</h2>
|
||||
<div class="button-row">
|
||||
<a href="https://discord.gg/yNzk4x7mpD">Join Discord</a>
|
||||
<router-link :to="{ name: 'contact' }"
|
||||
>Contact Us</router-link
|
||||
>
|
||||
</div>
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
<SMContainer full class="minecraft">
|
||||
<SMContainer>
|
||||
<h2>Play Minecraft with us</h2>
|
||||
<p>
|
||||
Sign up for our mailing list to receive expert tips and tricks,
|
||||
as well as updates on upcoming workshops.
|
||||
We invite you to join us on our Minecraft servers, supporting
|
||||
both Bedrock and Java clients, where you can participate in
|
||||
weekly challenges and mini-games.
|
||||
</p>
|
||||
<p class="minecraft-education">
|
||||
<img
|
||||
src="/img/minecraft-edu.png"
|
||||
height="96"
|
||||
width="96"
|
||||
class="minecraft-image" />
|
||||
We also offer workshops for
|
||||
<a
|
||||
href="https://education.minecraft.net/en-us/discover/what-is-minecraft"
|
||||
target="_blank"
|
||||
>Minecraft Education</a
|
||||
>, where you can learn to make it rain rabbits or grow flowers
|
||||
wherever you walk, all without the need for a school account.
|
||||
</p>
|
||||
<p class="pt-5">
|
||||
<img
|
||||
src="/img/minecraft-address.png"
|
||||
height="70"
|
||||
class="minecraft-address" />
|
||||
</p>
|
||||
<SMFormCard class="p-0" no-shadow>
|
||||
<SMForm v-model="form" @submit="handleSubscribe">
|
||||
<div class="form-row">
|
||||
<SMInput control="email" />
|
||||
<SMButton type="submit" label="Subscribe" />
|
||||
</div>
|
||||
</SMForm>
|
||||
</SMFormCard>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</SMContainer>
|
||||
<SMContainer class="subscribe">
|
||||
<h2>Be the first to know</h2>
|
||||
<p>
|
||||
Sign up for our mailing list to receive expert tips and tricks, as
|
||||
well as updates on upcoming workshops.
|
||||
</p>
|
||||
</SMContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from "vue";
|
||||
import { Ref, reactive, ref } from "vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormCard from "../components/SMFormCard.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMInput from "../depreciated/SMInput-old.vue";
|
||||
import SMHero from "../components/SMHero.vue";
|
||||
|
||||
import { excerpt } from "../helpers/string";
|
||||
import { api } from "../helpers/api";
|
||||
import { Form, FormControl } from "../helpers/form";
|
||||
import { And, Email, Required } from "../helpers/validate";
|
||||
import { Article } from "../helpers/api.types";
|
||||
import SMArticleCard from "../components/SMArticleCard.vue";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
let form = reactive(
|
||||
@@ -133,294 +167,292 @@ let form = reactive(
|
||||
})
|
||||
);
|
||||
|
||||
const handleSubscribe = async () => {
|
||||
form.loading(true);
|
||||
form.message();
|
||||
const articles: Ref<Article[]> = ref([]);
|
||||
|
||||
try {
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
const handleLoad = async () => {
|
||||
const result = await api.get({
|
||||
url: "/posts",
|
||||
params: {
|
||||
limit: 4,
|
||||
after: 1, // is this working???
|
||||
// order: "-date",
|
||||
},
|
||||
});
|
||||
|
||||
await api.post({
|
||||
url: "/subscriptions",
|
||||
body: {
|
||||
email: form.controls.email.value,
|
||||
captcha_token: captcha,
|
||||
},
|
||||
});
|
||||
|
||||
form.controls.email.value = "";
|
||||
form.message("Your email address has been subscribed.", "success");
|
||||
} catch (err) {
|
||||
form.apiErrors(err);
|
||||
}
|
||||
|
||||
form.loading(false);
|
||||
articles.value = result.data.posts;
|
||||
};
|
||||
|
||||
handleLoad();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-page-home {
|
||||
margin-top: -127px !important;
|
||||
background-color: #fff;
|
||||
|
||||
h2 {
|
||||
font-weight: 1000;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.about {
|
||||
margin-top: 5rem;
|
||||
margin-left: 2rem;
|
||||
margin-right: 2rem;
|
||||
background-color: #3d4e5d;
|
||||
color: rgb(230, 245, 235);
|
||||
border-radius: 24px;
|
||||
padding: 4rem 8rem;
|
||||
width: auto;
|
||||
align-self: center;
|
||||
|
||||
h2 {
|
||||
font-size: 400%;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 125%;
|
||||
line-height: 150%;
|
||||
}
|
||||
}
|
||||
|
||||
.workshops {
|
||||
margin: 8rem auto;
|
||||
align-self: center;
|
||||
|
||||
h2 {
|
||||
font-size: 300%;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 125%;
|
||||
line-height: 150%;
|
||||
max-width: 32rem;
|
||||
text-align: center;
|
||||
margin: 1rem auto 2rem auto;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 50rem;
|
||||
height: 20rem;
|
||||
width: 20rem;
|
||||
}
|
||||
}
|
||||
|
||||
.support {
|
||||
background-color: #e6f5eb;
|
||||
color: rgb(56, 79, 95);
|
||||
border-radius: 24px;
|
||||
padding: 4rem 5rem;
|
||||
margin-left: 2rem;
|
||||
margin-right: 2rem;
|
||||
width: auto;
|
||||
align-self: center;
|
||||
|
||||
img {
|
||||
border-radius: 24px;
|
||||
height: 80%;
|
||||
width: 80%;
|
||||
transform: rotateZ(-10deg);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 300%;
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 125%;
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
color: inherit;
|
||||
border: 2px solid rgb(56, 79, 95);
|
||||
border-radius: 24px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
transition: color 0.2s ease-in-out, border 0.2s ease-in-out,
|
||||
background 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: rgb(56, 79, 95);
|
||||
color: #e6f5eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.minecraft {
|
||||
margin-top: 4rem;
|
||||
background-image: url("/img/minecraft.png");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
padding: 4rem;
|
||||
color: #fff;
|
||||
|
||||
h2 {
|
||||
font-size: 300%;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 125%;
|
||||
line-height: 150%;
|
||||
text-align: center;
|
||||
max-width: 44rem;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.minecraft-education {
|
||||
text-align: left;
|
||||
|
||||
.minecraft-image {
|
||||
float: left;
|
||||
margin-top: 1rem;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.minecraft-address {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.subscribe {
|
||||
margin: 6rem auto 0 auto;
|
||||
align-self: center;
|
||||
|
||||
h2 {
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
font-size: 120%;
|
||||
line-height: 140%;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
background-color: #eee;
|
||||
border-radius: 24px;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
}
|
||||
.article-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
// .sm-page-home {
|
||||
// margin-top: -127px !important;
|
||||
// background-color: #fff;
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
.sm-page-home {
|
||||
.about {
|
||||
padding: 4rem;
|
||||
}
|
||||
// h2 {
|
||||
// font-weight: 1000;
|
||||
// text-align: center;
|
||||
// margin: 0;
|
||||
// }
|
||||
|
||||
.support {
|
||||
padding: 4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
// .about {
|
||||
// margin-top: 5rem;
|
||||
// margin-left: 2rem;
|
||||
// margin-right: 2rem;
|
||||
// background-color: #3d4e5d;
|
||||
// color: rgb(230, 245, 235);
|
||||
// border-radius: 24px;
|
||||
// padding: 4rem 8rem;
|
||||
// width: auto;
|
||||
// align-self: center;
|
||||
|
||||
@media only screen and (max-width: 896px) {
|
||||
.sm-page-home {
|
||||
.support {
|
||||
.row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// h2 {
|
||||
// font-size: 400%;
|
||||
// }
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.sm-page-home {
|
||||
.about {
|
||||
margin-top: 2rem;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
// p {
|
||||
// font-size: 125%;
|
||||
// line-height: 150%;
|
||||
// }
|
||||
// }
|
||||
|
||||
.workshops {
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
// .workshops {
|
||||
// margin: 8rem auto;
|
||||
// align-self: center;
|
||||
|
||||
.support {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
// h2 {
|
||||
// font-size: 300%;
|
||||
// }
|
||||
|
||||
.minecraft {
|
||||
margin-top: 0;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
// p {
|
||||
// font-size: 125%;
|
||||
// line-height: 150%;
|
||||
// max-width: 32rem;
|
||||
// text-align: center;
|
||||
// margin: 1rem auto 2rem auto;
|
||||
// }
|
||||
|
||||
.minecraft-education {
|
||||
text-align: center;
|
||||
// img {
|
||||
// border-radius: 50rem;
|
||||
// height: 20rem;
|
||||
// width: 20rem;
|
||||
// }
|
||||
// }
|
||||
|
||||
.minecraft-image {
|
||||
float: none;
|
||||
display: block;
|
||||
margin: 0 auto 1rem auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// .support {
|
||||
// background-color: #e6f5eb;
|
||||
// color: rgb(56, 79, 95);
|
||||
// border-radius: 24px;
|
||||
// padding: 4rem 5rem;
|
||||
// margin-left: 2rem;
|
||||
// margin-right: 2rem;
|
||||
// width: auto;
|
||||
// align-self: center;
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
.sm-page-home {
|
||||
.about {
|
||||
padding: 2rem;
|
||||
// img {
|
||||
// border-radius: 24px;
|
||||
// height: 80%;
|
||||
// width: 80%;
|
||||
// transform: rotateZ(-10deg);
|
||||
// }
|
||||
|
||||
h2 {
|
||||
font-size: 300%;
|
||||
}
|
||||
// h2 {
|
||||
// font-size: 300%;
|
||||
// text-align: left;
|
||||
// text-align: center;
|
||||
// margin-bottom: 1rem;
|
||||
// }
|
||||
|
||||
p {
|
||||
font-size: 100%;
|
||||
line-height: 150%;
|
||||
}
|
||||
}
|
||||
// p {
|
||||
// font-size: 125%;
|
||||
// line-height: 150%;
|
||||
// }
|
||||
|
||||
.workshops,
|
||||
.support,
|
||||
.minecraft,
|
||||
.subscribe {
|
||||
padding: 2rem;
|
||||
// .button-row {
|
||||
// display: flex;
|
||||
// justify-content: space-between;
|
||||
// width: 100%;
|
||||
// margin-top: 1rem;
|
||||
|
||||
h2 {
|
||||
font-size: 200%;
|
||||
}
|
||||
// a {
|
||||
// font-weight: bold;
|
||||
// color: inherit;
|
||||
// border: 2px solid rgb(56, 79, 95);
|
||||
// border-radius: 24px;
|
||||
// padding: 0.5rem 1.5rem;
|
||||
// transition: color 0.2s ease-in-out, border 0.2s ease-in-out,
|
||||
// background 0.2s ease-in-out;
|
||||
|
||||
p {
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// &:hover {
|
||||
// text-decoration: none;
|
||||
// background-color: rgb(56, 79, 95);
|
||||
// color: #e6f5eb;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// .minecraft {
|
||||
// margin-top: 4rem;
|
||||
// background-image: url("/img/minecraft.png");
|
||||
// background-repeat: no-repeat;
|
||||
// background-position: center;
|
||||
// background-size: cover;
|
||||
// padding: 4rem;
|
||||
// color: #fff;
|
||||
|
||||
// h2 {
|
||||
// font-size: 300%;
|
||||
// }
|
||||
|
||||
// p {
|
||||
// font-size: 125%;
|
||||
// line-height: 150%;
|
||||
// text-align: center;
|
||||
// max-width: 44rem;
|
||||
// margin: 1rem auto;
|
||||
// }
|
||||
|
||||
// .minecraft-education {
|
||||
// text-align: left;
|
||||
|
||||
// .minecraft-image {
|
||||
// float: left;
|
||||
// margin-top: 1rem;
|
||||
// margin-right: 2rem;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .minecraft-address {
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .subscribe {
|
||||
// margin: 6rem auto 0 auto;
|
||||
// align-self: center;
|
||||
|
||||
// h2 {
|
||||
// font-size: 200%;
|
||||
// }
|
||||
|
||||
// p {
|
||||
// text-align: center;
|
||||
// font-size: 120%;
|
||||
// line-height: 140%;
|
||||
// margin: 1rem auto;
|
||||
// }
|
||||
|
||||
// .form-row {
|
||||
// background-color: #eee;
|
||||
// border-radius: 24px;
|
||||
// padding: 2rem;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// width: 100%;
|
||||
// max-width: 600px;
|
||||
// margin: 1rem auto;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// @media only screen and (max-width: 1024px) {
|
||||
// .sm-page-home {
|
||||
// .about {
|
||||
// padding: 4rem;
|
||||
// }
|
||||
|
||||
// .support {
|
||||
// padding: 4rem;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// @media only screen and (max-width: 896px) {
|
||||
// .sm-page-home {
|
||||
// .support {
|
||||
// .row {
|
||||
// flex-direction: column;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// @media only screen and (max-width: 768px) {
|
||||
// .sm-page-home {
|
||||
// .about {
|
||||
// margin-top: 2rem;
|
||||
// margin-left: 0;
|
||||
// margin-right: 0;
|
||||
// border-radius: 0;
|
||||
// }
|
||||
|
||||
// .workshops {
|
||||
// margin-top: 4rem;
|
||||
// margin-bottom: 4rem;
|
||||
// }
|
||||
|
||||
// .support {
|
||||
// margin-left: 0;
|
||||
// margin-right: 0;
|
||||
// border-radius: 0;
|
||||
// }
|
||||
|
||||
// .minecraft {
|
||||
// margin-top: 0;
|
||||
// padding-left: 1rem;
|
||||
// padding-right: 1rem;
|
||||
|
||||
// .minecraft-education {
|
||||
// text-align: center;
|
||||
|
||||
// .minecraft-image {
|
||||
// float: none;
|
||||
// display: block;
|
||||
// margin: 0 auto 1rem auto;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// @media only screen and (max-width: 640px) {
|
||||
// .sm-page-home {
|
||||
// .about {
|
||||
// padding: 2rem;
|
||||
|
||||
// h2 {
|
||||
// font-size: 300%;
|
||||
// }
|
||||
|
||||
// p {
|
||||
// font-size: 100%;
|
||||
// line-height: 150%;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .workshops,
|
||||
// .support,
|
||||
// .minecraft,
|
||||
// .subscribe {
|
||||
// padding: 2rem;
|
||||
|
||||
// h2 {
|
||||
// font-size: 200%;
|
||||
// }
|
||||
|
||||
// p {
|
||||
// font-size: 100%;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<section class="card max-w-sm">
|
||||
<div class="card-header">
|
||||
<h2>Log in</h2>
|
||||
<p>Enter your website login details to view your account.</p>
|
||||
</div>
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<div class="card-body">
|
||||
<SMContainer>
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMFormCard>
|
||||
<template #header>
|
||||
<h2>Log in</h2>
|
||||
<p>
|
||||
Enter your website login details to view your account.
|
||||
</p>
|
||||
</template>
|
||||
<template #body>
|
||||
<SMInput control="username">
|
||||
<router-link to="/forgot-username"
|
||||
>Forgot username?</router-link
|
||||
@@ -17,26 +19,26 @@
|
||||
>Forgot password?</router-link
|
||||
>
|
||||
</SMInput>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
</template>
|
||||
<template #footer>
|
||||
<small>
|
||||
<span class="pr-1">Need an account?</span
|
||||
><router-link to="/register">Register</router-link>
|
||||
</small>
|
||||
<input type="submit" class="btn" title="Log in" />
|
||||
</div>
|
||||
</SMForm>
|
||||
</section>
|
||||
</div>
|
||||
<SMButton :form="form" type="submit" label="Log in" />
|
||||
</template>
|
||||
</SMFormCard>
|
||||
</SMForm>
|
||||
</SMContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormCard from "../components/SMFormCard.vue";
|
||||
import SMContainer from "../components/SMContainer.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMFormCard from "../components/SMFormCard.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { LoginResponse } from "../helpers/api.types";
|
||||
@@ -44,8 +46,9 @@ 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 router = useRouter();
|
||||
let form = reactive(
|
||||
Form({
|
||||
username: FormControl("", And([Required(), Min(4)])),
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
<template>
|
||||
<SMPage class="sm-page-post-list" :loading="pageLoading">
|
||||
<template #container>
|
||||
<SMMessage
|
||||
v-if="message"
|
||||
icon="alert-circle-outline"
|
||||
type="error"
|
||||
:message="message"
|
||||
class="mt-5" />
|
||||
<SMPanelList
|
||||
:not-found="!pageLoading && posts.length == 0"
|
||||
not-found-text="No news found">
|
||||
<SMPanel
|
||||
v-for="post in posts"
|
||||
:key="post.id"
|
||||
:image="mediaGetVariantUrl(post.hero, 'medium')"
|
||||
:to="{ name: 'post-view', params: { slug: post.slug } }"
|
||||
:title="post.title"
|
||||
:date="post.publish_at"
|
||||
:content="post.content"
|
||||
:show-date="false"
|
||||
button="Read More"
|
||||
button-type="outline" />
|
||||
</SMPanelList>
|
||||
<SMPagination
|
||||
v-model="postsPage"
|
||||
:total="postsTotal"
|
||||
:per-page="postsPerPage" />
|
||||
</template>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref, watch } from "vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import SMPagination from "../components/SMPagination.vue";
|
||||
import SMPanel from "../components/SMPanel.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 { mediaGetVariantUrl } from "../helpers/media";
|
||||
|
||||
const message = ref("");
|
||||
const pageLoading = ref(true);
|
||||
const posts: Ref<Post[]> = ref([]);
|
||||
|
||||
const postsPerPage = 9;
|
||||
let postsPage = ref(1);
|
||||
let postsTotal = ref(0);
|
||||
|
||||
/**
|
||||
* Load the page data.
|
||||
*/
|
||||
const handleLoad = () => {
|
||||
message.value = "";
|
||||
pageLoading.value = true;
|
||||
|
||||
api.get({
|
||||
url: "/posts",
|
||||
params: {
|
||||
limit: postsPerPage,
|
||||
page: postsPage.value,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
const data = result.data as PostCollection;
|
||||
|
||||
posts.value = data.posts;
|
||||
postsTotal.value = data.total;
|
||||
posts.value.forEach((post) => {
|
||||
post.publish_at = new SMDate(post.publish_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status != 404) {
|
||||
message.value =
|
||||
error.data?.message ||
|
||||
"The server is currently not available";
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
pageLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => postsPage.value,
|
||||
() => {
|
||||
handleLoad();
|
||||
}
|
||||
);
|
||||
|
||||
handleLoad();
|
||||
</script>
|
||||
@@ -1,82 +1,75 @@
|
||||
<template>
|
||||
<SMPage>
|
||||
<template #container>
|
||||
<SMFormCard full class="mt-5" :narrow="formDone">
|
||||
<template v-if="!formDone">
|
||||
<h1>Register</h1>
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput control="username" />
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
control="password"
|
||||
type="password"></SMInput>
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput control="first_name" />
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<SMInput control="last_name" />
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput control="email" />
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<SMInput control="phone">
|
||||
This field is optional.
|
||||
</SMInput>
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<div class="small">
|
||||
<span class="pr-1"
|
||||
>Already have an account?</span
|
||||
><router-link to="/login"
|
||||
>Log in</router-link
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton
|
||||
type="submit"
|
||||
label="Register"
|
||||
icon="arrow-forward-outline" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</SMForm>
|
||||
<SMContainer>
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMCard class="form-wide">
|
||||
<template #header>
|
||||
<h2>Register</h2>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>Email Sent!</h1>
|
||||
<p class="text-center">
|
||||
An email has been sent to you to confirm your details
|
||||
and to finish registering your account.
|
||||
</p>
|
||||
<SMFormFooter>
|
||||
<template #right>
|
||||
<SMButton :to="{ name: 'home' }" label="Home" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
<template #body>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput control="username" />
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
control="password"
|
||||
type="password"></SMInput>
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput control="first_name" />
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<SMInput control="last_name" />
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput control="email" />
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<SMInput control="phone">
|
||||
This field is optional.
|
||||
</SMInput>
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
</template>
|
||||
</SMFormCard>
|
||||
<template #footer>
|
||||
<div class="small">
|
||||
<span class="pr-1">Already have an account?</span
|
||||
><router-link to="/login">Log in</router-link>
|
||||
</div>
|
||||
<SMButton type="submit" label="Register" />
|
||||
</template>
|
||||
</SMCard>
|
||||
</SMForm>
|
||||
<!-- </template>
|
||||
<template v-else>
|
||||
<h1>Email Sent!</h1>
|
||||
<p class="text-center">
|
||||
An email has been sent to you to confirm your details and to
|
||||
finish registering your account.
|
||||
</p>
|
||||
<SMFormFooter>
|
||||
<template #right>
|
||||
<SMButton :to="{ name: 'home' }" label="Home" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</template>
|
||||
</SMPage>
|
||||
</SMCard> -->
|
||||
</SMContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import SMCard from "../components/SMCard.vue";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormCard from "../components/SMFormCard.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMInput from "../depreciated/SMInput-old.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Form, FormControl } from "../helpers/form";
|
||||
import {
|
||||
|
||||
@@ -49,7 +49,7 @@ import SMButton from "../components/SMButton.vue";
|
||||
import SMFormCard from "../components/SMFormCard.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMInput from "../depreciated/SMInput-old.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Form, FormControl } from "../helpers/form";
|
||||
import { Required } from "../helpers/validate";
|
||||
|
||||
@@ -49,7 +49,7 @@ import SMButton from "../components/SMButton.vue";
|
||||
import SMFormCard from "../components/SMFormCard.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMInput from "../depreciated/SMInput-old.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Form, FormControl } from "../helpers/form";
|
||||
import { And, Max, Min, Password, Required } from "../helpers/validate";
|
||||
|
||||
@@ -41,7 +41,7 @@ import SMButton from "../components/SMButton.vue";
|
||||
import SMFormCard from "../components/SMFormCard.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMInput from "../depreciated/SMInput-old.vue";
|
||||
|
||||
import { api } from "../helpers/api";
|
||||
import { Form, FormControl } from "../helpers/form";
|
||||
|
||||
@@ -23,7 +23,7 @@ import { reactive } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMForm from "../../components/SMForm.vue";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMInput from "../../depreciated/SMInput-old.vue";
|
||||
|
||||
import { api } from "../../helpers/api";
|
||||
import { Form, FormControl } from "../../helpers/form";
|
||||
|
||||
@@ -132,7 +132,7 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMEditor from "../../components/SMEditor.vue";
|
||||
import SMFormFooter from "../../components/SMFormFooter.vue";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMInput from "../../depreciated/SMInput-old.vue";
|
||||
import { api } from "../../helpers/api";
|
||||
import { SMDate } from "../../helpers/datetime";
|
||||
import { Form, FormControl } from "../../helpers/form";
|
||||
|
||||
@@ -70,7 +70,7 @@ 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 SMInput from "../../components/SMInput.vue";
|
||||
import SMInput from "../../depreciated/SMInput-old.vue";
|
||||
import { api } from "../../helpers/api";
|
||||
import { SMDate } from "../../helpers/datetime";
|
||||
import { debounce } from "../../helpers/debounce";
|
||||
|
||||
@@ -67,7 +67,7 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMFormCard from "../../components/SMFormCard.vue";
|
||||
import SMForm from "../../components/SMForm.vue";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMInput from "../../depreciated/SMInput-old.vue";
|
||||
|
||||
import { api } from "../../helpers/api";
|
||||
import { Form, FormControl } from "../../helpers/form";
|
||||
|
||||
@@ -73,7 +73,7 @@ import { debounce } from "../../helpers/debounce";
|
||||
import { bytesReadable } from "../../helpers/types";
|
||||
import { useUserStore } from "../../store/UserStore";
|
||||
import { useToastStore } from "../../store/ToastStore";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMInput from "../../depreciated/SMInput-old.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const search = ref("");
|
||||
|
||||
@@ -71,7 +71,7 @@ import SMButton from "../../components/SMButton.vue";
|
||||
import SMEditor from "../../components/SMEditor.vue";
|
||||
import SMForm from "../../components/SMForm.vue";
|
||||
import SMFormFooter from "../../components/SMFormFooter.vue";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMInput from "../../depreciated/SMInput-old.vue";
|
||||
import SMInputAttachments from "../../components/SMInputAttachments.vue";
|
||||
import { api } from "../../helpers/api";
|
||||
import { PostResponse, UserCollection } from "../../helpers/api.types";
|
||||
|
||||
@@ -63,7 +63,7 @@ import { openDialog } from "../../components/SMDialog";
|
||||
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMHeading from "../../components/SMHeading.vue";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMInput from "../../depreciated/SMInput-old.vue";
|
||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||
import SMMessage from "../../components/SMMessage.vue";
|
||||
import SMToolbar from "../../components/SMToolbar.vue";
|
||||
|
||||
@@ -38,7 +38,7 @@ import SMButton from "../../components/SMButton.vue";
|
||||
import SMForm from "../../components/SMForm.vue";
|
||||
import SMFormFooter from "../../components/SMFormFooter.vue";
|
||||
import SMHeading from "../../components/SMHeading.vue";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMInput from "../../depreciated/SMInput-old.vue";
|
||||
import { api } from "../../helpers/api";
|
||||
import { UserResponse } from "../../helpers/api.types";
|
||||
import { Form, FormControl } from "../../helpers/form";
|
||||
|
||||
Reference in New Issue
Block a user