diff --git a/analysis_options.yaml b/analysis_options.yaml index 5801b01..52dc4c2 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -20,8 +20,8 @@ analyzer: strong-mode: - implicit-casts: false - implicit-dynamic: false + # 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..8650609 100644 --- a/lib/app_constants.dart +++ b/lib/app_constants.dart @@ -7,10 +7,32 @@ class RoutePaths { 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 authorizedPostErrors = 'Failed to upload data: '; + static const authorizedPutErrors = 'Failed to update data: '; + static const invalidBearerToken = 'Invalid bearer token'; + static const duplicateRecord = + 'DioError [DioErrorType.response]: Http status error [409]'; + static const invalidMedia = + 'DioError [DioErrorType.response]: Http status error [415]'; + static const silentLoginFailed = "Silent login failed"; +} + +class LoginConstants { + static const silentLoginFailedTitle = 'Oops! You\'re not logged in.'; + static const silentLoginFailedDesc = + 'The system has logged you out (probably by mistake). Go to Profile to log back in.'; + static const loginFailedTitle = 'Sorry, unable to sign you in.'; + static const 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..2b206d8 --- /dev/null +++ b/lib/app_networking.dart @@ -0,0 +1,156 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:webreg_mobile_flutter/app_constants.dart'; +import 'package:webreg_mobile_flutter/app_styles.dart'; + +class NetworkHelper { + ///TODO: inside each service that file place a switch statement to handle all + ///TODO: different errors thrown by the Dio client DioErrorType.RESPONSE + 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 { + Dio dio = new Dio(); + dio.options.connectTimeout = 20000; + dio.options.receiveTimeout = 20000; + dio.options.responseType = ResponseType.plain; + final _response = await dio.get(url); + + if (_response.statusCode == 200) { + // If server returns an OK response, return the body + return _response.data; + } else { + ///TODO: log this as a bug because the response was bad + // If that response was not OK, throw an error. + throw Exception('Failed to fetch data: ' + _response.data); + } + } + + Future authorizedFetch( + String url, Map headers) async { + Dio dio = new Dio(); + dio.options.connectTimeout = 20000; + dio.options.receiveTimeout = 20000; + dio.options.responseType = ResponseType.plain; + dio.options.headers = headers; + final _response = await dio.get( + url, + ); + if (_response.statusCode == 200) { + // If server returns an OK response, return the body + return _response.data; + } else { + ///TODO: log this as a bug because the response was bad + // If that response was not OK, throw an error. + + throw Exception('Failed to fetch data: ' + _response.data); + } + } + + // Widget getSilentLoginDialog() { + // return AlertDialog( + // title: const Text(LoginConstants.silentLoginFailedTitle), + // content: Text(LoginConstants.silentLoginFailedDesc), + // actions: [ + // TextButton( + // style: TextButton.styleFrom( + // primary: ucLabelColor, + // ), + // onPressed: () { + // Get.back(closeOverlays: true); + // }, + // child: const Text('OK'), + // ), + // ], + // ); + // } + + Future authorizedPost( + String url, Map? headers, dynamic body) async { + Dio dio = new Dio(); + dio.options.connectTimeout = 20000; + dio.options.receiveTimeout = 20000; + dio.options.headers = headers; + final _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. + 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) { + String message = _response.data['message'] ?? ''; + throw Exception(ErrorConstants.authorizedPostErrors + message); + } else if (_response.statusCode == 500) { + String message = _response.data['message'] ?? ''; + throw Exception(ErrorConstants.authorizedPostErrors + message); + } else if (_response.statusCode == 409) { + 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 { + Dio dio = new Dio(); + dio.options.connectTimeout = 20000; + dio.options.receiveTimeout = 20000; + dio.options.headers = headers; + final _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. + 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) { + String message = _response.data['message'] ?? ''; + throw Exception(ErrorConstants.authorizedPutErrors + message); + } else if (_response.statusCode == 500) { + String message = _response.data['message'] ?? ''; + throw Exception(ErrorConstants.authorizedPutErrors + message); + } else { + throw Exception(ErrorConstants.authorizedPutErrors + 'unknown error'); + } + } + + Future authorizedDelete( + String url, Map headers) async { + Dio dio = new Dio(); + dio.options.connectTimeout = 20000; + dio.options.receiveTimeout = 20000; + dio.options.headers = headers; + try { + final _response = await dio.delete(url); + if (_response.statusCode == 200) { + // If server returns an OK response, return the body + return _response.data; + } else { + ///TODO: log this as a bug because the response was bad + // If that response was not OK, throw an error. + throw Exception('Failed to delete data: ' + _response.data); + } + } on TimeoutException catch (e) { + // 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..32cccd2 --- /dev/null +++ b/lib/app_provider.dart @@ -0,0 +1,25 @@ +import 'package:provider/provider.dart'; +import 'package:provider/single_child_widget.dart'; +import 'package:webreg_mobile_flutter/core/providers/schedule_of_classes.dart'; +import 'package:webreg_mobile_flutter/core/providers/user.dart'; + +List providers = [ + ChangeNotifierProvider( + create: (_) { + var _userDataProvider = UserDataProvider(); + + /// try to load any persistent saved data + /// once loaded from memory get the user's online profile + _userDataProvider = UserDataProvider(); + return _userDataProvider; + }, + ), + ChangeNotifierProxyProvider( + create: (_) { + return ScheduleOfClassesProvider(); + }, update: (_, UserDataProvider userDataProvider, + ScheduleOfClassesProvider? scheduleOfClassesProvider) { + scheduleOfClassesProvider!.userDataProvider = userDataProvider; + return scheduleOfClassesProvider; + }) +]; diff --git a/lib/app_router.dart b/lib/app_router.dart index 1c1a1d0..3b1b5fb 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -1,20 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.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_error.dart'; import 'package:webreg_mobile_flutter/ui/list/course_list_view.dart'; +import 'package:webreg_mobile_flutter/ui/login/login.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'; +import 'package:webreg_mobile_flutter/core/models/schedule_of_classes.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: return MaterialPageRoute(builder: (_) => BottomNavigation()); + case RoutePaths.AuthenticationError: + return MaterialPageRoute(builder: (_) => AuthenticationError()); + // case RoutePaths.Login: + // return MaterialPageRoute(builder: (_) => Login()); case RoutePaths.SearchView: return MaterialPageRoute(builder: (_) => SearchView()); case RoutePaths.CourseListView: return MaterialPageRoute(builder: (_) => CourseListView()); + case RoutePaths.SearchDetail: + final CourseData course = settings.arguments! as CourseData; + return MaterialPageRoute(builder: (_) { + return SearchDetail(data: course); + }); + + default: + return MaterialPageRoute(builder: (_) => BottomNavigation()); } } -} \ No newline at end of file +} diff --git a/lib/core/models/authentication.dart b/lib/core/models/authentication.dart new file mode 100644 index 0000000..22b0ff6 --- /dev/null +++ b/lib/core/models/authentication.dart @@ -0,0 +1,74 @@ +// 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 { + @HiveField(0) + String? accessToken; + // Deprecated reserved field number - DO NOT REMOVE + // @HiveField(1) + // String refreshToken; + @HiveField(2) + String? pid; + @HiveField(3) + String? ucsdaffiliation; + @HiveField(4) + int? expiration; + + 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, + ); + } + + 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/schedule_of_classes.dart b/lib/core/models/schedule_of_classes.dart new file mode 100644 index 0000000..ce40da2 --- /dev/null +++ b/lib/core/models/schedule_of_classes.dart @@ -0,0 +1,245 @@ +import 'dart:convert'; + +import 'dart:core'; + +import 'dart:core'; + +ScheduleOfClassesModel classScheduleModelFromJson(String str) => + ScheduleOfClassesModel.fromJson(json.decode(str)); + +String classScheduleModelToJson(ScheduleOfClassesModel data) => + json.encode(data.toJson()); + +class ScheduleOfClassesModel { + Metadata? metadata; + List? courses; + + ScheduleOfClassesModel({ + this.metadata, + this.courses, + }); + + factory ScheduleOfClassesModel.fromJson(Map json) => + ScheduleOfClassesModel( + metadata: Metadata.fromJson(json['metadata']), courses:List.from( json["data"].map((x) => CourseData.fromJson(x)))); + + // List.from( + // json["data"].map((x) => ClassData.fromJson(x))) + Map toJson() => { + "metadata": metadata!.toJson(), + "data": List.from(courses!.map((x) => x.toJson())) + }; +} + +class Metadata { + Metadata(); + + factory Metadata.fromJson(Map? json) => Metadata(); + + Map toJson() => {}; +} + +class CourseData { + String? subjectCode; + String? courseCode; + String? departmentCode; + String? courseTitle; + double? unitsMin; + double? unitsMax; + double? unitsInc; + String? academicLevel; + List? sections; + + 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: 0.0, + unitsMax: 0.0, + unitsInc: 0.0, + academicLevel: json['academicLevel'] ?? '', + sections: List.from( + json["sections"].map((x) => SectionData.fromJson(x)))); + + 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 { + 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; + + 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}); + + 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)))); + + 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())) + }; +} + +class MeetingData { + String? meetingType; + String? meetingDate; + String? dayCode; + String? dayCodeIsis; + String? startTime; + String? endTime; + String? buildingCode; + String? roomCode; + + 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'] ?? ''); + + Map toJson() => { + 'meetingType': meetingType, + 'meetingDate': meetingDate, + 'dayCode': dayCode, + 'dayCodeIsis': dayCodeIsis, + 'startTime': startTime, + 'endTime': endTime, + 'buildingCode': buildingCode, + 'roomCode': roomCode + }; +} + +class Instructor { + String? pid; + String? instructorName; + bool? primaryInstructor; + String? instructorEmailAddress; + double? workLoadUnitQty; + double? percentOfLoad; + + 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'] ?? '', + percentOfLoad: json['percentOfLoad'] ?? ''); + + Map toJson() => { + 'pid': pid, + 'instructorName': instructorName, + 'primaryInstructor': primaryInstructor, + 'instructorEmailAddress': instructorEmailAddress, + 'workLoadUnityQty': workLoadUnitQty, + 'percentOfLoad': percentOfLoad + }; +} diff --git a/lib/core/providers/schedule_of_classes.dart b/lib/core/providers/schedule_of_classes.dart new file mode 100644 index 0000000..1702704 --- /dev/null +++ b/lib/core/providers/schedule_of_classes.dart @@ -0,0 +1,75 @@ +import 'package:webreg_mobile_flutter/app_constants.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'; +import 'package:flutter/material.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; + //List get searchHistory => _searchHistory; + 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..99d2a9b --- /dev/null +++ b/lib/core/providers/user.dart @@ -0,0 +1,23 @@ +import 'dart:convert'; +import 'dart:typed_data' show Uint8List; + +import 'package:encrypt/encrypt.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive/hive.dart'; +import 'package:pointycastle/asymmetric/api.dart'; +import 'package:pointycastle/asymmetric/oaep.dart'; +import 'package:pointycastle/pointycastle.dart' as pc; + +class UserDataProvider extends ChangeNotifier { + String? accessToken = ''; + + set setToken(String? token) { + accessToken = token; + notifyListeners(); + } + + 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..2729ac8 --- /dev/null +++ b/lib/core/services/authentication.dart @@ -0,0 +1,15 @@ +import 'package:webreg_mobile_flutter/core/models/authentication.dart'; + +import '../../app_networking.dart'; + +class AuthenticationService { + AuthenticationService(); + String? error; + AuthenticationModel? _data; + DateTime? _lastUpdated; + + final NetworkHelper _networkHelper = NetworkHelper(); + + + +} diff --git a/lib/core/services/schedule_of_classes.dart b/lib/core/services/schedule_of_classes.dart new file mode 100644 index 0000000..3c3ad7f --- /dev/null +++ b/lib/core/services/schedule_of_classes.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +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 { + 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 { + //await getNewToken(); + + /// fetch data + final String? _response = await _networkHelper.authorizedFetch( + baseEndpoint + '?' + query, headers); + if (_response != null) { + final ScheduleOfClassesModel data = + classScheduleModelFromJson(_response); + classes = data; + } else { + /// parse data + + return false; + } + _isLoading = false; + return true; + } catch (e) { + _error = e.toString(); + _isLoading = false; + return false; + } + } + + 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..ba17954 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,35 +1,63 @@ +import 'dart:html'; + import 'package:flutter/material.dart'; +import 'package:get/route_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:webreg_mobile_flutter/app_provider.dart'; import 'package:webreg_mobile_flutter/ui/search/search_placeholder.dart'; import 'package:webreg_mobile_flutter/app_styles.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_router.dart' as webregMobileRouter; +import 'dart:js' as js; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +void getParams() { + var uri = Uri.dataFromString(window.location.href); + Map params = uri.queryParameters; + String? _token = params['token']; +} void main() { + setUrlStrategy(PathUrlStrategy()); runApp(MyApp()); } class MyApp extends StatelessWidget { + late String? _token = ''; +// Our current app URL + + void getParams() { + var uri = Uri.dataFromString(window.location.href); + 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) + final currentUri = Uri.base; + getParams(); + + return MultiProvider( + providers: providers, + child: GetMaterialApp( + 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)), ), + initialRoute: + _token == '' ? RoutePaths.AuthenticationError : RoutePaths.Home, + onGenerateRoute: webregMobileRouter.Router.generateRoute, ), - initialRoute: RoutePaths.Home, - onGenerateRoute: webregMobileRouter.Router.generateRoute, ); } } diff --git a/lib/ui/calendar/calendar.dart b/lib/ui/calendar/calendar.dart index 48de6c3..0f2ab34 100644 --- a/lib/ui/calendar/calendar.dart +++ b/lib/ui/calendar/calendar.dart @@ -92,7 +92,6 @@ class Calendar extends StatelessWidget { .difference(DateTime.parse(prefix + start)) .inMinutes .toDouble(); - print(diff.toString()); return diff; } @@ -177,16 +176,16 @@ class Calendar extends StatelessWidget { }), CalendarCard('10:00', '10:50', '2020-06-06T', 0, 'LE', 'CSE 110', - 'Center 109'), + 'Center 109', color), CalendarCard('10:00', '10:50', '2020-06-06T', 2, 'LE', 'CSE 110', - 'Center 109'), + 'Center 109', color), CalendarCard('10:00', '10:50', '2020-06-06T', 4, 'LE', 'CSE 110', - 'Center 109'), + 'Center 109', color), CalendarCard('11:00', '12:20', '2020-06-06T', 1, 'DI', 'CSE 100', - 'WLH 109'), + 'WLH 109', color), CalendarCard('11:00', '12:20', '2020-06-06T', 3, 'DI', 'CSE 100', - 'WLH 109'), + 'WLH 109', color), ])), BuildInfo(), ], diff --git a/lib/ui/calendar/calendar_card.dart b/lib/ui/calendar/calendar_card.dart index 1c77f98..8ef58c6 100644 --- a/lib/ui/calendar/calendar_card.dart +++ b/lib/ui/calendar/calendar_card.dart @@ -3,12 +3,13 @@ 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 { final String startTime, endTime, datePrefix, type, title, location; final int dayOfWeek; + final Color color; - const CalendarCard(this.startTime, this.endTime, this.datePrefix, this.dayOfWeek, this.type, this.title, this.location); + const CalendarCard(this.startTime, this.endTime, this.datePrefix, + this.dayOfWeek, this.type, this.title, this.location, this.color); @override _CalendarCardState createState() => _CalendarCardState(); @@ -18,79 +19,85 @@ class _CalendarCardState extends State { static const 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()); + 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; + double calendarCardWidth = (MediaQuery.of(context).size.width - + CalendarStyles.calendarTimeWidth - + 20) / + 7; bool _showModal = false; 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)), // 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)), + ])) + ])))); } } - - diff --git a/lib/ui/common/authentication_error.dart b/lib/ui/common/authentication_error.dart new file mode 100644 index 0000000..1cdec1b --- /dev/null +++ b/lib/ui/common/authentication_error.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:webreg_mobile_flutter/core/providers/user.dart'; +import 'dart:html' as html; + +import '../../app_constants.dart'; + +class AuthenticationError extends StatefulWidget { + @override + _AuthenticationErrorState createState() => _AuthenticationErrorState(); +} + +class _AuthenticationErrorState extends State { + late UserDataProvider userDataProvider; + String _token = ''; + String clientId = 'CLIENT_ID_PLACEHOLDER'; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + userDataProvider = Provider.of(context); + } + + @override + void initState() { + super.initState(); + + final currentUrl = Uri.base; + if (!currentUrl.fragment.contains('access_token=')) { + // You are not connected so redirect to the Twitch authentication page. + WidgetsBinding.instance!.addPostFrameCallback((_) { + html.window.location.assign( + 'https://api-qa.ucsd.edu:8243/authorize?response_type=token&client_id=$clientId&redirect_uri=${currentUrl.origin}&scope=viewing_activity_read', + ); + // final fragments = currentUrl.fragment.split('&'); + // _token = fragments + // .firstWhere((e) => e.startsWith('access_token=')) + // .substring('access_token='.length); + // userDataProvider.accessToken = _token; + }); + } else { + final fragments = currentUrl.fragment.split('&'); + _token = fragments + .firstWhere((e) => e.startsWith('access_token=')) + .substring('access_token='.length); + userDataProvider.accessToken = _token; + } + } +// @override +// void initState() { +// super.initState(); + +// final currentUrl = Uri.base; +// if (!currentUrl.fragment.contains('access_token=')) { +// // You are not connected so redirect to the Twitch authentication page. +// WidgetsBinding.instance!.addPostFrameCallback((_) { +// html.window.location.assign( +// 'https://id.twitch.tv/oauth2/authorize?response_type=token&client_id=$clientId&redirect_uri=${currentUrl.origin}&scope=viewing_activity_read', +// ); +// }); +// } else { +// // You are connected, you can grab the code from the url. +// final fragments = currentUrl.fragment.split('&'); +// _token = fragments +// .firstWhere((e) => e.startsWith('access_token=')) +// .substring('access_token='.length); +// } +// } + + @override + Widget build(BuildContext context) { + if (userDataProvider.accessToken != '') { + Navigator.pushNamedAndRemoveUntil( + context, RoutePaths.Home, (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..3fb8418 100644 --- a/lib/ui/common/build_info.dart +++ b/lib/ui/common/build_info.dart @@ -49,7 +49,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..1f7d776 100644 --- a/lib/ui/list/course_card.dart +++ b/lib/ui/list/course_card.dart @@ -6,445 +6,450 @@ import 'package:webreg_mobile_flutter/app_styles.dart'; 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', - ], + { + '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': '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', - ], + { + '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': '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', + ], + }, + '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': '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', - ], + { + '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', + ], + }, }, - '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', - ], - }, - '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', - ], - }, - '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: 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: Icon(Icons.autorenew, color: ColorPrimary), + onPressed: () {}, + ), + IconButton( + icon: Icon(Icons.delete, color: ColorPrimary), + onPressed: () {}, + ), + IconButton( + icon: Icon(Icons.add_circle, color: ColorPrimary), + onPressed: () {}, + ), + ])); } Widget renderSection() { @@ -452,29 +457,38 @@ 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 - ], - ) - ), + 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: [ + 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 + ], + )), 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 ), Expanded( flex: 5, @@ -489,88 +503,90 @@ class CourseCard extends StatelessWidget { return Card( elevation: 0, shape: RoundedRectangleBorder( - side: new BorderSide(color: ColorPrimary, width: 2.0), - borderRadius: BorderRadius.circular(10.0) - ), + side: new BorderSide(color: ColorPrimary, width: 2.0), + borderRadius: BorderRadius.circular(10.0)), margin: 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: [ + 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( - height: 30, - width: 30, - decoration: new BoxDecoration( - color: lightGray, - shape: BoxShape.circle, - ), - margin: EdgeInsets.only(right: 10), - child: Center( - child: Text( - '4' // TODO - ) - ) - ), + height: 30, + width: 30, + decoration: new BoxDecoration( + color: lightGray, + shape: BoxShape.circle, + ), + margin: EdgeInsets.only(right: 10), + child: Center( + child: Text('4' // TODO + ))), // 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 - ], - ) - ) - ] + 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 + ], + )) + ]), ), - ), - // 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( + // instructor andd section id + Container( + margin: 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 - ] - ) - ] + 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(), - ], - ) - ), + // 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..8f1d985 100644 --- a/lib/ui/list/course_list_view.dart +++ b/lib/ui/list/course_list_view.dart @@ -8,25 +8,20 @@ class CourseListView extends StatelessWidget { @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: [ + TermDropdown(), + Expanded( + child: Container( + child: ListView.builder( + itemCount: 10, + padding: EdgeInsets.symmetric(vertical: 8), + itemBuilder: (BuildContext context, int index) { + return Container( + child: CourseCard(), + ); + }), + )) + ])); } } @@ -43,38 +38,34 @@ class _TermDropdownState extends State { @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: EdgeInsets.only(top: 10), + padding: EdgeInsets.symmetric(horizontal: 60), + child: Stack(children: [ Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.alarm, color: Colors.black), - ] - ), + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.alarm, color: Colors.black), + ]), Center( - child: Text(_dropdownVal, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)) - ), + child: Text(_dropdownVal, + style: 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) { + 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: TextStyle(fontSize: 18)))); }).toList(), ) - ] - ) - ); + ])); } } diff --git a/lib/ui/login/login.dart b/lib/ui/login/login.dart new file mode 100644 index 0000000..bd44df4 --- /dev/null +++ b/lib/ui/login/login.dart @@ -0,0 +1,215 @@ +// import 'package:flutter/material.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/providers/user.dart'; + +// class Login extends StatefulWidget { +// @override +// _LoginState createState() => _LoginState(); +// } + +// class _LoginState extends State { +// late UserDataProvider _userDataProvider; +// final TextEditingController _emailTextFieldController = +// TextEditingController(); +// final TextEditingController _passwordTextFieldController = +// TextEditingController(); +// bool _passwordObscured = true; + +// @override +// void didChangeDependencies() { +// super.didChangeDependencies(); +// _userDataProvider = Provider.of(context); +// } + +// @override +// Widget build(BuildContext buildContext) { +// return Scaffold( +// appBar: AppBar( +// elevation: 0.0, +// title: const Text('UC San Diego WebReg'), +// ), +// backgroundColor: lightPrimaryColor, +// body: !_userDataProvider.isLoading! +// ? buildLoginWidget() +// : const CircularProgressIndicator(), +// ); +// } + +// Widget buildLoginWidget() { +// return Center( +// child: ConstrainedBox( +// constraints: const BoxConstraints(maxWidth: 300), +// child: SingleChildScrollView( +// child: Column( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// Image.asset( +// 'assets/images/UCSanDiegoLogo-nav.png', +// fit: BoxFit.contain, +// height: 50, +// color: Colors.white, +// ), +// const SizedBox(height: 100.0), +// Padding( +// padding: const EdgeInsets.only(top: 0.0), +// child: Container( +// decoration: BoxDecoration( +// borderRadius: +// const BorderRadius.all(Radius.circular(100)), +// color: Theme.of(context) +// .colorScheme +// .secondary), //lightTextFieldBorderColor, +// child: TextField( +// style: const TextStyle( +// textBaseline: TextBaseline.alphabetic, +// color: Colors.black), +// decoration: const InputDecoration( +// hintText: 'UCSD Email', +// focusedBorder: OutlineInputBorder( +// borderRadius: +// BorderRadius.all(Radius.circular(5)), +// ), +// enabledBorder: OutlineInputBorder( +// borderRadius: +// BorderRadius.all(Radius.circular(5)), +// ), +// contentPadding: EdgeInsets.only(left: 10), +// hintStyle: TextStyle(color: ColorPrimary), +// fillColor: Colors.white, +// filled: true, +// ), +// keyboardType: TextInputType.emailAddress, +// controller: _emailTextFieldController, +// ), +// )), +// const SizedBox(height: 15), +// Container( +// decoration: BoxDecoration( +// borderRadius: +// const BorderRadius.all(Radius.circular(100)), +// color: Theme.of(context).colorScheme.secondary), +// child: TextField( +// style: const TextStyle( +// textBaseline: TextBaseline.alphabetic, +// color: Colors.black, +// ), +// decoration: InputDecoration( +// hintText: 'Password', +// suffixIcon: IconButton( +// icon: Icon( +// // Based on passwordObscured state choose the icon +// _passwordObscured +// ? Icons.visibility_off +// : Icons.visibility, +// color: Theme.of(context).primaryColorDark, +// ), +// onPressed: () => _toggle(), +// ), +// focusedBorder: const OutlineInputBorder( +// borderSide: BorderSide( +// color: Colors.black, +// ), +// borderRadius: BorderRadius.all(Radius.circular(5)), +// ), +// enabledBorder: const OutlineInputBorder( +// borderSide: BorderSide( +// color: Colors.black, +// ), +// borderRadius: BorderRadius.all(Radius.circular(5)), +// ), +// contentPadding: const EdgeInsets.only(left: 10), +// hintStyle: const TextStyle(color: ColorPrimary), +// fillColor: Colors.white, +// filled: true, +// ), +// obscureText: _passwordObscured, +// controller: _passwordTextFieldController, +// ), +// ), +// const SizedBox(height: 20), +// Padding( +// padding: const EdgeInsets.only(top: 30.0), +// child: Row( +// children: [ +// Expanded( +// child: TextButton( +// style: TextButton.styleFrom( +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(5.0), +// side: const BorderSide(color: Colors.white), +// ), +// primary: ColorPrimary, +// textStyle: const TextStyle( +// color: lightButtonTextColor, +// )), +// child: const Text( +// 'Log in', +// style: TextStyle( +// color: Colors.white, +// decoration: TextDecoration.underline, +// ), +// ), +// onPressed: _userDataProvider.isLoading! +// ? null +// : () { +// _userDataProvider +// .manualLogin( +// _emailTextFieldController.text, +// _passwordTextFieldController.text) +// .then(( isLoggedIn) async { +// if (isLoggedIn) { +// Navigator.pushNamedAndRemoveUntil(context, +// RoutePaths.Home, (_) => false); +// } else { +// showAlertDialog(context); +// } +// }); +// }, + +// // ), +// )) +// ], +// ), +// ), +// ])))); +// } + +// // Toggles the password show status +// void _toggle() { +// setState(() { +// _passwordObscured = !_passwordObscured; +// }); +// } + +// showAlertDialog(BuildContext context) { +// // set up the button +// final Widget okButton = TextButton( +// style: TextButton.styleFrom( +// primary: Theme.of(context).buttonColor, +// ), +// child: const Text('OK'), +// onPressed: () { +// Navigator.of(context).pop(); +// }, +// ); + +// // set up the AlertDialog +// final AlertDialog alert = AlertDialog( +// title: const Text(LoginConstants.loginFailedTitle), +// content: const Text(LoginConstants.loginFailedDesc), +// actions: [ +// okButton, +// ], +// ); + +// // show the dialog +// showDialog( +// context: context, +// builder: (BuildContext context) { +// return alert; +// }, +// ); +// } +// } diff --git a/lib/ui/navigator/bottom.dart b/lib/ui/navigator/bottom.dart index 7d73716..1e37bfe 100644 --- a/lib/ui/navigator/bottom.dart +++ b/lib/ui/navigator/bottom.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:webreg_mobile_flutter/core/providers/user.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/ui/list/course_list_view.dart'; @@ -10,11 +12,13 @@ class BottomNavigation extends StatefulWidget { _BottomNavigationState createState() => _BottomNavigationState(); } -class _BottomNavigationState extends State with SingleTickerProviderStateMixin { +class _BottomNavigationState extends State + with SingleTickerProviderStateMixin { + UserDataProvider userDataProvider = UserDataProvider(); var currentTab = [ - Calendar(Colors.yellow), + Calendar(Colors.blue.shade200), CourseListView(), - Calendar(Colors.green), + Calendar(Colors.green.shade200), ]; int currentIndex = 0; @@ -25,54 +29,61 @@ class _BottomNavigationState extends State with SingleTickerPr color: ColorPrimary, fontSize: 16, ); + // @override + // void didChangeDependencies() { + // userDataProvider = Provider.of(context); + // } @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 currentUrl = Uri.base; + final fragments = currentUrl.fragment.split('&'); + _token = fragments + .firstWhere((e) => e.startsWith('access_token=')) + .substring('access_token='.length); + userDataProvider.setToken = _token; - ] - ) + 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), + ])), + label: '', + ), + BottomNavigationBarItem( + icon: Text("List", style: textStyles), + activeIcon: Text("List", 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, - ) - ); + 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..e3fedc1 100644 --- a/lib/ui/search/search_bar.dart +++ b/lib/ui/search/search_bar.dart @@ -1,6 +1,8 @@ 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 { final VoidCallback setOpenFilters; @@ -10,43 +12,40 @@ class SearchBar extends StatelessWidget { @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: 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, + ), + ])); } } @@ -65,7 +64,7 @@ class SearchBar extends StatelessWidget { // List _selectedFilters = List.filled(3, false); // AnimationController expandController; -// Animation animation; +// Animation animation; // void prepareAnimations() { // expandController = AnimationController( @@ -96,7 +95,7 @@ class SearchBar extends StatelessWidget { // // builder: (context) => Wrap( // // children: [ // // Container( -// // color: ColorPrimary, +// // color: ColorPrimary, // // height: 100, // // child: ListView( // // children: [ @@ -168,22 +167,23 @@ class _TermDropdownState extends State { @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( + 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(), ); } } @@ -195,61 +195,58 @@ class Search extends StatefulWidget { 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(); + 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) { + provider.searchBarController.text = text; + if (text.length > 0) { + _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..4451c9d --- /dev/null +++ b/lib/ui/search/search_detail.dart @@ -0,0 +1,329 @@ +// ignore_for_file: always_specify_types + +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 { + const SearchDetail({Key? key, required this.data}) : super(key: key); + final CourseData data; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + '${data.departmentCode} ${data.courseCode} \n${data.courseTitle}')), + body: Column( + children: [coursePrereqs(), courseDetails()], + )); + + 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, + ), + )))); + } + + Widget courseDetails() { + // Determine types of sections + final List sectionTypes = []; + final List sectionCards = []; + final List sectionObjects = []; + for (final SectionData section in data.sections!) { + if (section.instructionType != 'LE' || !sectionTypes.contains('LE')) { + sectionTypes.add(section.instructionType!); + sectionObjects.add(section); + } + } + sectionTypes.add('FI'); + sectionObjects.add(SectionData()); + + //Build section cards for different instruction types + 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 = []; + for (final SectionData section in data.sections!) { + 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 + 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 + 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 + 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 + 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; + } +} diff --git a/lib/ui/search/search_filters.dart b/lib/ui/search/search_filters.dart index 8ef7d4b..72e1a99 100644 --- a/lib/ui/search/search_filters.dart +++ b/lib/ui/search/search_filters.dart @@ -1,6 +1,4 @@ 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 { @override @@ -11,7 +9,7 @@ class SearchFiltersState extends State { @override Widget build(BuildContext context) { return ExpansionTile( - // title: , + title: Text('tmp'), ); } -} \ No newline at end of file +} diff --git a/lib/ui/search/search_view.dart b/lib/ui/search/search_view.dart index 1d980a2..5716539 100644 --- a/lib/ui/search/search_view.dart +++ b/lib/ui/search/search_view.dart @@ -1,104 +1,186 @@ +// ignore_for_file: use_key_in_widget_constructors, always_specify_types + import 'package:flutter/material.dart'; -import 'package:webreg_mobile_flutter/ui/search/search_bar.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/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; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + classesProvider = ScheduleOfClassesProvider(); - void setOpenFilters() { - this.setState(() { - openFilters = !openFilters; - }); } @override - Widget build(BuildContext context) { + Widget build(BuildContext context) { + const Widget _icon = Icon(Icons.search, size: 20, color: darkGray); 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: [ + TermDropdown(), + 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, + ), ), - ] - ) - ); - } + ), + Container( + margin: const EdgeInsets.only(right: 10.0), + child: _icon, + ), + ], + )), + ), + 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)); + } + + Widget body(bool showList) { + if (showList) { + return FutureBuilder( + future: classesProvider.fetchClasses( + 'subjectCodes=${searchString.split(' ')[0]}&termCode=SP22'), + builder: (BuildContext context, AsyncSnapshot response) { + if (response.hasData) { + return buildResultsList(context); + } else { + return const CircularProgressIndicator(); + } + }, + ); + } else { + return const Center( + child: Text( + 'Search by course code\ne.g. ANTH 23', + style: TextStyle(color: darkGray, fontSize: 18), + textAlign: TextAlign.center, + )); + } + } + + 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!; + 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, ), - // 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(), - ], - ), + 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(course.departmentCode! + ' ' + course.courseCode!, + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)) + ], + ), + Text(course.courseTitle!) + ], + )) + ]), + onTap: () { + 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..36c5e7e 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.0" + 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.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" 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,16 +111,72 @@ 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 source: sdk version: "0.0.0" flutter_web_plugins: - dependency: transitive + dependency: "direct main" 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.0.5" 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.5.0" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.2" 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..d9b6063 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,12 +3,22 @@ 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: + flutter_web_plugins: + sdk: flutter 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/scripts/codemagic-ci/build-notifier.js b/scripts/codemagic-ci/build-notifier.js index 8323f4b..6973e5f 100644 --- a/scripts/codemagic-ci/build-notifier.js +++ b/scripts/codemagic-ci/build-notifier.js @@ -90,9 +90,9 @@ const buildNotify = async () => { } else if (ENV_VARS.buildPlatform === 'WEB') { if (saveArtifactWebSuccess) { if (ENV_VARS.buildEnv === 'PROD') { - teamsMessage += 'URL:/webreg-mobile/' + ENV_VARS.appVersion + '-' + finalBuildNumber + '/index.html' + teamsMessage += 'URL:/webreg-mobile/index.html' } else if (ENV_VARS.buildEnv === 'QA') { - teamsMessage += 'URL:/webreg-mobile/' + ENV_VARS.appVersion + '-' + finalBuildNumber + '/index.html' + teamsMessage += 'URL:/webreg-mobile/index.html' } teamsMessage += 'Archive:' + buildArtifacts.buildWebFinalFilename + '' } else { diff --git a/scripts/codemagic-ci/post-publish.sh b/scripts/codemagic-ci/post-publish.sh index 69a7ef8..b2016ff 100644 --- a/scripts/codemagic-ci/post-publish.sh +++ b/scripts/codemagic-ci/post-publish.sh @@ -7,8 +7,12 @@ if [ "$FCI_BUILD_STEP_STATUS" == "success" ]; then echo " Moving build/web -> $APP_VERSION-$PROJECT_BUILD_NUMBER ..." cd build/ mv web "$APP_VERSION-$PROJECT_BUILD_NUMBER" + + echo " Removing old build assets from S3 ..." + aws s3 rm s3://ucsd-its-sandbox-wts-charles/webreg-mobile --recursive + echo " Syncing build assets to S3 ..." - aws s3 sync $APP_VERSION-$PROJECT_BUILD_NUMBER/ s3://ucsd-its-sandbox-wts-charles/webreg-mobile/$APP_VERSION-$PROJECT_BUILD_NUMBER + aws s3 sync $APP_VERSION-$PROJECT_BUILD_NUMBER/ s3://ucsd-its-sandbox-wts-charles/webreg-mobile elif [ "$FCI_BUILD_STEP_STATUS" == "skipped" ]; then echo "Build skipped - exiting." exit 1 diff --git a/scripts/codemagic-ci/set-env.js b/scripts/codemagic-ci/set-env.js index 01d28f5..46c2341 100644 --- a/scripts/codemagic-ci/set-env.js +++ b/scripts/codemagic-ci/set-env.js @@ -19,8 +19,6 @@ const prodEnvReplacements = async (targetEnv) => { envItem.QA.forEach((replacement, index) => { if (replacement === '"##BUILD_ENV##"') { data = data.replace(replacement, '"' + targetEnv + '"') - } else if (replacement === '') { - data = data.replace(replacement, '') } else if (replacement === 'version: 1.0.0+1') { data = data.replace(replacement, 'version: ' + appVersion + '+' + buildNumber) } else { @@ -45,8 +43,6 @@ const qaEnvReplacements = async (targetEnv) => { envItem.QA.forEach((replacement, index) => { if (replacement === '"##BUILD_ENV##"') { data = data.replace(replacement, '"' + targetEnv + '"') - } else if (replacement === '') { - data = data.replace(replacement, '') } else if (replacement === 'version: 1.0.0+1') { data = data.replace(replacement, 'version: ' + appVersion + '+' + buildNumber) } else { diff --git a/scripts/codemagic-ci/verify-env.sh b/scripts/codemagic-ci/verify-env.sh index 912b04b..3e563e5 100644 --- a/scripts/codemagic-ci/verify-env.sh +++ b/scripts/codemagic-ci/verify-env.sh @@ -8,9 +8,7 @@ if [ "$1" == "PROD" ]; then qa_dash=$(grep -rio "https.*-qa" lib | wc -l | sed -e "s/^[ \t]*//") dev_slash=$(grep -rio "https.*\/dev" lib | wc -l | sed -e "s/^[ \t]*//") dev_dash=$(grep -rio "https.*-dev" lib | wc -l | sed -e "s/^[ \t]*//") - base_href=$(grep -rio "##BASE_HREF##" web | wc -l | sed -e "s/^[ \t]*//") - invalid_count=$((qa_slash + qa_dash + dev_slash + dev_dash + base_href)) if [ "$invalid_count" -eq 0 ]; then echo "\nverify-env.sh PROD: PASS" @@ -20,15 +18,13 @@ if [ "$1" == "PROD" ]; then grep -rin "https.*-qa" lib grep -rin "https.*\/dev" lib grep -rin "https.*-dev" lib - grep -rin "##BASE_HREF##" web exit 1 fi elif [ "$1" == "QA" ]; then prod_slash=$(grep -rio "https.*\/prod" lib | wc -l | sed -e "s/^[ \t]*//") prod_dash=$(grep -rio "https.*-prod" lib | wc -l | sed -e "s/^[ \t]*//") - base_href=$(grep -rio "##BASE_HREF##" web | wc -l | sed -e "s/^[ \t]*//") - invalid_count=$((prod_slash + prod_dash + base_href)) + invalid_count=$((prod_slash + prod_dash)) if [ "$invalid_count" -eq 0 ]; then echo "\nset-env-qa: PASS" @@ -36,7 +32,6 @@ elif [ "$1" == "QA" ]; then echo "\nset-env-qa: FAIL (erors: ${invalid_count})" grep -rin "https.*\/prod" lib grep -rin "https.*-prod" lib - grep -rin "##BASE_HREF##" web exit 1 fi else diff --git a/web/index.html b/web/index.html index 01e1089..56b35bf 100644 --- a/web/index.html +++ b/web/index.html @@ -1,7 +1,7 @@ - + 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