Dependency refactor #17

Merged
nomadjimbob merged 155 commits from dependency-refactor into main 2023-02-27 12:30:57 +00:00
30 changed files with 1026 additions and 646 deletions
Showing only changes of commit ccc30a8b7a - Show all commits

View File

@@ -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;
}

View File

@@ -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'];
}
}

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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,
},

View File

@@ -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;

View File

@@ -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;

View File

@@ -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";
}
}

View File

@@ -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 = {

View 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;
}

View File

@@ -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;
}
}

View File

@@ -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 */

View 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;
};

View File

@@ -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()
);
});
};

View File

@@ -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";
}

View File

@@ -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;

View 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");
});
});

View 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");
});
});

View 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);
});
});

View File

@@ -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),

View File

@@ -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 =

View File

@@ -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 */
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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();
}
});