diff --git a/analysis_options.yaml b/analysis_options.yaml index 5801b01..b05fa82 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -19,9 +19,6 @@ # Android Studio, and the `flutter analyze` command. analyzer: - strong-mode: - implicit-casts: false - implicit-dynamic: false errors: # treat missing required parameters as a warning (not a hint) missing_required_param: warning diff --git a/assets/images/UCSanDiegoLogo-nav.png b/assets/images/UCSanDiegoLogo-nav.png new file mode 100644 index 0000000..1ec8bb8 Binary files /dev/null and b/assets/images/UCSanDiegoLogo-nav.png differ diff --git a/lib/app_constants.dart b/lib/app_constants.dart index 09f0ffd..48a3769 100644 --- a/lib/app_constants.dart +++ b/lib/app_constants.dart @@ -6,11 +6,32 @@ class RoutePaths { static const String FinalsList = 'finals_list'; static const String SearchView = 'search_view'; static const String CourseListView = 'course_list_view'; - static const String Login = 'login'; + static const String SearchDetail = 'search_detail'; + static const String AuthenticationError = 'authentication_error'; } class CalendarStyles { static const double calendarHeaderHeight = 50; static const double calendarTimeWidth = 35; static const double calendarRowHeight = 60; -} \ No newline at end of file +} + +class ErrorConstants { + static const String authorizedPostErrors = 'Failed to upload data: '; + static const String authorizedPutErrors = 'Failed to update data: '; + static const String invalidBearerToken = 'Invalid bearer token'; + static const String duplicateRecord = + 'DioError [DioErrorType.response]: Http status error [409]'; + static const String invalidMedia = + 'DioError [DioErrorType.response]: Http status error [415]'; + static const String silentLoginFailed = 'Silent login failed'; +} + +class LoginConstants { + static const String silentLoginFailedTitle = 'Oops! You\'re not logged in.'; + static const String silentLoginFailedDesc = + 'The system has logged you out (probably by mistake). Go to Profile to log back in.'; + static const String loginFailedTitle = 'Sorry, unable to sign you in.'; + static const String loginFailedDesc = + 'Be sure you are using the correct credentials; TritonLink login if you are a student, SSO (AD or Active Directory) if you are a Faculty/Staff.'; +} diff --git a/lib/app_networking.dart b/lib/app_networking.dart new file mode 100644 index 0000000..53a1601 --- /dev/null +++ b/lib/app_networking.dart @@ -0,0 +1,130 @@ +// ignore_for_file: always_specify_types + +import 'dart:async'; +import 'package:dio/dio.dart'; +import 'package:webreg_mobile_flutter/app_constants.dart'; + +class NetworkHelper { + const NetworkHelper(); + + static const int SSO_REFRESH_MAX_RETRIES = 3; + static const int SSO_REFRESH_RETRY_INCREMENT = 5000; + static const int SSO_REFRESH_RETRY_MULTIPLIER = 3; + + Future fetchData(String url) async { + final Dio dio = Dio(); + dio.options.connectTimeout = 20000; + dio.options.receiveTimeout = 20000; + dio.options.responseType = ResponseType.plain; + final Response _response = await dio.get(url); + + if (_response.statusCode == 200) { + // If server returns an OK response, return the body + return _response.data; + } else { + // If that response was not OK, throw an error. + throw Exception('Failed to fetch data: ' + _response.data); + } + } + + Future authorizedFetch( + String url, Map headers) async { + final Dio dio = Dio(); + dio.options.connectTimeout = 20000; + dio.options.receiveTimeout = 20000; + dio.options.responseType = ResponseType.plain; + dio.options.headers = headers; + final Response _response = await dio.get( + url, + ); + if (_response.statusCode == 200) { + // If server returns an OK response, return the body + return _response.data; + } else { + // If that response was not OK, throw an error. + throw Exception('Failed to fetch data: ' + _response.data); + } + } + + Future authorizedPost( + String url, Map? headers, dynamic body) async { + final Dio dio = Dio(); + dio.options.connectTimeout = 20000; + dio.options.receiveTimeout = 20000; + dio.options.headers = headers; + final Response _response = await dio.post(url, data: body); + if (_response.statusCode == 200 || _response.statusCode == 201) { + // If server returns an OK response, return the body + return _response.data; + } else if (_response.statusCode == 400) { + // If that response was not OK, throw an error. + final String message = _response.data['message'] ?? ''; + throw Exception(ErrorConstants.authorizedPostErrors + message); + } else if (_response.statusCode == 401) { + throw Exception(ErrorConstants.authorizedPostErrors + + ErrorConstants.invalidBearerToken); + } else if (_response.statusCode == 404) { + final String message = _response.data['message'] ?? ''; + throw Exception(ErrorConstants.authorizedPostErrors + message); + } else if (_response.statusCode == 500) { + final String message = _response.data['message'] ?? ''; + throw Exception(ErrorConstants.authorizedPostErrors + message); + } else if (_response.statusCode == 409) { + final String message = _response.data['message'] ?? ''; + throw Exception(ErrorConstants.duplicateRecord + message); + } else { + throw Exception(ErrorConstants.authorizedPostErrors + 'unknown error'); + } + } + + Future authorizedPut( + String url, Map headers, dynamic body) async { + final Dio dio = Dio(); + dio.options.connectTimeout = 20000; + dio.options.receiveTimeout = 20000; + dio.options.headers = headers; + final Response _response = await dio.put(url, data: body); + + if (_response.statusCode == 200 || _response.statusCode == 201) { + // If server returns an OK response, return the body + return _response.data; + } else if (_response.statusCode == 400) { + // If that response was not OK, throw an error. + final String message = _response.data['message'] ?? ''; + throw Exception(ErrorConstants.authorizedPutErrors + message); + } else if (_response.statusCode == 401) { + throw Exception(ErrorConstants.authorizedPutErrors + + ErrorConstants.invalidBearerToken); + } else if (_response.statusCode == 404) { + final String message = _response.data['message'] ?? ''; + throw Exception(ErrorConstants.authorizedPutErrors + message); + } else if (_response.statusCode == 500) { + final String message = _response.data['message'] ?? ''; + throw Exception(ErrorConstants.authorizedPutErrors + message); + } else { + throw Exception(ErrorConstants.authorizedPutErrors + 'unknown error'); + } + } + + Future authorizedDelete( + String url, Map headers) async { + final Dio dio = Dio(); + dio.options.connectTimeout = 20000; + dio.options.receiveTimeout = 20000; + dio.options.headers = headers; + try { + final Response _response = await dio.delete(url); + if (_response.statusCode == 200) { + // If server returns an OK response, return the body + return _response.data; + } else { + // If that response was not OK, throw an error. + throw Exception('Failed to delete data: ' + _response.data); + } + } on TimeoutException { + // Display an alert - i.e. no internet + } catch (err) { + return null; + } + } +} diff --git a/lib/app_provider.dart b/lib/app_provider.dart new file mode 100644 index 0000000..7db97c7 --- /dev/null +++ b/lib/app_provider.dart @@ -0,0 +1,34 @@ +import 'package:provider/provider.dart'; +import 'package:provider/single_child_widget.dart'; +import 'package:webreg_mobile_flutter/core/providers/profile.dart'; +import 'package:webreg_mobile_flutter/core/providers/schedule_of_classes.dart'; +import 'package:webreg_mobile_flutter/core/providers/user.dart'; + +// ignore: always_specify_types +List providers = [ + ChangeNotifierProvider( + create: (_) { + return UserDataProvider(); + }, + ), + ChangeNotifierProxyProvider( + create: (_) { + final ScheduleOfClassesProvider scheduleOfClassesProvider = + ScheduleOfClassesProvider(); + scheduleOfClassesProvider.userDataProvider = UserDataProvider(); + return scheduleOfClassesProvider; + }, update: (_, UserDataProvider userDataProvider, + ScheduleOfClassesProvider? scheduleOfClassesProvider) { + scheduleOfClassesProvider!.userDataProvider = userDataProvider; + return scheduleOfClassesProvider; + }), + ChangeNotifierProxyProvider(create: (_) { + final ProfileProvider profileProvider = ProfileProvider(); + profileProvider.userDataProvider = UserDataProvider(); + return profileProvider; + }, update: + (_, UserDataProvider userDataProvider, ProfileProvider? profileProvider) { + profileProvider!.userDataProvider = userDataProvider; + return profileProvider; + }) +]; diff --git a/lib/app_router.dart b/lib/app_router.dart index 1c1a1d0..15a1a47 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -1,20 +1,35 @@ +import 'package:flutter/material.dart'; import 'package:webreg_mobile_flutter/app_constants.dart'; -import 'package:webreg_mobile_flutter/ui/search/search_view.dart'; +import 'package:webreg_mobile_flutter/core/models/schedule_of_classes.dart'; +import 'package:webreg_mobile_flutter/ui/common/authentication_sso.dart'; import 'package:webreg_mobile_flutter/ui/list/course_list_view.dart'; import 'package:webreg_mobile_flutter/ui/navigator/bottom.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'package:webreg_mobile_flutter/ui/search/search_detail.dart'; +import 'package:webreg_mobile_flutter/ui/search/search_view.dart'; +// ignore: avoid_classes_with_only_static_members class Router { static Route generateRoute(RouteSettings settings) { - print('route' + settings.name); switch (settings.name) { case RoutePaths.Home: + // TODO(p8gonzal): Do not add const to BottomNavigation(), will cause popup authentication failure return MaterialPageRoute(builder: (_) => BottomNavigation()); + case RoutePaths.AuthenticationError: + return MaterialPageRoute( + builder: (_) => const AuthenticationSSO()); case RoutePaths.SearchView: return MaterialPageRoute(builder: (_) => SearchView()); case RoutePaths.CourseListView: - return MaterialPageRoute(builder: (_) => CourseListView()); + return MaterialPageRoute(builder: (_) => const CourseListView()); + case RoutePaths.SearchDetail: + final CourseData course = settings.arguments! as CourseData; + return MaterialPageRoute(builder: (_) { + return SearchDetail(data: course); + }); + + default: + return MaterialPageRoute( + builder: (_) => const BottomNavigation()); } } -} \ No newline at end of file +} diff --git a/lib/app_styles.dart b/lib/app_styles.dart index 6123e70..6f48f37 100644 --- a/lib/app_styles.dart +++ b/lib/app_styles.dart @@ -1,24 +1,26 @@ import 'package:flutter/material.dart'; /// App Styles -const headerStyle = TextStyle(fontSize: 35, fontWeight: FontWeight.w900); -const subHeaderStyle = TextStyle(fontSize: 16.0, fontWeight: FontWeight.w500); +const TextStyle headerStyle = + TextStyle(fontSize: 35, fontWeight: FontWeight.w900); +const TextStyle subHeaderStyle = + TextStyle(fontSize: 16.0, fontWeight: FontWeight.w500); // Theme agnostic styles -const agnosticDisabled = Color(0xFF8A8A8A); +const Color agnosticDisabled = Color(0xFF8A8A8A); /// App Layout // Card Layout -const cardMargin = 6.0; -const cardPaddingInner = 8.0; -const cardMinHeight = 60.0; -const listTileInnerPadding = 8.0; +const double cardMargin = 6.0; +const double cardPaddingInner = 8.0; +const double cardMinHeight = 60.0; +const double listTileInnerPadding = 8.0; //Card Heights -const cardContentMinHeight = 80.0; -const cardContentMaxHeight = 568.0; +const double cardContentMinHeight = 80.0; +const double cardContentMaxHeight = 568.0; -const webViewMinHeight = 20.0; +const double webViewMinHeight = 20.0; /// App Theme const MaterialColor ColorPrimary = MaterialColor( @@ -107,10 +109,10 @@ const Color lightTextFieldBorderColor = Color(0xFFFFFFFF); const Color lightAccentColor = Color(0xFFFFFFFF); const Color darkAccentColor = Color(0xFF333333); -const debugHeader = TextStyle(color: lightTextColor, fontSize: 14.0); -const debugRow = TextStyle(color: lightTextColor, fontSize: 12.0); +const TextStyle debugHeader = TextStyle(color: lightTextColor, fontSize: 14.0); +const TextStyle debugRow = TextStyle(color: lightTextColor, fontSize: 12.0); // Testing const Color c1 = Color.fromARGB(255, 255, 0, 0); const Color c2 = Color.fromARGB(255, 0, 255, 0); -const Color c3 = Color.fromARGB(255, 0, 0, 255); \ No newline at end of file +const Color c3 = Color.fromARGB(255, 0, 0, 255); diff --git a/lib/core/models/authentication.dart b/lib/core/models/authentication.dart new file mode 100644 index 0000000..37194b3 --- /dev/null +++ b/lib/core/models/authentication.dart @@ -0,0 +1,73 @@ +// To parse this JSON data, do +// +// final authenticationModel = authenticationModelFromJson(jsonString); + +import 'dart:convert'; + +import 'package:hive/hive.dart'; + +AuthenticationModel authenticationModelFromJson(String str) => + AuthenticationModel.fromJson(json.decode(str)); + +String authenticationModelToJson(AuthenticationModel data) => + json.encode(data.toJson()); + +@HiveType(typeId: 1) +class AuthenticationModel extends HiveObject { + AuthenticationModel({ + this.accessToken, + this.pid, + this.ucsdaffiliation, + this.expiration, + }); + + factory AuthenticationModel.fromJson(Map json) { + return AuthenticationModel( + accessToken: json['access_token'], + pid: json['pid'], + ucsdaffiliation: json['ucsdaffiliation'] ?? '', + expiration: json['expiration'] ?? 0, + ); + } + + @HiveField(0) + String? accessToken; + // Deprecated reserved field number - DO NOT REMOVE + // @HiveField(1) + @HiveField(2) + String? pid; + @HiveField(3) + String? ucsdaffiliation; + @HiveField(4) + int? expiration; + + Map toJson() => { + 'access_token': accessToken, + 'pid': pid, + 'ucsdaffiliation': ucsdaffiliation ?? '', + 'expiration': expiration, + }; + + /// Checks if the token we got back is expired + bool isLoggedIn(DateTime? lastUpdated) { + /// User has not logged in previously - isLoggedIn FALSE + if (lastUpdated == null) { + return false; + } + + /// User has no expiration or accessToken - isLoggedIn FALSE + if (expiration == null || accessToken == null) { + return false; + } + + /// User has expiration and accessToken + if (DateTime.now() + .isBefore(lastUpdated.add(Duration(seconds: expiration!)))) { + /// Current datetime < expiration datetime - isLoggedIn TRUE + return true; + } else { + /// Current datetime > expiration datetime - isLoggedIn FALSE + return false; + } + } +} diff --git a/lib/core/models/profile.dart b/lib/core/models/profile.dart new file mode 100644 index 0000000..0fe6875 --- /dev/null +++ b/lib/core/models/profile.dart @@ -0,0 +1,28 @@ +// ignore_for_file: always_specify_types + +import 'dart:convert'; +import 'dart:core'; + +import 'package:webreg_mobile_flutter/core/models/schedule_of_classes.dart'; + +ProfileModel profileModelFromJson(String str) => + ProfileModel.fromJson(json.decode(str)); + +String profileModelToJson(ProfileModel data) => json.encode(data.toJson()); + +class ProfileModel { + ProfileModel({this.enrolledCourses}); + + factory ProfileModel.fromJson(Map json) { + return ProfileModel( + enrolledCourses: List.from( + json['enrolledCourses'].map((x) => SectionData.fromJson(x)))); + } + + List? enrolledCourses; + + Map toJson() => { + 'enrolledCourses': + List.from(enrolledCourses!.map((x) => x.toJson())) + }; +} diff --git a/lib/core/models/schedule_of_classes.dart b/lib/core/models/schedule_of_classes.dart new file mode 100644 index 0000000..3d753c1 --- /dev/null +++ b/lib/core/models/schedule_of_classes.dart @@ -0,0 +1,261 @@ +// ignore_for_file: always_specify_types + +import 'dart:convert'; +import 'dart:core'; + +ScheduleOfClassesModel classScheduleModelFromJson(String str) => + ScheduleOfClassesModel.fromJson(json.decode(str)); + +String classScheduleModelToJson(ScheduleOfClassesModel data) => + json.encode(data.toJson()); + +class ScheduleOfClassesModel { + ScheduleOfClassesModel({ + this.metadata, + this.courses, + }); + + factory ScheduleOfClassesModel.fromJson(Map json) => + ScheduleOfClassesModel( + metadata: Metadata.fromJson(), + courses: List.from( + json['data'].map((x) => CourseData.fromJson(x)))); + + Metadata? metadata; + List? courses; + + Map toJson() => { + 'metadata': metadata!.toJson(), + 'data': List.from(courses!.map((x) => x.toJson())) + }; +} + +class Metadata { + Metadata(); + + factory Metadata.fromJson() => Metadata(); + + Map toJson() => {}; +} + +class CourseData { + CourseData( + {this.subjectCode, + this.courseCode, + this.departmentCode, + this.courseTitle, + this.unitsMin, + this.unitsMax, + this.unitsInc, + this.academicLevel, + this.sections}); + + factory CourseData.fromJson(Map json) => CourseData( + subjectCode: json['subjectCode'] ?? '', + courseCode: json['courseCode'] ?? '', + departmentCode: json['departmentCode'] ?? '', + courseTitle: json['courseTitle'] ?? '', + unitsMin: json['unitsMin'] ?? '', + unitsMax: json['unitsMax'] ?? '', + unitsInc: json['unitsInc'] ?? '', + academicLevel: json['academicLevel'] ?? '', + sections: List.from( + json['sections'].map((x) => SectionData.fromJson(x)))); + + String? subjectCode; + String? courseCode; + String? departmentCode; + String? courseTitle; + double? unitsMin; + double? unitsMax; + double? unitsInc; + String? academicLevel; + List? sections; + + Map toJson() => { + 'subjectCode': subjectCode, + 'courseCode': courseCode, + 'departmentCode': departmentCode, + 'courseTitle': courseTitle, + 'unitsMin': unitsMin, + 'unitsMax': unitsMax, + 'unitsInc': unitsInc, + 'academicLevel': academicLevel, + 'sections': List.from(sections!.map((x) => x.toJson())) + }; +} + +class SectionData { + SectionData( + {this.sectionId, + this.termCode, + this.sectionCode, + this.instructionType, + this.sectionStatus, + this.subtitle, + this.startDate, + this.endDate, + this.enrolledQuantity, + this.capacityQuantity, + this.stopEnrollmentFlag, + this.printFlag, + this.subterm, + this.planCode, + this.recurringMeetings, + this.additionalMeetings, + this.instructors, + //***********************// Only for prototpying purposes (p8gonzal) + this.units, + this.subjectCode, + this.courseCode}); + + factory SectionData.fromJson(Map json) => SectionData( + sectionId: json['sectionId'] ?? '', + termCode: json['termCode'] ?? '', + sectionCode: json['sectionCode'] ?? '', + instructionType: json['instructionType'] ?? '', + sectionStatus: json['sectionStatus'] ?? '', + subtitle: json['subtitle'] ?? '', + startDate: json['startDate'] ?? '', + endDate: json['endDate'] ?? '', + enrolledQuantity: json['enrolledQuantity'] ?? 0, + capacityQuantity: json['capacityQuantity'] ?? 0, + stopEnrollmentFlag: json['stopEnrollmentFlag'] ?? false, + printFlag: json['printFlag'] ?? '', + subterm: json['subterm'] ?? '', + planCode: json['planCode'] ?? '', + recurringMeetings: List.from( + json['recurringMeetings'].map((x) => MeetingData.fromJson(x))), + additionalMeetings: List.from( + json['additionalMeetings'].map((x) => MeetingData.fromJson(x))), + instructors: List.from( + json['instructors'].map((x) => Instructor.fromJson(x))), + //***********************// Only for prototpying purposes (p8gonzal) + units: json['units'] ?? 0, + subjectCode: json['subjectCode'] ?? '', + courseCode: json['courseCode'] ?? ''); + + String? sectionId; + String? termCode; + String? sectionCode; + String? instructionType; + String? sectionStatus; + String? subtitle; + String? startDate; + String? endDate; + int? enrolledQuantity; + int? capacityQuantity; + bool? stopEnrollmentFlag; + String? printFlag; + String? subterm; + String? planCode; + List? recurringMeetings; + List? additionalMeetings; + List? instructors; + //***********************// Only for prototpying purposes (p8gonzal) + double? units; + String? subjectCode; + String? courseCode; + + Map toJson() => { + 'sectionId': sectionId, + 'termCode': termCode, + 'sectionCode': sectionCode, + 'instructionType': instructionType, + 'sectionStatus': sectionStatus, + 'subtitle': subtitle, + 'startDate': startDate, + 'endDate': endDate, + 'enrolledQuantity': enrolledQuantity, + 'capacityQuantity': capacityQuantity, + 'stopEnrollmentFlag': stopEnrollmentFlag, + 'printFlag': printFlag, + 'subterm': subterm, + 'planCode': planCode, + 'recurringMeetings': + List.from(recurringMeetings!.map((x) => x.toJson())), + 'additionalMeetings': + List.from(additionalMeetings!.map((x) => x.toJson())), + 'instructors': List.from(instructors!.map((x) => x.toJson())), + //***********************// Only for prototpying purposes (p8gonzal) + 'units': units, + 'subjectCode': subjectCode, + 'courseCode': courseCode + }; +} + +class MeetingData { + MeetingData( + {this.meetingType, + this.meetingDate, + this.dayCode, + this.dayCodeIsis, + this.startTime, + this.endTime, + this.buildingCode, + this.roomCode}); + + factory MeetingData.fromJson(Map json) => MeetingData( + meetingType: json['meetingType'] ?? '', + meetingDate: json['meetingDate'] ?? '', + dayCode: json['dayCode'] ?? '', + dayCodeIsis: json['dayCodeIsis'] ?? '', + startTime: json['startTime'] ?? '', + endTime: json['endTime'] ?? '', + buildingCode: json['buildingCode'] ?? '', + roomCode: json['roomCode'] ?? ''); + + String? meetingType; + String? meetingDate; + String? dayCode; + String? dayCodeIsis; + String? startTime; + String? endTime; + String? buildingCode; + String? roomCode; + + Map toJson() => { + 'meetingType': meetingType, + 'meetingDate': meetingDate, + 'dayCode': dayCode, + 'dayCodeIsis': dayCodeIsis, + 'startTime': startTime, + 'endTime': endTime, + 'buildingCode': buildingCode, + 'roomCode': roomCode + }; +} + +class Instructor { + Instructor( + {this.pid, + this.instructorName, + this.primaryInstructor, + this.instructorEmailAddress, + this.workLoadUnitQty, + this.percentOfLoad}); + + factory Instructor.fromJson(Map json) => Instructor( + pid: json['pid'] ?? '', + instructorName: json['instructorName'] ?? '', + primaryInstructor: json['primaryInstructor'] ?? false, + instructorEmailAddress: json['instructorEmailAddress'] ?? '', + workLoadUnitQty: json['workLoadUnitQty'] ?? 1, + percentOfLoad: json['percentOfLoad'] ?? 100); + + String? pid; + String? instructorName; + bool? primaryInstructor; + String? instructorEmailAddress; + double? workLoadUnitQty; + double? percentOfLoad; + + Map toJson() => { + 'pid': pid, + 'instructorName': instructorName, + 'primaryInstructor': primaryInstructor, + 'instructorEmailAddress': instructorEmailAddress, + 'workLoadUnityQty': workLoadUnitQty, + 'percentOfLoad': percentOfLoad + }; +} diff --git a/lib/core/models/user.dart b/lib/core/models/user.dart new file mode 100644 index 0000000..53c22eb --- /dev/null +++ b/lib/core/models/user.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; +import 'dart:core'; + +import 'package:webreg_mobile_flutter/core/models/schedule_of_classes.dart'; + +UserModel userModelFromJson(String str) => UserModel.fromJson(json.decode(str)); + +String userModelToJson(UserModel data) => json.encode(data.toJson()); + +class UserModel { + UserModel( + {this.enrolledCourses, this.waitlistedCourses, this.plannedCourses}); + + factory UserModel.fromJson(Map json) => UserModel( + enrolledCourses: List.from(json['enrolledCourses'] + .map((Map x) => CourseData.fromJson(x))), + waitlistedCourses: List.from(json['waitlistedCourses'] + .map((Map x) => CourseData.fromJson(x))), + plannedCourses: List.from(json['plannedCourses'] + .map((Map x) => CourseData.fromJson(x)))); + + List? enrolledCourses; + List? waitlistedCourses; + List? plannedCourses; + + Map toJson() => { + 'enrolledCourses': List.from( + enrolledCourses!.map((CourseData x) => x.toJson())), + 'waitlistedCourses': List.from( + enrolledCourses!.map((CourseData x) => x.toJson())), + 'plannedCourses': List.from( + enrolledCourses!.map((CourseData x) => x.toJson())), + }; +} diff --git a/lib/core/providers/profile.dart b/lib/core/providers/profile.dart new file mode 100644 index 0000000..a00a88f --- /dev/null +++ b/lib/core/providers/profile.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:webreg_mobile_flutter/core/models/profile.dart'; +import 'package:webreg_mobile_flutter/core/providers/user.dart'; +import 'package:webreg_mobile_flutter/core/services/profile.dart'; + +class ProfileProvider extends ChangeNotifier { + ProfileProvider() { + /// Default States + _isLoading = false; + _noResults = false; + + /// Services initialized + _profileService = ProfileService(); + + /// Models initalized + _profileModel = ProfileModel(); + } + + /// STATES + bool? _isLoading; + DateTime? _lastUpdated; + String? _error; + bool? _noResults; + + /// MODELS + ProfileModel _profileModel = ProfileModel(); + + /// SERVICES + late ProfileService _profileService; + + late UserDataProvider _userDataProvider; + + set userDataProvider(UserDataProvider userDataProvider) { + _userDataProvider = userDataProvider; + } + + ProfileService get profileService => _profileService; + + Future fetchProfile() async { + final Map headers = { + 'Authorization': 'Bearer ${_userDataProvider.accessToken}' + }; + + return _profileService.fetchProfile(headers); + } + + /// SIMPLE GETTERS + bool? get isLoading => _isLoading; + String? get error => _error; + DateTime? get lastUpdated => _lastUpdated; + ProfileModel get profileModel => _profileModel; + bool? get noResults => _noResults; +} diff --git a/lib/core/providers/schedule_of_classes.dart b/lib/core/providers/schedule_of_classes.dart new file mode 100644 index 0000000..41ee0b3 --- /dev/null +++ b/lib/core/providers/schedule_of_classes.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:webreg_mobile_flutter/core/models/schedule_of_classes.dart'; +import 'package:webreg_mobile_flutter/core/providers/user.dart'; +import 'package:webreg_mobile_flutter/core/services/schedule_of_classes.dart'; + +class ScheduleOfClassesProvider extends ChangeNotifier { + ScheduleOfClassesProvider() { + /// DEFAULT STATES + _isLoading = false; + _noResults = false; + + /// initialize services here + _scheduleOfClassesService = ScheduleOfClassesService(); + + _scheduleOfClassesModels = ScheduleOfClassesModel(); + } + + /// STATES + bool? _isLoading; + DateTime? _lastUpdated; + String? _error; + bool? _noResults; + + /// MODELS + ScheduleOfClassesModel _scheduleOfClassesModels = ScheduleOfClassesModel(); + String? searchQuery; + String? term; + TextEditingController _searchBarController = TextEditingController(); + //UserDataProvider? _userDataProvider; + bool? lowerDiv; + bool? upperDiv; + bool? graduateDiv; + + /// SERVICES + late ScheduleOfClassesService _scheduleOfClassesService; + + ScheduleOfClassesService get scheduleOfClassesService => + _scheduleOfClassesService; + + late UserDataProvider _userDataProvider; + + set userDataProvider(UserDataProvider userDataProvider) { + _userDataProvider = userDataProvider; + } + + Future fetchClasses(String query) async { + final Map headers = { + 'Authorization': 'Bearer ${_userDataProvider.accessToken}' + }; + + return _scheduleOfClassesService.fetchClasses(headers, query); + } + + String createQuery(String query) { + /// create api call format here + return query; + } + + ///SIMPLE GETTERS + bool? get isLoading => _isLoading; + String? get error => _error; + DateTime? get lastUpdated => _lastUpdated; + ScheduleOfClassesModel get scheduleOfClassesModels => + _scheduleOfClassesModels; + TextEditingController get searchBarController => _searchBarController; + bool? get noResults => _noResults; + + ///Settlers + set searchBarController(TextEditingController value) { + _searchBarController = value; + notifyListeners(); + } +} diff --git a/lib/core/providers/user.dart b/lib/core/providers/user.dart new file mode 100644 index 0000000..3e70f89 --- /dev/null +++ b/lib/core/providers/user.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class UserDataProvider extends ChangeNotifier { + String? accessToken = ''; + + set setToken(String? token) { + accessToken = token; + } + + String? get getToken { + return accessToken; + } +} diff --git a/lib/core/services/authentication.dart b/lib/core/services/authentication.dart new file mode 100644 index 0000000..f80ef76 --- /dev/null +++ b/lib/core/services/authentication.dart @@ -0,0 +1,7 @@ + +class AuthenticationService { + AuthenticationService(); + String? error; + + +} diff --git a/lib/core/services/profile.dart b/lib/core/services/profile.dart new file mode 100644 index 0000000..a11c298 --- /dev/null +++ b/lib/core/services/profile.dart @@ -0,0 +1,35 @@ +import 'package:webreg_mobile_flutter/app_networking.dart'; +import 'package:webreg_mobile_flutter/core/models/profile.dart'; + +class ProfileService { + // TODO(p8gonzal): Note to not use this prototype. For development purposes only. + final String profilePrototype = + 'https://i4ghbvwuo9.execute-api.us-west-2.amazonaws.com/qa/profile'; + + String? _error; + ProfileModel? profile; + + final NetworkHelper _networkHelper = const NetworkHelper(); + + Future fetchProfile(Map headers) async { + _error = null; + + try { + + /// Fetch data + final String? _response = + await _networkHelper.fetchData(profilePrototype); + if (_response != null) { + /// Parse data + profile = profileModelFromJson(_response); + } else { + return false; + } + return true; + } catch (e) { + _error = e.toString(); + print(_error); + return false; + } + } +} diff --git a/lib/core/services/schedule_of_classes.dart b/lib/core/services/schedule_of_classes.dart new file mode 100644 index 0000000..946d454 --- /dev/null +++ b/lib/core/services/schedule_of_classes.dart @@ -0,0 +1,99 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:webreg_mobile_flutter/app_networking.dart'; +import 'package:webreg_mobile_flutter/core/models/schedule_of_classes.dart'; +import 'package:webreg_mobile_flutter/core/providers/user.dart'; + +class ScheduleOfClassesService { + final String academicTermAPI = + 'https://o17lydfach.execute-api.us-west-2.amazonaws.com/qa-peter/v1/term/current'; + bool _isLoading = false; + DateTime? _lastUpdated; + String? _error; + ScheduleOfClassesModel? classes; + UserDataProvider? userDataProvider = UserDataProvider(); + + final NetworkHelper _networkHelper = const NetworkHelper(); + + final String baseEndpoint = + 'https://api-qa.ucsd.edu:8243/get_schedule_of_classes/v1/classes/search'; + + Future fetchClasses(Map headers, String query) async { + _error = null; + _isLoading = true; + try { + /// fetch data + final String? _response = await _networkHelper.authorizedFetch( + baseEndpoint + '?' + query, headers); + if (_response != null) { + /// parse data + final ScheduleOfClassesModel data = + classScheduleModelFromJson(_response); + classes = data; + } else { + _isLoading = false; + return false; + } + return true; + } catch (e) { + _error = e.toString(); + _isLoading = false; + return false; + } + } + + Future> fetchTerms() async { + final List terms = ['FA', 'WI', 'SP', 'S1', 'S2']; + + try { + /// fetch data + final Map headers = { + 'accept': 'application/json', + }; + final String _response = + await _networkHelper.authorizedFetch(academicTermAPI, headers); + + // final Map responseMapping = json.decode(_response); + final String currentTerm = json.decode(_response)['term_code']; + final String currentYear = currentTerm.substring(2, 4); + final String currentQuarter = currentTerm.substring(0, 2); + final int indexOfQuarter = terms.indexOf(currentQuarter); + + //Rotate terms clockwise to present current term as last + for (int i = indexOfQuarter; i < terms.length - 1; i++) { + final String lastQuarter = terms[terms.length - 1]; + for (int quarterIndex = terms.length - 1; + quarterIndex > 0; + quarterIndex--) { + terms[quarterIndex] = terms[quarterIndex - 1]; + } + terms[0] = lastQuarter; + } + + // Illustration of possible term structure + // Update prefixes for each term + //WI21 - SP21 - S121 - S221 - FA21 + //SP21 - S121 - S221 - FA21 - WI22 + //S121 - S221 - FA21 - WI22 - SP22 + //S221 - FA21 - WI22 - SP22 - S122 + //FA21 - WI22 - SP22 - S122 - S222 + final int fallIdx = terms.indexOf('FA'); + for (int i = 0; i < terms.length; i++) { + if (i > fallIdx) { + terms[i] = terms[i] + currentYear; + } else { + terms[i] = terms[i] + (int.parse(currentYear) - 1).toString(); + } + } + return terms; + } catch (e) { + return terms; + } + } + + bool get isLoading => _isLoading; + + String? get error => _error; + + DateTime? get lastUpdated => _lastUpdated; +} diff --git a/lib/generated_plugin_registrant.dart b/lib/generated_plugin_registrant.dart new file mode 100644 index 0000000..256a7dc --- /dev/null +++ b/lib/generated_plugin_registrant.dart @@ -0,0 +1,18 @@ +// +// Generated file. Do not edit. +// + +// ignore_for_file: directives_ordering +// ignore_for_file: lines_longer_than_80_chars + +import 'package:flutter_secure_storage_web/flutter_secure_storage_web.dart'; +import 'package:package_info_plus_web/package_info_plus_web.dart'; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +// ignore: public_member_api_docs +void registerPlugins(Registrar registrar) { + FlutterSecureStorageWeb.registerWith(registrar); + PackageInfoPlugin.registerWith(registrar); + registrar.registerMessageHandler(); +} diff --git a/lib/main.dart b/lib/main.dart index a643823..ddc3815 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,35 +1,56 @@ +import 'dart:html'; + import 'package:flutter/material.dart'; -import 'package:webreg_mobile_flutter/ui/search/search_placeholder.dart'; -import 'package:webreg_mobile_flutter/app_styles.dart'; +import 'package:get/route_manager.dart'; +import 'package:provider/provider.dart'; import 'package:webreg_mobile_flutter/app_constants.dart'; -import 'package:webreg_mobile_flutter/app_router.dart' - as webregMobileRouter; +import 'package:webreg_mobile_flutter/app_provider.dart'; +import 'package:webreg_mobile_flutter/app_router.dart' as webreg_router; +import 'package:webreg_mobile_flutter/app_styles.dart'; void main() { runApp(MyApp()); } +// ignore: must_be_immutable class MyApp extends StatelessWidget { + MyApp({Key? key}) : super(key: key); + + late String? _token = ''; + + // Check if token is present in the UI, true when access from Campus Mobile + void getParams() { + final Uri uri = Uri.dataFromString(window.location.href); + final Map params = uri.queryParameters; + if (params['token'] != null) { + _token = params['token']; + } + } + // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: ColorPrimary, - primaryColor: lightPrimaryColor, - accentColor: darkAccentColor, - brightness: Brightness.light, - buttonColor: lightButtonColor, - textTheme: lightThemeText, - iconTheme: lightIconTheme, - appBarTheme: lightAppBarTheme, - bottomSheetTheme: BottomSheetThemeData( - backgroundColor: Colors.black.withOpacity(0) + getParams(); + + return MultiProvider( + providers: providers, + child: GetMaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + primaryColor: lightPrimaryColor, + brightness: Brightness.light, + textTheme: lightThemeText, + iconTheme: lightIconTheme, + appBarTheme: lightAppBarTheme, + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: Colors.black.withOpacity(0)), + colorScheme: ColorScheme.fromSwatch(primarySwatch: ColorPrimary) + .copyWith(secondary: darkAccentColor), ), + initialRoute: + _token == '' ? RoutePaths.AuthenticationError : RoutePaths.Home, + onGenerateRoute: webreg_router.Router.generateRoute, ), - initialRoute: RoutePaths.Home, - onGenerateRoute: webregMobileRouter.Router.generateRoute, ); } } diff --git a/lib/ui/calendar/bottom_course_card.dart b/lib/ui/calendar/bottom_course_card.dart index dc0f8f5..6bc5eba 100644 --- a/lib/ui/calendar/bottom_course_card.dart +++ b/lib/ui/calendar/bottom_course_card.dart @@ -1,65 +1,96 @@ +// ignore_for_file: use_key_in_widget_constructors + import 'package:flutter/material.dart'; import 'package:webreg_mobile_flutter/app_styles.dart'; class BottomCourseCard extends StatefulWidget { + const BottomCourseCard(this.context); final BuildContext context; - BottomCourseCard(this.context); - @override _BottomCourseCardState createState() => _BottomCourseCardState(); } class _BottomCourseCardState extends State { void onClick(BuildContext context) { - showModalBottomSheet(context: context, builder: (BuildContext context) { - return Container( - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Text('This is the modal bottom sheet. Tap anywhere to dismiss.', - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).accentColor, - fontSize: 24.0 - ) - ) - ) - ); - }); + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return Padding( + padding: const EdgeInsets.all(32.0), + child: Text( + 'This is the modal bottom sheet. Tap anywhere to dismiss.', + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontSize: 24.0))); + }); } - Widget renderSection() { + Widget renderSection() { return Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - flex: 3, - child: Row( - children: [ - Text('A00', style: TextStyle(fontSize: 11, color: darkGray)), // TODO - Text(' LE', style: TextStyle(fontSize: 11, color: darkGray)) // TODO - ], - ) - ), - Expanded( - flex: 3, - child: Row( - children: [ - Text('M', style: TextStyle(fontSize: 11, color: ColorPrimary)), // TODO - Text('T', style: TextStyle(fontSize: 11, color: lightGray)), // TODO - Text('W', style: TextStyle(fontSize: 11, color: ColorPrimary)), // TODO - Text('T', style: TextStyle(fontSize: 11, color: lightGray)), // TODO - Text('F', style: TextStyle(fontSize: 11, color: ColorPrimary)), // TODO - ], - ) - ), + flex: 3, + child: Row( + children: const [ + Text('A00', + style: TextStyle( + fontSize: 11, + color: + darkGray)), // TODO(p8gonzal): Connect with class schedule api or local profile + Text(' LE', + style: TextStyle( + fontSize: 11, + color: + darkGray)) // TODO(p8gonzal): Connect with class schedule api or local profile + ], + )), Expanded( + flex: 3, + child: Row( + children: const [ + Text('M', + style: TextStyle( + fontSize: 11, + color: + ColorPrimary)), // TODO(p8gonzal): Connect with class schedule api or local profile + Text('T', + style: TextStyle( + fontSize: 11, + color: + lightGray)), // TODO(p8gonzal): Connect with class schedule api or local profile + Text('W', + style: TextStyle( + fontSize: 11, + color: + ColorPrimary)), // TODO(p8gonzal): Connect with class schedule api or local profile + Text('T', + style: TextStyle( + fontSize: 11, + color: + lightGray)), // TODO(p8gonzal): Connect with class schedule api or local profile + Text('F', + style: TextStyle( + fontSize: 11, + color: + ColorPrimary)), // TODO(p8gonzal): Connect with class schedule api or local profile + ], + )), + const Expanded( flex: 5, - child: Text('3:30p - 4:50p', style: TextStyle(fontSize: 11, color: ColorPrimary)), // TODO + child: Text('3:30p - 4:50p', + style: TextStyle( + fontSize: 11, + color: + ColorPrimary)), // TODO(p8gonzal): Connect with class schedule api or local profile ), - Expanded( + const Expanded( flex: 5, - child: Text('PCYHN 106', style: TextStyle(fontSize: 11)), // TODO + child: Text('PCYHN 106', + style: TextStyle( + fontSize: + 11)), // TODO(p8gonzal): Connect with class schedule api or local profile ) ], ); @@ -70,27 +101,27 @@ class _BottomCourseCardState extends State { return Container( color: Colors.transparent, width: double.infinity, - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: Container( // shape: RoundedRectangleBorder( // borderRadius: BorderRadius.circular(10.0) // ), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(10.0)), + borderRadius: const BorderRadius.all(Radius.circular(10.0)), // border: Border.all(width: 1, color: ) - boxShadow: [ + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.25), spreadRadius: 0, blurRadius: 2.5, - offset: Offset(1, 1), + offset: const Offset(1, 1), ), BoxShadow( color: Colors.black.withOpacity(0.25), spreadRadius: 0, blurRadius: 2.5, - offset: Offset(-1, 1), + offset: const Offset(-1, 1), ), ], ), @@ -101,192 +132,94 @@ class _BottomCourseCardState extends State { children: [ // card title Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topRight: Radius.circular(10.0), - topLeft: Radius.circular(10.0), + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(10.0), + topLeft: Radius.circular(10.0), + ), + color: lightBlue, ), - color: lightBlue, - ), - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row(children: [ // units icon Container( - height: 35, - width: 35, - decoration: new BoxDecoration( - color: lightGray, - shape: BoxShape.circle, - ), - margin: EdgeInsets.only(right: 10), - child: Center( - child: Text( - '4',// TODO + height: 35, + width: 35, + decoration: const BoxDecoration( + color: lightGray, + shape: BoxShape.circle, + ), + margin: const EdgeInsets.only(right: 10), + child: const Center( + child: Text( + '4', // TODO(p8gonzal): Once data becomes available, unharcode style: TextStyle(fontSize: 18), - ) - ) - ), + ))), // course info Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('CSE 12', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), // TODO - GestureDetector( - child: Icon(Icons.close, size: 20, color: darkGray), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('CSE 12', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight + .bold)), // TODO(p8gonzal): Once data becomes available, unharcode + GestureDetector( + child: const Icon(Icons.close, + size: 20, color: darkGray), onTap: () { Navigator.pop(widget.context); - } - ) - ], - ), - Text('Basic Data Struct & OO design', style: TextStyle(fontSize: 16)) // TODO - ], - ) - ) - ] - ) - ), + }) + ], + ), + const Text('Basic Data Struct & OO design', + style: TextStyle( + fontSize: + 16)) // TODO(p8gonzal): Once data becomes available, unharcode + ], + )) + ])), Container( - margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - children: [ - // instructor andd section id - Container( - margin: EdgeInsets.only(top: 4, bottom: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Gillespie, Gary N', style: TextStyle(color: ColorPrimary, fontSize: 12)), // TODO - Row( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + children: [ + // instructor andd section id + Container( + margin: const EdgeInsets.only(top: 4, bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Section ID', style: TextStyle(color: darkGray, fontSize: 12)), // TODO - Text(' 983761', style: TextStyle(fontSize: 12)), // TODO - ] - ) - ] + const Text('Gillespie, Gary N', + style: TextStyle( + color: ColorPrimary, + fontSize: + 12)), // TODO(p8gonzal): Once data becomes available, unharcode + Row(children: const [ + Text('Section ID', + style: TextStyle( + color: darkGray, + fontSize: + 12)), // TODO(p8gonzal): Once data becomes available, unharcode + Text(' 983761', + style: TextStyle( + fontSize: + 12)), // TODO(p8gonzal): Once data becomes available, unharcode + ]) + ]), ), - ), - // course sections: di, final - renderSection(), - renderSection(), - ], - ) - ) + // course sections: di, final + renderSection(), + renderSection(), + ], + )) ], ), ), ); - - // return ElevatedButton( - // child: const Text('showBottomSheet'), - // onPressed: () { - // Scaffold.of(context).showBottomSheet( - // (BuildContext context) { - // return Container( - // color: Colors.transparent, - // width: double.infinity, - // padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), - // child: Card( - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(10.0) - // ), - // child: Column( - // mainAxisAlignment: MainAxisAlignment.start, - // crossAxisAlignment: CrossAxisAlignment.start, - // mainAxisSize: MainAxisSize.min, - // children: [ - // // card title - // Container( - // decoration: BoxDecoration( - // borderRadius: BorderRadius.only( - // topRight: Radius.circular(10.0), - // topLeft: Radius.circular(10.0), - // ), - // color: lightBlue, - // ), - // padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - // child: Row( - // children: [ - // // units icon - // Container( - // height: 35, - // width: 35, - // decoration: new BoxDecoration( - // color: lightGray, - // shape: BoxShape.circle, - // ), - // margin: EdgeInsets.only(right: 10), - // child: Center( - // child: Text( - // '4',// TODO - // style: TextStyle(fontSize: 18), - // ) - // ) - // ), - // // course info - // Expanded( - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text('CSE 12', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), // TODO - // GestureDetector( - // child: Icon(Icons.close, size: 20, color: darkGray), - // onTap: () { - // Navigator.pop(context); - // } - // ) - // ], - // ), - // Text('Basic Data Struct & OO design', style: TextStyle(fontSize: 16)) // TODO - // ], - // ) - // ) - // ] - // ) - // ), - // Container( - // margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - // child: Column( - // children: [ - // // instructor andd section id - // Container( - // margin: EdgeInsets.only(top: 4, bottom: 8), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text('Gillespie, Gary N', style: TextStyle(color: ColorPrimary, fontSize: 12)), // TODO - // Row( - // children: [ - // Text('Section ID', style: TextStyle(color: darkGray, fontSize: 12)), // TODO - // Text(' 983761', style: TextStyle(fontSize: 12)), // TODO - // ] - // ) - // ] - // ), - // ), - // // course sections: di, final - // renderSection(), - // renderSection(), - // ], - // ) - // ) - // ], - // ), - // ), - // ); - // }, - // ); - // }, - // ); } } - - diff --git a/lib/ui/calendar/calendar.dart b/lib/ui/calendar/calendar.dart index 48de6c3..16b6fc9 100644 --- a/lib/ui/calendar/calendar.dart +++ b/lib/ui/calendar/calendar.dart @@ -1,155 +1,84 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; import 'package:webreg_mobile_flutter/app_constants.dart'; import 'package:webreg_mobile_flutter/app_styles.dart'; +import 'package:webreg_mobile_flutter/core/models/profile.dart'; +import 'package:webreg_mobile_flutter/core/models/schedule_of_classes.dart'; +import 'package:webreg_mobile_flutter/core/providers/profile.dart'; +import 'package:webreg_mobile_flutter/core/providers/user.dart'; import 'package:webreg_mobile_flutter/ui/calendar/calendar_card.dart'; -import 'package:webreg_mobile_flutter/ui/common/build_info.dart'; -class Calendar extends StatelessWidget { - Calendar(this.color); +class Calendar extends StatefulWidget { + const Calendar({Key? key, required this.calendarType}) : super(key: key); + final String calendarType; - final Color color; - - static const earliestClass = '08:00'; - - static const times = [ - '8am', - '9am', - '10am', - '11am', - '12pm', - '1pm', - '2pm', - '3pm', - '4pm', - '5pm', - '6pm', - '7pm', - '8pm', - '9pm', - '10pm', - ]; - - static const dayOfWeek = [ - 'Mon', - 'Tues', - 'Wed', - 'Thurs', - 'Fri', - 'Sat', - 'Sun', - ]; + @override + State createState() => _CalendarViewState(); +} - static const courses = [ - { - 'datePrefix': '2020-06-06T', - 'startTime': '09:00', - 'endTime': '09:50', - 'dayOfWeek': 1, - 'type': 'LE', - 'title': 'CSE 120', - 'location': 'PCYNH 112', - }, - { - 'datePrefix': '2020-06-06T', - 'startTime': '09:00', - 'endTime': '09:50', - 'dayOfWeek': 3, - 'type': 'LE', - 'title': 'CSE 120', - 'location': 'PCYNH 112', - }, - { - 'datePrefix': '2020-06-06T', - 'startTime': '09:00', - 'endTime': '09:50', - 'dayOfWeek': 5, - 'type': 'LE', - 'title': 'CSE 120', - 'location': 'PCYNH 112', - }, - { - 'datePrefix': '2020-06-06T', - 'startTime': '14:00', - 'endTime': '15:20', - 'dayOfWeek': 1, - 'type': 'LE', - 'title': 'COGS 10', - 'location': 'WLH 110', - }, - { - 'datePrefix': '2020-06-06T', - 'startTime': '10:00', - 'endTime': '10:50', - 'dayOfWeek': 1, - 'type': 'DI', - 'title': 'CSE 123', - 'location': 'PCYNH 112', - }, - ]; +class _CalendarViewState extends State { + late ProfileProvider profileProvider; + late String calendarType; - double getTimeDifference(String start, String end, String prefix) { - double diff = DateTime.parse(prefix + end) - .difference(DateTime.parse(prefix + start)) - .inMinutes - .toDouble(); - print(diff.toString()); - return diff; + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Update the profile provider to access remote profile + profileProvider = Provider.of(context, listen: false); + profileProvider.userDataProvider = + Provider.of(context, listen: false); } @override Widget build(BuildContext context) { - double calendarCardWidth = (MediaQuery.of(context).size.width - - CalendarStyles.calendarTimeWidth - - 20) / - 7; - + // Import arguement of calendar type: Lectures/Discussion or Finals + calendarType = widget.calendarType; return Container( color: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 10), - child: Column( - children: [ - // calendar header - Container( - height: CalendarStyles.calendarHeaderHeight, - padding: EdgeInsets.only(top: 20, bottom: 15), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: lightGray), - ), - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.05), - spreadRadius: 1, - blurRadius: 2, - offset: Offset(0, 1), // changes position of shadow - ), - ], + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column(children: [ + // Calendar header + Container( + height: CalendarStyles.calendarHeaderHeight, + padding: const EdgeInsets.only(top: 20, bottom: 15), + decoration: BoxDecoration( + border: const Border( + bottom: BorderSide(color: lightGray), ), - child: Row( - children: [ - SizedBox( - width: CalendarStyles.calendarTimeWidth, - ), - Expanded( - child: Row( - children: dayOfWeek - .map((day) => Expanded( - flex: 1, - child: Container( - child: Center( - child: Text(day, - style: TextStyle( - fontSize: 10, - letterSpacing: -0.1)))), - )) - .toList(), - )) - ], - )), - - Expanded( - child: Stack(children: [ + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.05), + spreadRadius: 1, + blurRadius: 2, + offset: const Offset(0, 1), // changes position of shadow + ), + ], + ), + child: Row( + children: [ + const SizedBox( + width: CalendarStyles.calendarTimeWidth, + ), + Expanded( + child: Row( + children: dayOfWeek + .map((String day) => Expanded( + flex: 1, + child: Center( + child: Text(day, + style: const TextStyle( + fontSize: 10, letterSpacing: -0.1))), + )) + .toList(), + )) + ], + )), + + Expanded( + child: Stack( + children: [ // calendar body ListView.builder( itemCount: times.length, @@ -157,9 +86,9 @@ class Calendar extends StatelessWidget { itemBuilder: (BuildContext context, int index) { return Container( height: CalendarStyles.calendarRowHeight, - margin: EdgeInsets.all(0), - padding: EdgeInsets.all(0), - decoration: BoxDecoration( + margin: const EdgeInsets.all(0), + padding: const EdgeInsets.all(0), + decoration: const BoxDecoration( border: Border( bottom: BorderSide(color: lightGray), ), @@ -171,25 +100,334 @@ class Calendar extends StatelessWidget { width: CalendarStyles.calendarTimeWidth, child: Center( child: Text(times[index], - style: TextStyle(fontSize: 10)), + style: const TextStyle(fontSize: 10)), )) ])); }), - CalendarCard('10:00', '10:50', '2020-06-06T', 0, 'LE', 'CSE 110', - 'Center 109'), - CalendarCard('10:00', '10:50', '2020-06-06T', 2, 'LE', 'CSE 110', - 'Center 109'), - CalendarCard('10:00', '10:50', '2020-06-06T', 4, 'LE', 'CSE 110', - 'Center 109'), - - CalendarCard('11:00', '12:20', '2020-06-06T', 1, 'DI', 'CSE 100', - 'WLH 109'), - CalendarCard('11:00', '12:20', '2020-06-06T', 3, 'DI', 'CSE 100', - 'WLH 109'), - ])), - BuildInfo(), - ], - )); + //Extract relevant data from the profile and create calendar cards + FutureBuilder( + future: profileProvider.fetchProfile(), + builder: + (BuildContext context, AsyncSnapshot response) { + // When profile has not been fetched yet, returns a loading widget + if (response.hasData) { + final ProfileModel model = + profileProvider.profileService.profile!; + + final List? courseList = + model.enrolledCourses; + + // For switching between Lectures/Discussion and Finals + if (calendarType == 'LECT_DISC') { + // Lists to contain section objects and parallel list with section types + final List sectionCards = + []; + final List sectionTypes = []; + final List sectionObjects = + []; + + // Idenitify all section types that are active + for (final SectionData section in courseList!) { + if (section.sectionStatus != 'CA') { + sectionTypes.add(section.instructionType!); + sectionObjects.add(section); + } + } + // Identify lecture objects + int sectionIndex = 0; + final List lectureObjects = + []; + + // Isolate lecture objects because we need extrapolate days and times + while (sectionTypes.contains('LE')) { + final int lectureIndex = sectionTypes.indexOf('LE'); + final SectionData lectureObject = + sectionObjects.elementAt(lectureIndex); + lectureObjects.add(lectureObject); + sectionTypes.removeAt(lectureIndex); + sectionObjects.removeAt(lectureIndex); + } + + // Begin building calendar card for discussions/labs + sectionIndex = 0; + for (final String sectionType + in sectionTypes.toList()) { + sectionCards.add(buildCalendarCard( + sectionType, sectionObjects[sectionIndex])); + sectionIndex++; + } + + // Create new section objects with the first meeting in {recurringMeetings} is the correct day and time + for (final SectionData lecture in lectureObjects) { + for (int meetingIndex = 0; + meetingIndex < lecture.recurringMeetings!.length; + meetingIndex++) { + final SectionData copyOfLectureObject = + SectionData.fromJson(lecture.toJson()); + copyOfLectureObject.recurringMeetings = + []; + copyOfLectureObject.recurringMeetings! + .add(lecture.recurringMeetings![meetingIndex]); + sectionTypes + .add(copyOfLectureObject.instructionType!); + sectionObjects.add(copyOfLectureObject); + } + } + + // Build remaining section cards + sectionIndex = 0; + for (final String sectionType + in sectionTypes.toList()) { + sectionCards.add(buildCalendarCard( + sectionType, sectionObjects[sectionIndex])); + sectionIndex++; + } + + return Stack(children: [...sectionCards]); + } else { + // Lists to contain section objects and parallel list with section types + final List sectionCards = + []; + final List sectionTypes = []; + final List sectionObjects = + []; + + // Idenitfy all lecture objects as those contain final data + for (final SectionData section in courseList!) { + if (section.instructionType == 'LE') { + sectionTypes.add('FI'); + sectionObjects.add(section); + } + } + + // Build calendar cards from lecture objects, isolates final meeting data + int sectionIndex = 0; + for (final String sectionType + in sectionTypes.toList()) { + sectionCards.add(buildCalendarCard( + sectionType, sectionObjects[sectionIndex])); + sectionIndex++; + } + + return Stack(children: [...sectionCards]); + } + } else { + return CalendarCard('10:00', '10:50', '2020-06-06T', 0, + 'LE', '', '', Colors.blue.shade200); + } + }), + ], + )) + // BuildInfo(), + ])); + } + + /// @param meeting Meeting object to standardize start time and end time + void correctTimeFormat(MeetingData meeting) { + while (meeting.startTime!.length < 4) { + meeting.startTime = '0' + meeting.startTime!; + } + while (meeting.endTime!.length < 4) { + meeting.endTime = '0' + meeting.endTime!; + } + } + + static const List dayOfWeek = [ + 'Mon', + 'Tues', + 'Wed', + 'Thurs', + 'Fri', + 'Sat', + 'Sun', + ]; + + static const Map dayMapping = { + 'MO': 0, + 'TU': 1, + 'WE': 2, + 'TH': 3, + 'FR': 4 + }; + + static const List times = [ + '8am', + '9am', + '10am', + '11am', + '12pm', + '1pm', + '2pm', + '3pm', + '4pm', + '5pm', + '6pm', + '7pm', + '8pm', + '9pm', + '10pm', + ]; + + /// @param sectionType Type of section for calendar card we are creating: LE, DI, LA, FI ... + /// @param sectionObject Section object containing course info needed to create calendar card + CalendarCard buildCalendarCard( + String sectionType, SectionData sectionObject) { + switch (sectionType) { + case 'LE': + { + // Time parsing + // Checking for classes that start have *** format instead of **** + correctTimeFormat(sectionObject.recurringMeetings!.first); + const String prefix = '0000-01-01T'; + String startTime = DateFormat.jm().format(DateTime.parse( + prefix + sectionObject.recurringMeetings!.first.startTime!)); + String endTime = DateFormat.jm().format(DateTime.parse( + prefix + sectionObject.recurringMeetings!.first.endTime!)); + startTime = startTime.toLowerCase().replaceAll(' ', ''); + endTime = endTime.toLowerCase().replaceAll(' ', ''); + + // Room Code and Number parsing + final String room = + sectionObject.recurringMeetings!.first.buildingCode! + + ' ' + + sectionObject.recurringMeetings!.first.roomCode!.substring(1); + // Construct Card widget + return CalendarCard( + sectionObject.recurringMeetings!.first.startTime!, + sectionObject.recurringMeetings!.first.endTime!, + prefix, + dayMapping[sectionObject.recurringMeetings!.first.dayCode]!, + sectionType, + '${sectionObject.subjectCode} ${sectionObject.courseCode}', + room, + Colors.blue.shade200); + } + case 'DI': + { + // Time parsing + // Checking for classes that start have *** format instead of **** + correctTimeFormat(sectionObject.recurringMeetings!.first); + const String prefix = '0000-01-01T'; + String startTime = DateFormat.jm().format(DateTime.parse( + prefix + sectionObject.recurringMeetings!.first.startTime!)); + String endTime = DateFormat.jm().format(DateTime.parse( + prefix + sectionObject.recurringMeetings!.first.endTime!)); + startTime = startTime.toLowerCase().replaceAll(' ', ''); + endTime = endTime.toLowerCase().replaceAll(' ', ''); + + // Room Code and Number parsing + final String room = + sectionObject.recurringMeetings!.first.buildingCode! + + ' ' + + sectionObject.recurringMeetings!.first.roomCode!.substring(1); + + // Construct Card widget + return CalendarCard( + sectionObject.recurringMeetings!.first.startTime!, + sectionObject.recurringMeetings!.first.endTime!, + prefix, + dayMapping[sectionObject.recurringMeetings!.first.dayCode]!, + 'DI', + '${sectionObject.subjectCode} ${sectionObject.courseCode}', + room, + Colors.blue.shade200); + } + case 'LA': + { + // Time parsing + // Checking for classes that start have *** format instead of **** + correctTimeFormat(sectionObject.recurringMeetings!.first); + const String prefix = '0000-01-01T'; + String startTime = DateFormat.jm().format(DateTime.parse( + prefix + sectionObject.recurringMeetings!.first.startTime!)); + String endTime = DateFormat.jm().format(DateTime.parse( + prefix + sectionObject.recurringMeetings!.first.endTime!)); + startTime = startTime.toLowerCase().replaceAll(' ', ''); + endTime = endTime.toLowerCase().replaceAll(' ', ''); + + // Room Code and Number parsing + final String room = + sectionObject.recurringMeetings!.first.buildingCode! + + ' ' + + sectionObject.recurringMeetings!.first.roomCode!.substring(1); + + // Construct Card widget + return CalendarCard( + sectionObject.recurringMeetings!.first.startTime!, + sectionObject.recurringMeetings!.first.endTime!, + prefix, + dayMapping[sectionObject.recurringMeetings!.first.dayCode]!, + 'LA', + '${sectionObject.subjectCode} ${sectionObject.courseCode}', + room, + Colors.blue.shade200); + } + case 'FI': + { + MeetingData finalMeeting = MeetingData(); + for (final MeetingData meetingData + in sectionObject.additionalMeetings!) { + if (meetingData.meetingType == 'FI') { + finalMeeting = meetingData; + } + } + + // Time parsing + // Checking for classes that start have *** format instead of **** + correctTimeFormat(finalMeeting); + const String prefix = '0000-01-01T'; + String startTime = DateFormat.jm() + .format(DateTime.parse(prefix + finalMeeting.startTime!)); + String endTime = DateFormat.jm() + .format(DateTime.parse(prefix + finalMeeting.endTime!)); + startTime = startTime.toLowerCase().replaceAll(' ', ''); + endTime = endTime.toLowerCase().replaceAll(' ', ''); + + // Room Code and Number parsing + final String room = finalMeeting.buildingCode! + + ' ' + + finalMeeting.roomCode!.substring(1); + + // Construct Card widget + return CalendarCard( + finalMeeting.startTime!, + finalMeeting.endTime!, + prefix, + dayMapping[sectionObject.recurringMeetings!.first.dayCode]!, + 'FI', + '${sectionObject.subjectCode} ${sectionObject.courseCode}', + room, + Colors.blue.shade200); + } + default: + // Time parsing + // Checking for classes that start have *** format instead of **** + correctTimeFormat(sectionObject.recurringMeetings!.first); + const String prefix = '0000-01-01T'; + String startTime = DateFormat.jm().format(DateTime.parse( + prefix + sectionObject.recurringMeetings!.first.startTime!)); + String endTime = DateFormat.jm().format(DateTime.parse( + prefix + sectionObject.recurringMeetings!.first.endTime!)); + startTime = startTime.toLowerCase().replaceAll(' ', ''); + endTime = endTime.toLowerCase().replaceAll(' ', ''); + + // Room Code and Number parsing + final String room = + sectionObject.recurringMeetings!.first.buildingCode! + + ' ' + + sectionObject.recurringMeetings!.first.roomCode!.substring(1); + + // Construct Card widget + return CalendarCard( + startTime, + endTime, + prefix, + sectionObject.units!.toInt(), + sectionType, + '${sectionObject.subjectCode} ${sectionObject.courseCode}', + room, + Colors.blue.shade200); + } } } diff --git a/lib/ui/calendar/calendar_card.dart b/lib/ui/calendar/calendar_card.dart index 1c77f98..7e522ad 100644 --- a/lib/ui/calendar/calendar_card.dart +++ b/lib/ui/calendar/calendar_card.dart @@ -1,96 +1,101 @@ +// ignore_for_file: use_key_in_widget_constructors, prefer_const_constructors + import 'package:flutter/material.dart'; import 'package:webreg_mobile_flutter/app_constants.dart'; -import 'package:webreg_mobile_flutter/app_styles.dart'; import 'package:webreg_mobile_flutter/ui/calendar/bottom_course_card.dart'; - class CalendarCard extends StatefulWidget { + const CalendarCard(this.startTime, this.endTime, this.datePrefix, + this.dayOfWeek, this.type, this.title, this.location, this.color); + final String startTime, endTime, datePrefix, type, title, location; final int dayOfWeek; - - const CalendarCard(this.startTime, this.endTime, this.datePrefix, this.dayOfWeek, this.type, this.title, this.location); + final Color color; @override _CalendarCardState createState() => _CalendarCardState(); } class _CalendarCardState extends State { - static const earliestClass = '08:00'; + static const String earliestClass = '08:00'; double getTimeDifference(String start, String end, String prefix) { - double diff = DateTime.parse(prefix + end).difference(DateTime.parse(prefix + start)).inMinutes.toDouble(); - print(diff.toString()); + final double diff = DateTime.parse(prefix + end) + .difference(DateTime.parse(prefix + start)) + .inMinutes + .toDouble(); return diff; } - + @override Widget build(BuildContext context) { - double calendarCardWidth = (MediaQuery.of(context).size.width - CalendarStyles.calendarTimeWidth - 20) / 7; - bool _showModal = false; + final double calendarCardWidth = (MediaQuery.of(context).size.width - + CalendarStyles.calendarTimeWidth - + 20) / + 7; return Positioned( - top: getTimeDifference(earliestClass, widget.startTime, widget.datePrefix), - left: CalendarStyles.calendarTimeWidth + widget.dayOfWeek * calendarCardWidth, - child: GestureDetector( - onTap: () { - Scaffold.of(context).showBottomSheet( - (BuildContext context) { - return BottomCourseCard(context); - } - ); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(2.0)), - // border: Border.all(width: 1, color: ) - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.25), - spreadRadius: 0, - blurRadius: 2.5, - offset: Offset(1, 1), - ), - BoxShadow( - color: Colors.black.withOpacity(0.25), - spreadRadius: 0, - blurRadius: 2.5, - offset: Offset(-1, 1), - ), - ], - ), - height: getTimeDifference(widget.startTime, widget.endTime, widget.datePrefix), - width: calendarCardWidth, - child: Column( - children: [ - Container( - height: 12, + top: getTimeDifference( + earliestClass, widget.startTime, widget.datePrefix), + left: CalendarStyles.calendarTimeWidth + + widget.dayOfWeek * calendarCardWidth, + child: GestureDetector( + onTap: () { + Scaffold.of(context) + .showBottomSheet((BuildContext context) { + return BottomCourseCard(context); + }); + }, + child: Container( decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topRight: Radius.circular(2.0), - topLeft: Radius.circular(2.0), - ), - color: lightBlue, + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(2.0)), + // border: Border.all(width: 1, color: ) + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + spreadRadius: 0, + blurRadius: 2.5, + offset: Offset(1, 1), + ), + BoxShadow( + color: Colors.black.withOpacity(0.25), + spreadRadius: 0, + blurRadius: 2.5, + offset: Offset(-1, 1), + ), + ], ), - child: Center( - child: Text(widget.type, style: TextStyle(fontSize: 8, fontWeight: FontWeight.bold)), // TODO, replace with real data - ) - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(widget.title, style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, letterSpacing: -0.3)), - Text(widget.location, style: TextStyle(fontSize: 9, letterSpacing: -0.3)), - ] - ) - ) - ] - ) - ) - ) - ); + height: getTimeDifference( + widget.startTime, widget.endTime, widget.datePrefix), + width: calendarCardWidth, + child: Column(children: [ + Container( + height: 12, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(2.0), + topLeft: Radius.circular(2.0), + ), + color: widget.color, + ), + child: Center( + child: Text(widget.type, + style: TextStyle( + fontSize: 8, fontWeight: FontWeight.bold)), + )), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(widget.title, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + letterSpacing: -0.3)), + Text(widget.location, + style: TextStyle(fontSize: 9, letterSpacing: -0.3)), + ])) + ])))); } } - - diff --git a/lib/ui/common/authentication_sso.dart b/lib/ui/common/authentication_sso.dart new file mode 100644 index 0000000..e0854c6 --- /dev/null +++ b/lib/ui/common/authentication_sso.dart @@ -0,0 +1,59 @@ +// ignore_for_file: always_specify_types + +import 'dart:html' as html; +import 'package:flutter/material.dart'; +import '../../app_constants.dart'; + +const String CLIENT_ID = ''; + +class AuthenticationSSO extends StatefulWidget { + const AuthenticationSSO({Key? key}) : super(key: key); + + @override + _AuthenticationSSOState createState() => _AuthenticationSSOState(); +} + +class _AuthenticationSSOState extends State { + String _token = ''; + String clientId = CLIENT_ID; + late html.WindowBase popUpWindow; + + @override + void initState() { + super.initState(); + + final Uri currentUrl = Uri.base; + + // If token is not present, open SSO popup + if (!currentUrl.fragment.contains('access_token=')) { + WidgetsBinding.instance!.addPostFrameCallback((_) { + html.window.location.href = + 'https://api-qa.ucsd.edu:8243/authorize?response_type=token&client_id=$clientId&redirect_uri=${currentUrl.origin}&scope=viewing_activity_read'; + }); + } else { + // If it is present, okay to move onto main page + final List fragments = currentUrl.fragment.split('&'); + _token = fragments + .firstWhere((String e) => e.startsWith('access_token=')) + .substring('access_token='.length); + WidgetsBinding.instance!.addPostFrameCallback((_) => setState(() { + _token = fragments + .firstWhere((String e) => e.startsWith('access_token=')) + .substring('access_token='.length); + })); + } + } + + @override + Widget build(BuildContext context) { + if (_token != '') { + Navigator.pushNamedAndRemoveUntil( + context, RoutePaths.Home, (Route route) => false); + } + return const Scaffold( + body: Center( + child: Text('Missing Authentication Token'), + ), + ); + } +} diff --git a/lib/ui/common/build_info.dart b/lib/ui/common/build_info.dart index f05d8fd..d3b6422 100644 --- a/lib/ui/common/build_info.dart +++ b/lib/ui/common/build_info.dart @@ -1,7 +1,11 @@ +// ignore_for_file: prefer_const_constructors + import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; class BuildInfo extends StatefulWidget { + const BuildInfo({Key? key}) : super(key: key); + @override _BuildInfoState createState() => _BuildInfoState(); } @@ -27,7 +31,7 @@ class _BuildInfoState extends State { }); } - final String buildEnv = "##BUILD_ENV##"; + final String buildEnv = '##BUILD_ENV##'; @override Widget build(BuildContext context) { @@ -49,7 +53,6 @@ class _BuildInfoState extends State { textAlign: TextAlign.center, )); } catch (err) { - print(err); return Container(); } } diff --git a/lib/ui/list/course_card.dart b/lib/ui/list/course_card.dart index 9de4a62..e488a6c 100644 --- a/lib/ui/list/course_card.dart +++ b/lib/ui/list/course_card.dart @@ -1,450 +1,457 @@ +// ignore_for_file: always_specify_types + import 'package:flutter/material.dart'; -import 'package:webreg_mobile_flutter/ui/search/search_placeholder.dart'; -import 'package:webreg_mobile_flutter/ui/calendar/calendar.dart'; -import 'package:webreg_mobile_flutter/app_constants.dart'; import 'package:webreg_mobile_flutter/app_styles.dart'; +// (p8gonzal): Can use mock profile API to make this data no hard coded class CourseCard extends StatelessWidget { - static const MOCK_DATA = [ - { - 'lecture': { - 'subjectCode': 'COGS', - 'courseCode': '187B', - 'instructionType': 'LE', - 'sectionNumber': '960510', - 'sectionCode': 'A00', - 'specialMeetingCode': '', - 'longDescription': '', - 'sectionStatus': null, - 'enrollmentStatus': 'EN', - 'gradeOption': 'L', - 'creditHours': 4, - 'gradeOptionPlus': true, - 'creditHoursPlus': false, - 'courseTitle': 'Practicum in Pro Web Design', - 'enrollmentCapacity': 60, - 'enrollmentQuantity': 64, - 'countOnWaitlist': 2, - 'stopEnrollmentFlag': true, - 'classTimes': [ - { - 'dayCode': '2', - 'startDate': 1585551600000, - 'beginHHTime': '14', - 'beginMMTime': '0', - 'endHHTime': '15', - 'endMMTime': '20', - 'buildingCode': 'HSS', - 'roomCode': '1346', - 'endDate': '2019-03-15', - }, - { - 'dayCode': '4', - 'startDate': 1585551600000, - 'beginHHTime': '14', - 'beginMMTime': '0', - 'endHHTime': '15', - 'endMMTime': '20', - 'buildingCode': 'HSS', - 'roomCode': '1346', - 'endDate': '2019-03-15', - }, - ], - 'instructors': [ - 'Kirsh, David Joel', - ], - }, - 'final': { - 'subjectCode': 'COGS', - 'courseCode': '187B', - 'instructionType': 'LE', - 'sectionNumber': '960510', - 'sectionCode': 'A00', - 'specialMeetingCode': 'FI', - 'longDescription': '', - 'sectionStatus': null, - 'enrollmentStatus': null, - 'gradeOption': null, - 'creditHours': null, - 'gradeOptionPlus': false, - 'creditHoursPlus': false, - 'courseTitle': null, - 'enrollmentCapacity': null, - 'enrollmentQuantity': null, - 'countOnWaitlist': null, - 'stopEnrollmentFlag': false, - 'classTimes': [ - { - 'dayCode': '4', - 'startDate': 1585551600000, - 'beginHHTime': '15', - 'beginMMTime': '0', - 'endHHTime': '17', - 'endMMTime': '59', - 'buildingCode': 'HSS', - 'roomCode': '1346', - 'endDate': null, - }, - ], - 'instructors': [ - 'Kirsh, David Joel', - ], - }, - 'discussion': { - 'subjectCode': 'COGS', - 'courseCode': '187B', - 'instructionType': 'DI', - 'sectionNumber': '960510', - 'sectionCode': 'A00', - 'specialMeetingCode': '', - 'longDescription': '', - 'sectionStatus': null, - 'enrollmentStatus': null, - 'gradeOption': null, - 'creditHours': null, - 'gradeOptionPlus': false, - 'creditHoursPlus': false, - 'courseTitle': null, - 'enrollmentCapacity': null, - 'enrollmentQuantity': null, - 'countOnWaitlist': null, - 'stopEnrollmentFlag': false, - 'classTimes': [ - { - 'dayCode': '2', - 'startDate': 1585551600000, - 'beginHHTime': '11', - 'beginMMTime': '0', - 'endHHTime': '11', - 'endMMTime': '50', - 'buildingCode': 'HSS', - 'roomCode': '1346', - 'endDate': null, - }, - ], - 'instructors': [ - 'Kirsh, David Joel', - ], - }, - }, - { - 'lecture': { - 'subjectCode': 'COGS', - 'courseCode': '187B', - 'instructionType': 'LE', - 'sectionNumber': '960510', - 'sectionCode': 'A00', - 'specialMeetingCode': '', - 'longDescription': '', - 'sectionStatus': null, - 'enrollmentStatus': 'EN', - 'gradeOption': 'L', - 'creditHours': 4, - 'gradeOptionPlus': true, - 'creditHoursPlus': false, - 'courseTitle': 'Practicum in Pro Web Design', - 'enrollmentCapacity': 60, - 'enrollmentQuantity': 64, - 'countOnWaitlist': 2, - 'stopEnrollmentFlag': true, - 'classTimes': [ - { - 'dayCode': '2', - 'startDate': 1585551600000, - 'beginHHTime': '14', - 'beginMMTime': '0', - 'endHHTime': '15', - 'endMMTime': '20', - 'buildingCode': 'HSS', - 'roomCode': '1346', - 'endDate': '2019-03-15', - }, - { - 'dayCode': '4', - 'startDate': 1585551600000, - 'beginHHTime': '14', - 'beginMMTime': '0', - 'endHHTime': '15', - 'endMMTime': '20', - 'buildingCode': 'HSS', - 'roomCode': '1346', - 'endDate': '2019-03-15', - }, - ], - 'instructors': [ - 'Kirsh, David Joel', - ], - }, - 'final': { - 'subjectCode': 'COGS', - 'courseCode': '187B', - 'instructionType': 'LE', - 'sectionNumber': '960510', - 'sectionCode': 'A00', - 'specialMeetingCode': 'FI', - 'longDescription': '', - 'sectionStatus': null, - 'enrollmentStatus': null, - 'gradeOption': null, - 'creditHours': null, - 'gradeOptionPlus': false, - 'creditHoursPlus': false, - 'courseTitle': null, - 'enrollmentCapacity': null, - 'enrollmentQuantity': null, - 'countOnWaitlist': null, - 'stopEnrollmentFlag': false, - 'classTimes': [ - { - 'dayCode': '4', - 'startDate': 1585551600000, - 'beginHHTime': '15', - 'beginMMTime': '0', - 'endHHTime': '17', - 'endMMTime': '59', - 'buildingCode': 'HSS', - 'roomCode': '1346', - 'endDate': null, - }, - ], - 'instructors': [ - 'Kirsh, David Joel', - ], - }, - 'discussion': { - 'subjectCode': 'COGS', - 'courseCode': '187B', - 'instructionType': 'DI', - 'sectionNumber': '960510', - 'sectionCode': 'A00', - 'specialMeetingCode': '', - 'longDescription': '', - 'sectionStatus': null, - 'enrollmentStatus': null, - 'gradeOption': null, - 'creditHours': null, - 'gradeOptionPlus': false, - 'creditHoursPlus': false, - 'courseTitle': null, - 'enrollmentCapacity': null, - 'enrollmentQuantity': null, - 'countOnWaitlist': null, - 'stopEnrollmentFlag': false, - 'classTimes': [ - { - 'dayCode': '2', - 'startDate': 1585551600000, - 'beginHHTime': '11', - 'beginMMTime': '0', - 'endHHTime': '11', - 'endMMTime': '50', - 'buildingCode': 'HSS', - 'roomCode': '1346', - 'endDate': null, - }, - ], - 'instructors': [ - 'Kirsh, David Joel', - ], - }, - }, - { - 'discussion': { - 'subjectCode': 'LTEA', - 'courseCode': '120A', - 'instructionType': 'LA', - 'sectionNumber': '2064', - 'sectionCode': 'A01', - 'specialMeetingCode': null, - 'longDescription': '', - 'enrollmentStatus': 'EN', - 'gradeOption': 'P', - 'creditHours': 4, - 'gradeOptionPlus': false, - 'creditHoursPlus': false, - 'courseTitle': 'Chinese Films', - 'enrollmentCapacity': 320, - 'enrollmentQuantity': 308, - 'countOnWaitlist': 1, - 'stopEnrollmentFlag': false, - 'classTimes': [ - { - 'dayCode': '1', - 'startDate': 1585551600000, - 'beginHHTime': 17, - 'beginMMTime': 0, - 'endHHTime': 19, - 'endMMTime': 50, - 'buildingCode': '', - 'roomCode': '', - 'endDate': null, - }, - ], - 'instructors': [ - 'Zhang, Yingjin', - ], + const CourseCard({Key? key}) : super(key: key); + + static const List>> MOCK_DATA = [ + { + 'lecture': { + 'subjectCode': 'COGS', + 'courseCode': '187B', + 'instructionType': 'LE', + 'sectionNumber': '960510', + 'sectionCode': 'A00', + 'specialMeetingCode': '', + 'longDescription': '', + 'sectionStatus': null, + 'enrollmentStatus': 'EN', + 'gradeOption': 'L', + 'creditHours': 4, + 'gradeOptionPlus': true, + 'creditHoursPlus': false, + 'courseTitle': 'Practicum in Pro Web Design', + 'enrollmentCapacity': 60, + 'enrollmentQuantity': 64, + 'countOnWaitlist': 2, + 'stopEnrollmentFlag': true, + 'classTimes': [ + { + 'dayCode': '2', + 'startDate': 1585551600000, + 'beginHHTime': '14', + 'beginMMTime': '0', + 'endHHTime': '15', + 'endMMTime': '20', + 'buildingCode': 'HSS', + 'roomCode': '1346', + 'endDate': '2019-03-15', + }, + { + 'dayCode': '4', + 'startDate': 1585551600000, + 'beginHHTime': '14', + 'beginMMTime': '0', + 'endHHTime': '15', + 'endMMTime': '20', + 'buildingCode': 'HSS', + 'roomCode': '1346', + 'endDate': '2019-03-15', + }, + ], + 'instructors': [ + 'Kirsh, David Joel', + ], + }, + 'final': { + 'subjectCode': 'COGS', + 'courseCode': '187B', + 'instructionType': 'LE', + 'sectionNumber': '960510', + 'sectionCode': 'A00', + 'specialMeetingCode': 'FI', + 'longDescription': '', + 'sectionStatus': null, + 'enrollmentStatus': null, + 'gradeOption': null, + 'creditHours': null, + 'gradeOptionPlus': false, + 'creditHoursPlus': false, + 'courseTitle': null, + 'enrollmentCapacity': null, + 'enrollmentQuantity': null, + 'countOnWaitlist': null, + 'stopEnrollmentFlag': false, + 'classTimes': [ + { + 'dayCode': '4', + 'startDate': 1585551600000, + 'beginHHTime': '15', + 'beginMMTime': '0', + 'endHHTime': '17', + 'endMMTime': '59', + 'buildingCode': 'HSS', + 'roomCode': '1346', + 'endDate': null, + }, + ], + 'instructors': [ + 'Kirsh, David Joel', + ], + }, + 'discussion': { + 'subjectCode': 'COGS', + 'courseCode': '187B', + 'instructionType': 'DI', + 'sectionNumber': '960510', + 'sectionCode': 'A00', + 'specialMeetingCode': '', + 'longDescription': '', + 'sectionStatus': null, + 'enrollmentStatus': null, + 'gradeOption': null, + 'creditHours': null, + 'gradeOptionPlus': false, + 'creditHoursPlus': false, + 'courseTitle': null, + 'enrollmentCapacity': null, + 'enrollmentQuantity': null, + 'countOnWaitlist': null, + 'stopEnrollmentFlag': false, + 'classTimes': [ + { + 'dayCode': '2', + 'startDate': 1585551600000, + 'beginHHTime': '11', + 'beginMMTime': '0', + 'endHHTime': '11', + 'endMMTime': '50', + 'buildingCode': 'HSS', + 'roomCode': '1346', + 'endDate': null, + }, + ], + 'instructors': [ + 'Kirsh, David Joel', + ], + }, }, - 'lecture': { - 'subjectCode': 'LTEA', - 'courseCode': '120A', - 'instructionType': 'LE', - 'sectionNumber': '2063', - 'sectionCode': 'A00', - 'specialMeetingCode': '', - 'longDescription': 'Visions of the City', - 'enrollmentStatus': 'EN', - 'gradeOption': 'P', - 'creditHours': 4, - 'gradeOptionPlus': false, - 'creditHoursPlus': false, - 'courseTitle': 'Chinese Films', - 'enrollmentCapacity': 320, - 'enrollmentQuantity': 308, - 'countOnWaitlist': 1, - 'stopEnrollmentFlag': false, - 'classTimes': [ - { - 'dayCode': '1', - 'startDate': 1585551600000, - 'beginHHTime': 17, - 'beginMMTime': 0, - 'endHHTime': 19, - 'endMMTime': 50, - 'buildingCode': '', - 'roomCode': '', - 'endDate': null, - }, - ], - 'instructors': [ - 'Zhang, Yingjin', - ], + { + 'lecture': { + 'subjectCode': 'COGS', + 'courseCode': '187B', + 'instructionType': 'LE', + 'sectionNumber': '960510', + 'sectionCode': 'A00', + 'specialMeetingCode': '', + 'longDescription': '', + 'sectionStatus': null, + 'enrollmentStatus': 'EN', + 'gradeOption': 'L', + 'creditHours': 4, + 'gradeOptionPlus': true, + 'creditHoursPlus': false, + 'courseTitle': 'Practicum in Pro Web Design', + 'enrollmentCapacity': 60, + 'enrollmentQuantity': 64, + 'countOnWaitlist': 2, + 'stopEnrollmentFlag': true, + 'classTimes': [ + { + 'dayCode': '2', + 'startDate': 1585551600000, + 'beginHHTime': '14', + 'beginMMTime': '0', + 'endHHTime': '15', + 'endMMTime': '20', + 'buildingCode': 'HSS', + 'roomCode': '1346', + 'endDate': '2019-03-15', + }, + { + 'dayCode': '4', + 'startDate': 1585551600000, + 'beginHHTime': '14', + 'beginMMTime': '0', + 'endHHTime': '15', + 'endMMTime': '20', + 'buildingCode': 'HSS', + 'roomCode': '1346', + 'endDate': '2019-03-15', + }, + ], + 'instructors': [ + 'Kirsh, David Joel', + ], + }, + 'final': { + 'subjectCode': 'COGS', + 'courseCode': '187B', + 'instructionType': 'LE', + 'sectionNumber': '960510', + 'sectionCode': 'A00', + 'specialMeetingCode': 'FI', + 'longDescription': '', + 'sectionStatus': null, + 'enrollmentStatus': null, + 'gradeOption': null, + 'creditHours': null, + 'gradeOptionPlus': false, + 'creditHoursPlus': false, + 'courseTitle': null, + 'enrollmentCapacity': null, + 'enrollmentQuantity': null, + 'countOnWaitlist': null, + 'stopEnrollmentFlag': false, + 'classTimes': [ + { + 'dayCode': '4', + 'startDate': 1585551600000, + 'beginHHTime': '15', + 'beginMMTime': '0', + 'endHHTime': '17', + 'endMMTime': '59', + 'buildingCode': 'HSS', + 'roomCode': '1346', + 'endDate': null, + }, + ], + 'instructors': [ + 'Kirsh, David Joel', + ], + }, + 'discussion': { + 'subjectCode': 'COGS', + 'courseCode': '187B', + 'instructionType': 'DI', + 'sectionNumber': '960510', + 'sectionCode': 'A00', + 'specialMeetingCode': '', + 'longDescription': '', + 'sectionStatus': null, + 'enrollmentStatus': null, + 'gradeOption': null, + 'creditHours': null, + 'gradeOptionPlus': false, + 'creditHoursPlus': false, + 'courseTitle': null, + 'enrollmentCapacity': null, + 'enrollmentQuantity': null, + 'countOnWaitlist': null, + 'stopEnrollmentFlag': false, + 'classTimes': [ + { + 'dayCode': '2', + 'startDate': 1585551600000, + 'beginHHTime': '11', + 'beginMMTime': '0', + 'endHHTime': '11', + 'endMMTime': '50', + 'buildingCode': 'HSS', + 'roomCode': '1346', + 'endDate': null, + }, + ], + 'instructors': [ + 'Kirsh, David Joel', + ], + }, }, - 'final': { - 'subjectCode': 'LTEA', - 'courseCode': '120A', - 'instructionType': 'LE', - 'sectionNumber': '2063', - 'sectionCode': 'A00', - 'specialMeetingCode': 'FI', - 'longDescription': 'Visions of the City', - 'enrollmentStatus': null, - 'gradeOption': null, - 'creditHours': null, - 'gradeOptionPlus': false, - 'creditHoursPlus': false, - 'courseTitle': null, - 'enrollmentCapacity': null, - 'enrollmentQuantity': null, - 'countOnWaitlist': null, - 'stopEnrollmentFlag': false, - 'classTimes': [ - { - 'dayCode': '5', - 'startDate': 1591945200000, - 'beginHHTime': 19, - 'beginMMTime': 0, - 'endHHTime': 21, - 'endMMTime': 59, - 'buildingCode': '', - 'roomCode': '', - 'endDate': null, - }, - ], - 'instructors': [ - 'Zhang, Yingjin', - ], + { + 'discussion': { + 'subjectCode': 'LTEA', + 'courseCode': '120A', + 'instructionType': 'LA', + 'sectionNumber': '2064', + 'sectionCode': 'A01', + 'specialMeetingCode': null, + 'longDescription': '', + 'enrollmentStatus': 'EN', + 'gradeOption': 'P', + 'creditHours': 4, + 'gradeOptionPlus': false, + 'creditHoursPlus': false, + 'courseTitle': 'Chinese Films', + 'enrollmentCapacity': 320, + 'enrollmentQuantity': 308, + 'countOnWaitlist': 1, + 'stopEnrollmentFlag': false, + 'classTimes': [ + { + 'dayCode': '1', + 'startDate': 1585551600000, + 'beginHHTime': 17, + 'beginMMTime': 0, + 'endHHTime': 19, + 'endMMTime': 50, + 'buildingCode': '', + 'roomCode': '', + 'endDate': null, + }, + ], + 'instructors': [ + 'Zhang, Yingjin', + ], + }, + 'lecture': { + 'subjectCode': 'LTEA', + 'courseCode': '120A', + 'instructionType': 'LE', + 'sectionNumber': '2063', + 'sectionCode': 'A00', + 'specialMeetingCode': '', + 'longDescription': 'Visions of the City', + 'enrollmentStatus': 'EN', + 'gradeOption': 'P', + 'creditHours': 4, + 'gradeOptionPlus': false, + 'creditHoursPlus': false, + 'courseTitle': 'Chinese Films', + 'enrollmentCapacity': 320, + 'enrollmentQuantity': 308, + 'countOnWaitlist': 1, + 'stopEnrollmentFlag': false, + 'classTimes': [ + { + 'dayCode': '1', + 'startDate': 1585551600000, + 'beginHHTime': 17, + 'beginMMTime': 0, + 'endHHTime': 19, + 'endMMTime': 50, + 'buildingCode': '', + 'roomCode': '', + 'endDate': null, + }, + ], + 'instructors': [ + 'Zhang, Yingjin', + ], + }, + 'final': { + 'subjectCode': 'LTEA', + 'courseCode': '120A', + 'instructionType': 'LE', + 'sectionNumber': '2063', + 'sectionCode': 'A00', + 'specialMeetingCode': 'FI', + 'longDescription': 'Visions of the City', + 'enrollmentStatus': null, + 'gradeOption': null, + 'creditHours': null, + 'gradeOptionPlus': false, + 'creditHoursPlus': false, + 'courseTitle': null, + 'enrollmentCapacity': null, + 'enrollmentQuantity': null, + 'countOnWaitlist': null, + 'stopEnrollmentFlag': false, + 'classTimes': [ + { + 'dayCode': '5', + 'startDate': 1591945200000, + 'beginHHTime': 19, + 'beginMMTime': 0, + 'endHHTime': 21, + 'endMMTime': 59, + 'buildingCode': '', + 'roomCode': '', + 'endDate': null, + }, + ], + 'instructors': [ + 'Zhang, Yingjin', + ], + }, }, - }, - { - 'lecture': { - 'subjectCode': 'PHIL', - 'courseCode': '27', - 'instructionType': 'LE', - 'sectionNumber': '5027', - 'sectionCode': 'A02', - 'specialMeetingCode': '', - 'longDescription': '', - 'enrollmentStatus': 'EN', - 'gradeOption': 'P', - 'creditHours': 4, - 'gradeOptionPlus': false, - 'creditHoursPlus': false, - 'courseTitle': 'Ethics And Society', - 'enrollmentCapacity': 37, - 'enrollmentQuantity': 39, - 'countOnWaitlist': 2, - 'stopEnrollmentFlag': false, - 'classTimes': [ - { - 'dayCode': '1', - 'startDate': 1585551600000, - 'beginHHTime': 14, - 'beginMMTime': 0, - 'endHHTime': 14, - 'endMMTime': 50, - 'buildingCode': '', - 'roomCode': '', - 'endDate': 1591340400000, - }, - ], - 'instructors': [ - 'Brandt, Reuven A', - ], + { + 'lecture': { + 'subjectCode': 'PHIL', + 'courseCode': '27', + 'instructionType': 'LE', + 'sectionNumber': '5027', + 'sectionCode': 'A02', + 'specialMeetingCode': '', + 'longDescription': '', + 'enrollmentStatus': 'EN', + 'gradeOption': 'P', + 'creditHours': 4, + 'gradeOptionPlus': false, + 'creditHoursPlus': false, + 'courseTitle': 'Ethics And Society', + 'enrollmentCapacity': 37, + 'enrollmentQuantity': 39, + 'countOnWaitlist': 2, + 'stopEnrollmentFlag': false, + 'classTimes': [ + { + 'dayCode': '1', + 'startDate': 1585551600000, + 'beginHHTime': 14, + 'beginMMTime': 0, + 'endHHTime': 14, + 'endMMTime': 50, + 'buildingCode': '', + 'roomCode': '', + 'endDate': 1591340400000, + }, + ], + 'instructors': [ + 'Brandt, Reuven A', + ], + }, + 'discussion': { + 'subjectCode': 'PHIL', + 'courseCode': '27', + 'instructionType': 'DI', + 'sectionNumber': '5027', + 'sectionCode': 'A02', + 'specialMeetingCode': '', + 'longDescription': '', + 'enrollmentStatus': 'EN', + 'gradeOption': 'P', + 'creditHours': 4, + 'gradeOptionPlus': false, + 'creditHoursPlus': false, + 'courseTitle': 'Ethics And Society', + 'enrollmentCapacity': 37, + 'enrollmentQuantity': 39, + 'countOnWaitlist': 2, + 'stopEnrollmentFlag': false, + 'classTimes': [ + { + 'dayCode': '1', + 'startDate': 1585551600000, + 'beginHHTime': 14, + 'beginMMTime': 0, + 'endHHTime': 14, + 'endMMTime': 50, + 'buildingCode': '', + 'roomCode': '', + 'endDate': 1591340400000, + }, + ], + 'instructors': [ + 'Brandt, Reuven A', + ], + }, }, - 'discussion': { - 'subjectCode': 'PHIL', - 'courseCode': '27', - 'instructionType': 'DI', - 'sectionNumber': '5027', - 'sectionCode': 'A02', - 'specialMeetingCode': '', - 'longDescription': '', - 'enrollmentStatus': 'EN', - 'gradeOption': 'P', - 'creditHours': 4, - 'gradeOptionPlus': false, - 'creditHoursPlus': false, - 'courseTitle': 'Ethics And Society', - 'enrollmentCapacity': 37, - 'enrollmentQuantity': 39, - 'countOnWaitlist': 2, - 'stopEnrollmentFlag': false, - 'classTimes': [ - { - 'dayCode': '1', - 'startDate': 1585551600000, - 'beginHHTime': 14, - 'beginMMTime': 0, - 'endHHTime': 14, - 'endMMTime': 50, - 'buildingCode': '', - 'roomCode': '', - 'endDate': 1591340400000, - }, - ], - 'instructors': [ - 'Brandt, Reuven A', - ], - }, - }, -]; + ]; Widget renderActionButtons() { return Container( - width: 45, - decoration: BoxDecoration( - border: Border( + width: 45, + decoration: const BoxDecoration( + border: Border( left: BorderSide(color: lightGray), - ) - ), - child: Column( - children: [ - IconButton(icon: Icon(Icons.autorenew, color: ColorPrimary)), - IconButton(icon: Icon(Icons.delete, color: ColorPrimary)), - IconButton(icon: Icon(Icons.add_circle, color: ColorPrimary)), - ] - ) - ); + )), + child: Column(children: [ + IconButton( + icon: const Icon(Icons.autorenew, color: ColorPrimary), + onPressed: () {}, + ), + IconButton( + icon: const Icon(Icons.delete, color: ColorPrimary), + onPressed: () {}, + ), + IconButton( + icon: const Icon(Icons.add_circle, color: ColorPrimary), + onPressed: () {}, + ), + ])); } Widget renderSection() { @@ -452,33 +459,32 @@ class CourseCard extends StatelessWidget { // mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - flex: 3, - child: Row( - children: [ - Text('A00', style: TextStyle(fontSize: 11, color: darkGray)), // TODO - Text(' LE', style: TextStyle(fontSize: 11, color: darkGray)) // TODO - ], - ) - ), - Expanded( - flex: 3, - child: Row( - children: [ - Text('M', style: TextStyle(fontSize: 11, color: ColorPrimary)), // TODO - Text('T', style: TextStyle(fontSize: 11, color: lightGray)), // TODO - Text('W', style: TextStyle(fontSize: 11, color: ColorPrimary)), // TODO - Text('T', style: TextStyle(fontSize: 11, color: lightGray)), // TODO - Text('F', style: TextStyle(fontSize: 11, color: ColorPrimary)), // TODO - ], - ) - ), + flex: 3, + child: Row( + children: const [ + Text('A00', style: TextStyle(fontSize: 11, color: darkGray)), + Text(' LE', style: TextStyle(fontSize: 11, color: darkGray)) + ], + )), Expanded( + flex: 3, + child: Row( + children: const [ + Text('M', style: TextStyle(fontSize: 11, color: ColorPrimary)), + Text('T', style: TextStyle(fontSize: 11, color: lightGray)), + Text('W', style: TextStyle(fontSize: 11, color: ColorPrimary)), + Text('T', style: TextStyle(fontSize: 11, color: lightGray)), + Text('F', style: TextStyle(fontSize: 11, color: ColorPrimary)), + ], + )), + const Expanded( flex: 5, - child: Text('3:30p - 4:50p', style: TextStyle(fontSize: 11, color: ColorPrimary)), // TODO + child: Text('3:30p - 4:50p', + style: TextStyle(fontSize: 11, color: ColorPrimary)), ), - Expanded( + const Expanded( flex: 5, - child: Text('PCYHN 106', style: TextStyle(fontSize: 11)), // TODO + child: Text('PCYHN 106', style: TextStyle(fontSize: 11)), ) ], ); @@ -489,88 +495,84 @@ class CourseCard extends StatelessWidget { return Card( elevation: 0, shape: RoundedRectangleBorder( - side: new BorderSide(color: ColorPrimary, width: 2.0), - borderRadius: BorderRadius.circular(10.0) - ), - margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4), + side: const BorderSide(color: ColorPrimary, width: 2.0), + borderRadius: BorderRadius.circular(10.0)), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ClipPath( child: Row( children: [ Expanded( child: Container( - margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // course header: units, course code, course name - Container( - child: Row( - children: [ - // units icon - Container( + margin: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // course header: units, course code, course name + Row(children: [ + // units icon + Container( height: 30, width: 30, - decoration: new BoxDecoration( + decoration: const BoxDecoration( color: lightGray, shape: BoxShape.circle, ), - margin: EdgeInsets.only(right: 10), - child: Center( - child: Text( - '4' // TODO - ) - ) - ), - // course info - Expanded( + margin: const EdgeInsets.only(right: 10), + child: const Center(child: Text('4'))), + // course info + Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('CSE 12', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), // TODO - Text('Enrolled - Letter', style: TextStyle(color: ColorPrimary, fontSize: 12)), // TODO - ], - ), - Text('Basic Data Struct & OO design') // TODO + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + Text('CSE 12', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold)), + Text('Enrolled - Letter', + style: TextStyle( + color: ColorPrimary, fontSize: 12)), ], - ) - ) - ] - ), - ), - // instructor andd section id - Container( - margin: EdgeInsets.only(top: 8, bottom: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Gillespie, Gary N', style: TextStyle(color: ColorPrimary, fontSize: 12)), // TODO - Row( + ), + const Text('Basic Data Struct & OO design') + ], + )) + ]), + // instructor andd section id + Container( + margin: const EdgeInsets.only(top: 8, bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Section ID', style: TextStyle(color: darkGray, fontSize: 12)), // TODO - Text(' 983761', style: TextStyle(fontSize: 12)), // TODO - ] - ) - ] + const Text('Gillespie, Gary N', + style: TextStyle( + color: ColorPrimary, fontSize: 12)), + Row(children: const [ + Text('Section ID', + style: TextStyle( + color: darkGray, fontSize: 12)), + Text(' 983761', + style: TextStyle(fontSize: 12)), + ]) + ]), ), - ), - // course sections: di, final - renderSection(), - renderSection(), - ], - ) - ), + // course sections: di, final + renderSection(), + renderSection(), + ], + )), ), renderActionButtons() ], ), clipper: ShapeBorderClipper( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)) - ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10))), ), ); } -} \ No newline at end of file +} diff --git a/lib/ui/list/course_list_view.dart b/lib/ui/list/course_list_view.dart index aa06caa..97dec0f 100644 --- a/lib/ui/list/course_list_view.dart +++ b/lib/ui/list/course_list_view.dart @@ -1,80 +1,76 @@ +// ignore_for_file: prefer_const_literals_to_create_immutables + import 'package:flutter/material.dart'; import 'package:webreg_mobile_flutter/ui/list/course_card.dart'; -import 'package:webreg_mobile_flutter/ui/calendar/calendar.dart'; -import 'package:webreg_mobile_flutter/app_constants.dart'; -import 'package:webreg_mobile_flutter/app_styles.dart'; class CourseListView extends StatelessWidget { + const CourseListView({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return Center( - child: Column( - children: [ - TermDropdown(), - Expanded( - child: Container( - child: ListView.builder( - itemCount: 10, - padding: EdgeInsets.symmetric(vertical: 8), - itemBuilder: (BuildContext context, int index) { - return Container( - child: CourseCard(), - ); - } - ), - ) - ) - ] - ) - ); + child: Column(children: [ + const TermDropdown(), + Expanded( + child: ListView.builder( + itemCount: 10, + padding: const EdgeInsets.symmetric(vertical: 8), + itemBuilder: (BuildContext context, int index) { + return const CourseCard(); + })) + ])); } } class TermDropdown extends StatefulWidget { + const TermDropdown({Key? key}) : super(key: key); + @override _TermDropdownState createState() => _TermDropdownState(); } -// TODO +// TODO(p8gonzal): Can be replaced with live API used in filter for search. class _TermDropdownState extends State { - List dropdownItems = ['Fall 19', 'Winter 20', 'Spring 20', 'Fall 20']; + List dropdownItems = [ + 'Fall 19', + 'Winter 20', + 'Spring 20', + 'Fall 20' + ]; String _dropdownVal = 'Fall 19'; @override Widget build(BuildContext context) { return Container( - height: 40, - margin: EdgeInsets.only(top: 10), - padding: EdgeInsets.symmetric(horizontal: 60), - child: Stack( - children: [ + height: 40, + margin: const EdgeInsets.only(top: 10), + padding: const EdgeInsets.symmetric(horizontal: 60), + child: Stack(children: [ Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.alarm, color: Colors.black), - ] - ), + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.alarm, color: Colors.black), + ]), Center( - child: Text(_dropdownVal, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)) - ), + child: Text(_dropdownVal, + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold))), DropdownButton( isExpanded: true, underline: Container(height: 0), - icon: Icon(Icons.expand_more, color: Colors.black, size: 30), - onChanged: (String newVal) { + icon: const Icon(Icons.expand_more, color: Colors.black, size: 30), + onChanged: (String? newVal) { setState(() { - _dropdownVal = newVal; + _dropdownVal = newVal!; }); }, items: dropdownItems.map>((String val) { return DropdownMenuItem( - value: val, - child: Center(child: Text(val, style: TextStyle(fontSize: 18))) - ); + value: val, + child: Center( + child: Text(val, style: const TextStyle(fontSize: 18)))); }).toList(), ) - ] - ) - ); + ])); } } diff --git a/lib/ui/navigator/bottom.dart b/lib/ui/navigator/bottom.dart index 7d73716..acdb1ff 100644 --- a/lib/ui/navigator/bottom.dart +++ b/lib/ui/navigator/bottom.dart @@ -1,78 +1,94 @@ +// ignore_for_file: prefer_const_constructors + import 'package:flutter/material.dart'; -import 'package:webreg_mobile_flutter/ui/search/search_placeholder.dart'; +import 'package:provider/provider.dart'; +import 'package:webreg_mobile_flutter/app_styles.dart'; +import 'package:webreg_mobile_flutter/core/providers/user.dart'; import 'package:webreg_mobile_flutter/ui/calendar/calendar.dart'; import 'package:webreg_mobile_flutter/ui/list/course_list_view.dart'; -import 'package:webreg_mobile_flutter/app_constants.dart'; -import 'package:webreg_mobile_flutter/app_styles.dart'; +import 'package:webreg_mobile_flutter/ui/search/search_placeholder.dart'; class BottomNavigation extends StatefulWidget { + const BottomNavigation({Key? key}) : super(key: key); + @override _BottomNavigationState createState() => _BottomNavigationState(); } -class _BottomNavigationState extends State with SingleTickerProviderStateMixin { - var currentTab = [ - Calendar(Colors.yellow), - CourseListView(), - Calendar(Colors.green), +class _BottomNavigationState extends State + with SingleTickerProviderStateMixin { + UserDataProvider userDataProvider = UserDataProvider(); + List currentTab = [ + const Calendar( + calendarType: 'LECT_DISC', + ), + const CourseListView(), + // Finals Calendar + const Calendar(calendarType: 'FINALS'), ]; int currentIndex = 0; static const TextStyle textStyles = TextStyle(fontSize: 16, color: darkGray); static const TextStyle activeStyles = TextStyle( - // decoration: TextDecoration.underline, fontWeight: FontWeight.bold, color: ColorPrimary, fontSize: 16, ); + @override + void didChangeDependencies() { + super.didChangeDependencies(); + userDataProvider = Provider.of(context, listen: false); + } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - centerTitle: true, - title: Text("Webreg", style: TextStyle( - fontWeight: FontWeight.normal, - )), - actions: [ - SearchPlaceholder() - ] - ), - body: currentTab[currentIndex], - bottomNavigationBar: BottomNavigationBar( - type: BottomNavigationBarType.fixed, - currentIndex: currentIndex, - onTap: (index) { - setState(() { currentIndex = index; }); - }, - items: [ - BottomNavigationBarItem( - icon: Text("Calendar", style: textStyles), - activeIcon: Container( - child: Column( - children: [ - Text("Calendar", style: activeStyles), + String _token = ''; + final Uri currentUrl = Uri.base; + final List fragments = currentUrl.fragment.split('&'); + _token = fragments + .firstWhere((String e) => e.startsWith('access_token=')) + .substring('access_token='.length); + userDataProvider.setToken = _token; - ] - ) + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: const Text('Webreg', + style: TextStyle( + fontWeight: FontWeight.normal, + )), + actions: const [SearchPlaceholder()]), + body: currentTab[currentIndex], + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + currentIndex: currentIndex, + onTap: (int index) { + setState(() { + currentIndex = index; + }); + }, + items: [ + BottomNavigationBarItem( + icon: const Text('Calendar', style: textStyles), + activeIcon: Column(children: const [ + Text('Calendar', style: activeStyles), + ]), + label: '', ), - label: '', - ), - BottomNavigationBarItem( - icon: Text("List", style: textStyles), - activeIcon: Text("List", style: activeStyles), - label: '', - ), - BottomNavigationBarItem( - icon: Text("Finals", style: textStyles), - activeIcon: Text("Finals", style: activeStyles), - label: '', - ), - ], - showSelectedLabels: false, - showUnselectedLabels: false, - backgroundColor: vWhite, - ) - ); + const BottomNavigationBarItem( + icon: Text('List', style: textStyles), + activeIcon: Text('List', style: activeStyles), + label: '', + ), + const BottomNavigationBarItem( + icon: Text('Finals', style: textStyles), + activeIcon: Text('Finals', style: activeStyles), + label: '', + ), + ], + showSelectedLabels: false, + showUnselectedLabels: false, + backgroundColor: vWhite, + )); } -} \ No newline at end of file +} diff --git a/lib/ui/search/search_bar.dart b/lib/ui/search/search_bar.dart index 0b6e63f..d4d3844 100644 --- a/lib/ui/search/search_bar.dart +++ b/lib/ui/search/search_bar.dart @@ -1,255 +1,159 @@ +// ignore_for_file: unused_import, prefer_const_constructors + import 'package:flutter/material.dart'; import 'package:webreg_mobile_flutter/app_constants.dart'; import 'package:webreg_mobile_flutter/app_styles.dart'; +import 'package:webreg_mobile_flutter/core/models/schedule_of_classes.dart'; +import 'package:webreg_mobile_flutter/core/providers/schedule_of_classes.dart'; class SearchBar extends StatelessWidget { + const SearchBar(this.setOpenFilters, {Key? key}) : super(key: key); final VoidCallback setOpenFilters; - SearchBar(this.setOpenFilters); - @override Widget build(BuildContext context) { return MediaQuery.removePadding( - context: context, - removeBottom: true, - child: AppBar( - titleSpacing: 0.0, - centerTitle: true, - title: Container( - decoration: new BoxDecoration( - color: lightGray, - borderRadius: new BorderRadius.all(Radius.circular(100.0)), - border: Border.all(width: 1.0, color: Color(0xFF034263)), - ), - margin: EdgeInsets.symmetric(vertical: 10.0), - child: Search(), - ), - automaticallyImplyLeading: false, - leading: Center( - child:IconButton( - icon: Icon(Icons.arrow_back, color: Colors.white), - padding: EdgeInsets.symmetric(horizontal: 9), - alignment: Alignment.centerLeft, - iconSize: 25, - onPressed: () { - Navigator.pop(context); - } - ), - ), - actions: [ - IconButton( - icon: Icon(Icons.filter_list, color: Colors.white), - padding: EdgeInsets.symmetric(horizontal: 9), - alignment: Alignment.centerLeft, - iconSize: 25, - onPressed: this.setOpenFilters, - ), - ] - ) - ); + context: context, + removeBottom: true, + child: AppBar( + titleSpacing: 0.0, + centerTitle: true, + title: Container( + decoration: BoxDecoration( + color: lightGray, + borderRadius: BorderRadius.all(Radius.circular(100.0)), + border: Border.all(width: 1.0, color: Color(0xFF034263)), + ), + margin: EdgeInsets.symmetric(vertical: 10.0), + child: Search(), + ), + automaticallyImplyLeading: false, + leading: Center( + child: IconButton( + icon: Icon(Icons.arrow_back, color: Colors.white), + padding: EdgeInsets.symmetric(horizontal: 9), + alignment: Alignment.centerLeft, + iconSize: 25, + onPressed: () { + Navigator.pop(context); + }), + ), + actions: [ + IconButton( + icon: Icon(Icons.filter_list, color: Colors.white), + padding: EdgeInsets.symmetric(horizontal: 9), + alignment: Alignment.centerLeft, + iconSize: 25, + onPressed: setOpenFilters, + ), + ])); } } -// class Filters extends StatefulWidget { -// Widget child; -// bool expand; -// Filters({this.expand = false, this.child}); - -// @override -// _FiltersState createState() => _FiltersState(); -// } - -// class _FiltersState extends State with SingleTickerProviderStateMixin { -// bool expand = false; -// Widget child; -// List _selectedFilters = List.filled(3, false); - -// AnimationController expandController; -// Animation animation; - -// void prepareAnimations() { -// expandController = AnimationController( -// vsync: this, -// duration: Duration(milliseconds: 500) -// ); -// animation = CurvedAnimation( -// parent: expandController, -// curve: Curves.fastOutSlowIn, -// ); -// } - -// void _runExpandCheck() { -// if(widget.expand) { -// expandController.forward(); -// } -// else { -// expandController.reverse(); -// } -// } - -// // void handleBottomSheet(BuildContext context) { -// // _openBottomSheet = !_openBottomSheet; - -// // if(_openBottomSheet) { -// // showBottomSheet( -// // context: context, -// // builder: (context) => Wrap( -// // children: [ -// // Container( -// // color: ColorPrimary, -// // height: 100, -// // child: ListView( -// // children: [ -// // ListTile( -// // title: Text('Show lower division', style: TextStyle(color: Colors.white)), -// // selected: _selectedFilters[0], -// // onTap: () => _selectedFilters[0] = true, -// // ), -// // ListTile( -// // leading: Icon(Icons.favorite), -// // title: Text('Show upper division'), -// // selected: _selectedFilters[1], -// // onTap: () => _selectedFilters[1] = true, -// // ), -// // ListTile( -// // leading: Icon(Icons.favorite), -// // title: Text('Show graduate division'), -// // selected: _selectedFilters[2], -// // onTap: () => _selectedFilters[2] = true, -// // ), -// // ] -// // ) -// // ) -// // ] -// // )); -// // } else { -// // Navigator.pop(context); -// // } -// // } -// // -// @override -// void didUpdateWidget(Filters oldWidget) { -// super.didUpdateWidget(oldWidget); -// _runExpandCheck(); -// } - -// @override -// void dispose() { -// expandController.dispose(); -// super.dispose(); -// } - -// @override -// Widget build(BuildContext context) { -// return SizeTransition( -// axisAlignment: 1.0, -// sizeFactor: animation, -// child: widget.child -// ); -// // return IconButton( -// // icon: Icon(Icons.filter_list, color: Colors.white), -// // padding: EdgeInsets.symmetric(horizontal: 9), -// // alignment: Alignment.centerLeft, -// // iconSize: 25, -// // onPressed: () { handleBottomSheet(context); }, -// // ); -// } -// } - class TermDropdown extends StatefulWidget { + const TermDropdown({Key? key}) : super(key: key); + @override _TermDropdownState createState() => _TermDropdownState(); } +// TODO(p8gonzal): Can be replaced by live API used in search class _TermDropdownState extends State { - List dropdownItems = ['FA19', 'WI20', 'SP20', 'FA20']; - String _dropdownVal = 'FA19'; + List dropdownItems = ['SP21', 'FA21', 'WI22', 'SP22']; + String _dropdownVal = 'SP22'; + late DropdownButton dropdownButton; + + String get dropDownValue { + return _dropdownVal; + } @override Widget build(BuildContext context) { - return DropdownButton( - underline: Container( - height: 0 - ), - value: _dropdownVal, - icon: Icon(Icons.arrow_drop_down, color: Colors.black, size: 20), - onChanged: (String newVal) { - setState(() { - _dropdownVal = newVal; - }); - }, - items: dropdownItems.map>((String val) { - return DropdownMenuItem( + dropdownButton = DropdownButton( + underline: Container(height: 0), + value: _dropdownVal, + icon: Icon(Icons.arrow_drop_down, color: Colors.black, size: 20), + onChanged: (String? newVal) { + setState(() { + _dropdownVal = newVal!; + }); + }, + items: dropdownItems.map>((String val) { + return DropdownMenuItem( value: val, - child: Text(val, style: TextStyle(color: darkGray, fontSize: 14, fontWeight: FontWeight.bold)) - ); - }).toList(), + child: Text(val, + style: TextStyle( + color: darkGray, + fontSize: 14, + fontWeight: FontWeight.bold))); + }).toList(), ); + + return dropdownButton; } } class Search extends StatefulWidget { + const Search({Key? key}) : super(key: key); + @override _SearchState createState() => _SearchState(); } class _SearchState extends State { Widget _icon = Icon(Icons.search, size: 20, color: darkGray); - final _searchText = TextEditingController(); - + ScheduleOfClassesProvider provider = ScheduleOfClassesProvider(); @override Widget build(BuildContext context) { - return Container( - height: 35, - child: Row(children: [ - Container( - margin: const EdgeInsets.only(left: 10.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - TermDropdown(), - Container( - width: 1.0, - color: darkGray, - margin: const EdgeInsets.only(right: 10.0), - ) - ], - ) - ), - Expanded( - child: TextField( - onChanged: (text) { - // _searchText = text; - if(text.length > 0) { - _icon = GestureDetector( - child: Icon(Icons.close, size: 20, color: darkGray), - onTap: () { - _searchText.clear(); + return SizedBox( + height: 35, + child: Row( + children: [ + Container( + margin: const EdgeInsets.only(left: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + TermDropdown(), + Container( + width: 1.0, + color: darkGray, + margin: const EdgeInsets.only(right: 10.0), + ) + ], + )), + Expanded( + child: TextField( + onChanged: (String text) { + provider.searchBarController.text = text; + if (text.isNotEmpty) { + _icon = GestureDetector( + child: Icon(Icons.close, size: 20, color: darkGray), + onTap: () { + provider.searchBarController.clear(); + }); + } else { + _icon = Icon(Icons.search, size: 20, color: darkGray); } - ); - } else { - _icon = Icon(Icons.search, size: 20, color: darkGray); - } - }, - controller: _searchText, - autofocus: true, - textAlignVertical: TextAlignVertical.center, - style: TextStyle(fontSize: 16), - decoration: InputDecoration( - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(vertical: 0), - hintText: 'Search', - isDense: true, + }, + controller: provider.searchBarController, + autofocus: true, + textAlignVertical: TextAlignVertical.center, + style: TextStyle(fontSize: 16), + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 0), + hintText: 'Search', + isDense: true, + ), + ), ), - ), - ), - Container( - margin: const EdgeInsets.only(right: 10.0), - child: _icon, - ), - ], - ) - ); + Container( + margin: const EdgeInsets.only(right: 10.0), + child: _icon, + ), + ], + )); } -} \ No newline at end of file +} diff --git a/lib/ui/search/search_detail.dart b/lib/ui/search/search_detail.dart new file mode 100644 index 0000000..b34e675 --- /dev/null +++ b/lib/ui/search/search_detail.dart @@ -0,0 +1,429 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:webreg_mobile_flutter/app_styles.dart'; +import 'package:webreg_mobile_flutter/core/models/schedule_of_classes.dart'; + +/* This UI page is used to show course offerings/details (prerequisites, sections, finals) +* after the user has searched and selected a course. +*/ +class SearchDetail extends StatelessWidget { + SearchDetail({Key? key, required this.data}) : super(key: key); + final CourseData data; + Map> instructorSections = + >{}; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + '${data.departmentCode} ${data.courseCode} \n${data.courseTitle}')), + body: ListView( + children: [coursePrereqs(), courseDetails()], + )); + + // TODO(p8gonzal): Need an API with this specific data + Card coursePrereqs() { + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: const SizedBox( + width: double.maxFinite, + height: 50, + child: Center( + child: Text( + 'Course Prerequisites and Level Restrictions', + style: TextStyle( + color: ColorSecondary, + fontWeight: FontWeight.bold, + ), + )))); + } + + // Determine types of sections + Widget courseDetails() { + final List sectionCards = []; + instructorSections = >{}; + + // Groups courses based on the professor + for (final SectionData section in data.sections!) { + final String sectionLetter = section.sectionCode![0]; + instructorSections.update(sectionLetter, (List value) { + value.add(section); + return value; + }, ifAbsent: () { + final List sectionList = []; + sectionList.add(section); + return sectionList; + }); + } + + // Collect all section types except for lecture sections + instructorSections.forEach((String key, List value) { + final List sectionTypes = []; + final List sectionObjects = []; + + // Ensure we only add one lecture object to the list of section data + // Fails when lecture has already been added + for (final SectionData section in value) { + if ((section.instructionType != 'LE' || !sectionTypes.contains('LE')) && + section.sectionStatus != 'CA') { + sectionTypes.add(section.instructionType!); + sectionObjects.add(section); + } + } + // Add an empty final section model, will be filled by lecture model later + sectionTypes.add('FI'); + sectionObjects.add(SectionData()); + + // Build section cards based off the list + int sectionIndex = 0; + for (final String sectionType in sectionTypes.toList()) { + sectionCards + .add(buildSectionCard(sectionType, sectionObjects[sectionIndex])); + sectionIndex++; + } + }); + + return Column(children: [...sectionCards]); + } + + Card buildSectionCard(String sectionType, SectionData sectionObject) { + switch (sectionType) { + case 'LE': + { + // Accumalate all lecture meetings in section + SectionData lectureObject = SectionData(); + final List lectureMeetings = []; + //instructorSections[sectionObject.sectionCode![0]]; + + for (final SectionData section + in instructorSections[sectionObject.sectionCode![0]]!) { + if (section.instructionType == 'LE') { + lectureMeetings.addAll(section.recurringMeetings!); + lectureObject = section; + } + } + + // Instructor name + String instructorName = ''; + for (final Instructor instructor in lectureObject.instructors!) { + if (instructor.primaryInstructor!) { + instructorName = instructor.instructorName!; + } + } + + // DAY Section + List days = resetDays(); + days = setDays(days, lectureMeetings); + + // Time parsing + // Checking for classes that start have *** format instead of **** + correctTimeFormat(lectureMeetings[0]); + const String prefix = '0000-01-01T'; + String startTime = DateFormat.jm() + .format(DateTime.parse(prefix + lectureMeetings[0].startTime!)); + String endTime = DateFormat.jm() + .format(DateTime.parse(prefix + lectureMeetings[0].endTime!)); + startTime = startTime.toLowerCase().replaceAll(' ', ''); + endTime = endTime.toLowerCase().replaceAll(' ', ''); + + // Room Code and Number parsing + final String room = lectureMeetings[0].buildingCode! + + ' ' + + lectureMeetings[0].roomCode!.substring(1); + + // Construct Card widget + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + margin: const EdgeInsets.only( + left: 10.0, right: 10.0, top: 2.0, bottom: 0.0), + child: SizedBox( + height: 50, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 0.0, right: 0.0, top: 5, bottom: 10), + child: Text( + instructorName, + style: const TextStyle(color: ColorSecondary), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + lectureObject.sectionCode!, + style: const TextStyle(color: darkGray), + ), + Text(sectionType), + ...days, + Text(startTime + ' - ' + endTime), + Text(room) + ], + ) + ], + ), + ), + ); + } + case 'DI': + { + // Accumalate all discussion meetings in section + final SectionData discussionObject = sectionObject; + final List discussionMeetings = []; + discussionMeetings.addAll(discussionObject.recurringMeetings!); + + // DAY Section + List days = resetDays(); + days = setDays(days, discussionMeetings); + + // Time + // Checking for classes that start have *** format instead of **** + correctTimeFormat(discussionMeetings[0]); + const String prefix = '0000-01-01T'; + String startTime = DateFormat.jm().format( + DateTime.parse(prefix + discussionMeetings[0].startTime!)); + String endTime = DateFormat.jm() + .format(DateTime.parse(prefix + discussionMeetings[0].endTime!)); + startTime = startTime.toLowerCase().replaceAll(' ', ''); + endTime = endTime.toLowerCase().replaceAll(' ', ''); + + // Room parsing + final String room = discussionMeetings[0].buildingCode! + + ' ' + + discussionMeetings[0].roomCode!.substring(1); + + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + margin: const EdgeInsets.only( + left: 10.0, right: 10.0, top: 2.0, bottom: 0.0), + child: SizedBox( + height: 35, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + discussionObject.sectionCode!, + style: const TextStyle(color: darkGray), + ), + Text(sectionType), + ...days, + Text(startTime + ' - ' + endTime), + Text(room) + ], + ), + )); + } + case 'LA': + { + // Accumalate all lab meetings in section + final SectionData labObject = sectionObject; + final List labMeetings = []; + labMeetings.addAll(sectionObject.recurringMeetings!); + + // DAY Section + List days = resetDays(); + days = setDays(days, labMeetings); + + // Time + // Checking for classes that start have *** format instead of **** + correctTimeFormat(labMeetings[0]); + const String prefix = '0000-01-01T'; + String startTime = DateFormat.jm() + .format(DateTime.parse(prefix + labMeetings[0].startTime!)); + String endTime = DateFormat.jm() + .format(DateTime.parse(prefix + labMeetings[0].endTime!)); + startTime = startTime.toLowerCase().replaceAll(' ', ''); + endTime = endTime.toLowerCase().replaceAll(' ', ''); + + // Room parsing + final String room = labMeetings[0].buildingCode! + + ' ' + + labMeetings[0].roomCode!.substring(1); + + // Construct Card widget + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + margin: const EdgeInsets.only( + left: 10.0, right: 10.0, top: 2.0, bottom: 0.0), + child: SizedBox( + height: 35, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + labObject.sectionCode!, + style: const TextStyle(color: darkGray), + ), + Text(sectionType), + ...days, + Text(startTime + ' - ' + endTime), + Text(room) + ], + ), + ), + ); + } + case 'TU': + { + // Accumalate all supplemental instruction meetings in section + final SectionData labObject = sectionObject; + final List labMeetings = []; + labMeetings.addAll(sectionObject.recurringMeetings!); + + // DAY Section + List days = resetDays(); + days = setDays(days, labMeetings); + + // Time + // Checking for classes that start have *** format instead of **** + correctTimeFormat(labMeetings[0]); + const String prefix = '0000-01-01T'; + String startTime = DateFormat.jm() + .format(DateTime.parse(prefix + labMeetings[0].startTime!)); + String endTime = DateFormat.jm() + .format(DateTime.parse(prefix + labMeetings[0].endTime!)); + startTime = startTime.toLowerCase().replaceAll(' ', ''); + endTime = endTime.toLowerCase().replaceAll(' ', ''); + + // Room parsing + final String room = labMeetings[0].buildingCode! + + ' ' + + labMeetings[0].roomCode!.substring(1); + + // Construct Card widget + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + margin: const EdgeInsets.only( + left: 10.0, right: 10.0, top: 2.0, bottom: 0.0), + child: SizedBox( + height: 35, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + labObject.sectionCode!, + style: const TextStyle(color: darkGray), + ), + Text(sectionType), + ...days, + Text(startTime + ' - ' + endTime), + Text(room) + ], + ), + ), + ); + } + case 'FI': + { + // Accumalate all lecture meetings in section + final List finalMeetings = []; + for (final SectionData section in data.sections!) { + if (section.instructionType == 'LE') { + finalMeetings.addAll(section.additionalMeetings!); + } + } + + // Time + // Checking for classes that start have *** format instead of **** + correctTimeFormat(finalMeetings[0]); + const String prefix = '0000-01-01T'; + String startTime = DateFormat.jm() + .format(DateTime.parse(prefix + finalMeetings[0].startTime!)); + startTime = startTime.toLowerCase().replaceAll(' ', ''); + String endTime = DateFormat.jm() + .format(DateTime.parse(prefix + finalMeetings[0].endTime!)); + endTime = endTime.toLowerCase().replaceAll(' ', ''); + + final String finalDate = DateFormat.MMMMd('en_US') + .format(DateTime.parse(finalMeetings[0].meetingDate!)); + + // Parse building code and room + final String room = ' ' + + finalMeetings[0].buildingCode! + + ' ' + + finalMeetings[0].roomCode!.substring(1); + + // Construct Card widget + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + margin: const EdgeInsets.only( + left: 10.0, right: 10.0, top: 2.0, bottom: 0.0), + child: SizedBox( + height: 35, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Text( + 'FINAL', + style: TextStyle(color: darkGray), + ), + Text(finalDate + ' ' + startTime + ' - ' + endTime), + Text(room) + ], + ), + )); + } + default: + return const Card( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Text('No data available.'), + ), + ); + } + } + + List resetDays() { + return [ + const Text('M', style: TextStyle(color: darkGray)), + const Text('T', style: TextStyle(color: darkGray)), + const Text('W', style: TextStyle(color: darkGray)), + const Text('T', style: TextStyle(color: darkGray)), + const Text('F', style: TextStyle(color: darkGray)) + ]; + } + + List setDays(List days, List meetings) { + for (final MeetingData meeting in meetings) { + if (meeting.dayCode == 'MO') { + days[0] = const Text('M', style: TextStyle(color: ColorSecondary)); + } else if (meeting.dayCode == 'TU') { + days[1] = const Text('T', style: TextStyle(color: ColorSecondary)); + } else if (meeting.dayCode == 'WE') { + days[2] = const Text('W', style: TextStyle(color: ColorSecondary)); + } else if (meeting.dayCode == 'TH') { + days[3] = const Text('T', style: TextStyle(color: ColorSecondary)); + } else if (meeting.dayCode == 'FR') { + days[4] = const Text('F', style: TextStyle(color: ColorSecondary)); + } + } + return days; + } + + // Fix an incorrectly formatted time + void correctTimeFormat(MeetingData meeting) { + while (meeting.startTime!.length < 4) { + meeting.startTime = '0' + meeting.startTime!; + } + while (meeting.endTime!.length < 4) { + meeting.endTime = '0' + meeting.endTime!; + } + } +} diff --git a/lib/ui/search/search_filters.dart b/lib/ui/search/search_filters.dart index 8ef7d4b..75b8bb6 100644 --- a/lib/ui/search/search_filters.dart +++ b/lib/ui/search/search_filters.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:webreg_mobile_flutter/app_constants.dart'; -import 'package:webreg_mobile_flutter/app_styles.dart'; class SearchFilters extends StatefulWidget { + const SearchFilters({Key? key}) : super(key: key); + @override - SearchFiltersState createState() => new SearchFiltersState(); + SearchFiltersState createState() => SearchFiltersState(); } class SearchFiltersState extends State { @override Widget build(BuildContext context) { - return ExpansionTile( - // title: , + return const ExpansionTile( + title: Text('Filters'), ); } -} \ No newline at end of file +} diff --git a/lib/ui/search/search_placeholder.dart b/lib/ui/search/search_placeholder.dart index 8ce5a4e..9c8badd 100644 --- a/lib/ui/search/search_placeholder.dart +++ b/lib/ui/search/search_placeholder.dart @@ -1,36 +1,37 @@ +// ignore_for_file: prefer_const_constructors + import 'package:flutter/material.dart'; import 'package:webreg_mobile_flutter/app_constants.dart'; import 'package:webreg_mobile_flutter/app_styles.dart'; class SearchPlaceholder extends StatelessWidget { + const SearchPlaceholder({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return Hero( - tag: 'search_bar', - child: RawMaterialButton( - onPressed: () { - Navigator.pushNamed(context, RoutePaths.SearchView); - }, - child: Container( - width: 63.0, - margin: EdgeInsets.symmetric(horizontal: 7.0, vertical: 10.0), - decoration: new BoxDecoration( - color: lightGray, - borderRadius: new BorderRadius.all(Radius.circular(100.0)), - ), - child: Container( - alignment: Alignment.centerRight, - margin: EdgeInsets.only(right: 5.0), - child: Icon(Icons.search, color: Colors.black, size: 25), - ) - ) - ) - ); + tag: 'search_bar', + child: RawMaterialButton( + onPressed: () { + Navigator.pushNamed(context, RoutePaths.SearchView); + }, + child: Container( + width: 63.0, + margin: + const EdgeInsets.symmetric(horizontal: 7.0, vertical: 10.0), + decoration: BoxDecoration( + color: lightGray, + borderRadius: BorderRadius.all(const Radius.circular(100.0)), + ), + child: Container( + alignment: Alignment.centerRight, + margin: const EdgeInsets.only(right: 5.0), + child: + const Icon(Icons.search, color: Colors.black, size: 25), + )))); } - @override State createState() { - // TODO: implement createState throw UnimplementedError(); } } diff --git a/lib/ui/search/search_view.dart b/lib/ui/search/search_view.dart index 1d980a2..cb95a72 100644 --- a/lib/ui/search/search_view.dart +++ b/lib/ui/search/search_view.dart @@ -1,104 +1,308 @@ +// ignore_for_file: use_key_in_widget_constructors, always_specify_types, cast_nullable_to_non_nullable + import 'package:flutter/material.dart'; -import 'package:webreg_mobile_flutter/ui/search/search_bar.dart'; +import 'package:provider/provider.dart'; +import 'package:webreg_mobile_flutter/app_constants.dart'; import 'package:webreg_mobile_flutter/app_styles.dart'; +import 'package:webreg_mobile_flutter/core/models/schedule_of_classes.dart'; +import 'package:webreg_mobile_flutter/core/providers/schedule_of_classes.dart'; +import 'package:webreg_mobile_flutter/core/providers/user.dart'; +import 'package:webreg_mobile_flutter/ui/search/search_bar.dart'; -// contains search bar and search results class SearchView extends StatefulWidget { @override _SearchViewState createState() => _SearchViewState(); } class _SearchViewState extends State { - bool openFilters = false; - List selectedFilters = List.filled(3, false); - List filters = ['Show lower division', 'Show upper division', 'Show gradudate division']; - // Map filters = {'Show lower division': false, 'Show upper division': false, 'Show gradudate division': false}; + late Future> classes; + late String searchString; + late ScheduleOfClassesProvider classesProvider; + bool showList = false; + late TermDropdown termDropdown; + List dropdownItems = ['SP21', 'FA21', 'WI22', 'SP22']; + String _dropdownVal = 'SP22'; + bool firstRun = true; - void setOpenFilters() { - this.setState(() { - openFilters = !openFilters; - }); + @override + void didChangeDependencies() { + super.didChangeDependencies(); + classesProvider = + Provider.of(context, listen: false); + classesProvider.userDataProvider = + Provider.of(context, listen: false); } @override - Widget build(BuildContext context) { + Widget build(BuildContext context) { return Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(kToolbarHeight), - child: Hero( - tag: 'search_bar', - child: SearchBar(setOpenFilters), - ), - ), - body: Stack( - children: [ - Center( - child: Text( - "Search by course code\ne.g. ANTH 23", - style: TextStyle(color: darkGray, fontSize: 18), - textAlign: TextAlign.center, - ) - ), - openFilters ? Positioned( - top: 0, - left: 0, - child: Container( - width: MediaQuery.of(context).size.width, - padding: EdgeInsets.symmetric(vertical: 10), - height: 120, - decoration: new BoxDecoration(color: ColorPrimary), - child: ListView.builder( - padding: const EdgeInsets.all(8), - itemCount: selectedFilters.length, - itemBuilder: (BuildContext context, int index) { - return Container( - height: 30, - padding: EdgeInsets.symmetric(horizontal: 35), - // color: Colors.amber[colorCodes[index]], - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(filters[index], style: TextStyle(color: Colors.white, fontSize: 16)), - Switch( - value: selectedFilters[index], - onChanged: (value) { + appBar: AppBar( + titleSpacing: 0.0, + centerTitle: true, + title: Container( + decoration: BoxDecoration( + color: lightGray, + borderRadius: const BorderRadius.all(Radius.circular(100.0)), + border: Border.all(width: 1.0, color: const Color(0xFF034263)), + ), + margin: const EdgeInsets.symmetric(vertical: 10.0), + child: SizedBox( + height: 35, + child: Row( + children: [ + Container( + margin: const EdgeInsets.only(left: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + FutureBuilder( + future: classesProvider.scheduleOfClassesService + .fetchTerms(), + builder: (BuildContext context, + AsyncSnapshot response) { + // Only call lambda upon first run to avoid unneccessary network load + + if (response.hasData || !firstRun) { + if (firstRun) { + dropdownItems = + response.data as List; + _dropdownVal = dropdownItems[ + dropdownItems.length - 1]; + } + //Otherwise will use local reference + return DropdownButton( + underline: Container(height: 0), + value: _dropdownVal, + icon: const Icon(Icons.arrow_drop_down, + color: Colors.black, size: 20), + onChanged: (String? newVal) { + setState(() { + firstRun = false; + _dropdownVal = newVal!; + }); + }, + items: dropdownItems + .map>( + (String val) { + return DropdownMenuItem( + value: val, + child: Text(val, + style: const TextStyle( + color: darkGray, + fontSize: 14, + fontWeight: + FontWeight.bold))); + }).toList(), + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), + Container( + width: 1.0, + color: darkGray, + margin: const EdgeInsets.only(right: 10.0), + ) + ], + )), + Expanded( + child: TextField( + onSubmitted: (String text) { setState(() { - selectedFilters[index] = value; + searchString = text; + showList = true; }); }, - activeTrackColor: Colors.green, - activeColor: Colors.white, + autofocus: true, + textAlignVertical: TextAlignVertical.center, + style: const TextStyle(fontSize: 16), + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 0), + hintText: 'Search', + isDense: true, + ), ), - ] - ) - ); - } - ), - // ListView( - // children: [ - // ListTile( - // title: Text('Show lower division', style: TextStyle(color: Colors.white)), - // // selected: _selectedFilters[0], - // // onTap: () => _selectedFilters[0] = true, - // ), - // ListTile( - // leading: Icon(Icons.favorite), - // title: Text('Show upper division'), - // // selected: _selectedFilters[1], - // // onTap: () => _selectedFilters[1] = true, - // ), - // ListTile( - // leading: Icon(Icons.favorite), - // title: Text('Show graduate division'), - // // selected: _selectedFilters[2], - // // onTap: () => _selectedFilters[2] = true, - // ), - // ] - // ) - ) - ) : SizedBox(), + ), + Container( + margin: const EdgeInsets.only(right: 10.0), + child: + const Icon(Icons.search, size: 20, color: darkGray), + ), + ], + )), + ), + automaticallyImplyLeading: false, + leading: Center( + child: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + padding: const EdgeInsets.symmetric(horizontal: 9), + alignment: Alignment.centerLeft, + iconSize: 25, + onPressed: () { + Navigator.pop(context); + }), + ), + actions: [ + IconButton( + icon: const Icon(Icons.filter_list, color: Colors.white), + padding: const EdgeInsets.symmetric(horizontal: 9), + alignment: Alignment.centerLeft, + iconSize: 25, + onPressed: () {} //this.setOpenFilters, + ), + ]), + body: body(showList)); + } + + //Body will change based on if search has been completed by the API or not + Widget body(bool showList) { + bool validQuery = false; + String searchBuilder = ''; + final String termCode = _dropdownVal; + + // Will be true when user has clicked into search bar + if (showList) { + final List words = searchString.split(' '); + + switch (words.length) { + // This case is true when looking for course only (e.g. CSE) + case 1: + { + //Verify that subject code could be valid + if (words[0].length > 4 || words[0].length < 3) { + validQuery = false; + break; + } + //Attempt to use the single query as a subject code, limit at 10 * #approx entries per course == 100 + validQuery = true; + searchBuilder = + 'subjectCodes=${words[0]}&termCode=$termCode&limit=100'; + } + break; + // This is the case when searching for specific course (e.g. CSE 110) + case 2: + { + final String firstWord = words[0]; + final String secondWord = words[1]; + // Verify that the course and subject code could be valid + if ((firstWord.length > 4 || firstWord.length < 3) && + (secondWord.length > 4 || secondWord.length < 3)) { + validQuery = false; + break; + } + validQuery = true; + searchBuilder = + 'subjectCodes=$firstWord&courseCodes=$secondWord&termCode=$termCode&limit=100'; + } + break; + default: + { + validQuery = false; + } + } + + if (!validQuery) { + return const Center( + child: Text( + 'Not a valid query. Please type in a valid course or subject code.', + style: TextStyle(color: darkGray, fontSize: 18), + textAlign: TextAlign.center, + )); + } + // Will build results list when schedule of classes api has returned + return FutureBuilder( + future: classesProvider.fetchClasses(searchBuilder), + builder: (BuildContext context, AsyncSnapshot response) { + if (response.hasData) { + return buildResultsList(context); + } else { + return const CircularProgressIndicator(); + } + }, + ); + } else { + // When user has not clicked into search bar, hint is displayed + return const Center( + child: Text( + 'Search by course code\ne.g. ANTH 23', + style: TextStyle(color: darkGray, fontSize: 18), + textAlign: TextAlign.center, + )); + } + } + + // Method builds results list once a model of the repsonse has been completed + Widget buildResultsList(BuildContext context) { + // List arguments = widget.args; + // loops through and adds buttons for the user to click on + /// add content into for loop here + final ScheduleOfClassesModel model = + classesProvider.scheduleOfClassesService.classes!; + + // Check for non valid courses + if (model.courses!.isEmpty) { + return Column( + children: const [ + Expanded( + child: Center( + child: Text( + 'Not a valid query. Please type in a valid course or subject code.', + style: TextStyle(color: darkGray, fontSize: 18), + textAlign: TextAlign.center, + ))) ], - ), + ); + } + final List contentList = []; + for (final CourseData course in model.courses!) { + contentList.add(ListTile( + title: Row(children: [ + // units icon + Container( + height: 30, + width: 30, + decoration: const BoxDecoration( + color: lightGray, + shape: BoxShape.circle, + ), + margin: const EdgeInsets.only(right: 10), + child: Center(child: Text(course.unitsMax.toString()))), + + // Course info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(course.departmentCode! + ' ' + course.courseCode!, + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)) + ], + ), + Text(course.courseTitle!) + ], + )) + ]), + onTap: () { + // Go to detail view for a specific course, passing in data model + Navigator.pushNamed(context, RoutePaths.SearchDetail, + arguments: course); + }, + )); + } + // adds SizedBox to have a grey underline for the last item in the list + final ListView contentListView = ListView( + shrinkWrap: true, + children: + ListTile.divideTiles(tiles: contentList, context: context).toList(), + ); + return Column( + children: [Expanded(child: contentListView)], ); } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index d7f289e..f572d93 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,13 +1,27 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + asn1lib: + dependency: transitive + description: + name: asn1lib + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,14 +35,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -43,6 +57,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" cupertino_icons: dependency: "direct main" description: @@ -50,6 +78,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + dio: + dependency: "direct main" + description: + name: dio + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + encrypt: + dependency: "direct main" + description: + name: encrypt + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1" fake_async: dependency: transitive description: @@ -69,6 +111,48 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -79,6 +163,20 @@ packages: description: flutter source: sdk version: "0.0.0" + get: + dependency: "direct main" + description: + name: get + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.4" + hive: + dependency: "direct main" + description: + name: hive + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" http: dependency: transitive description: @@ -93,6 +191,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.0" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" js: dependency: transitive description: @@ -106,21 +211,28 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" package_info_plus: dependency: "direct main" description: name: package_info_plus url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" package_info_plus_linux: dependency: transitive description: @@ -176,7 +288,21 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.2" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.0" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.3" sky_engine: dependency: transitive description: flutter @@ -188,7 +314,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" stack_trace: dependency: transitive description: @@ -223,7 +349,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19" + version: "0.4.3" typed_data: dependency: transitive description: @@ -237,7 +363,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" win32: dependency: transitive description: @@ -246,5 +372,5 @@ packages: source: hosted version: "2.0.5" sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" + dart: ">=2.14.0 <3.0.0" + flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0411afa..baf8977 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,12 +3,20 @@ description: A new Flutter project. publish_to: none version: 1.0.0+1 environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" +encrypt: 5.0.0 dependencies: cupertino_icons: 1.0.0 flutter: sdk: flutter package_info_plus: 1.0.1 + dio: 4.0.0 + get: 4.1.4 + intl: 0.17.0 + provider: ^6.0.2 + encrypt: ^5.0.1 + hive: ^2.0.5 + flutter_secure_storage: ^5.0.2 dev_dependencies: flutter_test: sdk: flutter diff --git a/web/static.html b/web/static.html new file mode 100644 index 0000000..2b105e0 --- /dev/null +++ b/web/static.html @@ -0,0 +1,18 @@ + + + + + + Authenticated + + + + + + + + + \ No newline at end of file