This commit is contained in:
2023-02-27 19:40:03 +10:00
parent 1ee2a1189d
commit 955c06f5aa
30 changed files with 767 additions and 586 deletions

View File

@@ -5,6 +5,7 @@
:class="[
'sm-button',
classType,
{ 'sm-button-small': small },
{ 'sm-button-block': block },
{ 'sm-dropdown-button': dropdown },
]"
@@ -37,7 +38,12 @@
v-else-if="!isEmpty(to) && typeof to == 'string'"
:href="to"
:disabled="disabled"
:class="['sm-button', classType, { 'sm-button-block': block }]"
:class="[
'sm-button',
classType,
{ 'sm-button-small': small },
{ 'sm-button-block': block },
]"
:type="buttonType">
{{ label }}
<ion-icon v-if="icon" :icon="icon" />
@@ -46,7 +52,12 @@
v-else-if="!isEmpty(to) && typeof to == 'object'"
:to="to"
:disabled="disabled"
:class="['sm-button', classType, { 'sm-button-block': block }]">
: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" />
@@ -67,7 +78,7 @@ const props = defineProps({
},
iconLocation: {
type: String,
default: "before",
default: "after",
required: false,
validator: (value: string) => {
return ["before", "after"].includes(value);
@@ -89,6 +100,11 @@ const props = defineProps({
default: false,
required: false,
},
small: {
type: Boolean,
default: false,
required: false,
},
dropdown: {
type: Object,
default: null,

View File

@@ -27,8 +27,8 @@ const emits = defineEmits(["submit"]);
/**
* Handle the user submitting the form.
*/
const handleSubmit = function () {
if (props.modelValue.validate()) {
const handleSubmit = async function () {
if (await props.modelValue.validate()) {
emits("submit");
}
};

View File

@@ -5,7 +5,7 @@
<script setup lang="ts">
import DOMPurify from "dompurify";
import { computed } from "vue";
import "../../../import-meta";
import { ImportMetaExtras } from "../../../import-meta";
const props = defineProps({
html: {
@@ -22,7 +22,9 @@ const computedContent = computed(() => {
let html = "";
const regex = new RegExp(
`<a ([^>]*?)href="${import.meta.env.APP_URL}(.*?>.*?)</a>`,
`<a ([^>]*?)href="${
(import.meta as ImportMetaExtras).env.APP_URL
}(.*?>.*?)</a>`,
"ig"
);

View File

@@ -286,6 +286,7 @@ const handleMediaSelect = async (event) => {
flex-direction: column;
margin-bottom: map-get($spacer, 4);
flex: 1;
width: 100%;
&.sm-input-active {
label {

View File

@@ -23,7 +23,7 @@
</template>
</ul>
<SMButton
:to="{ name: 'workshop-list' }"
:to="{ name: 'event-list' }"
class="sm-navbar-cta"
label="Find a workshop"
icon="arrow-forward-outline" />
@@ -70,13 +70,13 @@ const menuItems = [
{
name: "news",
label: "News",
to: { name: "news" },
to: { name: "post-list" },
icon: "newspaper-outline",
},
{
name: "workshops",
label: "Workshops",
to: { name: "workshop-list" },
to: { name: "event-list" },
icon: "library-outline",
},
{

View File

@@ -0,0 +1,135 @@
<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>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps({
modelValue: {
type: Number,
required: true,
},
total: {
type: Number,
required: true,
},
perPage: {
type: Number,
required: true,
},
});
const emits = defineEmits(["update:modelValue"]);
/**
* Returns the pagination info
*/
const computedPaginationInfo = computed(() => {
if (props.total == 0) {
return "0 - 0 of 0";
}
const start = (props.modelValue - 1) * props.perPage + 1;
const end = start + props.perPage - 1;
return `${start} - ${end} of ${props.total}`;
});
/**
* Return the total number of pages.
*/
const computedTotalPages = computed(() => {
return Math.ceil(props.total / props.perPage);
});
/**
* Return if the previous button should be disabled.
*/
const computedDisablePrevButton = computed(() => {
return props.modelValue <= 1;
});
/**
* Return if the next button should be disabled.
*/
const computedDisableNextButton = computed(() => {
return props.modelValue >= computedTotalPages.value;
});
/**
* 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);
}
};
/**
* 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);
}
};
</script>
<style lang="scss">
.sm-pagination {
display: flex;
justify-content: center;
align-items: center;
ion-icon {
border: 1px solid $secondary-color;
border-radius: 4px;
padding: 0.25rem;
cursor: pointer;
transition: color 0.1s ease-in-out, background-color 0.1s ease-in-out;
color: $font-color;
&.disabled {
cursor: not-allowed;
color: $secondary-color;
}
&:not(.disabled) {
&:hover {
background-color: $secondary-color;
color: #eee;
}
}
}
.sm-pagination-info {
margin: 0 map-get($spacer, 3);
}
}
</style>

View File

@@ -31,7 +31,11 @@
{{ computedContent }}
</div>
<div v-if="button.length > 0" class="sm-panel-button">
<SMButton :to="to" :type="buttonType" :label="button" />
<SMButton
:to="to"
:type="buttonType"
:block="true"
:label="button" />
</div>
</div>
</router-link>
@@ -288,5 +292,9 @@ watch(
line-height: 130%;
flex: 1;
}
.sm-panel-button {
margin-top: map-get($spacer, 4);
}
}
</style>

View File

@@ -1,14 +1,25 @@
<template>
<div class="sm-toolbar">
<div class="sm-toolbar-column sm-toolbar-column-left">
<div v-if="slots.left" class="sm-toolbar-column sm-toolbar-column-left">
<slot name="left"></slot>
</div>
<div class="sm-toolbar-column sm-toolbar-column-right">
<div v-if="slots.default" class="sm-toolbar-column">
<slot></slot>
</div>
<div
v-if="slots.right"
class="sm-toolbar-column sm-toolbar-column-right">
<slot name="right"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { useSlots } from "vue";
const slots = useSlots();
</script>
<style lang="scss">
.sm-toolbar {
display: flex;
@@ -17,68 +28,41 @@
.sm-toolbar-column {
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
align-items: flex-start;
&.sm-toolbar-column-left {
justify-content: flex-start;
}
input {
margin-bottom: 0;
& > * {
margin: 0 map-get($spacer, 1);
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
// &.form-footer-column-left, &.form-footer-column-right {
// a, button {
// margin-left: map-get($spacer, 1);
// margin-right: map-get($spacer, 1);
// &:first-of-type {
// margin-left: 0;
// }
// &:last-of-type {
// margin-right: 0;
// }
// }
// }
&.sm-toolbar-column-right {
justify-content: flex-end;
}
// }
// }
}
}
// @media screen and (max-width: 768px) {
// .form-footer {
// flex-direction: column-reverse;
@media screen and (max-width: 768px) {
.sm-toolbar {
.sm-toolbar-column {
flex-direction: column;
// .form-footer-column {
// &.form-footer-column-left, &.form-footer-column-right {
// display: flex;
// flex-direction: column-reverse;
// justify-content: center;
// & > * {
// 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;
// }
// }
// &.form-footer-column-left {
// margin-bottom: -#{map-get($spacer, 1) / 2};
// }
// &.form-footer-column-right {
// margin-top: -#{map-get($spacer, 1) / 2};
// }
// }
& > * {
margin: 0;
}
}
}
}
</style>

View File

@@ -20,7 +20,7 @@ interface ApiOptions {
export interface ApiResponse {
status: number;
message: string;
data: Record<string, unknown>;
data: unknown;
json?: Record<string, unknown>;
}
@@ -84,10 +84,13 @@ export const api = {
const fetchOptions: RequestInit = {
method: options.method || "GET",
headers: options.headers,
body: options.body,
signal: options.signal || null,
};
if (typeof options.body == "string" && options.body.length > 0) {
fetchOptions.body = options.body;
}
const progressStore = useProgressStore();
progressStore.start();

View File

@@ -1,6 +1,15 @@
export interface Event {
id: string;
title: string;
hero: string;
content: string;
start_at: string;
end_at: string;
location: string;
address: string;
status: string;
registration_type: string;
registration_data: string;
}
export interface EventResponse {
@@ -8,7 +17,7 @@ export interface EventResponse {
}
export interface EventCollection {
events: Event;
events: Event[];
}
export interface Media {
@@ -50,6 +59,7 @@ export interface PostResponse {
export interface PostCollection {
posts: Array<Post>;
total: number;
}
export interface User {

View File

@@ -6,7 +6,7 @@ import {
ValidationResult,
} from "./validate";
type FormObjectValidateFunction = (item: string | null) => boolean;
type FormObjectValidateFunction = (item: string | null) => Promise<boolean>;
type FormObjectLoadingFunction = (state: boolean) => void;
type FormObjectMessageFunction = (
message?: string,
@@ -30,26 +30,27 @@ export interface FormObject {
}
const defaultFormObject: FormObject = {
validate: function (item = null) {
validate: async function (item = null) {
const keys = item ? [item] : Object.keys(this.controls);
let valid = true;
keys.every(async (key) => {
if (
typeof this[key] == "object" &&
Object.keys(this[key]).includes("validation")
) {
this[key].validation.result = await this[
key
].validation.validator.validate(this[key].value);
await Promise.all(
keys.map(async (key) => {
if (
typeof this.controls[key] == "object" &&
Object.keys(this.controls[key]).includes("validation")
) {
const validationResult = await this.controls[
key
].validation.validator.validate(this.controls[key].value);
this.controls[key].validation.result = validationResult;
if (!this[key].validation.result.valid) {
valid = false;
if (!validationResult.valid) {
valid = false;
}
}
}
return true;
});
})
);
return valid;
},

View File

@@ -37,14 +37,6 @@ export const routes = [
},
component: () => import("@/views/ResetPassword.vue"),
},
{
path: "/about",
name: "about",
meta: {
title: "About",
},
component: () => import("@/views/About.vue"),
},
{
path: "/privacy",
name: "privacy",
@@ -90,16 +82,16 @@ export const routes = [
children: [
{
path: "",
name: "workshop-list",
name: "event-list",
meta: {
title: "Workshops",
},
component: () => import("@/views/WorkshopList.vue"),
component: () => import("@/views/EventList.vue"),
},
{
path: ":id",
name: "workshop-view",
component: () => import("@/views/WorkshopView.vue"),
name: "event-view",
component: () => import("@/views/EventView.vue"),
},
],
},
@@ -141,16 +133,16 @@ export const routes = [
children: [
{
path: "",
name: "news",
name: "post-list",
meta: {
title: "News",
},
component: () => import("@/views/NewsList.vue"),
component: () => import("@/views/PostList.vue"),
},
{
path: ":slug",
name: "post-view",
component: () => import("@/views/NewsView.vue"),
component: () => import("@/views/PostView.vue"),
},
],
},
@@ -171,7 +163,7 @@ export const routes = [
children: [
{
path: "",
name: "post-list",
name: "dashboard-post-list",
meta: {
title: "Posts",
middleware: "authenticated",
@@ -181,7 +173,7 @@ export const routes = [
},
{
path: "create",
name: "post-create",
name: "dashboard-post-create",
meta: {
title: "Create Post",
middleware: "authenticated",
@@ -191,7 +183,7 @@ export const routes = [
},
{
path: ":id",
name: "post-edit",
name: "dashboard-post-edit",
meta: {
title: "Edit Post",
middleware: "authenticated",
@@ -206,7 +198,7 @@ export const routes = [
children: [
{
path: "",
name: "event-list",
name: "dashboard-event-list",
meta: {
title: "Events",
middleware: "authenticated",
@@ -216,7 +208,7 @@ export const routes = [
},
{
path: "create",
name: "event-create",
name: "dashboard-event-create",
meta: {
title: "Create Event",
middleware: "authenticated",
@@ -226,7 +218,7 @@ export const routes = [
},
{
path: ":id",
name: "event-edit",
name: "dashboard-event-edit",
meta: {
title: "Event Post",
middleware: "authenticated",
@@ -238,7 +230,7 @@ export const routes = [
},
{
path: "details",
name: "account-details",
name: "dashboard-account-details",
meta: {
title: "Account Details",
middleware: "authenticated",
@@ -250,7 +242,7 @@ export const routes = [
children: [
{
path: "",
name: "user-list",
name: "dashboard-user-list",
meta: {
title: "Users",
middleware: "authenticated",
@@ -260,7 +252,7 @@ export const routes = [
},
{
path: ":id",
name: "user-edit",
name: "dashboard-user-edit",
meta: {
title: "Edit User",
middleware: "authenticated",
@@ -275,7 +267,7 @@ export const routes = [
children: [
{
path: "",
name: "media",
name: "dashboard-media",
meta: {
title: "Media",
middleware: "authenticated",
@@ -285,7 +277,7 @@ export const routes = [
},
{
path: "upload",
name: "media-upload",
name: "dashboard-media-upload",
meta: {
title: "Upload Media",
middleware: "authenticated",
@@ -295,7 +287,7 @@ export const routes = [
},
{
path: "edit/:id",
name: "media-edit",
name: "dashboard-media-edit",
meta: {
title: "Edit Media",
middleware: "authenticated",
@@ -307,7 +299,7 @@ export const routes = [
},
{
path: "discord-bot-logs",
name: "discord-bot-logs",
name: "dashboard-discord-bot-logs",
meta: {
title: "Discord Bot Logs",
middleware: "authenticated",

View File

@@ -1,8 +1,8 @@
<template>
<SMPage class="workshop-list">
<SMPage class="sm-workshop-list">
<template #container>
<h1>Workshops</h1>
<div class="toolbar">
<SMToolbar>
<SMInput
v-model="filterKeywords"
label="Keywords"
@@ -17,21 +17,21 @@
label="Date Range"
:feedback-invalid="dateRangeError"
@change="handleFilter" />
</div>
</SMToolbar>
<SMMessage
v-if="formMessage.message"
:icon="formMessage.icon"
:type="formMessage.type"
:message="formMessage.message"
v-if="formMessage"
icon="alert-circle-outline"
type="error"
:message="formMessage"
class="mt-5" />
<SMPanelList
:loading="loading"
:not-found="events.value?.length == 0"
:not-found="events.length == 0"
not-found-text="No workshops found">
<SMPanel
v-for="event in events.value"
v-for="event in events"
:key="event.id"
:to="{ name: 'workshop-view', params: { id: event.id } }"
:to="{ name: 'event-view', params: { id: event.id } }"
:title="event.title"
:image="event.hero"
:show-time="true"
@@ -54,30 +54,25 @@ import SMInput from "../components/SMInput.vue";
import SMMessage from "../components/SMMessage.vue";
import SMPanel from "../components/SMPanel.vue";
import SMPanelList from "../components/SMPanelList.vue";
import SMToolbar from "../components/SMToolbar.vue";
import { api } from "../helpers/api";
import { Event, EventCollection } from "../helpers/api.types";
import { SMDate } from "../helpers/datetime";
const loading = ref(true);
const events = reactive([]);
let events: Event[] = reactive([]);
const dateRangeError = ref("");
const formMessage = reactive({
icon: "",
type: "",
message: "",
});
const formMessage = ref("");
const filterKeywords = ref("");
const filterLocation = ref("");
const filterDateRange = ref("");
/**
* Load page data.
*/
const handleLoad = async () => {
formMessage.type = "error";
formMessage.icon = "alert-circle-outline";
formMessage.message = "";
events.value = [];
let query = {};
query["limit"] = 10;
@@ -111,11 +106,16 @@ const handleLoad = async () => {
dateRangeError.value = "";
} else {
dateRangeError.value = "Invalid date range";
return;
}
} else {
dateRangeError.value = "";
}
loading.value = true;
formMessage.value = "";
events = [];
if (Object.keys(query).length == 1 && Object.keys(query)[0] == "limit") {
query["end_at"] =
">" +
@@ -127,10 +127,12 @@ const handleLoad = async () => {
params: query,
})
.then((result) => {
if (result.data.events) {
events.value = result.data.events;
const data = result.data as EventCollection;
events.value.forEach((item) => {
if (data && data.events) {
events = data.events;
events.forEach((item) => {
item.start_at = new SMDate(item.start_at, {
format: "yyyy-MM-dd HH:mm:ss",
utc: true,
@@ -145,7 +147,7 @@ const handleLoad = async () => {
})
.catch((error) => {
if (error.status != 404) {
formMessage.message =
formMessage.value =
error.response?.data?.message ||
"Could not load any events from the server.";
}
@@ -156,7 +158,6 @@ const handleLoad = async () => {
};
const handleFilter = async () => {
loading.value = true;
handleLoad();
};
@@ -188,7 +189,7 @@ handleLoad();
}
@media screen and (max-width: 768px) {
.workshop-list .toolbar {
.sm-workshop-list .toolbar {
flex-direction: column;
& > * {

View File

@@ -1,21 +1,25 @@
<template>
<SMPage :full="true" :loading="imageUrl.length == 0" class="workshop-view">
<SMPage
:full="true"
:loading="imageUrl.length == 0"
class="sm-workshop-view"
:error="pageError">
<div
class="workshop-image"
class="sm-workshop-image"
:style="{ backgroundImage: `url('${imageUrl}')` }"></div>
<SMContainer>
<SMMessage
v-if="formMessage.message"
:icon="formMessage.icon"
:type="formMessage.type"
:message="formMessage.message"
v-if="formMessage"
icon="alert-circle-outline"
type="error"
:message="formMessage"
class="mt-5" />
<SMContainer class="workshop-page">
<div class="workshop-body">
<h2 class="workshop-title">{{ event.title }}</h2>
<SMHTML :html="event.content" class="workshop-content" />
<SMContainer class="sm-workshop-page">
<div class="sm-workshop-body">
<h2 class="sm-workshop-title">{{ event.title }}</h2>
<SMHTML :html="event.content" class="sm-workshop-content" />
</div>
<div class="workshop-info">
<div class="sm-workshop-info">
<div
v-if="
event.status == 'closed' ||
@@ -24,17 +28,17 @@
format: 'ymd',
}).isBefore())
"
class="workshop-registration workshop-registration-closed">
class="sm-workshop-registration sm-workshop-registration-closed">
Registration for this event has closed.
</div>
<div
v-if="event.status == 'soon'"
class="workshop-registration workshop-registration-soon">
class="sm-workshop-registration sm-workshop-registration-soon">
Registration for this event will open soon.
</div>
<div
v-if="event.status == 'cancelled'"
class="workshop-registration workshop-registration-cancelled">
class="sm-workshop-registration sm-workshop-registration-cancelled">
This event has been cancelled.
</div>
<div
@@ -45,7 +49,7 @@
}).isAfter() &&
event.registration_type == 'none'
"
class="workshop-registration workshop-registration-none">
class="sm-workshop-registration sm-workshop-registration-none">
Registration not required for this event.<br />Arrive
early to avoid disappointment as seating maybe limited.
</div>
@@ -57,12 +61,13 @@
}).isAfter() &&
event.registration_type != 'none'
"
class="workshop-registration workshop-registration-url">
class="sm-workshop-registration sm-workshop-registration-url">
<SMButton
:href="registerUrl"
:block="true"
label="Register for Event"></SMButton>
</div>
<div class="workshop-date">
<div class="sm-workshop-date">
<h4><ion-icon name="calendar-outline" />Date / Time</h4>
<p
v-for="(line, index) in workshopDate"
@@ -71,7 +76,7 @@
{{ line }}
</p>
</div>
<div class="workshop-location">
<div class="sm-workshop-location">
<h4><ion-icon name="location-outline" />Location</h4>
<p>
{{
@@ -88,27 +93,37 @@
</template>
<script setup lang="ts">
import { computed, reactive, ref } from "vue";
import { computed, Ref, ref } from "vue";
import { useRoute } from "vue-router";
import SMButton from "../components/SMButton.vue";
import SMHTML from "../components/SMHTML.vue";
import SMMessage from "../components/SMMessage.vue";
import { api } from "../helpers/api";
import { Event, EventResponse, MediaResponse } from "../helpers/api.types";
import { SMDate } from "../helpers/datetime";
import { imageLoad } from "../helpers/image";
import { useApplicationStore } from "../store/ApplicationStore";
import { ApiEvent, ApiMedia } from "../helpers/api.types";
import { imageLoad } from "../helpers/image";
const applicationStore = useApplicationStore();
const event = ref({});
/**
* Event data
*/
const event: Ref<Event | null> = ref(null);
const imageUrl = ref("");
const route = useRoute();
const formMessage = reactive({
icon: "",
type: "",
message: "",
});
/**
* Page message.
*/
const formMessage = ref("");
/**
* Page error.
*/
let pageError = 200;
const workshopDate = computed(() => {
let str: string[] = [];
@@ -166,23 +181,23 @@ const registerUrl = computed(() => {
return href;
});
/**
* Load the page data.
*/
const handleLoad = async () => {
formMessage.type = "error";
formMessage.icon = "alert-circle-outline";
formMessage.message = "";
formMessage.value = "";
api.get(`/events/${route.params.id}`)
api.get({
url: "/events/{event}",
params: {
event: route.params.id,
},
})
.then((result) => {
event.value =
result.data &&
(result.data as ApiEvent).event &&
Object.keys((result.data as ApiEvent).event).length > 0
? (result.data as ApiEvent).event
: {};
if (event.value) {
// event.value = result.data.event as ApiEventItem;
const eventData = result.data as EventResponse;
if (eventData && eventData.event) {
event.value = eventData.event;
event.value.start_at = new SMDate(event.value.start_at, {
format: "ymd",
utc: true,
@@ -195,53 +210,46 @@ const handleLoad = async () => {
applicationStore.setDynamicTitle(event.value.title);
handleLoadImage();
} else {
formMessage.message =
"Could not load event information from the server.";
pageError = 404;
}
})
.catch((error) => {
formMessage.message =
formMessage.value =
error.data?.message ||
"Could not load event information from the server.";
});
// try {
// const result = await api.get(`/events/${route.params.id}`);
// event.value = result.data.event as ApiEventItem;
// event.value.start_at = timestampUtcToLocal(event.value.start_at);
// event.value.end_at = timestampUtcToLocal(event.value.end_at);
// applicationStore.setDynamicTitle(event.value.title);
// handleLoadImage();
// } catch (error) {
// formMessage.message =
// error.data?.message ||
// "Could not load event information from the server.";
// }
};
/**
* Load the hero image.
*/
const handleLoadImage = async () => {
try {
const result = await api.get(`/media/${event.value.hero}`);
const data = result.data as ApiMedia;
api.get({
url: "/media/{medium}",
params: {
medium: event.value.hero,
},
})
.then((result) => {
const data = result.data as MediaResponse;
if (data && data.medium) {
imageLoad(data.medium.url, (url) => {
imageUrl.value = url;
});
}
} catch (error) {
/* empty */
}
if (data && data.medium) {
imageLoad(data.medium.url, (url) => {
imageUrl.value = url;
});
}
})
.catch(() => {
/* empty */
});
};
handleLoad();
</script>
<style lang="scss">
.workshop-view {
.workshop-image {
.sm-workshop-view {
.sm-workshop-image {
display: flex;
justify-content: center;
align-items: center;
@@ -253,27 +261,27 @@ handleLoad();
background-color: #eee;
transition: background-image 0.2s;
.workshop-image-loader {
.sm-workshop-image-loader {
font-size: 5rem;
color: $secondary-color;
}
}
.workshop-page {
.sm-workshop-page {
display: flex;
flex-direction: row;
.workshop-body,
.workshop-info {
.sm-workshop-body,
.sm-workshop-info {
line-height: 1.5rem;
}
.workshop-body {
.sm-workshop-body {
flex: 1;
text-align: left;
}
.workshop-info {
.sm-workshop-info {
width: 18rem;
margin-left: 2rem;
@@ -296,17 +304,13 @@ handleLoad();
font-size: 90%;
}
.workshop-registration {
.sm-workshop-registration {
margin-top: 1.5rem;
line-height: 1.25rem;
.button {
display: block;
}
}
.workshop-registration-none,
.workshop-registration-soon {
.sm-workshop-registration-none,
.sm-workshop-registration-soon {
border: 1px solid #ffeeba;
background-color: #fff3cd;
color: #856404;
@@ -315,8 +319,8 @@ handleLoad();
padding: 0.5rem;
}
.workshop-registration-closed,
.workshop-registration-cancelled {
.sm-workshop-registration-closed,
.sm-workshop-registration-cancelled {
border: 1px solid #f5c2c7;
background-color: #f8d7da;
color: #842029;
@@ -325,8 +329,8 @@ handleLoad();
padding: 0.5rem;
}
.workshop-date,
.workshop-location {
.sm-workshop-date,
.sm-workshop-location {
padding: 0 1rem;
}
}
@@ -334,14 +338,14 @@ handleLoad();
}
@media screen and (max-width: 768px) {
.workshop-view .workshop-page {
.sm-workshop-view .sm-workshop-page {
flex-direction: column;
.workshop-body {
.sm-workshop-body {
text-align: center;
}
.workshop-info {
.sm-workshop-info {
width: 100%;
margin-left: 0;

View File

@@ -41,7 +41,7 @@
skills that they can use throughout their lives.
</p>
<SMButton
:to="{ name: 'workshop-list' }"
:to="{ name: 'event-list' }"
label="Explore Workshops" />
</SMColumn>
<SMColumn
@@ -111,7 +111,7 @@
as well as updates on upcoming workshops.
</p>
<SMDialog class="p-0" no-shadow>
<SMForm v-model="form" @submit.prevent="handleSubscribe">
<SMForm v-model="form" @submit="handleSubscribe">
<div class="form-row">
<SMInput control="email" />
<SMButton type="submit" label="Subscribe" />
@@ -133,6 +133,7 @@ import SMForm from "../components/SMForm.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { EventCollection, PostCollection } from "../helpers/api.types";
import { SMDate } from "../helpers/datetime";
import { Form, FormControl } from "../helpers/form";
import { excerpt } from "../helpers/string";
@@ -156,47 +157,55 @@ const handleLoad = async () => {
params: {
limit: 3,
},
}).then((response) => {
if (response.data.posts) {
response.data.posts.forEach((post) => {
posts.push({
title: post.title,
content: excerpt(post.content, 200),
image: post.hero,
url: { name: "post-view", params: { slug: post.slug } },
cta: "Read More...",
});
});
}
});
})
.then((result) => {
const data = result.data as PostCollection;
try {
let result = await api.get({
url: "/events",
params: {
limit: 3,
end_at:
">" +
new SMDate("now").format("yyyy-MM-dd HH:mm:ss", {
utc: true,
}),
},
if (data && data.posts) {
data.posts.forEach((post) => {
posts.push({
title: post.title,
content: excerpt(post.content, 200),
image: post.hero,
url: { name: "post-view", params: { slug: post.slug } },
cta: "Read More...",
});
});
}
})
.catch(() => {
/* empty */
});
if (result.data.events) {
result.data.events.forEach((event) => {
events.push({
title: event.title,
content: excerpt(event.content, 200),
image: event.hero,
url: { name: "workshop-view", params: { id: event.id } },
cta: "View Workshop",
api.get({
url: "/events",
params: {
limit: 3,
end_at:
">" +
new SMDate("now").format("yyyy-MM-dd HH:mm:ss", {
utc: true,
}),
},
})
.then((result) => {
const data = result.data as EventCollection;
if (data && data.events) {
data.events.forEach((event) => {
events.push({
title: event.title,
content: excerpt(event.content, 200),
image: event.hero,
url: { name: "event-view", params: { id: event.id } },
cta: "View Workshop",
});
});
});
}
} catch (error) {
/* empty */
}
}
})
.catch(() => {
/* empty */
});
for (let i = 1; i <= Math.max(posts.length, events.length); i++) {
if (i <= posts.length) {
@@ -219,12 +228,12 @@ const handleSubscribe = async () => {
await api.post({
url: "/subscriptions",
body: {
email: form.email.value,
email: form.controls.email.value,
captcha_token: captcha,
},
});
form.email.value = "";
form.controls.email.value = "";
form.message("Your email address has been subscribed.", "success");
} catch (err) {
form.apiErrors(err);

View File

@@ -57,6 +57,9 @@ const form = reactive(
const redirectQuery = useRoute().query.redirect;
/**
* Handle the user submitting the login form.
*/
const handleSubmit = async () => {
form.message();
form.loading(true);
@@ -70,7 +73,7 @@ const handleSubmit = async () => {
},
});
const login = result.data as unknown as LoginResponse;
const login = result.data as LoginResponse;
userStore.setUserDetails(login.user);
userStore.setUserToken(login.token);
@@ -84,6 +87,7 @@ const handleSubmit = async () => {
router.push({ name: "dashboard" });
}
} catch (err) {
form.controls.password.value = "";
form.apiErrors(err);
} finally {
form.loading(false);

View File

@@ -1,49 +1,35 @@
<template>
<SMPage no-breadcrumbs background="/img/background.jpg">
<SMRow>
<SMDialog narrow class="mt-5" :loading="formLoading">
<h1>Logged out</h1>
<SMRow>
<SMColumn class="justify-content-center">
<p class="mt-0 text-center">
You have now been logged out
</p>
</SMColumn>
</SMRow>
<SMRow>
<SMColumn class="justify-content-center">
<SMButton :to="{ name: 'home' }" label="Home" />
</SMColumn>
</SMRow>
</SMDialog>
</SMRow>
<SMLoader :loading="true" />
</SMPage>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
import SMLoader from "../components/SMLoader.vue";
import { api } from "../helpers/api";
import { useToastStore } from "../store/ToastStore";
import { useUserStore } from "../store/UserStore";
import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue";
const router = useRouter();
const userStore = useUserStore();
const formLoading = ref(false);
const toastStore = useToastStore();
/**
* Logout the current user and redirect to home page.
*/
const logout = async () => {
formLoading.value = true;
try {
await api.post({
url: "/logout",
api.post({
url: "/logout",
}).finally(() => {
userStore.clearUser();
toastStore.addToast({
title: "Logged Out",
content: "You have been logged out.",
type: "success",
});
} catch (err) {
console.log(err);
}
userStore.clearUser();
formLoading.value = false;
router.push({ name: "home" });
});
};
logout();

View File

@@ -1,42 +1,45 @@
<template>
<SMContainer class="rules">
<h1>Connecting to our Minecraft Server</h1>
<ol>
<li>
Open up your Minecraft on your computer (Java) or tablet
(Bedrock) and make sure you are using version 1.19.3
</li>
<li>Click Multiplayer</li>
<li>Click Add Server</li>
<li>Enter Server Name STEMMechanics</li>
<li>Enter Server Address mc.stemmech.com.au</li>
<li>
We have a custom resourcepack which you can enable before
joining
</li>
<li>Click Done</li>
<li>Join the Server!</li>
</ol>
<h2>Goodbye Drustcraft</h2>
<p>
STEMMechanics launched the Drustcraft server three years ago and
since then, players have had countless enjoyable experiences. Cities
were built, bosses defeated, and most importantly, a tight-knit
community formed.
</p>
<p>
Maintaining the server design became overwhelming and took away the
fun of playing Minecraft. Hence, in January, the decision was made
to shut down Drustcraft and offer a more straightforward Minecraft
server, retaining the beloved elements of Drustcraft like
mini-games, bosses, and survival. Join us on the new STEMMechanics
Minecraft server, where the Drustcraft community awaits.
</p>
</SMContainer>
<SMPage class="sm-minecraft">
<template #container>
<h1>Connecting to our Minecraft Server</h1>
<ol>
<li>
Open up your Minecraft on your computer (Java) or tablet
(Bedrock) and make sure you are using version 1.19.3
</li>
<li>Click Multiplayer</li>
<li>Click Add Server</li>
<li>Enter Server Name STEMMechanics</li>
<li>Enter Server Address mc.stemmech.com.au</li>
<li>
We have a custom resourcepack which you can enable before
joining
</li>
<li>Click Done</li>
<li>Join the Server!</li>
</ol>
<h2>Goodbye Drustcraft</h2>
<p>
STEMMechanics launched the Drustcraft server three years ago and
since then, players have had countless enjoyable experiences.
Cities were built, bosses defeated, and most importantly, a
tight-knit community formed.
</p>
<p>
Maintaining the server design became overwhelming and took away
the fun of playing Minecraft. Hence, in January, the decision
was made to shut down Drustcraft and offer a more
straightforward Minecraft server, retaining the beloved elements
of Drustcraft like mini-games, bosses, and survival. Join us on
the new STEMMechanics Minecraft server, where the Drustcraft
community awaits.
</p>
</template>
</SMPage>
</template>
<style lang="scss">
.rules {
.sm-minecraft {
h2 {
margin-bottom: 0.5rem;
}

View File

@@ -1,166 +0,0 @@
<template>
<SMPage
:loading="pageLoading"
full
class="page-post-view"
:page-error="error">
<div
class="heading-image"
:style="{
backgroundImage: `url('${post.hero_url}')`,
}"></div>
<SMContainer>
<div class="heading-info">
<h1>{{ post.title }}</h1>
<div class="date-author">
<ion-icon name="calendar-outline" />
{{ formattedPublishAt(post.publish_at) }}, by
{{ post.user_username }}
</div>
</div>
<component :is="formattedContent" ref="content"></component>
<SMAttachments :attachments="post.attachments || []" />
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { useRoute } from "vue-router";
import { api } from "../helpers/api";
import { SMDate } from "../helpers/datetime";
import { useApplicationStore } from "../store/ApplicationStore";
import SMAttachments from "../components/SMAttachments.vue";
const applicationStore = useApplicationStore();
const route = useRoute();
let post = ref({});
let content = ref(null);
let error = ref(0);
let pageLoading = ref(true);
const loadData = async () => {
if (route.params.slug) {
try {
let res = await api.get({
url: "/posts",
params: {
slug: `=${route.params.slug}`,
limit: 1,
},
});
if (!res.data.posts) {
error.value = 500;
} else {
if (res.data.total == 0) {
error.value = 404;
} else {
post.value = res.data.posts[0];
post.value.publish_at = new SMDate(post.value.publish_at, {
format: "ymd",
utc: true,
}).format("yyyy/MM/dd HH:mm:ss");
applicationStore.setDynamicTitle(post.value.title);
try {
let result = await api.get({
url: `/media/${post.value.hero}`,
});
post.value.hero_url = result.data.medium.url;
} catch (error) {
/* empty */
}
try {
let result = await api.get({
url: `/users/${post.value.user_id}`,
});
post.value.user_username = result.data.user.username;
} catch (error) {
/* empty */
}
}
}
} catch (err) {
error.value = 500;
}
}
pageLoading.value = false;
};
const formattedPublishAt = (dateStr) => {
return new SMDate(dateStr, { format: "yMd" }).format("MMMM d, yyyy");
};
const formattedContent = computed(() => {
let html = post.value.content;
if (html) {
const regex = new RegExp(
`<a ([^>]*?)href="${import.meta.env.APP_URL}(.*?>.*?)</a>`,
"ig"
);
html = html.replace(regex, '<router-link $1to="$2</router-link>');
}
return {
template: `<div class="content">${html}</div>`,
};
});
loadData();
</script>
<style lang="scss">
.page-post-view {
.heading-image {
background-color: #eee;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
height: 15rem;
}
.heading-info {
padding: 0 map-get($spacer, 3);
h1 {
text-align: left;
margin-bottom: 0.5rem;
text-overflow: ellipsis;
overflow: hidden;
word-wrap: break-word;
}
.date-author {
font-size: 80%;
svg {
margin-right: 0.5rem;
}
}
}
.content {
margin-top: map-get($spacer, 4);
padding: 0 map-get($spacer, 3);
a span {
color: $primary-color !important;
}
p {
line-height: 1.5rem;
}
}
}
@media only screen and (max-width: 768px) {
.page-post-view .heading-image {
height: 10rem;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<SMPage class="news-list">
<SMPage class="sm-post-list" :loading="pageLoading">
<template #container>
<SMMessage
v-if="message"
@@ -8,8 +8,7 @@
:message="message"
class="mt-5" />
<SMPanelList
:loading="loading"
:not-found="!loading && posts.length == 0"
:not-found="!pageLoading && posts.length == 0"
not-found-text="No news found">
<SMPanel
v-for="post in posts"
@@ -23,37 +22,51 @@
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 } from "vue";
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";
const message = ref("");
const loading = ref(true);
const pageLoading = ref(true);
const posts: Ref<Post[]> = ref([]);
const handleLoad = async () => {
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: 5,
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",
@@ -62,13 +75,23 @@ const handleLoad = async () => {
});
})
.catch((error) => {
message.value =
error.data?.message || "The server is currently not available";
if (error.status != 404) {
message.value =
error.data?.message ||
"The server is currently not available";
}
})
.finally(() => {
loading.value = false;
pageLoading.value = false;
});
};
watch(
() => postsPage.value,
() => {
handleLoad();
}
);
handleLoad();
</script>

View File

@@ -0,0 +1,180 @@
<template>
<SMPage
:loading="pageLoading"
full
class="sm-page-post-view"
:page-error="pageError">
<div class="sm-heading-image" :style="styleObject"></div>
<SMContainer>
<div class="sm-heading-info">
<h1>{{ post.title }}</h1>
<div class="sm-date-author">
<ion-icon name="calendar-outline" />
{{ formattedPublishAt(post.publish_at) }}, by
{{ postUser.username }}
</div>
</div>
<SMHTML :html="post.content" />
<SMAttachments :attachments="post.attachments || []" />
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import { ref, Ref } from "vue";
import { useRoute } from "vue-router";
import SMAttachments from "../components/SMAttachments.vue";
import SMHTML from "../components/SMHTML.vue";
import { api } from "../helpers/api";
import {
MediaResponse,
Post,
PostCollection,
User,
UserResponse,
} from "../helpers/api.types";
import { SMDate } from "../helpers/datetime";
import { useApplicationStore } from "../store/ApplicationStore";
const applicationStore = useApplicationStore();
/**
* The post data.
*/
let post: Ref<Post> = ref(null);
/**
* The current page error.
*/
let pageError = ref(200);
/**
* Is the page loading.
*/
let pageLoading = ref(false);
/**
* Post styles.
*/
let styleObject = {};
/**
* Post user.
*/
let postUser: User | null = null;
const loadData = () => {
let slug = useRoute().params.slug || "";
if (slug.length > 0) {
pageLoading.value = true;
api.get({
url: "/posts/",
params: {
slug: `=${slug}`,
limit: 1,
},
}).then((result) => {
const data = result.data as PostCollection;
if (data && data.posts && data.total && data.total > 0) {
post.value = data.posts[0];
post.value.publish_at = new SMDate(post.value.publish_at, {
format: "ymd",
utc: true,
}).format("yyyy/MM/dd HH:mm:ss");
applicationStore.setDynamicTitle(post.value.title);
// Get hero image
api.get({
url: "/media/{medium}",
params: {
medium: post.value.hero,
},
})
.then((mediumResult) => {
const mediumData = mediumResult.data as MediaResponse;
if (mediumData && mediumData.medium) {
styleObject[
"backgroundImage"
] = `url('${mediumData.medium.url}')`;
}
})
.catch(() => {
/* empty */
});
// Get user data
api.get({
url: "/users/{id}",
params: {
id: post.value.user_id,
},
})
.then((userResult) => {
const userData = userResult.data as UserResponse;
if (userData && userData.user) {
postUser = userData.user;
}
})
.catch(() => {
/* empty */
});
} else {
pageError.value = 404;
}
});
} else {
pageError.value = 404;
}
};
const formattedPublishAt = (dateStr) => {
return new SMDate(dateStr, { format: "yMd" }).format("MMMM d, yyyy");
};
loadData();
</script>
<style lang="scss">
.sm-page-post-view {
.sm-heading-image {
background-color: #eee;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
height: 15rem;
}
.sm-heading-info {
padding: 0 map-get($spacer, 3);
h1 {
text-align: left;
margin-bottom: 0.5rem;
text-overflow: ellipsis;
overflow: hidden;
word-wrap: break-word;
}
.date-author {
font-size: 80%;
svg {
margin-right: 0.5rem;
}
}
}
}
@media only screen and (max-width: 768px) {
.sm-page-post-view .sm-heading-image {
height: 10rem;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<SMContainer class="privacy">
<SMContainer class="sm-privacy">
<h1>Privacy Policy</h1>
<h3>We take our customers' privacy & security seriously.</h3>
<p>
@@ -322,7 +322,7 @@
</template>
<style lang="scss">
.privacy {
.sm-privacy {
h4 {
margin-bottom: 0.5rem;
}

View File

@@ -71,12 +71,12 @@
<script setup lang="ts">
import { reactive, ref } from "vue";
import { useReCaptcha } from "vue-recaptcha-v3";
import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import {
@@ -89,14 +89,11 @@ import {
Required,
} from "../helpers/validate";
import { useReCaptcha } from "vue-recaptcha-v3";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
let abortController: AbortController | null = null;
const checkUsername = async (value: string): boolean | string => {
const checkUsername = (value: string): boolean | string => {
if (lastUsernameCheck.value != value) {
console.log("api-get");
lastUsernameCheck.value = value;
if (abortController != null) {
@@ -106,14 +103,13 @@ const checkUsername = async (value: string): boolean | string => {
abortController = new AbortController();
let x = await api
.get({
url: "/users",
params: {
username: value,
},
signal: abortController.signal,
})
api.get({
url: "/users",
params: {
username: value,
},
signal: abortController.signal,
})
.then((response) => {
console.log("The username has already been taken.", response);
return "The username has already been taken.";
@@ -129,15 +125,13 @@ const checkUsername = async (value: string): boolean | string => {
return true;
});
return x;
}
console.log("here");
return true;
};
const formDone = ref(false);
const lastUsernameCheck = ref("");
const form = reactive(
Form({
first_name: FormControl("", Required()),
@@ -159,36 +153,21 @@ const handleSubmit = async () => {
await api.post({
url: "/register",
body: {
first_name: form.first_name.value,
last_name: form.last_name.value,
email: form.email.value,
phone: form.phone.value,
username: form.username.value,
password: form.password.value,
first_name: form.controls.first_name.value,
last_name: form.controls.last_name.value,
email: form.controls.email.value,
phone: form.controls.phone.value,
username: form.controls.username.value,
password: form.controls.password.value,
captcha_token: captcha,
},
});
formDone.value = true;
} catch (err) {
form.apiErrors(err);
} catch (error) {
form.apiErrors(error);
} finally {
form.loading(false);
}
form.loading(false);
};
const lastUsernameCheck = ref("");
// const debouncedFilter = debounce(checkUsername, 1000);
// let oldUsernameValue = "";
// watch(
// form,
// (value) => {
// if (value.username.value !== oldUsernameValue) {
// oldUsernameValue = value.username.value;
// // debouncedFilter(lastUsernameCheck.value);
// }
// },
// { deep: true }
// );
</script>

View File

@@ -50,7 +50,6 @@ import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { Required } from "../helpers/validate";
@@ -73,7 +72,7 @@ const handleSubmit = async () => {
await api.post({
url: "/users/resendVerifyEmailCode",
body: {
username: form.username.value,
username: form.controls.username.value,
captcha_token: captcha,
},
});
@@ -85,8 +84,8 @@ const handleSubmit = async () => {
} else {
form.apiErrors(error);
}
} finally {
form.loading(false);
}
form.loading(false);
};
</script>

View File

@@ -50,7 +50,6 @@ import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Max, Min, Password, Required } from "../helpers/validate";
@@ -65,7 +64,12 @@ const form = reactive(
);
if (useRoute().query.code !== undefined) {
form.code.value = useRoute().query.code;
let queryCode = useRoute().query.code;
if (Array.isArray(queryCode)) {
queryCode = queryCode[0];
}
form.controls.code.value = queryCode;
}
const handleSubmit = async () => {
@@ -78,17 +82,17 @@ const handleSubmit = async () => {
await api.post({
url: "/users/resetPassword",
body: {
code: form.code.value,
password: form.password.value,
code: form.controls.code.value,
password: form.controls.password.value,
captcha_token: captcha,
},
});
formDone.value = true;
} catch (error) {
form.apiError(error);
form.apiErrors(error);
} finally {
form.loading(false);
}
form.loading(false);
};
</script>

View File

@@ -1,5 +1,5 @@
<template>
<SMPage class="rules">
<SMPage class="sm-rules">
<h1>Rules</h1>
<p>
Oh gosh, no body likes rules but to ensure that we have a fun,
@@ -78,7 +78,7 @@
<script setup lang="ts"></script>
<style lang="scss">
.rules {
.sm-rules {
h2 {
margin-bottom: 0.5rem;
}

View File

@@ -1,5 +1,5 @@
<template>
<SMPage class="terms">
<SMPage class="sm-terms">
<h1>Terms and Conditions</h1>
<p>
Please read these terms carefully. By accessing or using our website
@@ -562,5 +562,3 @@
</p>
</SMPage>
</template>
<script setup lang="ts"></script>

View File

@@ -65,7 +65,7 @@ const handleSubmit = async () => {
await api.delete({
url: "/subscriptions",
body: {
email: form.email.value,
email: form.controls.email.value,
captcha_token: captcha,
},
});
@@ -73,13 +73,18 @@ const handleSubmit = async () => {
formDone.value = true;
} catch (error) {
form.apiErrors(error);
} finally {
form.loading(false);
}
form.loading(false);
};
if (useRoute().query.email !== undefined) {
form.email.value = useRoute().query.email;
let queryEmail = useRoute().query.email;
if (Array.isArray(queryEmail)) {
queryEmail = queryEmail[0];
}
form.controls.email.value = queryEmail;
handleSubmit();
}
</script>

View File

@@ -9,42 +9,42 @@
</router-link>
<router-link
v-if="userStore.permissions.includes('admin/posts')"
to="/dashboard/posts"
:to="{ name: 'dashboard-post-list' }"
class="box">
<ion-icon name="newspaper-outline" />
<h2>Posts</h2>
</router-link>
<router-link
v-if="userStore.permissions.includes('admin/users')"
:to="{ name: 'user-list' }"
:to="{ name: 'dashboard-user-list' }"
class="box">
<ion-icon name="people-outline" />
<h2>Users</h2>
</router-link>
<router-link
v-if="userStore.permissions.includes('admin/events')"
to="/dashboard/events"
:to="{ name: 'dashboard-event-list' }"
class="box">
<ion-icon name="calendar-outline" />
<h2>Events</h2>
</router-link>
<router-link
v-if="userStore.permissions.includes('admin/courses')"
to="/dashboard/courses"
:to="{ name: 'dashboard-course-list' }"
class="box">
<ion-icon name="school-outline" />
<h2>{{ courseBoxTitle }}</h2>
</router-link>
<router-link
v-if="userStore.permissions.includes('admin/media')"
to="/dashboard/media"
:to="{ name: 'dashboard-media-list' }"
class="box">
<ion-icon name="film-outline" />
<h2>Media</h2>
</router-link>
<router-link
v-if="userStore.permissions.includes('logs/discord')"
:to="{ name: 'discord-bot-logs' }"
:to="{ name: 'dashboard-discord-bot-logs' }"
class="box">
<ion-icon name="logo-discord" />
<h2>Discord Bot Logs</h2>