日付ピッカー(軽量)
:LiTarget: 用途
モバイル対応の軽量日付選択UI。外部ライブラリ非依存。
:LiSparkle: 特徴
- カレンダー表示
- キーボード入力
- 今日/明日のクイック選択
:LiCode: 実コード(SCALE Base より自動抽出)
:LiInfo:
components/tasks/DatePicker.tsxの中身そのもの。コピペ即可。
"use client";
import { useEffect, useMemo, useState } from "react";
interface DatePickerProps {
value: string;
onChange: (val: string) => void;
label?: string;
hideQuickPresets?: boolean; // クイックプリセットを隠す
}
function addDays(base: Date, d: number): string {
const x = new Date(base);
x.setDate(x.getDate() + d);
return x.toISOString().slice(0, 10);
}
/**
* 月/日 select + クイックプリセット(今日/明日/2日後/3日後/1週間後)の期日ピッカー。
* Value format: "YYYY-MM-DD". Year auto-fills to current year.
*
* 月と日の両方が揃ってから onChange を発火する仕様。
* 月だけ選んだ時点で onChange が走ると親が自動で閉じてしまうので、
* ピッカーを開いたまま続けて日を選べるようにする。
*/
export default function DatePicker({ value, onChange, label, hideQuickPresets = false }: DatePickerProps) {
const year = new Date().getFullYear();
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]);
// 内部 state(月・日を独立に保持)
const [month, setMonth] = useState<string>("");
const [day, setDay] = useState<string>("");
// 外部 value が変わったら内部 state を同期
useEffect(() => {
if (!value) { setMonth(""); setDay(""); return; }
const parts = value.split("-");
if (parts.length < 3) { setMonth(""); setDay(""); return; }
setMonth(String(parseInt(parts[1], 10)));
setDay(String(parseInt(parts[2], 10)));
}, [value]);
const handleMonth = (m: string) => {
setMonth(m);
if (!m) { onChange(""); return; }
if (!day) {
// 日が未選択なら確定させない(ピッカーを開いたままにする)
return;
}
onChange(`${year}-${m.padStart(2, "0")}-${day.padStart(2, "0")}`);
};
const handleDay = (d: string) => {
setDay(d);
if (!d) { onChange(""); return; }
if (!month) {
// 月が未選択なら確定させない
return;
}
onChange(`${year}-${month.padStart(2, "0")}-${d.padStart(2, "0")}`);
};
const sel =
"bg-[#0c0c0f] border border-[#2a2a36] rounded-lg text-white text-sm px-2 py-1.5 outline-none focus:border-blue-500/60 transition-colors appearance-none cursor-pointer";
return (
<div className="flex flex-col gap-1.5">
{label && <label className="text-xs text-[#888] font-medium">{label}</label>}
{/* クイックプリセット */}
{!hideQuickPresets && (
<div className="flex flex-wrap items-center gap-1 mb-1">
{presets.map(p => {
const active = value === p.value;
return (
<button
key={p.label}
type="button"
onClick={() => onChange(p.value)}
className={`px-2 py-0.5 text-[11px] rounded-md border transition-colors ${
active
? 'bg-blue-500 border-blue-500 text-white'
: 'bg-[#0c0c0f] border-[#2a2a36] text-[#aaa] hover:text-white hover:border-blue-500/50'
}`}
>
{p.label}
</button>
);
})}
</div>
)}
{/* 月日セレクト */}
<div className="flex items-center gap-1">
<select value={month} onChange={(e) => handleMonth(e.target.value)} className={`${sel} w-[72px]`}>
<option value="">月</option>
{Array.from({ length: 12 }, (_, i) => i + 1).map((m) => (
<option key={m} value={String(m)}>{m}月</option>
))}
</select>
<span className="text-[#888] text-sm">/</span>
<select value={day} onChange={(e) => handleDay(e.target.value)} className={`${sel} w-[72px]`}>
<option value="">日</option>
{Array.from({ length: 31 }, (_, i) => i + 1).map((d) => (
<option key={d} value={String(d)}>{d}日</option>
))}
</select>
{value && !hideQuickPresets && (
<button
type="button"
onClick={() => { setMonth(""); setDay(""); onChange(''); }}
className="text-[#888] hover:text-red-400 text-xs ml-1"
title="クリア"
>
✕
</button>
)}
</div>
{/* 月だけ・日だけ選んだ状態のヒント */}
{((month && !day) || (!month && day)) && (
<p className="text-[10px] text-amber-400/80">
{month && !day ? '日も選んでください' : '月も選んでください'}
</p>
)}
</div>
);
}
:LiFolder: ソースファイルのパス
/Users/oogushiyuuki/Library/CloudStorage/GoogleDrive-y-ogushi@scale-group.co.jp/マイドライブ/AI/scale-base/components/tasks/DatePicker.tsx
:LiHandPointer: 使い方
対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。
:LiAlertCircle: 注意事項
- 依存パッケージを忘れず追加