In this article, we’ll learn how to build a real-world Ticketing System with Strapi and Vue.js, where users can buy tickets for upcoming events. Our case study will be a system to purchase tickets for upcoming movies.
The completed version of your application should look like the image below:
The Strapi documentation says that Strapi is a flexible, open-source, headless CMS that gives developers the freedom to choose their favorite tools and frameworks and allows editors to manage and distribute their content easily.
Strapi helps us build an API quickly with no hassle of creating a server from scratch. With Strapi, we can do everything literally, and it’s easily customizable. We can add our code and edit functionalities easily. Strapi is amazing, and its capabilities would leave you stunned.
Strapi provides an admin panel to edit and create APIs. It also provides easily-editable code and uses JavaScript.
To install Strapi, head over to the Strapi docs at Strapi We’ll be using the SQLite database for this project. To install Strapi, run the following commands:
1 yarn create strapi-app my-project # using yarn
2 npx create-strapi-app@latest my-project # using npx
Replace my-project
with the name you wish to call your application directory. Your package manager will create a directory with the specified name and install Strapi.
If you have followed the instructions correctly, you should have Strapi installed on your machine. Run the following commands to start the Strapi development server:
1 yarn develop # using yarn
2 npm run develop # using npm
The development server starts the app on http://localhost:1337/admin.
Let’s create our Event
collection type:
Content-Type Builder
under Plugins
on the side menu.collection types
, click create new collection type
.collection-type
named Event
.name
as short text
date
as Datetime
image
as media
(single media)price
as Number
(decimaltickets-available
as Number
The final Event
collection type should look like the image below:
Next, we create our Ticket
collection type:
Content-Type Builder
under Plugins
on the side menu.collection types
, click create new collection type
collection-type
named Ticket
.reference_number
as UID
- seats_with
as Number
- seats_without
as Number
- total
as Number
- total_seats
as Number
- event
as relation
(An event has many tickets.)
The final Ticket
collection type should look like the image below:
To seed the database, create some data under the Events
collection type. In order to do that, follow the steps below:
Content Manager
on the side menu.collection types
, select Event
.create new entry
.Strapi has user permission and roles that are assigned to authenticated
and public
users. Since our system does not require user login and sign-up, we need to enable public access for our Content types
.
Follow these steps to allow public access:
Settings
under general
in the side menu.User and permission plugins
, click Roles
.public
.permissions
, different collection types
are listed. Click on Event
, then check both find
and findOne
.Ticket
.create
, find
, and findOne
.save
.We have successfully allowed public access to our content types; we can now make API
calls appropriately.
Next, we will install and configure Vue.Js to work with our Strapi backend.
To install Vue.js using the @vue/CLI package, visit the Vue CLI docs or run one of these commands to get started.
1 npm install -g @vue/cli
2 # OR
3 yarn global add @vue/cli
Run the following commands to create a Vue.js project once you have installed the Vue CLI on your local machine.
1 vue create my-project
Replace my-project
with the name you wish to call your project.
The above command should start a command-line application that walks you through creating a Vue.js project. Select whatever options you like, but select Router
, Vuex
, and linter/formatter
because the first two are essential in our application. The last thing is to format the code nicely.
After Vue CLI has finished creating your project, run the following command.
1 cd my-project
2 yarn serve //using yarn
3 npm serve //using npm
Finally, visit the following URL: [http://localhost:8080](http://localhost:8080/)
to open your Vue.js application in your browser.
We will use Tailwind CSS as our CSS framework. Let's see how we can integrate Tailwind CSS into our Vue.js Application.
1 npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
2 or
3 yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
In the root of your Vue.js folder, create a postcss.config.js
and write the following lines.
1 module.exports = {
2 plugins: {
3 tailwindcss: {},
4 autoprefixer: {},
5 }
6 }
Also, in the root of the Vue.js folder, create a tailwindcss.config.js
and write the following lines.
1 module.exports = {
2 purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
3 darkMode: false, // or 'media' or 'class'
4 theme: {
5 extend: {},
6 },
7 variants: {
8 extend: {},
9 },
10 plugins: [],
11 }
We have extended the components of the font by adding some fonts which we will use. These fonts have to be installed on your local machine to work appropriately but feel free to use whatever fonts you like.
Finally, create an index.css
file in your src
folder and add the following lines.
1 /* ./src/main.css */
2 @tailwind base;
3 @tailwind components;
4 @tailwind utilities;
We need a package for to make API calls to our Strapi backend, and we'll be using the Axios package for that purpose.
Run the following command to install Axios on your machine.
1 npm install --save axios
2 or
3 yarn add axios
In this section, we’ll build the components that make up our vue.js application.
Create an EventList.vue
file located in the src/components
folder, and add the following lines of code to the file.
1 <template>
2 <div class="list">
3 <div v-for="(event, i) in events" :key="i" class="mb-3">
4 <figure
5 class="md:flex bg-gray-100 rounded-xl p-8 md:p-0 dark:bg-gray-800"
6 >
7 <img
8 class="w-24 h-24 md:w-48 md:h-auto md:rounded-none rounded-full mx-auto"
9 :src="`http://localhost:1337${event.attributes.image.data.attributes.formats.large.url}`"
10 alt=""
11 width="384"
12 height="512"
13 />
14 <div class="pt-6 md:p-8 text-center md:text-left space-y-4">
15 <blockquote>
16 <h1 class="text-xl md:text-2xl mb-3 font-bold uppercase">
17 {{ event.attributes.name }}
18 </h1>
19 <p class="text-sm md:text-lg font-medium">
20 Lorem ipsum dolor sit amet consectetur, adipisicing elit. Debitis
21 dolore dignissimos exercitationem, optio corrupti nihil veniam
22 quod unde reprehenderit cum accusantium quaerat nostrum placeat,
23 sapiente tempore perspiciatis maiores iure esse?
24 </p>
25 </blockquote>
26 <figcaption class="font-medium">
27 <div class="text-gray-700 dark:text-gray-500">
28 tickets available: {{ event.attributes.tickets_available == 0 ? 'sold out' : event.attributes.tickets_available }}
29 </div>
30 <div class="text-gray-700 dark:text-gray-500">
31 {{ formatDate(event.attributes.date) }}
32 </div>
33 </figcaption>
34 <!-- <router-link to="/about"> -->
35 <button :disabled=" event.attributes.tickets_available == 0 " @click="getDetail(event.id)" class="bg-black text-white p-3">
36 Get tickets
37 </button>
38 <!-- </router-link> -->
39 </div>
40 </figure>
41 </div>
42 </div>
43 </template>
44 <script>
45 import axios from "axios";
46 export default {
47 data() {
48 return {
49 events: [],
50 };
51 },
52 methods: {
53 getDetail(id) {
54 console.log("btn clicked");
55 this.$router.push(`/event/${id}`);
56 },
57 formatDate(date) {
58 const timeArr = new Date(date).toLocaleTimeString().split(":");
59 const DorN = timeArr.pop().split(" ")[1];
60 return `${new Date(date).toDateString()} ${timeArr.join(":")} ${DorN}`;
61 },
62 },
63 async created() {
64 const res = await axios.get("http://localhost:1337/api/events?populate=*");
65 this.events = res.data.data;
66 },
67 };
68 </script>
69 <style scoped></style>
Create an EventView.vue
file located in the src/components
folder, and add the following lines of code to the file.
1 <template>
2 <div class="">
3 <!-- showcase -->
4 <div
5 :style="{
6 backgroundImage: `url(${img})`,
7 backgroundColor: `rgba(0, 0, 0, 0.8)`,
8 backgroundBlendMode: `multiply`,
9 backgroundRepeat: `no-repeat`,
10 backgroundSize: `cover`,
11 height: `70vh`,
12 }"
13 class="w-screen flex items-center relative"
14 ref="showcase"
15 >
16 <div class="w-1/2 p-5">
17 <h1 class="text-2xl md:text-6xl text-white mb-3 uppercase font-bold my-auto">
18 {{ event.attributes.name }}
19 </h1>
20 <p class="leading-normal md:text-lg mb-3 font-thin text-white">
21 Lorem ipsum dolor sit amet consectetur adipisicing elit. Velit natus
22 illum cupiditate qui, asperiores quod sapiente. A exercitationem
23 quidem cupiditate repudiandae, odio sequi quae nam ipsam obcaecati
24 itaque, suscipit dolores.
25 </p>
26 <p class="text-white"><span class="font-bold">Tickets available:</span> {{ event.attributes.tickets_available }} </p>
27 <p class="text-white"><span class="font-bold">Airing Date:</span> {{ formatDate(event.attributes.date) }}</p>
28 </div>
29 </div>
30 <div class="text-center flex justify-center items-center">
31 <div class="mt-3 mb-3">
32 <h3 class="text-4xl mt-5 mb-5">Get Tickets</h3>
33 <table class="table-auto w-screen">
34 <thead>
35 <tr>
36 <th class="w-1/2">Options</th>
37 <th>Price</th>
38 <th>Quantity</th>
39 <th>Total</th>
40 </tr>
41 </thead>
42 <tbody>
43 <tr class="p-3">
44 <td class="p-3">Seats without popcorn and drinks</td>
45 <td class="p-3">${{ formatCurrency(price_of_seats_without) }}</td>
46 <td class="p-3">
47 <select class="p-3" id="" v-model="no_of_seats_without">
48 <option
49 class="p-3 bg-dark"
50 v-for="(num, i) of quantityModel"
51 :key="i"
52 :value="`${num}`"
53 >
54 {{ num }}
55 </option>
56 </select>
57 </td>
58 <td>${{ formatCurrency(calcWithoutTotal) }}</td>
59 </tr>
60 <tr class="p-3">
61 <td class="p-3">Seats with popcorn and drinks</td>
62 <td class="p-3">${{ formatCurrency(price_of_seats_with) }}</td>
63 <td class="p-3">
64 <select class="p-3" id="" v-model="no_of_seats_with">
65 <option
66 class="p-3 bg-black"
67 v-for="(num, i) of quantityModel"
68 :key="i"
69 :value="`${num}`"
70 >
71 {{ num }}
72 </option>
73 </select>
74 </td>
75 <td>${{ formatCurrency(calcWithTotal) }}</td>
76 </tr>
77 </tbody>
78 </table>
79 <div class="m-3">
80 <p class="mb-3">Ticket Total: ${{ formatCurrency(calcTotal) }}</p>
81 <button
82 @click="bookTicket"
83 :disabled="calcTotal == 0"
84 class="bg-black text-white p-3"
85 >
86 Book Now
87 </button>
88 </div>
89 </div>
90 </div>
91 <ticket
92 :data="res"
93 class="mx-auto h-full z-10 absolute top-0"
94 v-if="booked == true"
95 />
96 </div>
97 </template>
98 <script>
99 import axios from "axios";
100 import randomstring from "randomstring";
101 import ticket from "../components/Ticket.vue";
102 export default {
103 data() {
104 return {
105 quantityModel: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
106 no_of_seats_without: 0,
107 price_of_seats_without: 3,
108 no_of_seats_with: 0,
109 price_of_seats_with: 4,
110 id: "",
111 event: {},
112 img: "",
113 booked: false,
114 };
115 },
116 components: {
117 ticket,
118 },
119 methods: {
120 getDetail() {
121 console.log("btn clicked");
122 this.$router.push("/");
123 },
124 assignValue(num) {
125 console.log(num);
126 this.no_of_seats_without = num;
127 },
128 async bookTicket() {
129 console.log("booking ticket");
130 console.log(this.booked, "booked");
131 try {
132 const res = await axios.post(`http://localhost:1337/api/tickets`, {
133 data: {
134 seats_with: this.no_of_seats_with,
135 seats_without: this.no_of_seats_without,
136 total_seats:
137 parseInt(this.no_of_seats_without) +
138 parseInt(this.no_of_seats_with),
139 total: this.calcTotal,
140 event: this.id,
141 reference_number: randomstring.generate(),
142 },
143 });
144 this.res = res.data;
145 this.res.event = this.event.attributes.name;
146 this.res.date = this.event.attributes.date;
147 this.booked = true;
148 this.no_of_seats_with = 0;
149 this.no_of_seats_without = 0;
150
151 } catch (error) {
152 return alert(
153 "cannot book ticket as available tickets have been exceeded. Pick a number of ticket that is less than or equal to the available tickets"
154 );
155 }
156 },
157 formatCurrency(num) {
158 if (num.toString().indexOf(".") != -1) {
159 return num;
160 } else {
161 return `${num}.00`;
162 }
163 },
164 formatDate(date) {
165 const timeArr = new Date(date).toLocaleTimeString().split(":");
166 const DorN = timeArr.pop().split(" ")[1];
167 return `${new Date(date).toDateString()} ${timeArr.join(":")} ${DorN}`;
168 },
169 },
170 computed: {
171 calcWithoutTotal() {
172 return (
173 parseFloat(this.no_of_seats_without) *
174 parseFloat(this.price_of_seats_without)
175 );
176 },
177 calcWithTotal() {
178 return (
179 parseFloat(this.no_of_seats_with) * parseFloat(this.price_of_seats_with)
180 );
181 },
182 calcTotal() {
183 return this.calcWithoutTotal + this.calcWithTotal;
184 },
185 },
186 async created() {
187 this.id = this.$route.params.id;
188 try {
189 const res = await axios.get(
190 `http://localhost:1337/api/events/${this.$route.params.id}?populate=*`
191 );
192 this.event = res.data.data;
193 this.price_of_seats_without = res.data.data.attributes.price;
194 this.price_of_seats_with = res.data.data.attributes.price + 2;
195 const img =
196 res.data.data.attributes.image.data.attributes.formats.large.url;
197 this.img = `"http://localhost:1337${img}"`;
198
199 } catch (error) {
200 return alert('An Error occurred, please try agian')
201 }
202
203 },
204 };
205 </script>
206 <style scoped></style>
Create a Ticket.vue
file located in the src/components
folder, and add the following lines of code to the file.
1 <template>
2 <div
3 class="h-full w-full modal flex overflow-y-hidden justify-center items-center"
4 >
5 <div class="bg-white p-5">
6 <p class="m-2">
7 Show: <span class="uppercase">{{ data.event }}</span>
8 </p>
9 <p class="m-2">Date: {{ formatDate(data.date) }}</p>
10 <p class="m-2">TicketID: {{ data.reference_number }}</p>
11 <p class="m-2">
12 Seats without Pop corn and Drinks: {{ data.seats_without }} seats
13 </p>
14 <p class="m-2">
15 Seats with Pop corn and Drinks: {{ data.seats_with }} seats
16 </p>
17 <p class="m-2">
18 Total seats:
19 {{ parseInt(data.seats_without) + parseInt(data.seats_with) }} seats
20 </p>
21 <p class="m-2">Price total: ${{ data.total }}.00</p>
22 <router-link to="/">
23 <button class="m-2 p-3 text-white bg-black">Done</button>
24 </router-link>
25 </div>
26 </div>
27 </template>
28 <script>
29 export default {
30 name: "Ticket",
31 data() {
32 return {};
33 },
34 props: ["data"],
35 components: {},
36 methods: {
37 formatDate(date) {
38 const timeArr = new Date(date).toLocaleTimeString().split(":");
39 const DorN = timeArr.pop().split(" ")[1];
40 return `${new Date(date).toDateString()} ${timeArr.join(":")} ${DorN}`;
41 },
42 },
43 };
44 </script>
45 <style scoped>
46 .show_case {
47 /* background: rgba(0, 0, 0, 0.5); */
48 /* background-blend-mode: multiply; */
49 background-repeat: no-repeat;
50 background-size: cover;
51 }
52 .show_img {
53 object-fit: cover;
54 opacity: 1;
55 }
56 ._img_background {
57 background: rgba(0, 0, 0, 0.5);
58 }
59 .modal {
60 overflow: hidden;
61 background: rgba(0, 0, 0, 0.5);
62 }
63 </style>
In this section, we’ll use the components built in the last section to build out the pages on our frontend.
The Events
page makes use of the EventsView.vue
component, which we created in the previous section.
Create an Event.vue
file located in the src/views
folder, and edit the content of the file to the following:
1 <template>
2 <div class="about">
3 <event-view />
4 </div>
5 </template>
6 <script>
7 import EventView from "../components/EventView.vue";
8 export default {
9 name: "Event",
10 components: {
11 EventView,
12 },
13 };
14 </script>
15 <style scoped>
16 .show_case {
17 /* background: rgba(0, 0, 0, 0.5); */
18 /* background-blend-mode: multiply; */
19 background-repeat: no-repeat;
20 background-size: cover;
21 }
22 .show_img {
23 object-fit: cover;
24 opacity: 1;
25 }
26 ._img_background {
27 background: rgba(0, 0, 0, 0.5);
28 }
29 </style>
The Home
page makes use of the EventList.vue
component, which we created in the previous section.
Create an Home.vue
file located in the src/views
folder, and edit the content of the file to the following:
1 <template>
2 <div class="home">
3 <h1 class="text-center text-xl mb-3 font-bold mt-4">Upcoming Events</h1>
4 <div class="flex self-center justify-center">
5 <event-list class="w-5/6" />
6 </div>
7 </div>
8 </template>
9 <script>
10 // @ is an alias to /src
11 import EventList from "../components/EventList.vue";
12 export default {
13 name: "Home",
14 components: {
15 EventList,
16 },
17 };
18 </script>
We created some new view files that we need to make accessible as routes. However, for that to happen, we need to update our router to reflect the changes made.
In order to make the changes to Vue router, follow the steps below:
index.js
file located at src/router
, and edit the content to the following:1 import Vue from "vue";
2 import VueRouter from "vue-router";
3 import Home from "../views/Home.vue";
4 import Event from "../views/Event.vue";
5 Vue.use(VueRouter);
6 const routes = [
7 {
8 path: "/",
9 name: "Home",
10 component: Home,
11 },
12 {
13 path: "/event/:id",
14 name: "Event",
15 component: Event,
16 }
17 ];
18 const router = new VueRouter({
19 mode: "history",
20 base: process.env.BASE_URL,
21 routes,
22 });
23 export default router;
One major advantage of Strapi
is that it allows us to edit the controllers, services, and more.
In this section, we’re going to edit the ticket controller
in our Strapi
backend. We want to carry out some logic when creating a new ticket, such as:
Follow the steps below to edit the ticket controller
:
strapi
folder in your favorite code editor.src/api/ticket
folder.src/api/ticket
folder, click the controllers.ticket.js
.ticket.js
to contain the following code:1 'use strict';
2 /**
3 * ticket controller
4 */
5 const { createCoreController } = require('@strapi/strapi').factories;
6 module.exports = createCoreController('api::ticket.ticket', ({ strapi }) => ({
7 async create(ctx) {
8 const event_id = Number(ctx.request.body.data.event)
9 // some logic here
10 const event = await strapi.service('api::event.event').findOne(event_id, {
11 populate: "tickets"
12 })
13 if(ctx.request.body.data.total_seats > event.tickets_available) {
14 return ctx.badRequest('Cannot book ticket at the moment')
15 }
16 const response = await strapi.service('api::ticket.ticket').create(ctx.request.body)
17 await strapi.service('api::event.event').update(event_id, { data: {
18 tickets_available: event.tickets_available - ctx.request.body.data.total_seats
19 }})
20 return response;
21 }
22
23 }));
I hope this tutorial has given you an insight into how to build a ticketing system with Strapi
. There’s so much more you could add to this application, just think of this as a starting point.
Alexander Godwin is a Software Developer and writer that likes to write code and build things. Learning by doing is the best way and it's how Alex helps others learn. Follow him on Twitter (@oviecodes)