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>
198 lines
6.7 KiB
Dart
198 lines
6.7 KiB
Dart
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)),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|