Skip to content

5 logic gamification competitive leaderboard - #18

Merged
Shaam143 merged 8 commits into
mainfrom
5-logic-gamification-competitive-leaderboard
Feb 25, 2026
Merged

5 logic gamification competitive leaderboard#18
Shaam143 merged 8 commits into
mainfrom
5-logic-gamification-competitive-leaderboard

Conversation

@SNFASA

@SNFASA SNFASA commented Feb 24, 2026

Copy link
Copy Markdown
Owner

History page backend and frontend

@SNFASA
SNFASA requested a review from Copilot February 24, 2026 17:40
@SNFASA SNFASA self-assigned this Feb 24, 2026
@SNFASA SNFASA added ui Flutter frontend backend Firebase & Cloud Functions labels Feb 24, 2026

@SNFASA SNFASA left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no conflict

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements a history page feature for tracking user scan activities, along with several code quality improvements including updating deprecated opacity APIs throughout the codebase. The PR adds new backend infrastructure (repository and controller) for real-time scan history streaming, updates the navigation to include a History tab, and improves weekly points tracking logic with reset functionality.

Changes:

  • Added History screen with real-time scan tracking and today's scan count display
  • Implemented backend support with HistoryRepository, HistoryController, and updates to ScanRepository for weekly reset logic
  • Replaced deprecated withOpacity() API calls with withValues(alpha:) across multiple UI files
  • Fixed division-by-zero bug in analytics screen and improved type safety in analytics controller

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pubspec.yaml Added intl package dependency for date formatting
pubspec.lock Updated lock file with intl package details
lib/features/home/ui/text.txt Contains sensitive test data that should be removed
lib/features/home/ui/history_screen.dart New screen displaying user's scan history with real-time updates
lib/features/home/repositories/history_repository.dart New repository for streaming scan history from Firestore
lib/features/home/controllers/history_controller.dart New controller managing history state and today's scan count logic
lib/features/home/repositories/scan_repository.dart Added weekly reset logic for leaderboard points tracking
lib/features/home/models/user_model.dart Added lastScanWeekId field for tracking weekly resets
lib/features/home/ui/home_screen.dart Added History navigation destination, contains dead logout code
lib/features/home/ui/scoreboard_screen.dart Updated opacity API calls to use withValues
lib/features/home/ui/template_screen.dart Updated opacity API calls to use withValues
lib/features/home/ui/camera_screen.dart Updated opacity API calls to use withValues
lib/features/home/ui/analytics_screen.dart Fixed division-by-zero bug, updated opacity API calls
lib/features/home/controllers/analytics_controller.dart Improved type safety with explicit type conversions
lib/features/auth/ui/welcome_screen.dart Updated opacity API calls to use withValues
lib/features/auth/ui/register_screen.dart Updated opacity API calls to use withValues
lib/core/widgets/smart_result_modal.dart Updated opacity API calls to use withValues
lib/core/widgets/impact_row.dart Updated opacity API calls to use withValues
Comments suppressed due to low confidence (3)

