added event pricing

This commit is contained in:
2023-02-28 19:16:10 +10:00
parent 041dd4b314
commit 9fd46b9fd9
7 changed files with 184 additions and 38 deletions

View File

@@ -27,9 +27,11 @@ class Event extends Model
'registration_type', 'registration_type',
'registration_data', 'registration_data',
'hero', 'hero',
'content' 'content',
'price'
]; ];
/** /**
* Get all of the post's attachments. * Get all of the post's attachments.
*/ */

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('events', function (Blueprint $table) {
$table->string('price')->default("");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('events', function (Blueprint $table) {
$table->dropColumn('price');
});
}
};

View File

@@ -19,14 +19,19 @@
<div v-if="showDate && date" class="sm-panel-date"> <div v-if="showDate && date" class="sm-panel-date">
<ion-icon <ion-icon
v-if="showTime == false && endDate.length == 0" v-if="showTime == false && endDate.length == 0"
name="calendar-outline" /> name="calendar-outline"
<ion-icon v-else name="time-outline" /> class="icon" />
<ion-icon v-else name="time-outline" class="icon" />
<p>{{ computedDate }}</p> <p>{{ computedDate }}</p>
</div> </div>
<div v-if="location" class="sm-panel-location"> <div v-if="location" class="sm-panel-location">
<ion-icon name="location-outline" /> <ion-icon class="icon" name="location-outline" />
<p>{{ location }}</p> <p>{{ location }}</p>
</div> </div>
<div v-if="price" class="sm-panel-price">
<span class="icon">$</span>
<p>{{ computedPrice }}</p>
</div>
<div v-if="content" class="sm-panel-content"> <div v-if="content" class="sm-panel-content">
{{ computedContent }} {{ computedContent }}
</div> </div>
@@ -52,7 +57,12 @@ import { api } from "../helpers/api";
import { MediaResponse } from "../helpers/api.types"; import { MediaResponse } from "../helpers/api.types";
import { SMDate } from "../helpers/datetime"; import { SMDate } from "../helpers/datetime";
import { imageLoad } from "../helpers/image"; 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 { isUUID } from "../helpers/uuid";
import SMButton from "./SMButton.vue"; import SMButton from "./SMButton.vue";
@@ -134,6 +144,11 @@ const props = defineProps({
default: "primary", default: "primary",
required: false, required: false,
}, },
price: {
type: String,
default: "",
required: false,
},
}); });
let styleObject = reactive({}); let styleObject = reactive({});
@@ -180,14 +195,32 @@ const computedContent = computed(() => {
return excerpt(replaceHtmlEntites(stripHtmlTags(props.content)), 200); return excerpt(replaceHtmlEntites(stripHtmlTags(props.content)), 200);
}); });
/**
* Return a computed day number from props.date
*/
const computedDay = computed(() => { const computedDay = computed(() => {
return new SMDate(props.date, { format: "yMd" }).format("dd"); return new SMDate(props.date, { format: "yMd" }).format("dd");
}); });
/**
* Return a computed month name from props.date
*/
const computedMonth = computed(() => { const computedMonth = computed(() => {
return new SMDate(props.date, { format: "yMd" }).format("MMM"); 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 () => { onMounted(async () => {
if (props.image && props.image.length > 0 && isUUID(props.image)) { if (props.image && props.image.length > 0 && isUUID(props.image)) {
api.get({ url: "/media/{medium}", params: { medium: props.image } }) api.get({ url: "/media/{medium}", params: { medium: props.image } })
@@ -290,19 +323,21 @@ watch(
} }
.sm-panel-date, .sm-panel-date,
.sm-panel-location { .sm-panel-location,
.sm-panel-price {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: top; align-items: top;
font-size: 80%; font-size: 80%;
margin-bottom: 0.4rem; margin-bottom: 0.4rem;
ion-icon { .icon {
flex: 0 1 1rem; flex: 0 1 1rem;
margin-right: map-get($spacer, 1); margin-right: map-get($spacer, 1);
padding-top: 0.1rem; padding-top: 0.1rem;
height: 1rem; height: 1rem;
padding: 0.25rem 0; padding: 0.25rem 0;
text-align: center;
} }
p { p {

View File

@@ -5,11 +5,13 @@ export interface Event {
content: string; content: string;
start_at: string; start_at: string;
end_at: string; end_at: string;
publish_at: string;
location: string; location: string;
address: string; address: string;
status: string; status: string;
registration_type: string; registration_type: string;
registration_data: string; registration_data: string;
price: string;
} }
export interface EventResponse { export interface EventResponse {

View File

@@ -79,3 +79,39 @@ export const replaceHtmlEntites = (txt: string): string => {
return translate[entity]; 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);
}
};

View File

@@ -61,7 +61,11 @@
label="Register for Event"></SMButton> label="Register for Event"></SMButton>
</div> </div>
<div class="sm-workshop-date"> <div class="sm-workshop-date">
<h4><ion-icon name="calendar-outline" />Date / Time</h4> <h4>
<ion-icon
class="icon"
name="calendar-outline" />Date / Time
</h4>
<p <p
v-for="(line, index) in workshopDate" v-for="(line, index) in workshopDate"
:key="index" :key="index"
@@ -70,7 +74,11 @@
</p> </p>
</div> </div>
<div class="sm-workshop-location"> <div class="sm-workshop-location">
<h4><ion-icon name="location-outline" />Location</h4> <h4>
<ion-icon
class="icon"
name="location-outline" />Location
</h4>
<p> <p>
{{ {{
event.location == "online" event.location == "online"
@@ -79,6 +87,9 @@
}} }}
</p> </p>
</div> </div>
<div v-if="event.price" class="sm-workshop-price">
<h4><span class="icon">$</span>{{ computedPrice }}</h4>
</div>
</div> </div>
</SMContainer> </SMContainer>
</SMContainer> </SMContainer>
@@ -95,6 +106,7 @@ import { api } from "../helpers/api";
import { Event, EventResponse, MediaResponse } from "../helpers/api.types"; import { Event, EventResponse, MediaResponse } from "../helpers/api.types";
import { SMDate } from "../helpers/datetime"; import { SMDate } from "../helpers/datetime";
import { imageLoad } from "../helpers/image"; import { imageLoad } from "../helpers/image";
import { stringToNumber } from "../helpers/string";
import { useApplicationStore } from "../store/ApplicationStore"; import { useApplicationStore } from "../store/ApplicationStore";
const applicationStore = useApplicationStore(); const applicationStore = useApplicationStore();
@@ -162,6 +174,18 @@ const workshopDate = computed(() => {
return str; 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(() => { const registerUrl = computed(() => {
let href = ""; let href = "";
@@ -290,10 +314,11 @@ handleLoad();
align-items: center; align-items: center;
height: 1rem; height: 1rem;
ion-icon { .icon {
display: inline-block; display: inline-block;
width: 1rem; width: 1rem;
margin-right: 0.5rem; margin-right: 0.5rem;
text-align: center;
} }
} }
@@ -329,7 +354,8 @@ handleLoad();
} }
.sm-workshop-date, .sm-workshop-date,
.sm-workshop-location { .sm-workshop-location,
.sm-workshop-price {
padding: 0 1rem; padding: 0 1rem;
} }
} }

View File

@@ -60,6 +60,11 @@
</SMColumn> </SMColumn>
</SMRow> </SMRow>
<SMRow> <SMRow>
<SMColumn>
<SMInput control="price"
>Leave blank to hide from public.</SMInput
>
</SMColumn>
<SMColumn> <SMColumn>
<SMInput <SMInput
type="select" type="select"
@@ -71,6 +76,8 @@
link: 'Link', link: 'Link',
}" /> }" />
</SMColumn> </SMColumn>
</SMRow>
<SMRow>
<SMColumn> <SMColumn>
<SMInput <SMInput
v-if="registration_data?.visible" v-if="registration_data?.visible"
@@ -136,6 +143,7 @@ import {
} from "../../helpers/validate"; } from "../../helpers/validate";
import SMInputAttachments from "../../components/SMInputAttachments.vue"; import SMInputAttachments from "../../components/SMInputAttachments.vue";
import SMForm from "../../components/SMForm.vue"; import SMForm from "../../components/SMForm.vue";
import { EventResponse } from "../../helpers/api.types";
const route = useRoute(); const route = useRoute();
const page_title = route.params.id ? "Edit Event" : "Create New Event"; const page_title = route.params.id ? "Edit Event" : "Create New Event";
@@ -230,54 +238,58 @@ const form = reactive(
), ),
hero: FormControl("", Required()), hero: FormControl("", Required()),
content: FormControl(), content: FormControl(),
price: FormControl(),
}) })
); );
const loadData = async () => { const loadData = async () => {
form.loading(true);
if (route.params.id) { if (route.params.id) {
try { try {
let res = await api.get("/events/" + route.params.id); form.loading(true);
if (!res.data.event) {
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"); throw new Error("The server is currently not available");
} }
form.controls.title.value = res.data.event.title; form.controls.title.value = data.event.title;
form.controls.location.value = res.data.event.location; form.controls.location.value = data.event.location;
form.controls.address.value = res.data.event.address form.controls.address.value = data.event.address
? res.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", format: "ymd",
utc: true, utc: true,
}).format("yyyy/MM/dd HH:mm:ss"); }).format("yyyy/MM/dd HH:mm");
form.controls.end_at.value = new SMDate(res.data.event.end_at, { form.controls.end_at.value = new SMDate(data.event.end_at, {
format: "ymd", format: "ymd",
utc: true, utc: true,
}).format("yyyy/MM/dd HH:mm:ss"); }).format("yyyy/MM/dd HH:mm");
form.controls.status.value = res.data.event.status; form.controls.status.value = data.event.status;
form.controls.publish_at.value = new SMDate( form.controls.publish_at.value = new SMDate(data.event.publish_at, {
res.data.event.publish_at, format: "ymd",
{ utc: true,
format: "ymd", }).format("yyyy/MM/dd HH:mm");
utc: true,
}
).format("yyyy/MM/dd HH:mm:ss");
form.controls.registration_type.value = form.controls.registration_type.value =
res.data.event.registration_type; data.event.registration_type;
form.controls.registration_data.value = form.controls.registration_data.value =
res.data.event.registration_data; data.event.registration_data;
form.controls.content.value = res.data.event.content form.controls.content.value = data.event.content
? res.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) { } catch (err) {
pageError.value = err.response.status; pageError.value = err.response.status;
} finally {
form.loading(false);
} }
} }
form.loading(false);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
@@ -303,6 +315,7 @@ const handleSubmit = async () => {
registration_data: form.controls.registration_data.value, registration_data: form.controls.registration_data.value,
content: form.controls.content.value, content: form.controls.content.value,
hero: form.controls.hero.value, hero: form.controls.hero.value,
price: form.controls.price.value,
}; };
if (route.params.id) { if (route.params.id) {