Skip to content

Commit 12a363e

Browse files
authored
Merge pull request #21 from LEO0331/lc/dev/newfeatures
Lc/dev/newfeatures
2 parents b63cf93 + a761375 commit 12a363e

File tree

11 files changed

+990
-339
lines changed

11 files changed

+990
-339
lines changed

lib/core/i18n/app_strings.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ class AppStrings {
7878
String get showPickupCodeHelp => _zh
7979
? '請在取餐時向企業出示這組 4 位數代碼。'
8080
: 'Show this 4-digit code to the enterprise at pickup.';
81+
String get frequentEnterprise => _zh ? '常態捐贈企業' : 'Frequent enterprise';
82+
String get privacyFaqTitle => _zh ? '隱私與常見問題' : 'Privacy & FAQ';
83+
String get privacyNotice => _zh
84+
? '隱私提醒:平台僅顯示必要媒合資訊,不公開個人聯絡方式。'
85+
: 'Privacy note: Boxmatch only shows minimum matching info and does not expose personal contacts.';
86+
String get faqNotice => _zh
87+
? 'FAQ:若遇到臨時改地點或可疑行為,請在預約頁按「回報風險事件」。'
88+
: 'FAQ: if pickup location is changed privately or suspicious behavior occurs, use "Report safety concern".';
8189

8290
String get retry => _zh ? '重試' : 'Retry';
8391
String get genericLoadErrorTitle => _zh ? '讀取失敗' : 'Unable to load';

lib/features/surplus/presentation/browse/listings_page.dart

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ class _ListingsPageState extends State<ListingsPage> {
7171
);
7272
}
7373

