Dependency refactor #17
@@ -166,6 +166,7 @@ code {
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
margin-top: map-get($spacer, 5);
|
||||
min-height: 50vh;
|
||||
|
||||
.image {
|
||||
flex: 1;
|
||||
|
||||
@@ -27,6 +27,12 @@ $success-color: #198754;
|
||||
$success-color-dark: #12653e;
|
||||
$success-color-darker: #0c4329;
|
||||
|
||||
$warning-color-lighter: #fff8e2;
|
||||
$warning-color-light: #fff6d9;
|
||||
$warning-color: #fff3cd;
|
||||
$warning-color-dark: #ffd75a;
|
||||
$warning-color-darker: #ffc203;
|
||||
|
||||
$border-color: #dddddd;
|
||||
$secondary-background-color: #efefef;
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
import { computed, ComputedRef } from "vue";
|
||||
import { RouteRecordRaw, useRoute } from "vue-router";
|
||||
import { routes } from "../router";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
|
||||
const applicationStore = useApplicationStore();
|
||||
|
||||
/**
|
||||
* Return a list of routes from the current page back to the root
|
||||
@@ -76,6 +79,20 @@ const computedRouteCrumbs: ComputedRef<RouteRecordRaw[]> = computed(() => {
|
||||
};
|
||||
|
||||
let itemList = findMatch(routes);
|
||||
if (itemList) {
|
||||
if (applicationStore.dynamicTitle.length > 0) {
|
||||
let meta = {};
|
||||
|
||||
if ("meta" in itemList[itemList.length - 1]) {
|
||||
meta = itemList[itemList.length - 1]["meta"];
|
||||
}
|
||||
|
||||
meta["title"] = applicationStore.dynamicTitle;
|
||||
|
||||
itemList[itemList.length - 1]["meta"] = meta;
|
||||
}
|
||||
}
|
||||
|
||||
return itemList || [];
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -13,14 +13,17 @@
|
||||
@click="handleClick">
|
||||
<ion-icon
|
||||
v-if="icon && dropdown == null && iconLocation == 'before'"
|
||||
:icon="icon" />
|
||||
:icon="icon"
|
||||
class="sm-button-icon-before" />
|
||||
<span>{{ label }}</span>
|
||||
<ion-icon
|
||||
v-if="icon && dropdown == null && iconLocation == 'after'"
|
||||
:icon="icon" />
|
||||
:icon="icon"
|
||||
class="sm-button-icon-after" />
|
||||
<ion-icon
|
||||
v-if="dropdown != null"
|
||||
name="caret-down-outline"
|
||||
class="sm-button-icon-dropdown"
|
||||
@click.stop="handleClickToggleDropdown" />
|
||||
<ul
|
||||
v-if="dropdown != null"
|
||||
@@ -189,17 +192,17 @@ a.sm-button,
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
border-right: 1px solid $primary-color;
|
||||
border-right: 1px solid $primary-color-lighter;
|
||||
padding-top: calc(#{map-get($spacer, 1)} / 1.5);
|
||||
padding-bottom: calc(#{map-get($spacer, 1)} / 1.5);
|
||||
padding-left: map-get($spacer, 3);
|
||||
padding-right: map-get($spacer, 3);
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
.sm-button-icon-dropdown {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
padding: 0 map-get($spacer, 1) 0 map-get($spacer, 1);
|
||||
padding: 0 0.3rem 0 0.2rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -316,19 +319,39 @@ a.sm-button,
|
||||
margin: 0;
|
||||
background-color: #f8f8f8;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 8px;
|
||||
color: $primary-color;
|
||||
box-shadow: 0 0 14px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 0 14px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 12px 16px;
|
||||
padding: map-get($spacer, 1);
|
||||
font-size: 100%;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background-color: $primary-color;
|
||||
color: #f8f8f8;
|
||||
}
|
||||
|
||||
.sm-button-icon-before {
|
||||
margin-right: map-get($spacer, 1);
|
||||
}
|
||||
|
||||
.sm-button-icon-after {
|
||||
margin-left: map-get($spacer, 1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -113,7 +113,8 @@ const handleClickIndicator = (index: number) => {
|
||||
*/
|
||||
const handleCarouselUpdate = () => {
|
||||
if (slides.value != null) {
|
||||
slideElements.value = slides.value.querySelectorAll(".carousel-slide");
|
||||
slideElements.value =
|
||||
slides.value.querySelectorAll(".sm-carousel-slide");
|
||||
maxSlide.value = slideElements.value.length - 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
{
|
||||
'sm-input-active': inputActive,
|
||||
'sm-feedback-invalid': feedbackInvalid,
|
||||
'sm-input-small': small,
|
||||
},
|
||||
computedClassType,
|
||||
]">
|
||||
@@ -107,6 +108,11 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: "text",
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
feedbackInvalid: {
|
||||
type: String,
|
||||
default: "",
|
||||
@@ -249,7 +255,7 @@ const handleFocus = (event: Event) => {
|
||||
|
||||
const handleBlur = async (event: Event) => {
|
||||
if (objControl) {
|
||||
objControl.validate();
|
||||
await objControl.validate();
|
||||
objControl.isValid();
|
||||
}
|
||||
|
||||
@@ -276,10 +282,6 @@ const handleMediaSelect = async (event) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-column > .sm-input-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sm-input-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -288,6 +290,26 @@ const handleMediaSelect = async (event) => {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
||||
&.sm-input-small {
|
||||
font-size: 80%;
|
||||
|
||||
&.sm-input-active {
|
||||
label {
|
||||
transform: translate(6px, -3px) scale(0.7);
|
||||
}
|
||||
|
||||
input {
|
||||
padding: calc(#{map-get($spacer, 1)} * 1.5) map-get($spacer, 2)
|
||||
calc(#{map-get($spacer, 1)} / 2) map-get($spacer, 2);
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
label {
|
||||
padding: map-get($spacer, 1) map-get($spacer, 2);
|
||||
}
|
||||
}
|
||||
|
||||
&.sm-input-active {
|
||||
label {
|
||||
transform: translate(8px, -3px) scale(0.7);
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, ref, Ref } from "vue";
|
||||
import { ref, Ref, watch } from "vue";
|
||||
import { openDialog } from "vue3-promise-dialog";
|
||||
import { api } from "../helpers/api";
|
||||
import { Media, MediaResponse } from "../helpers/api.types";
|
||||
@@ -125,10 +125,6 @@ handleLoad();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-column > .sm-input-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sm-input-group.sm-input-attachments {
|
||||
display: block;
|
||||
|
||||
|
||||
@@ -92,10 +92,6 @@ const hasPermission = (): boolean => {
|
||||
|
||||
&.sm-no-breadcrumbs {
|
||||
margin-bottom: 0;
|
||||
|
||||
.sm-page {
|
||||
padding-bottom: calc(map-get($spacer, 5) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
.sm-page {
|
||||
|
||||
@@ -41,7 +41,7 @@ const computedPaginationInfo = computed(() => {
|
||||
}
|
||||
|
||||
const start = (props.modelValue - 1) * props.perPage + 1;
|
||||
const end = start + props.perPage - 1;
|
||||
const end = Math.min(start + props.perPage - 1, props.total);
|
||||
|
||||
return `${start} - ${end} of ${props.total}`;
|
||||
});
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
:block="true"
|
||||
:label="button" />
|
||||
</div>
|
||||
<div
|
||||
v-if="banner"
|
||||
:class="['sm-panel-banner', `sm-panel-banner-${bannerType}`]">
|
||||
{{ banner }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
@@ -119,6 +124,16 @@ const props = defineProps({
|
||||
default: "primary",
|
||||
required: false,
|
||||
},
|
||||
banner: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
bannerType: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
let styleObject = reactive({});
|
||||
@@ -204,6 +219,8 @@ watch(
|
||||
color: $font-color !important;
|
||||
margin-bottom: map-get($spacer, 5);
|
||||
transition: box-shadow 0.2s ease-in-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
color: $font-color;
|
||||
@@ -296,5 +313,42 @@ watch(
|
||||
.sm-panel-button {
|
||||
margin-top: map-get($spacer, 4);
|
||||
}
|
||||
|
||||
.sm-panel-banner {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 65px;
|
||||
right: -10px;
|
||||
height: 20px;
|
||||
width: 120px;
|
||||
font-size: 70%;
|
||||
text-transform: uppercase;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
background-color: $primary-color;
|
||||
transform-origin: 100%;
|
||||
transform: rotateZ(45deg);
|
||||
|
||||
&.sm-panel-banner-success {
|
||||
background-color: $success-color;
|
||||
}
|
||||
|
||||
&.sm-panel-banner-danger {
|
||||
background-color: $danger-color;
|
||||
font-size: 60%;
|
||||
}
|
||||
|
||||
&.sm-panel-banner-warning {
|
||||
background-color: $warning-color-darker;
|
||||
color: $font-color;
|
||||
font-size: 60%;
|
||||
}
|
||||
|
||||
&.sm-panel-banner-expired {
|
||||
background-color: purple;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface EventResponse {
|
||||
|
||||
export interface EventCollection {
|
||||
events: Event[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface Media {
|
||||
|
||||
@@ -132,7 +132,7 @@ interface FormControlValidation {
|
||||
|
||||
const defaultFormControlValidation: FormControlValidation = {
|
||||
validator: {
|
||||
validate: (): ValidationResult => {
|
||||
validate: async (): Promise<ValidationResult> => {
|
||||
return defaultValidationResult;
|
||||
},
|
||||
},
|
||||
@@ -148,7 +148,7 @@ type FormControlIsValid = () => boolean;
|
||||
|
||||
export interface FormControlObject {
|
||||
value: string;
|
||||
validate: () => ValidationResult;
|
||||
validate: () => Promise<ValidationResult>;
|
||||
validation: FormControlValidation;
|
||||
clearValidations: FormControlClearValidations;
|
||||
setValidationResult: FormControlSetValidation;
|
||||
@@ -179,11 +179,11 @@ export const FormControl = (
|
||||
this.validation.result = defaultValidationResult;
|
||||
},
|
||||
setValidationResult: createValidationResult,
|
||||
validate: function () {
|
||||
validate: async function () {
|
||||
if (this.validation.validator) {
|
||||
this.validation.result = this.validation.validator.validate(
|
||||
this.value
|
||||
);
|
||||
this.validation.result =
|
||||
await this.validation.validator.validate(this.value);
|
||||
|
||||
return this.validation.result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SMDate } from "./datetime";
|
||||
import { bytesReadable } from "../helpers/types";
|
||||
import { SMDate } from "./datetime";
|
||||
|
||||
export interface ValidationObject {
|
||||
validate: (value: string) => ValidationResult;
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
@@ -42,7 +42,7 @@ interface ValidationMinOptions {
|
||||
}
|
||||
|
||||
interface ValidationMinObject extends ValidationMinOptions {
|
||||
validate: (value: string) => ValidationResult;
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationMinOptions: ValidationMinOptions = {
|
||||
@@ -81,8 +81,8 @@ export function Min(
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): ValidationResult {
|
||||
return {
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid:
|
||||
this.type == "String"
|
||||
? value.toString().length >= this.min
|
||||
@@ -92,7 +92,7 @@ export function Min(
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -110,7 +110,7 @@ interface ValidationMaxOptions {
|
||||
}
|
||||
|
||||
interface ValidationMaxObject extends ValidationMaxOptions {
|
||||
validate: (value: string) => ValidationResult;
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationMaxOptions: ValidationMaxOptions = {
|
||||
@@ -149,8 +149,8 @@ export function Max(
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): ValidationResult {
|
||||
return {
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid:
|
||||
this.type == "String"
|
||||
? value.toString().length <= this.max
|
||||
@@ -160,7 +160,7 @@ export function Max(
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -173,7 +173,7 @@ interface ValidationPasswordOptions {
|
||||
}
|
||||
|
||||
interface ValidationPasswordObject extends ValidationPasswordOptions {
|
||||
validate: (value: string) => ValidationResult;
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationPasswordOptions: ValidationPasswordOptions = {
|
||||
@@ -194,8 +194,8 @@ export function Password(
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): ValidationResult {
|
||||
return {
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid: /(?=.*[A-Za-z])(?=.*\d)(?=.*[.@$!%*#?&])[A-Za-z\d.@$!%*#?&]{1,}$/.test(
|
||||
value
|
||||
),
|
||||
@@ -204,7 +204,7 @@ export function Password(
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -217,7 +217,7 @@ interface ValidationEmailOptions {
|
||||
}
|
||||
|
||||
interface ValidationEmailObject extends ValidationEmailOptions {
|
||||
validate: (value: string) => ValidationResult;
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationEmailOptions: ValidationEmailOptions = {
|
||||
@@ -235,8 +235,8 @@ export function Email(options?: ValidationEmailOptions): ValidationEmailObject {
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): ValidationResult {
|
||||
return {
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid:
|
||||
value.length == 0 ||
|
||||
/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(value),
|
||||
@@ -245,7 +245,7 @@ export function Email(options?: ValidationEmailOptions): ValidationEmailObject {
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -258,7 +258,7 @@ interface ValidationPhoneOptions {
|
||||
}
|
||||
|
||||
interface ValidationPhoneObject extends ValidationPhoneOptions {
|
||||
validate: (value: string) => ValidationResult;
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationPhoneOptions: ValidationPhoneOptions = {
|
||||
@@ -276,8 +276,8 @@ export function Phone(options?: ValidationPhoneOptions): ValidationPhoneObject {
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): ValidationResult {
|
||||
return {
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid:
|
||||
value.length == 0 ||
|
||||
/^(\+|00)?[0-9][0-9 \-().]{7,32}$/.test(value),
|
||||
@@ -286,7 +286,7 @@ export function Phone(options?: ValidationPhoneOptions): ValidationPhoneObject {
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -299,7 +299,7 @@ interface ValidationNumberOptions {
|
||||
}
|
||||
|
||||
interface ValidationNumberObject extends ValidationNumberOptions {
|
||||
validate: (value: string) => ValidationResult;
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationNumberOptions: ValidationNumberOptions = {
|
||||
@@ -319,15 +319,15 @@ export function Number(
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): ValidationResult {
|
||||
return {
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid: value.length == 0 || /^0?\d+$/.test(value),
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -346,7 +346,7 @@ interface ValidationDateOptions {
|
||||
}
|
||||
|
||||
interface ValidationDateObject extends ValidationDateOptions {
|
||||
validate: (value: string) => ValidationResult;
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationDateOptions: ValidationDateOptions = {
|
||||
@@ -372,7 +372,7 @@ export function Date(options?: ValidationDateOptions): ValidationDateObject {
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): ValidationResult {
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
let valid = true;
|
||||
let invalidMessageType = "invalidMessage";
|
||||
|
||||
@@ -409,14 +409,14 @@ export function Date(options?: ValidationDateOptions): ValidationDateObject {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return {
|
||||
return Promise.resolve({
|
||||
valid: valid,
|
||||
invalidMessages: [
|
||||
typeof this[invalidMessageType] === "string"
|
||||
? this[invalidMessageType]
|
||||
: this[invalidMessageType](this),
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -435,7 +435,7 @@ interface ValidationTimeOptions {
|
||||
}
|
||||
|
||||
interface ValidationTimeObject extends ValidationTimeOptions {
|
||||
validate: (value: string) => ValidationResult;
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationTimeOptions: ValidationTimeOptions = {
|
||||
@@ -461,7 +461,7 @@ export function Time(options?: ValidationTimeOptions): ValidationTimeObject {
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): ValidationResult {
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
let valid = true;
|
||||
let invalidMessageType = "invalidMessage";
|
||||
|
||||
@@ -498,14 +498,14 @@ export function Time(options?: ValidationTimeOptions): ValidationTimeObject {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return {
|
||||
return Promise.resolve({
|
||||
valid: valid,
|
||||
invalidMessages: [
|
||||
typeof this[invalidMessageType] === "string"
|
||||
? this[invalidMessageType]
|
||||
: this[invalidMessageType](this),
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -526,7 +526,7 @@ interface ValidationDateTimeOptions {
|
||||
}
|
||||
|
||||
interface ValidationDateTimeObject extends ValidationDateTimeOptions {
|
||||
validate: (value: string) => ValidationResult;
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationDateTimeOptions: ValidationDateTimeOptions = {
|
||||
@@ -554,7 +554,7 @@ export function DateTime(
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): ValidationResult {
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
let valid = true;
|
||||
let invalidMessageType = "invalidMessage";
|
||||
|
||||
@@ -591,14 +591,14 @@ export function DateTime(
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return {
|
||||
return Promise.resolve({
|
||||
valid: valid,
|
||||
invalidMessages: [
|
||||
typeof this[invalidMessageType] === "string"
|
||||
? this[invalidMessageType]
|
||||
: this[invalidMessageType](this),
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -606,7 +606,7 @@ export function DateTime(
|
||||
/**
|
||||
* CUSTOM
|
||||
*/
|
||||
type ValidationCustomCallback = (value: string) => boolean | string;
|
||||
type ValidationCustomCallback = (value: string) => Promise<boolean | string>;
|
||||
|
||||
interface ValidationCustomOptions {
|
||||
callback: ValidationCustomCallback;
|
||||
@@ -618,7 +618,7 @@ interface ValidationCustomObject extends ValidationCustomOptions {
|
||||
}
|
||||
|
||||
const defaultValidationCustomOptions: ValidationCustomOptions = {
|
||||
callback: () => {
|
||||
callback: async () => {
|
||||
return true;
|
||||
},
|
||||
invalidMessage: "This field is invalid.",
|
||||
@@ -692,24 +692,24 @@ export function Custom(
|
||||
export const And = (list: Array<ValidationObject>) => {
|
||||
return {
|
||||
list: list,
|
||||
validate: function (value: string) {
|
||||
validate: async function (value: string) {
|
||||
const validationResult: ValidationResult = {
|
||||
valid: true,
|
||||
invalidMessages: [],
|
||||
};
|
||||
|
||||
this.list.every((item: ValidationObject) => {
|
||||
const validationItemResult = item.validate(value);
|
||||
if (validationItemResult.valid == false) {
|
||||
validationResult.valid = false;
|
||||
validationResult.invalidMessages =
|
||||
validationResult.invalidMessages.concat(
|
||||
validationItemResult.invalidMessages
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
await Promise.all(
|
||||
this.list.map(async (item: ValidationObject) => {
|
||||
const validationItemResult = await item.validate(value);
|
||||
if (validationItemResult.valid == false) {
|
||||
validationResult.valid = false;
|
||||
validationResult.invalidMessages =
|
||||
validationResult.invalidMessages.concat(
|
||||
validationItemResult.invalidMessages
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return validationResult;
|
||||
},
|
||||
@@ -724,7 +724,7 @@ interface ValidationRequiredOptions {
|
||||
}
|
||||
|
||||
interface ValidationRequiredObject extends ValidationRequiredOptions {
|
||||
validate: (value: string) => ValidationResult;
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationRequiredOptions: ValidationRequiredOptions = {
|
||||
@@ -744,15 +744,15 @@ export function Required(
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): ValidationResult {
|
||||
return {
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid: value.length > 0,
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -765,7 +765,7 @@ interface ValidationUrlOptions {
|
||||
}
|
||||
|
||||
interface ValidationUrlObject extends ValidationUrlOptions {
|
||||
validate: (value: string) => ValidationResult;
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationUrlOptions: ValidationUrlOptions = {
|
||||
@@ -783,8 +783,8 @@ export function Url(options?: ValidationUrlOptions): ValidationUrlObject {
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): ValidationResult {
|
||||
return {
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid: /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*(:\d+)?([/?#][^\s]*)?$/.test(
|
||||
value
|
||||
),
|
||||
@@ -793,7 +793,7 @@ export function Url(options?: ValidationUrlOptions): ValidationUrlObject {
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -807,7 +807,7 @@ interface ValidationFileSizeOptions {
|
||||
}
|
||||
|
||||
interface ValidationFileSizeObject extends ValidationFileSizeOptions {
|
||||
validate: (value: File) => ValidationResult;
|
||||
validate: (value: File) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationFileSizeOptions: ValidationFileSizeOptions = {
|
||||
@@ -830,15 +830,15 @@ export function FileSize(
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: File): ValidationResult {
|
||||
return {
|
||||
validate: function (value: File): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid: value.size < options.size,
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,29 +29,36 @@
|
||||
:not-found="events.length == 0"
|
||||
not-found-text="No workshops found">
|
||||
<SMPanel
|
||||
v-for="event in events"
|
||||
:key="event.id"
|
||||
:to="{ name: 'event-view', params: { id: event.id } }"
|
||||
:title="event.title"
|
||||
:image="event.hero"
|
||||
v-for="item in events"
|
||||
:key="item.event.id"
|
||||
:to="{ name: 'event-view', params: { id: item.event.id } }"
|
||||
:title="item.event.title"
|
||||
:image="item.event.hero"
|
||||
:show-time="true"
|
||||
:date="event.start_at"
|
||||
:end-date="event.end_at"
|
||||
:date="item.event.start_at"
|
||||
:end-date="item.event.end_at"
|
||||
:date-in-image="true"
|
||||
:location="
|
||||
event.location == 'online'
|
||||
item.event.location == 'online'
|
||||
? 'Online Event'
|
||||
: event.address
|
||||
"></SMPanel>
|
||||
: item.event.address
|
||||
"
|
||||
:banner="item.banner"
|
||||
:banner-type="item.bannerType"></SMPanel>
|
||||
</SMPanelList>
|
||||
<SMPagination
|
||||
v-model="postsPage"
|
||||
:total="postsTotal"
|
||||
:per-page="postsPerPage" />
|
||||
</template>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { reactive, ref, watch } from "vue";
|
||||
import SMInput from "../components/SMInput.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 SMToolbar from "../components/SMToolbar.vue";
|
||||
@@ -59,8 +66,14 @@ import { api } from "../helpers/api";
|
||||
import { Event, EventCollection } from "../helpers/api.types";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
|
||||
interface EventData {
|
||||
event: Event;
|
||||
banner: string;
|
||||
bannerType: string;
|
||||
}
|
||||
|
||||
const loading = ref(true);
|
||||
let events: Event[] = reactive([]);
|
||||
let events: EventData[] = reactive([]);
|
||||
const dateRangeError = ref("");
|
||||
|
||||
const formMessage = ref("");
|
||||
@@ -69,12 +82,15 @@ const filterKeywords = ref("");
|
||||
const filterLocation = ref("");
|
||||
const filterDateRange = ref("");
|
||||
|
||||
const postsPerPage = 9;
|
||||
let postsPage = ref(1);
|
||||
let postsTotal = ref(0);
|
||||
|
||||
/**
|
||||
* Load page data.
|
||||
*/
|
||||
const handleLoad = async () => {
|
||||
let query = {};
|
||||
query["limit"] = 10;
|
||||
|
||||
if (filterKeywords.value && filterKeywords.value.length > 0) {
|
||||
query["q"] = filterKeywords.value;
|
||||
@@ -116,12 +132,20 @@ const handleLoad = async () => {
|
||||
formMessage.value = "";
|
||||
events = [];
|
||||
|
||||
if (Object.keys(query).length == 1 && Object.keys(query)[0] == "limit") {
|
||||
if (Object.keys(query).length == 0) {
|
||||
const now = new Date();
|
||||
const startingDate = new Date(now.setDate(now.getDate() - 14));
|
||||
|
||||
query["end_at"] =
|
||||
">" +
|
||||
new SMDate("now").format("yyyy/MM/dd HH:mm:ss", { utc: true });
|
||||
new SMDate(startingDate).format("yyyy/MM/dd HH:mm:ss", {
|
||||
utc: true,
|
||||
});
|
||||
}
|
||||
|
||||
query["limit"] = postsPerPage;
|
||||
query["page"] = postsPage.value;
|
||||
|
||||
api.get({
|
||||
url: "/events",
|
||||
params: query,
|
||||
@@ -129,19 +153,50 @@ const handleLoad = async () => {
|
||||
.then((result) => {
|
||||
const data = result.data as EventCollection;
|
||||
|
||||
postsTotal.value = data.total;
|
||||
|
||||
if (data && data.events) {
|
||||
events = data.events;
|
||||
events = [];
|
||||
|
||||
events.forEach((item) => {
|
||||
item.start_at = new SMDate(item.start_at, {
|
||||
data.events.forEach((item) => {
|
||||
let banner = "";
|
||||
let bannerType = "";
|
||||
|
||||
const parsedStartAt = new SMDate(item.start_at, {
|
||||
format: "yyyy-MM-dd HH:mm:ss",
|
||||
utc: true,
|
||||
}).format("yyyy-MM-dd HH:mm:ss");
|
||||
});
|
||||
|
||||
item.end_at = new SMDate(item.end_at, {
|
||||
const parsedEndAt = new SMDate(item.end_at, {
|
||||
format: "yyyy-MM-dd HH:mm:ss",
|
||||
utc: true,
|
||||
}).format("yyyy-MM-dd HH:mm:ss");
|
||||
});
|
||||
|
||||
item.start_at = parsedStartAt.format("yyyy-MM-dd HH:mm:ss");
|
||||
item.end_at = parsedEndAt.format("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
if (
|
||||
parsedEndAt.isBefore(new SMDate("now")) ||
|
||||
item.status == "closed"
|
||||
) {
|
||||
banner = "closed";
|
||||
bannerType = "expired";
|
||||
} else if (item.status == "open") {
|
||||
banner = "open";
|
||||
bannerType = "success";
|
||||
} else if (item.status == "cancelled") {
|
||||
banner = "cancelled";
|
||||
bannerType = "danger";
|
||||
} else if (item.status == "soon") {
|
||||
banner = "Open Soon";
|
||||
bannerType = "warning";
|
||||
}
|
||||
|
||||
events.push({
|
||||
event: item,
|
||||
banner: banner,
|
||||
bannerType: bannerType,
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -161,6 +216,13 @@ const handleFilter = async () => {
|
||||
handleLoad();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => postsPage.value,
|
||||
() => {
|
||||
handleLoad();
|
||||
}
|
||||
);
|
||||
|
||||
handleLoad();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -187,37 +187,37 @@ const registerUrl = computed(() => {
|
||||
const handleLoad = async () => {
|
||||
formMessage.value = "";
|
||||
|
||||
api.get({
|
||||
url: "/events/{event}",
|
||||
params: {
|
||||
event: route.params.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
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,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
event.value.end_at = new SMDate(event.value.end_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
|
||||
applicationStore.setDynamicTitle(event.value.title);
|
||||
handleLoadImage();
|
||||
} else {
|
||||
pageError = 404;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
formMessage.value =
|
||||
error.data?.message ||
|
||||
"Could not load event information from the server.";
|
||||
try {
|
||||
let result = await api.get({
|
||||
url: "/events/{event}",
|
||||
params: {
|
||||
event: route.params.id,
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
event.value.end_at = new SMDate(event.value.end_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
|
||||
applicationStore.setDynamicTitle(event.value.title);
|
||||
handleLoadImage();
|
||||
} else {
|
||||
pageError = 404;
|
||||
}
|
||||
} catch (error) {
|
||||
formMessage.value =
|
||||
error.data?.message ||
|
||||
"Could not load event information from the server.";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -152,65 +152,66 @@ const handleLoad = async () => {
|
||||
let posts = [];
|
||||
let events = [];
|
||||
|
||||
api.get({
|
||||
url: "/posts",
|
||||
params: {
|
||||
limit: 3,
|
||||
},
|
||||
})
|
||||
.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),
|
||||
image: post.hero,
|
||||
url: { name: "post-view", params: { slug: post.slug } },
|
||||
cta: "Read More...",
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
try {
|
||||
const result = await api.get({
|
||||
url: "/posts",
|
||||
params: {
|
||||
limit: 3,
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
const data = result.data as PostCollection;
|
||||
|
||||
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",
|
||||
});
|
||||
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 */
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api.get({
|
||||
url: "/events",
|
||||
params: {
|
||||
limit: 3,
|
||||
end_at:
|
||||
">" +
|
||||
new SMDate("now").format("yyyy-MM-dd HH:mm:ss", {
|
||||
utc: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
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 {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
for (let i = 1; i <= Math.max(posts.length, events.length); i++) {
|
||||
if (i <= posts.length) {
|
||||
slides.value.push(posts[i - 1]);
|
||||
}
|
||||
|
||||
if (i <= events.length) {
|
||||
slides.value.push(events[i - 1]);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</SMRow>
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<div>
|
||||
<div class="small">
|
||||
<span class="pr-1"
|
||||
>Already have an account?</span
|
||||
><router-link to="/login"
|
||||
@@ -92,42 +92,33 @@ import {
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
let abortController: AbortController | null = null;
|
||||
|
||||
const checkUsername = (value: string): boolean | string => {
|
||||
if (lastUsernameCheck.value != value) {
|
||||
lastUsernameCheck.value = value;
|
||||
const checkUsername = async (value: string): Promise<boolean | string> => {
|
||||
try {
|
||||
if (lastUsernameCheck.value != value) {
|
||||
lastUsernameCheck.value = value;
|
||||
|
||||
if (abortController != null) {
|
||||
abortController.abort();
|
||||
abortController = null;
|
||||
if (abortController != null) {
|
||||
abortController.abort();
|
||||
abortController = null;
|
||||
}
|
||||
|
||||
abortController = new AbortController();
|
||||
|
||||
await api.get({
|
||||
url: "/users",
|
||||
params: {
|
||||
username: `=${value}`,
|
||||
},
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
return "The username has already been taken.";
|
||||
}
|
||||
|
||||
abortController = new AbortController();
|
||||
|
||||
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.";
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
if (error.status != 404) {
|
||||
return (
|
||||
error.json?.message ||
|
||||
"An unexpected server error occurred."
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const formDone = ref(false);
|
||||
|
||||
@@ -30,7 +30,10 @@
|
||||
</template>
|
||||
<template #item-title="item">
|
||||
<router-link
|
||||
:to="{ name: 'event-edit', params: { id: item.id } }"
|
||||
:to="{
|
||||
name: 'dashboard-event-edit',
|
||||
params: { id: item.id },
|
||||
}"
|
||||
>{{ item.title }}</router-link
|
||||
>
|
||||
</template>
|
||||
@@ -164,15 +167,15 @@ watch(search, () => {
|
||||
});
|
||||
|
||||
const handleClickRow = (item) => {
|
||||
router.push({ name: "event-edit", params: { id: item.id } });
|
||||
router.push({ name: "dashboard-event-edit", params: { id: item.id } });
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
router.push({ name: "event-create" });
|
||||
router.push({ name: "dashboard-event-create" });
|
||||
};
|
||||
|
||||
const handleEdit = (item) => {
|
||||
router.push({ name: "event-edit", params: { id: item.id } });
|
||||
router.push({ name: "dashboard-event-edit", params: { id: item.id } });
|
||||
};
|
||||
|
||||
const handleDelete = async (item) => {
|
||||
|
||||
@@ -178,11 +178,11 @@ watch(search, (value) => {
|
||||
});
|
||||
|
||||
const handleClickRow = (item) => {
|
||||
router.push({ name: "media-edit", params: { id: item.id } });
|
||||
router.push({ name: "dashboard-media-edit", params: { id: item.id } });
|
||||
};
|
||||
|
||||
const handleEdit = (item) => {
|
||||
router.push({ name: "media-edit", params: { id: item.id } });
|
||||
router.push({ name: "dashboard-media-edit", params: { id: item.id } });
|
||||
};
|
||||
|
||||
const handleDelete = async (item) => {
|
||||
|
||||
@@ -12,10 +12,15 @@
|
||||
<SMButton
|
||||
type="primary"
|
||||
label="Create Post"
|
||||
:small="true"
|
||||
@click="handleCreate" />
|
||||
</template>
|
||||
<template #right>
|
||||
<input v-model="search" placeholder="Search" />
|
||||
<SMInput
|
||||
v-model="search"
|
||||
label="Search"
|
||||
:small="true"
|
||||
style="max-width: 250px" />
|
||||
</template>
|
||||
</SMToolbar>
|
||||
|
||||
@@ -31,7 +36,10 @@
|
||||
</template>
|
||||
<template #item-title="item">
|
||||
<router-link
|
||||
:to="{ name: 'post-edit', params: { id: item.id } }"
|
||||
:to="{
|
||||
name: 'dashboard-post-edit',
|
||||
params: { id: item.id },
|
||||
}"
|
||||
>{{ item.title }}</router-link
|
||||
>
|
||||
</template>
|
||||
@@ -58,6 +66,7 @@ import { openDialog } from "vue3-promise-dialog";
|
||||
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMHeading from "../../components/SMHeading.vue";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||
import SMMessage from "../../components/SMMessage.vue";
|
||||
import SMToolbar from "../../components/SMToolbar.vue";
|
||||
@@ -181,15 +190,15 @@ watch(search, () => {
|
||||
});
|
||||
|
||||
const handleClickRow = (item) => {
|
||||
router.push({ name: "post-edit", params: { id: item.id } });
|
||||
router.push({ name: "dashboard-post-edit", params: { id: item.id } });
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
router.push({ name: "post-create" });
|
||||
router.push({ name: "dashboard-post-create" });
|
||||
};
|
||||
|
||||
const handleEdit = (item) => {
|
||||
router.push({ name: "post-edit", params: { id: item.id } });
|
||||
router.push({ name: "dashboard-post-edit", params: { id: item.id } });
|
||||
};
|
||||
|
||||
const handleDelete = async (item) => {
|
||||
|
||||
@@ -134,7 +134,7 @@ const bodyItemClassNameFunction = (column) => {
|
||||
};
|
||||
|
||||
const handleEdit = (user) => {
|
||||
router.push({ name: "user-edit", params: { id: user.id } });
|
||||
router.push({ name: "dashboard-user-edit", params: { id: user.id } });
|
||||
};
|
||||
|
||||
const handleDelete = async (user) => {
|
||||
|
||||
Reference in New Issue
Block a user