Files
lunchpick/lib/presentation/widgets/category_selector.dart

335 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lunchpick/core/constants/app_colors.dart';
import 'package:lunchpick/core/utils/category_mapper.dart';
import 'package:lunchpick/presentation/providers/restaurant_provider.dart';
class CategorySelector extends ConsumerWidget {
final String? selectedCategory;
final Function(String?) onCategorySelected;
final bool showAllOption;
final bool multiSelect;
final List<String>? selectedCategories;
final Function(List<String>)? onMultipleSelected;
const CategorySelector({
super.key,
this.selectedCategory,
required this.onCategorySelected,
this.showAllOption = true,
this.multiSelect = false,
this.selectedCategories,
this.onMultipleSelected,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final categoriesAsync = ref.watch(categoriesProvider);
return categoriesAsync.when(
data: (categories) {
return SizedBox(
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
if (showAllOption && !multiSelect) ...[
_buildCategoryChip(
context: context,
label: '전체',
icon: Icons.restaurant_menu,
color: isDark
? AppColors.darkPrimary
: AppColors.lightPrimary,
isSelected: selectedCategory == null,
onTap: () => onCategorySelected(null),
),
const SizedBox(width: 8),
],
...categories.map((category) {
final isSelected = multiSelect
? selectedCategories?.contains(category) ?? false
: selectedCategory == category;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: _buildCategoryChip(
context: context,
label: CategoryMapper.getDisplayName(category),
icon: CategoryMapper.getIcon(category),
color: CategoryMapper.getColor(category),
isSelected: isSelected,
onTap: () {
if (multiSelect) {
_handleMultiSelect(category);
} else {
onCategorySelected(category);
}
},
),
);
}).toList(),
],
),
);
},
loading: () => const SizedBox(
height: 50,
child: Center(child: CircularProgressIndicator()),
),
error: (error, stack) => const SizedBox(
height: 50,
child: Center(child: Text('카테고리를 불러올 수 없습니다')),
),
);
}
void _handleMultiSelect(String category) {
if (onMultipleSelected == null || selectedCategories == null) return;
final List<String> updatedCategories = List.from(selectedCategories!);
if (updatedCategories.contains(category)) {
updatedCategories.remove(category);
} else {
updatedCategories.add(category);
}
onMultipleSelected!(updatedCategories);
}
Widget _buildCategoryChip({
required BuildContext context,
required String label,
required IconData icon,
required Color color,
required bool isSelected,
required VoidCallback onTap,
}) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected
? color.withOpacity(0.2)
: isDark
? AppColors.darkSurface
: AppColors.lightBackground,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected ? color : Colors.transparent,
width: 2,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 20,
color: isSelected
? color
: isDark
? AppColors.darkTextSecondary
: AppColors.lightTextSecondary,
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: isSelected
? color
: isDark
? AppColors.darkText
: AppColors.lightText,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
),
);
}
}
/// 카테고리 선택 다이얼로그
class CategorySelectionDialog extends ConsumerWidget {
final List<String> selectedCategories;
final String title;
final String? subtitle;
const CategorySelectionDialog({
super.key,
required this.selectedCategories,
this.title = '카테고리 선택',
this.subtitle,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final categoriesAsync = ref.watch(categoriesProvider);
return AlertDialog(
backgroundColor: isDark ? AppColors.darkSurface : AppColors.lightSurface,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: TextStyle(
fontSize: 14,
color: isDark
? AppColors.darkTextSecondary
: AppColors.lightTextSecondary,
),
),
],
],
),
content: categoriesAsync.when(
data: (categories) => SizedBox(
width: double.maxFinite,
child: GridView.builder(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
final isSelected = selectedCategories.contains(category);
return _CategoryGridItem(
category: category,
isSelected: isSelected,
onTap: () {
final updatedCategories = List<String>.from(
selectedCategories,
);
if (isSelected) {
updatedCategories.remove(category);
} else {
updatedCategories.add(category);
}
Navigator.pop(context, updatedCategories);
},
);
},
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) =>
Center(child: Text('카테고리를 불러올 수 없습니다: $error')),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'취소',
style: TextStyle(
color: isDark
? AppColors.darkTextSecondary
: AppColors.lightTextSecondary,
),
),
),
TextButton(
onPressed: () => Navigator.pop(context, selectedCategories),
child: const Text('확인'),
),
],
);
}
}
class _CategoryGridItem extends StatelessWidget {
final String category;
final bool isSelected;
final VoidCallback onTap;
const _CategoryGridItem({
required this.category,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final color = CategoryMapper.getColor(category);
final icon = CategoryMapper.getIcon(category);
final displayName = CategoryMapper.getDisplayName(category);
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected
? color.withOpacity(0.2)
: isDark
? AppColors.darkCard
: AppColors.lightCard,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? color : Colors.transparent,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 28,
color: isSelected
? color
: isDark
? AppColors.darkTextSecondary
: AppColors.lightTextSecondary,
),
const SizedBox(height: 4),
Text(
displayName,
style: TextStyle(
fontSize: 12,
color: isSelected
? color
: isDark
? AppColors.darkText
: AppColors.lightText,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
);
}
}