74-
Widget _buildFilterCard(BuildContext context, AppStrings s) {
74+
Widget _buildFilterCard(
75+
BuildContext context,
76+
AppStrings s, {
77+
required int favoriteCount,
78+
}) {
7579
final isZh = AppScope.of(context).localeController.isZhTw;
7680
return Container(
7781
margin: const EdgeInsets.fromLTRB(12, 8, 12, 8),
@@ -134,6 +138,29 @@ class _ListingsPageState extends State<ListingsPage> {
134138
),
135139
label: Text(isZh ? '僅收藏場館' : 'Favorites only'),
136140
),
141+
if (_favoritesOnly)
142+
ActionChip(
143+
avatar: const Icon(Icons.filter_alt_off_outlined, size: 16),
144+
label: Text(isZh ? '清除篩選' : 'Clear filter'),
145+
onPressed: () => _setFavoritesOnly(false),
146+
),
147+
],
148+
),
149+
const SizedBox(height: 8),
150+
Row(
151+
children: [
152+
Text(
153+
isZh
154+
? '收藏場館:$favoriteCount'
155+
: 'Favorite venues: $favoriteCount',
156+
style: Theme.of(context).textTheme.bodySmall,
157+
),
158+
const Spacer(),
159+
TextButton.icon(
160+
onPressed: () => context.go('/map'),
161+
icon: const Icon(Icons.map_outlined, size: 16),
162+
label: Text(isZh ? '地圖' : 'Map'),
163+
),
137164
],
138165
),
139166
],
@@ -232,7 +259,11 @@ class _ListingsPageState extends State<ListingsPage> {
232259
return Column(
233260
children: [
234261
if (!dependencies.usingFirebase) _buildModeNotice(s),
235-
_buildFilterCard(context, s),
262+
_buildFilterCard(
263+
context,
264+
s,
265+
favoriteCount: favoriteVenueIds.length,
266+
),
236267
Expanded(
237268
child: ListView(
238269
padding: const EdgeInsets.all(24),
@@ -244,6 +275,16 @@ class _ListingsPageState extends State<ListingsPage> {
244275
s.noActiveListings,
245276
textAlign: TextAlign.center,
246277
),
278+
const SizedBox(height: 12),
279+
FilledButton.tonalIcon(
280+
onPressed: () => context.go('/map'),
281+
icon: const Icon(Icons.map_outlined),
282+
label: Text(
283+
AppScope.of(context).localeController.isZhTw
284+
? '去場館地圖看看'
285+
: 'Open venue map',
286+
),
287+
),
247288
],
248289
),
249290
),
@@ -254,7 +295,11 @@ class _ListingsPageState extends State<ListingsPage> {
254295
return Column(
255296
children: [
256297
if (!dependencies.usingFirebase) _buildModeNotice(s),
257-
_buildFilterCard(context, s),
298+
_buildFilterCard(
299+
context,
300+
s,
301+
favoriteCount: favoriteVenueIds.length,
302+
),
258303
Expanded(
259304
child: ListView.builder(
260305
itemCount: listings.length,

lib/features/surplus/presentation/browse/my_reservations_page.dart

Lines changed: 150 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,19 @@ class _MyReservationsPageState extends State<MyReservationsPage> {
9090
}
9191
}
9292

93+
Color _statusColor(ReservationStatus status) {
94+
switch (status) {
95+
case ReservationStatus.reserved:
96+
return const Color(0xFF2D6A4F);
97+
case ReservationStatus.completed:
98+
return const Color(0xFF1D8348);
99+
case ReservationStatus.expired:
100+
return const Color(0xFF8E7D63);
101+
case ReservationStatus.cancelled:
102+
return const Color(0xFF9E9E9E);
103+
}
104+
}
105+
93106
@override
94107
Widget build(BuildContext context) {
95108
final s = AppStrings.of(context);
@@ -127,43 +140,148 @@ class _MyReservationsPageState extends State<MyReservationsPage> {
127140

128141
final items = snapshot.data ?? const <_ReservationWithListing>[];
129142
if (items.isEmpty) {
130-
return Center(child: Text(s.noMyReservations));
143+
return Center(
144+
child: Padding(
145+
padding: const EdgeInsets.all(24),
146+
child: Column(
147+
mainAxisSize: MainAxisSize.min,
148+
children: [
149+
const Icon(Icons.receipt_long_outlined, size: 44),
150+
const SizedBox(height: 10),
151+
Text(s.noMyReservations),
152+
const SizedBox(height: 12),
153+
FilledButton.tonalIcon(
154+
onPressed: () => context.go('/'),
155+
icon: const Icon(Icons.search_outlined),
156+
label: Text(
157+
AppScope.of(context).localeController.isZhTw
158+
? '去找可領取餐點'
159+
: 'Browse listings',
160+
),
161+
),
162+
],
163+
),
164+
),
165+
);
131166
}
132167

133-
return ListView.builder(
168+
final aliasFrequency = <String, int>{};
169+
for (final item in items) {
170+
final alias = item.listing?.displayNameOptional?.trim() ?? '';
171+
if (alias.isEmpty) {
172+
continue;
173+
}
174+
final normalized = alias.toLowerCase();
175+
aliasFrequency[normalized] = (aliasFrequency[normalized] ?? 0) + 1;
176+
}
177+
178+
return ListView(
134179
padding: const EdgeInsets.all(12),
135-
itemCount: items.length,
136-
itemBuilder: (context, index) {
137-
final item = items[index];
138-
final reservation = item.reservation;
139-
final listing = item.listing;
140-
final statusLabel = s.statusLabel(switch (reservation.status) {
141-
ReservationStatus.reserved => AppStatusLabel.reserved,
142-
ReservationStatus.completed => AppStatusLabel.completed,
143-
ReservationStatus.expired => AppStatusLabel.expired,
144-
ReservationStatus.cancelled => AppStatusLabel.cancelled,
145-
});
146-
return Card(
147-
child: ListTile(
148-
title: Text('${listing?.itemType ?? 'Item'} · $statusLabel'),
149-
subtitle: Text(
150-
'Code: ${reservation.pickupCode}\n'
151-
'Pickup: ${listing?.pickupPointText ?? '-'}\n'
152-
'Expires: ${formatDateTime(reservation.expiresAt)}',
153-
),
154-
isThreeLine: true,
155-
trailing: reservation.status == ReservationStatus.reserved
156-
? OutlinedButton(
157-
onPressed: () => _cancelReservation(reservation),
158-
child: Text(s.cancelReservation),
159-
)
160-
: null,
161-
onTap: () => context.go(
162-
'/listing/${reservation.listingId}/reservation/${reservation.id}',
180+
children: [
181+
Card(
182+
color: Theme.of(context).colorScheme.surfaceContainerLow,
183+
child: Padding(
184+
padding: const EdgeInsets.all(12),
185+
child: Column(
186+
crossAxisAlignment: CrossAxisAlignment.start,
187+
children: [
188+
Text(
189+
s.privacyFaqTitle,
190+
style: Theme.of(context).textTheme.titleSmall,
191+
),
192+
const SizedBox(height: 4),
193+
Text(
194+
s.privacyNotice,
195+
style: Theme.of(context).textTheme.bodySmall,
196+
),
197+
const SizedBox(height: 4),
198+
Text(
199+
s.faqNotice,
200+
style: Theme.of(context).textTheme.bodySmall,
201+
),
202+
],
163203
),
164204
),
165-
);
166-
},
205+
),
206+
const SizedBox(height: 8),
207+
...items.map((item) {
208+
final reservation = item.reservation;
209+
final listing = item.listing;
210+
final alias = listing?.displayNameOptional?.trim() ?? '';
211+
final isFrequentEnterprise =
212+
alias.isNotEmpty &&
213+
(aliasFrequency[alias.toLowerCase()] ?? 0) >= 2;
214+
final statusLabel = s.statusLabel(switch (reservation.status) {
215+
ReservationStatus.reserved => AppStatusLabel.reserved,
216+
ReservationStatus.completed => AppStatusLabel.completed,
217+
ReservationStatus.expired => AppStatusLabel.expired,
218+
ReservationStatus.cancelled => AppStatusLabel.cancelled,
219+
});
220+
return Card(
221+
child: ListTile(
222+
title: Wrap(
223+
spacing: 8,
224+
runSpacing: 6,
225+
crossAxisAlignment: WrapCrossAlignment.center,
226+
children: [
227+
Text(listing?.itemType ?? 'Item'),
228+
Container(
229+
padding: const EdgeInsets.symmetric(
230+
horizontal: 8,
231+
vertical: 2,
232+
),
233+
decoration: BoxDecoration(
234+
color: _statusColor(
235+
reservation.status,
236+
).withValues(alpha: 0.12),
237+
borderRadius: BorderRadius.circular(999),
238+
),
239+
child: Text(
240+
statusLabel,
241+
style: TextStyle(
242+
color: _statusColor(reservation.status),
243+
fontWeight: FontWeight.w600,
244+
),
245+
),
246+
),
247+
],
248+
),
249+
subtitle: Column(
250+
crossAxisAlignment: CrossAxisAlignment.start,
251+
mainAxisSize: MainAxisSize.min,
252+
children: [
253+
Text(
254+
'Code: ${reservation.pickupCode}\n'
255+
'Pickup: ${listing?.pickupPointText ?? '-'}\n'
256+
'Expires: ${formatDateTime(reservation.expiresAt)}',
257+
),
258+
if (isFrequentEnterprise) ...[
259+
const SizedBox(height: 6),
260+
Chip(
261+
visualDensity: VisualDensity.compact,
262+
avatar: const Icon(
263+
Icons.verified,
264+
size: 16,
265+
color: Color(0xFF2D6A4F),
266+
),
267+
label: Text(s.frequentEnterprise),
268+
),
269+
],
270+
],
271+
),
272+
trailing: reservation.status == ReservationStatus.reserved
273+
? OutlinedButton(
274+
onPressed: () => _cancelReservation(reservation),
275+
child: Text(s.cancelReservation),
276+
)
277+
: null,
278+
onTap: () => context.go(
279+
'/listing/${reservation.listingId}/reservation/${reservation.id}',
280+
),
281+
),
282+
);
283+
}),
284+
],
167285
);
168286
},
169287
),

0 commit comments

Comments
 (0)