タイムピッカー - Vibe Coding Showcase

タイムピッカー

時間を選択するための入力コンポーネント。ドロップダウンやスピナーで直感的に時刻を設定

デザインプレビュー

タイムピッカーデモ

タイムピッカーの特徴

柔軟な時間単位設定

5分、10分、15分、30分単位など、用途に応じて時間の刻み幅を自由に設定可能。会議予約や診療予約など様々なシーンに対応

12/24時間形式対応

国際的な利用を考慮し、12時間形式(AM/PM表示)と24時間形式の両方に対応。ユーザーの地域設定に合わせて切り替え可能

直感的なUI

ドロップダウン形式で素早く時刻を選択。選択済みの時刻はハイライト表示され、視覚的にわかりやすい設計

AI活用ガイド

予約システム用タイムピッカー

プロンプト例:

診療予約システム用のタイムピッカーを実装してください。営業時間(9:00-18:00)内のみ選択可能、30分単位、昼休み(12:00-13:00)は選択不可、既に予約済みの時間帯は無効化表示、選択可能な直近の時刻を自動提案する機能を含めてください。

勤怠管理システム

プロンプト例:

勤怠管理用のタイムピッカーを作成してください。出勤・退勤時刻の入力、深夜勤務の判定(22:00-5:00)、休憩時間の自動計算、残業時間の警告表示、前日からの連続勤務対応(24時間を超える表示)を実装してください。

スケジューラー連携

プロンプト例:

会議スケジューラー用のタイムピッカーを実装してください。複数タイムゾーン対応、参加者の空き時間をリアルタイム表示、会議室の予約状況連携、定期会議の時間パターン設定、カレンダーAPIとの連携機能を含めてください。

実装コード

ファイルサイズ: 5.0KB TypeScript

コンポーネント実装

import React, { useState, useRef, useEffect } from 'react';

interface TimePickerProps {
  value?: string;
  onChange?: (time: string) => void;
  format?: '12h' | '24h';
  minuteStep?: number;
  disabled?: boolean;
  placeholder?: string;
  className?: string;
}