lib/features/home/ui/text.txt:289

  • This file contains what appears to be debug/test data including a real email address and personal information (nabil.0413@gmail.com, "Syed Nabil"). This sensitive data should not be committed to the repository. Additionally, this appears to be sample Firestore document data that should either be removed or placed in a proper documentation or test fixtures location.
    lib/features/home/ui/text.txt:280
  • The dates in this test/debug data (February 19, 2026 and February 19, 2026) are in the future. While this may be intentional for testing, it's worth noting that future dates could cause issues with date-based logic or comparisons if this data is used anywhere in the application.
    lib/features/home/ui/home_screen.dart:56
  • Dead code: The comment says "Logout index = 5" but the check is for index == 6. Since there are only 6 navigation destinations (indices 0-5), this condition will never be true. The logout functionality has been moved to the Profile screen, so this entire block (lines 27-56) can be removed.
    // Logout index = 5
    if (index == 6) {
      final confirm = await showDialog<bool>(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('Logout'),
          content: const Text('Are you sure you want to logout?'),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context, false),
              child: const Text('Cancel'),
            ),
            ElevatedButton(
              onPressed: () => Navigator.pop(context, true),
              child: const Text('Logout'),
            ),
          ],
        ),
      );

      if (confirm == true) {
        await ref.read(authProvider.notifier).logout();

        Navigator.of(context).pushNamedAndRemoveUntil(
          '/login',
          (route) => false,
        );
      }
      return;
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -0,0 +1,270 @@
import 'package:flutter/material.dart';

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title "5 logic gamification competitive leaderboard" doesn't match the actual changes which primarily implement a history page backend and frontend. The title should be updated to better reflect the actual changes, such as "Add history page with scan tracking" or similar.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment thread lib/features/home/ui/history_screen.dart
Comment on lines +19 to +65
try {
// Fetch the current user data to check the last recorded week
final userDoc = await userRef.get();

WriteBatch batch = _firestore.batch();

// 1. Save scan details
batch.set(scanRef, scan.toMap());
// 1. Save individual scan details
batch.set(scanRef, scan.toMap());

// 2. Update user's aggregate stats and check week reset logic
// Inside ScanRepository.saveScan()
batch.update(userRef, {
'categoryCounts.${scan.category}': FieldValue.increment(1), // Updates the specific category
'totalScans': FieldValue.increment(1),
'ecoPoints': FieldValue.increment(scan.pointsEarned),
'co2Offset': FieldValue.increment(scan.co2Saved),
});
// 2. Weekly Reset Logic
// We check if the 'lastScanWeekId' in Firestore matches the scan's current 'weekId'
bool isNewWeek = true;
if (userDoc.exists) {
final data = userDoc.data() as Map<String, dynamic>;
final String lastWeekId = data['lastScanWeekId'] ?? "";

// If the stored week ID matches the current scan's week ID, it's NOT a new week
if (lastWeekId == scan.weekId) {
isNewWeek = false;
}
}

await batch.commit();
// 3. Update user's aggregate stats
if (isNewWeek) {
// RESET: Start weekly points fresh for the new week
batch.update(userRef, {
'categoryCounts.${scan.category}': FieldValue.increment(1),
'totalScans': FieldValue.increment(1),
'ecoPoints': FieldValue.increment(scan.pointsEarned),
'weeklyPoints': scan.pointsEarned, // Set to current scan points (Overwrite old week)
'co2Offset': FieldValue.increment(scan.co2Saved),
'lastScanWeekId': scan.weekId, // Update the tracking week ID
});
} else {
// INCREMENT: Add to existing weekly points
batch.update(userRef, {
'categoryCounts.${scan.category}': FieldValue.increment(1),
'totalScans': FieldValue.increment(1),
'ecoPoints': FieldValue.increment(scan.pointsEarned),
'weeklyPoints': FieldValue.increment(scan.pointsEarned), // Correctly updating the scoreboard
'co2Offset': FieldValue.increment(scan.co2Saved),
'lastScanWeekId': scan.weekId,
});
}

// Commit all changes at once
await batch.commit();

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential race condition: The code performs a read operation (userRef.get()) followed by a batch update. If two scans are submitted nearly simultaneously in a new week, both could read the old lastScanWeekId, both determine it's a new week, and both could reset weeklyPoints instead of one resetting and one incrementing. Consider using a Firestore transaction instead of a batch operation to ensure atomic read-modify-write.

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +33
final Map<String, dynamic> rawData = Map<String, dynamic>.from(data);
return Map<String, double>.from(rawData.map((key, value) => MapEntry(key, (value as num).toDouble())));

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary type conversion: The code converts data to Map<String, dynamic> and then converts back. The intermediate rawData variable adds complexity without clear benefit. Consider simplifying to: return Map<String, double>.from(data.map((key, value) => MapEntry(key, (value as num).toDouble())));

Suggested change
final Map<String, dynamic> rawData = Map<String, dynamic>.from(data);
return Map<String, double>.from(rawData.map((key, value) => MapEntry(key, (value as num).toDouble())));
return Map<String, double>.from(
data.map((key, value) => MapEntry(key, (value as num).toDouble())),
);

Copilot uses AI. Check for mistakes.

Copilot AI commented Feb 24, 2026

Copy link
Copy Markdown

@SNFASA I've opened a new pull request, #19, to work on those changes. Once the pull request is ready, I'll request review from you.

@SNFASA SNFASA left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Shaam143 @ferozpoloz @al-qis

tinggal full testing and masukkan API key Google Maps (android/ios)

android:name="flutterEmbedding"
android:value="2" />
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="YOUR_ANDROID_API_KEY_HERE"/>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Shaam143 nanti letak API Google maps ( Android)

_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("YOUR_iOS_API_KEY_HERE")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Shaam143 API Key Google maps (IOS)

@SNFASA
SNFASA requested a review from Shaam143 February 24, 2026 20:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 37 changed files in this pull request and generated 12 comments.

Comments suppressed due to low confidence (1)

lib/features/home/ui/text.txt:216

  • This file contains what looks like a Firestore export/debug dump including personal data (email, name) and timestamps inside the lib/ tree. This is sensitive data and should not be committed; remove it from the repo (or move sanitized example data into documentation/tests if needed).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +33 to +37
data: (centers) => centers.map((c) => Marker(
markerId: MarkerId(c.name),
position: LatLng(c.location!.latitude, c.location!.longitude),
infoWindow: InfoWindow(title: c.name, snippet: c.type),
)).toSet(),

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

location is nullable in RecyclingCenterModel, but the marker builder force-unwraps it (c.location!). If any center doc is missing location, this will throw during map render. Filter out centers with null locations or provide a fallback before creating LatLng.

Suggested change
data: (centers) => centers.map((c) => Marker(
markerId: MarkerId(c.name),
position: LatLng(c.location!.latitude, c.location!.longitude),
infoWindow: InfoWindow(title: c.name, snippet: c.type),
)).toSet(),
data: (centers) => centers
.where((c) => c.location != null)
.map((c) => Marker(
markerId: MarkerId(c.name),
position: LatLng(c.location!.latitude, c.location!.longitude),
infoWindow: InfoWindow(title: c.name, snippet: c.type),
))
.toSet(),

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +41
if (!serviceEnabled) return Future.error('Location services are disabled.');

LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCurrentLocation() requests permission only when denied but doesn't handle deniedForever or the case where the user still denies after requesting. As written, it will call getCurrentPosition() even without permission, which can throw. Add explicit handling for deniedForever and re-check permission after the request.

Suggested change
if (!serviceEnabled) return Future.error('Location services are disabled.');
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (!serviceEnabled) {
return Future.error('Location services are disabled.');
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.denied) {
return Future.error('Location permissions are denied');
}
if (permission == LocationPermission.deniedForever) {
return Future.error(
'Location permissions are permanently denied, we cannot request permissions.',
);
}

Copilot uses AI. Check for mistakes.
android:name="flutterEmbedding"
android:value="2" />
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="YOUR_ANDROID_API_KEY_HERE"/>

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commits a Google Maps API key placeholder into the manifest. API keys should be provided via build config/CI secrets (e.g., manifest placeholder, gradle resValue, or injected string resource) and not stored directly in source-controlled XML.

Suggested change
android:value="YOUR_ANDROID_API_KEY_HERE"/>
android:value="${MAPS_API_KEY}"/>

Copilot uses AI. Check for mistakes.
Comment on lines +25 to 40
Future<void> changePassword({required String currentPassword, required String newPassword}) async {
final user = FirebaseAuth.instance.currentUser;
if (user == null || user.email == null) throw "User not found";

// 1. Re-authenticate the user (Required by Firebase for security)
AuthCredential credential = EmailAuthProvider.credential(
email: user.email!,
password: currentPassword,
);

await user.reauthenticateWithCredential(credential);

// 2. Update to new password
await user.updatePassword(newPassword);
}
}

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This password-change method bypasses the injected auth instance by using FirebaseAuth.instance, which makes the class harder to test and inconsistent with the rest of the repository. It also throws a raw string and has inconsistent formatting. Prefer using the injected auth, throwing an Exception type, and aligning formatting with the rest of the file.

