Nuxt4で簡単な投票アプリをつくる

Nuxt4のキャッチアップもかねて、単純な投票機能をもったアプリをつくってみたい。
※結果だけ先に書くと、このアプリではNuxt4の新機能にはたどりつけていません :pray: ディレクトリが `app`に集約されたのは、今更感はあれどみやすくスッキリしてよいと感じた。

## 仕様
- ひとり10ポイントもっている
- 投票は1人に10ポイントいれてもいいし、10人に1ポイントずついれてもいい
- 各投票のポイントをリアルタイムに表示する


## レポジトリ作成

パッケージマネージャは前から気になっていたbunをつかうことにする。

```shell
> npx nuxi init nuxt-poll
✔ Which package manager would you like to use?
bun
◐ Installing dependencies...                                                                                       
bun install v1.1.34 (5e5e7c60)

$ nuxt prepare
✔ Types generated in .nuxt

+ nuxt@3.14.1592
+ vue@3.5.13
+ vue-router@4.5.0

609 packages installed [21.13s]
```

https://nuxt.com/docs/api/commands/init

## Nuxt4のディレクトリ構造に設定

まだNuxt4は正式リリースされていないので、`nuxt@3.14.1592`を起点にNuxt4用の設定が必要。

https://nuxt.com/docs/getting-started/upgrade#testing-nuxt-4

1. compatibilityVersionを4にする
1. appディレクトリを作成し、その配下にディレクトリを作るようにする


```ts:nuxt.config.ts
export default defineNuxtConfig({
  future: {
    compatibilityVersion: 4,
  },
})
```


app配下に配置するディレクトリ
- assets
- components
- composables
- layouts
- middleware
- pages
- plugins

## トップページ作成

```vue:app/pages/index.vue
<script setup lang="ts">
</script>

<template>
  <div>
    <h1>投票画面</h1>
  </div>
</template>
```

確認
```shell
> bun dev
```


![CleanShot 2024-12-13 at 20.31.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883/3b4a5176-5187-9ba4-4e1b-cea4c5ff8375.png)

## 投票アイテムの表示

```pages/poll.vue
<script setup lang="ts">
type Item = {
  name: string
}
const people: Item[] = [
  { name: '林修（予備校講師）' },
  { name: '池上彰（ジャーナリスト）' },
  { name: '松岡修造（元プロテニス選手）' },
  { name: '樹木希林（女優）' },
  { name: '北野武（映画監督・タレント）' },
  { name: '堀江貴文（実業家）' },
  { name: '羽生結弦（フィギュアスケーター）' },
  { name: '村上春樹（作家）' },
  { name: '大谷翔平（プロ野球選手）' },
  { name: '天海祐希（女優）' }]
</script>

<template>
  <div>
    <h1>投票画面</h1>
    <h2>先生になってほしい有名人は？</h2>

    <div>
      のこりポイント
      10
    </div>

    <div v-for="item of people">
      <div class="card">
        <div>
          {{ item.name }}
        </div>
        <div class="right">
          <button>
            投票する
          </button>
          <div class="point">
            0
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="css">
.card {
  padding: 1rem 0.5rem;
  margin: 0.5rem;
  border: 1px solid #333;
  border-radius: 5px;
  display: flex;
  justify-content: space-between;
}

.right {
  display: flex;
  gap: 30px;
  align-items: center;
  margin-right: 2rem;
}

.point {
  font-size: x-large;
}
</style>
```

![CleanShot 2024-12-13 at 20.50.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883/bbf392bb-cbde-843f-faaf-8aff4a3e7ade.png)


## 投票するの実装（投票数の増加）

```ts
type Item = {
  name: string,
  points: number
}
// Item[] → ref(Item[]) の配列にする
const people = ref([
  {
    name: '林修（予備校講師）',
    points: 0
  },
  // ... おなじように points: 0 を書いていく
])

// 投票の実装
// 渡されたitemの投票数をカウントアップ
const clickPoll = (item: Item) => {
  item.points += 1
}
```

```vue
<button @click="clickPoll(item)">
  投票する
</button>
<div class="point">
  {{ item.points }} // 現在の投票数を表示
</div>
```

## 持ち点の増減の実装

```html
<div>
  のこりポイント
  <span class="point">
    {{ remainPoints }}
  </span>
</div>
```
```ts
const remainPoints = ref(10)

const clickPoll = (item: Item) => {
  --remainPoints.value
  item.points += 1
}
```
## 10ポイント使ったら投票無効にする

```html
<button @click="clickPoll(item)" :disabled="remainPoints === 0">
  投票する
</button>
```

## 動作イメージ

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883/1075fcad-fd2e-6858-eb1b-487d2706cd58.gif)