Jak vytvořit aplikaci pro prodej vstupenek pomocí Vue.js a Strapi

V tomto článku se naučíme, jak vytvořit skutečný systém prodeje vstupenek pomocí Strapi a Vue.js, kde si uživatelé mohou zakoupit vstupenky na nadcházející události. Naší případovou studií bude systém nákupu vstupenek na připravované filmy.

Co budete pro tento tutoriál potřebovat

  • Základní znalost Vue.js
  • Znalost JavaScriptu
  • Node.js (verze 14 doporučená pro Strapi)

Obsah

  • Krátký úvod do Strapi, bezhlavého CMS
  • Lešení projektu Strapi
  • Vytváření sbírek vstupenek
  • Vytváření sbírek událostí
  • Nasazení databáze
  • Povolení přístupu veřejnosti
  • Vytvoření aplikace Vue.js
  • Nastavení CSS Tailwind
  • Stavební komponenty a pohledy
  • Ruční úprava backendu Strapi
  • Závěr

Dokončená verze vaší aplikace by měla vypadat jako na obrázku níže:

Krátký úvod do Strapi, bezhlavého CMS

Dokumentace Strapi říká, že Strapi je flexibilní, open-source, bezhlavý CMS, který dává vývojářům svobodu ve výběru jejich oblíbených nástrojů a frameworků a umožňuje editorům snadno spravovat a distribuovat jejich obsah.

Strapi nám pomáhá rychle vytvořit API bez problémů s vytvářením serveru od začátku. Se Strapi můžeme dělat všechno doslova a je to snadno přizpůsobitelné. Můžeme snadno přidat náš kód a upravit funkce. Strapi je úžasný a jeho schopnosti by vás nechaly omráčit.

Strapi poskytuje administrátorský panel pro úpravy a vytváření API. Poskytuje také snadno upravitelný kód a používá JavaScript.

Lešení projektu Strapi

Chcete-li nainstalovat Strapi, přejděte do dokumentace Strapi na Strapi. Pro tento projekt budeme používat databázi SQLite. Chcete-li nainstalovat Strapi, spusťte následující příkazy:

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

Nahraďte my-project s názvem, kterému chcete volat adresář aplikací. Váš správce balíčků vytvoří adresář se zadaným názvem a nainstaluje Strapi.

Pokud jste postupovali podle pokynů správně, měli byste mít na svém počítači nainstalován Strapi. Spusťte následující příkazy pro spuštění vývojového serveru Strapi:

    yarn develop # using yarn
    npm run develop # using npm

Vývojový server spustí aplikaci na http://localhost:1337/admin.

Vytváření sbírek událostí

Vytvořme náš Event typ sbírky:

  1. Klikněte na Content-Type Builder pod Plugins v postranní nabídce.
  2. Pod collection types , klikněte na create new collection type .
  3. Vytvořte nový collection-type s názvem Event .
  4. V části typ obsahu produktu: vytvořte následující pole
    • name jako short text
    • date jako Datetime
    • image jako media (jedno médium)
    • price jako Number (desítkové
    • tickets-available jako Number

Konečný Event typ kolekce by měl vypadat jako na obrázku níže:

Vytváření sbírek vstupenek

Dále vytvoříme Ticket typ sbírky:

  1. Klikněte na Content-Type Builder pod Plugins v postranní nabídce.
  2. Pod collection types , klikněte na create new collection type
  3. Vytvořte nový collection-type s názvem Ticket .
  4. Vytvořte následující pole pod typem obsahu produktu:
    • reference_number jako UID
    • seats_with jako Number
    • seats_without jako Number
    • total jako Number
    • total_seats jako Number
    • event jako relation (Událost má mnoho vstupenek.)

Konečný Ticket typ kolekce by měl vypadat jako na obrázku níže:

Nasazení databáze

Chcete-li databázi nasadit, vytvořte některá data pod Events typ sbírky. Chcete-li to provést, postupujte podle následujících kroků:

  1. Klikněte na Content Manager v postranní nabídce.
  2. Pod collection types , vyberte Event .
  3. Klikněte na create new entry .
  4. Vytvořte tolik nových položek, kolik chcete.

Povolení veřejného přístupu

Strapi má uživatelská oprávnění a role, které jsou přiřazeny authenticated a public uživatelů. Protože náš systém nevyžaduje přihlášení a registraci uživatele, musíme našemu Content types povolit veřejný přístup .

Chcete-li povolit veřejný přístup, postupujte takto:

  1. Klikněte na Settings pod general v postranní nabídce.
  2. Pod User and permission plugins , klikněte na Roles .
  3. Klikněte na public .
  4. Pod permissions , jiný collection types jsou uvedeny. Klikněte na Event , pak zaškrtněte obě find a findOne .
  5. Dále klikněte na Ticket .
  6. Zkontrolujte create , find a findOne .
  7. Nakonec klikněte na save .

Úspěšně jsme umožnili veřejný přístup k našim typům obsahu; nyní můžeme vytvořit API volá správně.

Vytváření aplikace Vue.js

Dále nainstalujeme a nakonfigurujeme Vue.Js, aby fungoval s naším backendem Strapi.

Chcete-li nainstalovat Vue.js pomocí balíčku @vue/CLI, navštivte dokumenty Vue CLI nebo spusťte jeden z těchto příkazů.

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

Jakmile nainstalujete Vue CLI na místní počítač, spusťte následující příkazy a vytvořte projekt Vue.js.

    vue create my-project

Nahraďte my-project s názvem, který chcete nazývat svůj projekt.

Výše uvedený příkaz by měl spustit aplikaci příkazového řádku, která vás provede vytvořením projektu Vue.js. Vyberte libovolné možnosti, ale vyberte Router , Vuex a linter/formatter protože první dva jsou v naší aplikaci zásadní. Poslední věcí je pěkně naformátovat kód.

Poté, co Vue CLI dokončí vytváření vašeho projektu, spusťte následující příkaz.

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

Nakonec navštivte následující adresu URL:[http://localhost:8080](http://localhost:8080/) otevřete aplikaci Vue.js ve vašem prohlížeči.

Nastavení CSS Tailwind

Jako náš CSS framework použijeme Tailwind CSS. Pojďme se podívat, jak můžeme integrovat Tailwind CSS do naší aplikace 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

V kořenovém adresáři vaší složky Vue.js vytvořte postcss.config.js a napište následující řádky.

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

V kořenovém adresáři složky Vue.js také vytvořte tailwindcss.config.js a napište následující řádky.

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

Komponenty fontu jsme rozšířili přidáním některých fontů, které budeme používat. Tyto fonty musí být nainstalovány na vašem místním počítači, aby správně fungovaly, ale můžete použít jakákoli písma, která se vám líbí.

Nakonec vytvořte index.css soubor ve vašem src složku a přidejte následující řádky.

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

Instalace Axios pro volání API

Potřebujeme balíček pro volání API do našeho Stripi backend a budeme používat Axios balíček pro tento účel.

Spuštěním následujícího příkazu nainstalujte Axios na vašem stroji.

    npm install --save axios
    or
    yarn add axios

Stavební komponenty

V této části vytvoříme komponenty, které tvoří naši aplikaci vue.js.

Vytvoření komponenty „EventList“:

Vytvořte EventList.vue soubor umístěný v src/components složku a do souboru přidejte následující řádky kódu.

    <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="`http://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("http://localhost:1337/api/events?populate=*");
        this.events = res.data.data;
      },
    };
    </script>
    <style scoped></style>

