import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; class SuperportShadDatePicker extends StatefulWidget { final String label; final DateTime? value; final ValueChanged? onChanged; final DateTime? firstDate; final DateTime? lastDate; final String dateFormat; final String? placeholder; final bool enabled; final bool required; final String? errorText; final String? helperText; final bool allowClear; final DatePickerMode mode; const SuperportShadDatePicker({ super.key, required this.label, this.value, this.onChanged, this.firstDate, this.lastDate, this.dateFormat = 'yyyy-MM-dd', this.placeholder, this.enabled = true, this.required = false, this.errorText, this.helperText, this.allowClear = true, this.mode = DatePickerMode.day, }); @override State createState() => _SuperportShadDatePickerState(); } class _SuperportShadDatePickerState extends State { late TextEditingController _controller; final FocusNode _focusNode = FocusNode(); @override void initState() { super.initState(); _controller = TextEditingController( text: widget.value != null ? DateFormat(widget.dateFormat).format(widget.value!) : '', ); } @override void didUpdateWidget(SuperportShadDatePicker oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.value != widget.value) { _controller.text = widget.value != null ? DateFormat(widget.dateFormat).format(widget.value!) : ''; } } @override void dispose() { _controller.dispose(); _focusNode.dispose(); super.dispose(); } Future _selectDate() async { final theme = ShadTheme.of(context); final DateTime? picked = await showDatePicker( context: context, initialDate: widget.value ?? DateTime.now(), firstDate: widget.firstDate ?? DateTime(1900), lastDate: widget.lastDate ?? DateTime(2100), initialDatePickerMode: widget.mode, locale: const Locale('ko', 'KR'), builder: (context, child) { return Theme( data: ThemeData.light().copyWith( primaryColor: theme.colorScheme.primary, colorScheme: ColorScheme.light( primary: theme.colorScheme.primary, onPrimary: theme.colorScheme.primaryForeground, surface: theme.colorScheme.card, onSurface: theme.colorScheme.cardForeground, ), ), child: child!, ); }, ); if (picked != null) { widget.onChanged?.call(picked); } } @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( widget.label, style: theme.textTheme.small.copyWith( fontWeight: FontWeight.w600, fontSize: 14, ), ), if (widget.required) Text( ' *', style: theme.textTheme.small.copyWith( color: theme.colorScheme.destructive, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 8), InkWell( onTap: widget.enabled ? _selectDate : null, borderRadius: BorderRadius.circular(6), child: IgnorePointer( child: ShadInput( controller: _controller, focusNode: _focusNode, placeholder: Text(widget.placeholder ?? 'YYYY-MM-DD'), enabled: widget.enabled, readOnly: true, ), ), ), if (widget.errorText != null) ...[ const SizedBox(height: 4), Text( widget.errorText!, style: theme.textTheme.small.copyWith( color: theme.colorScheme.destructive, fontSize: 12, ), ), ], if (widget.helperText != null && widget.errorText == null) ...[ const SizedBox(height: 4), Text( widget.helperText!, style: theme.textTheme.small.copyWith( color: theme.colorScheme.mutedForeground, fontSize: 12, ), ), ], ], ); } } class SuperportShadDateRangePicker extends StatefulWidget { final String label; final DateTimeRange? value; final ValueChanged? onChanged; final DateTime? firstDate; final DateTime? lastDate; final String dateFormat; final String? placeholder; final bool enabled; final bool required; final String? errorText; final String? helperText; final bool allowClear; const SuperportShadDateRangePicker({ super.key, required this.label, this.value, this.onChanged, this.firstDate, this.lastDate, this.dateFormat = 'yyyy-MM-dd', this.placeholder, this.enabled = true, this.required = false, this.errorText, this.helperText, this.allowClear = true, }); @override State createState() => _SuperportShadDateRangePickerState(); } class _SuperportShadDateRangePickerState extends State { late TextEditingController _controller; final FocusNode _focusNode = FocusNode(); @override void initState() { super.initState(); _updateControllerText(); } void _updateControllerText() { if (widget.value != null) { final startText = DateFormat(widget.dateFormat).format(widget.value!.start); final endText = DateFormat(widget.dateFormat).format(widget.value!.end); _controller = TextEditingController(text: '$startText ~ $endText'); } else { _controller = TextEditingController(); } } @override void didUpdateWidget(SuperportShadDateRangePicker oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.value != widget.value) { _updateControllerText(); } } @override void dispose() { _controller.dispose(); _focusNode.dispose(); super.dispose(); } Future _selectDateRange() async { final theme = ShadTheme.of(context); final DateTimeRange? picked = await showDateRangePicker( context: context, initialDateRange: widget.value, firstDate: widget.firstDate ?? DateTime(1900), lastDate: widget.lastDate ?? DateTime(2100), locale: const Locale('ko', 'KR'), builder: (context, child) { return Theme( data: ThemeData.light().copyWith( primaryColor: theme.colorScheme.primary, colorScheme: ColorScheme.light( primary: theme.colorScheme.primary, onPrimary: theme.colorScheme.primaryForeground, surface: theme.colorScheme.card, onSurface: theme.colorScheme.cardForeground, ), ), child: child!, ); }, ); if (picked != null) { widget.onChanged?.call(picked); } } @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( widget.label, style: theme.textTheme.small.copyWith( fontWeight: FontWeight.w600, fontSize: 14, ), ), if (widget.required) Text( ' *', style: theme.textTheme.small.copyWith( color: theme.colorScheme.destructive, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 8), InkWell( onTap: widget.enabled ? _selectDateRange : null, borderRadius: BorderRadius.circular(6), child: IgnorePointer( child: ShadInput( controller: _controller, focusNode: _focusNode, placeholder: Text(widget.placeholder ?? 'YYYY-MM-DD ~ YYYY-MM-DD'), enabled: widget.enabled, readOnly: true, ), ), ), if (widget.errorText != null) ...[ const SizedBox(height: 4), Text( widget.errorText!, style: theme.textTheme.small.copyWith( color: theme.colorScheme.destructive, fontSize: 12, ), ), ], if (widget.helperText != null && widget.errorText == null) ...[ const SizedBox(height: 4), Text( widget.helperText!, style: theme.textTheme.small.copyWith( color: theme.colorScheme.mutedForeground, fontSize: 12, ), ), ], ], ); } } class KoreanDateTimeFormatter { static String format(DateTime dateTime, {String pattern = 'yyyy년 MM월 dd일 (E)'}) { final formatter = DateFormat(pattern, 'ko_KR'); return formatter.format(dateTime); } static String formatRange(DateTimeRange range, {String pattern = 'yyyy.MM.dd'}) { final formatter = DateFormat(pattern, 'ko_KR'); return '${formatter.format(range.start)} ~ ${formatter.format(range.end)}'; } static String formatRelative(DateTime dateTime) { final now = DateTime.now(); final difference = now.difference(dateTime); if (difference.inDays == 0) { if (difference.inHours == 0) { if (difference.inMinutes == 0) { return '방금 전'; } return '${difference.inMinutes}분 전'; } return '${difference.inHours}시간 전'; } else if (difference.inDays == 1) { return '어제'; } else if (difference.inDays == 2) { return '그저께'; } else if (difference.inDays < 7) { return '${difference.inDays}일 전'; } else if (difference.inDays < 30) { return '${(difference.inDays / 7).round()}주 전'; } else if (difference.inDays < 365) { return '${(difference.inDays / 30).round()}개월 전'; } else { return '${(difference.inDays / 365).round()}년 전'; } } }