Files
superport/lib/widgets/shadcn/shad_date_picker.dart

368 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
class SuperportShadDatePicker extends StatefulWidget {
final String label;
final DateTime? value;
final ValueChanged<DateTime?>? 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<SuperportShadDatePicker> createState() => _SuperportShadDatePickerState();
}
class _SuperportShadDatePickerState extends State<SuperportShadDatePicker> {
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<void> _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<DateTimeRange?>? 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<SuperportShadDateRangePicker> createState() => _SuperportShadDateRangePickerState();
}
class _SuperportShadDateRangePickerState extends State<SuperportShadDateRangePicker> {
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<void> _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()}년 전';
}
}
}