Vytvoření komponenty „EventView“:

Vytvořte EventView.vue soubor umístěný v src/components složku a do souboru přidejte následující řádky kódu.

    <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(`http://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(
            `http://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 = `"http://localhost:1337${img}"`;

        } catch (error) {
          return alert('An Error occurred, please try agian')
        }

      },
    };
    </script>
    <style scoped></style>

Vytváření sbírek vstupenek

Vytvořte Ticket.vue soubor umístěný v src/components složku a do souboru přidejte následující řádky kódu.

    <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>

Zobrazení budovy

V této části použijeme komponenty vytvořené v poslední části k sestavení stránek na našem frontendu.

Vytvoření zobrazení „Události“

Events stránka využívá EventsView.vue komponentu, kterou jsme vytvořili v předchozí části.

Vytvořte Event.vue soubor umístěný v src/views složku a upravte obsah souboru na následující:

    <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>

Vytvoření zobrazení „Domů“:

Home stránka využívá EventList.vue komponentu, kterou jsme vytvořili v předchozí části.

Vytvořte Home.vue soubor umístěný v src/views složku a upravte obsah souboru na následující:

    <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>

Aktualizace směrovače Vue

Vytvořili jsme některé nové soubory zobrazení, které musíme zpřístupnit jako trasy. Aby k tomu však došlo, musíme aktualizovat náš router, aby odrážel provedené změny.

Chcete-li provést změny na routeru Vue, postupujte podle následujících kroků:

  • Otevřete index.js soubor umístěný na src/router a upravte obsah na následující:
    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;

Ruční úprava backendu Strapi

Jedna z hlavních výhod Strapi je, že nám umožňuje upravovat ovladače, služby a další.

V této části upravíme ticket controller v našem Strapi backend. Při vytváření nového tiketu chceme provést určitou logiku, například:

  1. Kontrola, zda jsou dostupné vstupenky na akci dostatečné na vytvoření nových vstupenek.
  2. Kontrola, zda nebyly vyčerpány dostupné vstupenky na akci.

Chcete-li upravit ticket controller, postupujte podle následujících kroků :

  • Otevřete strapi složku ve vašem oblíbeném editoru kódu.
  • Přejděte na src/api/ticket složka.
  • Pod src/api/ticket klikněte na ovladače.
  • Otevřete ticket.js .
  • Nakonec aktualizujte obsah ticket.js obsahovat následující kód:
    '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;
          }

    }));

Závěr

Doufám, že vám tento tutoriál poskytl přehled o tom, jak vytvořit systém prodeje vstupenek s Strapi . Je toho mnohem víc, co byste mohli do této aplikace přidat, myslete na to jako na výchozí bod.

  • Rozhraní frontend repo pro tento výukový program lze nalézt zde.
  • Backend repo pro tento výukový program lze nalézt zde.