diff --git a/bu_passport/android/app/src/main/AndroidManifest.xml b/bu_passport/android/app/src/main/AndroidManifest.xml index 228c8b1..8197273 100644 --- a/bu_passport/android/app/src/main/AndroidManifest.xml +++ b/bu_passport/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,7 @@ + + + + NSLocationWhenInUseUsageDescription + We need your location to provide this feature + NSLocationAlwaysUsageDescription + We need your location to provide this feature + NSCameraUsageDescription This app requires camera access to allow users to take and upload photos for their profile. NSPhotoLibraryUsageDescription diff --git a/bu_passport/lib/classes/event.dart b/bu_passport/lib/classes/event.dart index fca10d3..dfa7edd 100644 --- a/bu_passport/lib/classes/event.dart +++ b/bu_passport/lib/classes/event.dart @@ -1,3 +1,5 @@ +import 'package:bu_passport/classes/session.dart'; +import 'package:bu_passport/classes/sticker.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; class Event { @@ -7,21 +9,66 @@ class Event { final String eventLocation; final String eventDescription; final String eventURL; - final DateTime eventStartTime; - final DateTime eventEndTime; final int eventPoints; final List savedUsers; + final List eventSessions; + final List eventStickers; Event({ required this.eventID, required this.eventTitle, required this.eventPhoto, required this.eventLocation, - required this.eventStartTime, - required this.eventEndTime, required this.eventDescription, required this.eventPoints, required this.eventURL, required this.savedUsers, + required this.eventSessions, + required this.eventStickers, }); + + @override + String toString() { + return 'NewEvent\n' + '(ID: $eventID,\n' + ' Title: $eventTitle, \n' + 'Photo URL: $eventPhoto, \n' + 'Location: $eventLocation, \n' + //'Description: $eventDescription, \n' + 'Points: $eventPoints, \n' + 'Event URL: $eventURL, \n' + 'Sessions: [\n${eventSessions.map((session) => ' ${session.toString()}').join(',\n')}\n])'; + } + + bool hasSessionOnDay(DateTime date) { + return eventSessions.any((session) { + final startOfDay = DateTime(date.year, date.month, date.day); + final endOfDay = startOfDay.add(Duration(days: 1)); + + return (session.sessionStartTime.isAfter(startOfDay) && + session.sessionStartTime.isBefore(endOfDay)) || + (session.sessionEndTime.isAfter(startOfDay) && + session.sessionEndTime.isBefore(endOfDay)); + }); + } + + bool hasUpcomingSessions(DateTime time) { + + // Check if there are any sessions that haven't ended + for (var session in eventSessions) { + if (session.sessionEndTime.isAfter(time)) { + return true; // There's at least one active session + } + } + return false; // No active sessions + } + bool isEventHappening() { + DateTime now = DateTime.now(); + for (var session in eventSessions) { + if (session.sessionStartTime.isBefore(now) && session.sessionEndTime.isAfter(now)) { + return true; // Event is happening + } + } + return false; // Event is not happening + } } diff --git a/bu_passport/lib/classes/session.dart b/bu_passport/lib/classes/session.dart new file mode 100644 index 0000000..6e3313f --- /dev/null +++ b/bu_passport/lib/classes/session.dart @@ -0,0 +1,25 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class Session { + final String sessionID; + final DateTime sessionStartTime; + final DateTime sessionEndTime; + final List savedUsers; + + Session({ + required this.sessionID, + required this.sessionStartTime, + required this.sessionEndTime, + required this.savedUsers, + + }); + @override + String toString() { + return 'Session(' + 'ID: $sessionID, ' + 'Start Time: $sessionStartTime, ' + 'End Time: $sessionEndTime, ' + 'Saved Users: $savedUsers' + ')'; + } +} diff --git a/bu_passport/lib/classes/sticker.dart b/bu_passport/lib/classes/sticker.dart new file mode 100644 index 0000000..e92b63d --- /dev/null +++ b/bu_passport/lib/classes/sticker.dart @@ -0,0 +1,15 @@ +class Sticker { + final String name; + + Sticker({ + required this.name, + }); + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is Sticker && other.name == name; + } + + @override + int get hashCode => name.hashCode; +} diff --git a/bu_passport/lib/classes/user.dart b/bu_passport/lib/classes/user.dart index 97fadc5..bd2dee1 100644 --- a/bu_passport/lib/classes/user.dart +++ b/bu_passport/lib/classes/user.dart @@ -1,3 +1,5 @@ +import 'package:bu_passport/classes/sticker.dart'; + class Users { final String firstName; final String lastName; @@ -10,6 +12,7 @@ class Users { final String userProfileURL; final Map userSavedEvents; final bool admin; + final Map userStickerCollection; Users({ required this.firstName, @@ -23,5 +26,6 @@ class Users { required this.userPoints, required this.userProfileURL, required this.admin, + required this.userStickerCollection, }); } diff --git a/bu_passport/lib/components/checkin_options_dialog.dart b/bu_passport/lib/components/checkin_options_dialog.dart new file mode 100644 index 0000000..679b867 --- /dev/null +++ b/bu_passport/lib/components/checkin_options_dialog.dart @@ -0,0 +1,22 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class CheckInOptionsDialog extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text("Check-in Options"), + content: Text("Would you like to check in with a photo?"), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text("No"), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text("Yes"), + ), + ], + ); + } +} \ No newline at end of file diff --git a/bu_passport/lib/components/checkin_success_dialog.dart b/bu_passport/lib/components/checkin_success_dialog.dart new file mode 100644 index 0000000..18da564 --- /dev/null +++ b/bu_passport/lib/components/checkin_success_dialog.dart @@ -0,0 +1,81 @@ +import 'dart:io'; +import 'package:bu_passport/components/sticker_painters.dart'; +import 'package:flutter/material.dart'; +import 'postmark_widget.dart'; +import 'dart:ui' as ui; + + + + +class SuccessDialog extends StatelessWidget { + final String eventTitle; + final int points; + final ui.Image? image; + final ui.Image? logo; + final ui.Image? frame; + final ui.Image? sticker1; + final ui.Image? sticker2; + static const double STAMP_SIZE=100; + static const double POSTMARK_SIZE=100; + + const SuccessDialog({ + Key? key, + required this.eventTitle, + required this.points, + this.image, this.frame, this.sticker1, this.sticker2, this.logo, + }) : super(key: key); + + + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'SUCCESS!', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Color(0xFFCC0000), + fontFamily: 'Inter', + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + if (image != null) + CustomPaint( + size: Size(STAMP_SIZE,STAMP_SIZE), + painter: ImagePainter(image,frame,sticker1,sticker2), + ), + if(image==null) + CustomPaint( + size: const Size(STAMP_SIZE, STAMP_SIZE), + painter: LogoPainter(logo,sticker1,sticker2), + ), + const SizedBox(height: 16), + Text( + eventTitle, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + CustomPaint( + size: Size(POSTMARK_SIZE,POSTMARK_SIZE), + painter: PostmarkPainter(points: points),// points + ), + ], + ), + ), + ); + } +} diff --git a/bu_passport/lib/components/event_widget.dart b/bu_passport/lib/components/event_widget.dart index 9814725..618c756 100644 --- a/bu_passport/lib/components/event_widget.dart +++ b/bu_passport/lib/components/event_widget.dart @@ -1,12 +1,15 @@ import 'package:bu_passport/classes/event.dart'; +import 'package:bu_passport/components/time_span.dart'; import 'package:bu_passport/pages/event_page.dart'; -import 'package:bu_passport/services/firebase_service.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; +import '../services/firebase_service.dart'; +import '../services/web_image_service.dart'; + class EventWidget extends StatefulWidget { final Event event; final Function onUpdateEventPage; @@ -97,7 +100,7 @@ class _EventWidgetState extends State { borderRadius: BorderRadius.circular(10.0), border: Border.all(color: Colors.grey), image: DecorationImage( - image: AssetImage(widget.event.eventPhoto), + image: WebImageService.buildImageProvider(widget.event.eventPhoto), fit: BoxFit.cover, colorFilter: ColorFilter.mode( Colors.black.withOpacity(0.3), BlendMode.multiply), @@ -139,14 +142,8 @@ class _EventWidgetState extends State { ), ), SizedBox(height: sizedBoxHeight * 0.5), - Text( - DateFormat.yMMMd() - .format(widget.event.eventStartTime), - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.w500, - color: Colors.white, - ), + EventDateRangeDisplay( + sessions: widget.event.eventSessions, ), ], ), diff --git a/bu_passport/lib/components/postmark_widget.dart b/bu_passport/lib/components/postmark_widget.dart new file mode 100644 index 0000000..0f99e7f --- /dev/null +++ b/bu_passport/lib/components/postmark_widget.dart @@ -0,0 +1,112 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class PostmarkPainter extends CustomPainter { + final int points; // Added to accept points + + PostmarkPainter({required this.points}); + + @override + void paint(Canvas canvas, Size size) { + final double radius = size.width / 2; + final _color = Color(0xFFCC0000); + canvas.translate(radius, radius); + canvas.rotate(_degreeToRadian(-20)); + canvas.translate(-radius, -radius); + + // Paint object for the dashed outer circle + final Paint outerPaint = Paint() + ..color = _color + ..style = PaintingStyle.stroke + ..strokeWidth = 0.03*size.height; + + // Dashed circle logic + double dashWidth = 0.08*size.height; + double dashSpace = 0.07*size.height; + double angle = 0; + + while (angle < 360) { + final startX = radius + (radius - 0.05*size.height) * cos(_degreeToRadian(angle)); + final startY = radius + (radius - 0.05*size.height) * sin(_degreeToRadian(angle)); + final endX = radius + (radius - 0.05*size.height) * cos(_degreeToRadian(angle + dashWidth)); + final endY = radius + (radius - 0.05*size.height) * sin(_degreeToRadian(angle + dashWidth)); + + canvas.drawLine( + Offset(startX, startY), + Offset(endX, endY), + outerPaint, + ); + + angle += dashWidth + dashSpace; + } + + // Inner solid circle + final Paint innerPaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.fill; + + canvas.drawCircle(Offset(radius, radius), radius - 0.1*size.height, innerPaint); + + // Paint inner circle border + final Paint innerBorderPaint = Paint() + ..color = _color + ..style = PaintingStyle.stroke + ..strokeWidth = 0.02*size.height; + + canvas.drawCircle(Offset(radius, radius), radius - 0.12*size.height, innerBorderPaint); + + // Draw points text + final TextStyle textStyle = TextStyle( + fontFamily: 'Inter', + color: _color, + fontSize: 0.38*size.height, + fontWeight: FontWeight.bold, + ); + + final TextPainter textPainter = TextPainter( + text: TextSpan( + text: '$points', + style: textStyle, + ), + textDirection: TextDirection.ltr, + ); + + textPainter.layout(); + textPainter.paint( + canvas, + Offset( + (size.width - textPainter.width) / 2, + (size.height - textPainter.height) / 2 - 0.08*size.height, + ), + ); + + // Draw "pts" label + final TextStyle ptsStyle = TextStyle( + color: _color, + fontSize: 0.18*size.height, + fontWeight: FontWeight.bold, + ); + + final TextPainter ptsPainter = TextPainter( + text: TextSpan( + text: 'Pts', + style: ptsStyle, + ), + textDirection: TextDirection.ltr, + ); + + ptsPainter.layout(); + ptsPainter.paint( + canvas, + Offset( + (size.width - ptsPainter.width) / 2, + (size.height - ptsPainter.height) / 2 + 0.18*size.height, + ), + ); + } + + double _degreeToRadian(double degree) => degree * (pi / 180); + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/bu_passport/lib/components/sticker_painters.dart b/bu_passport/lib/components/sticker_painters.dart new file mode 100644 index 0000000..a2d2713 --- /dev/null +++ b/bu_passport/lib/components/sticker_painters.dart @@ -0,0 +1,152 @@ + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../classes/sticker.dart'; +import 'postmark_widget.dart'; +import 'dart:ui' as ui; +import 'dart:typed_data'; +import 'package:http/http.dart' as http; + + + + +class ImagePainter extends CustomPainter { + final ui.Image? image; + final Color _frameColor = Color(0xFFCC0000); // Frame color + final double _frameThickness = 8.0; // Thickness of the frame + final ui.Image? frame; + final ui.Image? sticker1; + final ui.Image? sticker2; + + + + ImagePainter(this.image, this.frame, this.sticker1, this.sticker2); + + + @override + void paint(Canvas canvas, Size size) { + + final double image_padding = size.height*0.167; + final double frame_padding = size.height*0.167; + + if(image!=null&&frame!=null){ + canvas.drawImageRect( + image!, + Rect.fromLTWH(0, 0, image!.width.toDouble(), image!.height.toDouble()), + Rect.fromLTWH(image_padding/2+frame_padding/2, image_padding/2+frame_padding/2, size.width-image_padding-frame_padding, size.height-image_padding-frame_padding), // Center the image by adding padding + Paint(), + ); + canvas.drawImageRect( + frame!, + Rect.fromLTWH(0, 0, frame!.width.toDouble(), frame!.height.toDouble()), + Rect.fromLTWH(frame_padding/2, frame_padding/2, size.width-frame_padding, size.height-frame_padding), // Increase size for the frame + Paint(), + ); + + if (sticker1 != null) { + canvas.drawImageRect( + sticker1!, + Rect.fromLTWH(0, 0, sticker1!.width.toDouble(), sticker1!.height.toDouble()), + Rect.fromLTWH(0, size.height*0.58, size.height*0.42, size.height*0.42), // Bottom left + Paint(), + ); + } + + if (sticker2 != null) { + canvas.drawImageRect( + sticker2!, + Rect.fromLTWH(0, 0, sticker2!.width.toDouble(), sticker2!.height.toDouble()), + Rect.fromLTWH(size.width*0.58, 0, size.width*0.42, size.width*0.42), // Upper right + Paint(), + ); + } + } + + + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +class LogoPainter extends CustomPainter { + final ui.Image? image; + final Color _frameColor = Color(0xFFCC0000); + final ui.Image? sticker1; + final ui.Image? sticker2; + + + + LogoPainter(this.image, this.sticker1, this.sticker2); + + + @override + void paint(Canvas canvas, Size size) { + if (image != null) { + final double centerX = size.width / 2; + final double centerY = size.height / 2; + final double radius = size.width / 2; + final double _outerFrameThickness = size.height*0.0292; + final double _innerFrameThickness = size.height*0.01; + final _middleFrameThickness = size.height*0.01667; + + final Paint outerFramePaint = Paint() + ..color = _frameColor + ..style = PaintingStyle.stroke + ..strokeWidth = _outerFrameThickness; + canvas.drawCircle(Offset(centerX, centerY), radius, outerFramePaint); + + final Paint middleFramePaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = _middleFrameThickness; + canvas.drawCircle(Offset(centerX, centerY), radius - (_outerFrameThickness / 2), middleFramePaint); + + final Paint innerFramePaint = Paint() + ..color = _frameColor + ..style = PaintingStyle.stroke + ..strokeWidth = _innerFrameThickness; + canvas.drawCircle(Offset(centerX, centerY), radius - (_outerFrameThickness + _middleFrameThickness), innerFramePaint); + + + canvas.save(); + canvas.clipPath(Path()..addOval(Rect.fromCircle(center: Offset(centerX, centerY), radius: radius - _outerFrameThickness-_middleFrameThickness-_innerFrameThickness-3.0))); + + canvas.drawImageRect( + image!, + Rect.fromLTWH(0, 0, image!.width.toDouble(), image!.height.toDouble()), + Rect.fromLTWH(0, 0, size.width, size.height), + Paint(), + ); + canvas.restore(); + + if (sticker1 != null) { + final double stickerSize = size.height*0.4167; + canvas.drawImageRect( + sticker1!, + Rect.fromLTWH(0, 0, sticker1!.width.toDouble(), sticker1!.height.toDouble()), + Rect.fromLTWH(-stickerSize * 0.1, size.height - stickerSize, stickerSize, stickerSize), // Adjusted position + Paint(), + ); + } + + if (sticker2 != null) { + final double stickerSize = size.height*0.4167; + canvas.drawImageRect( + sticker2!, + Rect.fromLTWH(0, 0, sticker2!.width.toDouble(), sticker2!.height.toDouble()), + Rect.fromLTWH(size.width - stickerSize, -stickerSize * 0.1, stickerSize, stickerSize), // Adjusted position + Paint(), + ); + } + + canvas.restore(); + } + } + + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/bu_passport/lib/components/time_span.dart b/bu_passport/lib/components/time_span.dart new file mode 100644 index 0000000..f56caba --- /dev/null +++ b/bu_passport/lib/components/time_span.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../classes/session.dart'; + +class EventTimeDisplay extends StatelessWidget { + final DateTime startTime; + final DateTime endTime; + + const EventTimeDisplay({ + Key? key, + required this.startTime, + required this.endTime, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: const TextStyle(fontSize: 16.0, color: Colors.black), + children: [ + const TextSpan( + text: 'Start: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan( + text: DateFormat('h:mm a, EEEE, MMMM d, y').format(startTime), + ), + ], + ), + ), + const SizedBox(height: 8.0), // Use desired spacing + RichText( + text: TextSpan( + style: const TextStyle(fontSize: 16.0, color: Colors.black), + children: [ + const TextSpan( + text: 'End: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan( + text: DateFormat('h:mm a, EEEE, MMMM d, y').format(endTime), + ), + ], + ), + ), + const SizedBox(height: 8.0), + ], + ); + } +} + + +class AllSessionsDisplay extends StatelessWidget { + final List sessions; + + const AllSessionsDisplay({ + Key? key, + required this.sessions, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: sessions.map((session) { + return EventTimeDisplay( + startTime: session.sessionStartTime, + endTime: session.sessionEndTime, + ); + }).toList(), + ); + } +} + +class EventDateRangeDisplay extends StatelessWidget { + final List sessions; + + const EventDateRangeDisplay({ + Key? key, + required this.sessions, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (sessions.isEmpty) return Text("No sessions available"); + + // Find the minimum start time and maximum end time + DateTime earliestStart = sessions.first.sessionStartTime; + DateTime latestEnd = sessions.first.sessionEndTime; + + for (var session in sessions) { + if (session.sessionStartTime.isBefore(earliestStart)) { + earliestStart = session.sessionStartTime; + } + if (session.sessionEndTime.isAfter(latestEnd)) { + latestEnd = session.sessionEndTime; + } + } + + // Format the dates + String startDate = DateFormat.yMMMd().format(earliestStart); + String endDate = DateFormat.yMMMd().format(latestEnd); + + return Text( + "$startDate - $endDate", + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ); + } +} diff --git a/bu_passport/lib/pages/calendar_page.dart b/bu_passport/lib/pages/calendar_page.dart index 5880211..8e4fc0f 100644 --- a/bu_passport/lib/pages/calendar_page.dart +++ b/bu_passport/lib/pages/calendar_page.dart @@ -1,10 +1,11 @@ +import 'package:bu_passport/classes/event.dart'; import 'package:bu_passport/components/event_widget.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:table_calendar/table_calendar.dart'; -import 'package:bu_passport/services/firebase_service.dart'; -import 'package:bu_passport/classes/event.dart'; + +import '../services/firebase_service.dart'; class CalendarPage extends StatefulWidget { const CalendarPage({Key? key}) : super(key: key); @@ -42,7 +43,9 @@ class _CalendarPageState extends State { }); } - void updateEventPage() {} + void updateEventPage() { + _fetchEvents(); + } @override Widget build(BuildContext context) { @@ -105,10 +108,7 @@ class _CalendarPageState extends State { // Provide events to the calendar eventLoader: (day) { return _allEvents - .where((event) => - event.eventStartTime.year == day.year && - event.eventStartTime.month == day.month && - event.eventStartTime.day == day.day) + .where((event) =>event.hasSessionOnDay(day)) .toList(); }, ), @@ -127,12 +127,15 @@ class _CalendarPageState extends State { child: ListView.builder( itemCount: selectedEvents.length, itemBuilder: (context, index) { + final uniqueKey = + '${selectedEvents[index].eventID}-${DateTime.now().millisecondsSinceEpoch.hashCode}'; return Container( margin: EdgeInsets.symmetric( vertical: itemVerticalMargin, horizontal: itemHorizontalMargin, ), child: EventWidget( + key: ValueKey(uniqueKey), event: selectedEvents[index], onUpdateEventPage: updateEventPage), ); diff --git a/bu_passport/lib/pages/event_page.dart b/bu_passport/lib/pages/event_page.dart index cb368ab..433d5ff 100644 --- a/bu_passport/lib/pages/event_page.dart +++ b/bu_passport/lib/pages/event_page.dart @@ -1,14 +1,28 @@ +//import 'dart:nativewrappers/_internal/vm/lib/typed_data_patch.dart'; +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:bu_passport/classes/event.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:bu_passport/services/geocoding_service.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:geolocator/geolocator.dart'; -import 'package:bu_passport/classes/event.dart'; -import 'package:bu_passport/services/firebase_service.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:timezone/timezone.dart' as tz; import 'package:intl/intl.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'dart:ui' as ui; +import '../components/checkin_options_dialog.dart'; +import '../components/checkin_success_dialog.dart'; +import '../components/time_span.dart'; +import '../services/firebase_service.dart'; +import '../services/web_image_service.dart'; + +import 'package:http/http.dart' as http; class EventPage extends StatefulWidget { final Event event; @@ -29,7 +43,30 @@ class _EventPageState extends State { bool _isSaved = false; // Track whether the user is interested in the event bool _isCheckedIn = false; // To track if the user has checked in + String photoUrl = ""; + + + Future _loadImage(File file) async { + final bytes = await file.readAsBytes(); + return await decodeImageFromList(bytes); + } + Future _loadImageFromAssets(String path) async { + // Load the image data from the assets + final ByteData data = await rootBundle.load(path); + final List bytes = data.buffer.asUint8List(); + + // Decode the image data to a ui.Image + return await decodeImageFromList(Uint8List.fromList(bytes)); + } + + Future _loadImageFromUrl(String url) async { + final response = await http.get(Uri.parse(url)); + final Uint8List bytes = response.bodyBytes; + final Completer completer = Completer(); + ui.decodeImageFromList(bytes, completer.complete); + return completer.future; + } // Checks if user saved event -- if so, the button will reflect that @override void initState() { @@ -127,6 +164,73 @@ class _EventPageState extends State { } } + Future checkInWithPhoto() async { + try{ + firebaseService.checkInUserForEvent( + widget.event.eventID, widget.event.eventPoints+5, widget.event.eventStickers); + final picker = ImagePicker(); + final pickedFile = await picker.pickImage(source: ImageSource.camera); + if(pickedFile!=null){ + final Uint8List imageBytes = await pickedFile.readAsBytes(); + firebaseService.uploadCheckinImage(widget.event.eventID, imageBytes); + final photo = await _loadImage(File(pickedFile.path)); + final frame = await _loadImageFromAssets('assets/images/stickers/frame.png'); + ui.Image? sticker1; + ui.Image? sticker2; + if (widget.event.eventStickers.isNotEmpty) sticker1 = await _loadImageFromAssets('assets/images/stickers/'+widget.event.eventStickers[0].name+".png"); + if (widget.event.eventStickers.length > 1) sticker2 = await _loadImageFromAssets('assets/images/stickers/'+widget.event.eventStickers[1].name+".png"); + widget.onUpdateEventPage(); + showDialog( + context: context, + builder: (BuildContext context) { + return SuccessDialog( + points: widget.event.eventPoints+5, + eventTitle: widget.event.eventTitle, + image: photo, + frame: frame, + sticker1: sticker1, + sticker2: sticker2, + ); + }, + ); + }else{ + + } + + }catch(e){ + print("Unable to checkin: ${e.toString()}"); + return; + } + + } + + Future checkInWithoutPhoto()async { + try{ + firebaseService.checkInUserForEvent( + widget.event.eventID, widget.event.eventPoints, widget.event.eventStickers); + ui.Image? sticker1; + ui.Image? sticker2; + if (widget.event.eventStickers.isNotEmpty) sticker1 = await _loadImageFromAssets('assets/images/stickers/'+widget.event.eventStickers[0].name+".png"); + if (widget.event.eventStickers.length > 1) sticker2 = await _loadImageFromAssets('assets/images/stickers/'+widget.event.eventStickers[1].name+".png"); + final icon = await _loadImageFromUrl(widget.event.eventPhoto); + showDialog( + context: context, + builder: (BuildContext context) { + return SuccessDialog( + points: widget.event.eventPoints, + eventTitle: widget.event.eventTitle, + logo: icon, + sticker1: sticker1, + sticker2: sticker2, + ); + }, + ); + }catch(e){ + return; + } + + } + @override Widget build(BuildContext context) { double screenWidth = MediaQuery.of(context).size.width; @@ -140,11 +244,11 @@ class _EventPageState extends State { body: ListView( // crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Image.asset( - widget.event.eventPhoto, + Image( + image: WebImageService.buildImageProvider(widget.event.eventPhoto), // Use the helper function fit: BoxFit.cover, width: double.infinity, - height: screenHeight * 0.4, // Adjust the height as needed + height: screenHeight * 0.4, ), Padding( padding: EdgeInsets.all(edgeInsets * 2.5), @@ -173,36 +277,8 @@ class _EventPageState extends State { ), ), SizedBox(height: sizedBoxHeight), - RichText( - text: TextSpan( - style: TextStyle(fontSize: 16.0, color: Colors.black), - children: [ - TextSpan( - text: 'Start: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - TextSpan( - text: DateFormat('h:mm a, EEEE, MMMM d, y') - .format(widget.event.eventStartTime), - ), - ], - ), - ), - SizedBox(height: sizedBoxHeight), - RichText( - text: TextSpan( - style: TextStyle(fontSize: 16.0, color: Colors.black), - children: [ - TextSpan( - text: 'End: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - TextSpan( - text: DateFormat('h:mm a, EEEE, MMMM d, y') - .format(widget.event.eventEndTime), - ), - ], - ), + AllSessionsDisplay( + sessions: widget.event.eventSessions, ), SizedBox(height: sizedBoxHeight), GestureDetector( @@ -253,22 +329,29 @@ class _EventPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( - onPressed: (!_isSaved || - !isEventToday(widget.event.eventStartTime) || + onPressed: ( + !widget.event.isEventHappening() || _isCheckedIn) ? null : () async { + // Your check-in logic here. On successful check-in, update the _isCheckedIn state. bool success = await checkIn(); if (success) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text("Checked in successfully!"))); - firebaseService.checkInUserForEvent( - widget.event.eventID, widget.event.eventPoints); + bool withPhoto = await showDialog( + context: context, + builder: (BuildContext context) { + return CheckInOptionsDialog(); + }, + ); + if(withPhoto){ + checkInWithPhoto(); + } else { + checkInWithoutPhoto(); + } - widget.onUpdateEventPage(); } else { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text( "Unable to check in: location too far or permission denied."))); } @@ -277,7 +360,7 @@ class _EventPageState extends State { style: ElevatedButton.styleFrom( backgroundColor: _isCheckedIn ? Colors.grey - : (_isSaved ? Colors.red : Colors.grey), + : (Colors.red), ), ), SizedBox(width: sizedBoxHeight * 3), // Optional spacing diff --git a/bu_passport/lib/pages/explore_page.dart b/bu_passport/lib/pages/explore_page.dart index 71fb7a8..9a4f69a 100644 --- a/bu_passport/lib/pages/explore_page.dart +++ b/bu_passport/lib/pages/explore_page.dart @@ -1,9 +1,10 @@ -import 'package:bu_passport/classes/event.dart'; import 'package:bu_passport/components/event_widget.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; -import 'package:bu_passport/services/firebase_service.dart'; + +import '../classes/event.dart'; +import '../services/firebase_service.dart'; class ExplorePage extends StatefulWidget { const ExplorePage({Key? key}) : super(key: key); diff --git a/bu_passport/lib/pages/leaderboard_page.dart b/bu_passport/lib/pages/leaderboard_page.dart index 59fd5dd..fc8b59b 100644 --- a/bu_passport/lib/pages/leaderboard_page.dart +++ b/bu_passport/lib/pages/leaderboard_page.dart @@ -19,7 +19,7 @@ class LeaderboardPageState extends State { int userTickets = 0; int? userRank; // Variable to store user rank FirebaseService firebaseService = - FirebaseService(db: FirebaseFirestore.instance); + FirebaseService(db: FirebaseFirestore.instance); @override void initState() { super.initState(); diff --git a/bu_passport/lib/pages/profile_page.dart b/bu_passport/lib/pages/profile_page.dart index 1811aaf..f6a7bb3 100644 --- a/bu_passport/lib/pages/profile_page.dart +++ b/bu_passport/lib/pages/profile_page.dart @@ -7,12 +7,13 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:bu_passport/classes/categorized_events.dart'; import 'package:bu_passport/components/event_widget.dart'; -import 'package:bu_passport/classes/event.dart'; import 'package:bu_passport/util/profile_pic.dart'; import 'package:bu_passport/util/image_select.dart'; -import 'package:bu_passport/services/firebase_service.dart'; + +import '../classes/categorized_events.dart'; +import '../classes/event.dart'; +import '../services/firebase_service.dart'; // The ProfilePage is a StatefulWidget that allows users to view and edit their profile. class ProfilePage extends StatefulWidget { @@ -175,11 +176,14 @@ class _ProfilePageState extends State return ListView.builder( itemCount: events.length, itemBuilder: (context, index) { + final uniqueKey = + '${events[index].eventID}-${DateTime.now().millisecondsSinceEpoch.hashCode}'; final event = events[index]; return Card( margin: EdgeInsets.symmetric( vertical: verticalMargin, horizontal: horizontalMargin), child: EventWidget( + key: ValueKey(uniqueKey), event: event, onUpdateEventPage: updateEventPage)); // Use your EventWidget to display each event }, diff --git a/bu_passport/lib/services/firebase_service.dart b/bu_passport/lib/services/firebase_service.dart index ee751f5..6c7144c 100644 --- a/bu_passport/lib/services/firebase_service.dart +++ b/bu_passport/lib/services/firebase_service.dart @@ -1,34 +1,69 @@ +import 'dart:typed_data'; + import 'package:bu_passport/classes/user.dart'; -import 'package:bu_passport/classes/categorized_events.dart'; -import 'package:bu_passport/classes/event.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_storage/firebase_storage.dart'; + +import '../classes/categorized_events.dart'; +import '../classes/event.dart'; +import '../classes/session.dart'; +import '../classes/sticker.dart'; class FirebaseService { final FirebaseFirestore db; + static const EVENT_COLLECTION = "new_events"; + static const USER_COLLECTION = "users"; + static const ATTENDANCE_COLLECTION = "attendances"; + + static const CHECKIN_PHOTO_PATH = "checkinPhotos"; const FirebaseService({required this.db}); // Function to fetch events from Firestore - Future> fetchEvents() async { List eventList = []; try { - QuerySnapshot snapshot = await this.db.collection('events').get(); + QuerySnapshot snapshot = await this.db.collection(EVENT_COLLECTION).get(); snapshot.docs.forEach((doc) { final eventData = doc.data() as Map; + final sessionData = eventData['eventSessions'] as Map; + + List sessions = []; + if (sessionData != null) { + sessions = sessionData.entries.map((entry) { + final sessionID = entry.key; + final sessionDetails = entry.value as Map; + + return Session( + sessionID: sessionID, + sessionStartTime: sessionDetails['startTime'] != null + ? (sessionDetails['startTime'] as Timestamp).toDate() + : DateTime.now(), // Default if startTime is null + sessionEndTime: sessionDetails['endTime'] != null + ? (sessionDetails['endTime'] as Timestamp).toDate() + : DateTime.now(), // Default if endTime is null + savedUsers: sessionDetails['savedUsers'] != null + ? List.from(sessionDetails['savedUsers']) + : [], // Default if null + ); + }).toList(); + } + Event event = Event( eventID: doc.id, eventTitle: eventData['eventTitle'] ?? '', eventURL: eventData['eventURL'] ?? '', eventPhoto: eventData['eventPhoto'] ?? '', eventLocation: eventData['eventLocation'] ?? '', - eventStartTime: (eventData['eventStartTime'] as Timestamp?)!.toDate(), - eventEndTime: (eventData['eventEndTime'] as Timestamp?)!.toDate(), eventDescription: eventData['eventDescription'] ?? '', eventPoints: eventData['eventPoints'] ?? 0, savedUsers: List.from(eventData['savedUsers'] ?? []), + eventSessions: sessions, + eventStickers: (eventData['eventStickers'] as List?) + ?.map((stickerName) => Sticker(name: stickerName as String)) + .toList() ?? [], ); eventList.add(event); @@ -40,18 +75,16 @@ class FirebaseService { } } - // Function to fetch events for a specific date from a list of events + + // Function to fetch events which have sessions happening on a particular day List fetchEventsForDay(DateTime date, List events) { - return events.where((event) { - return event.eventStartTime.day == date.day && - event.eventStartTime.month == date.month && - event.eventStartTime.year == date.year; - }).toList(); + return events.where((event) => event.hasSessionOnDay(date)).toList(); } - // Function to filter events based on a search query + + // Function to filter events based on a search query List filterEvents(List events, String query) { if (query.isEmpty) { return events; @@ -62,15 +95,18 @@ class FirebaseService { } // Function to fetch user details from Firestore - Future fetchUser(String userUID) async { try { DocumentSnapshot snapshot = - await this.db.collection('users').doc(userUID).get(); + await this.db.collection('users').doc(userUID).get(); if (snapshot.exists) { final userData = snapshot.data() as Map; + Map stickerData = Map.from(userData['userCollectedStickers'] ?? {}); + + // Convert Map to Map + Map stickerCollection = stickerData.map((name, owned) => MapEntry(Sticker(name: name), owned)); Users user = Users( firstName: userData['firstName'], lastName: userData['lastName'], @@ -82,8 +118,9 @@ class FirebaseService { userYear: userData['userYear'], userPoints: userData['userPoints'], userSavedEvents: - Map.from(userData['userSavedEvents'] ?? {}), - admin: userData['admin'], + Map.from(userData['userSavedEvents'] ?? {}), + admin: userData['admin'], + userStickerCollection: stickerCollection, ); return user; } else { @@ -96,7 +133,6 @@ class FirebaseService { } // Function to save an event to userSavedEvents - Future saveEvent(String eventId) async { final userUID = FirebaseAuth.instance.currentUser?.uid; final userDoc = this.db.collection('users').doc(userUID); @@ -107,7 +143,7 @@ class FirebaseService { await userDoc.update({ 'userSavedEvents.$eventId': false, }); - await this.db.collection('events').doc(eventId).update({ + await this.db.collection(EVENT_COLLECTION).doc(eventId).update({ 'savedUsers': FieldValue.arrayUnion([userUID]), }); } catch (error) { @@ -116,7 +152,6 @@ class FirebaseService { } // Function to unsave an event from userSavedEvents - Future unsaveEvent(String eventId) async { final userUID = FirebaseAuth.instance.currentUser?.uid; final userDoc = this.db.collection('users').doc(userUID); @@ -126,7 +161,7 @@ class FirebaseService { await userDoc.update({ 'userSavedEvents.$eventId': FieldValue.delete(), }); - await this.db.collection('events').doc(eventId).update({ + await this.db.collection(EVENT_COLLECTION).doc(eventId).update({ 'savedUsers': FieldValue.arrayRemove([userUID]), }); } catch (error) { @@ -134,10 +169,11 @@ class FirebaseService { } } + // Function to check if a user has saved an event Future hasUserSavedEvent(String userUID, String eventId) async { DocumentSnapshot userDocSnapshot = - await this.db.collection('users').doc(userUID).get(); + await this.db.collection('users').doc(userUID).get(); if (userDocSnapshot.exists) { final userData = userDocSnapshot.data() as Map; @@ -154,7 +190,6 @@ class FirebaseService { } // Function to categorize events into attended and saved events - Future fetchAndCategorizeEvents() async { final userUID = FirebaseAuth.instance.currentUser?.uid; @@ -163,7 +198,7 @@ class FirebaseService { } final userDoc = - await FirebaseFirestore.instance.collection('users').doc(userUID).get(); + await FirebaseFirestore.instance.collection('users').doc(userUID).get(); if (!userDoc.exists) { throw Exception("User document does not exist"); } @@ -172,64 +207,91 @@ class FirebaseService { Map savedEvents = userData!['userSavedEvents'] ?? {}; - final now = DateTime.now(); - final DateTime today = DateTime(now.year, now.month, now.day); - final List attendedEvents = []; final List userSavedEvents = []; await Future.forEach(savedEvents.entries, - (MapEntry entry) async { - String eventId = entry.key; - bool isCheckedIn = entry.value; - + (MapEntry entry) async { + String eventId = entry.key; + Event? event = await fetchEventById(eventId); + if (event != null) { + userSavedEvents.add(event); + } + }); + + final attendanceQuerySnapshot = await this.db.collection(ATTENDANCE_COLLECTION) + .where('userID', isEqualTo: userUID) + .get(); + + for (var attendanceDoc in attendanceQuerySnapshot.docs) { + final eventId = attendanceDoc['eventID']; + + // Fetch the event details using the event ID Event? event = await fetchEventById(eventId); if (event != null) { - DateTime startOfDayEvent = DateTime(event.eventStartTime.year, - event.eventStartTime.month, event.eventStartTime.day); - if ((startOfDayEvent.isBefore(now) || - startOfDayEvent.isAtSameMomentAs(today)) && - isCheckedIn) { - // Event has already occurred (attended) - attendedEvents.add(event); - } else { - // Event is upcoming - userSavedEvents.add(event); - } + attendedEvents.add(event); } - }); + } return CategorizedEvents( attendedEvents: attendedEvents, userSavedEvents: userSavedEvents); } // Function to fetch an event by its ID - Future fetchEventById(String eventId) async { DocumentSnapshot> snapshot = - await this.db.collection('events').doc(eventId).get(); + await this.db.collection(EVENT_COLLECTION).doc(eventId).get(); if (snapshot.exists && snapshot.data() != null) { - Map eventData = snapshot.data()!; + final eventData = snapshot.data() as Map; + final sessionData = eventData['eventSessions'] as Map; + + List sessions = []; + if (sessionData != null) { + sessions = sessionData.entries.map((entry) { + final sessionID = entry.key; + final sessionDetails = entry.value as Map; + + return Session( + sessionID: sessionID, + sessionStartTime: sessionDetails['startTime'] != null + ? (sessionDetails['startTime'] as Timestamp).toDate() + : DateTime.now(), // Default if startTime is null + sessionEndTime: sessionDetails['endTime'] != null + ? (sessionDetails['endTime'] as Timestamp).toDate() + : DateTime.now(), // Default if endTime is null + savedUsers: sessionDetails['savedUsers'] != null + ? List.from(sessionDetails['savedUsers']) + : [], // Default if null + ); + }).toList(); + } + Event event = Event( - eventID: eventData['eventID'] ?? '', + eventID: eventId, eventTitle: eventData['eventTitle'] ?? '', + eventURL: eventData['eventURL'] ?? '', eventPhoto: eventData['eventPhoto'] ?? '', eventLocation: eventData['eventLocation'] ?? '', - eventStartTime: (eventData['eventStartTime'] as Timestamp?)!.toDate(), - eventEndTime: (eventData['eventEndTime'] as Timestamp?)!.toDate(), - eventURL: eventData['eventURL'] ?? '', eventDescription: eventData['eventDescription'] ?? '', eventPoints: eventData['eventPoints'] ?? 0, savedUsers: List.from(eventData['savedUsers'] ?? []), + eventSessions: sessions, + eventStickers: (eventData['eventStickers'] as List?) + ?.map((stickerName) => Sticker(name: stickerName as String)) + .toList() ?? [], ); return event; } throw Exception("Event not found"); } + + // Function to check in a user for an event - void checkInUserForEvent(String eventID, int eventPoints) { + void checkInUserForEvent(String eventID, int eventPoints, List stickers) { final userUID = FirebaseAuth.instance.currentUser?.uid; + final attendanceDocID = '${eventID}_$userUID'; + final attendanceDoc = this.db.collection(ATTENDANCE_COLLECTION).doc(attendanceDocID); if (userUID == null) { throw Exception("User is not logged in"); } @@ -237,37 +299,38 @@ class FirebaseService { final userDoc = this.db.collection('users').doc(userUID); try { - // Atomically add the new event ID to the user's saved events list - userDoc.update({ - 'userSavedEvents.$eventID': true, - }); userDoc.update({ 'userPoints': FieldValue.increment(eventPoints), }); - print("Event check-in successful"); + attendanceDoc.set({ + 'checkInTime': FieldValue.serverTimestamp(), + 'eventID': eventID, + 'userID': userUID, + }); + for(Sticker s in stickers){ + addStickerToUserCollection(userUID, s); + } + // } catch (error) { print("Failed to check-in for event: $error"); } } // Function to check if a user has checked in for an event - Future isUserCheckedInForEvent(String userUID, String eventId) async { - DocumentSnapshot userDocSnapshot = - await this.db.collection('users').doc(userUID).get(); - - if (userDocSnapshot.exists) { - final userData = userDocSnapshot.data() as Map; - Map savedEvents = userData['userSavedEvents'] ?? {}; + final attendanceDocID = '${eventId}_$userUID'; + final attendanceDoc = this.db.collection(ATTENDANCE_COLLECTION).doc(attendanceDocID); - // Check if the eventId exists in the list - return savedEvents.containsKey(eventId) && savedEvents[eventId]; + try { + final docSnapshot = await attendanceDoc.get(); + return docSnapshot.exists; + } catch (error) { + print("Error checking attendance: $error"); + return true; // Return true in case of error (user not checked in) } - return false; } // Function to update the user's profile URL - Future updateUserProfileURL(String profileURL) async { final userUID = FirebaseAuth.instance.currentUser?.uid; if (userUID == null) { @@ -286,32 +349,56 @@ class FirebaseService { } // Function to fetch events after current time for explore page - Future> fetchEventsFromNow() async { final now = DateTime.now(); List eventList = []; try { - QuerySnapshot snapshot = await this.db.collection('events').get(); + QuerySnapshot snapshot = await this.db.collection(EVENT_COLLECTION).get(); snapshot.docs.forEach((doc) { final eventData = doc.data() as Map; + final sessionData = eventData['eventSessions'] as Map; + + List sessions = []; + if (sessionData != null) { + sessions = sessionData.entries.map((entry) { + final sessionID = entry.key; + final sessionDetails = entry.value as Map; + + return Session( + sessionID: sessionID, + sessionStartTime: sessionDetails['startTime'] != null + ? (sessionDetails['startTime'] as Timestamp).toDate() + : DateTime.now(), // Default if startTime is null + sessionEndTime: sessionDetails['endTime'] != null + ? (sessionDetails['endTime'] as Timestamp).toDate() + : DateTime.now(), // Default if endTime is null + savedUsers: sessionDetails['savedUsers'] != null + ? List.from(sessionDetails['savedUsers']) + : [], // Default if null + ); + }).toList(); + } + Event event = Event( eventID: doc.id, eventTitle: eventData['eventTitle'] ?? '', eventURL: eventData['eventURL'] ?? '', eventPhoto: eventData['eventPhoto'] ?? '', eventLocation: eventData['eventLocation'] ?? '', - eventStartTime: (eventData['eventStartTime'] as Timestamp?)!.toDate(), - eventEndTime: (eventData['eventEndTime'] as Timestamp?)!.toDate(), eventDescription: eventData['eventDescription'] ?? '', eventPoints: eventData['eventPoints'] ?? 0, savedUsers: List.from(eventData['savedUsers'] ?? []), + eventSessions: sessions, + eventStickers: (eventData['eventStickers'] as List?) + ?.map((stickerName) => Sticker(name: stickerName as String)) + .toList() ?? [], ); eventList.add(event); }); eventList = - eventList.where((event) => event.eventEndTime.isAfter(now)).toList(); + eventList.where((event) => event.hasUpcomingSessions(now)).toList(); return eventList; } catch (error) { print("Failed to fetch events: $error"); @@ -320,15 +407,18 @@ class FirebaseService { } // Function to fetch all users - Future> fetchAllUsers() async { List users = []; try { QuerySnapshot> snapshot = - await this.db.collection('users').get(); + await this.db.collection('users').get(); snapshot.docs.forEach((doc) { final userData = doc.data(); + Map stickerData = Map.from(userData['userCollectedStickers'] ?? {}); + + // Convert Map to Map + Map stickerCollection = stickerData.map((name, owned) => MapEntry(Sticker(name: name), owned)); Users user = Users( firstName: userData['firstName'], lastName: userData['lastName'], @@ -338,10 +428,11 @@ class FirebaseService { userUID: userData['userUID'], userYear: userData['userYear'], userSavedEvents: - Map.from(userData['userSavedEvents'] ?? {}), + Map.from(userData['userSavedEvents'] ?? {}), userPoints: userData['userPoints'], userProfileURL: userData['userProfileURL'], admin: userData['admin'], + userStickerCollection: stickerCollection, ); users.add(user); }); @@ -351,4 +442,63 @@ class FirebaseService { return []; } } + + + Future uploadCheckinImage(String eventID, Uint8List imageBytes) async { + final userUID = FirebaseAuth.instance.currentUser?.uid; + String fileName = "checkin_${userUID}_${eventID}.jpg"; + + try { + final ref = FirebaseStorage.instance + .ref() + .child(CHECKIN_PHOTO_PATH) + .child(fileName); + UploadTask uploadTask = ref.putData(imageBytes); + + TaskSnapshot snapshot = await uploadTask; + String downloadUrl = await snapshot.ref.getDownloadURL(); + final attendanceDoc = FirebaseFirestore.instance + .collection('attendances') + .doc('${eventID}_$userUID'); + + await attendanceDoc.update({ + 'checkInPhoto': downloadUrl, + }); + return downloadUrl; + + } catch (e) { + print("Error uploading image: $e"); + return "null"; + } + } + + Future addStickerToUserCollection(String userID, Sticker sticker) async { + try { + final userDocRef = db.collection(USER_COLLECTION).doc(userID); + + await db.runTransaction((transaction) async { + final userDoc = await transaction.get(userDocRef); + + if (userDoc.exists) { + Map stickers = + (userDoc.data()?['userCollectedStickers'] as Map?)?.map((key, value) => MapEntry(key, value as bool)) ?? {}; + + if (!stickers.containsKey(sticker.name)) { + stickers[sticker.name] = true; + + transaction.set(userDocRef, {'userCollectedStickers': stickers}, SetOptions(merge: true)); + } + } else { + throw Exception("User document does not exist."); + } + }); + + print("Sticker '${sticker.name}' added to user $userID's collection."); + } catch (e) { + print("Failed to add sticker: $e"); + throw Exception("Failed to add sticker to user's collection."); + } + } + + } diff --git a/bu_passport/lib/services/web_image_service.dart b/bu_passport/lib/services/web_image_service.dart new file mode 100644 index 0000000..c7102f6 --- /dev/null +++ b/bu_passport/lib/services/web_image_service.dart @@ -0,0 +1,16 @@ +import 'package:flutter/cupertino.dart'; + +class WebImageService { + // Helper function to build the correct ImageProvider + static ImageProvider buildImageProvider(String imageUrl) { + try { + //print(imageUrl); + if (imageUrl == null || imageUrl.isEmpty) { + return const AssetImage('assets/images/arts/placeholder-image.jpeg'); + } + return NetworkImage(imageUrl); + } catch (e) { + return const AssetImage('assets/images/arts/placeholder-image.jpeg'); + } + } +} \ No newline at end of file diff --git a/bu_passport/pubspec.yaml b/bu_passport/pubspec.yaml index 6d9d140..a016825 100644 --- a/bu_passport/pubspec.yaml +++ b/bu_passport/pubspec.yaml @@ -83,7 +83,8 @@ flutter: assets: - assets/images/onboarding/ - assets/images/arts/ - - assets/images/leaderboard/ + - assets/images/leaderboard/ + - assets/images/stickers/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware @@ -109,3 +110,8 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages + fonts: + - family: Inter + fonts: + - asset: assets/fonts/Inter_28pt-ExtraBold.ttf + weight: 700