From 9fd46b9fd9dc40818bbb3639ecf6b49bf46c0a16 Mon Sep 17 00:00:00 2001 From: James Collins Date: Tue, 28 Feb 2023 19:16:10 +1000 Subject: [PATCH] added event pricing --- app/Models/Event.php | 4 +- ...02_28_090609_add_price_to_events_table.php | 32 +++++++++ resources/js/components/SMPanel.vue | 47 +++++++++++-- resources/js/helpers/api.types.ts | 2 + resources/js/helpers/string.ts | 36 ++++++++++ resources/js/views/EventView.vue | 34 ++++++++-- resources/js/views/dashboard/EventEdit.vue | 67 +++++++++++-------- 7 files changed, 184 insertions(+), 38 deletions(-) create mode 100644 database/migrations/2023_02_28_090609_add_price_to_events_table.php diff --git a/app/Models/Event.php b/app/Models/Event.php index eac9a9b..071a834 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -27,9 +27,11 @@ class Event extends Model 'registration_type', 'registration_data', 'hero', - 'content' + 'content', + 'price' ]; + /** * Get all of the post's attachments. */ diff --git a/database/migrations/2023_02_28_090609_add_price_to_events_table.php b/database/migrations/2023_02_28_090609_add_price_to_events_table.php new file mode 100644 index 0000000..bee4565 --- /dev/null +++ b/database/migrations/2023_02_28_090609_add_price_to_events_table.php @@ -0,0 +1,32 @@ +string('price')->default(""); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('events', function (Blueprint $table) { + $table->dropColumn('price'); + }); + } +}; diff --git a/resources/js/components/SMPanel.vue b/resources/js/components/SMPanel.vue index 8e84584..00680bf 100644 --- a/resources/js/components/SMPanel.vue +++ b/resources/js/components/SMPanel.vue @@ -19,14 +19,19 @@
- + name="calendar-outline" + class="icon" /> +

{{ computedDate }}

- +

{{ location }}

+
+ $ +

{{ computedPrice }}

