LoginSignup
4
1

More than 3 years have passed since last update.

忘年会のプレゼント交換で使用するWebサービスを作成した話

Last updated at Posted at 2019-12-24

はじめに

この記事はOIC ITCreate Club Advent Calendar 2019の25日目の記事です。

きっかけ

弊部の忘年会の際にプレゼント交換をすることになったのですが、先輩から**「なんか作れない?」**という無茶振りを受け、作成しました。

実装

※忘年会前日に6時間で作成したものです。

完成したものはGitHub Pagesで公開しています。
こちらからアクセスできます。

早く作りたかったので使い慣れたNuxt.jsで開発しています。またBootstrapVueも使用しています。
詳しく知りたい方は、リポジトリをご覧下さい。

1 ... 人数を入力する画面

急遽キャンセルが出るとまずいので参加人数を入力できるようにします。
参加人数を入力後、スタートボタンを押すとstart()メソッドが呼び出されます。start()メソッド内では、入力された人数が0以下ならエラー文を表示します。また、data内で定義したstartedをtureにし、参加人数の入力画面からガチャを回す画面へと移ります。移る前に、gachaListへ人数分の長さを持つ配列を代入します。

pages/index.vue
<template>
  <div class="container p-5">
    <h1 class="c-title text-center mb-5">ITC忘年会~プレゼント交換~</h1>

    // 1
    <form v-if="!started" @submit.prevent="start">
      <div class="mb-5">
        <p>人数を入力してね!</p>
        <div class="d-flex align-items-end">
          <input type="number" class="form-control w-25 mr-3" v-model="peoples" />
          <div></div>
        </div>
        <p v-if="notPeoples" class="text-danger">1人以上入力してください。</p>
      </div>
      <div class="text-center">
        <button class="btn btn-primary px-5 py-2" @click="start">スタート</button>
      </div>
    </form>

  </div>
</template>

<script>
export default {
  data() {
    return {
      peoples: 1,
      notPeoples: false,
      gachaList: [],
      started: false
    }
  },
  methods: {
    start() {
      this.notPeoples = false;
      if (this.peoples <= 0) {
        this.notPeoples = true;
        return;
      }
      for (let i = 1; i <= this.peoples; i++) this.gachaList.push(i);
      this.started = true;
    }
  }
}
</script>

<style scoped>
.c-title {
  font-size: 64px;
}
</style>

2 ... ガチャを回す画面

スタートボタンが押された後に表示する画面です。
少し凝ったものを作成したかったため、CSSでガチャガチャを作成しました。また、回すボタンを押すとガチャガチャを回すアニメーションも作成しました。呼び出す側(親)からstartAnimationをガチャガチャコンポーネント(子)へ渡し、それがtrueの場合アニメーションが再生されるように実装しています。
ガチャガチャのCSSに関してはCodePenで公開しましたのでそちらをご確認下さい。

pages/index.vue
<template>
  <div class="container p-5">
    <h1 class="c-title text-center mb-5">ITC忘年会~プレゼント交換~</h1>

    // 1
    <form v-if="!started" @submit.prevent="start">
      ...
    </form>

    // 2
    <div v-if="started">
      <div>残りカプセル:{{gachaList.length}}</div>
      <Gacha :startAnimation="startAnimation" />
      <div class="text-center">
        <button type="button" class="btn btn-primary px-5 py-2" @click="turn">回す</button>
      </div>
    </div>

  </div>
</template>

<script>
import Gacha from "~/components/Gacha"

export default {
  components: {
    Gacha
  },
  data() {
    return {
      ...
      startAnimation: false
    }
  },
  methods: {
    ...
    turn() {
      this.startAnimation = true;
    }
  }
}
</script>
...
components/Gacha.vue
<template>
  <div class="c-gacha">
    <div class="c-gacha-container">
      <div class="c-gacha-box">
        <div class="c-gacha-capsule c-move-1">
          <div class="c-gacha-capsule-color c-red" />
        </div>
        <div class="c-gacha-capsule c-move-2">
          <div class="c-gacha-capsule-color c-blue" />
        </div>
        ...
      </div>
      <div class="c-gacha-body">
        <div class="c-gacha-handle">
          <div v-if="startAnimation" class="c-gacha-handle-bar c-rotate-animation" />
          <div v-else class="c-gacha-handle-bar" />
        </div>
        <div class="c-gacha-exit">
          <div v-if="startAnimation" class="c-gacha-capsule c-show-animation">
            <div class="c-gacha-capsule-color" :style="{background: returnColor()}" />
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    startAnimation: {
      default: false
    }
  },
  data() {
    return {
      colorClass: [
        'red',
        'blue',
        'green',
        'yellow'
      ]
    }
  },
  methods: {
    returnColor() {
      return this.colorClass[Math.floor(Math.random() * 4)];
    }
  }
}
</script>

<style scoped>
...
</style>

3 ... どのプレゼントが当たったかを表示するモーダル

ガチャを回した後にどのプレゼントが当たったかを表示するモーダルです。
※事前にプレゼントに番号を振っておきましょう

