В этой статье мы узнаем, как создать реальную систему продажи билетов с помощью Strapi и Vue.js, где пользователи смогут покупать билеты на предстоящие мероприятия. Нашим кейсом будет система покупки билетов на предстоящие фильмы.

Что вам понадобится для этого урока

  • Базовые знания Vue.js
  • Знание JavaScript
  • Node.js (рекомендуется v14 для Strapi)

Оглавление

  • Краткое введение в Strapi, безголовую CMS
  • Строительные леса проекта Strapi
  • Создание коллекций билетов
  • Создание коллекций событий
  • Заполнение базы данных
  • Разрешение общего доступа
  • Создание приложения Vue.js
  • Настройка CSS Tailwind
  • Компоненты здания и представления
  • Редактирование бэкенда Strapi вручную
  • Заключение

Завершенная версия вашего приложения должна выглядеть так, как показано на рисунке ниже:

Краткое введение в Strapi, безголовую CMS

В Документации Strapi говорится, что Strapi — это гибкая CMS с открытым исходным кодом без управления контентом, которая дает разработчикам свободу выбора своих любимых инструментов и фреймворков, а редакторам позволяет легко управлять своим контентом и распространять его.

Strapi помогает нам быстро создать API без необходимости создавать сервер с нуля. Со Strapi мы можем делать буквально все, и его легко настроить. Мы можем легко добавлять наш код и редактировать функциональные возможности. Strapi великолепен, и его возможности ошеломят вас.

Strapi предоставляет панель администратора для редактирования и создания API. Он также предоставляет легко редактируемый код и использует JavaScript.

Создание строительных лесов для проекта Strapi

Чтобы установить Strapi, перейдите к документации Strapi на сайте Strapi. Мы будем использовать базу данных SQLite для этого проекта. Чтобы установить Strapi, выполните следующие команды:

yarn create strapi-app my-project # using yarn
    npx create-strapi-app@latest my-project # using npx

Замените my-project именем, которое вы хотите назвать своим каталогом приложений. Ваш менеджер пакетов создаст каталог с указанным именем и установит Strapi.

Если вы правильно следовали инструкциям, на вашем компьютере должен быть установлен Strapi. Выполните следующие команды, чтобы запустить сервер разработки Strapi:

yarn develop # using yarn
    npm run develop # using npm

Сервер разработки запускает приложение на https://localhost:1337/admin.

Создание коллекций событий