Suggested change
Future<void> changePassword({required String currentPassword, required String newPassword}) async {
final user = FirebaseAuth.instance.currentUser;
if (user == null || user.email == null) throw "User not found";
// 1. Re-authenticate the user (Required by Firebase for security)
AuthCredential credential = EmailAuthProvider.credential(
email: user.email!,
password: currentPassword,
);
await user.reauthenticateWithCredential(credential);
// 2. Update to new password
await user.updatePassword(newPassword);
}
}
Future<void> changePassword({
required String currentPassword,
required String newPassword,
}) async {
final user = auth.currentUser;
if (user == null || user.email == null) {
throw Exception('User not found');
}
// 1. Re-authenticate the user (Required by Firebase for security)
final AuthCredential credential = EmailAuthProvider.credential(
email: user.email!,
password: currentPassword,
);
await user.reauthenticateWithCredential(credential);
// 2. Update to new password
await user.updatePassword(newPassword);
}
}

Copilot uses AI. Check for mistakes.
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:url_launcher/url_launcher.dart'; // Add this for Directions
import '../controllers/centers_controller.dart';
import '../models/recycleingcenter_model.dart';

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model file/import name recycleingcenter_model.dart appears misspelled ("recycleing"). Consider renaming to recycling_center_model.dart (and updating imports) to avoid propagating the typo across the codebase and to improve discoverability.

