feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
// lib/features/booking/providers/checkout_provider.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../../core/api/api_endpoints.dart';
|
||||
import '../../../core/utils/error_utils.dart';
|
||||
import '../models/booking_models.dart';
|
||||
import '../services/booking_service.dart';
|
||||
@@ -24,8 +28,11 @@ class CheckoutProvider extends ChangeNotifier {
|
||||
// Shipping
|
||||
ShippingDetails? shippingDetails;
|
||||
|
||||
// Coupon
|
||||
// Coupon / promo
|
||||
String? couponCode;
|
||||
double discountAmount = 0.0;
|
||||
String? promoMessage;
|
||||
bool promoApplied = false;
|
||||
|
||||
// Status
|
||||
bool loading = false;
|
||||
@@ -40,6 +47,9 @@ class CheckoutProvider extends ChangeNotifier {
|
||||
cart = [];
|
||||
shippingDetails = null;
|
||||
couponCode = null;
|
||||
discountAmount = 0.0;
|
||||
promoMessage = null;
|
||||
promoApplied = false;
|
||||
paymentId = null;
|
||||
error = null;
|
||||
loading = true;
|
||||
@@ -65,7 +75,7 @@ class CheckoutProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
double get subtotal => cart.fold(0, (sum, item) => sum + item.subtotal);
|
||||
double get total => subtotal; // expand with discount/tax later
|
||||
double get total => subtotal - discountAmount;
|
||||
|
||||
bool get hasItems => cart.isNotEmpty;
|
||||
|
||||
@@ -95,6 +105,62 @@ class CheckoutProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Apply a promo code against the backend.
|
||||
Future<bool> applyPromo(String code) async {
|
||||
if (code.trim().isEmpty) return false;
|
||||
loading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final token = prefs.getString('access_token') ?? '';
|
||||
final response = await http.post(
|
||||
Uri.parse('${ApiEndpoints.baseUrl}/bookings/apply-promo/'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
body: jsonEncode({'code': code.trim(), 'event_id': eventId}),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
if (data['valid'] == true) {
|
||||
discountAmount = (data['discount_amount'] as num?)?.toDouble() ?? 0.0;
|
||||
couponCode = code.trim();
|
||||
promoMessage = data['message'] as String? ?? 'Promo applied!';
|
||||
promoApplied = true;
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
promoMessage = data['message'] as String? ?? 'Invalid promo code';
|
||||
promoApplied = false;
|
||||
discountAmount = 0.0;
|
||||
couponCode = null;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
promoMessage = 'Could not apply promo code';
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
promoMessage = 'Could not apply promo code';
|
||||
return false;
|
||||
} finally {
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove applied promo code.
|
||||
void resetPromo() {
|
||||
discountAmount = 0.0;
|
||||
couponCode = null;
|
||||
promoMessage = null;
|
||||
promoApplied = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Process checkout on backend.
|
||||
Future<Map<String, dynamic>> processCheckout() async {
|
||||
loading = true;
|
||||
@@ -139,6 +205,9 @@ class CheckoutProvider extends ChangeNotifier {
|
||||
cart = [];
|
||||
shippingDetails = null;
|
||||
couponCode = null;
|
||||
discountAmount = 0.0;
|
||||
promoMessage = null;
|
||||
promoApplied = false;
|
||||
paymentId = null;
|
||||
error = null;
|
||||
loading = false;
|
||||
|
||||
@@ -72,6 +72,11 @@ class EventModel {
|
||||
final double? averageRating;
|
||||
final int? reviewCount;
|
||||
|
||||
// Contributor fields (EVT-001)
|
||||
final String? contributorId;
|
||||
final String? contributorName;
|
||||
final String? contributorTier;
|
||||
|
||||
EventModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
@@ -97,6 +102,9 @@ class EventModel {
|
||||
this.importantInfo = const [],
|
||||
this.averageRating,
|
||||
this.reviewCount,
|
||||
this.contributorId,
|
||||
this.contributorName,
|
||||
this.contributorTier,
|
||||
});
|
||||
|
||||
/// Safely parse a double from backend (may arrive as String or num)
|
||||
@@ -156,6 +164,9 @@ class EventModel {
|
||||
importantInfo: _parseImportantInfo(j['important_info']),
|
||||
averageRating: (j['average_rating'] as num?)?.toDouble(),
|
||||
reviewCount: (j['review_count'] as num?)?.toInt(),
|
||||
contributorId: j['contributor_id']?.toString(),
|
||||
contributorName: j['contributor_name'] as String?,
|
||||
contributorTier: j['contributor_tier'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,28 @@ class EventsService {
|
||||
return EventModel.fromJson(Map<String, dynamic>.from(res));
|
||||
}
|
||||
|
||||
/// Related events by event_type_id (EVT-002).
|
||||
/// Fetches events with the same category, silently returns [] on failure.
|
||||
Future<List<EventModel>> getEventsByCategory(int eventTypeId, {int limit = 5}) async {
|
||||
try {
|
||||
final res = await _api.post(
|
||||
ApiEndpoints.eventsByCategory,
|
||||
body: {'event_type_id': eventTypeId, 'page_size': limit, 'page': 1},
|
||||
requiresAuth: false,
|
||||
);
|
||||
final results = res['results'] ?? res['events'] ?? res['data'] ?? [];
|
||||
if (results is List) {
|
||||
return results
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map((e) => EventModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
} catch (_) {
|
||||
// silently fail — related events are non-critical
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Events by month and year for calendar (POST to /events/events-by-month-year/)
|
||||
Future<Map<String, dynamic>> getEventsByMonthYear(String month, int year) async {
|
||||
final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false);
|
||||
|
||||
@@ -45,6 +45,33 @@ class GamificationService {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public contributor profile (any user by userId / email)
|
||||
// GET /v1/gamification/dashboard?user_id={userId}
|
||||
// ---------------------------------------------------------------------------
|
||||
Future<DashboardResponse> getDashboardForUser(String userId) async {
|
||||
final url = '${ApiEndpoints.gamificationDashboard}?user_id=$userId';
|
||||
final res = await _api.post(url, requiresAuth: false);
|
||||
|
||||
final profileJson = res['profile'] as Map<String, dynamic>? ?? {};
|
||||
final rawSubs = res['submissions'] as List? ?? [];
|
||||
final rawAchievements = res['achievements'] as List? ?? [];
|
||||
|
||||
final submissions = rawSubs
|
||||
.map((s) => SubmissionModel.fromJson(Map<String, dynamic>.from(s as Map)))
|
||||
.toList();
|
||||
|
||||
final achievements = rawAchievements
|
||||
.map((a) => AchievementBadge.fromJson(Map<String, dynamic>.from(a as Map)))
|
||||
.toList();
|
||||
|
||||
return DashboardResponse(
|
||||
profile: UserGamificationProfile.fromJson(profileJson),
|
||||
submissions: submissions,
|
||||
achievements: achievements,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convenience — returns just the profile (backward-compatible with provider).
|
||||
Future<UserGamificationProfile> getProfile() async {
|
||||
final dashboard = await getDashboard();
|
||||
@@ -152,11 +179,16 @@ class GamificationService {
|
||||
}
|
||||
|
||||
static const _defaultBadges = [
|
||||
AchievementBadge(id: 'badge-01', title: 'First Submission', description: 'Submitted your first event.', iconName: 'edit', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-02', title: 'Silver Streak', description: 'Reached Silver tier.', iconName: 'star', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-03', title: 'Gold Rush', description: 'Reach Gold tier (500 EP).', iconName: 'emoji_events', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-04', title: 'Top 10', description: 'Appear in the district leaderboard top 10.', iconName: 'leaderboard', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-05', title: 'Image Pro', description: 'Submit 10 events with 3+ images.', iconName: 'photo_library', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-06', title: 'Pioneer', description: 'One of the first 100 contributors.', iconName: 'verified', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-01', title: 'First Step', description: 'Submit your first event.', iconName: 'edit', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-02', title: 'Silver Streak', description: 'Submit 5 events.', iconName: 'trending_up', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-03', title: 'Gold Rush', description: 'Submit 15 events.', iconName: 'emoji_events', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-04', title: 'Top 10', description: 'Reach top 10 on leaderboard.', iconName: 'leaderboard', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-05', title: 'Image Pro', description: 'Upload 10 event images.', iconName: 'photo_library', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-06', title: 'Pioneer', description: 'One of the first contributors.', iconName: 'rocket_launch', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-07', title: 'Community Star', description: 'Get 10 events approved.', iconName: 'verified', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-08', title: 'Event Hunter', description: 'Submit 25 events.', iconName: 'event_hunter', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-09', title: 'District Champion', description: 'Rank #1 in your district.', iconName: 'location_on', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-10', title: 'Platinum Achiever', description: 'Earn 1500 EP.', iconName: 'diamond', isUnlocked: false, progress: 0.0),
|
||||
AchievementBadge(id: 'badge-11', title: 'Diamond Legend', description: 'Earn 5000 EP.', iconName: 'workspace_premium', isUnlocked: false, progress: 0.0),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,97 @@
|
||||
// lib/features/reviews/widgets/review_summary.dart
|
||||
import 'dart:math' show pi;
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/review_models.dart';
|
||||
import 'star_display.dart';
|
||||
|
||||
class _RatingRingPainter extends CustomPainter {
|
||||
final double rating;
|
||||
|
||||
const _RatingRingPainter({required this.rating});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final radius = size.width / 2 - 6;
|
||||
|
||||
// Background track
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
2 * pi,
|
||||
false,
|
||||
Paint()
|
||||
..color = Colors.white12
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 7
|
||||
..strokeCap = StrokeCap.round,
|
||||
);
|
||||
|
||||
// Filled arc
|
||||
if (rating > 0) {
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
(rating.clamp(0.0, 5.0) / 5.0) * 2 * pi,
|
||||
false,
|
||||
Paint()
|
||||
..color = const Color(0xFFFBBF24)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 7
|
||||
..strokeCap = StrokeCap.round,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_RatingRingPainter old) => old.rating != rating;
|
||||
}
|
||||
|
||||
class _RatingRingWidget extends StatelessWidget {
|
||||
final double rating;
|
||||
final int reviewCount;
|
||||
|
||||
const _RatingRingWidget({required this.rating, required this.reviewCount});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 84,
|
||||
height: 84,
|
||||
child: CustomPaint(
|
||||
painter: _RatingRingPainter(rating: rating),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
rating.toStringAsFixed(1),
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'/5',
|
||||
style: TextStyle(fontSize: 10, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$reviewCount ${reviewCount == 1 ? 'review' : 'reviews'}',
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReviewSummary extends StatelessWidget {
|
||||
final ReviewStatsModel stats;
|
||||
@@ -24,27 +114,10 @@ class ReviewSummary extends StatelessWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Left: average rating + stars + count
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
stats.averageRating.toStringAsFixed(1),
|
||||
style: const TextStyle(
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1E293B),
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
StarDisplay(rating: stats.averageRating, size: 18),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${stats.reviewCount} review${stats.reviewCount == 1 ? '' : 's'}',
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF64748B)),
|
||||
),
|
||||
],
|
||||
// Left: circular rating ring
|
||||
_RatingRingWidget(
|
||||
rating: stats.averageRating,
|
||||
reviewCount: stats.reviewCount,
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
// Right: distribution bars
|
||||
|
||||
197
lib/features/share/share_rank_card.dart
Normal file
197
lib/features/share/share_rank_card.dart
Normal file
@@ -0,0 +1,197 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import '../../widgets/tier_avatar_ring.dart';
|
||||
|
||||
class ShareRankCard extends StatefulWidget {
|
||||
final String username;
|
||||
final String tier;
|
||||
final int rank;
|
||||
final int ep;
|
||||
final int rewardPoints;
|
||||
|
||||
const ShareRankCard({
|
||||
super.key,
|
||||
required this.username,
|
||||
required this.tier,
|
||||
required this.rank,
|
||||
required this.ep,
|
||||
this.rewardPoints = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ShareRankCard> createState() => _ShareRankCardState();
|
||||
}
|
||||
|
||||
class _ShareRankCardState extends State<ShareRankCard> {
|
||||
final GlobalKey _boundaryKey = GlobalKey();
|
||||
bool _sharing = false;
|
||||
|
||||
static const _tierGradients = {
|
||||
'Bronze': [Color(0xFF92400E), Color(0xFFD97706)],
|
||||
'Silver': [Color(0xFF475569), Color(0xFF94A3B8)],
|
||||
'Gold': [Color(0xFF92400E), Color(0xFFFBBF24)],
|
||||
'Platinum': [Color(0xFF4C1D95), Color(0xFF8B5CF6)],
|
||||
'Diamond': [Color(0xFF1E3A8A), Color(0xFF60A5FA)],
|
||||
};
|
||||
|
||||
List<Color> get _gradient {
|
||||
return _tierGradients[widget.tier] ?? [const Color(0xFF0F172A), const Color(0xFF1E293B)];
|
||||
}
|
||||
|
||||
Future<void> _share() async {
|
||||
if (_sharing) return;
|
||||
setState(() => _sharing = true);
|
||||
try {
|
||||
final boundary = _boundaryKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
|
||||
final image = await boundary.toImage(pixelRatio: 3.0);
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
if (byteData == null) return;
|
||||
final bytes = byteData.buffer.asUint8List();
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final file = File('${tempDir.path}/eventify_rank_${widget.username}.png');
|
||||
await file.writeAsBytes(bytes);
|
||||
|
||||
await Share.shareXFiles(
|
||||
[XFile(file.path)],
|
||||
text: 'I\'m ranked #${widget.rank} on Eventify with ${widget.ep} EP! 🏆 #Eventify #Kerala',
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Could not share rank card')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _sharing = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RepaintBoundary(
|
||||
key: _boundaryKey,
|
||||
child: Container(
|
||||
width: 320,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF0F172A), Color(0xFF1E293B)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Tier gradient header bar
|
||||
Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(colors: _gradient),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Avatar
|
||||
TierAvatarRing(username: widget.username, tier: widget.tier, size: 80),
|
||||
const SizedBox(height: 12),
|
||||
// Username
|
||||
Text(
|
||||
widget.username,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Tier badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(colors: _gradient),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
widget.tier.isEmpty ? 'Contributor' : widget.tier,
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Stats row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_stat('Rank', '#${widget.rank}'),
|
||||
Container(width: 1, height: 40, color: Colors.white12),
|
||||
_stat('EP', '${widget.ep}'),
|
||||
Container(width: 1, height: 40, color: Colors.white12),
|
||||
_stat('RP', '${widget.rewardPoints}'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Branding
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Icon(Icons.bolt, size: 14, color: Color(0xFF3B82F6)),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'EVENTIFY',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFF3B82F6),
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _sharing ? null : _share,
|
||||
icon: _sharing
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||
)
|
||||
: const Icon(Icons.share, size: 18),
|
||||
label: Text(_sharing ? 'Sharing...' : 'Share Rank Card'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF1D4ED8),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _stat(String label, String value) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user