Давайте создадим наш тип коллекции Event:

  1. Нажмите на Content-Type Builder под Plugins в боковом меню.
  2. Под collection types нажмите create new collection type.
  3. Создайте новый collection-type с именем Event .
  4. Создайте следующие поля в разделе product content-type:
  • name as short text
  • date as Datetime
  • image как media(один носитель)
  • price как Number(десятичный
  • tickets-available as Number

Окончательный тип коллекции Event должен выглядеть так, как показано на рисунке ниже:

Создание коллекций билетов

Далее мы создаем наш тип коллекции Ticket:

  1. Нажмите на Content-Type Builder под Plugins в боковом меню.
  2. Под collection types нажмите create new collection type
  3. Создайте новый collection-type с именем Ticket .
  4. Создайте следующие поля под product content-type: — reference_number as UID - seats_with as Number - seats_without as Number - total as Number - total_seats as Number - event as relation (У мероприятия много билетов.)

Окончательный тип коллекции Ticket должен выглядеть так, как показано на рисунке ниже:

Заполнение базы данных

Чтобы заполнить базу данных, создайте некоторые данные в типе коллекции Events. Для этого выполните следующие действия:

  1. Нажмите на Content Manager в боковом меню.
  2. Под collection types выберите Event.
  3. Нажмите на create new entry.
  4. Создайте столько новых записей, сколько хотите.

Разрешение общего доступа

Strapi имеет права пользователя и роли, назначенные пользователям authenticated и public. Поскольку наша система не требует входа в систему и регистрации пользователя, нам нужно включить публичный доступ для нашего Content types.

Выполните следующие действия, чтобы разрешить публичный доступ:

  1. Нажмите на Settings под general в боковом меню.
  2. Под User and permission plugins нажмите Roles.
  3. Нажмите на public.
  4. Под permissions перечислены разные collection types. Нажмите на Event, затем отметьте find и findOne.
  5. Далее нажмите на Ticket.
  6. Проверьте create, find и findOne.
  7. Наконец, нажмите save.

Мы успешно разрешили публичный доступ к нашим типам контента; теперь мы можем правильно выполнять вызовы API.

Создание приложения Vue.js

Далее мы установим и настроим Vue.Js для работы с нашим бэкэндом Strapi.

Чтобы установить Vue.js с помощью пакета @vue/CLI, посетите Документацию Vue CLI или выполните одну из этих команд, чтобы начать.

npm install -g @vue/cli 
    # OR
    yarn global add @vue/cli

Выполните следующие команды, чтобы создать проект Vue.js после того, как вы установили Vue CLI на свой локальный компьютер.

vue create my-project

Замените my-project именем, которое вы хотите назвать своим проектом.

Приведенная выше команда должна запустить приложение командной строки, которое поможет вам создать проект Vue.js. Выберите любые параметры, которые вам нравятся, но выберите Router, Vuex и linter/formatter, потому что первые два необходимы в нашем приложении. Последнее, что нужно сделать, это красиво отформатировать код.

После того, как Vue CLI завершит создание вашего проекта, выполните следующую команду.

cd my-project
    yarn serve //using yarn
    npm serve //using npm

Наконец, перейдите по следующему URL-адресу: [https://localhost:8080](https://localhost:8080/), чтобы открыть приложение Vue.js в браузере.

Настройка CSS Tailwind

Мы будем использовать Tailwind CSS в качестве нашего CSS-фреймворка. Давайте посмотрим, как мы можем интегрировать Tailwind CSS в наше приложение Vue.js.

npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
    or
    yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9

В корне папки Vue.js создайте postcss.config.js и напишите следующие строки.

module.exports = {
      plugins: {
        tailwindcss: {},
        autoprefixer: {},
      }
    }

Также в корне папки Vue.js создайте tailwindcss.config.js и напишите следующие строки.

module.exports = {
      purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
      darkMode: false, // or 'media' or 'class'
      theme: {
        extend: {},
      },
      variants: {
        extend: {},
      },
      plugins: [],
    }

Мы расширили компоненты шрифта, добавив некоторые шрифты, которые мы будем использовать. Эти шрифты должны быть установлены на вашем локальном компьютере для правильной работы, но не стесняйтесь использовать любые шрифты, которые вам нравятся.

Наконец, создайте файл index.css в папке src и добавьте следующие строки.

/* ./src/main.css */
    @tailwind base;
    @tailwind components;
    @tailwind utilities;

Установка Axios для вызовов API

Нам нужен пакет для выполнения вызовов API к серверной части Strapi, и для этой цели мы будем использовать пакет Axios.

Выполните следующую команду, чтобы установить Axios на свой компьютер.

npm install --save axios
    or
    yarn add axios

Компоненты здания

В этом разделе мы создадим компоненты, составляющие наше приложение vue.js.

Чтобы построить компонент «EventList»:

Создайте файл EventList.vue, расположенный в папке src/components, и добавьте в него следующие строки кода.

<template>
      <div class="list">
        <div v-for="(event, i) in events" :key="i" class="mb-3">
          <figure
            class="md:flex bg-gray-100 rounded-xl p-8 md:p-0 dark:bg-gray-800"
          >
            <img
              class="w-24 h-24 md:w-48 md:h-auto md:rounded-none rounded-full mx-auto"
              :src="`https://localhost:1337${event.attributes.image.data.attributes.formats.large.url}`"
              alt=""
              width="384"
              height="512"
            />
            <div class="pt-6 md:p-8 text-center md:text-left space-y-4">
              <blockquote>
                <h1 class="text-xl md:text-2xl mb-3 font-bold uppercase">
                  {{ event.attributes.name }}
                </h1>
                <p class="text-sm md:text-lg font-medium">
                  Lorem ipsum dolor sit amet consectetur, adipisicing elit. Debitis
                  dolore dignissimos exercitationem, optio corrupti nihil veniam
                  quod unde reprehenderit cum accusantium quaerat nostrum placeat,
                  sapiente tempore perspiciatis maiores iure esse?
                </p>
              </blockquote>
              <figcaption class="font-medium">
                <div class="text-gray-700 dark:text-gray-500">
                  tickets available: {{ event.attributes.tickets_available == 0 ? 'sold out' : event.attributes.tickets_available }}
                </div>
                <div class="text-gray-700 dark:text-gray-500">
                  {{ formatDate(event.attributes.date) }}
                </div>
              </figcaption>
              <!-- <router-link to="/about"> -->
              <button :disabled=" event.attributes.tickets_available == 0 " @click="getDetail(event.id)" class="bg-black text-white p-3">
                Get tickets
              </button>
              <!-- </router-link> -->
            </div>
          </figure>
        </div>
      </div>
    </template>
    <script>
    import axios from "axios";
    export default {
      data() {
        return {
          events: [],
        };
      },
      methods: {
        getDetail(id) {
          console.log("btn clicked");
          this.$router.push(`/event/${id}`);
        },
        formatDate(date) {
          const timeArr = new Date(date).toLocaleTimeString().split(":");
          const DorN = timeArr.pop().split(" ")[1];
          return `${new Date(date).toDateString()} ${timeArr.join(":")} ${DorN}`;
        },
      },
      async created() {
        const res = await axios.get("https://localhost:1337/api/events?populate=*");
        this.events = res.data.data;
      },
    };
    </script>
    <style scoped></style>

Чтобы создать компонент «EventView»:

Создайте файл EventView.vue, расположенный в папке src/components, и добавьте в него следующие строки кода.

<template>
      <div class="">
        <!-- showcase -->
        <div
          :style="{
            backgroundImage: `url(${img})`,
            backgroundColor: `rgba(0, 0, 0, 0.8)`,
            backgroundBlendMode: `multiply`,
            backgroundRepeat: `no-repeat`,
            backgroundSize: `cover`,
            height: `70vh`,
          }"
          class="w-screen flex items-center relative"
          ref="showcase"
        >
          <div class="w-1/2 p-5">
            <h1 class="text-2xl md:text-6xl text-white mb-3 uppercase font-bold my-auto">
              {{ event.attributes.name }}
            </h1>
            <p class="leading-normal md:text-lg mb-3 font-thin text-white">
              Lorem ipsum dolor sit amet consectetur adipisicing elit. Velit natus
              illum cupiditate qui, asperiores quod sapiente. A exercitationem
              quidem cupiditate repudiandae, odio sequi quae nam ipsam obcaecati
              itaque, suscipit dolores.
            </p>
            <p class="text-white"><span class="font-bold">Tickets available:</span> {{ event.attributes.tickets_available }} </p>
            <p class="text-white"><span class="font-bold">Airing Date:</span> {{ formatDate(event.attributes.date) }}</p>
          </div>
        </div>
        <div class="text-center flex justify-center items-center">
          <div class="mt-3 mb-3">
            <h3 class="text-4xl mt-5 mb-5">Get Tickets</h3>
            <table class="table-auto w-screen">
              <thead>
                <tr>
                  <th class="w-1/2">Options</th>
                  <th>Price</th>
                  <th>Quantity</th>
                  <th>Total</th>
                </tr>
              </thead>
              <tbody>
                <tr class="p-3">
                  <td class="p-3">Seats without popcorn and drinks</td>
                  <td class="p-3">${{ formatCurrency(price_of_seats_without) }}</td>
                  <td class="p-3">
                    <select class="p-3" id="" v-model="no_of_seats_without">
                      <option
                        class="p-3 bg-dark"
                        v-for="(num, i) of quantityModel"
                        :key="i"
                        :value="`${num}`"
                      >
                        {{ num }}
                      </option>
                    </select>
                  </td>
                  <td>${{ formatCurrency(calcWithoutTotal) }}</td>
                </tr>
                <tr class="p-3">
                  <td class="p-3">Seats with popcorn and drinks</td>
                  <td class="p-3">${{ formatCurrency(price_of_seats_with) }}</td>
                  <td class="p-3">
                    <select class="p-3" id="" v-model="no_of_seats_with">
                      <option
                        class="p-3 bg-black"
                        v-for="(num, i) of quantityModel"
                        :key="i"
                        :value="`${num}`"
                      >
                        {{ num }}
                      </option>
                    </select>
                  </td>
                  <td>${{ formatCurrency(calcWithTotal) }}</td>
                </tr>
              </tbody>
            </table>
            <div class="m-3">
              <p class="mb-3">Ticket Total: ${{ formatCurrency(calcTotal) }}</p>
              <button
                @click="bookTicket"
                :disabled="calcTotal == 0"
                class="bg-black text-white p-3"
              >
                Book Now
              </button>
            </div>
          </div>
        </div>
        <ticket
          :data="res"
          class="mx-auto h-full z-10 absolute top-0"
          v-if="booked == true"
        />
      </div>
    </template>
    <script>
    import axios from "axios";
    import randomstring from "randomstring";
    import ticket from "../components/Ticket.vue";
    export default {
      data() {
        return {
          quantityModel: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
          no_of_seats_without: 0,
          price_of_seats_without: 3,
          no_of_seats_with: 0,
          price_of_seats_with: 4,
          id: "",
          event: {},
          img: "",
          booked: false,
        };
      },
      components: {
        ticket,
      },
      methods: {
        getDetail() {
          console.log("btn clicked");
          this.$router.push("/");
        },
        assignValue(num) {
          console.log(num);
          this.no_of_seats_without = num;
        },
        async bookTicket() {
          console.log("booking ticket");
          console.log(this.booked, "booked");
          try {
            const res = await axios.post(`https://localhost:1337/api/tickets`, {
              data: {
                seats_with: this.no_of_seats_with,
                seats_without: this.no_of_seats_without,
                total_seats:
                  parseInt(this.no_of_seats_without) +
                  parseInt(this.no_of_seats_with),
                total: this.calcTotal,
                event: this.id,
                reference_number: randomstring.generate(),
              },
            });
            this.res = res.data;
            this.res.event = this.event.attributes.name;
            this.res.date = this.event.attributes.date;
            this.booked = true;
            this.no_of_seats_with = 0;
            this.no_of_seats_without = 0;
            
          } catch (error) {
            return alert(
              "cannot book ticket as available tickets have been exceeded. Pick a number of ticket that is less than or equal to the available tickets"
            );
          }
        },
        formatCurrency(num) {
          if (num.toString().indexOf(".") != -1) {
            return num;
          } else {
            return `${num}.00`;
          }
        },
        formatDate(date) {
          const timeArr = new Date(date).toLocaleTimeString().split(":");
          const DorN = timeArr.pop().split(" ")[1];
          return `${new Date(date).toDateString()} ${timeArr.join(":")} ${DorN}`;
        },
      },
      computed: {
        calcWithoutTotal() {
          return (
            parseFloat(this.no_of_seats_without) *
            parseFloat(this.price_of_seats_without)
          );
        },
        calcWithTotal() {
          return (
            parseFloat(this.no_of_seats_with) * parseFloat(this.price_of_seats_with)
          );
        },
        calcTotal() {
          return this.calcWithoutTotal + this.calcWithTotal;
        },
      },
      async created() {
        this.id = this.$route.params.id;
        try {
          const res = await axios.get(
            `https://localhost:1337/api/events/${this.$route.params.id}?populate=*`
          );
          this.event = res.data.data;
          this.price_of_seats_without = res.data.data.attributes.price;
          this.price_of_seats_with = res.data.data.attributes.price + 2;
          const img =
            res.data.data.attributes.image.data.attributes.formats.large.url;
          this.img = `"https://localhost:1337${img}"`;
          
        } catch (error) {
          return alert('An Error occurred, please try agian')
        }
        
      },
    };
    </script>
    <style scoped></style>

Собирайте коллекции билетов

Создайте файл Ticket.vue, расположенный в папке src/components, и добавьте в него следующие строки кода.

<template>
      <div
        class="h-full w-full modal flex overflow-y-hidden justify-center items-center"
      >
        <div class="bg-white p-5">
          <p class="m-2">
            Show: <span class="uppercase">{{ data.event }}</span>
          </p>
          <p class="m-2">Date: {{ formatDate(data.date) }}</p>
          <p class="m-2">TicketID: {{ data.reference_number }}</p>
          <p class="m-2">
            Seats without Pop corn and Drinks: {{ data.seats_without }} seats
          </p>
          <p class="m-2">
            Seats with Pop corn and Drinks: {{ data.seats_with }} seats
          </p>
          <p class="m-2">
            Total seats:
            {{ parseInt(data.seats_without) + parseInt(data.seats_with) }} seats
          </p>
          <p class="m-2">Price total: ${{ data.total }}.00</p>
          <router-link to="/">
            <button class="m-2 p-3 text-white bg-black">Done</button>
          </router-link>
        </div>
      </div>
    </template>
    <script>
    export default {
      name: "Ticket",
      data() {
        return {};
      },
      props: ["data"],
      components: {},
      methods: {
        formatDate(date) {
          const timeArr = new Date(date).toLocaleTimeString().split(":");
          const DorN = timeArr.pop().split(" ")[1];
          return `${new Date(date).toDateString()} ${timeArr.join(":")} ${DorN}`;
        },
      },
    };
    </script>
    <style scoped>
    .show_case {
      /* background: rgba(0, 0, 0, 0.5); */
      /* background-blend-mode: multiply; */
      background-repeat: no-repeat;
      background-size: cover;
    }
    .show_img {
      object-fit: cover;
      opacity: 1;
    }
    ._img_background {
      background: rgba(0, 0, 0, 0.5);
    }
    .modal {
      overflow: hidden;
      background: rgba(0, 0, 0, 0.5);
    }
    </style>

Виды зданий

В этом разделе мы будем использовать компоненты, созданные в предыдущем разделе, для создания страниц в нашем интерфейсе.

Создание представления «События»

Страница Events использует компонент EventsView.vue, который мы создали в предыдущем разделе.

Создайте файл Event.vue, расположенный в папке src/views, и отредактируйте содержимое файла следующим образом:

<template>
      <div class="about">
        <event-view />
      </div>
    </template>
    <script>
    import EventView from "../components/EventView.vue";
    export default {
      name: "Event",
      components: {
        EventView,
      },
    };
    </script>
    <style scoped>
    .show_case {
      /* background: rgba(0, 0, 0, 0.5); */
      /* background-blend-mode: multiply; */
      background-repeat: no-repeat;
      background-size: cover;
    }
    .show_img {
      object-fit: cover;
      opacity: 1;
    }
    ._img_background {
      background: rgba(0, 0, 0, 0.5);
    }
    </style>

Чтобы построить «домашнее» представление:

Страница Home использует компонент EventList.vue, который мы создали в предыдущем разделе.

Создайте файл Home.vue, расположенный в папке src/views, и отредактируйте содержимое файла следующим образом:

<template>
      <div class="home">
        <h1 class="text-center text-xl mb-3 font-bold mt-4">Upcoming Events</h1>
        <div class="flex self-center justify-center">
          <event-list class="w-5/6" />
        </div>
      </div>
    </template>
    <script>
    // @ is an alias to /src
    import EventList from "../components/EventList.vue";
    export default {
      name: "Home",
      components: {
         EventList,
      },
    };
    </script>

Обновление маршрутизатора Vue

Мы создали несколько новых файлов представлений, которые нам нужно сделать доступными как маршруты. Однако для этого нам нужно обновить наш маршрутизатор, чтобы отразить внесенные изменения.

Чтобы внести изменения в маршрутизатор Vue, выполните следующие действия:

  • Откройте файл index.js, расположенный по адресу src/router, и отредактируйте его содержимое следующим образом:
import Vue from "vue";
    import VueRouter from "vue-router";
    import Home from "../views/Home.vue";
    import Event from "../views/Event.vue";
    Vue.use(VueRouter);
    const routes = [
      {
        path: "/",
        name: "Home",
        component: Home,
      },
      {
        path: "/event/:id",
        name: "Event",
        component: Event,
      }
    ];
    const router = new VueRouter({
      mode: "history",
      base: process.env.BASE_URL,
      routes,
    });
    export default router;

Редактирование бэкенда Strapi вручную

Одним из основных преимуществ Strapi является то, что он позволяет нам редактировать контроллеры, сервисы и многое другое.

В этом разделе мы собираемся отредактировать ticket controller в нашем бэкенде Strapi. Мы хотим реализовать некоторую логику при создании нового тикета, например:

  1. Проверка того, достаточно ли билетов, доступных для мероприятия, для покрытия создания новых билетов.
  2. Проверка того, не исчерпаны ли билеты, доступные на мероприятие.

Выполните следующие шаги, чтобы отредактировать ticket controller:

  • Откройте папку strapi в вашем любимом редакторе кода.
  • Перейдите в папку src/api/ticket.
  • В папке src/api/ticket щелкните контроллеры.
  • Откройте ticket.js.
  • Наконец, обновите содержимое ticket.js, чтобы оно содержало следующий код:
'use strict';
    /**
     *  ticket controller
     */
    const { createCoreController } = require('@strapi/strapi').factories;
    module.exports = createCoreController('api::ticket.ticket', ({ strapi }) => ({
        async create(ctx) {
            const event_id = Number(ctx.request.body.data.event)
            // some logic here
            const event = await strapi.service('api::event.event').findOne(event_id, {
                populate: "tickets"
            })
            if(ctx.request.body.data.total_seats > event.tickets_available) {
                return ctx.badRequest('Cannot book ticket at the moment')
            }
            const response = await strapi.service('api::ticket.ticket').create(ctx.request.body)
            await strapi.service('api::event.event').update(event_id, { data: {
                tickets_available: event.tickets_available - ctx.request.body.data.total_seats
            }})
            return response;
          }
          
    }));

Заключение

Я надеюсь, что это руководство дало вам представление о том, как создать систему продажи билетов с помощью Strapi. В это приложение можно добавить гораздо больше, просто подумайте об этом как о отправной точке.