回すボタンを押すとランダムな値(0 〜 gachaListの長さ - 1の間)を生成します。それを使用しモーダルに表示する数字を決定します。
ガチャガチャのアニメーションが終了してからopenModal()メソッドを呼び出し、showModaltrueにすることでモーダルを表示しています。また、自分のプレゼントを引いてしまった場合を考慮し、モーダルにはカプセルを戻すボタンも付けました。
ガチャを続けるボタンを押すとsplice()メソッドを使用しgachaList[ランダムな値]を取り除きます。

pages/index.vue
<template>
  <div class="container p-5">
    <h1 class="c-title text-center mb-5">ITC忘年会~プレゼント交換~</h1>

    // 1
    <form v-if="!started" @submit.prevent="start">
      ...
    </form>

    // 2
    <div v-if="started">
      ...
    </div>

    // 3
    <modal
      v-if="showModal"
      :number="hitPeopleNumber"
      @returnCapsule="returnCapsule"
      @continueGacha="continueGacha"
    />

  </div>
</template>

<script>
...
import Modal from "~/components/Modal"

export default {
  components: {
    ...
    Modal
  },
  data() {
    return {
      ...
      randomIndex: 0,
      hitPeopleNumber: 0,
      showModal: false
    }
  },
  methods: {
    ...
    turn() {
      ...
      this.randomIndex = Math.floor(Math.random() * this.gachaList.length);
      this.hitPeopleNumber = this.gachaList[this.randomIndex];
      setTimeout(this.openModal, 4000);
    },
    openModal() {
      this.showModal = true;
    },
    returnCapsule() {
      this.startAnimation = false;
      this.showModal = false;
    },
    continueGacha() {
      this.gachaList.splice(this.randomIndex, 1);
      this.startAnimation = false;
      this.showModal = false;
    }
  }
}
</script>
...
components/Modal.vue
<template>
  <div>
    <div class="c-modal">
      <div class="c-modal-content">
        <h3 class="c-modal-title">{{ number }}</h3>
        <div class="c-modal-footer">
          <button class="btn btn-danger mb-3" @click="returnCapsule">カプセルを戻す</button>
          <button class="btn btn-primary mb-3" @click="continueGacha">ガチャを続ける</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    number: {
      default: 1
    }
  },
  methods: {
    close() {
      this.$emit('closeModal');
    },
    returnCapsule() {
      this.$emit('returnCapsule');
    },
    continueGacha() {
      this.$emit('continueGacha');
    }
  }
}
</script>

<style scoped>
.c-modal {
  width: 100vw;
  height: 100vh;
  background-color: rgba(100, 100, 100, 0.4);
  position: fixed;
  top: 0;
  left: 0;
}
.c-modal-content {
  width: 50%;
  height: 500px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: white;
  text-align: center;
}
.c-modal-title {
  position: absolute;
  top: 42.5%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 150px;
}
.c-modal-footer {
  display: flex;
  flex-wrap: wrap;
  position: absolute;
  width: 100%;
  justify-content: space-around;
  bottom: 30px;
  padding: 0 20px;
}
</style>

4 ... 終了を表示するモーダル

ガチャガチャを回し終わった後に表示するモーダルです。
ガチャガチャを人数分回しgachaListの長さが0になると、finishedtrueとなり、終了を表示するモーダルが表示されます。モーダルの閉じるボタンを押すと参加人数を入力する画面へ切り替わります。

pages/index.vue
<template>
  <div class="container p-5">
    <h1 class="c-title text-center mb-5">ITC忘年会~プレゼント交換~</h1>

    // 1
    <form v-if="!started" @submit.prevent="start">
      ...
    </form>

    // 2
    <div v-if="started">
      ...
    </div>

    // 3
    <modal
      ...
    />

    // 4
    <finish-modal
      v-if="finished"
      @close="closeFinishModal"
    />

  </div>
</template>

<script>
...
import FinishModal from "~/components/FinishModal"

export default {
  components: {
    ...
    FinishModal
  },
  data() {
    return {
      ...
      finished: false
    }
  },
  methods: {
    ...
    continueGacha() {
      ...
      if (this.gachaList.length === 0) this.finish();
    },
    finish() {
      this.finished = true;
    },
    closeFinishModal() {
      this.started = false;
      this.finished = false;
    }
  }
}
</script>
...
components/FinishModal.vue
<template>
  <div>
    <div class="c-modal">
      <div class="c-modal-content">
        <h3 class="c-modal-title">終了しました!</h3>
        <button class="btn btn-primary px-5 c-modal-btn-primary" @click="close">閉じる</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    close() {
      this.$emit('close');
    }
  }
}
</script>

<style scoped>
.c-modal {
  width: 100vw;
  height: 100vh;
  background-color: rgba(100, 100, 100, 0.4);
  position: fixed;
  top: 0;
  left: 0;
}
.c-modal-content {
  width: 50%;
  height: 500px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: white;
  text-align: center;
}
.c-modal-title {
  width: 100%;
  position: absolute;
  top: 42.5%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 50px;
  padding: 0 20px;
}
.c-modal-btn-primary {
  position: absolute;
  bottom: 50px;
  left: 50%;
  transform: translateX(-50%);
}
</style>

これで完成です。
再度ですが、こちらから完成したサービスを確認することができます。

最後に

実際の忘年会ではこのWebサービスを使用し、プレゼント交換が楽しく行われました。
参加して下さった方々が楽しそうにしていたので、作成した甲斐があったと思います。

4
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1