From e9752c3d618831d0bb396f3a15451d7b73656d41 Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Sat, 4 Apr 2026 17:17:36 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=203=20=E2=80=94=2026=20medium-pri?= =?UTF-8?q?ority=20gaps=20implemented?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- assets/data/kerala_pincodes.json | 465 ++++++++++++++++++ .../booking/providers/checkout_provider.dart | 73 ++- lib/features/events/models/event_models.dart | 11 + .../events/services/events_service.dart | 22 + .../services/gamification_service.dart | 44 +- .../reviews/widgets/review_summary.dart | 117 ++++- lib/features/share/share_rank_card.dart | 197 ++++++++ lib/screens/checkout_screen.dart | 108 ++++ lib/screens/contribute_screen.dart | 229 ++++++--- lib/screens/contributor_profile_screen.dart | 271 ++++++++++ lib/screens/home_screen.dart | 16 +- lib/screens/learn_more_screen.dart | 250 ++++++++++ lib/screens/profile_screen.dart | 385 +++++++++++++-- lib/screens/search_screen.dart | 71 ++- lib/widgets/eventify_bottom_sheet.dart | 96 ++++ lib/widgets/glass_card.dart | 53 ++ lib/widgets/tier_avatar_ring.dart | 117 +++++ pubspec.lock | 2 +- pubspec.yaml | 2 + 19 files changed, 2346 insertions(+), 183 deletions(-) create mode 100644 assets/data/kerala_pincodes.json create mode 100644 lib/features/share/share_rank_card.dart create mode 100644 lib/screens/contributor_profile_screen.dart create mode 100644 lib/widgets/eventify_bottom_sheet.dart create mode 100644 lib/widgets/glass_card.dart create mode 100644 lib/widgets/tier_avatar_ring.dart diff --git a/assets/data/kerala_pincodes.json b/assets/data/kerala_pincodes.json new file mode 100644 index 0000000..d0d0599 --- /dev/null +++ b/assets/data/kerala_pincodes.json @@ -0,0 +1,465 @@ +[ + {"city": "Thiruvananthapuram", "district": "Thiruvananthapuram", "pincode": "695001"}, + {"city": "Kazhakoottam", "district": "Thiruvananthapuram", "pincode": "695582"}, + {"city": "Technopark", "district": "Thiruvananthapuram", "pincode": "695581"}, + {"city": "Varkala", "district": "Thiruvananthapuram", "pincode": "695141"}, + {"city": "Attingal", "district": "Thiruvananthapuram", "pincode": "695101"}, + {"city": "Neyyattinkara", "district": "Thiruvananthapuram", "pincode": "695121"}, + {"city": "Nedumangad", "district": "Thiruvananthapuram", "pincode": "695541"}, + {"city": "Pothencode", "district": "Thiruvananthapuram", "pincode": "695584"}, + {"city": "Peroorkada", "district": "Thiruvananthapuram", "pincode": "695005"}, + {"city": "Kazhakkoottam", "district": "Thiruvananthapuram", "pincode": "695582"}, + {"city": "Nemom", "district": "Thiruvananthapuram", "pincode": "695020"}, + {"city": "Kaniyapuram", "district": "Thiruvananthapuram", "pincode": "695301"}, + {"city": "Balaramapuram", "district": "Thiruvananthapuram", "pincode": "695501"}, + {"city": "Kallambalam", "district": "Thiruvananthapuram", "pincode": "695605"}, + {"city": "Chirayinkeezhu", "district": "Thiruvananthapuram", "pincode": "695304"}, + {"city": "Venjaramoodu", "district": "Thiruvananthapuram", "pincode": "695607"}, + {"city": "Kattakada", "district": "Thiruvananthapuram", "pincode": "695572"}, + {"city": "Aryanad", "district": "Thiruvananthapuram", "pincode": "695542"}, + {"city": "Vellanad", "district": "Thiruvananthapuram", "pincode": "695543"}, + {"city": "Parassala", "district": "Thiruvananthapuram", "pincode": "695502"}, + {"city": "Kollam", "district": "Kollam", "pincode": "691001"}, + {"city": "Karunagappally", "district": "Kollam", "pincode": "690518"}, + {"city": "Kundara", "district": "Kollam", "pincode": "691501"}, + {"city": "Punalur", "district": "Kollam", "pincode": "691305"}, + {"city": "Kottarakkara", "district": "Kollam", "pincode": "691506"}, + {"city": "Paravur", "district": "Kollam", "pincode": "691301"}, + {"city": "Chavara", "district": "Kollam", "pincode": "691583"}, + {"city": "Pathanapuram", "district": "Kollam", "pincode": "689695"}, + {"city": "Ezhukone", "district": "Kollam", "pincode": "691505"}, + {"city": "Anchal", "district": "Kollam", "pincode": "691306"}, + {"city": "Kottamkara", "district": "Kollam", "pincode": "691007"}, + {"city": "Perinad", "district": "Kollam", "pincode": "691601"}, + {"city": "Kulathupuzha", "district": "Kollam", "pincode": "691310"}, + {"city": "Ittiva", "district": "Kollam", "pincode": "691311"}, + {"city": "Mynagappally", "district": "Kollam", "pincode": "690503"}, + {"city": "Pathanamthitta", "district": "Pathanamthitta", "pincode": "689645"}, + {"city": "Adoor", "district": "Pathanamthitta", "pincode": "691523"}, + {"city": "Thiruvalla", "district": "Pathanamthitta", "pincode": "689101"}, + {"city": "Pandalam", "district": "Pathanamthitta", "pincode": "689501"}, + {"city": "Ranni", "district": "Pathanamthitta", "pincode": "689673"}, + {"city": "Kozhencherry", "district": "Pathanamthitta", "pincode": "689641"}, + {"city": "Konni", "district": "Pathanamthitta", "pincode": "689691"}, + {"city": "Mallapally", "district": "Pathanamthitta", "pincode": "686501"}, + {"city": "Tiruvalla", "district": "Pathanamthitta", "pincode": "689101"}, + {"city": "Aranmula", "district": "Pathanamthitta", "pincode": "689533"}, + {"city": "Angamaly", "district": "Pathanamthitta", "pincode": "686540"}, + {"city": "Alappuzha", "district": "Alappuzha", "pincode": "688001"}, + {"city": "Cherthala", "district": "Alappuzha", "pincode": "688524"}, + {"city": "Kayamkulam", "district": "Alappuzha", "pincode": "690502"}, + {"city": "Mavelikkara", "district": "Alappuzha", "pincode": "690101"}, + {"city": "Haripad", "district": "Alappuzha", "pincode": "690514"}, + {"city": "Mararikulam", "district": "Alappuzha", "pincode": "688523"}, + {"city": "Ambalapuzha", "district": "Alappuzha", "pincode": "688561"}, + {"city": "Kuttanad", "district": "Alappuzha", "pincode": "688501"}, + {"city": "Mannar", "district": "Alappuzha", "pincode": "689622"}, + {"city": "Chengannur", "district": "Alappuzha", "pincode": "689121"}, + {"city": "Purakkad", "district": "Alappuzha", "pincode": "688551"}, + {"city": "Aroor", "district": "Alappuzha", "pincode": "688534"}, + {"city": "Kottayam", "district": "Kottayam", "pincode": "686001"}, + {"city": "Changanacherry", "district": "Kottayam", "pincode": "686101"}, + {"city": "Pala", "district": "Kottayam", "pincode": "686575"}, + {"city": "Ettumanoor", "district": "Kottayam", "pincode": "686631"}, + {"city": "Vaikom", "district": "Kottayam", "pincode": "686141"}, + {"city": "Kanjirappally", "district": "Kottayam", "pincode": "686507"}, + {"city": "Erattupetta", "district": "Kottayam", "pincode": "686121"}, + {"city": "Ponkunnam", "district": "Kottayam", "pincode": "686506"}, + {"city": "Karukachal", "district": "Kottayam", "pincode": "686540"}, + {"city": "Kaduthuruthy", "district": "Kottayam", "pincode": "686604"}, + {"city": "Meenachil", "district": "Kottayam", "pincode": "686516"}, + {"city": "Thodupuzha", "district": "Idukki", "pincode": "685584"}, + {"city": "Idukki", "district": "Idukki", "pincode": "685602"}, + {"city": "Munnar", "district": "Idukki", "pincode": "685612"}, + {"city": "Kattappana", "district": "Idukki", "pincode": "685508"}, + {"city": "Adimali", "district": "Idukki", "pincode": "685561"}, + {"city": "Kumily", "district": "Idukki", "pincode": "685509"}, + {"city": "Devikulam", "district": "Idukki", "pincode": "685613"}, + {"city": "Peermedu", "district": "Idukki", "pincode": "685531"}, + {"city": "Nedumkandam", "district": "Idukki", "pincode": "685553"}, + {"city": "Rajakkad", "district": "Idukki", "pincode": "685505"}, + {"city": "Ernakulam", "district": "Ernakulam", "pincode": "682001"}, + {"city": "Kochi", "district": "Ernakulam", "pincode": "682001"}, + {"city": "Cochin", "district": "Ernakulam", "pincode": "682001"}, + {"city": "Aluva", "district": "Ernakulam", "pincode": "683101"}, + {"city": "Angamaly", "district": "Ernakulam", "pincode": "683572"}, + {"city": "Perumbavoor", "district": "Ernakulam", "pincode": "683542"}, + {"city": "Muvattupuzha", "district": "Ernakulam", "pincode": "686661"}, + {"city": "Kalady", "district": "Ernakulam", "pincode": "683574"}, + {"city": "Kothamangalam", "district": "Ernakulam", "pincode": "686691"}, + {"city": "Kakkanad", "district": "Ernakulam", "pincode": "682030"}, + {"city": "Edappally", "district": "Ernakulam", "pincode": "682024"}, + {"city": "Kalamassery", "district": "Ernakulam", "pincode": "683104"}, + {"city": "Fort Kochi", "district": "Ernakulam", "pincode": "682001"}, + {"city": "Vypeen", "district": "Ernakulam", "pincode": "683515"}, + {"city": "Cherai", "district": "Ernakulam", "pincode": "683514"}, + {"city": "Paravur", "district": "Ernakulam", "pincode": "683513"}, + {"city": "Palluruthy", "district": "Ernakulam", "pincode": "682006"}, + {"city": "Mattancherry", "district": "Ernakulam", "pincode": "682002"}, + {"city": "Ponnurunni", "district": "Ernakulam", "pincode": "682019"}, + {"city": "Maradu", "district": "Ernakulam", "pincode": "682304"}, + {"city": "Thrikkakara", "district": "Ernakulam", "pincode": "682021"}, + {"city": "Vyttila", "district": "Ernakulam", "pincode": "682019"}, + {"city": "Palarivattom", "district": "Ernakulam", "pincode": "682025"}, + {"city": "Panangad", "district": "Ernakulam", "pincode": "682506"}, + {"city": "Cheranalloor", "district": "Ernakulam", "pincode": "682034"}, + {"city": "Poothrikka", "district": "Ernakulam", "pincode": "683517"}, + {"city": "Manjapra", "district": "Ernakulam", "pincode": "683581"}, + {"city": "Mulanthuruthy", "district": "Ernakulam", "pincode": "682314"}, + {"city": "Thrissur", "district": "Thrissur", "pincode": "680001"}, + {"city": "Irinjalakuda", "district": "Thrissur", "pincode": "680121"}, + {"city": "Chalakudy", "district": "Thrissur", "pincode": "680307"}, + {"city": "Guruvayur", "district": "Thrissur", "pincode": "680101"}, + {"city": "Kodungallur", "district": "Thrissur", "pincode": "680664"}, + {"city": "Kunnamkulam", "district": "Thrissur", "pincode": "680503"}, + {"city": "Chavakkad", "district": "Thrissur", "pincode": "680506"}, + {"city": "Wadakkanchery", "district": "Thrissur", "pincode": "680582"}, + {"city": "Trichur", "district": "Thrissur", "pincode": "680001"}, + {"city": "Ollur", "district": "Thrissur", "pincode": "680306"}, + {"city": "Puthur", "district": "Thrissur", "pincode": "680553"}, + {"city": "Cherpu", "district": "Thrissur", "pincode": "680561"}, + {"city": "Mala", "district": "Thrissur", "pincode": "680732"}, + {"city": "Anthikkad", "district": "Thrissur", "pincode": "680641"}, + {"city": "Palakkad", "district": "Palakkad", "pincode": "678001"}, + {"city": "Ottapalam", "district": "Palakkad", "pincode": "679101"}, + {"city": "Shornur", "district": "Palakkad", "pincode": "679121"}, + {"city": "Mannarkkad", "district": "Palakkad", "pincode": "678582"}, + {"city": "Chittur", "district": "Palakkad", "pincode": "678101"}, + {"city": "Alathur", "district": "Palakkad", "pincode": "678541"}, + {"city": "Pattambi", "district": "Palakkad", "pincode": "679303"}, + {"city": "Cherpulassery", "district": "Palakkad", "pincode": "679503"}, + {"city": "Palakkad Town", "district": "Palakkad", "pincode": "678001"}, + {"city": "Kuzhalmannam", "district": "Palakkad", "pincode": "678702"}, + {"city": "Vadakkencherry", "district": "Palakkad", "pincode": "678683"}, + {"city": "Malampuzha", "district": "Palakkad", "pincode": "678651"}, + {"city": "Malappuram", "district": "Malappuram", "pincode": "676505"}, + {"city": "Tirur", "district": "Malappuram", "pincode": "676101"}, + {"city": "Ponnani", "district": "Malappuram", "pincode": "679577"}, + {"city": "Manjeri", "district": "Malappuram", "pincode": "676121"}, + {"city": "Perinthalmanna", "district": "Malappuram", "pincode": "679322"}, + {"city": "Kondotty", "district": "Malappuram", "pincode": "673638"}, + {"city": "Tirurrangadi", "district": "Malappuram", "pincode": "676306"}, + {"city": "Kottakkal", "district": "Malappuram", "pincode": "676503"}, + {"city": "Nilambur", "district": "Malappuram", "pincode": "679329"}, + {"city": "Wandoor", "district": "Malappuram", "pincode": "679328"}, + {"city": "Edappal", "district": "Malappuram", "pincode": "679576"}, + {"city": "Vengara", "district": "Malappuram", "pincode": "676304"}, + {"city": "Valanchery", "district": "Malappuram", "pincode": "676552"}, + {"city": "Pandikkad", "district": "Malappuram", "pincode": "676521"}, + {"city": "Kozhikode", "district": "Kozhikode", "pincode": "673001"}, + {"city": "Calicut", "district": "Kozhikode", "pincode": "673001"}, + {"city": "Vatakara", "district": "Kozhikode", "pincode": "673101"}, + {"city": "Koyilandy", "district": "Kozhikode", "pincode": "673305"}, + {"city": "Ramanattukara", "district": "Kozhikode", "pincode": "673633"}, + {"city": "Feroke", "district": "Kozhikode", "pincode": "673631"}, + {"city": "Perambra", "district": "Kozhikode", "pincode": "673525"}, + {"city": "Quilandy", "district": "Kozhikode", "pincode": "673305"}, + {"city": "Balussery", "district": "Kozhikode", "pincode": "673612"}, + {"city": "Chelannur", "district": "Kozhikode", "pincode": "673616"}, + {"city": "Koduvally", "district": "Kozhikode", "pincode": "673572"}, + {"city": "Nanmanda", "district": "Kozhikode", "pincode": "673613"}, + {"city": "Thamarassery", "district": "Kozhikode", "pincode": "673573"}, + {"city": "Ulliyeri", "district": "Kozhikode", "pincode": "673315"}, + {"city": "Elathur", "district": "Kozhikode", "pincode": "673303"}, + {"city": "Olavanna", "district": "Kozhikode", "pincode": "673019"}, + {"city": "Wayanad", "district": "Wayanad", "pincode": "673121"}, + {"city": "Kalpetta", "district": "Wayanad", "pincode": "673121"}, + {"city": "Mananthavady", "district": "Wayanad", "pincode": "670645"}, + {"city": "Sulthan Bathery", "district": "Wayanad", "pincode": "673592"}, + {"city": "Ambalavayal", "district": "Wayanad", "pincode": "673593"}, + {"city": "Vythiri", "district": "Wayanad", "pincode": "673576"}, + {"city": "Panamaram", "district": "Wayanad", "pincode": "670721"}, + {"city": "Meenangadi", "district": "Wayanad", "pincode": "673591"}, + {"city": "Pulpally", "district": "Wayanad", "pincode": "673579"}, + {"city": "Meppadi", "district": "Wayanad", "pincode": "673577"}, + {"city": "Kannur", "district": "Kannur", "pincode": "670001"}, + {"city": "Thalassery", "district": "Kannur", "pincode": "670101"}, + {"city": "Payyanur", "district": "Kannur", "pincode": "670307"}, + {"city": "Iritty", "district": "Kannur", "pincode": "670703"}, + {"city": "Mattannur", "district": "Kannur", "pincode": "670702"}, + {"city": "Kuthuparamba", "district": "Kannur", "pincode": "670643"}, + {"city": "Taliparamba", "district": "Kannur", "pincode": "670141"}, + {"city": "Anthoor", "district": "Kannur", "pincode": "670306"}, + {"city": "Peravoor", "district": "Kannur", "pincode": "670673"}, + {"city": "Sreekandapuram", "district": "Kannur", "pincode": "670631"}, + {"city": "Payyavoor", "district": "Kannur", "pincode": "670693"}, + {"city": "Kasaragod", "district": "Kasaragod", "pincode": "671121"}, + {"city": "Kanhangad", "district": "Kasaragod", "pincode": "671315"}, + {"city": "Manjeshwar", "district": "Kasaragod", "pincode": "671323"}, + {"city": "Hosdurg", "district": "Kasaragod", "pincode": "671315"}, + {"city": "Nileshwar", "district": "Kasaragod", "pincode": "671314"}, + {"city": "Cheruvathur", "district": "Kasaragod", "pincode": "671310"}, + {"city": "Uppala", "district": "Kasaragod", "pincode": "671322"}, + {"city": "Bekal", "district": "Kasaragod", "pincode": "671318"}, + {"city": "Edneer", "district": "Kasaragod", "pincode": "671541"}, + {"city": "Parappa", "district": "Kasaragod", "pincode": "671533"}, + {"city": "Vivekanandapuram", "district": "Thiruvananthapuram", "pincode": "695003"}, + {"city": "Kesavadasapuram", "district": "Thiruvananthapuram", "pincode": "695004"}, + {"city": "Kowdiar", "district": "Thiruvananthapuram", "pincode": "695003"}, + {"city": "Sasthamangalam", "district": "Thiruvananthapuram", "pincode": "695010"}, + {"city": "Palayam", "district": "Thiruvananthapuram", "pincode": "695034"}, + {"city": "Vazhuthacaud", "district": "Thiruvananthapuram", "pincode": "695014"}, + {"city": "Mannanthala", "district": "Thiruvananthapuram", "pincode": "695015"}, + {"city": "Karamana", "district": "Thiruvananthapuram", "pincode": "695002"}, + {"city": "Enchakkal", "district": "Thiruvananthapuram", "pincode": "695615"}, + {"city": "Sreekaryam", "district": "Thiruvananthapuram", "pincode": "695017"}, + {"city": "Ulloor", "district": "Thiruvananthapuram", "pincode": "695011"}, + {"city": "Medical College", "district": "Thiruvananthapuram", "pincode": "695011"}, + {"city": "Nalanchira", "district": "Thiruvananthapuram", "pincode": "695015"}, + {"city": "Vellayambalam", "district": "Thiruvananthapuram", "pincode": "695010"}, + {"city": "Ambalamukku", "district": "Thiruvananthapuram", "pincode": "695005"}, + {"city": "Ookode", "district": "Thiruvananthapuram", "pincode": "695616"}, + {"city": "Kariavattom", "district": "Thiruvananthapuram", "pincode": "695581"}, + {"city": "Aakkulam", "district": "Thiruvananthapuram", "pincode": "695011"}, + {"city": "Pallichal", "district": "Thiruvananthapuram", "pincode": "695020"}, + {"city": "Thirumala", "district": "Thiruvananthapuram", "pincode": "695006"}, + {"city": "Thiruvanikkulam", "district": "Ernakulam", "pincode": "682305"}, + {"city": "Kumbalangi", "district": "Ernakulam", "pincode": "682007"}, + {"city": "Thevara", "district": "Ernakulam", "pincode": "682013"}, + {"city": "Kadavanthra", "district": "Ernakulam", "pincode": "682020"}, + {"city": "Kacheripady", "district": "Ernakulam", "pincode": "682018"}, + {"city": "Gandhinagar", "district": "Ernakulam", "pincode": "682020"}, + {"city": "Vallarpadam", "district": "Ernakulam", "pincode": "683112"}, + {"city": "Willingdon Island", "district": "Ernakulam", "pincode": "682003"}, + {"city": "Mulavukad", "district": "Ernakulam", "pincode": "682504"}, + {"city": "Aroor", "district": "Ernakulam", "pincode": "688534"}, + {"city": "Kumbalam", "district": "Ernakulam", "pincode": "682507"}, + {"city": "Chottanikkara", "district": "Ernakulam", "pincode": "682312"}, + {"city": "Kolenchery", "district": "Ernakulam", "pincode": "682311"}, + {"city": "Kizhakkambalam", "district": "Ernakulam", "pincode": "683562"}, + {"city": "Paravur North", "district": "Ernakulam", "pincode": "683513"}, + {"city": "Varappuzha", "district": "Ernakulam", "pincode": "683517"}, + {"city": "Vadavucode", "district": "Ernakulam", "pincode": "682023"}, + {"city": "Karuvelipady", "district": "Ernakulam", "pincode": "682005"}, + {"city": "Manjaly", "district": "Ernakulam", "pincode": "683580"}, + {"city": "Koratty", "district": "Thrissur", "pincode": "680308"}, + {"city": "Puzhakkal", "district": "Thrissur", "pincode": "680553"}, + {"city": "Thrissur Town", "district": "Thrissur", "pincode": "680020"}, + {"city": "Viyyur", "district": "Thrissur", "pincode": "680010"}, + {"city": "Pookattupady", "district": "Thrissur", "pincode": "683577"}, + {"city": "Ashtamichira", "district": "Thrissur", "pincode": "680732"}, + {"city": "Karalam", "district": "Thrissur", "pincode": "680702"}, + {"city": "Erumappetty", "district": "Thrissur", "pincode": "680642"}, + {"city": "Pariyaram", "district": "Thrissur", "pincode": "680724"}, + {"city": "Mulankunnathukavu", "district": "Thrissur", "pincode": "680581"}, + {"city": "Peringottukara", "district": "Thrissur", "pincode": "680586"}, + {"city": "Shoranur Town", "district": "Palakkad", "pincode": "679122"}, + {"city": "Kalpathy", "district": "Palakkad", "pincode": "678003"}, + {"city": "Kollengode", "district": "Palakkad", "pincode": "678506"}, + {"city": "Palakkad Fort", "district": "Palakkad", "pincode": "678001"}, + {"city": "Nemmara", "district": "Palakkad", "pincode": "678508"}, + {"city": "Kongad", "district": "Palakkad", "pincode": "678631"}, + {"city": "Ayilur", "district": "Palakkad", "pincode": "679514"}, + {"city": "Parali", "district": "Palakkad", "pincode": "679517"}, + {"city": "Agali", "district": "Palakkad", "pincode": "678581"}, + {"city": "Mannarkkad Town", "district": "Palakkad", "pincode": "678583"}, + {"city": "Tenhipalam", "district": "Malappuram", "pincode": "673636"}, + {"city": "Calicut University", "district": "Malappuram", "pincode": "673635"}, + {"city": "Tirur Town", "district": "Malappuram", "pincode": "676102"}, + {"city": "Manjeri Town", "district": "Malappuram", "pincode": "676122"}, + {"city": "Parappanangadi", "district": "Malappuram", "pincode": "676303"}, + {"city": "Areekode", "district": "Malappuram", "pincode": "673639"}, + {"city": "Karuvarakundu", "district": "Malappuram", "pincode": "673523"}, + {"city": "Malappuram Town", "district": "Malappuram", "pincode": "676506"}, + {"city": "Pottasseri", "district": "Malappuram", "pincode": "679321"}, + {"city": "Kolathur", "district": "Malappuram", "pincode": "676103"}, + {"city": "Kottappuram", "district": "Thrissur", "pincode": "680663"}, + {"city": "Triprayar", "district": "Thrissur", "pincode": "680566"}, + {"city": "Pavaratty", "district": "Thrissur", "pincode": "680507"}, + {"city": "Mathilakam", "district": "Thrissur", "pincode": "680686"}, + {"city": "Pazhayannur", "district": "Thrissur", "pincode": "679533"}, + {"city": "Kannayi", "district": "Kannur", "pincode": "670003"}, + {"city": "Chirakkal", "district": "Kannur", "pincode": "670011"}, + {"city": "Dharmadam", "district": "Kannur", "pincode": "670005"}, + {"city": "Pappinisseri", "district": "Kannur", "pincode": "670561"}, + {"city": "Chovva", "district": "Kannur", "pincode": "670006"}, + {"city": "Thana", "district": "Kannur", "pincode": "670671"}, + {"city": "Madayi", "district": "Kannur", "pincode": "670302"}, + {"city": "Payyoli", "district": "Kozhikode", "pincode": "673522"}, + {"city": "Kuttiyadi", "district": "Kozhikode", "pincode": "673508"}, + {"city": "Beypore", "district": "Kozhikode", "pincode": "673015"}, + {"city": "Calicut Beach", "district": "Kozhikode", "pincode": "673032"}, + {"city": "Nadapuram", "district": "Kozhikode", "pincode": "673504"}, + {"city": "Thodannur", "district": "Kozhikode", "pincode": "673307"}, + {"city": "Kalpetta Town", "district": "Wayanad", "pincode": "673122"}, + {"city": "Pulpally Town", "district": "Wayanad", "pincode": "673578"}, + {"city": "Kuppadi", "district": "Wayanad", "pincode": "673596"}, + {"city": "Noolpuzha", "district": "Wayanad", "pincode": "673594"}, + {"city": "Thrissur City", "district": "Thrissur", "pincode": "680022"}, + {"city": "Ollur Town", "district": "Thrissur", "pincode": "680311"}, + {"city": "Velur", "district": "Thrissur", "pincode": "680601"}, + {"city": "Chazhur", "district": "Thrissur", "pincode": "680613"}, + {"city": "Mannuthy", "district": "Thrissur", "pincode": "680651"}, + {"city": "Kottuvally", "district": "Ernakulam", "pincode": "686663"}, + {"city": "Puthencruz", "district": "Ernakulam", "pincode": "682308"}, + {"city": "Thalayolaparambu", "district": "Kottayam", "pincode": "686605"}, + {"city": "Nattakom", "district": "Kottayam", "pincode": "686013"}, + {"city": "Kumarakom", "district": "Kottayam", "pincode": "686563"}, + {"city": "Kidangoor", "district": "Kottayam", "pincode": "686542"}, + {"city": "Kottayam City", "district": "Kottayam", "pincode": "686002"}, + {"city": "Ettumanoor Town", "district": "Kottayam", "pincode": "686632"}, + {"city": "Uzhavoor", "district": "Kottayam", "pincode": "686634"}, + {"city": "Athirampuzha", "district": "Kottayam", "pincode": "686562"}, + {"city": "Lalam", "district": "Idukki", "pincode": "685505"}, + {"city": "Erattayar", "district": "Idukki", "pincode": "685508"}, + {"city": "Marayoor", "district": "Idukki", "pincode": "685620"}, + {"city": "Udumbanchola", "district": "Idukki", "pincode": "685550"}, + {"city": "Vagamon", "district": "Idukki", "pincode": "685503"}, + {"city": "Painavu", "district": "Idukki", "pincode": "685603"}, + {"city": "Kollam City", "district": "Kollam", "pincode": "691013"}, + {"city": "Thevally", "district": "Kollam", "pincode": "691009"}, + {"city": "Asramam", "district": "Kollam", "pincode": "691002"}, + {"city": "Chinnakada", "district": "Kollam", "pincode": "691001"}, + {"city": "Sakthikulangara", "district": "Kollam", "pincode": "691581"}, + {"city": "Thangassery", "district": "Kollam", "pincode": "691008"}, + {"city": "Eravipuram", "district": "Kollam", "pincode": "691021"}, + {"city": "Neendakara", "district": "Kollam", "pincode": "691582"}, + {"city": "Thrikkadavathu", "district": "Pathanamthitta", "pincode": "689674"}, + {"city": "Manarcaud", "district": "Pathanamthitta", "pincode": "689502"}, + {"city": "Kadapra", "district": "Pathanamthitta", "pincode": "689643"}, + {"city": "Kulanada", "district": "Pathanamthitta", "pincode": "689503"}, + {"city": "Niranam", "district": "Pathanamthitta", "pincode": "689621"}, + {"city": "Venmony", "district": "Alappuzha", "pincode": "689544"}, + {"city": "Kainakary", "district": "Alappuzha", "pincode": "688504"}, + {"city": "Kalavoor", "district": "Alappuzha", "pincode": "688522"}, + {"city": "Thanneermukkom", "district": "Alappuzha", "pincode": "688527"}, + {"city": "Thycattussery", "district": "Alappuzha", "pincode": "688526"}, + {"city": "Kanichukulangara", "district": "Alappuzha", "pincode": "688522"}, + {"city": "Kasaragod Town", "district": "Kasaragod", "pincode": "671122"}, + {"city": "Kanhangad Town", "district": "Kasaragod", "pincode": "671316"}, + {"city": "Badiadka", "district": "Kasaragod", "pincode": "671551"}, + {"city": "Vellarikundu", "district": "Kasaragod", "pincode": "671348"}, + {"city": "Trikaripur", "district": "Kasaragod", "pincode": "671310"}, + {"city": "Chandera", "district": "Kasaragod", "pincode": "671541"}, + {"city": "Mulleria", "district": "Kasaragod", "pincode": "671542"}, + {"city": "Periya", "district": "Kasaragod", "pincode": "671543"}, + {"city": "Manjeswaram", "district": "Kasaragod", "pincode": "671323"}, + {"city": "Mavoor", "district": "Kozhikode", "pincode": "673661"}, + {"city": "Kappad", "district": "Kozhikode", "pincode": "673521"}, + {"city": "Kadalundi", "district": "Kozhikode", "pincode": "673302"}, + {"city": "Thiruvambady", "district": "Kozhikode", "pincode": "673603"}, + {"city": "Kuttiady", "district": "Kozhikode", "pincode": "673506"}, + {"city": "Meppayur", "district": "Kozhikode", "pincode": "673524"}, + {"city": "Atholi", "district": "Kozhikode", "pincode": "673315"}, + {"city": "Chelembra", "district": "Malappuram", "pincode": "673634"}, + {"city": "Tirur Junction", "district": "Malappuram", "pincode": "676103"}, + {"city": "Tanur", "district": "Malappuram", "pincode": "676302"}, + {"city": "Kuttippuram", "district": "Malappuram", "pincode": "679571"}, + {"city": "Vettom", "district": "Malappuram", "pincode": "676551"}, + {"city": "Tirurangadi", "district": "Malappuram", "pincode": "676306"}, + {"city": "Munderi", "district": "Malappuram", "pincode": "673641"}, + {"city": "Iringal", "district": "Kozhikode", "pincode": "673521"}, + {"city": "Koyilandy Town", "district": "Kozhikode", "pincode": "673306"}, + {"city": "Panniankara", "district": "Kozhikode", "pincode": "673020"}, + {"city": "Changuvetty", "district": "Kozhikode", "pincode": "673009"}, + {"city": "West Hill", "district": "Kozhikode", "pincode": "673005"}, + {"city": "Azhiyur", "district": "Kozhikode", "pincode": "673309"}, + {"city": "Puthige", "district": "Kasaragod", "pincode": "671124"}, + {"city": "Kallar", "district": "Idukki", "pincode": "685552"}, + {"city": "Vandanmedu", "district": "Idukki", "pincode": "685533"}, + {"city": "Mankulam", "district": "Idukki", "pincode": "685615"}, + {"city": "Pooppara", "district": "Idukki", "pincode": "685580"}, + {"city": "Keerithodu", "district": "Idukki", "pincode": "686508"}, + {"city": "Sreekantapuram", "district": "Kannur", "pincode": "670632"}, + {"city": "Alakode", "district": "Kannur", "pincode": "670571"}, + {"city": "Ezhome", "district": "Kannur", "pincode": "670334"}, + {"city": "Kottiyoor", "district": "Kannur", "pincode": "670731"}, + {"city": "Irikkur", "district": "Kannur", "pincode": "670592"}, + {"city": "Arakkal", "district": "Kannur", "pincode": "670301"}, + {"city": "Ramanthali", "district": "Kannur", "pincode": "670602"}, + {"city": "Panoor", "district": "Kannur", "pincode": "670692"}, + {"city": "Calicut University Campus", "district": "Malappuram", "pincode": "673635"}, + {"city": "Thenjipalam", "district": "Malappuram", "pincode": "673637"}, + {"city": "Irimbiliyam", "district": "Malappuram", "pincode": "676552"}, + {"city": "Kuzhimanna", "district": "Malappuram", "pincode": "676501"}, + {"city": "Edavanna", "district": "Malappuram", "pincode": "676542"}, + {"city": "Kolathur Town", "district": "Malappuram", "pincode": "676103"}, + {"city": "Pulamanthole", "district": "Malappuram", "pincode": "679323"}, + {"city": "Angadippuram", "district": "Malappuram", "pincode": "679321"}, + {"city": "Perumpadappu", "district": "Malappuram", "pincode": "679579"}, + {"city": "Thurakkal", "district": "Malappuram", "pincode": "676509"}, + {"city": "Kallachi", "district": "Kozhikode", "pincode": "673507"}, + {"city": "Thiruvallur", "district": "Ernakulam", "pincode": "682013"}, + {"city": "Elamakkara", "district": "Ernakulam", "pincode": "682026"}, + {"city": "Periyar Nagar", "district": "Ernakulam", "pincode": "683102"}, + {"city": "Njarakkal", "district": "Ernakulam", "pincode": "683503"}, + {"city": "Munambam", "district": "Ernakulam", "pincode": "683513"}, + {"city": "Cherai Beach", "district": "Ernakulam", "pincode": "683514"}, + {"city": "Puthenvelikkara", "district": "Ernakulam", "pincode": "683594"}, + {"city": "Valayanchirangara", "district": "Ernakulam", "pincode": "683556"}, + {"city": "Kizhillam", "district": "Ernakulam", "pincode": "683547"}, + {"city": "Moovattupuzha", "district": "Ernakulam", "pincode": "686662"}, + {"city": "Malayattoor", "district": "Ernakulam", "pincode": "683587"}, + {"city": "Alangad", "district": "Ernakulam", "pincode": "683511"}, + {"city": "Kadungalloor", "district": "Ernakulam", "pincode": "683110"}, + {"city": "Piravom", "district": "Ernakulam", "pincode": "686664"}, + {"city": "Kunnathunad", "district": "Ernakulam", "pincode": "686691"}, + {"city": "Udayamperoor", "district": "Ernakulam", "pincode": "682307"}, + {"city": "Vennala", "district": "Ernakulam", "pincode": "682028"}, + {"city": "Vaduthala", "district": "Ernakulam", "pincode": "682023"}, + {"city": "Pallikara", "district": "Ernakulam", "pincode": "683565"}, + {"city": "Keezhmad", "district": "Ernakulam", "pincode": "683583"}, + {"city": "Kottuvally Town", "district": "Ernakulam", "pincode": "686664"}, + {"city": "Ramamangalam", "district": "Ernakulam", "pincode": "686663"}, + {"city": "Karumalloor", "district": "Ernakulam", "pincode": "683576"}, + {"city": "Manjapra Town", "district": "Ernakulam", "pincode": "683582"}, + {"city": "Pambady", "district": "Kottayam", "pincode": "686502"}, + {"city": "Chempu", "district": "Kottayam", "pincode": "686587"}, + {"city": "Kottayam Junction", "district": "Kottayam", "pincode": "686003"}, + {"city": "Baker Hill", "district": "Kottayam", "pincode": "686001"}, + {"city": "Pallom", "district": "Kottayam", "pincode": "686007"}, + {"city": "Arpookara", "district": "Kottayam", "pincode": "686008"}, + {"city": "Kudamaloor", "district": "Kottayam", "pincode": "686562"}, + {"city": "Peroor", "district": "Kottayam", "pincode": "686545"}, + {"city": "Changanacherry Town", "district": "Kottayam", "pincode": "686102"}, + {"city": "Vakathanam", "district": "Kottayam", "pincode": "686538"}, + {"city": "Nattassery", "district": "Kottayam", "pincode": "686535"}, + {"city": "Karoor", "district": "Kottayam", "pincode": "686544"}, + {"city": "Manarcad", "district": "Kottayam", "pincode": "686543"}, + {"city": "Kumarakom Beach", "district": "Kottayam", "pincode": "686563"}, + {"city": "Alappuzha Beach", "district": "Alappuzha", "pincode": "688012"}, + {"city": "Alappuzha North", "district": "Alappuzha", "pincode": "688001"}, + {"city": "Thuravoor", "district": "Alappuzha", "pincode": "688534"}, + {"city": "Thakazhi", "district": "Alappuzha", "pincode": "688562"}, + {"city": "Chambakkulam", "district": "Alappuzha", "pincode": "688538"}, + {"city": "Muttar", "district": "Alappuzha", "pincode": "689623"}, + {"city": "Aryad", "district": "Alappuzha", "pincode": "688536"}, + {"city": "Punnapra", "district": "Alappuzha", "pincode": "688004"}, + {"city": "Pathirapally", "district": "Alappuzha", "pincode": "689652"}, + {"city": "Pallippad", "district": "Alappuzha", "pincode": "690515"}, + {"city": "Thiruvalla Town", "district": "Pathanamthitta", "pincode": "689102"}, + {"city": "Pathanamthitta Town", "district": "Pathanamthitta", "pincode": "689646"}, + {"city": "Adoor Town", "district": "Pathanamthitta", "pincode": "691524"}, + {"city": "Pandalam Town", "district": "Pathanamthitta", "pincode": "689502"}, + {"city": "Elanthoor", "district": "Pathanamthitta", "pincode": "689643"}, + {"city": "Naranganam", "district": "Pathanamthitta", "pincode": "686535"}, + {"city": "Mezhuveli", "district": "Pathanamthitta", "pincode": "689648"}, + {"city": "Murickassery", "district": "Idukki", "pincode": "685512"}, + {"city": "Thankamani", "district": "Idukki", "pincode": "685509"}, + {"city": "Upputhara", "district": "Idukki", "pincode": "685505"}, + {"city": "Vandiperiyar", "district": "Idukki", "pincode": "685533"}, + {"city": "Kanjikuzhy", "district": "Idukki", "pincode": "686607"}, + {"city": "Karimannoor", "district": "Idukki", "pincode": "686541"}, + {"city": "Myladumpara", "district": "Idukki", "pincode": "685515"}, + {"city": "Vathikudy", "district": "Idukki", "pincode": "685514"}, + {"city": "Kothamangalam Town", "district": "Ernakulam", "pincode": "686692"}, + {"city": "Periyapuram", "district": "Thrissur", "pincode": "680025"}, + {"city": "Ollur Junction", "district": "Thrissur", "pincode": "680307"}, + {"city": "Kunnamkulam Town", "district": "Thrissur", "pincode": "680504"}, + {"city": "Guruvayur Town", "district": "Thrissur", "pincode": "680102"}, + {"city": "Irinjalakuda Town", "district": "Thrissur", "pincode": "680122"}, + {"city": "Chalakudy Town", "district": "Thrissur", "pincode": "680308"}, + {"city": "Kodungallur Town", "district": "Thrissur", "pincode": "680665"}, + {"city": "Kanjani", "district": "Thrissur", "pincode": "680571"}, + {"city": "Puthukkad", "district": "Thrissur", "pincode": "680301"}, + {"city": "Moothakunnam", "district": "Ernakulam", "pincode": "683516"}, + {"city": "Nayarambalam", "district": "Ernakulam", "pincode": "682509"}, + {"city": "Chendamangalam", "district": "Ernakulam", "pincode": "683512"}, + {"city": "North Paravur", "district": "Ernakulam", "pincode": "683513"}, + {"city": "Alangad Town", "district": "Ernakulam", "pincode": "683512"}, + {"city": "Vadavucode Puthencruz", "district": "Ernakulam", "pincode": "682308"}, + {"city": "Rayamangalam", "district": "Ernakulam", "pincode": "686692"}, + {"city": "Edakkattuvayal", "district": "Ernakulam", "pincode": "683565"}, + {"city": "Varapuzha", "district": "Ernakulam", "pincode": "683517"}, + {"city": "Pallippuram", "district": "Ernakulam", "pincode": "683513"} +] diff --git a/lib/features/booking/providers/checkout_provider.dart b/lib/features/booking/providers/checkout_provider.dart index 685ce95..d6cbdeb 100644 --- a/lib/features/booking/providers/checkout_provider.dart +++ b/lib/features/booking/providers/checkout_provider.dart @@ -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 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> 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; diff --git a/lib/features/events/models/event_models.dart b/lib/features/events/models/event_models.dart index 1f3d333..3c481f9 100644 --- a/lib/features/events/models/event_models.dart +++ b/lib/features/events/models/event_models.dart @@ -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?, ); } } diff --git a/lib/features/events/services/events_service.dart b/lib/features/events/services/events_service.dart index 4a3f7bc..c73ed55 100644 --- a/lib/features/events/services/events_service.dart +++ b/lib/features/events/services/events_service.dart @@ -83,6 +83,28 @@ class EventsService { return EventModel.fromJson(Map.from(res)); } + /// Related events by event_type_id (EVT-002). + /// Fetches events with the same category, silently returns [] on failure. + Future> 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((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> getEventsByMonthYear(String month, int year) async { final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false); diff --git a/lib/features/gamification/services/gamification_service.dart b/lib/features/gamification/services/gamification_service.dart index 5d68a51..26945c9 100644 --- a/lib/features/gamification/services/gamification_service.dart +++ b/lib/features/gamification/services/gamification_service.dart @@ -45,6 +45,33 @@ class GamificationService { ); } + // --------------------------------------------------------------------------- + // Public contributor profile (any user by userId / email) + // GET /v1/gamification/dashboard?user_id={userId} + // --------------------------------------------------------------------------- + Future 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? ?? {}; + final rawSubs = res['submissions'] as List? ?? []; + final rawAchievements = res['achievements'] as List? ?? []; + + final submissions = rawSubs + .map((s) => SubmissionModel.fromJson(Map.from(s as Map))) + .toList(); + + final achievements = rawAchievements + .map((a) => AchievementBadge.fromJson(Map.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 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), ]; } diff --git a/lib/features/reviews/widgets/review_summary.dart b/lib/features/reviews/widgets/review_summary.dart index f0231c4..b1979e5 100644 --- a/lib/features/reviews/widgets/review_summary.dart +++ b/lib/features/reviews/widgets/review_summary.dart @@ -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 diff --git a/lib/features/share/share_rank_card.dart b/lib/features/share/share_rank_card.dart new file mode 100644 index 0000000..b22c975 --- /dev/null +++ b/lib/features/share/share_rank_card.dart @@ -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 createState() => _ShareRankCardState(); +} + +class _ShareRankCardState extends State { + 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 get _gradient { + return _tierGradients[widget.tier] ?? [const Color(0xFF0F172A), const Color(0xFF1E293B)]; + } + + Future _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)), + ), + ], + ); + } +} diff --git a/lib/screens/checkout_screen.dart b/lib/screens/checkout_screen.dart index 2add183..9c5de9b 100644 --- a/lib/screens/checkout_screen.dart +++ b/lib/screens/checkout_screen.dart @@ -31,6 +31,7 @@ class _CheckoutScreenState extends State { final _nameCtrl = TextEditingController(); final _emailCtrl = TextEditingController(); final _phoneCtrl = TextEditingController(); + final _promoCtrl = TextEditingController(); @override void initState() { @@ -77,6 +78,7 @@ class _CheckoutScreenState extends State { _nameCtrl.dispose(); _emailCtrl.dispose(); _phoneCtrl.dispose(); + _promoCtrl.dispose(); super.dispose(); } @@ -253,6 +255,84 @@ class _CheckoutScreenState extends State { _field('Full Name', _nameCtrl, validator: (v) => v!.isEmpty ? 'Required' : null), _field('Email', _emailCtrl, type: TextInputType.emailAddress, validator: (v) => v!.isEmpty ? 'Required' : null), _field('Phone', _phoneCtrl, type: TextInputType.phone, validator: (v) => v!.isEmpty ? 'Required' : null), + const SizedBox(height: 8), + Consumer( + builder: (context, provider, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextFormField( + controller: _promoCtrl, + textCapitalization: TextCapitalization.characters, + decoration: InputDecoration( + labelText: 'Promo Code (optional)', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), + filled: true, + fillColor: Colors.grey.shade50, + suffixIcon: provider.promoApplied + ? const Icon(Icons.check_circle, color: Colors.green, size: 20) + : null, + ), + enabled: !provider.promoApplied, + ), + ), + const SizedBox(width: 8), + SizedBox( + height: 56, + child: provider.promoApplied + ? OutlinedButton( + onPressed: () { + provider.resetPromo(); + _promoCtrl.clear(); + }, + style: OutlinedButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Remove'), + ) + : ElevatedButton( + onPressed: provider.loading + ? null + : () async { + final ok = await provider.applyPromo(_promoCtrl.text); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(provider.promoMessage ?? + (ok ? 'Promo applied!' : 'Invalid promo code')), + backgroundColor: ok ? Colors.green : Colors.red, + ), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0B63D6), + foregroundColor: Colors.white, + ), + child: const Text('Apply'), + ), + ), + ], + ), + if (provider.promoApplied && provider.promoMessage != null) ...[ + const SizedBox(height: 6), + Row( + children: [ + const Icon(Icons.local_offer, size: 14, color: Colors.green), + const SizedBox(width: 4), + Text( + '${provider.promoMessage} — saves \u20b9${provider.discountAmount.toStringAsFixed(0)}', + style: const TextStyle(fontSize: 12, color: Colors.green), + ), + ], + ), + ], + ], + ); + }, + ), ], ), ), @@ -293,6 +373,34 @@ class _CheckoutScreenState extends State { ), )), const Divider(height: 32), + if (provider.promoApplied) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Subtotal', style: TextStyle(color: Colors.grey.shade700)), + Text('Rs ${provider.subtotal.toStringAsFixed(0)}', style: TextStyle(color: Colors.grey.shade700)), + ], + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon(Icons.local_offer, size: 14, color: Colors.green), + const SizedBox(width: 4), + Text( + provider.couponCode != null ? 'Promo (${provider.couponCode})' : 'Promo discount', + style: const TextStyle(color: Colors.green), + ), + ], + ), + Text('- Rs ${provider.discountAmount.toStringAsFixed(0)}', + style: const TextStyle(color: Colors.green, fontWeight: FontWeight.w600)), + ], + ), + const SizedBox(height: 8), + ], Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/screens/contribute_screen.dart b/lib/screens/contribute_screen.dart index eb018d8..0af4f61 100644 --- a/lib/screens/contribute_screen.dart +++ b/lib/screens/contribute_screen.dart @@ -14,7 +14,11 @@ import 'package:share_plus/share_plus.dart'; import '../core/app_decoration.dart'; import '../features/gamification/models/gamification_models.dart'; import '../features/gamification/providers/gamification_provider.dart'; +import '../widgets/glass_card.dart'; import '../widgets/landscape_section_header.dart'; +import '../widgets/tier_avatar_ring.dart'; +import '../features/share/share_rank_card.dart'; +import 'contributor_profile_screen.dart'; // ───────────────────────────────────────────────────────────────────────────── // Tier colour map @@ -29,12 +33,19 @@ const _tierColors = { // Icon map for achievement badges const _badgeIcons = { - 'edit': Icons.edit_outlined, - 'star': Icons.star_outline, - 'emoji_events': Icons.emoji_events_outlined, - 'leaderboard': Icons.leaderboard_outlined, - 'photo_library': Icons.photo_library_outlined, - 'verified': Icons.verified_outlined, + 'edit': Icons.edit_outlined, + 'star': Icons.star_outline, + 'emoji_events': Icons.emoji_events_outlined, + 'leaderboard': Icons.leaderboard_outlined, + 'photo_library': Icons.photo_library_outlined, + 'verified': Icons.verified_outlined, + // ACH-002: icons for expanded badge set (badges 02, 06–11) + 'trending_up': Icons.trending_up, + 'rocket_launch': Icons.rocket_launch_outlined, + 'event_hunter': Icons.search_outlined, + 'location_on': Icons.location_on_outlined, + 'diamond': Icons.diamond_outlined, + 'workspace_premium': Icons.workspace_premium_outlined, }; // District list for the contribution form @@ -254,7 +265,56 @@ class _ContributeScreenState extends State children: [ _epStatCard('Lifetime EP', '${profile?.lifetimeEp ?? 0}', Icons.star, const Color(0xFFF59E0B)), const SizedBox(width: 8), - _epStatCard('Liquid EP', '${profile?.currentEp ?? 0}', Icons.bolt, const Color(0xFF3B82F6)), + // GAM-003 + GAM-004: Liquid EP card with cycle countdown and progress + Expanded( + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFF3B82F6).withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFF3B82F6).withOpacity(0.3)), + ), + child: Column( + children: [ + const Icon(Icons.bolt, color: Color(0xFF3B82F6), size: 20), + const SizedBox(height: 4), + Text( + '${profile?.currentEp ?? 0}', + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 16), + ), + const SizedBox(height: 2), + const Text('Liquid EP', style: TextStyle(color: Colors.white60, fontSize: 10), textAlign: TextAlign.center), + if (provider.currentUserStats?.rewardCycleDays != null) ...[ + const SizedBox(height: 4), + Text( + 'Converts in ${provider.currentUserStats!.rewardCycleDays}d', + style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)), + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Builder( + builder: (_) { + final days = provider.currentUserStats?.rewardCycleDays ?? 30; + final elapsed = (30 - days).clamp(0, 30); + final ratio = elapsed / 30; + return LinearProgressIndicator( + value: ratio, + minHeight: 4, + backgroundColor: Colors.white12, + valueColor: AlwaysStoppedAnimation( + ratio > 0.7 ? const Color(0xFFFBBF24) : const Color(0xFF3B82F6), + ), + ); + }, + ), + ), + ], + ], + ), + ), + ), const SizedBox(width: 8), _epStatCard('Reward Pts', '${profile?.currentRp ?? 0}', Icons.redeem, const Color(0xFF10B981)), ], @@ -1169,20 +1229,34 @@ class _ContributeScreenState extends State // Badge icon colors final iconColors = { - 'edit': const Color(0xFF3B82F6), - 'star': const Color(0xFFF59E0B), - 'emoji_events': const Color(0xFFF97316), - 'leaderboard': const Color(0xFF8B5CF6), - 'photo_library': const Color(0xFF6B7280), - 'verified': const Color(0xFF10B981), + 'edit': const Color(0xFF3B82F6), + 'star': const Color(0xFFF59E0B), + 'emoji_events': const Color(0xFFF97316), + 'leaderboard': const Color(0xFF8B5CF6), + 'photo_library': const Color(0xFF6B7280), + 'verified': const Color(0xFF10B981), + // ACH-002: colors for expanded badge set + 'trending_up': const Color(0xFF0EA5E9), + 'rocket_launch': const Color(0xFFEC4899), + 'event_hunter': const Color(0xFF64748B), + 'location_on': const Color(0xFF22C55E), + 'diamond': const Color(0xFF06B6D4), + 'workspace_premium': const Color(0xFFE879F9), }; final bgColors = { - 'edit': const Color(0xFFDBEAFE), - 'star': const Color(0xFFFEF3C7), - 'emoji_events': const Color(0xFFFED7AA), - 'leaderboard': const Color(0xFFEDE9FE), - 'photo_library': const Color(0xFFF3F4F6), - 'verified': const Color(0xFFD1FAE5), + 'edit': const Color(0xFFDBEAFE), + 'star': const Color(0xFFFEF3C7), + 'emoji_events': const Color(0xFFFED7AA), + 'leaderboard': const Color(0xFFEDE9FE), + 'photo_library': const Color(0xFFF3F4F6), + 'verified': const Color(0xFFD1FAE5), + // ACH-002: backgrounds for expanded badge set + 'trending_up': const Color(0xFFE0F2FE), + 'rocket_launch': const Color(0xFFFCE7F3), + 'event_hunter': const Color(0xFFF1F5F9), + 'location_on': const Color(0xFFDCFCE7), + 'diamond': const Color(0xFFCFFAFE), + 'workspace_premium': const Color(0xFFFAE8FF), }; final iconColor = isUnlocked ? (iconColors[badge.iconName] ?? const Color(0xFF6B7280)) : const Color(0xFF9CA3AF); @@ -1520,9 +1594,10 @@ class _ContributeScreenState extends State // ═══════════════════════════════════════════════════════════════════════════ Widget _buildContributeTab(BuildContext context, GamificationProvider provider) { final theme = Theme.of(context); + final bottomInset = MediaQuery.of(context).padding.bottom; return SingleChildScrollView( physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.fromLTRB(16, 20, 16, 32), + padding: EdgeInsets.fromLTRB(16, 20, 16, 32 + bottomInset), child: Form( key: _formKey, child: Column( @@ -2021,6 +2096,26 @@ class _ContributeScreenState extends State // ── Time period toggle (top-right) + district scroll ────────────────── _buildLeaderboardFilters(provider), + // LDR-003: Current user stats card at top of leaderboard + if (provider.currentUserStats != null) + Builder(builder: (context) { + final stats = provider.currentUserStats!; + return GlassCard( + margin: const EdgeInsets.fromLTRB(16, 8, 16, 4), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildStatChip('Rank', '#${stats.rank}', Icons.leaderboard), + Container(width: 1, height: 32, color: Colors.white12), + _buildStatChip('EP', '${stats.points}', Icons.bolt), + Container(width: 1, height: 32, color: Colors.white12), + _buildStatChip('Cycle', '${stats.rewardCycleDays}d', Icons.timelapse), + ], + ), + ); + }), + Expanded( child: Container( color: const Color(0xFFFAFBFC), @@ -2183,29 +2278,16 @@ class _ContributeScreenState extends State return Expanded( child: Column( children: [ - // Avatar with rank badge overlaid + // GAM-006: Avatar with tier ring + rank badge overlaid Stack( clipBehavior: Clip.none, children: [ - // Avatar circle - Container( - width: avatarSize, - height: avatarSize, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: const Color(0xFFE0F2FE), - border: Border.all(color: pillarColors[i], width: 2.5), - ), - child: Center( - child: Text( - e.username.isNotEmpty ? e.username[0].toUpperCase() : '?', - style: TextStyle( - fontSize: i == 1 ? 24 : 18, - fontWeight: FontWeight.w800, - color: pillarColors[i], - ), - ), - ), + // TierAvatarRing — tier-coloured glow ring + TierAvatarRing( + username: e.username, + tier: tierLabel(e.tier), + size: avatarSize, + imageUrl: e.avatarUrl, ), // Rank badge — bottom-right corner of avatar Positioned( @@ -2267,7 +2349,17 @@ class _ContributeScreenState extends State final tierColor = _tierColors[entry.tier]!; final isMe = entry.isCurrentUser; - return Container( + return GestureDetector( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ContributorProfileScreen( + contributorId: entry.username, + contributorName: entry.username, + ), + ), + ), + child: Container( decoration: BoxDecoration( color: isMe ? const Color(0xFFEFF6FF) : Colors.white, border: const Border(bottom: BorderSide(color: Color(0xFFE5E7EB), width: 0.8)), @@ -2287,21 +2379,12 @@ class _ContributeScreenState extends State ), ), ), - // Avatar circle - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: const Color(0xFFE0F2FE), - border: Border.all(color: tierColor.withOpacity(0.5), width: 1.5), - ), - child: Center( - child: Text( - entry.username.isNotEmpty ? entry.username[0].toUpperCase() : '?', - style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14, color: tierColor), - ), - ), + // GAM-006: TierAvatarRing replaces plain avatar circle + TierAvatarRing( + username: entry.username, + tier: tierLabel(entry.tier), + size: 36, + imageUrl: entry.avatarUrl, ), const SizedBox(width: 8), // Name @@ -2360,6 +2443,7 @@ class _ContributeScreenState extends State ), ], ), + ), ); } @@ -2386,10 +2470,19 @@ class _ContributeScreenState extends State ), GestureDetector( onTap: () { - Share.share( - 'I\'m ranked #${me.rank} on @EventifyPlus with ${me.lifetimeEp} EP! 🏆 ' - 'Discover & contribute to events near you at eventifyplus.com', - subject: 'My Eventify.Plus Leaderboard Rank', + final gam = context.read(); + showDialog( + context: context, + builder: (_) => Dialog( + backgroundColor: Colors.transparent, + child: ShareRankCard( + username: me.username, + tier: tierLabel(me.tier), + rank: me.rank, + ep: me.lifetimeEp, + rewardPoints: gam.profile?.currentRp ?? 0, + ), + ), ); }, child: Container( @@ -2410,6 +2503,19 @@ class _ContributeScreenState extends State ); } + // LDR-003: Stat chip helper for current-user leaderboard card + Widget _buildStatChip(String label, String value, IconData icon) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14, color: const Color(0xFF94A3B8)), + const SizedBox(height: 2), + Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: Colors.white)), + Text(label, style: const TextStyle(fontSize: 10, color: Color(0xFF64748B))), + ], + ); + } + // ═══════════════════════════════════════════════════════════════════════════ // TAB 2 — ACHIEVEMENTS // ═══════════════════════════════════════════════════════════════════════════ @@ -2421,9 +2527,10 @@ class _ContributeScreenState extends State } final badges = provider.achievements; + final bottomInset = MediaQuery.of(context).padding.bottom; return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), + padding: EdgeInsets.fromLTRB(16, 16, 16, 32 + bottomInset), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/screens/contributor_profile_screen.dart b/lib/screens/contributor_profile_screen.dart new file mode 100644 index 0000000..3671ea4 --- /dev/null +++ b/lib/screens/contributor_profile_screen.dart @@ -0,0 +1,271 @@ +// lib/screens/contributor_profile_screen.dart +// CTR-004 — Public contributor profile page. +// Shows avatar, tier ring, EP stats, and submission grid for any contributor. + +import 'package:flutter/material.dart'; + +import '../features/gamification/models/gamification_models.dart'; +import '../features/gamification/services/gamification_service.dart'; +import '../widgets/tier_avatar_ring.dart'; + +class ContributorProfileScreen extends StatefulWidget { + final String contributorId; + final String contributorName; + + const ContributorProfileScreen({ + super.key, + required this.contributorId, + required this.contributorName, + }); + + @override + State createState() => _ContributorProfileScreenState(); +} + +class _ContributorProfileScreenState extends State { + DashboardResponse? _data; + bool _loading = true; + String? _error; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + try { + final data = await GamificationService().getDashboardForUser(widget.contributorId); + if (mounted) { + setState(() { + _data = data; + _loading = false; + }); + } + } catch (_) { + if (mounted) { + setState(() { + _error = 'Could not load profile'; + _loading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF0F172A), + appBar: AppBar( + backgroundColor: const Color(0xFF0F172A), + foregroundColor: Colors.white, + title: Text( + widget.contributorName, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + elevation: 0, + ), + body: _loading + ? const Center( + child: CircularProgressIndicator(color: Color(0xFF3B82F6)), + ) + : _error != null + ? Center( + child: Text( + _error!, + style: const TextStyle(color: Colors.white54), + ), + ) + : _buildContent(), + ); + } + + Widget _buildContent() { + final profile = _data!.profile; + final submissions = _data!.submissions; + final tierStr = tierLabel(profile.tier); + + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + // Avatar with tier ring + TierAvatarRing( + username: widget.contributorName, + tier: tierStr, + size: 88, + ), + const SizedBox(height: 12), + Text( + widget.contributorName, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFF1E3A8A), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + tierStr, + style: const TextStyle(fontSize: 12, color: Color(0xFF93C5FD)), + ), + ), + const SizedBox(height: 16), + // Stats row + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _statCard('EP', '${profile.currentEp}'), + _statCard('Events', '${submissions.length}'), + _statCard( + 'Approved', + '${submissions.where((s) => s.status.toUpperCase() == 'APPROVED').length}', + ), + ], + ), + ], + ), + ), + ), + if (submissions.isNotEmpty) + SliverPadding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 24), + sliver: SliverGrid( + delegate: SliverChildBuilderDelegate( + (context, i) => _buildSubmissionTile(submissions[i]), + childCount: submissions.length, + ), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + childAspectRatio: 1.1, + ), + ), + ) + else + const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.all(32), + child: Text( + 'No submissions yet', + style: TextStyle(color: Colors.white38), + ), + ), + ), + ), + ], + ); + } + + Widget _statCard(String label, String value) { + return Column( + children: [ + Text( + value, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w800, + color: Colors.white, + ), + ), + const SizedBox(height: 2), + Text( + label, + style: const TextStyle(fontSize: 12, color: Color(0xFF94A3B8)), + ), + ], + ); + } + + Widget _buildSubmissionTile(SubmissionModel s) { + final Color statusColor; + switch (s.status.toUpperCase()) { + case 'APPROVED': + statusColor = const Color(0xFF22C55E); + break; + case 'REJECTED': + statusColor = const Color(0xFFEF4444); + break; + default: + statusColor = const Color(0xFFFBBF24); // PENDING + } + + // SubmissionModel.images is List; use first image if present. + final String? firstImage = s.images.isNotEmpty ? s.images.first : null; + + return Container( + decoration: BoxDecoration( + color: const Color(0xFF1E293B), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + if (firstImage != null && firstImage.isNotEmpty) + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SizedBox.expand( + child: Image.network( + firstImage, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container(color: const Color(0xFF334155)), + ), + ), + ), + Positioned( + top: 6, + right: 6, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.9), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + s.status, + style: const TextStyle( + fontSize: 9, + color: Colors.white, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + if (s.eventName.isNotEmpty) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.all(6), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [Colors.black87, Colors.transparent], + ), + borderRadius: BorderRadius.vertical(bottom: Radius.circular(10)), + ), + child: Text( + s.eventName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 10, color: Colors.white), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 3720f32..08ce25b 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -50,7 +50,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM bool _loading = true; // Hero carousel - final PageController _heroPageController = PageController(viewportFraction: 0.88); + final PageController _heroPageController = PageController(viewportFraction: 0.9); late final ValueNotifier _heroPageNotifier; Timer? _autoScrollTimer; @@ -453,10 +453,11 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ), // Floating bottom navigation (always visible) + // bottom offset accounts for home indicator on iPhone/Android gesture bar Positioned( left: 16, right: 16, - bottom: 16, + bottom: MediaQuery.of(context).padding.bottom + 16, child: _buildFloatingBottomNav(), ), ], @@ -1532,11 +1533,14 @@ class _HomeScreenState extends State with SingleTickerProviderStateM _selectedDateFilter.isNotEmpty ? 'No events for "$_selectedDateFilter"' : 'No events found', style: const TextStyle(color: Color(0xFF9CA3AF)), )) - : ListView.separated( - scrollDirection: Axis.horizontal, + : PageView.builder( + controller: PageController(viewportFraction: 0.85), + physics: const PageScrollPhysics(), itemCount: _allFilteredByDate.length, - separatorBuilder: (_, __) => const SizedBox(width: 12), - itemBuilder: (context, index) => _buildTopEventCard(_allFilteredByDate[index]), + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.only(right: 12), + child: _buildTopEventCard(_allFilteredByDate[index]), + ), ), ), const SizedBox(height: 24), diff --git a/lib/screens/learn_more_screen.dart b/lib/screens/learn_more_screen.dart index 8a7c4d7..8002909 100644 --- a/lib/screens/learn_more_screen.dart +++ b/lib/screens/learn_more_screen.dart @@ -15,6 +15,8 @@ import '../core/auth/auth_guard.dart'; import '../core/utils/error_utils.dart'; import '../core/constants.dart'; import '../features/reviews/widgets/review_section.dart'; +import '../widgets/tier_avatar_ring.dart'; +import 'contributor_profile_screen.dart'; import 'checkout_screen.dart'; class LearnMoreScreen extends StatefulWidget { @@ -59,6 +61,10 @@ class _LearnMoreScreenState extends State { // Google Map GoogleMapController? _mapController; + // Related events (EVT-002) + List _relatedEvents = []; + bool _loadingRelated = false; + @override void initState() { super.initState(); @@ -100,6 +106,7 @@ class _LearnMoreScreenState extends State { _event = ev; }); _startAutoScroll(); + _loadRelatedEvents(); return; // success } catch (e) { debugPrint('_loadFullDetails attempt ${attempt + 1} failed for event ${widget.eventId}: $e'); @@ -120,6 +127,7 @@ class _LearnMoreScreenState extends State { if (!mounted) return; setState(() => _event = ev); _startAutoScroll(); + _loadRelatedEvents(); } catch (e) { if (!mounted) return; setState(() => _error = userFriendlyError(e)); @@ -128,6 +136,19 @@ class _LearnMoreScreenState extends State { } } + /// Fetch related events by the same event type (EVT-002). + Future _loadRelatedEvents() async { + if (_event?.eventTypeId == null) return; + if (mounted) setState(() => _loadingRelated = true); + try { + final events = await _service.getEventsByCategory(_event!.eventTypeId!, limit: 6); + final filtered = events.where((e) => e.id != widget.eventId).take(5).toList(); + if (mounted) setState(() => _relatedEvents = filtered); + } finally { + if (mounted) setState(() => _loadingRelated = false); + } + } + // --------------------------------------------------------------------------- // Carousel helpers // --------------------------------------------------------------------------- @@ -441,8 +462,12 @@ class _LearnMoreScreenState extends State { if (_event!.importantInfo.isEmpty && (_event!.importantInformation ?? '').isNotEmpty) _buildImportantInfoFallback(theme), + // EVT-001: Contributor widget + _buildContributorSection(theme), const SizedBox(height: 24), ReviewSection(eventId: widget.eventId), + // EVT-002: Related events horizontal row + _buildRelatedEventsSection(theme), ], ), ), @@ -619,11 +644,15 @@ class _LearnMoreScreenState extends State { if (_event!.importantInfo.isEmpty && (_event!.importantInformation ?? '').isNotEmpty) _buildImportantInfoFallback(theme), + // EVT-001: Contributor widget + _buildContributorSection(theme), const SizedBox(height: 24), Padding( padding: const EdgeInsets.symmetric(horizontal: 0), child: ReviewSection(eventId: widget.eventId), ), + // EVT-002: Related events horizontal row + _buildRelatedEventsSection(theme), const SizedBox(height: 100), ], ), @@ -1335,6 +1364,227 @@ class _LearnMoreScreenState extends State { return items; } + // --------------------------------------------------------------------------- + // 8. CONTRIBUTOR WIDGET (EVT-001) + // --------------------------------------------------------------------------- + + Widget _buildContributorSection(ThemeData theme) { + final name = _event?.contributorName; + if (name == null || name.isEmpty) return const SizedBox.shrink(); + final tier = _event!.contributorTier ?? ''; + return Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.brightness == Brightness.dark + ? const Color(0xFF1E293B) + : theme.cardColor, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: theme.brightness == Brightness.dark + ? Colors.white.withOpacity(0.08) + : theme.dividerColor, + ), + ), + child: Row( + children: [ + TierAvatarRing( + username: name, + tier: tier, + size: 40, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Contributed by', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.hintColor, + fontSize: 11, + ), + ), + const SizedBox(height: 2), + Text( + name, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + if (tier.isNotEmpty) + Container( + margin: const EdgeInsets.only(top: 2), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + color: theme.colorScheme.primary.withOpacity(0.15), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + tier, + style: TextStyle( + fontSize: 10, + color: theme.colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + if (_event?.contributorId != null) + IconButton( + icon: Icon(Icons.arrow_forward_ios, + size: 14, color: theme.hintColor), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ContributorProfileScreen( + contributorId: _event!.contributorId!, + contributorName: _event!.contributorName!, + ), + ), + ); + }, + ), + ], + ), + ), + ); + } + + // --------------------------------------------------------------------------- + // 9. RELATED EVENTS ROW (EVT-002) + // --------------------------------------------------------------------------- + + Widget _buildRelatedEventsSection(ThemeData theme) { + if (_loadingRelated) { + return Padding( + padding: const EdgeInsets.fromLTRB(20, 28, 20, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Related Events', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + fontSize: 18, + ), + ), + const SizedBox(height: 12), + const Center( + child: SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ], + ), + ); + } + if (_relatedEvents.isEmpty) return const SizedBox.shrink(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 28, 20, 8), + child: Text( + 'Related Events', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + fontSize: 18, + ), + ), + ), + SizedBox( + height: 200, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _relatedEvents.length, + itemBuilder: (context, i) { + final e = _relatedEvents[i]; + final displayName = e.title ?? e.name; + final imageUrl = e.thumbImg ?? ''; + return GestureDetector( + onTap: () => Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => LearnMoreScreen(eventId: e.id), + ), + ), + child: Container( + width: 140, + margin: const EdgeInsets.only(right: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: theme.brightness == Brightness.dark + ? const Color(0xFF1E293B) + : theme.cardColor, + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withOpacity(0.06), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + ), + child: imageUrl.isNotEmpty + ? CachedNetworkImage( + imageUrl: imageUrl, + height: 100, + width: 140, + fit: BoxFit.cover, + errorWidget: (_, __, ___) => Container( + height: 100, + width: 140, + color: theme.dividerColor, + child: Icon(Icons.event, + size: 32, color: theme.hintColor), + ), + ) + : Container( + height: 100, + width: 140, + color: theme.dividerColor, + child: Icon(Icons.event, + size: 32, color: theme.hintColor), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Text( + displayName, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.w500, + height: 1.35, + ), + ), + ), + ], + ), + ), + ); + }, + ), + ), + const SizedBox(height: 8), + ], + ); + } + Widget _buildImportantInfoFallback(ThemeData theme) { final parsed = _parseHtmlImportantInfo(_event!.importantInformation!); diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index b4ae914..1be9b7f 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -1,9 +1,12 @@ +import 'dart:convert'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import '../core/utils/error_utils.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -13,11 +16,14 @@ import '../features/events/models/event_models.dart'; import '../features/gamification/providers/gamification_provider.dart'; import '../features/gamification/models/gamification_models.dart'; import '../widgets/skeleton_loader.dart'; +import '../widgets/tier_avatar_ring.dart'; import 'learn_more_screen.dart'; import 'settings_screen.dart'; +import '../core/api/api_endpoints.dart'; import '../core/app_decoration.dart'; import '../core/constants.dart'; import '../widgets/landscape_section_header.dart'; +import '../features/share/share_rank_card.dart'; class ProfileScreen extends StatefulWidget { const ProfileScreen({Key? key}) : super(key: key); @@ -31,10 +37,35 @@ class _ProfileScreenState extends State String _username = ''; String _email = 'not provided'; String _profileImage = ''; + String? _eventifyId; + String? _userTier; + String? _district; + DateTime? _districtChangedAt; final ImagePicker _picker = ImagePicker(); + // 14 Kerala districts + static const List _districts = [ + 'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha', + 'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad', + 'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', + ]; + final EventsService _eventsService = EventsService(); + // AUTH-005: District change cooldown (183-day lock) + bool get _districtLocked { + if (_districtChangedAt == null) return false; + return DateTime.now().difference(_districtChangedAt!) < const Duration(days: 183); + } + + String get _districtNextChange { + if (_districtChangedAt == null) return ''; + final next = _districtChangedAt!.add(const Duration(days: 183)); + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + return '${next.day} ${months[next.month - 1]} ${next.year}'; + } + List _ongoingEvents = []; List _upcomingEvents = []; List _pastEvents = []; @@ -149,6 +180,21 @@ class _ProfileScreenState extends State _profileImage = prefs.getString(profileImageKey) ?? prefs.getString('profileImage') ?? ''; + // AUTH-003/PROF-001: Eventify ID + _eventifyId = prefs.getString('eventify_id'); + + // PROF-004 partial: tier for avatar ring + _userTier = prefs.getString('user_tier') ?? prefs.getString('level'); + + // PROF-002: District + _district = prefs.getString('district'); + + // AUTH-005: District change cooldown + final districtChangedStr = prefs.getString('district_changed_at'); + if (districtChangedStr != null) { + _districtChangedAt = DateTime.tryParse(districtChangedStr); + } + await _loadEventsForProfile(prefs); if (mounted) setState(() {}); } @@ -266,6 +312,8 @@ class _ProfileScreenState extends State } final String path = xfile.path; await _saveProfile(_username, _email, path); + // PROF-004: Upload to server on mobile + await _uploadProfilePhoto(path); } catch (e) { debugPrint('Image pick error: $e'); ScaffoldMessenger.of(context) @@ -273,6 +321,77 @@ class _ProfileScreenState extends State } } + // PROF-004: Upload profile photo to server + Future _uploadProfilePhoto(String filePath) async { + try { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('access_token') ?? ''; + if (token.isEmpty) return; + final request = http.MultipartRequest( + 'PATCH', + Uri.parse('${ApiEndpoints.baseUrl}/user/update-profile/'), + ); + request.headers['Authorization'] = 'Bearer $token'; + request.files.add(await http.MultipartFile.fromPath('profile_picture', filePath)); + final response = await request.send(); + if (response.statusCode == 200) { + final body = await response.stream.bytesToString(); + final data = jsonDecode(body) as Map; + if (data['profile_picture'] != null) { + final newUrl = data['profile_picture'].toString(); + final prefs2 = await SharedPreferences.getInstance(); + final currentEmail = prefs2.getString('current_email') ?? prefs2.getString('email') ?? ''; + final profileImageKey = currentEmail.isNotEmpty ? 'profileImage_$currentEmail' : 'profileImage'; + await prefs2.setString(profileImageKey, newUrl); + if (mounted) setState(() => _profileImage = newUrl); + } + } + } catch (e) { + debugPrint('Photo upload error: $e'); + } + } + + // PROF-002: Update district via API with cooldown check + Future _updateDistrict(String district) async { + if (_districtLocked) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('District locked until $_districtNextChange')), + ); + } + return; + } + try { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('access_token') ?? ''; + final response = await http.patch( + Uri.parse('${ApiEndpoints.baseUrl}/user/update-profile/'), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + body: jsonEncode({'district': district}), + ); + if (response.statusCode == 200) { + final now = DateTime.now(); + await prefs.setString('district', district); + await prefs.setString('district_changed_at', now.toIso8601String()); + if (mounted) { + setState(() { + _district = district; + _districtChangedAt = now; + }); + } + } + } catch (e) { + debugPrint('District update error: $e'); + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(userFriendlyError(e)))); + } + } + } + Future _enterAssetPathDialog() async { final ctl = TextEditingController(text: _profileImage); final result = await showDialog( @@ -420,6 +539,87 @@ class _ProfileScreenState extends State style: theme.textTheme.bodySmall ?.copyWith(color: theme.hintColor), ), + const SizedBox(height: 20), + + // PROF-002: District picker + Align( + alignment: Alignment.centerLeft, + child: Text( + 'District', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(height: 8), + + // AUTH-005: Cooldown lock indicator + if (_districtLocked) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + const Icon(Icons.lock_outline, + size: 14, color: Colors.amber), + const SizedBox(width: 4), + Text( + 'District locked until $_districtNextChange', + style: const TextStyle( + fontSize: 12, color: Colors.amber), + ), + ], + ), + ), + + // District pill grid + StatefulBuilder( + builder: (ctx2, setInner) { + return Wrap( + spacing: 8, + runSpacing: 8, + children: _districts.map((d) { + final isSelected = _district == d; + return GestureDetector( + onTap: _districtLocked + ? null + : () async { + setInner(() {}); + await _updateDistrict(d); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: isSelected + ? const Color(0xFF3B82F6) + : theme.cardColor, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected + ? const Color(0xFF3B82F6) + : theme.dividerColor, + ), + ), + child: Text( + d, + style: TextStyle( + fontSize: 12, + color: isSelected + ? Colors.white + : theme.textTheme.bodyMedium?.color, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + ), + ), + ), + ); + }).toList(), + ); + }, + ), + const SizedBox(height: 8), ], ), ), @@ -433,54 +633,22 @@ class _ProfileScreenState extends State }); } - // ───────── Avatar builder (reused, with size param) ───────── + // ───────── Avatar builder (AUTH-006 / PROF-004: DiceBear via TierAvatarRing) ───────── Widget _buildProfileAvatar({double size = 96}) { final path = _profileImage.trim(); + // Resolve a network-compatible URL: http URLs pass through directly, + // file paths and assets fall back to null so DiceBear is used. + String? imageUrl; if (path.startsWith('http')) { - return ClipOval( - child: CachedNetworkImage( - imageUrl: path, - memCacheWidth: (size * 2).toInt(), - memCacheHeight: (size * 2).toInt(), - width: size, - height: size, - fit: BoxFit.cover, - placeholder: (_, __) => - Container(width: size, height: size, color: const Color(0xFFE5E7EB)), - errorWidget: (_, __, ___) => - Icon(Icons.person, size: size / 2, color: Colors.grey))); + imageUrl = path; } - if (kIsWeb) { - return ClipOval( - child: Image.asset( - path.isNotEmpty ? path : 'assets/images/profile.jpg', - width: size, - height: size, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => - Icon(Icons.person, size: size / 2, color: Colors.grey))); - } - if (path.isNotEmpty && - (path.startsWith('/') || path.contains(Platform.pathSeparator))) { - final file = File(path); - if (file.existsSync()) { - return ClipOval( - child: Image.file(file, - width: size, - height: size, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => - Icon(Icons.person, size: size / 2, color: Colors.grey))); - } - } - return ClipOval( - child: Image.asset('assets/images/profile.jpg', - width: size, - height: size, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => - Icon(Icons.person, size: size / 2, color: Colors.grey))); + return TierAvatarRing( + username: _username.isNotEmpty ? _username : _email, + tier: _userTier ?? '', + size: size, + imageUrl: imageUrl, + ); } // ───────── Event list tile (updated styling) ───────── @@ -636,7 +804,34 @@ class _ProfileScreenState extends State child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(width: 40), // balance + // Share rank card button + Consumer( + builder: (context, gam, _) => GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (_) => Dialog( + backgroundColor: Colors.transparent, + child: ShareRankCard( + username: _username, + tier: gam.currentUserStats?.level ?? _userTier ?? '', + rank: gam.currentUserStats?.rank ?? 0, + ep: gam.profile?.currentEp ?? 0, + rewardPoints: gam.profile?.currentRp ?? 0, + ), + ), + ); + }, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: BorderRadius.circular(10)), + child: const Icon(Icons.share_outlined, color: Colors.white), + ), + ), + ), const Spacer(), Text( 'Profile', @@ -927,6 +1122,51 @@ class _ProfileScreenState extends State ), ), ), + const SizedBox(height: 6), + + // AUTH-003 / PROF-001: Eventify ID badge + if (_eventifyId != null && _eventifyId!.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: _eventifyId!)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Eventify ID copied'), + duration: Duration(seconds: 2), + ), + ); + }, + child: Container( + margin: const EdgeInsets.only(top: 2), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFF1E3A8A).withOpacity(0.3), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: const Color(0xFF3B82F6).withOpacity(0.5)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.badge_outlined, + size: 12, color: Color(0xFF93C5FD)), + const SizedBox(width: 4), + Text( + _eventifyId!, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF93C5FD), + fontFamily: 'monospace', + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + ), const SizedBox(height: 8), // Email @@ -1581,7 +1821,10 @@ class _ProfileScreenState extends State return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, // CustomScrollView: only visible event cards are built — no full-tree Column renders - body: CustomScrollView( + // SafeArea(bottom: false) — bottom is handled by home screen's floating nav buffer + body: SafeArea( + bottom: false, + child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ // Header gradient + Profile card overlap (same visual as before) @@ -1667,6 +1910,7 @@ class _ProfileScreenState extends State const SliverToBoxAdapter(child: SizedBox(height: 32)), ], ), + ), ); } @@ -1688,7 +1932,56 @@ class _ProfileScreenState extends State children: [ _gamStatCard('Lifetime EP', '${p.lifetimeEp}', Icons.star, const Color(0xFFF59E0B), theme), const SizedBox(width: 10), - _gamStatCard('Liquid EP', '${p.currentEp}', Icons.bolt, const Color(0xFF3B82F6), theme), + // GAM-003 + GAM-004: Liquid EP with cycle countdown and progress bar + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + decoration: BoxDecoration( + color: const Color(0xFF3B82F6).withOpacity(0.08), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFF3B82F6).withOpacity(0.2)), + ), + child: Column( + children: [ + const Icon(Icons.bolt, color: Color(0xFF3B82F6), size: 22), + const SizedBox(height: 4), + Text( + '${p.currentEp}', + style: TextStyle(fontWeight: FontWeight.w800, fontSize: 16, color: theme.textTheme.bodyLarge?.color), + ), + const SizedBox(height: 2), + Text('Liquid EP', style: TextStyle(color: theme.hintColor, fontSize: 10), textAlign: TextAlign.center), + if (gp.currentUserStats?.rewardCycleDays != null) ...[ + const SizedBox(height: 4), + Text( + 'Converts in ${gp.currentUserStats!.rewardCycleDays}d', + style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)), + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Builder( + builder: (_) { + final days = gp.currentUserStats?.rewardCycleDays ?? 30; + final elapsed = (30 - days).clamp(0, 30); + final ratio = elapsed / 30; + return LinearProgressIndicator( + value: ratio, + minHeight: 4, + backgroundColor: Colors.white12, + valueColor: AlwaysStoppedAnimation( + ratio > 0.7 ? const Color(0xFFFBBF24) : const Color(0xFF3B82F6), + ), + ); + }, + ), + ), + ], + ], + ), + ), + ), const SizedBox(width: 10), _gamStatCard('Reward Pts', '${p.currentRp}', Icons.redeem, const Color(0xFF10B981), theme), ], diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 4a2c7ef..9d393c8 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -1,6 +1,8 @@ // lib/screens/search_screen.dart +import 'dart:convert'; import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../core/utils/error_utils.dart'; // Location packages @@ -46,50 +48,41 @@ class _SearchScreenState extends State { 'Kottayam', ]; - /// Searchable location database – Kerala towns/cities with pincodes. - static const List<_LocationItem> _locationDb = [ - _LocationItem(city: 'Thiruvananthapuram', district: 'Thiruvananthapuram', pincode: '695001'), - _LocationItem(city: 'Kazhakoottam', district: 'Thiruvananthapuram', pincode: '695582'), - _LocationItem(city: 'Neyyattinkara', district: 'Thiruvananthapuram', pincode: '695121'), - _LocationItem(city: 'Attingal', district: 'Thiruvananthapuram', pincode: '695101'), - _LocationItem(city: 'Kochi', district: 'Ernakulam', pincode: '682001'), - _LocationItem(city: 'Ernakulam', district: 'Ernakulam', pincode: '682011'), - _LocationItem(city: 'Aluva', district: 'Ernakulam', pincode: '683101'), - _LocationItem(city: 'Kakkanad', district: 'Ernakulam', pincode: '682030'), - _LocationItem(city: 'Fort Kochi', district: 'Ernakulam', pincode: '682001'), - _LocationItem(city: 'Kozhikode', district: 'Kozhikode', pincode: '673001'), - _LocationItem(city: 'Feroke', district: 'Kozhikode', pincode: '673631'), - _LocationItem(city: 'Kollam', district: 'Kollam', pincode: '691001'), - _LocationItem(city: 'Karunagappally', district: 'Kollam', pincode: '690518'), - _LocationItem(city: 'Thrissur', district: 'Thrissur', pincode: '680001'), - _LocationItem(city: 'Chavakkad', district: 'Thrissur', pincode: '680506'), - _LocationItem(city: 'Guruvayoor', district: 'Thrissur', pincode: '680101'), - _LocationItem(city: 'Irinjalakuda', district: 'Thrissur', pincode: '680121'), - _LocationItem(city: 'Kannur', district: 'Kannur', pincode: '670001'), - _LocationItem(city: 'Thalassery', district: 'Kannur', pincode: '670101'), - _LocationItem(city: 'Alappuzha', district: 'Alappuzha', pincode: '688001'), - _LocationItem(city: 'Cherthala', district: 'Alappuzha', pincode: '688524'), - _LocationItem(city: 'Palakkad', district: 'Palakkad', pincode: '678001'), - _LocationItem(city: 'Ottapalam', district: 'Palakkad', pincode: '679101'), - _LocationItem(city: 'Malappuram', district: 'Malappuram', pincode: '676505'), - _LocationItem(city: 'Manjeri', district: 'Malappuram', pincode: '676121'), - _LocationItem(city: 'Tirur', district: 'Malappuram', pincode: '676101'), - _LocationItem(city: 'Kottayam', district: 'Kottayam', pincode: '686001'), - _LocationItem(city: 'Pala', district: 'Kottayam', pincode: '686575'), - _LocationItem(city: 'Pathanamthitta', district: 'Pathanamthitta', pincode: '689645'), - _LocationItem(city: 'Idukki', district: 'Idukki', pincode: '685602'), - _LocationItem(city: 'Munnar', district: 'Idukki', pincode: '685612'), - _LocationItem(city: 'Wayanad', district: 'Wayanad', pincode: '673121'), - _LocationItem(city: 'Kalpetta', district: 'Wayanad', pincode: '673121'), - _LocationItem(city: 'Kasaragod', district: 'Kasaragod', pincode: '671121'), - _LocationItem(city: 'Whitefield', district: 'Bengaluru', pincode: '560066'), - _LocationItem(city: 'Bengaluru', district: 'Karnataka', pincode: '560001'), - ]; + /// Searchable location database – loaded from assets/data/kerala_pincodes.json. + List<_LocationItem> _locationDb = []; + bool _pinsLoaded = false; List<_LocationItem> _searchResults = []; bool _showSearchResults = false; bool _loadingLocation = false; + @override + void initState() { + super.initState(); + _loadKeralaData(); + } + + Future _loadKeralaData() async { + if (_pinsLoaded) return; + try { + final jsonStr = await rootBundle.loadString('assets/data/kerala_pincodes.json'); + final List list = jsonDecode(jsonStr); + final loaded = list.map((e) => _LocationItem( + city: e['city'] as String, + district: e['district'] as String?, + pincode: e['pincode'] as String?, + )).toList(); + if (mounted) { + setState(() { + _locationDb = loaded; + _pinsLoaded = true; + }); + } + } catch (_) { + // fallback: keep empty list, search won't crash + } + } + @override void dispose() { _ctrl.dispose(); diff --git a/lib/widgets/eventify_bottom_sheet.dart b/lib/widgets/eventify_bottom_sheet.dart new file mode 100644 index 0000000..939fb45 --- /dev/null +++ b/lib/widgets/eventify_bottom_sheet.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; + +void showEventifyBottomSheet( + BuildContext context, { + required String title, + required Widget child, + double initialSize = 0.5, + double minSize = 0.3, + double maxSize = 0.9, + bool isDismissible = true, +}) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: isDismissible, + backgroundColor: Colors.transparent, + builder: (_) => DraggableScrollableSheet( + initialChildSize: initialSize, + minChildSize: minSize, + maxChildSize: maxSize, + expand: false, + builder: (_, scrollController) => _EventifyBottomSheetContent( + title: title, + child: child, + scrollController: scrollController, + ), + ), + ); +} + +class _EventifyBottomSheetContent extends StatelessWidget { + const _EventifyBottomSheetContent({ + required this.title, + required this.child, + required this.scrollController, + }); + + final String title; + final Widget child; + final ScrollController scrollController; + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Color(0xFF0F172A), + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 12), + Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white54), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ), + const Divider(color: Colors.white12, height: 1), + Expanded( + child: SingleChildScrollView( + controller: scrollController, + child: child, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/glass_card.dart b/lib/widgets/glass_card.dart new file mode 100644 index 0000000..79238a7 --- /dev/null +++ b/lib/widgets/glass_card.dart @@ -0,0 +1,53 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; + +class GlassCard extends StatelessWidget { + const GlassCard({ + super.key, + required this.child, + this.padding = const EdgeInsets.all(16), + this.margin, + this.borderRadius = 16, + this.blur = 10, + this.backgroundColor, + this.borderColor, + }); + + final Widget child; + final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry? margin; + final double borderRadius; + final double blur; + final Color? backgroundColor; + final Color? borderColor; + + @override + Widget build(BuildContext context) { + final effectiveBackground = + backgroundColor ?? const Color(0xFF1E293B).withOpacity(0.6); + final effectiveBorder = + borderColor ?? Colors.white.withOpacity(0.08); + + Widget card = ClipRRect( + borderRadius: BorderRadius.circular(borderRadius), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur), + child: Container( + padding: padding, + decoration: BoxDecoration( + color: effectiveBackground, + borderRadius: BorderRadius.circular(borderRadius), + border: Border.all(color: effectiveBorder, width: 1), + ), + child: child, + ), + ), + ); + + if (margin != null) { + return Container(margin: margin, child: card); + } + + return card; + } +} diff --git a/lib/widgets/tier_avatar_ring.dart b/lib/widgets/tier_avatar_ring.dart new file mode 100644 index 0000000..2036ea3 --- /dev/null +++ b/lib/widgets/tier_avatar_ring.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; + +class TierAvatarRing extends StatelessWidget { + final String username; + final String tier; + final double size; + final bool showDiceBear; + final String? imageUrl; + final VoidCallback? onTap; + + static const Map _tierColors = { + 'Bronze': Color(0xFFFED7AA), + 'Silver': Color(0xFFE2E8F0), + 'Gold': Color(0xFFFEF3C7), + 'Platinum': Color(0xFFEDE9FE), + 'Diamond': Color(0xFFE0E7FF), + }; + + static const Color _fallbackColor = Color(0xFF475569); + + const TierAvatarRing({ + super.key, + required this.username, + required this.tier, + this.size = 48.0, + this.showDiceBear = true, + this.imageUrl, + this.onTap, + }); + + Color get _ringColor => _tierColors[tier] ?? _fallbackColor; + + String get _avatarUrl { + if (imageUrl != null && imageUrl!.isNotEmpty) { + return imageUrl!; + } + return 'https://api.dicebear.com/9.x/notionists/svg?seed=$username'; + } + + Widget _buildAvatar() { + final double radius = size / 2 - 5; + + if (!showDiceBear) { + return CircleAvatar( + radius: radius, + backgroundColor: const Color(0xFF1E293B), + child: Icon( + Icons.person, + color: Colors.white54, + size: size * 0.5, + ), + ); + } + + return CachedNetworkImage( + imageUrl: _avatarUrl, + imageBuilder: (context, imageProvider) => CircleAvatar( + radius: radius, + backgroundImage: imageProvider, + ), + placeholder: (context, url) => CircleAvatar( + radius: radius, + backgroundColor: const Color(0xFF1E293B), + child: SizedBox( + width: size * 0.4, + height: size * 0.4, + child: const CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white38, + ), + ), + ), + errorWidget: (context, url, error) => CircleAvatar( + radius: radius, + backgroundColor: const Color(0xFF1E293B), + child: Icon( + Icons.person, + color: Colors.white54, + size: size * 0.5, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final Color ringColor = _ringColor; + final double containerSize = size + 6; + + final Widget ring = Container( + width: containerSize, + height: containerSize, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: ringColor, width: 3), + boxShadow: [ + BoxShadow( + color: ringColor.withOpacity(0.4), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ), + child: Center(child: _buildAvatar()), + ); + + if (onTap != null) { + return GestureDetector( + onTap: onTap, + child: ring, + ); + } + + return ring; + } +} diff --git a/pubspec.lock b/pubspec.lock index 46d3c3a..ce93622 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -641,7 +641,7 @@ packages: source: hosted version: "1.1.0" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" diff --git a/pubspec.yaml b/pubspec.yaml index 7ff5f11..99cbfd7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: google_maps_flutter: ^2.5.0 url_launcher: ^6.2.1 share_plus: ^7.2.1 + path_provider: ^2.1.0 provider: ^6.1.2 video_player: ^2.8.1 cached_network_image: ^3.3.1 @@ -39,6 +40,7 @@ flutter: - assets/images/ - assets/icon/hand_stop.svg - assets/login-bg.mp4 + - assets/data/kerala_pincodes.json fonts: - family: Gilroy fonts: