cleanup
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
interface ImportMeta {
|
||||
export interface ImportMetaExtras extends ImportMeta {
|
||||
env: {
|
||||
APP_URL: string;
|
||||
[key: string]: string;
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
135
resources/js/components/SMPagination.vue
Normal file
135
resources/js/components/SMPagination.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// &.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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
&:last-child {
|
||||
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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
if (
|
||||
typeof this[key] == "object" &&
|
||||
Object.keys(this[key]).includes("validation")
|
||||
typeof this.controls[key] == "object" &&
|
||||
Object.keys(this.controls[key]).includes("validation")
|
||||
) {
|
||||
this[key].validation.result = await this[
|
||||
const validationResult = await this.controls[
|
||||
key
|
||||
].validation.validator.validate(this[key].value);
|
||||
].validation.validator.validate(this.controls[key].value);
|
||||
this.controls[key].validation.result = validationResult;
|
||||
|
||||
if (!this[key].validation.result.valid) {
|
||||
if (!validationResult.valid) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return valid;
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
& > * {
|
||||
@@ -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) {
|
||||
})
|
||||
.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;
|
||||
|
||||
@@ -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,9 +157,12 @@ const handleLoad = async () => {
|
||||
params: {
|
||||
limit: 3,
|
||||
},
|
||||
}).then((response) => {
|
||||
if (response.data.posts) {
|
||||
response.data.posts.forEach((post) => {
|
||||
})
|
||||
.then((result) => {
|
||||
const data = result.data as PostCollection;
|
||||
|
||||
if (data && data.posts) {
|
||||
data.posts.forEach((post) => {
|
||||
posts.push({
|
||||
title: post.title,
|
||||
content: excerpt(post.content, 200),
|
||||
@@ -168,10 +172,12 @@ const handleLoad = async () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
|
||||
try {
|
||||
let result = await api.get({
|
||||
api.get({
|
||||
url: "/events",
|
||||
params: {
|
||||
limit: 3,
|
||||
@@ -181,22 +187,25 @@ const handleLoad = async () => {
|
||||
utc: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
})
|
||||
.then((result) => {
|
||||
const data = result.data as EventCollection;
|
||||
|
||||
if (result.data.events) {
|
||||
result.data.events.forEach((event) => {
|
||||
if (data && data.events) {
|
||||
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 } },
|
||||
url: { name: "event-view", params: { id: event.id } },
|
||||
cta: "View Workshop",
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
})
|
||||
.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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
api.post({
|
||||
url: "/logout",
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
}).finally(() => {
|
||||
userStore.clearUser();
|
||||
formLoading.value = false;
|
||||
toastStore.addToast({
|
||||
title: "Logged Out",
|
||||
content: "You have been logged out.",
|
||||
type: "success",
|
||||
});
|
||||
router.push({ name: "home" });
|
||||
});
|
||||
};
|
||||
|
||||
logout();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<SMContainer class="rules">
|
||||
<SMPage class="sm-minecraft">
|
||||
<template #container>
|
||||
<h1>Connecting to our Minecraft Server</h1>
|
||||
<ol>
|
||||
<li>
|
||||
@@ -20,23 +21,25 @@
|
||||
<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.
|
||||
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.
|
||||
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>
|
||||
</template>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.rules {
|
||||
.sm-minecraft {
|
||||
h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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) => {
|
||||
if (error.status != 404) {
|
||||
message.value =
|
||||
error.data?.message || "The server is currently not available";
|
||||
error.data?.message ||
|
||||
"The server is currently not available";
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
pageLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => postsPage.value,
|
||||
() => {
|
||||
handleLoad();
|
||||
}
|
||||
);
|
||||
|
||||
handleLoad();
|
||||
</script>
|
||||
180
resources/js/views/PostView.vue
Normal file
180
resources/js/views/PostView.vue
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,8 +103,7 @@ const checkUsername = async (value: string): boolean | string => {
|
||||
|
||||
abortController = new AbortController();
|
||||
|
||||
let x = await api
|
||||
.get({
|
||||
api.get({
|
||||
url: "/users",
|
||||
params: {
|
||||
username: value,
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user