+
{{ computedContent }}
@@ -52,7 +57,12 @@ import { api } from "../helpers/api"; import { MediaResponse } from "../helpers/api.types"; import { SMDate } from "../helpers/datetime"; import { imageLoad } from "../helpers/image"; -import { excerpt, replaceHtmlEntites, stripHtmlTags } from "../helpers/string"; +import { + excerpt, + replaceHtmlEntites, + stringToNumber, + stripHtmlTags, +} from "../helpers/string"; import { isUUID } from "../helpers/uuid"; import SMButton from "./SMButton.vue"; @@ -134,6 +144,11 @@ const props = defineProps({ default: "primary", required: false, }, + price: { + type: String, + default: "", + required: false, + }, }); let styleObject = reactive({}); @@ -180,14 +195,32 @@ const computedContent = computed(() => { return excerpt(replaceHtmlEntites(stripHtmlTags(props.content)), 200); }); +/** + * Return a computed day number from props.date + */ const computedDay = computed(() => { return new SMDate(props.date, { format: "yMd" }).format("dd"); }); +/** + * Return a computed month name from props.date + */ const computedMonth = computed(() => { return new SMDate(props.date, { format: "yMd" }).format("MMM"); }); +/** + * Return a computed price amount, if a form of 0, return "Free" + */ +const computedPrice = computed(() => { + const parsedPrice = stringToNumber(props.price); + if (parsedPrice == 0) { + return "Free"; + } + + return props.price; +}); + onMounted(async () => { if (props.image && props.image.length > 0 && isUUID(props.image)) { api.get({ url: "/media/{medium}", params: { medium: props.image } }) @@ -290,19 +323,21 @@ watch( } .sm-panel-date, - .sm-panel-location { + .sm-panel-location, + .sm-panel-price { display: flex; flex-direction: row; align-items: top; font-size: 80%; margin-bottom: 0.4rem; - ion-icon { + .icon { flex: 0 1 1rem; margin-right: map-get($spacer, 1); padding-top: 0.1rem; height: 1rem; padding: 0.25rem 0; + text-align: center; } p { diff --git a/resources/js/helpers/api.types.ts b/resources/js/helpers/api.types.ts index eddd60c..a8341ff 100644 --- a/resources/js/helpers/api.types.ts +++ b/resources/js/helpers/api.types.ts @@ -5,11 +5,13 @@ export interface Event { content: string; start_at: string; end_at: string; + publish_at: string; location: string; address: string; status: string; registration_type: string; registration_data: string; + price: string; } export interface EventResponse { diff --git a/resources/js/helpers/string.ts b/resources/js/helpers/string.ts index f1421c0..37bd4cd 100644 --- a/resources/js/helpers/string.ts +++ b/resources/js/helpers/string.ts @@ -79,3 +79,39 @@ export const replaceHtmlEntites = (txt: string): string => { return translate[entity]; }); }; + +/** + * Convert a string to a number, ignoring items like dollar signs, etc. + * + * @param {string} str The string to convert to a number + * @returns {number} A number with the minimum amount of decimal places (or 0) + */ +export const stringToNumber = (str: string): number => { + str = str.replace(/[^\d.-]/g, ""); + const num = Number.parseFloat(str); + return isNaN(num) ? 0 : parseFloat(num.toFixed(2)); +}; + +/** + * Convert a number or string to a price (0 or 0.00). + * + * @param {number|string} numOrString The number of string to convert to a price. + * @returns {string} The converted result. + */ +export const toPrice = (numOrString: number | string): string => { + let num = 0; + + if (typeof numOrString == "string") { + num = stringToNumber(numOrString); + } else { + num = numOrString; + } + + if (num % 1 === 0) { + // Number has no decimal places + return num.toFixed(0); + } else { + // Number has decimal places + return num.toFixed(2); + } +}; diff --git a/resources/js/views/EventView.vue b/resources/js/views/EventView.vue index 0b153e5..6a0e5b0 100644 --- a/resources/js/views/EventView.vue +++ b/resources/js/views/EventView.vue @@ -61,7 +61,11 @@ label="Register for Event">
-

Date / Time

+

+ Date / Time +

-

Location

+

+ Location +

{{ event.location == "online" @@ -79,6 +87,9 @@ }}

+
+

${{ computedPrice }}

+
@@ -95,6 +106,7 @@ import { api } from "../helpers/api"; import { Event, EventResponse, MediaResponse } from "../helpers/api.types"; import { SMDate } from "../helpers/datetime"; import { imageLoad } from "../helpers/image"; +import { stringToNumber } from "../helpers/string"; import { useApplicationStore } from "../store/ApplicationStore"; const applicationStore = useApplicationStore(); @@ -162,6 +174,18 @@ const workshopDate = computed(() => { return str; }); +/** + * Return a computed price amount, if a form of 0, return "Free" + */ +const computedPrice = computed(() => { + const parsedPrice = stringToNumber(event.value.price || "0"); + if (parsedPrice == 0) { + return "Free"; + } + + return event.value.price; +}); + const registerUrl = computed(() => { let href = ""; @@ -290,10 +314,11 @@ handleLoad(); align-items: center; height: 1rem; - ion-icon { + .icon { display: inline-block; width: 1rem; margin-right: 0.5rem; + text-align: center; } } @@ -329,7 +354,8 @@ handleLoad(); } .sm-workshop-date, - .sm-workshop-location { + .sm-workshop-location, + .sm-workshop-price { padding: 0 1rem; } } diff --git a/resources/js/views/dashboard/EventEdit.vue b/resources/js/views/dashboard/EventEdit.vue index 7c18be1..7ff4522 100644 --- a/resources/js/views/dashboard/EventEdit.vue +++ b/resources/js/views/dashboard/EventEdit.vue @@ -60,6 +60,11 @@ + + Leave blank to hide from public. + + + { - form.loading(true); - if (route.params.id) { try { - let res = await api.get("/events/" + route.params.id); - if (!res.data.event) { + form.loading(true); + + const result = await api.get({ + url: "/events/{id}", + params: { id: route.params.id }, + }); + const data = result.data as EventResponse; + + if (!data || !data.event) { throw new Error("The server is currently not available"); } - form.controls.title.value = res.data.event.title; - form.controls.location.value = res.data.event.location; - form.controls.address.value = res.data.event.address - ? res.data.event.address + form.controls.title.value = data.event.title; + form.controls.location.value = data.event.location; + form.controls.address.value = data.event.address + ? data.event.address : ""; - form.controls.start_at.value = new SMDate(res.data.event.start_at, { + form.controls.start_at.value = new SMDate(data.event.start_at, { format: "ymd", utc: true, - }).format("yyyy/MM/dd HH:mm:ss"); - form.controls.end_at.value = new SMDate(res.data.event.end_at, { + }).format("yyyy/MM/dd HH:mm"); + form.controls.end_at.value = new SMDate(data.event.end_at, { format: "ymd", utc: true, - }).format("yyyy/MM/dd HH:mm:ss"); - form.controls.status.value = res.data.event.status; - form.controls.publish_at.value = new SMDate( - res.data.event.publish_at, - { - format: "ymd", - utc: true, - } - ).format("yyyy/MM/dd HH:mm:ss"); + }).format("yyyy/MM/dd HH:mm"); + form.controls.status.value = data.event.status; + form.controls.publish_at.value = new SMDate(data.event.publish_at, { + format: "ymd", + utc: true, + }).format("yyyy/MM/dd HH:mm"); form.controls.registration_type.value = - res.data.event.registration_type; + data.event.registration_type; form.controls.registration_data.value = - res.data.event.registration_data; - form.controls.content.value = res.data.event.content - ? res.data.event.content + data.event.registration_data; + form.controls.content.value = data.event.content + ? data.event.content : ""; - form.controls.hero.value = res.data.event.hero; + form.controls.hero.value = data.event.hero; + form.controls.price.value = data.event.price; } catch (err) { pageError.value = err.response.status; + } finally { + form.loading(false); } } - - form.loading(false); }; const handleSubmit = async () => { @@ -303,6 +315,7 @@ const handleSubmit = async () => { registration_data: form.controls.registration_data.value, content: form.controls.content.value, hero: form.controls.hero.value, + price: form.controls.price.value, }; if (route.params.id) {