Compare commits
2 Commits
1d79dde5e1
...
af1794925d
| Author | SHA1 | Date | |
|---|---|---|---|
| af1794925d | |||
| 2652736d31 |
@@ -93,11 +93,64 @@ class _CafeDetailScreenState extends ConsumerState<CafeDetailScreen> {
|
|||||||
final description = cafe['description'] as String?;
|
final description = cafe['description'] as String?;
|
||||||
final address = cafe['address'] as String?;
|
final address = cafe['address'] as String?;
|
||||||
final city = cafe['city'] as String?;
|
final city = cafe['city'] as String?;
|
||||||
|
// Defensive parsing — public DTO key names may vary.
|
||||||
|
final cover = (cafe['coverImageUrl'] ?? cafe['coverUrl'] ?? cafe['cover']) as String?;
|
||||||
|
final isOpen = cafe['isOpenNow'] as bool?;
|
||||||
|
final gallery = (cafe['galleryUrls'] ?? cafe['gallery']) is List
|
||||||
|
? ((cafe['galleryUrls'] ?? cafe['gallery']) as List)
|
||||||
|
.map((e) => e.toString())
|
||||||
|
.where((e) => e.isNotEmpty)
|
||||||
|
.toList()
|
||||||
|
: <String>[];
|
||||||
|
// WorkingHoursPublicDto: a day-keyed object {sat..fri}, each {isOpen,open,close}.
|
||||||
|
final hours = cafe['workingHours'] is Map
|
||||||
|
? (cafe['workingHours'] as Map)
|
||||||
|
: const <dynamic, dynamic>{};
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
Text(name, style: Theme.of(context).textTheme.headlineSmall),
|
if (cover != null && cover.isNotEmpty) ...[
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: Image.network(
|
||||||
|
cover,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) =>
|
||||||
|
Container(color: Colors.black12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(name,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall),
|
||||||
|
),
|
||||||
|
if (isOpen != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: (isOpen ? Colors.green : Colors.red)
|
||||||
|
.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
isOpen ? 'باز است' : 'بسته است',
|
||||||
|
style: TextStyle(
|
||||||
|
color: isOpen ? Colors.green[800] : Colors.red[800],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -117,6 +170,59 @@ class _CafeDetailScreenState extends ConsumerState<CafeDetailScreen> {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(description),
|
Text(description),
|
||||||
],
|
],
|
||||||
|
if (gallery.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SizedBox(
|
||||||
|
height: 110,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: gallery.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||||
|
itemBuilder: (_, i) => ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.network(
|
||||||
|
gallery[i],
|
||||||
|
width: 150,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) =>
|
||||||
|
Container(width: 150, color: Colors.black12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (hours.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text('ساعات کاری',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
...const [
|
||||||
|
('sat', 'شنبه'),
|
||||||
|
('sun', 'یکشنبه'),
|
||||||
|
('mon', 'دوشنبه'),
|
||||||
|
('tue', 'سهشنبه'),
|
||||||
|
('wed', 'چهارشنبه'),
|
||||||
|
('thu', 'پنجشنبه'),
|
||||||
|
('fri', 'جمعه'),
|
||||||
|
].map((d) {
|
||||||
|
final m = hours[d.$1] is Map
|
||||||
|
? hours[d.$1] as Map
|
||||||
|
: const <dynamic, dynamic>{};
|
||||||
|
final open = (m['open'] ?? '').toString();
|
||||||
|
final close = (m['close'] ?? '').toString();
|
||||||
|
final isOpen = m['isOpen'] == true && open.isNotEmpty;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(d.$2),
|
||||||
|
Text(isOpen ? '$open - $close' : 'تعطیل'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -1,25 +1,95 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show ValueGetter;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../cart/cart_state.dart';
|
import '../cart/cart_state.dart';
|
||||||
|
|
||||||
typedef DiscoverFilters = ({String? q, double? minRating, String sort});
|
/// Discovery filters. A class (not a record) so the many optional filters can be
|
||||||
|
/// changed one at a time via copyWith without re-listing every field.
|
||||||
|
class DiscoverFilters {
|
||||||
|
const DiscoverFilters({
|
||||||
|
this.q,
|
||||||
|
this.minRating,
|
||||||
|
this.sort = 'rating',
|
||||||
|
this.openNow = false,
|
||||||
|
this.priceTier,
|
||||||
|
this.themes = const [],
|
||||||
|
this.vibes = const [],
|
||||||
|
this.occasions = const [],
|
||||||
|
this.spaceFeatures = const [],
|
||||||
|
});
|
||||||
|
|
||||||
final discoverFiltersProvider = StateProvider<DiscoverFilters>(
|
final String? q;
|
||||||
(_) => (q: null, minRating: null, sort: 'rating'),
|
final double? minRating;
|
||||||
);
|
final String sort;
|
||||||
|
final bool openNow;
|
||||||
|
final String? priceTier;
|
||||||
|
final List<String> themes;
|
||||||
|
final List<String> vibes;
|
||||||
|
final List<String> occasions;
|
||||||
|
final List<String> spaceFeatures;
|
||||||
|
|
||||||
final discoverProvider = FutureProvider.autoDispose<List<Map<String, dynamic>>>((ref) {
|
int get activeCount =>
|
||||||
final filters = ref.watch(discoverFiltersProvider);
|
(minRating != null ? 1 : 0) +
|
||||||
|
(openNow ? 1 : 0) +
|
||||||
|
(priceTier != null ? 1 : 0) +
|
||||||
|
themes.length +
|
||||||
|
vibes.length +
|
||||||
|
occasions.length +
|
||||||
|
spaceFeatures.length;
|
||||||
|
|
||||||
|
DiscoverFilters copyWith({
|
||||||
|
ValueGetter<String?>? q,
|
||||||
|
ValueGetter<double?>? minRating,
|
||||||
|
String? sort,
|
||||||
|
bool? openNow,
|
||||||
|
ValueGetter<String?>? priceTier,
|
||||||
|
List<String>? themes,
|
||||||
|
List<String>? vibes,
|
||||||
|
List<String>? occasions,
|
||||||
|
List<String>? spaceFeatures,
|
||||||
|
}) {
|
||||||
|
return DiscoverFilters(
|
||||||
|
q: q != null ? q() : this.q,
|
||||||
|
minRating: minRating != null ? minRating() : this.minRating,
|
||||||
|
sort: sort ?? this.sort,
|
||||||
|
openNow: openNow ?? this.openNow,
|
||||||
|
priceTier: priceTier != null ? priceTier() : this.priceTier,
|
||||||
|
themes: themes ?? this.themes,
|
||||||
|
vibes: vibes ?? this.vibes,
|
||||||
|
occasions: occasions ?? this.occasions,
|
||||||
|
spaceFeatures: spaceFeatures ?? this.spaceFeatures,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final discoverFiltersProvider =
|
||||||
|
StateProvider<DiscoverFilters>((_) => const DiscoverFilters());
|
||||||
|
|
||||||
|
final discoverProvider =
|
||||||
|
FutureProvider.autoDispose<List<Map<String, dynamic>>>((ref) {
|
||||||
|
final f = ref.watch(discoverFiltersProvider);
|
||||||
return ref.watch(publicApiProvider).discover(
|
return ref.watch(publicApiProvider).discover(
|
||||||
city: 'تهران',
|
city: 'تهران',
|
||||||
q: filters.q,
|
q: f.q,
|
||||||
minRating: filters.minRating,
|
minRating: f.minRating,
|
||||||
sort: filters.sort,
|
sort: f.sort,
|
||||||
|
openNow: f.openNow,
|
||||||
|
priceTier: f.priceTier,
|
||||||
|
themes: f.themes,
|
||||||
|
vibes: f.vibes,
|
||||||
|
occasions: f.occasions,
|
||||||
|
spaceFeatures: f.spaceFeatures,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Available themes/vibes/occasions/spaceFeatures for the filter sheet.
|
||||||
|
final discoverTaxonomyProvider =
|
||||||
|
FutureProvider.autoDispose<Map<String, dynamic>?>((ref) {
|
||||||
|
return ref.watch(publicApiProvider).discoverTaxonomy();
|
||||||
|
});
|
||||||
|
|
||||||
class DiscoverScreen extends ConsumerStatefulWidget {
|
class DiscoverScreen extends ConsumerStatefulWidget {
|
||||||
const DiscoverScreen({super.key});
|
const DiscoverScreen({super.key});
|
||||||
|
|
||||||
@@ -39,7 +109,16 @@ class _DiscoverScreenState extends ConsumerState<DiscoverScreen> {
|
|||||||
void _applySearch() {
|
void _applySearch() {
|
||||||
final q = _searchController.text.trim();
|
final q = _searchController.text.trim();
|
||||||
ref.read(discoverFiltersProvider.notifier).update(
|
ref.read(discoverFiltersProvider.notifier).update(
|
||||||
(s) => (q: q.isEmpty ? null : q, minRating: s.minRating, sort: s.sort),
|
(s) => s.copyWith(q: () => q.isEmpty ? null : q),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openFilters() async {
|
||||||
|
await showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
showDragHandle: true,
|
||||||
|
builder: (_) => const _DiscoverFilterSheet(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,17 +149,27 @@ class _DiscoverScreenState extends ConsumerState<DiscoverScreen> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'جستجوی نام کافه...',
|
hintText: 'کافه دنج برای کار، نزدیک من...',
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
isDense: true,
|
isDense: true,
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: _applySearch,
|
onPressed: _applySearch,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
textInputAction: TextInputAction.search,
|
||||||
onSubmitted: (_) => _applySearch(),
|
onSubmitted: (_) => _applySearch(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Badge(
|
||||||
|
isLabelVisible: filters.activeCount > 0,
|
||||||
|
label: Text('${filters.activeCount}'),
|
||||||
|
child: IconButton.filledTonal(
|
||||||
|
icon: const Icon(Icons.tune),
|
||||||
|
onPressed: _openFilters,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -90,26 +179,29 @@ class _DiscoverScreenState extends ConsumerState<DiscoverScreen> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
FilterChip(
|
FilterChip(
|
||||||
label: const Text('همه'),
|
label: const Text('باز است'),
|
||||||
selected: filters.minRating == null,
|
selected: filters.openNow,
|
||||||
onSelected: (_) {
|
onSelected: (v) => ref
|
||||||
ref.read(discoverFiltersProvider.notifier).update(
|
.read(discoverFiltersProvider.notifier)
|
||||||
(s) => (q: s.q, minRating: null, sort: s.sort),
|
.update((s) => s.copyWith(openNow: v)),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
FilterChip(
|
||||||
|
label: const Text('همه امتیازها'),
|
||||||
|
selected: filters.minRating == null,
|
||||||
|
onSelected: (_) => ref
|
||||||
|
.read(discoverFiltersProvider.notifier)
|
||||||
|
.update((s) => s.copyWith(minRating: () => null)),
|
||||||
|
),
|
||||||
for (final min in [3.0, 4.0, 4.5])
|
for (final min in [3.0, 4.0, 4.5])
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 8),
|
padding: const EdgeInsets.only(right: 8),
|
||||||
child: FilterChip(
|
child: FilterChip(
|
||||||
label: Text('★ $min+'),
|
label: Text('★ $min+'),
|
||||||
selected: filters.minRating == min,
|
selected: filters.minRating == min,
|
||||||
onSelected: (_) {
|
onSelected: (_) => ref
|
||||||
ref.read(discoverFiltersProvider.notifier).update(
|
.read(discoverFiltersProvider.notifier)
|
||||||
(s) => (q: s.q, minRating: min, sort: s.sort),
|
.update((s) => s.copyWith(minRating: () => min)),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -121,7 +213,6 @@ class _DiscoverScreenState extends ConsumerState<DiscoverScreen> {
|
|||||||
value: filters.sort,
|
value: filters.sort,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'مرتبسازی',
|
labelText: 'مرتبسازی',
|
||||||
border: OutlineInputBorder(),
|
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
items: const [
|
items: const [
|
||||||
@@ -131,9 +222,9 @@ class _DiscoverScreenState extends ConsumerState<DiscoverScreen> {
|
|||||||
],
|
],
|
||||||
onChanged: (sort) {
|
onChanged: (sort) {
|
||||||
if (sort == null) return;
|
if (sort == null) return;
|
||||||
ref.read(discoverFiltersProvider.notifier).update(
|
ref
|
||||||
(s) => (q: s.q, minRating: s.minRating, sort: sort),
|
.read(discoverFiltersProvider.notifier)
|
||||||
);
|
.update((s) => s.copyWith(sort: sort));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -143,20 +234,56 @@ class _DiscoverScreenState extends ConsumerState<DiscoverScreen> {
|
|||||||
if (cafes.isEmpty) {
|
if (cafes.isEmpty) {
|
||||||
return const Center(child: Text('کافهای یافت نشد'));
|
return const Center(child: Text('کافهای یافت نشد'));
|
||||||
}
|
}
|
||||||
return ListView.separated(
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async => ref.refresh(discoverProvider.future),
|
||||||
|
child: ListView.separated(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: cafes.length,
|
itemCount: cafes.length,
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) =>
|
||||||
final cafe = cafes[index];
|
_CafeCard(cafe: cafes[index]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () =>
|
||||||
|
const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (e, _) => Center(child: Text('خطا: $e')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CafeCard extends StatelessWidget {
|
||||||
|
const _CafeCard({required this.cafe});
|
||||||
|
final Map<String, dynamic> cafe;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final slug = cafe['slug'] as String;
|
final slug = cafe['slug'] as String;
|
||||||
final name = cafe['name'] as String? ?? slug;
|
final name = cafe['name'] as String? ?? slug;
|
||||||
final avg = (cafe['averageRating'] as num?)?.toDouble() ?? 0;
|
final avg = (cafe['averageRating'] as num?)?.toDouble() ?? 0;
|
||||||
final count = cafe['reviewCount'] as int? ?? 0;
|
final count = cafe['reviewCount'] as int? ?? 0;
|
||||||
final address = cafe['address'] as String?;
|
final address = cafe['address'] as String?;
|
||||||
|
final isOpen = cafe['isOpenNow'] as bool?;
|
||||||
return Card(
|
return Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(name),
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(name)),
|
||||||
|
if (isOpen != null)
|
||||||
|
Text(
|
||||||
|
isOpen ? 'باز' : 'بسته',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: isOpen ? Colors.green : Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -169,16 +296,165 @@ class _DiscoverScreenState extends ConsumerState<DiscoverScreen> {
|
|||||||
onTap: () => context.push('/cafe/$slug'),
|
onTap: () => context.push('/cafe/$slug'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DiscoverFilterSheet extends ConsumerWidget {
|
||||||
|
const _DiscoverFilterSheet();
|
||||||
|
|
||||||
|
static const _priceTiers = [
|
||||||
|
('budget', 'اقتصادی'),
|
||||||
|
('moderate', 'متوسط'),
|
||||||
|
('upscale', 'لاکچری'),
|
||||||
|
('luxury', 'بسیار لاکچری'),
|
||||||
|
];
|
||||||
|
|
||||||
|
List<({String key, String label})> _parseTax(dynamic raw) {
|
||||||
|
if (raw is! List) return const [];
|
||||||
|
return raw
|
||||||
|
.map<({String key, String label})>((e) {
|
||||||
|
if (e is Map) {
|
||||||
|
final k = (e['key'] ?? e['value'] ?? e['id'] ?? '').toString();
|
||||||
|
final l = (e['labelFa'] ?? e['label'] ?? e['nameFa'] ?? e['name'] ?? k)
|
||||||
|
.toString();
|
||||||
|
return (key: k, label: l);
|
||||||
|
}
|
||||||
|
final s = e.toString();
|
||||||
|
return (key: s, label: s);
|
||||||
|
})
|
||||||
|
.where((t) => t.key.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final filters = ref.watch(discoverFiltersProvider);
|
||||||
|
final taxonomy = ref.watch(discoverTaxonomyProvider);
|
||||||
|
final notifier = ref.read(discoverFiltersProvider.notifier);
|
||||||
|
|
||||||
|
Widget chips(String title, List<({String key, String label})> items,
|
||||||
|
List<String> selected, void Function(List<String>) onChange) {
|
||||||
|
if (items.isEmpty) return const SizedBox.shrink();
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(0, 12, 0, 6),
|
||||||
|
child: Text(title, style: Theme.of(context).textTheme.titleSmall),
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
for (final it in items)
|
||||||
|
FilterChip(
|
||||||
|
label: Text(it.label),
|
||||||
|
selected: selected.contains(it.key),
|
||||||
|
onSelected: (v) {
|
||||||
|
final next = List<String>.from(selected);
|
||||||
|
if (v) {
|
||||||
|
next.add(it.key);
|
||||||
|
} else {
|
||||||
|
next.remove(it.key);
|
||||||
|
}
|
||||||
|
onChange(next);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Directionality(
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
16,
|
||||||
|
0,
|
||||||
|
16,
|
||||||
|
16 + MediaQuery.of(context).viewInsets.bottom,
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text('فیلترها',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Spacer(),
|
||||||
|
if (filters.activeCount > 0)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () =>
|
||||||
|
notifier.state = const DiscoverFilters(),
|
||||||
|
child: const Text('پاک کردن'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('فقط کافههای باز'),
|
||||||
|
value: filters.openNow,
|
||||||
|
onChanged: (v) => notifier.update((s) => s.copyWith(openNow: v)),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0, 8, 0, 6),
|
||||||
|
child: Text('محدوده قیمت'),
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
for (final p in _priceTiers)
|
||||||
|
ChoiceChip(
|
||||||
|
label: Text(p.$2),
|
||||||
|
selected: filters.priceTier == p.$1,
|
||||||
|
onSelected: (v) => notifier.update(
|
||||||
|
(s) => s.copyWith(priceTier: () => v ? p.$1 : null),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
taxonomy.when(
|
||||||
|
data: (tax) {
|
||||||
|
if (tax == null) return const SizedBox.shrink();
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
chips('فضا و حالوهوا', _parseTax(tax['themes']),
|
||||||
|
filters.themes,
|
||||||
|
(v) => notifier.update((s) => s.copyWith(themes: v))),
|
||||||
|
chips('وایب', _parseTax(tax['vibes']), filters.vibes,
|
||||||
|
(v) => notifier.update((s) => s.copyWith(vibes: v))),
|
||||||
|
chips('مناسبت', _parseTax(tax['occasions']),
|
||||||
|
filters.occasions,
|
||||||
|
(v) => notifier.update((s) => s.copyWith(occasions: v))),
|
||||||
|
chips('امکانات', _parseTax(tax['spaceFeatures']),
|
||||||
|
filters.spaceFeatures,
|
||||||
|
(v) => notifier.update(
|
||||||
|
(s) => s.copyWith(spaceFeatures: v))),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Padding(
|
||||||
error: (e, _) => Center(child: Text('خطا: $e')),
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
error: (_, __) => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('نمایش نتایج'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user