Suggested change
import '../models/recycleingcenter_model.dart';
import '../models/recycling_center_model.dart';

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +111
onPressed: () => _openDirections(center.location!.latitude, center.location!.longitude),
),
onTap: () => controller.animateToCenter(center.location!.latitude, center.location!.longitude),

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These callbacks force-unwrap center.location! (directions and onTap). Since location is nullable, this can crash when a center lacks coordinates. Consider disabling the directions button / tap handler (or showing an error) when location == null.

Suggested change
onPressed: () => _openDirections(center.location!.latitude, center.location!.longitude),
),
onTap: () => controller.animateToCenter(center.location!.latitude, center.location!.longitude),
onPressed: () {
final location = center.location;
if (location == null) {
debugPrint('Center "${center.name}" has no location; cannot open directions.');
return;
}
_openDirections(location.latitude, location.longitude);
},
),
onTap: () {
final location = center.location;
if (location == null) {
debugPrint('Center "${center.name}" has no location; cannot animate map to center.');
return;
}
controller.animateToCenter(location.latitude, location.longitude);
},

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +23
void setFilter(String category) {
selectedCategory = category;
ref.invalidateSelf(); // Refresh the stream and UI
}

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling ref.invalidateSelf() in setFilter restarts the Firestore stream subscription every time the user changes the chip filter, which is unnecessary work and can increase reads/flicker. Prefer keeping the centers stream stable and storing selectedCategory in separate state (e.g., a StateProvider) or in the provider state, then filter the already-streamed list.

Copilot uses AI. Check for mistakes.
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("YOUR_iOS_API_KEY_HERE")

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoding an API key placeholder in source code is risky and will also break maps unless manually edited per developer. Load the Google Maps API key from build configuration (e.g., Info.plist / xcconfig) and keep it out of version control; avoid committing placeholders in the app delegate.

Suggested change
GMSServices.provideAPIKey("YOUR_iOS_API_KEY_HERE")
if let apiKey = Bundle.main.object(forInfoDictionaryKey: "GOOGLE_MAPS_API_KEY") as? String,
!apiKey.isEmpty {
GMSServices.provideAPIKey(apiKey)
}

Copilot uses AI. Check for mistakes.
Comment on lines +19 to 24
try {
// Fetch the current user data to check the last recorded week
final userDoc = await userRef.get();

WriteBatch batch = _firestore.batch();

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weekly reset logic reads userRef.get() and then applies a batch update based on that snapshot. If multiple scans are saved concurrently (or from multiple devices), both calls can see the same lastScanWeekId and overwrite weeklyPoints, losing increments. Use a Firestore transaction (or other atomic server-side mechanism) so the week check + update happens atomically.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../controllers/history_controller.dart';

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR title ("5 logic gamification competitive leaderboard") doesn’t match the actual changes, which add a History screen, Recycling Centers map, and profile editing/password change flows. Consider renaming the PR to reflect the history/centers features so it’s easier to track in release notes and future git history.

Copilot uses AI. Check for mistakes.
@Shaam143
Shaam143 merged commit 724a03d into main Feb 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Firebase & Cloud Functions ui Flutter frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants