QRコード読み取りサイトを作成してみた

## はじめに
初めましての人もそうでない人もこんにちは！

花粉が飛び回る季節になってきましたね！
私の友人は風邪＋花粉でダウンしています
私は花粉症になったことがないため気持ちがあんまりわからないですよね...
花粉症だと自覚していないだけかもしれませんが

さて花粉症の危険性がない状態でNext.jsとTypeScriptを使ってQRコードを読み取ってURLを表示するサイトを作成したのでぜひ最後までご覧ください！

## 作ってみた！
**準備**
```bash
npx create-next-app read_qr
✔ Would you like to use TypeScript? …  Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … No
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for next dev? … No
✔ Would you like to customize the import alias (@/* by default)? … No

cd read_qr
npm install html5-qrcode
mkdir components
cd components
touch QrUploader.tsx
cd ..
mkdir styles
cd styles
touch QrUploader.css
```

**コーディング**
```css: styles /QrUploader.css
.pattern-bg {
    background-image: 
      radial-gradient(circle at 25px 25px, rgba(255,255,255,0.2) 2px, transparent 0),
      radial-gradient(circle at 75px 75px, rgba(255,255,255,0.2) 2px, transparent 0);
    background-size: 100px 100px;
  }
  
  .qr-uploader {
    background: linear-gradient(135deg, #FFB6C1, #ADD8E6, #98FB98);
    background-size: 200% 200%;
    padding: 2.5rem;
    border-radius: 1.5rem;
    box-shadow: 
      0 20px 25px -5px rgb(0 0 0 / 0.1),
      0 8px 10px -6px rgb(0 0 0 / 0.1);
    backdrop-filter: blur(12px);
    border: 2px solid rgba(255, 255, 255, 0.4);
    transition: transform 0.3s ease;
    position: relative;
    overflow: hidden;
    animation: gradientBG 10s ease infinite;
  }
  
  .sparkles {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: 
      linear-gradient(125deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.3) 30%, rgba(255,255,255,0) 50%);
    transform: translateX(-100%);
    animation: shine 3s infinite;
  }
  
  .qr-content {
    position: relative;
    z-index: 1;
  }
  
  .qr-uploader:hover {
    transform: translateY(-5px);
  }
  
  .upload-container {
    display: flex;
    justify-content: center;
    margin-bottom: 2rem;
  }
  
  .file-input {
    display: none;
  }
  
  .upload-label {
    background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
    color: white;
    padding: 1rem 2rem;
    border-radius: 999px;
    cursor: pointer;
    transition: all 0.3s ease;
    font-weight: 600;
    letter-spacing: 0.5px;
    box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
  }
  
  .upload-label:hover {
    transform: scale(1.05);
    box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4);
    background: linear-gradient(135deg, #FF8E53 0%, #FF6B6B 100%);
  }
  
  .upload-label:active {
    transform: scale(0.98);
  }
  
  .preview-container {
    display: flex;
    justify-content: center;
    margin: 2rem 0;
    position: relative;
  }
  
  .preview-frame {
    background: linear-gradient(135deg, #FFD1D1, #FFE9D1, #D1FFD1);
    padding: 1.5rem;
    border-radius: 1.5rem;
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
    position: relative;
    animation: fadeIn 0.5s ease;
    border: 2px solid rgba(255, 255, 255, 0.6);
  }
  
  .preview-image {
    border-radius: 0.75rem;
    object-fit: contain;
    background: white;
    padding: 0.5rem;
  }
  
  .result-container {
    margin-top: 1.5rem;
    padding: 1.5rem;
    background: linear-gradient(135deg, rgba(255, 182, 193, 0.4), rgba(173, 216, 230, 0.4));
    border-radius: 1rem;
    border: 2px solid rgba(255, 255, 255, 0.5);
    animation: slideUp 0.5s ease;
    backdrop-filter: blur(4px);
  }
  
  .result-text {
    font-weight: 600;
    color: #FF4B4B;
    margin-bottom: 0.75rem;
    font-size: 1.1rem;
    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
  }
  
  .result-link {
    color: #FF6B6B;
    word-break: break-all;
    text-decoration: none;
    transition: color 0.2s ease;
    font-weight: 500;
    padding: 0.5rem 1rem;
    background: rgba(255, 255, 255, 0.3);
    border-radius: 0.5rem;
    display: inline-block;
  }
  
  .result-link:hover {
    color: #FF8E53;
    background: rgba(255, 255, 255, 0.5);
  }
  
  .error-message {
    color: #FF4B4B;
    text-align: center;
    margin-top: 1rem;
    padding: 0.75rem 1.5rem;
    background: linear-gradient(135deg, rgba(255, 182, 193, 0.9), rgba(255, 105, 180, 0.9));
    border-radius: 999px;
    animation: shake 0.5s ease;
    font-weight: 500;
    box-shadow: 0 4px 10px rgba(255, 105, 180, 0.3);
    border: 1px solid rgba(255, 255, 255, 0.4);
  }
  
  @keyframes fadeIn {
    from {
      opacity: 0;
      transform: scale(0.95);
    }
    to {
      opacity: 1;
      transform: scale(1);
    }
  }
  
  @keyframes slideUp {
    from {
      opacity: 0;
      transform: translateY(20px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
  
  @keyframes shake {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-5px); }
    75% { transform: translateX(5px); }
  }
  
  @keyframes shine {
    0% { transform: translateX(-100%); }
    50%, 100% { transform: translateX(100%); }
  }
  
  @keyframes gradientBG {
    0% { background-position: 0% 50%; }
    50% { background-position: 100% 50%; }
    100% { background-position: 0% 50%; }
  }
  
  @keyframes float {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-10px); }
  }
  
  .animate-title {
    animation: float 3s ease-in-out infinite;
  }
  
  @media (max-width: 640px) {
    .qr-uploader {
      padding: 1.5rem;
    }
    
    .upload-label {
      padding: 0.75rem 1.5rem;
      font-size: 0.9rem;
    }
    
    .preview-frame {
      padding: 0.75rem;
    }
    
    .result-container {
      padding: 1rem;
    }
  }
```

```tsx: components/ QrUploader.tsx
'use client'

import { useState } from 'react'
import Image from 'next/image'
import { Html5Qrcode } from 'html5-qrcode'
import '../styles/QrUploader.css'

export default function QrUploader() {
  const [previewUrl, setPreviewUrl] = useState<string>('')
  const [scanResult, setScanResult] = useState<string>('')
  const [fileError, setFileError] = useState<string>('')

  const navigateToUrl = (url: string) => {
    if (url) {
      window.open(url, '_blank', 'noopener,noreferrer')
    }
  }

  const examineQrCode = async (file: File) => {
    const html5QrCode = new Html5Qrcode('qr-reader')
    const imageUrl = URL.createObjectURL(file)
    setPreviewUrl(imageUrl)

    try {
      const result = await html5QrCode.scanFile(file, true)
      if (result) {
        setScanResult(result)
        navigateToUrl(result)
      } else {
        setFileError('QRコードを読み取れませんでした 📸 別の画像を試してみてください！')
      }
    } catch {
      setFileError('QRコードを読み取れませんでした 📸 別の画像を試してみてください！')
    }

    try {
      await html5QrCode.clear()
    } catch {
    }
  }

  const uploadImage = async (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0]
    setFileError('')
    setScanResult('')
    
    if (!file) return
    
    if (!file.type.includes('image')) {
      setFileError('画像ファイルをアップロードしてください 🖼️')
      return
    }

    await examineQrCode(file)
  }

  return (
    <div className="qr-uploader">
      <div className="sparkles"></div>
      <div className="qr-content">
        <div className="upload-container">
          <input
            type="file"
            accept="image/*"
            onChange={uploadImage}
            className="file-input"
            id="qr-input"
          />
          <label htmlFor="qr-input" className="upload-label">
            📸 QRコードを選択する
          </label>
        </div>

        {fileError && (
          <p className="error-message">{fileError}</p>
        )}

        {previewUrl && (
          <div className="preview-container">
            <div className="preview-frame">
              <Image
                src={previewUrl}
                alt="QRコードプレビュー"
                width={200}
                height={200}
                className="preview-image"
              />
            </div>
          </div>
        )}

        {scanResult && (
          <div className="result-container">
            <p className="result-text">🎯 読み取り成功！</p>
            <a href={scanResult} target="_blank" rel="noopener noreferrer" className="result-link">
              {scanResult}
            </a>
          </div>
        )}

        <div id="qr-reader" style={{ display: 'none' }} />
      </div>
    </div>
  )
}
```

```tsx: app/ page.tsx
import QrUploader from '@/components/QrUploader'

export default function HomePage() {
  return (
    <main className="min-h-screen bg-gradient-to-br from-violet-400 via-fuchsia-300 to-pink-300 py-12 px-4 pattern-bg">
      <div className="max-w-3xl mx-auto">
        <h1 className="text-4xl font-bold text-black mb-8 text-center drop-shadow-lg animate-title">
          QRコードリーダー
        </h1>
        <QrUploader />
      </div>
    </main>
  )
}
```

コードをコピペしたら以下のコマンドをターミナルに入力して実行してください！

```
npm run dev
```

## 実行してみた！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/afcbfedc-15f4-4456-a6b3-e2c991be4cec.png)

うまく表示できました！
次にQRコードを入れてみたいと思います！

以前URLからQRコードを生成するアプリを作成したのでこれを用いて作成したQRコードを読み込ませていきます！

https://qiita.com/FrohleinYoshie/items/f69b185ed4eec6272e20

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/a1ae0686-c0f4-4203-93bf-676ef4365a67.png)

今回は上の画像を使って検証したいと思います！

**画像を添付後**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/774e2e17-eb11-475d-856c-67c248154bc6.png)

無事に読み取りが成功してリンク先に正しく移動することができました！

## 終わりに
今回の記事はいかがだったでしょうか？
新学期も近くまで迫ってきたので体調に気をつけてください！
またどこかの記事でお会いしましょう！