export const TimePicker: React.FC<TimePickerProps> = ({
  value = '',
  onChange,
  format = '24h',
  minuteStep = 15,
  disabled = false,
  placeholder = '時刻を選択',
  className = ''
}) => {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedTime, setSelectedTime] = useState(value);
  const dropdownRef = useRef<HTMLDivElement>(null);

  const hours = format === '24h' 
    ? Array.from({ length: 24 }, (_, i) => i)
    : Array.from({ length: 12 }, (_, i) => i + 1);

  const minutes = Array.from(
    { length: 60 / minuteStep }, 
    (_, i) => i * minuteStep
  );

  const formatTime = (hour: number, minute: number, period?: 'AM' | 'PM') => {
    const h = hour.toString().padStart(2, '0');
    const m = minute.toString().padStart(2, '0');
    
    if (format === '12h' && period) {
      const displayHour = hour === 0 ? 12 : hour;
      return `${displayHour}:${m} ${period}`;
    }
    
    return `${h}:${m}`;
  };

  const handleTimeSelect = (hour: number, minute: number, period?: 'AM' | 'PM') => {
    const time = formatTime(hour, minute, period);
    setSelectedTime(time);
    onChange?.(time);
    setIsOpen(false);
  };

  useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
        setIsOpen(false);
      }
    };

    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);

  const buttonClasses = [
    'w-full px-4 py-2 pr-10 text-left bg-white border rounded-lg',
    'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent',
    disabled ? 'bg-gray-100 cursor-not-allowed' : 'hover:border-gray-400',
    isOpen ? 'ring-2 ring-blue-500 border-transparent' : 'border-gray-300'
  ].join(' ');

  const chevronClasses = [
    'absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400',
    'transition-transform duration-200',
    isOpen ? 'rotate-180' : ''
  ].join(' ');

  return (
    <div className={`relative ${className}`} ref={dropdownRef}>
      <button
        type="button"
        onClick={() => !disabled && setIsOpen(!isOpen)}
        disabled={disabled}
        className={buttonClasses}
      >
        <div className="flex items-center">
          <svg className="w-5 h-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
          </svg>
          <span className={selectedTime ? 'text-gray-900' : 'text-gray-500'}>
            {selectedTime || placeholder}
          </span>
        </div>
        <svg 
          className={chevronClasses}
          fill="none" 
          stroke="currentColor" 
          viewBox="0 0 24 24"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
        </svg>
      </button>

      {isOpen && (
        <div className="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg">
          <div className="p-2 max-h-64 overflow-y-auto">
            {format === '24h' ? (
              <div className="grid grid-cols-4 gap-1">
                {hours.map(hour => 
                  minutes.map(minute => {
                    const timeStr = formatTime(hour, minute);
                    const isSelected = selectedTime === timeStr;
                    const btnClasses = [
                      'px-2 py-1 text-sm rounded hover:bg-blue-50',
                      isSelected ? 'bg-blue-500 text-white hover:bg-blue-600' : 'text-gray-700'
                    ].join(' ');
                    
                    return (
                      <button
                        key={`${hour}-${minute}`}
                        onClick={() => handleTimeSelect(hour, minute)}
                        className={btnClasses}
                      >
                        {timeStr}
                      </button>
                    );
                  })
                )}
              </div>
            ) : (
              <div>
                <div className="mb-2">
                  <div className="text-xs font-semibold text-gray-500 mb-1">午前</div>
                  <div className="grid grid-cols-4 gap-1">
                    {hours.map(hour => 
                      minutes.map(minute => {
                        const timeStr = formatTime(hour, minute, 'AM');
                        const isSelected = selectedTime === timeStr;
                        const btnClasses = [
                          'px-2 py-1 text-sm rounded hover:bg-blue-50',
                          isSelected ? 'bg-blue-500 text-white hover:bg-blue-600' : 'text-gray-700'
                        ].join(' ');
                        
                        return (
                          <button
                            key={`am-${hour}-${minute}`}
                            onClick={() => handleTimeSelect(hour, minute, 'AM')}
                            className={btnClasses}
                          >
                            {timeStr}
                          </button>
                        );
                      })
                    )}
                  </div>
                </div>
                <div>
                  <div className="text-xs font-semibold text-gray-500 mb-1">午後</div>
                  <div className="grid grid-cols-4 gap-1">
                    {hours.map(hour => 
                      minutes.map(minute => {
                        const timeStr = formatTime(hour, minute, 'PM');
                        const isSelected = selectedTime === timeStr;
                        const btnClasses = [
                          'px-2 py-1 text-sm rounded hover:bg-blue-50',
                          isSelected ? 'bg-blue-500 text-white hover:bg-blue-600' : 'text-gray-700'
                        ].join(' ');
                        
                        return (
                          <button
                            key={`pm-${hour}-${minute}`}
                            onClick={() => handleTimeSelect(hour, minute, 'PM')}
                            className={btnClasses}
                          >
                            {timeStr}
                          </button>
                        );
                      })
                    )}
                  </div>
                </div>
              </div>
            )}
          </div>
        </div>
      )}
    </div>
  );
};

使用例

// 基本的な使用例
import { TimePicker } from './components/time-picker';
import { useState } from 'react';

function AppointmentForm() {
  const [startTime, setStartTime] = useState('');
  const [endTime, setEndTime] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    if (!startTime || !endTime) {
      alert('開始時刻と終了時刻を選択してください');
      return;
    }
    
    // 時刻の妥当性チェック
    const start = new Date(`2024-01-01 ${startTime}`);
    const end = new Date(`2024-01-01 ${endTime}`);
    
    if (start >= end) {
      alert('終了時刻は開始時刻より後に設定してください');
      return;
    }
    
    console.log('予約時間:', { startTime, endTime });
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4 max-w-md">
      <div className="grid grid-cols-2 gap-4">
        <div>
          <label className="block text-sm font-medium mb-1">
            開始時刻
          </label>
          <TimePicker
            value={startTime}
            onChange={setStartTime}
            format="24h"
            minuteStep={30}
            placeholder="開始時刻"
          />
        </div>
        
        <div>
          <label className="block text-sm font-medium mb-1">
            終了時刻
          </label>
          <TimePicker
            value={endTime}
            onChange={setEndTime}
            format="24h"
            minuteStep={30}
            placeholder="終了時刻"
          />
        </div>
      </div>
      
      <button
        type="submit"
        className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
        disabled={!startTime || !endTime}
      >
        予約を確定
      </button>
    </form>
  );
}