updates
This commit is contained in:
@@ -165,9 +165,10 @@ abstract class FilterAbstract
|
||||
*
|
||||
* @param array $attributes Attributes currently visible.
|
||||
* @param User|null $user Current logged in user or null.
|
||||
* @param object $modelData Model data if a single object is requested.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function seeAttributes(array $attributes, mixed $user)
|
||||
protected function seeAttributes(array $attributes, mixed $user, ?object $modelData = null)
|
||||
{
|
||||
return $attributes;
|
||||
}
|
||||
@@ -224,7 +225,7 @@ abstract class FilterAbstract
|
||||
}
|
||||
|
||||
/* Run attribute modifiers*/
|
||||
$modifiedAttribs = $this->seeAttributes($attributes, $this->request->user());
|
||||
$modifiedAttribs = $this->seeAttributes($attributes, $this->request->user(), $model);
|
||||
if (is_array($modifiedAttribs) === true) {
|
||||
$attributes = $modifiedAttribs;
|
||||
}
|
||||
|
||||
@@ -19,11 +19,12 @@ class UserFilter extends FilterAbstract
|
||||
*
|
||||
* @param array $attributes Attributes currently visible.
|
||||
* @param User|null $user Current logged in user or null.
|
||||
* @param object $userData User model if single object is requested.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function seeAttributes(array $attributes, mixed $user)
|
||||
protected function seeAttributes(array $attributes, mixed $user, ?object $userData = null)
|
||||
{
|
||||
if ($user?->hasPermission('admin/users') !== true) {
|
||||
if ($user?->hasPermission('admin/users') !== true && ($user === null || $userData === null || $user?->id !== $userData?->id)) {
|
||||
return ['id', 'username'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,31 +129,31 @@ const handleClickItem = (item: string) => {
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
}
|
||||
|
||||
// New content here
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ul {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -56,7 +56,7 @@ const handleMouseLeave = () => {
|
||||
|
||||
const handleSlidePrev = () => {
|
||||
if (currentSlide.value == 0) {
|
||||
currentSlide.value = maxSlide;
|
||||
currentSlide.value = maxSlide.value;
|
||||
} else {
|
||||
currentSlide.value--;
|
||||
}
|
||||
@@ -165,10 +165,12 @@ const disconnectMutationObserver = () => {
|
||||
|
||||
.carousel-slide-prev {
|
||||
left: 1rem;
|
||||
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
|
||||
}
|
||||
|
||||
.carousel-slide-next {
|
||||
right: 1rem;
|
||||
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
|
||||
}
|
||||
|
||||
.carousel-slide-indicators {
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { ApiMedia } from "../helpers/api.types";
|
||||
import { imageLoad } from "../helpers/image";
|
||||
import SMButton from "./SMButton.vue";
|
||||
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||
|
||||
@@ -53,17 +55,20 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
let imageUrl = ref(null);
|
||||
let imageUrl = ref("");
|
||||
|
||||
const handleLoad = async () => {
|
||||
try {
|
||||
let result = await api.get(`/media/${props.image}`);
|
||||
if (result.json.medium) {
|
||||
imageUrl.value = result.json.medium.url;
|
||||
const handleLoad = () => {
|
||||
imageUrl.value = "";
|
||||
|
||||
api.get(`/media/${props.image}`).then((result) => {
|
||||
const data = result.data as ApiMedia;
|
||||
|
||||
if (data && data.medium) {
|
||||
imageLoad(data.medium.url, (url) => {
|
||||
imageUrl.value = url;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
imageUrl.value = "";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleLoad();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
'dialog',
|
||||
{ 'dialog-narrow': narrow },
|
||||
{ 'dialog-full': full },
|
||||
{ 'dialog-noshadow': noShadow },
|
||||
]">
|
||||
<transition name="fade">
|
||||
<div v-if="loading" class="dialog-loading-cover">
|
||||
@@ -37,6 +38,10 @@ defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
noShadow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -54,6 +59,10 @@ defineProps({
|
||||
min-width: map-get($spacer, 5) * 12;
|
||||
box-shadow: 4px 4px 20px rgba(0, 0, 0, 0.5);
|
||||
|
||||
&.dialog-noshadow {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
& > h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
type == 'email' ||
|
||||
type == 'password' ||
|
||||
type == 'email' ||
|
||||
type == 'url'
|
||||
type == 'url' ||
|
||||
type == 'daterange'
|
||||
"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
@@ -130,8 +131,8 @@ const objForm = inject("form", props.form);
|
||||
const objControl =
|
||||
!isEmpty(objForm) && props.control != "" ? objForm[props.control] : null;
|
||||
|
||||
const label = ref("");
|
||||
const feedbackInvalid = ref("");
|
||||
const label = ref(props.label);
|
||||
const feedbackInvalid = ref(props.feedbackInvalid);
|
||||
|
||||
watch(
|
||||
() => props.label,
|
||||
@@ -238,6 +239,7 @@ const inline = computed(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: map-get($spacer, 4);
|
||||
flex: 1;
|
||||
|
||||
&.sm-input-active {
|
||||
label {
|
||||
|
||||
@@ -73,7 +73,7 @@ const menuItems = [
|
||||
name: "workshops",
|
||||
label: "Workshops",
|
||||
to: "/workshops",
|
||||
icon: "shapes-outline",
|
||||
icon: "library-outline",
|
||||
},
|
||||
// {
|
||||
// name: "courses",
|
||||
@@ -107,7 +107,7 @@ const menuItems = [
|
||||
name: "dashboard",
|
||||
label: "Dashboard",
|
||||
to: "/dashboard",
|
||||
icon: "apps-outline",
|
||||
icon: "grid-outline",
|
||||
show: () => userStore.id,
|
||||
inNav: false,
|
||||
},
|
||||
|
||||
@@ -13,18 +13,23 @@
|
||||
class="sm-page"
|
||||
:style="styleObject">
|
||||
<slot></slot>
|
||||
<SMContainer v-if="slots.container"
|
||||
><slot name="container"></slot
|
||||
></SMContainer>
|
||||
</div>
|
||||
</SMLoader>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
import { useSlots } from "vue";
|
||||
import SMLoader from "./SMLoader.vue";
|
||||
import SMErrorForbidden from "./errors/Forbidden.vue";
|
||||
import SMErrorInternal from "./errors/Internal.vue";
|
||||
import SMErrorNotFound from "./errors/NotFound.vue";
|
||||
import SMBreadcrumbs from "../components/SMBreadcrumbs.vue";
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
import SMContainer from "./SMContainer.vue";
|
||||
|
||||
const props = defineProps({
|
||||
pageError: {
|
||||
@@ -53,6 +58,8 @@ const props = defineProps({
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
const userStore = useUserStore();
|
||||
let styleObject = {};
|
||||
|
||||
@@ -74,7 +81,7 @@ const hasPermission = () => {
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
margin-bottom: calc(map-get($spacer, 5) * 2);
|
||||
padding-bottom: calc(map-get($spacer, 5) * 2);
|
||||
|
||||
&.sm-no-breadcrumbs {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -46,8 +46,10 @@ import {
|
||||
stripHtmlTags,
|
||||
} from "../helpers/common";
|
||||
import { format } from "date-fns";
|
||||
import SMButton from "./SMButton.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { imageLoad } from "../helpers/image";
|
||||
import SMButton from "./SMButton.vue";
|
||||
import { ApiMedia } from "../helpers/api.types";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
@@ -149,15 +151,15 @@ const hideImageLoader = computed(() => {
|
||||
|
||||
onMounted(async () => {
|
||||
if (imageUrl.value && imageUrl.value.length > 0 && isUUID(imageUrl.value)) {
|
||||
try {
|
||||
let result = await api.get(`/media/${props.image}`);
|
||||
api.get(`/media/${props.image}`).then((result) => {
|
||||
const data = result.data as ApiMedia;
|
||||
|
||||
if (result.json.medium) {
|
||||
imageUrl.value = result.json.medium.url;
|
||||
if (data && data.medium) {
|
||||
imageLoad(data.medium.url, (url) => {
|
||||
imageUrl.value = url;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -168,6 +170,7 @@ onMounted(async () => {
|
||||
flex-direction: column;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 28px rgba(0, 0, 0, 0.05);
|
||||
max-width: 21rem;
|
||||
width: 100%;
|
||||
@@ -227,6 +230,7 @@ onMounted(async () => {
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 0 map-get($spacer, 3) map-get($spacer, 3) map-get($spacer, 3);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
@@ -241,7 +245,7 @@ onMounted(async () => {
|
||||
font-size: 80%;
|
||||
margin-bottom: 0.4rem;
|
||||
|
||||
svg {
|
||||
ion-icon {
|
||||
flex: 0 1 1rem;
|
||||
margin-right: map-get($spacer, 1);
|
||||
padding-top: 0.1rem;
|
||||
|
||||
@@ -2,36 +2,39 @@
|
||||
<SMModal>
|
||||
<SMDialog :loading="formLoading">
|
||||
<h1>Change Password</h1>
|
||||
<SMMessage
|
||||
v-if="isSuccessful"
|
||||
type="success"
|
||||
message="Your password has been changed successfully" />
|
||||
<SMInput
|
||||
v-if="!isSuccessful"
|
||||
v-model="formData.password.value"
|
||||
type="password"
|
||||
label="New Password"
|
||||
required
|
||||
:error="formData.password.error" />
|
||||
<SMFormFooter>
|
||||
<template v-if="!isSuccessful" #left>
|
||||
<SMButton
|
||||
type="secondary"
|
||||
label="Cancel"
|
||||
@click="handleCancel()" />
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton
|
||||
type="primary"
|
||||
:label="btnConfirm"
|
||||
@click="handleConfirm()" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
<SMForm v-model="form">
|
||||
<SMMessage
|
||||
v-if="isSuccessful"
|
||||
type="success"
|
||||
message="Your password has been changed successfully" />
|
||||
<SMInput
|
||||
v-if="!isSuccessful"
|
||||
control="password"
|
||||
type="password"
|
||||
label="New Password" />
|
||||
<SMFormFooter>
|
||||
<template v-if="!isSuccessful" #left>
|
||||
<SMButton
|
||||
type="secondary"
|
||||
label="Cancel"
|
||||
@click="handleCancel()" />
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton
|
||||
type="primary"
|
||||
:label="btnConfirm"
|
||||
@click="handleConfirm()" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</SMForm>
|
||||
</SMDialog>
|
||||
</SMModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { api } from "../../helpers/api";
|
||||
import { FormControl } from "../../helpers/form";
|
||||
import { And, Required, Password } from "../../helpers/validate";
|
||||
import { useUserStore } from "../../store/UserStore";
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
|
||||
import { closeDialog } from "vue3-promise-dialog";
|
||||
@@ -42,20 +45,9 @@ import SMButton from "../SMButton.vue";
|
||||
import SMFormFooter from "../SMFormFooter.vue";
|
||||
import SMInput from "../SMInput.vue";
|
||||
|
||||
const formData = reactive({
|
||||
password: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A password is needed",
|
||||
min: 8,
|
||||
min_message: "Your password needs to be at least %d characters",
|
||||
password: "special",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const controlPassword = reactive(
|
||||
FormControl("", And([Required(), Password()]))
|
||||
);
|
||||
const userStore = useUserStore();
|
||||
const formLoading = ref(false);
|
||||
const isSuccessful = ref(false);
|
||||
@@ -72,18 +64,20 @@ const handleConfirm = async () => {
|
||||
if (isSuccessful.value == true) {
|
||||
closeDialog(true);
|
||||
} else {
|
||||
const valid = controlPassword.validate();
|
||||
|
||||
try {
|
||||
formLoading.value = true;
|
||||
await api.put({
|
||||
url: `/users/${userStore.id}`,
|
||||
body: {
|
||||
password: formData.password.value,
|
||||
password: controlPassword.value,
|
||||
},
|
||||
});
|
||||
|
||||
isSuccessful.value = true;
|
||||
} catch (err) {
|
||||
formData.password.error =
|
||||
controlPassword.error =
|
||||
err.json?.message || "An unexpected error occurred";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ interface ApiOptions {
|
||||
progress?: ApiProgressCallback;
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
export interface ApiResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
data: { [key: string]: unknown };
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
const apiDefaultHeaders = {
|
||||
|
||||
16
resources/js/helpers/api.types.ts
Normal file
16
resources/js/helpers/api.types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface ApiEventItem {
|
||||
start_at: string;
|
||||
end_at: string;
|
||||
}
|
||||
|
||||
export interface ApiEvent {
|
||||
event: ApiEventItem;
|
||||
}
|
||||
|
||||
export interface ApiMediaItem {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ApiMedia {
|
||||
medium: ApiMediaItem;
|
||||
}
|
||||
@@ -1,319 +1,443 @@
|
||||
import { isString } from "../helpers/common";
|
||||
export class SMDate {
|
||||
date: Date | null = null;
|
||||
dayString: string[] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
|
||||
export const dayString = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
fullDayString: string[] = [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
];
|
||||
|
||||
export const fullDayString = [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tueday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
];
|
||||
monthString: string[] = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
];
|
||||
|
||||
export const monthString = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
];
|
||||
fullMonthString: string[] = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
export const fullMonthString = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
export const format = (objDate: Date, format: string): string => {
|
||||
const result = format;
|
||||
|
||||
const year = objDate.getFullYear().toString();
|
||||
const month = (objDate.getMonth() + 1).toString();
|
||||
const date = objDate.getDate().toString();
|
||||
const day = objDate.getDay().toString();
|
||||
const hour = objDate.getHours().toString();
|
||||
const min = objDate.getMinutes().toString();
|
||||
const sec = objDate.getSeconds().toString();
|
||||
|
||||
const apm = objDate.getHours() >= 12 ? "am" : "pm";
|
||||
/* eslint-disable indent */
|
||||
const apmhours = (
|
||||
objDate.getHours() > 12
|
||||
? objDate.getHours() - 12
|
||||
: objDate.getHours() == 0
|
||||
? 12
|
||||
: objDate.getHours()
|
||||
).toString();
|
||||
/* eslint-enable indent */
|
||||
|
||||
// year
|
||||
result.replace(/\byy\b/g, year.slice(-2));
|
||||
result.replace(/\byyyy\b/g, year);
|
||||
|
||||
// month
|
||||
result.replace(/\bM\b/g, month);
|
||||
result.replace(/\bMM\b/g, (0 + month).slice(-2));
|
||||
result.replace(/\bMMM\b/g, monthString[month]);
|
||||
result.replace(/\bMMMM\b/g, fullMonthString[month]);
|
||||
|
||||
// day
|
||||
result.replace(/\bd\b/g, date);
|
||||
result.replace(/\bdd\b/g, (0 + date).slice(-2));
|
||||
result.replace(/\bddd\b/g, dayString[day]);
|
||||
result.replace(/\bdddd\b/g, fullDayString[day]);
|
||||
|
||||
// hour
|
||||
result.replace(/\bH\b/g, hour);
|
||||
result.replace(/\bHH\b/g, (0 + hour).slice(-2));
|
||||
result.replace(/\bh\b/g, apmhours);
|
||||
result.replace(/\bhh\b/g, (0 + apmhours).slice(-2));
|
||||
|
||||
// min
|
||||
result.replace(/\bm\b/g, min);
|
||||
result.replace(/\bmm\b/g, (0 + min).slice(-2));
|
||||
|
||||
// sec
|
||||
result.replace(/\bs\b/g, sec);
|
||||
result.replace(/\bss\b/g, (0 + sec).slice(-2));
|
||||
|
||||
// am/pm
|
||||
result.replace(/\baa\b/g, apm);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const timestampUtcToLocal = (utc: string): string => {
|
||||
try {
|
||||
const iso = new Date(
|
||||
utc.replace(
|
||||
/([0-9]{4}-[0-9]{2}-[0-9]{2}),? ([0-9]{2}:[0-9]{2}:[0-9]{2})/,
|
||||
"$1T$2.000Z"
|
||||
)
|
||||
);
|
||||
return format(iso, "yyyy/MM/dd HH:mm:ss");
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const timestampLocalToUtc = (local) => {
|
||||
try {
|
||||
const d = new Date(local);
|
||||
return d
|
||||
.toISOString()
|
||||
.replace(
|
||||
/([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2}:[0-9]{2}).*/,
|
||||
"$1 $2"
|
||||
);
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const timestampNowLocal = () => {
|
||||
const d = new Date();
|
||||
return (
|
||||
d.getFullYear() +
|
||||
"-" +
|
||||
("0" + (d.getMonth() + 1)).slice(-2) +
|
||||
"-" +
|
||||
("0" + d.getDate()).slice(-2) +
|
||||
" " +
|
||||
("0" + d.getHours()).slice(-2) +
|
||||
":" +
|
||||
("0" + d.getMinutes()).slice(-2) +
|
||||
":" +
|
||||
("0" + d.getSeconds()).slice(-2)
|
||||
);
|
||||
};
|
||||
|
||||
export const timestampNowUtc = () => {
|
||||
try {
|
||||
const d = new Date();
|
||||
return d
|
||||
.toISOString()
|
||||
.replace(
|
||||
/([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2}:[0-9]{2}).*/,
|
||||
"$1 $2"
|
||||
);
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const timestampBeforeNow = (timestamp) => {
|
||||
try {
|
||||
return new Date(timestamp) < new Date();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const timestampAfterNow = (timestamp) => {
|
||||
try {
|
||||
return new Date(timestamp) > new Date();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const relativeDate = (d) => {
|
||||
if (isString(d)) {
|
||||
d = new Date(d);
|
||||
}
|
||||
|
||||
// const d = new Date(0);
|
||||
// // d.setUTCSeconds(parseInt(epoch));
|
||||
// d.setUTCSeconds(epoch);
|
||||
|
||||
const now = new Date();
|
||||
const dif = Math.round((now.getTime() - d.getTime()) / 1000);
|
||||
|
||||
if (dif < 60) {
|
||||
// let v = dif;
|
||||
// return v + " sec" + (v != 1 ? "s" : "") + " ago";
|
||||
return "Just now";
|
||||
} else if (dif < 3600) {
|
||||
const v = Math.round(dif / 60);
|
||||
return v + " min" + (v != 1 ? "s" : "") + " ago";
|
||||
} else if (dif < 86400) {
|
||||
const v = Math.round(dif / 3600);
|
||||
return v + " hour" + (v != 1 ? "s" : "") + " ago";
|
||||
} else if (dif < 604800) {
|
||||
const v = Math.round(dif / 86400);
|
||||
return v + " day" + (v != 1 ? "s" : "") + " ago";
|
||||
} else if (dif < 2419200) {
|
||||
const v = Math.round(dif / 604800);
|
||||
return v + " week" + (v != 1 ? "s" : "") + " ago";
|
||||
}
|
||||
|
||||
return (
|
||||
monthString[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear()
|
||||
);
|
||||
};
|
||||
|
||||
export const isValidAusDate = (dateString: string): boolean => {
|
||||
const [day, month, year] = dateString.split("/");
|
||||
const date = new Date(`${year}-${month}-${day}`);
|
||||
return (
|
||||
!isNaN(date.getTime()) &&
|
||||
date.toISOString().slice(0, 10) === `${year}-${month}-${day}`
|
||||
);
|
||||
};
|
||||
|
||||
export const parseAusDate = (dateString: string): Date | null => {
|
||||
const [day, month, year] = dateString.split("/");
|
||||
const date = new Date(`${year}-${month}-${day}`);
|
||||
if (
|
||||
!isNaN(date.getTime()) &&
|
||||
date.toISOString().slice(0, 10) === `${year}-${month}-${day}`
|
||||
constructor(
|
||||
dateOrString: string | Date = "",
|
||||
options: { format?: string; utc?: boolean } = {}
|
||||
) {
|
||||
return null;
|
||||
this.date = new Date();
|
||||
|
||||
if (typeof dateOrString === "string") {
|
||||
if (dateOrString.length > 0) {
|
||||
this.parse(dateOrString, options);
|
||||
}
|
||||
} else if (
|
||||
dateOrString instanceof Date &&
|
||||
!isNaN(dateOrString.getTime())
|
||||
) {
|
||||
this.date = dateOrString;
|
||||
}
|
||||
}
|
||||
|
||||
return date;
|
||||
};
|
||||
/**
|
||||
* Parse a string date into a Date object
|
||||
*
|
||||
* @param {string} dateString The date string.
|
||||
* @param {object} options (optional) Options object.
|
||||
* @param {string} options.format (optional) The format of the date string.
|
||||
* @param {boolean} options.utc (optional) The date string is UTC.
|
||||
* @returns {SMDate} SMDate object.
|
||||
*/
|
||||
public parse(
|
||||
dateString: string,
|
||||
options: { format?: string; utc?: boolean } = {}
|
||||
): SMDate {
|
||||
const now = new Date();
|
||||
|
||||
export const isValidTime = (timeString: string): boolean => {
|
||||
return /^([01]\d|2[0-3]):[0-5]\d$/.test(timeString);
|
||||
};
|
||||
// Parse the date format to determine the order of the date components
|
||||
const order = (options.format || "dmy").toLowerCase().split("");
|
||||
options.utc = options.utc || false;
|
||||
|
||||
export const convertTimeToMinutes = (timeString: string): number => {
|
||||
if (isValidTime(timeString)) {
|
||||
const [hour, minute] = timeString
|
||||
.split(":")
|
||||
.map((str) => parseInt(str, 10));
|
||||
return hour * 60 + minute;
|
||||
// Split the date string into an array of components based on the length of each date component
|
||||
const components = dateString.split(/[ /-]/);
|
||||
let time = "";
|
||||
|
||||
for (let i = 0; i < components.length; i++) {
|
||||
if (components[i].includes(":")) {
|
||||
time = components[i];
|
||||
components.splice(i, 1);
|
||||
if (i < components.length && /^(am|pm)$/i.test(components[i])) {
|
||||
time += " " + components[i].toUpperCase();
|
||||
components.splice(i, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Map the date components to the expected order based on the format
|
||||
const [day, month, year] =
|
||||
order[0] === "d"
|
||||
? [components[0], components[1], components[2]]
|
||||
: order[0] === "m"
|
||||
? [components[1], components[0], components[2]]
|
||||
: [components[2], components[1], components[0]];
|
||||
|
||||
let parsedDay: number = 0,
|
||||
parsedMonth: number = 0,
|
||||
parsedYear: number = 0;
|
||||
|
||||
if (day && day.length != 0 && month && month.length != 0) {
|
||||
// Parse the day, month, and year components
|
||||
parsedDay = parseInt(day.padStart(2, "0"), 10);
|
||||
parsedMonth = this.getMonthAsNumber(month);
|
||||
parsedYear = year
|
||||
? parseInt(year.padStart(4, "20"), 10)
|
||||
: now.getFullYear();
|
||||
} else {
|
||||
parsedDay = now.getDate();
|
||||
parsedMonth = now.getMonth() + 1;
|
||||
parsedYear = now.getFullYear();
|
||||
}
|
||||
|
||||
let parsedHours: number = 0,
|
||||
parsedMinutes: number = 0,
|
||||
parsedSeconds: number = 0;
|
||||
if (time) {
|
||||
const match = time.match(/(\d+)(?::(\d+))?(?::(\d+))? ?(AM|PM)?/i);
|
||||
if (match) {
|
||||
parsedHours = parseInt(match[1]);
|
||||
parsedMinutes = match[2] ? parseInt(match[2]) : 0;
|
||||
parsedSeconds = match[3] ? parseInt(match[3]) : 0;
|
||||
if (match[4] && /pm/i.test(match[4])) {
|
||||
parsedHours += 12;
|
||||
}
|
||||
if (match[4] && /am/i.test(match[4]) && parsedHours === 12) {
|
||||
parsedHours = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a date object with the parsed components
|
||||
let date: Date | null = null;
|
||||
if (options.utc) {
|
||||
date = new Date(
|
||||
Date.UTC(
|
||||
parsedYear,
|
||||
parsedMonth - 1,
|
||||
parsedDay,
|
||||
parsedHours,
|
||||
parsedMinutes,
|
||||
parsedSeconds
|
||||
)
|
||||
);
|
||||
} else {
|
||||
date = new Date(
|
||||
parsedYear,
|
||||
parsedMonth - 1,
|
||||
parsedDay,
|
||||
parsedHours,
|
||||
parsedMinutes,
|
||||
parsedSeconds
|
||||
);
|
||||
}
|
||||
|
||||
// Test created date object
|
||||
let checkYear: number,
|
||||
checkMonth: number,
|
||||
checkDay: number,
|
||||
checkHours: number,
|
||||
checkMinutes: number,
|
||||
checkSeconds: number;
|
||||
if (options.utc) {
|
||||
const isoDate = date.toISOString();
|
||||
checkYear = parseInt(isoDate.substring(0, 4), 10);
|
||||
checkMonth = parseInt(isoDate.substring(5, 7), 10);
|
||||
checkDay = new Date(isoDate).getUTCDate();
|
||||
checkHours = parseInt(isoDate.substring(11, 13), 10);
|
||||
checkMinutes = parseInt(isoDate.substring(14, 16), 10);
|
||||
checkSeconds = parseInt(isoDate.substring(17, 18), 10);
|
||||
} else {
|
||||
checkYear = date.getFullYear();
|
||||
checkMonth = date.getMonth() + 1;
|
||||
checkDay = date.getDate();
|
||||
checkHours = date.getHours();
|
||||
checkMinutes = date.getMinutes();
|
||||
checkSeconds = date.getSeconds();
|
||||
}
|
||||
|
||||
if (
|
||||
isNaN(date.getTime()) == false &&
|
||||
checkYear == parsedYear &&
|
||||
checkMonth == parsedMonth &&
|
||||
checkDay == parsedDay &&
|
||||
checkHours == parsedHours &&
|
||||
checkMinutes == parsedMinutes &&
|
||||
checkSeconds == parsedSeconds
|
||||
) {
|
||||
this.date = date;
|
||||
} else {
|
||||
this.date = null;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
/**
|
||||
* Format the date to a string.
|
||||
*
|
||||
* @param {string} format The format to return.
|
||||
* @param {object} options (optional) Function options.
|
||||
* @param {boolean} options.utc (optional) Format the date to be as UTC instead of local.
|
||||
* @returns {string} The formatted date.
|
||||
*/
|
||||
public format(format: string, options: { utc?: boolean } = {}): string {
|
||||
if (this.date == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
export const createAusDateTimeObject = (
|
||||
dateString: string,
|
||||
timeString: string
|
||||
): Date | null => {
|
||||
const dateRegex =
|
||||
/^(0?[1-9]|[1-2][0-9]|3[0-1])\/(0?[1-9]|1[0-2])\/(19|20)\d{2}$/;
|
||||
const timeRegex = /^([01]\d|2[0-3]):[0-5]\d$/;
|
||||
let result = format;
|
||||
|
||||
if (!dateRegex.test(dateString) || !timeRegex.test(timeString)) {
|
||||
return null;
|
||||
}
|
||||
let year: string,
|
||||
month: string,
|
||||
date: string,
|
||||
day: number,
|
||||
hour: string,
|
||||
min: string,
|
||||
sec: string;
|
||||
if (options.utc) {
|
||||
const isoDate = this.date.toISOString();
|
||||
year = isoDate.substring(0, 4);
|
||||
month = isoDate.substring(5, 7);
|
||||
date = isoDate.substring(8, 10);
|
||||
day = new Date(isoDate).getUTCDay();
|
||||
hour = isoDate.substring(11, 13);
|
||||
min = isoDate.substring(14, 16);
|
||||
sec = isoDate.substring(17, 18);
|
||||
} else {
|
||||
year = this.date.getFullYear().toString();
|
||||
month = (this.date.getMonth() + 1).toString();
|
||||
date = this.date.getDate().toString();
|
||||
day = this.date.getDay();
|
||||
hour = this.date.getHours().toString();
|
||||
min = this.date.getMinutes().toString();
|
||||
sec = this.date.getSeconds().toString();
|
||||
}
|
||||
|
||||
const [day, month, year] = dateString
|
||||
.split("/")
|
||||
.map((str) => parseInt(str, 10));
|
||||
const [hour, minute] = timeString
|
||||
.split(":")
|
||||
.map((str) => parseInt(str, 10));
|
||||
return new Date(year, month - 1, day, hour, minute);
|
||||
};
|
||||
const apm = parseInt(hour, 10) >= 12 ? "pm" : "am";
|
||||
/* eslint-disable indent */
|
||||
const apmhours = (
|
||||
parseInt(hour, 10) > 12
|
||||
? parseInt(hour, 10) - 12
|
||||
: parseInt(hour, 10) == 0
|
||||
? 12
|
||||
: parseInt(hour, 10)
|
||||
).toString();
|
||||
/* eslint-enable indent */
|
||||
|
||||
export const parseAusDateTime = (dateTimeStr: string): Date | null => {
|
||||
const dateStr = dateTimeStr.split(" ")[0];
|
||||
const timeStr = dateTimeStr.split(" ")[1];
|
||||
// year
|
||||
result = result.replace(/\byy\b/g, year.slice(-2));
|
||||
result = result.replace(/\byyyy\b/g, year);
|
||||
|
||||
let year: number, month: number, day: number;
|
||||
const dateParts = dateStr.split("/");
|
||||
if (dateParts[2] && dateParts[2].length === 4) {
|
||||
// If year is in yyyy format
|
||||
year = +dateParts[2];
|
||||
month = +dateParts[1];
|
||||
day = +dateParts[0];
|
||||
} else {
|
||||
// If year is in yy format
|
||||
year = +(
|
||||
new Date().getFullYear().toString().substr(0, 2) + dateParts[2]
|
||||
// month
|
||||
result = result.replace(/\bM\b/g, month);
|
||||
result = result.replace(/\bMM\b/g, (0 + month).slice(-2));
|
||||
result = result.replace(
|
||||
/\bMMM\b/g,
|
||||
this.monthString[parseInt(month) - 1]
|
||||
);
|
||||
month = +dateParts[1];
|
||||
day = +dateParts[0];
|
||||
result = result.replace(
|
||||
/\bMMMM\b/g,
|
||||
this.fullMonthString[parseInt(month) - 1]
|
||||
);
|
||||
|
||||
// day
|
||||
result = result.replace(/\bd\b/g, date);
|
||||
result = result.replace(/\bdd\b/g, (0 + date).slice(-2));
|
||||
result = result.replace(/\bEEE\b/g, this.dayString[day]);
|
||||
result = result.replace(/\bEEEE\b/g, this.fullDayString[day]);
|
||||
|
||||
// hour
|
||||
result = result.replace(/\bH\b/g, hour);
|
||||
result = result.replace(/\bHH\b/g, (0 + hour).slice(-2));
|
||||
result = result.replace(/\bh\b/g, apmhours);
|
||||
result = result.replace(/\bhh\b/g, (0 + apmhours).slice(-2));
|
||||
|
||||
// min
|
||||
result = result.replace(/\bm\b/g, min);
|
||||
result = result.replace(/\bmm\b/g, (0 + min).slice(-2));
|
||||
|
||||
// sec
|
||||
result = result.replace(/\bs\b/g, sec);
|
||||
result = result.replace(/\bss\b/g, (0 + sec).slice(-2));
|
||||
|
||||
// am/pm
|
||||
result = result.replace(/\baa\b/g, apm);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
let hour = 0,
|
||||
minute = 0,
|
||||
second = 0;
|
||||
if (timeStr) {
|
||||
const timeParts = timeStr.split(":");
|
||||
hour = +timeParts[0];
|
||||
minute = +timeParts[1];
|
||||
|
||||
if (timeParts[2]) {
|
||||
second = +timeParts[2];
|
||||
/**
|
||||
* Return a relative date string from now.
|
||||
*
|
||||
* @returns {string} A relative date string.
|
||||
*/
|
||||
public relative(): string {
|
||||
if (this.date === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (dateTimeStr.toLowerCase().includes("pm") && hour < 12) {
|
||||
hour += 12;
|
||||
const now = new Date();
|
||||
const dif = Math.round((now.getTime() - this.date.getTime()) / 1000);
|
||||
|
||||
if (dif < 60) {
|
||||
// let v = dif;
|
||||
// return v + " sec" + (v != 1 ? "s" : "") + " ago";
|
||||
return "Just now";
|
||||
} else if (dif < 3600) {
|
||||
const v = Math.round(dif / 60);
|
||||
return v + " min" + (v != 1 ? "s" : "") + " ago";
|
||||
} else if (dif < 86400) {
|
||||
const v = Math.round(dif / 3600);
|
||||
return v + " hour" + (v != 1 ? "s" : "") + " ago";
|
||||
} else if (dif < 604800) {
|
||||
const v = Math.round(dif / 86400);
|
||||
return v + " day" + (v != 1 ? "s" : "") + " ago";
|
||||
} else if (dif < 2419200) {
|
||||
const v = Math.round(dif / 604800);
|
||||
return v + " week" + (v != 1 ? "s" : "") + " ago";
|
||||
}
|
||||
|
||||
return (
|
||||
this.monthString[this.date.getMonth()] +
|
||||
" " +
|
||||
this.date.getDate() +
|
||||
", " +
|
||||
this.date.getFullYear()
|
||||
);
|
||||
}
|
||||
|
||||
return new Date(year, month - 1, day, hour, minute, second);
|
||||
};
|
||||
/**
|
||||
* If the date is before the passed date.
|
||||
*
|
||||
* @param {Date|SMDate} d (optional) The date to check. If none, use now
|
||||
* @returns {boolean} If the date is before the passed date.
|
||||
*/
|
||||
public isBefore(d: Date | SMDate = new Date()): boolean {
|
||||
const otherDate = d instanceof SMDate ? d.date : d;
|
||||
if (otherDate == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return otherDate < otherDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the date is after the passed date.
|
||||
*
|
||||
* @param {Date|SMDate} d (optional) The date to check. If none, use now
|
||||
* @returns {boolean} If the date is after the passed date.
|
||||
*/
|
||||
public isAfter(d: Date | SMDate = new Date()): boolean {
|
||||
const otherDate = d instanceof SMDate ? d.date : d;
|
||||
if (otherDate == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return otherDate > otherDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a month number from a string or a month number or month name
|
||||
*
|
||||
* @param {string} monthString The month string as number or name
|
||||
* @returns {number} The month number
|
||||
*/
|
||||
private getMonthAsNumber(monthString: string): number {
|
||||
const months = this.fullMonthString.map((month) => month.toLowerCase());
|
||||
|
||||
const shortMonths = months.map((month) => month.slice(0, 3));
|
||||
const monthIndex = months.indexOf(monthString.toLowerCase());
|
||||
if (monthIndex !== -1) {
|
||||
return monthIndex + 1;
|
||||
}
|
||||
const shortMonthIndex = shortMonths.indexOf(monthString.toLowerCase());
|
||||
if (shortMonthIndex !== -1) {
|
||||
return shortMonthIndex + 1;
|
||||
}
|
||||
const monthNumber = parseInt(monthString, 10);
|
||||
if (!isNaN(monthNumber) && monthNumber >= 1 && monthNumber <= 12) {
|
||||
return monthNumber;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the current date is valid.
|
||||
*
|
||||
* @returns {boolean} If the current date is valid.
|
||||
*/
|
||||
public isValid(): boolean {
|
||||
return this.date !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string with only the first occurrence of characters
|
||||
*
|
||||
* @param {string} str The string to modify.
|
||||
* @param {string} characters The characters to use to test.
|
||||
* @returns {string} A string that only contains the first occurrence of the characters.
|
||||
*/
|
||||
private onlyFirstOccurrence(
|
||||
str: string,
|
||||
characters: string = "dMy"
|
||||
): string {
|
||||
let findCharacters = characters.split("");
|
||||
const replaceRegex = new RegExp("[^" + characters + "]", "g");
|
||||
let result = "";
|
||||
|
||||
str = str.replace(replaceRegex, "");
|
||||
if (str.length > 0) {
|
||||
str.split("").forEach((strChar) => {
|
||||
if (
|
||||
findCharacters.length > 0 &&
|
||||
findCharacters.includes(strChar)
|
||||
) {
|
||||
result += strChar;
|
||||
|
||||
const index = findCharacters.findIndex(
|
||||
(findChar) => findChar === strChar
|
||||
);
|
||||
if (index !== -1) {
|
||||
findCharacters = findCharacters
|
||||
.slice(0, index)
|
||||
.concat(findCharacters.slice(index + 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ type FormSetValidation = (
|
||||
|
||||
interface FormControlObject {
|
||||
value: string;
|
||||
validate: () => ValidationResult;
|
||||
validation: FormControlValidation;
|
||||
clearValidations: FormClearValidations;
|
||||
setValidationResult: FormSetValidation;
|
||||
@@ -132,6 +133,13 @@ export const FormControl = (
|
||||
this.validation.result = defaultValidationResult;
|
||||
},
|
||||
setValidationResult: createValidationResult,
|
||||
validate: function () {
|
||||
if (this.validation.validator) {
|
||||
return this.validation.validator(this.value);
|
||||
}
|
||||
|
||||
return defaultValidationResult;
|
||||
},
|
||||
};
|
||||
};
|
||||
/* eslint-enable indent */
|
||||
|
||||
14
resources/js/helpers/image.ts
Normal file
14
resources/js/helpers/image.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
type ImageLoadCallback = (url: string) => void;
|
||||
|
||||
export const imageLoad = (
|
||||
url: string,
|
||||
callback: ImageLoadCallback,
|
||||
postfix = "h=50"
|
||||
) => {
|
||||
callback(`${url}?${postfix}`);
|
||||
const tmp = new Image();
|
||||
tmp.onload = function () {
|
||||
callback(url);
|
||||
};
|
||||
tmp.src = url;
|
||||
};
|
||||
@@ -1,5 +1,8 @@
|
||||
export const toTitleCase = (str) => {
|
||||
return str.replace(/\w\S*/g, function (txt) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
return (
|
||||
txt.charAt(0).toUpperCase() +
|
||||
txt.substr(1).replaceAll("_", " ").toLowerCase()
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
parseAusDate,
|
||||
parseAusDateTime,
|
||||
isValidTime,
|
||||
convertTimeToMinutes,
|
||||
} from "../helpers/datetime";
|
||||
import { SMDate } from "./datetime";
|
||||
import { bytesReadable } from "../helpers/common";
|
||||
|
||||
export interface ValidationObject {
|
||||
@@ -226,7 +221,7 @@ interface ValidationEmailObject extends ValidationEmailOptions {
|
||||
}
|
||||
|
||||
const defaultValidationEmailOptions: ValidationEmailOptions = {
|
||||
invalidMessage: "Your Email is not in a supported format.",
|
||||
invalidMessage: "Your email is not in a supported format.",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -379,26 +374,32 @@ export function Date(options?: ValidationDateOptions): ValidationDateObject {
|
||||
let valid = true;
|
||||
let invalidMessageType = "invalidMessage";
|
||||
|
||||
const parsedDate = parseAusDate(value);
|
||||
const parsedDate = new SMDate(value);
|
||||
|
||||
if (parsedDate != null) {
|
||||
const beforeDate = parseAusDate(
|
||||
if (parsedDate.isValid() == true) {
|
||||
const beforeDate = new SMDate(
|
||||
typeof (options["before"] = options?.before || "") ===
|
||||
"function"
|
||||
"function"
|
||||
? options.before(value)
|
||||
: options.before
|
||||
);
|
||||
const afterDate = parseAusDate(
|
||||
const afterDate = new SMDate(
|
||||
typeof (options["after"] = options?.after || "") ===
|
||||
"function"
|
||||
"function"
|
||||
? options.after(value)
|
||||
: options.after
|
||||
);
|
||||
if (beforeDate != null && parsedDate > beforeDate) {
|
||||
if (
|
||||
beforeDate.isValid() == true &&
|
||||
parsedDate.isBefore(beforeDate) == false
|
||||
) {
|
||||
valid = false;
|
||||
invalidMessageType = "invalidBeforeMessage";
|
||||
}
|
||||
if (afterDate != null && parsedDate > afterDate) {
|
||||
if (
|
||||
afterDate.isValid() == true &&
|
||||
parsedDate.isAfter(afterDate) == false
|
||||
) {
|
||||
valid = false;
|
||||
invalidMessageType = "invalidAfterMessage";
|
||||
}
|
||||
@@ -462,26 +463,32 @@ export function Time(options?: ValidationTimeOptions): ValidationTimeObject {
|
||||
let valid = true;
|
||||
let invalidMessageType = "invalidMessage";
|
||||
|
||||
if (isValidTime(value)) {
|
||||
const parsedTime = convertTimeToMinutes(value);
|
||||
const beforeTime = convertTimeToMinutes(
|
||||
const parsedTime = new SMDate(value);
|
||||
if (parsedTime.isValid() == true) {
|
||||
const beforeTime = new SMDate(
|
||||
typeof (options["before"] = options?.before || "") ===
|
||||
"function"
|
||||
"function"
|
||||
? options.before(value)
|
||||
: options.before
|
||||
);
|
||||
const afterTime = convertTimeToMinutes(
|
||||
const afterTime = new SMDate(
|
||||
typeof (options["after"] = options?.after || "") ===
|
||||
"function"
|
||||
"function"
|
||||
? options.after(value)
|
||||
: options.after
|
||||
);
|
||||
|
||||
if (beforeTime != -1 && parsedTime > beforeTime) {
|
||||
if (
|
||||
beforeTime.isValid() == true &&
|
||||
parsedTime.isBefore(beforeTime) == false
|
||||
) {
|
||||
valid = false;
|
||||
invalidMessageType = "invalidBeforeMessage";
|
||||
}
|
||||
if (afterTime != -1 && parsedTime > afterTime) {
|
||||
if (
|
||||
afterTime.isValid() == true &&
|
||||
parsedTime.isAfter(afterTime) == false
|
||||
) {
|
||||
valid = false;
|
||||
invalidMessageType = "invalidAfterMessage";
|
||||
}
|
||||
@@ -549,26 +556,32 @@ export function DateTime(
|
||||
let valid = true;
|
||||
let invalidMessageType = "invalidMessage";
|
||||
|
||||
const parsedDate = parseAusDateTime(value);
|
||||
const parsedDate = new SMDate(value);
|
||||
|
||||
if (parsedDate != null) {
|
||||
const beforeDate = parseAusDate(
|
||||
if (parsedDate.isValid() == true) {
|
||||
const beforeDate = new SMDate(
|
||||
typeof (options["before"] = options?.before || "") ===
|
||||
"function"
|
||||
"function"
|
||||
? options.before(value)
|
||||
: options.before
|
||||
);
|
||||
const afterDate = parseAusDate(
|
||||
const afterDate = new SMDate(
|
||||
typeof (options["after"] = options?.after || "") ===
|
||||
"function"
|
||||
"function"
|
||||
? options.after(value)
|
||||
: options.after
|
||||
);
|
||||
if (beforeDate != null && parsedDate > beforeDate) {
|
||||
if (
|
||||
beforeDate.isValid() == true &&
|
||||
parsedDate.isBefore(beforeDate) == false
|
||||
) {
|
||||
valid = false;
|
||||
invalidMessageType = "invalidBeforeMessage";
|
||||
}
|
||||
if (afterDate != null && parsedDate > afterDate) {
|
||||
if (
|
||||
afterDate.isValid() == true &&
|
||||
parsedDate.isAfter(afterDate) == false
|
||||
) {
|
||||
valid = false;
|
||||
invalidMessageType = "invalidAfterMessage";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { api } from "../helpers/api";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export interface UserDetails {
|
||||
@@ -11,7 +10,7 @@ export interface UserDetails {
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export interface UserStore {
|
||||
export interface UserState {
|
||||
id: string;
|
||||
token: string;
|
||||
username: string;
|
||||
@@ -22,9 +21,9 @@ export interface UserStore {
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
export const useUserStore = defineStore<string, UserState>({
|
||||
id: "user",
|
||||
state: (): UserStore => ({
|
||||
state: (): UserState => ({
|
||||
id: "",
|
||||
token: "",
|
||||
username: "",
|
||||
@@ -50,19 +49,6 @@ export const useUserStore = defineStore({
|
||||
this.$state.token = token;
|
||||
},
|
||||
|
||||
async fetchUser() {
|
||||
const res = await api.get("/users/" + this.$state.id);
|
||||
|
||||
this.$state.id = res.json.user.id;
|
||||
this.$state.token = res.json.token;
|
||||
this.$state.username = res.json.user.username;
|
||||
this.$state.firstName = res.json.user.first_name;
|
||||
this.$state.lastName = res.json.user.last_name;
|
||||
this.$state.email = res.json.user.email;
|
||||
this.$state.phone = res.json.user.phone;
|
||||
this.$state.permissions = res.json.user.permissions || [];
|
||||
},
|
||||
|
||||
clearUser() {
|
||||
this.$state.id = null;
|
||||
this.$state.token = null;
|
||||
|
||||
33
resources/js/tests/datetime.test.ts
Normal file
33
resources/js/tests/datetime.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { expect, describe, it } from "vitest";
|
||||
import { format } from "../helpers/datetime";
|
||||
|
||||
describe("format()", () => {
|
||||
it("should return an empty string when the first argument is not a Date object", () => {
|
||||
const result = format("not a date", "yyyy-MM-dd");
|
||||
expect(result).toEqual("");
|
||||
});
|
||||
|
||||
it("should format the date correctly", () => {
|
||||
const date = new Date("2022-02-19T12:34:56");
|
||||
const result = format(date, "yyyy-MM-dd HH:mm:ss");
|
||||
expect(result).toEqual("2022-02-19 12:34:56");
|
||||
});
|
||||
|
||||
it("should handle single-digit month and day", () => {
|
||||
const date = new Date("2022-01-01T00:00:00");
|
||||
const result = format(date, "yy-M-d");
|
||||
expect(result).toEqual("22-1-1");
|
||||
});
|
||||
|
||||
it("should handle day of week and month name abbreviations", () => {
|
||||
const date = new Date("2022-03-22T00:00:00");
|
||||
const result = format(date, "EEE, MMM dd, yyyy");
|
||||
expect(result).toEqual("Tue, Mar 22, 2022");
|
||||
});
|
||||
|
||||
it("should handle 12-hour clock with am/pm", () => {
|
||||
const date = new Date("2022-01-01T12:34:56");
|
||||
const result = format(date, "hh:mm:ss aa");
|
||||
expect(result).toEqual("12:34:56 pm");
|
||||
});
|
||||
});
|
||||
14
resources/js/tests/string.test.ts
Normal file
14
resources/js/tests/string.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { expect, describe, it } from "vitest";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
|
||||
describe("toTitleCase()", () => {
|
||||
it("should return a converted title case string", () => {
|
||||
const result = toTitleCase("titlecase");
|
||||
expect(result).toEqual("Titlecase");
|
||||
});
|
||||
|
||||
it("should return a converted title case string and spaces", () => {
|
||||
const result = toTitleCase("titlecase_and_more");
|
||||
expect(result).toEqual("Titlecase and more");
|
||||
});
|
||||
});
|
||||
28
resources/js/tests/validate.test.ts
Normal file
28
resources/js/tests/validate.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { expect, describe, it } from "vitest";
|
||||
import { Email } from "../helpers/validate";
|
||||
|
||||
describe("Email()", () => {
|
||||
it("should return valid=false when an invalid email address is passed to the validate function", () => {
|
||||
const v = Email();
|
||||
const result = v.validate("invalid email");
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("should return valid=false when an invalid email address is passed to the validate function", () => {
|
||||
const v = Email();
|
||||
const result = v.validate("fake@outlook");
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("should return valid=true when an valid email address is passed to the validate function", () => {
|
||||
const v = Email();
|
||||
const result = v.validate("fake@outlook.com");
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("should return valid=true when an valid email address is passed to the validate function", () => {
|
||||
const v = Email();
|
||||
const result = v.validate("fake@outlook.com.au");
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -110,7 +110,7 @@
|
||||
Sign up for our mailing list to receive expert tips and tricks,
|
||||
as well as updates on upcoming workshops.
|
||||
</p>
|
||||
<SMDialog class="p-0">
|
||||
<SMDialog class="p-0" no-shadow>
|
||||
<SMForm v-model="form" @submit.prevent="handleSubscribe">
|
||||
<div class="form-row">
|
||||
<SMInput control="email" />
|
||||
@@ -125,7 +125,7 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { excerpt } from "../helpers/common";
|
||||
import { timestampNowUtc } from "../helpers/datetime";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMCarousel from "../components/SMCarousel.vue";
|
||||
@@ -156,39 +156,33 @@ const handleLoad = async () => {
|
||||
params: {
|
||||
limit: 3,
|
||||
},
|
||||
progress: ({ loaded, total }) => {
|
||||
console.log("progress", `${loaded} - ${total}`);
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.posts) {
|
||||
response.data.posts.forEach((post) => {
|
||||
posts.push({
|
||||
title: post.title,
|
||||
content: excerpt(post.content, 200),
|
||||
image: post.hero,
|
||||
url: { name: "post-view", params: { slug: post.slug } },
|
||||
cta: "Read More...",
|
||||
});
|
||||
}).then((response) => {
|
||||
if (response.data.posts) {
|
||||
response.data.posts.forEach((post) => {
|
||||
posts.push({
|
||||
title: post.title,
|
||||
content: excerpt(post.content, 200),
|
||||
image: post.hero,
|
||||
url: { name: "post-view", params: { slug: post.slug } },
|
||||
cta: "Read More...",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("error", error);
|
||||
/* empty */
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
let result = await api.get({
|
||||
url: "/events",
|
||||
params: {
|
||||
limit: 3,
|
||||
end_at: ">" + timestampNowUtc(),
|
||||
end_at:
|
||||
">" +
|
||||
new SMDate().format("yyyy-MM-dd HH:mm:ss", { utc: true }),
|
||||
},
|
||||
});
|
||||
|
||||
if (result.json.events) {
|
||||
result.json.events.forEach((event) => {
|
||||
if (result.data.events) {
|
||||
result.data.events.forEach((event) => {
|
||||
events.push({
|
||||
title: event.title,
|
||||
content: excerpt(event.content, 200),
|
||||
|
||||
@@ -32,7 +32,7 @@ import SMMessage from "../components/SMMessage.vue";
|
||||
import SMPanelList from "../components/SMPanelList.vue";
|
||||
import SMPanel from "../components/SMPanel.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
import { timestampUtcToLocal } from "../helpers/datetime";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
|
||||
const formMessage = reactive({
|
||||
icon: "",
|
||||
@@ -58,7 +58,10 @@ const handleLoad = async () => {
|
||||
posts.value = result.json.posts;
|
||||
|
||||
posts.value.forEach((post) => {
|
||||
post.publish_at = timestampUtcToLocal(post.publish_at);
|
||||
post.publish_at = new SMDate(post.publish_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
});
|
||||
} catch (error) {
|
||||
formMessage.message =
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
<template>
|
||||
<SMPage :loading="pageLoading" full class="page-post-view">
|
||||
<SMPageError :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>
|
||||
<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>
|
||||
<component :is="formattedContent" ref="content"></component>
|
||||
</SMContainer>
|
||||
</SMPageError>
|
||||
</div>
|
||||
<component :is="formattedContent" ref="content"></component>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import SMPageError from "../components/SMPageError.vue";
|
||||
import { fullMonthString } from "../helpers/common";
|
||||
import { timestampUtcToLocal } from "../helpers/datetime";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { api } from "../helpers/api";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
@@ -48,17 +49,18 @@ const loadData = async () => {
|
||||
limit: 1,
|
||||
},
|
||||
});
|
||||
if (!res.json.posts) {
|
||||
if (!res.data.posts) {
|
||||
error.value = 500;
|
||||
} else {
|
||||
if (res.json.total == 0) {
|
||||
if (res.data.total == 0) {
|
||||
error.value = 404;
|
||||
} else {
|
||||
post.value = res.json.posts[0];
|
||||
post.value = res.data.posts[0];
|
||||
|
||||
post.value.publish_at = timestampUtcToLocal(
|
||||
post.value.publish_at
|
||||
);
|
||||
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);
|
||||
|
||||
@@ -66,7 +68,7 @@ const loadData = async () => {
|
||||
let result = await api.get({
|
||||
url: `/media/${post.value.hero}`,
|
||||
});
|
||||
post.value.hero_url = result.json.medium.url;
|
||||
post.value.hero_url = result.data.medium.url;
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
@@ -75,7 +77,7 @@ const loadData = async () => {
|
||||
let result = await api.get({
|
||||
url: `/users/${post.value.user_id}`,
|
||||
});
|
||||
post.value.user_username = result.json.user.username;
|
||||
post.value.user_username = result.data.user.username;
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
@@ -1,65 +1,66 @@
|
||||
<template>
|
||||
<SMPage class="mx-auto workshop-list">
|
||||
<h1>Workshops</h1>
|
||||
<div class="toolbar">
|
||||
<SMInput
|
||||
v-model="filterKeywords"
|
||||
placeholder="Keywords"
|
||||
@change="handleFilter"></SMInput>
|
||||
<SMInput
|
||||
v-model="filterLocation"
|
||||
placeholder="Location"
|
||||
@change="handleFilter"></SMInput>
|
||||
<SMDatePicker
|
||||
v-model="filterDateRange"
|
||||
:range="true"
|
||||
placeholder="Date Range"
|
||||
@update:model-value="handleFilter"></SMDatePicker>
|
||||
</div>
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:icon="formMessage.icon"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
class="mt-5" />
|
||||
<SMPanelList
|
||||
:loading="loading"
|
||||
:not-found="events.value?.length == 0"
|
||||
not-found-text="No workshops found">
|
||||
<SMPanel
|
||||
v-for="event in events.value"
|
||||
:key="event.id"
|
||||
:to="{ name: 'workshop-view', params: { id: event.id } }"
|
||||
:title="event.title"
|
||||
:image="event.hero"
|
||||
:show-time="true"
|
||||
:date="event.start_at"
|
||||
:end-date="event.end_at"
|
||||
:date-in-image="true"
|
||||
:location="
|
||||
event.location == 'online' ? 'Online Event' : event.address
|
||||
"></SMPanel>
|
||||
</SMPanelList>
|
||||
<SMPage class="workshop-list">
|
||||
<template #container>
|
||||
<h1>Workshops</h1>
|
||||
<div class="toolbar">
|
||||
<SMInput
|
||||
v-model="filterKeywords"
|
||||
label="Keywords"
|
||||
@change="handleFilter" />
|
||||
<SMInput
|
||||
v-model="filterLocation"
|
||||
label="Location"
|
||||
@change="handleFilter" />
|
||||
<SMInput
|
||||
v-model="filterDateRange"
|
||||
type="daterange"
|
||||
label="Date Range"
|
||||
:feedback-invalid="dateRangeError"
|
||||
@change="handleFilter" />
|
||||
</div>
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:icon="formMessage.icon"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
class="mt-5" />
|
||||
<SMPanelList
|
||||
:loading="loading"
|
||||
:not-found="events.value?.length == 0"
|
||||
not-found-text="No workshops found">
|
||||
<SMPanel
|
||||
v-for="event in events.value"
|
||||
:key="event.id"
|
||||
:to="{ name: 'workshop-view', params: { id: event.id } }"
|
||||
:title="event.title"
|
||||
:image="event.hero"
|
||||
:show-time="true"
|
||||
:date="event.start_at"
|
||||
:end-date="event.end_at"
|
||||
:date-in-image="true"
|
||||
:location="
|
||||
event.location == 'online'
|
||||
? 'Online Event'
|
||||
: event.address
|
||||
"></SMPanel>
|
||||
</SMPanelList>
|
||||
</template>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMDatePicker from "../components/SMDatePicker.vue";
|
||||
import { reactive, ref } from "vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import SMPanelList from "../components/SMPanelList.vue";
|
||||
import SMPanel from "../components/SMPanel.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
import { reactive, ref } from "vue";
|
||||
import { api } from "../helpers/api";
|
||||
import {
|
||||
timestampLocalToUtc,
|
||||
timestampNowUtc,
|
||||
timestampUtcToLocal,
|
||||
} from "../helpers/datetime";
|
||||
|
||||
const loading = ref(true);
|
||||
const events = reactive([]);
|
||||
const dateRangeError = ref("");
|
||||
|
||||
const formMessage = reactive({
|
||||
icon: "",
|
||||
@@ -78,50 +79,77 @@ const handleLoad = async () => {
|
||||
|
||||
events.value = [];
|
||||
|
||||
try {
|
||||
let query = {};
|
||||
query["limit"] = 10;
|
||||
let query = {};
|
||||
query["limit"] = 10;
|
||||
|
||||
if (filterKeywords.value && filterKeywords.value.length > 0) {
|
||||
query["q"] = filterKeywords.value;
|
||||
}
|
||||
if (filterLocation.value && filterLocation.value.length > 0) {
|
||||
query["qlocation"] = filterLocation.value;
|
||||
}
|
||||
if (filterDateRange.value && Array.isArray(filterDateRange.value)) {
|
||||
query["start_at"] =
|
||||
timestampLocalToUtc(filterDateRange.value[0]) +
|
||||
"<>" +
|
||||
timestampLocalToUtc(filterDateRange.value[1]);
|
||||
}
|
||||
|
||||
if (
|
||||
Object.keys(query).length == 1 &&
|
||||
Object.keys(query)[0] == "limit"
|
||||
) {
|
||||
query["end_at"] = ">" + timestampNowUtc();
|
||||
}
|
||||
|
||||
let result = await api.get({
|
||||
url: "/events",
|
||||
params: query,
|
||||
});
|
||||
|
||||
if (result.json.events) {
|
||||
events.value = result.json.events;
|
||||
|
||||
events.value.forEach((item) => {
|
||||
item.start_at = timestampUtcToLocal(item.start_at);
|
||||
item.end_at = timestampUtcToLocal(item.end_at);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response.status != 404) {
|
||||
formMessage.message =
|
||||
error.response?.data?.message ||
|
||||
"Could not load any events from the server.";
|
||||
}
|
||||
if (filterKeywords.value && filterKeywords.value.length > 0) {
|
||||
query["q"] = filterKeywords.value;
|
||||
}
|
||||
if (filterLocation.value && filterLocation.value.length > 0) {
|
||||
query["qlocation"] = filterLocation.value;
|
||||
}
|
||||
if (filterDateRange.value && filterDateRange.value.length > 0) {
|
||||
let error = false;
|
||||
const filterDates = filterDateRange.value
|
||||
.split(/ *- */)
|
||||
.map((dateString) => {
|
||||
const date = new SMDate(dateString).format("yyyy/MM/dd");
|
||||
|
||||
if (date.length == 0) {
|
||||
error = true;
|
||||
}
|
||||
|
||||
return date;
|
||||
});
|
||||
|
||||
if (!error) {
|
||||
if (filterDates.length == 1) {
|
||||
query["start_at"] = `>=${filterDates[0]}`;
|
||||
} else if (filterDates.length >= 2) {
|
||||
query["start_at"] = `${filterDates[0]}<>${filterDates[1]}`;
|
||||
}
|
||||
|
||||
dateRangeError.value = "";
|
||||
} else {
|
||||
dateRangeError.value = "Invalid date range";
|
||||
}
|
||||
} else {
|
||||
dateRangeError.value = "";
|
||||
}
|
||||
|
||||
if (Object.keys(query).length == 1 && Object.keys(query)[0] == "limit") {
|
||||
query["end_at"] =
|
||||
">" + new SMDate().format("yyyy/MM/dd HH:mm:ss", { utc: true });
|
||||
}
|
||||
|
||||
api.get({
|
||||
url: "/events",
|
||||
params: query,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data.events) {
|
||||
events.value = result.data.events;
|
||||
|
||||
events.value.forEach((item) => {
|
||||
item.start_at = 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, {
|
||||
format: "yyyy-MM-dd HH:mm:ss",
|
||||
utc: true,
|
||||
}).format("yyyy-MM-dd HH:mm:ss");
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status != 404) {
|
||||
formMessage.message =
|
||||
error.response?.data?.message ||
|
||||
"Could not load any events from the server.";
|
||||
}
|
||||
});
|
||||
|
||||
loading.value = false;
|
||||
};
|
||||
@@ -135,14 +163,37 @@ handleLoad();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.workshop-list .toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.workshop-list {
|
||||
background-color: #f8f8f8;
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
|
||||
& > * {
|
||||
padding-left: map-get($spacer, 1);
|
||||
padding-right: map-get($spacer, 1);
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.workshop-list .toolbar {
|
||||
flex-direction: column;
|
||||
|
||||
& > * {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
<template>
|
||||
<SMContainer :full="true" class="workshop-view">
|
||||
<SMPage :full="true" :loading="imageUrl.length == 0" class="workshop-view">
|
||||
<div
|
||||
class="workshop-image"
|
||||
:style="{ backgroundImage: `url('${imageUrl}')` }">
|
||||
<ion-icon
|
||||
v-if="imageUrl.length == 0"
|
||||
class="workshop-image-loader"
|
||||
name="image-outline" />
|
||||
</div>
|
||||
<template #inner>
|
||||
:style="{ backgroundImage: `url('${imageUrl}')` }"></div>
|
||||
<SMContainer>
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:icon="formMessage.icon"
|
||||
@@ -25,7 +20,7 @@
|
||||
v-if="
|
||||
event.status == 'closed' ||
|
||||
(event.status == 'open' &&
|
||||
timestampBeforeNow(event.end_at))
|
||||
new SMDate(event.end_at, {format: 'ymd'}).isBefore()
|
||||
"
|
||||
class="workshop-registration workshop-registration-closed">
|
||||
Registration for this event has closed
|
||||
@@ -38,7 +33,9 @@
|
||||
<div
|
||||
v-if="
|
||||
event.status == 'open' &&
|
||||
timestampAfterNow(event.end_at) &&
|
||||
new SMDate(event.end_at, {
|
||||
format: 'ymd',
|
||||
}).isAfter() &&
|
||||
event.registration_type == 'none'
|
||||
"
|
||||
class="workshop-registration workshop-registration-none">
|
||||
@@ -48,7 +45,9 @@
|
||||
<div
|
||||
v-if="
|
||||
event.status == 'open' &&
|
||||
timestampAfterNow(event.end_at) &&
|
||||
new SMDate(event.end_at, {
|
||||
format: 'ymd',
|
||||
}).isAfter() &&
|
||||
event.registration_type != 'none'
|
||||
"
|
||||
class="workshop-registration workshop-registration-url">
|
||||
@@ -77,8 +76,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</SMContainer>
|
||||
</template>
|
||||
</SMContainer>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -86,15 +85,13 @@ import { api } from "../helpers/api";
|
||||
import { computed, ref, reactive } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMHTML from "../components/SMHTML.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import {
|
||||
format,
|
||||
timestampUtcToLocal,
|
||||
timestampBeforeNow,
|
||||
timestampAfterNow,
|
||||
} from "../helpers/datetime";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
import { ApiEvent, ApiMedia } from "../helpers/api.types";
|
||||
import { imageLoad } from "../helpers/image";
|
||||
|
||||
const applicationStore = useApplicationStore();
|
||||
const event = ref({});
|
||||
@@ -107,7 +104,7 @@ const formMessage = reactive({
|
||||
});
|
||||
|
||||
const workshopDate = computed(() => {
|
||||
let str = [];
|
||||
let str: string[] = [];
|
||||
|
||||
if (Object.keys(event.value).length > 0) {
|
||||
if (
|
||||
@@ -118,19 +115,30 @@ const workshopDate = computed(() => {
|
||||
) !=
|
||||
event.value.end_at.substring(0, event.value.end_at.indexOf(" "))
|
||||
) {
|
||||
str = [format(new Date(event.value.start_at), "dd/MM/yyyy")];
|
||||
str = [
|
||||
new SMDate(event.value.start_at, { format: "ymd" }).format(
|
||||
"dd/MM/yyyy"
|
||||
),
|
||||
];
|
||||
if (event.value.end_at.length > 0) {
|
||||
str[0] =
|
||||
str[0] +
|
||||
" - " +
|
||||
format(new Date(event.value.end_at), "dd/MM/yyyy");
|
||||
new SMDate(event.value.end_at, { format: "ymd" }).format(
|
||||
"dd/MM/yyyy"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
str = [
|
||||
format(new Date(event.value.start_at), "EEEE dd MMM yyyy"),
|
||||
format(new Date(event.value.start_at), "h:mm aa") +
|
||||
new SMDate(event.value.start_at, { format: "ymd" }).format(
|
||||
"EEEE dd MMM yyyy"
|
||||
),
|
||||
new SMDate(event.value.start_at, { format: "ymd" }).format(
|
||||
"h:mm aa"
|
||||
) +
|
||||
" - " +
|
||||
format(new Date(event.value.end_at), "h:mm aa"),
|
||||
SMDate(event.value.end_at, { format: "ymd" }),
|
||||
format("h:mm aa"),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -155,27 +163,66 @@ const handleLoad = async () => {
|
||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
||||
formMessage.message = "";
|
||||
|
||||
try {
|
||||
const result = await api.get(`events/${route.params.id}`);
|
||||
event.value = result.json.event;
|
||||
api.get(`/events/${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
|
||||
: {};
|
||||
|
||||
event.value.start_at = timestampUtcToLocal(event.value.start_at);
|
||||
event.value.end_at = timestampUtcToLocal(event.value.end_at);
|
||||
if (event.value) {
|
||||
// event.value = result.data.event as ApiEventItem;
|
||||
|
||||
applicationStore.setDynamicTitle(event.value.title);
|
||||
handleLoadImage();
|
||||
} catch (error) {
|
||||
formMessage.message =
|
||||
error.json?.message ||
|
||||
"Could not load event information from the server.";
|
||||
}
|
||||
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 {
|
||||
formMessage.message =
|
||||
"Could not load event information from the server.";
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
formMessage.message =
|
||||
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.";
|
||||
// }
|
||||
};
|
||||
|
||||
const handleLoadImage = async () => {
|
||||
try {
|
||||
const result = await api.get(`media/${event.value.hero}`);
|
||||
if (result.json.medium) {
|
||||
imageUrl.value = result.json.medium.url;
|
||||
console.log(event.value);
|
||||
const result = await api.get(`/media/${event.value.hero}`);
|
||||
const data = result.data as ApiMedia;
|
||||
|
||||
if (data && data.medium) {
|
||||
imageLoad(data.medium.url, (url) => {
|
||||
imageUrl.value = url;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
@@ -197,6 +244,7 @@ handleLoad();
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-color: #eee;
|
||||
transition: background-image 0.2s;
|
||||
|
||||
.workshop-image-loader {
|
||||
font-size: 5rem;
|
||||
@@ -228,7 +276,7 @@ handleLoad();
|
||||
align-items: center;
|
||||
height: 1rem;
|
||||
|
||||
svg {
|
||||
ion-icon {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
|
||||
@@ -31,7 +31,7 @@ import SMButton from "../../components/SMButton.vue";
|
||||
import SMTabGroup from "../../components/SMTabGroup.vue";
|
||||
import SMTab from "../../components/SMTab.vue";
|
||||
import SMMessage from "../../components/SMMessage.vue";
|
||||
import axios from "axios";
|
||||
import { api } from "../../helpers/api";
|
||||
|
||||
let formLoading = ref(false);
|
||||
let logOutputContent = ref("");
|
||||
@@ -49,7 +49,7 @@ const loadData = async () => {
|
||||
|
||||
try {
|
||||
formLoading.value = true;
|
||||
let res = await axios.get(`logs/discord`);
|
||||
let res = await api.get(`logs/discord`);
|
||||
|
||||
logOutputContent.value = res.data.log.output;
|
||||
if (logOutputContent.value.length === 0) {
|
||||
|
||||
@@ -116,10 +116,7 @@ import {
|
||||
} from "../../helpers/validate";
|
||||
import { FormObject, FormControl } from "../../helpers/form";
|
||||
import { useRoute } from "vue-router";
|
||||
import {
|
||||
timestampLocalToUtc,
|
||||
timestampUtcToLocal,
|
||||
} from "../../helpers/datetime";
|
||||
import { SMDate } from "../../helpers/datetime";
|
||||
import { api } from "../../helpers/api";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
@@ -241,12 +238,19 @@ const loadData = async () => {
|
||||
form.address.value = res.data.event.address
|
||||
? res.data.event.address
|
||||
: "";
|
||||
form.start_at.value = timestampUtcToLocal(res.data.event.start_at);
|
||||
form.end_at.value = timestampUtcToLocal(res.data.event.end_at);
|
||||
form.start_at.value = new SMDate(res.data.event.start_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
form.end_at.value = new SMDate(res.data.event.end_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
form.status.value = res.data.event.status;
|
||||
form.publish_at.value = timestampUtcToLocal(
|
||||
res.data.event.publish_at
|
||||
);
|
||||
form.publish_at.value = new SMDate(res.data.event.publish_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
form.registration_type.value = res.data.event.registration_type;
|
||||
form.registration_data.value = res.data.event.registration_data;
|
||||
form.content.value = res.data.event.content
|
||||
@@ -267,13 +271,21 @@ const handleSubmit = async () => {
|
||||
title: form.title.value,
|
||||
location: form.location.value,
|
||||
address: form.address.value,
|
||||
start_at: timestampLocalToUtc(form.start_at.value),
|
||||
end_at: timestampLocalToUtc(form.end_at.value),
|
||||
start_at: new SMDate(form.start_at.value, { format: "dmy" }).format(
|
||||
"yyyy/MM/dd HH:mm:ss",
|
||||
{ utc: true }
|
||||
),
|
||||
end_at: new SMDate(form.end_at.value, { format: "dmy" }).format(
|
||||
"yyyy/MM/dd HH:mm:ss",
|
||||
{ utc: true }
|
||||
),
|
||||
status: form.status.value,
|
||||
publish_at:
|
||||
form.publish_at.value == ""
|
||||
? ""
|
||||
: timestampLocalToUtc(form.publish_at.value),
|
||||
: new SMDate(form.publish_at.value, {
|
||||
format: "dmy",
|
||||
}).format("yyyy/MM/dd HH:mm:ss", { utc: true }),
|
||||
registration_type: form.registration_type.value,
|
||||
registration_data: form.registration_data.value,
|
||||
content: form.content.value,
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import EasyDataTable from "vue3-easy-data-table";
|
||||
import { api } from "../../helpers/api";
|
||||
import { relativeDate } from "../../helpers/datetime";
|
||||
import { SMDate } from "../../helpers/datetime";
|
||||
import { useRouter } from "vue-router";
|
||||
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
import { openDialog } from "vue3-promise-dialog";
|
||||
@@ -134,7 +134,7 @@ const loadFromServer = async () => {
|
||||
|
||||
items.value.forEach(async (row) => {
|
||||
if (Object.keys(users).includes(row.user_id) === false) {
|
||||
await axios.get(`users/${row.user_id}`).then((res) => {
|
||||
await api.get(`users/${row.user_id}`).then((res) => {
|
||||
users[row.user_id] = res.data.user.username;
|
||||
});
|
||||
}
|
||||
@@ -146,10 +146,16 @@ const loadFromServer = async () => {
|
||||
}
|
||||
|
||||
if (row.created_at !== "undefined") {
|
||||
row.created_at = relativeDate(row.created_at);
|
||||
row.created_at = new SMDate(row.created_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).relative();
|
||||
}
|
||||
if (row.updated_at !== "undefined") {
|
||||
row.updated_at = relativeDate(row.updated_at);
|
||||
row.updated_at = new SMDate(row.updated_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).relative();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user