added event pricing
This commit is contained in:
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user