SCALE — Build Lab
UI部品 · REACT COMPONENT

期日クイックピッカー

CATEGORYUI部品 TYPEReact Component EFFORT30〜60分 DIFFICULTY
PRIMARY CODE
tsx · components/tasks/DueDateQuickPicker.tsx
'use client';

// 期日のクイック選択UI
// プリセット: 今日 / 明日 / 2日後 / 3日後 / 1週間後 / カスタム
// 選択するとYYYY-MM-DD文字列をonChangeに返す

import { useMemo } from 'react';

export interface DueDateQuickPickerProps {
  value: string;                      // YYYY-MM-DD
  onChange: (v: string) => void;
  compact?: boolean;                  // 小さいUI
  className?: string;
}

function addDays(base: Date, d: number): string {
  const x = new Date(base);
  x.setDate(x.getDate() + d);
  return x.toISOString().slice(0, 10);
}

export function DueDateQuickPicker({ value, onChange, compact = false, className = '' }: DueDateQuickPickerProps) {
  const today = useMemo(() => new Date(), []);
  const presets = useMemo(() => [
    { label: '今日', value: addDays(today, 0) },
    { label: '明日', value: addDays(today, 1) },
    { label: '2日後', value: addDays(today, 2) },
    { label: '3日後', value: addDays(today, 3) },
    { label: '1週間後', value: addDays(today, 7) },
  ], [today]);

  const selectedPreset = presets.find(p => p.value === value);
  const isCustom = value && !selectedPreset;

  const baseCls = compact
    ? 'px-2 py-0.5 text-[11px] rounded-md'
    : 'px-2.5 py-1 text-xs rounded-md';

  return (
    <div className={`flex flex-wrap items-center gap-1 ${className}`}>
      {presets.map(p => {
        const active = value === p.value;
        return (
          <button
            key={p.label}
            type="button"
            onClick={() => onChange(p.value)}
            className={`${baseCls} border transition-colors ${
              active
                ? 'bg-blue-500 border-blue-500 text-white'
                : 'bg-bg3 border-border text-text2 hover:text-text hover:border-blue-500/50'
            }`}
          >
            {p.label}
          </button>
        );
      })}
      {/* カスタム日付 */}
      <input
        type="date"
        value={value || ''}
        onChange={(e) => onChange(e.target.value)}
        className={`${baseCls} border bg-bg3 text-text placeholder:text-text3 focus:outline-none focus:border-blue-500 ${
          isCustom ? 'border-blue-500' : 'border-border'
        }`}
      />
      {isCustom && value && (
        <span className="text-[10px] text-blue-300 ml-1">カスタム</span>
      )}
      {value && (
        <button
          type="button"
          onClick={() => onChange('')}
          className={`${baseCls} border border-border bg-bg3 text-text3 hover:text-red-400`}
          title="期日をクリア"
        >
          ✕
        </button>
      )}
    </div>
  );
}

前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • タスク期日設定
  • リマインダー

期日クイックピッカー

:LiTarget: 用途

「今日」「明日」「来週」などのクイック選択ボタン付き期日UI。

:LiSparkle: 特徴

  • クイック選択
  • カスタム日付
  • 相対日付表示

:LiCode: 実コード(SCALE Base より自動抽出)

:LiInfo: components/tasks/DueDateQuickPicker.tsx の中身そのもの。コピペ即可。

'use client';

// 期日のクイック選択UI
// プリセット: 今日 / 明日 / 2日後 / 3日後 / 1週間後 / カスタム
// 選択するとYYYY-MM-DD文字列をonChangeに返す

import { useMemo } from 'react';

export interface DueDateQuickPickerProps {
  value: string;                      // YYYY-MM-DD
  onChange: (v: string) => void;
  compact?: boolean;                  // 小さいUI
  className?: string;
}

function addDays(base: Date, d: number): string {
  const x = new Date(base);
  x.setDate(x.getDate() + d);
  return x.toISOString().slice(0, 10);
}

export function DueDateQuickPicker({ value, onChange, compact = false, className = '' }: DueDateQuickPickerProps) {
  const today = useMemo(() => new Date(), []);
  const presets = useMemo(() => [
    { label: '今日', value: addDays(today, 0) },
    { label: '明日', value: addDays(today, 1) },
    { label: '2日後', value: addDays(today, 2) },
    { label: '3日後', value: addDays(today, 3) },
    { label: '1週間後', value: addDays(today, 7) },
  ], [today]);

  const selectedPreset = presets.find(p => p.value === value);
  const isCustom = value && !selectedPreset;

  const baseCls = compact
    ? 'px-2 py-0.5 text-[11px] rounded-md'
    : 'px-2.5 py-1 text-xs rounded-md';

  return (
    <div className={`flex flex-wrap items-center gap-1 ${className}`}>
      {presets.map(p => {
        const active = value === p.value;
        return (
          <button
            key={p.label}
            type="button"
            onClick={() => onChange(p.value)}
            className={`${baseCls} border transition-colors ${
              active
                ? 'bg-blue-500 border-blue-500 text-white'
                : 'bg-bg3 border-border text-text2 hover:text-text hover:border-blue-500/50'
            }`}
          >
            {p.label}
          </button>
        );
      })}
      {/* カスタム日付 */}
      <input
        type="date"
        value={value || ''}
        onChange={(e) => onChange(e.target.value)}
        className={`${baseCls} border bg-bg3 text-text placeholder:text-text3 focus:outline-none focus:border-blue-500 ${
          isCustom ? 'border-blue-500' : 'border-border'
        }`}
      />
      {isCustom && value && (
        <span className="text-[10px] text-blue-300 ml-1">カスタム</span>
      )}
      {value && (
        <button
          type="button"
          onClick={() => onChange('')}
          className={`${baseCls} border border-border bg-bg3 text-text3 hover:text-red-400`}
          title="期日をクリア"
        >

        </button>
      )}
    </div>
  );
}

:LiFolder: ソースファイルのパス

/Users/oogushiyuuki/Library/CloudStorage/GoogleDrive-y-ogushi@scale-group.co.jp/マイドライブ/AI/scale-base/components/tasks/DueDateQuickPicker.tsx

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項

  • 依存パッケージを忘れず追加