どのようにVueとトレリオクローンアプリケーションを構築する.シリーズポートフォリオアプリ


「ポートフォリオApps」シリーズのこの第3のエピソードは、Trelloクローンを造るために専用されます.クラシックライト?私はVueマスターに見つけることができるよりも柔軟なチュートリアルを提案する.私はあなたがそれをお楽しみください!

1.0/セットアップ
  • 1.1 | Install Vue
  • 1.2 | Creating a new project
  • 1.3 | Vuex
  • 1.4 | App.vue & Main.js config

  • コンポーネント/ルータ
  • 2.1 | Components
  • 2.2 | View & Router

  • [ 1.1 ] Vue 3をインストールする
    # Install latest stable of Vue
    
    yarn global add @vue/cli
    

    新しいプロジェクトの作成

    This time, let's manually select features for this new Vue application.

    # run this command
    
    vue create trello-clone
    

    Do you remember previous tutorial about "Shopping Cart App" ? We saved a preset configuration. Let's use it !

    I named it "config-portfolio".




    [ 1.3 ] Vuexの設定

    As each tutorial, we are going to use Vuex. Let's dive in.

    # ../store/index.js
    
    import { createStore } from "vuex";
    
    import rootMutations from "./mutations.js";
    import rootActions from "./actions.js";
    import rootGetters from "./getters.js";
    
    const store = createStore({
      state() {
        return {
          overlay: false,
          lastListId: 3,
          lastCardId: 5,
          currentData: null,
          lists: [
            {
              id: 1,
              name: "list #1",
            },
            {
              id: 2,
              name: "list #2",
            },
            {
              id: 3,
              name: "list #3",
            },
          ],
          cards: [
            {
              listId: 1,
              id: 1,
              name: "card 1",
            },
            {
              listId: 2,
              id: 2,
              name: "card 2",
            },
            {
              listId: 3,
              id: 3,
              name: "card 3",
            },
          ],
        };
      },
      mutations: rootMutations,
      actions: rootActions,
      getters: rootGetters,
    });
    
    export default store;
    
    

    We need creating 6 actions & mutations.

    # ../store/actions.js
    
    export default {
      createList(context, payload) {
        context.commit("createNewList", payload);
      },
      createCard(context, payload) {
        context.commit("createNewCard", payload);
      },
      toggleOverlay(context) {
        context.commit("toggleOverlay");
      },
      openForm(context, payload) {
        context.commit("openForm", payload);
      },
      saveCard(context, payload) {
        context.commit("saveCard", payload);
      },
      deleteCard(context, payload) {
        context.commit("deleteCard", payload);
      },
    };
    
    
    # ../store/mutations.js
    
    export default {
      createNewList(state, payload) {
        state.lastListId++;
        const list = {
          id: state.lastListId,
          name: payload,
        };
        state.lists.push(list);
      },
      createNewCard(state, payload) {
        state.lastCardId++;
        const card = {
          listId: payload.listId,
          id: this.lastCardId,
          name: payload.name,
        };
        state.cards.push(card);
      },
      toggleOverlay(state) {
        state.overlay = !state.overlay;
      },
      openForm(state, payload) {
        state.currentData = payload;
      },
      saveCard(state, payload) {
        state.cards = state.cards.map((card) => {
          if (card.id === payload.id) {
            return Object.assign({}, card, payload);
          }
          return card;
        });
      },
      deleteCard(state, payload) {
        const indexToDelete = state.cards
          .map((card) => card.id)
          .indexOf(payload.id);
        state.cards.splice(indexToDelete, 1);
      },
    };
    
    

    To complete our store configuration, let's initialize 6 getters.

    export default {
      lastListId(state) {
        return state.lastListId;
      },
      lastCardId(state) {
        return state.lastCardId;
      },
      lists(state) {
        return state.lists;
      },
      cards(state) {
        return state.cards;
      },
      overlay(state) {
        return state.overlay;
      },
      currentData(state) {
        return state.currentData;
      },
    };
    
    

    Great ! Our Vuex configuration is over now 👍


    [ 1.4 ]アプリケーション.Vue & mainjs

    Before create our components, we need to change some detail in App.vue and main.js files :

    # ../App.vue
    
    <template>
      <router-view />
    </template>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
    }
    
    body {
      margin: 0;
      overflow: hidden;
    }
    
    input {
      border: none;
      font-size: 15px;
      outline: none;
    }
    </style>
    
    
    # ../main.js
    
    import { createApp } from "vue";
    import App from "./App.vue";
    import router from "./router";
    import store from "./store/index.js";
    
    const app = createApp(App);
    
    app.use(router);
    app.use(store);
    app.mount("#app");
    
    

    Well configuration files is over ! Next step ? Creating all components.


    [ 2.1 ]コンポーネント

    This Trello Clone needs four components :

    components
    |-- Card.vue
    |-- CardList.vue
    |-- Overlay.vue
    |-- Popup.vue
    
    
    # ../components/Card.vue
    
    <template>
      <input
        class="input-card"
        type="text"
        placeholder="Create a Card"
        v-model="cardName"
        @keyup.enter="createCard"
      />
    </template>
    
    <script>
    export default {
      props: ["listId"],
      methods: {
        createCard() {
          if (this.cardName !== "") {
            const card = {
              listId: this.listId,
              name: this.cardName,
            };
            this.$store.dispatch("createCard", card);
            this.cardName = "";
          }
        },
      },
    };
    </script>
    
    <style>
    .input-card {
      position: relative;
      background-color: white;
      min-height: 30px;
      width: 100%;
      display: flex;
      align-items: center;
      border-radius: 5px;
      padding: 10px;
      word-break: break-all;
      font-size: 16px;
    }
    </style>
    
    

    About CardsList.vue component, we need installing a new dependency which allow us using "drag and drop" easily.

    https://github.com/anish2690/vue-draggable-next
    npm install vue-draggable-next
    
    # or
    
    yarn add vue-draggable-next
    
    # ../components/CardsList.vue
    
    <template>
      <draggable :options="{ group: 'cards' }" group="cards" ghostClass="ghost">
        <span
          class="element-card"
          v-for="(card, index) in cards"
          :key="index"
          @click="togglePopup(card)"
        >
          {{ card.name }}
        </span>
      </draggable>
    </template>
    
    <script>
    import { VueDraggableNext } from "vue-draggable-next";
    
    export default {
      props: ["listId", "listName"],
      components: {
        draggable: VueDraggableNext,
      },
      methods: {
        togglePopup(data) {
          const currentData = {
            listId: this.listId,
            listName: this.listName,
            id: data.id,
            name: data.name,
          };
          this.$store.dispatch("toggleOverlay");
          this.$store.dispatch("openForm", currentData);
        },
      },
      computed: {
        cards() {
          const cardFilteredByListId = this.$store.getters["cards"];
          return cardFilteredByListId.filter((card) => {
            if (card.listId === this.listId) {
              return true;
            } else {
              return false;
            }
          });
        },
        overlayIsActive() {
          return this.$store.getters["overlay"];
        },
      },
    };
    </script>
    
    <style>
    .element-card {
      position: relative;
      background-color: white;
      height: auto;
      display: flex;
      align-items: center;
      padding: 10px;
      border-radius: 5px;
      min-height: 30px;
      margin-bottom: 10px;
      word-break: break-all;
      text-align: left;
    }
    </style>
    
    
    # ../components/Overlay.vue
    
    <template>
      <transition>
        <div v-if="overlayIsActive" class="overlay" @click="closeOverlay"></div>
      </transition>
    </template>
    
    <script>
    export default {
      methods: {
        closeOverlay() {
          this.$store.dispatch("toggleOverlay");
        },
      },
      computed: {
        overlayIsActive() {
          return this.$store.getters["overlay"];
        },
      },
    };
    </script>
    
    <style>
    .overlay {
      background-color: rgba(0, 0, 0, 0.5);
      position: absolute;
      height: 100%;
      width: 100%;
      z-index: 500;
    }
    
    .v-enter-from {
      opacity: 0;
    }
    
    .v-enter-active {
      transition: all 0.3s ease-out;
    }
    
    .v-enter-to {
      opacity: 1;
    }
    
    .v-leave-from {
      opacity: 1;
    }
    
    .v-leave-active {
      transition: all 0.3s ease-in;
    }
    
    .v-leave-to {
      opacity: 0;
    }
    </style>
    
    
    # ../components/Popup.vue
    
    <template>
      <transition>
        <div v-if="overlay" class="modal">
          <h1>List Name : {{ currentData.listName }}</h1>
          <input :placeholder="currentData.name" v-model="cardName" />
          <div class="container-button">
            <button class="blue" @click="saveElement">save</button>
            <button class="red" @click="deleteElement">delete</button>
          </div>
        </div>
      </transition>
    </template>
    
    <script>
    import { mapGetters } from "vuex";
    
    export default {
      data() {
        return {
          cardName: null,
        };
      },
      computed: {
        ...mapGetters(["overlay", "currentData"]),
      },
      methods: {
        saveElement() {
          if (this.cardName === null) {
            this.cardName = this.currentData.name;
          }
          const card = {
            listId: this.currentData.listId,
            id: this.currentData.id,
            name: this.cardName,
          };
          this.$store.dispatch("saveCard", card);
          this.cardName = null;
          this.$store.dispatch("toggleOverlay");
        },
        deleteElement() {
          this.$store.dispatch("deleteCard", this.currentData);
          this.$store.dispatch("toggleOverlay");
        },
      },
    };
    </script>
    
    <style scoped>
    .v-enter-from {
      opacity: 0;
    }
    
    .v-enter-active {
      transition: all 0.3s ease-out;
    }
    
    .v-enter-to {
      opacity: 1;
    }
    
    .v-leave-from {
      opacity: 1;
    }
    
    .v-leave-active {
      transition: all 0.3s ease-in;
    }
    
    .v-leave-to {
      opacity: 0;
    }
    
    .modal {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      gap: 20px;
      position: absolute;
      height: 500px;
      width: 500px;
      border-radius: 10px;
      background-color: rgba(235, 236, 240, 1);
      z-index: 550;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }
    
    input {
      width: 250px;
      height: 50px;
      padding: 10px 20px 10px 20px;
      border: 1px solid rgba(60, 60, 60, 0.2);
      border-radius: 15px;
    }
    
    button {
      display: flex;
      border: none;
      color: white;
      padding: 15px 32px;
      text-align: center;
      text-decoration: none;
      display: inline-block;
      font-size: 16px;
      border-radius: 15px;
      cursor: pointer;
      transition-duration: 0.4s;
    }
    
    button:hover {
      color: white;
    }
    
    .blue {
      background-color: rgba(1, 100, 255, 1);
    }
    
    .blue:hover {
      background-color: rgba(1, 100, 255, 0.8);
    }
    
    .red {
      background-color: rgba(250, 52, 75, 1);
    }
    .red:hover {
      background-color: rgba(250, 52, 75, 0.8);
    }
    
    .container-button {
      display: flex;
      flex-direction: row;
      gap: 30px;
    }
    </style>
    
    
    パーフェクト!つだけ最後のステップと我々はこのトレリオクローンを使用することができます.

    ビュー&ルータ

    Let's import all components needs in "Board.vue" view.

    # ../views/Board.vue
    
    <template>
      <main class="list-container">
        <Overlay />
        <Popup />
        <section class="list-wrapper">
          <draggable
            :options="{ group: 'lists' }"
            group="lists"
            ghostClass="ghost"
            class="list-draggable"
          >
            <div class="list-card" v-for="(list, index) in lists" :key="index">
              <label class="list-header">{{ list.name }}</label>
              <div class="list-content">
                <CardsList :listId="list.id" :listName="list.name" />
              </div>
              <div class="list-footer">
                <Card :listId="list.id" />
              </div>
            </div>
          </draggable>
          <input
            type="text"
            class="input-new-list"
            placeholder="Create a List"
            v-model="listName"
            @keyup.enter="createList"
          />
        </section>
      </main>
    </template>
    
    <script>
    import { VueDraggableNext } from "vue-draggable-next";
    import CardsList from "@/components/CardsList";
    import Card from "@/components/Card.vue";
    import Overlay from "@/components/Overlay";
    import Popup from "@/components/Popup";
    
    export default {
      components: {
        draggable: VueDraggableNext,
        CardsList,
        Card,
        Overlay,
        Popup,
      },
      data() {
        return {
          listName: "",
        };
      },
      methods: {
        createList() {
          if (this.listName !== "") {
            this.$store.dispatch("createList", this.listName);
            this.listName = "";
          }
        },
      },
      computed: {
        lists() {
          return this.$store.getters["lists"];
        },
      },
    };
    </script>
    
    <style>
    .list-container {
      position: relative;
      display: flex;
      width: 100vw;
      height: 100vh;
      border: 1px;
      z-index: 10;
    }
    
    .list-wrapper {
      position: relative;
      display: flex;
      flex-direction: row;
      box-sizing: border-box;
      min-width: 100vw;
      height: 100vh;
      padding: 20px;
      background-repeat: no-repeat;
      background-attachment: fixed;
      background-position: center;
      background-size: cover;
      background-image: url("../assets/background-image.jpg");
      gap: 20px;
      overflow-x: scroll;
      overflow-y: hidden;
    }
    
    .ghost {
      opacity: 0.5;
    }
    
    .list-draggable {
      display: flex;
      gap: 20px;
    }
    
    .input-new-list {
      display: flex;
      height: 30px;
      padding: 10px;
      border-radius: 5px;
      background-color: rgba(235, 236, 240, 0.5);
      min-width: 260px;
    }
    
    .input-new-list::placeholder {
      color: white;
    }
    
    .list-card {
      position: relative;
      display: flex;
      flex-direction: column;
      min-width: 300px;
      height: auto;
    }
    
    .list-header {
      position: relative;
      display: flex;
      justify-content: center;
      word-break: break-all;
      align-items: center;
      min-width: 280px;
      max-width: 280px;
      line-height: 50px;
      padding: 0px 10px 0px 10px;
      background-color: rgba(235, 236, 240, 1);
      border-radius: 10px 10px 0px 0px;
      color: rgba(24, 43, 77, 1);
      user-select: none;
    }
    
    .list-content {
      overflow-y: scroll;
      position: relative;
      display: flex;
      flex-direction: column;
      min-width: 280px;
      max-width: 280px;
      height: auto;
      background-color: rgba(235, 236, 240, 1);
      padding: 0px 10px 0px 10px;
      box-shadow: 1.5px 1.5px 1.5px 0.1px rgba(255, 255, 255, 0.1);
      color: rgba(24, 43, 77, 1);
    }
    
    .list-footer {
      position: relative;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 280px;
      background-color: rgba(235, 236, 240, 1);
      border-radius: 0px 0px 10px 10px;
      color: white;
      border-top: 0.5px solid rgba(255, 255, 255, 0.25);
      padding: 0px 10px 10px 10px;
    }
    </style>
    
    
    About the background, you just download a image on https://unsplash.com/ ファイルをインポートし、次のように名前を変更します.
    assets
    |-- background-image.jpg
    
    
    # ../router/index.js
    
    import { createRouter, createWebHistory } from "vue-router";
    import Board from "../views/Board.vue";
    
    const routes = [
      {
        path: "/",
        name: "Board",
        component: Board,
      },
    ];
    
    const router = createRouter({
      history: createWebHistory(process.env.BASE_URL),
      routes,
    });
    
    export default router;
    
    
    完了です!したい結果を確認するには?あなたの端末で“ヤーンサーブ/NPMランサーブ”を実行するか、下のリンクをクリックするだけです.
    https://trello-clone-sith.netlify.app/
    次のエピソードでお会いしましょう😉