From 45df1312d42dba46be0af405e4d2e979db19c360 Mon Sep 17 00:00:00 2001 From: jacobecontreras Date: Wed, 23 Apr 2025 18:37:00 -0700 Subject: [PATCH 1/3] Create a basic authentication system --- backend/main.py | 1 + backend/task.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/main.py b/backend/main.py index a2c1eb2..27abaf9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1174,6 +1174,7 @@ def handle_respond_to_invite() -> Response: response_json = jsonify([{"error_no": "0", "message": f"Successfully {status} invitation"}]) return response_json + if __name__ == "__main__": print("Running main.py") make_new_log("main", "Server started") # type: ignore diff --git a/backend/task.py b/backend/task.py index 94d42bb..0cd27da 100644 --- a/backend/task.py +++ b/backend/task.py @@ -53,7 +53,7 @@ def check_table() -> Connection: """This will check if the task table exists.""" try: - data_con: Connection = connect("data/data.db") + data_con: Connection = connect("../data/data.db") except Error as e_msg: raise BackendError("Backend Error: Cannot Connect to Database", 200) from e_msg From 8bf715cce25253ba589711863cc454213515c838 Mon Sep 17 00:00:00 2001 From: jacobecontreras Date: Thu, 24 Apr 2025 21:46:01 -0700 Subject: [PATCH 2/3] Finished group management page, updated all pages titles to be bold, fixed theme issues --- backend/main.py | 20 +- backend/task.py | 2 +- roomiebuddy/lib/NavScreen.dart | 4 +- roomiebuddy/lib/main.dart | 7 +- roomiebuddy/lib/pages/add_roomate_page.dart | 289 ------- roomiebuddy/lib/pages/add_taskpage.dart | 8 +- roomiebuddy/lib/pages/calendar_page.dart | 8 +- roomiebuddy/lib/pages/group_page.dart | 815 ++++++++++++++++++ roomiebuddy/lib/pages/home_page.dart | 17 +- roomiebuddy/lib/pages/login_screen.dart | 10 +- roomiebuddy/lib/pages/settings_page.dart | 24 +- roomiebuddy/lib/pages/signup_screen.dart | 8 +- roomiebuddy/lib/providers/theme_provider.dart | 16 +- roomiebuddy/lib/services/auth_storage.dart | 22 +- 14 files changed, 926 insertions(+), 324 deletions(-) delete mode 100644 roomiebuddy/lib/pages/add_roomate_page.dart create mode 100644 roomiebuddy/lib/pages/group_page.dart diff --git a/backend/main.py b/backend/main.py index 27abaf9..67cc126 100644 --- a/backend/main.py +++ b/backend/main.py @@ -234,8 +234,24 @@ def handle_login() -> Response: make_new_log("login", e) return response_json + try: + data_con = connect("data/data.db") + data_cursor = data_con.cursor() + data_cursor.execute( + "SELECT username FROM user WHERE uuid = ?;", + (user_id,), + ) + username = data_cursor.fetchone()[0] + data_con.close() + except Exception as e: + response_json = jsonify( + [{"error_no": "2", "message": "Trouble with backend! Sorry!"}] + ) + make_new_log("login", e) + return response_json + response_json = jsonify( - [{"error_no": "0", "message": "success", "user_id": user_id}] + [{"error_no": "0", "message": "success", "user_id": user_id, "username": username}] ) return response_json @@ -1061,7 +1077,7 @@ def handle_invite_to_group() -> Response: try: # Verify password before inviting - if not check_password(connect("../data/data.db"), inviter_id, password): + if not check_password(connect("data/data.db"), inviter_id, password): response_json = jsonify( [{"error_no": "4", "message": "Password is incorrect"}] ) diff --git a/backend/task.py b/backend/task.py index 0cd27da..94d42bb 100644 --- a/backend/task.py +++ b/backend/task.py @@ -53,7 +53,7 @@ def check_table() -> Connection: """This will check if the task table exists.""" try: - data_con: Connection = connect("../data/data.db") + data_con: Connection = connect("data/data.db") except Error as e_msg: raise BackendError("Backend Error: Cannot Connect to Database", 200) from e_msg diff --git a/roomiebuddy/lib/NavScreen.dart b/roomiebuddy/lib/NavScreen.dart index 73a9ebc..33da0ee 100644 --- a/roomiebuddy/lib/NavScreen.dart +++ b/roomiebuddy/lib/NavScreen.dart @@ -6,7 +6,7 @@ import 'package:roomiebuddy/providers/theme_provider.dart'; import 'pages/home_page.dart'; import 'pages/calendar_page.dart'; import 'pages/add_taskpage.dart'; -import 'pages/add_roomate_page.dart'; +import 'pages/group_page.dart'; import 'pages/settings_page.dart'; class Navscreen extends StatefulWidget { @@ -21,7 +21,7 @@ class _NavscreenState extends State { HomePage(), CalendarPage(), AddTaskpage(), - AddRoomatePage(), + GroupPage(), SettingsPage(), ]; int selectedIndex = 0; diff --git a/roomiebuddy/lib/main.dart b/roomiebuddy/lib/main.dart index 8fc9fa7..a72173b 100644 --- a/roomiebuddy/lib/main.dart +++ b/roomiebuddy/lib/main.dart @@ -17,13 +17,12 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + return MaterialApp( debugShowCheckedModeBanner: false, title: 'Roomie Buddy', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), + theme: themeProvider.themeData, home: const LoginScreen(clearCredentials: false), // Set to false to enable "remember me" functionality ); } diff --git a/roomiebuddy/lib/pages/add_roomate_page.dart b/roomiebuddy/lib/pages/add_roomate_page.dart deleted file mode 100644 index e7f60c8..0000000 --- a/roomiebuddy/lib/pages/add_roomate_page.dart +++ /dev/null @@ -1,289 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; -import 'package:roomiebuddy/providers/theme_provider.dart'; - -class AddRoomatePage extends StatefulWidget { - const AddRoomatePage({super.key}); - - @override - State createState() => _AddRoomatePageState(); -} - -class _AddRoomatePageState extends State { - - String nickName = "Naruto"; - String userID = "235454"; - String groupName = "House of Buddies"; - String groupID = "103958"; - - bool hasRequests = true; - - @override - Widget build(BuildContext context) { - final themeProvider = Provider.of(context); - final TextEditingController invitedUser = TextEditingController(); //send to back end at somepoint - - return Scaffold( - appBar: AppBar( - title: Text('Add Roommate'), - ), - body: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - - // -------------------- Profile Name & acc# -------------------- // - const Text( - 'About You', - style: TextStyle( - color: Colors.grey, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 30), - //Username - Row( - children: [ - const Text( - 'My Nickname: ', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Text( - nickName, - style: const TextStyle( - fontSize: 24, - //fontWeight: FontWeight.bold, - ), - ), - ] - ), - //User ID - Row( - children: [ - const Text( - 'User ID: ', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Text( - userID, - style: const TextStyle( - fontSize: 24, - ), - ), - const SizedBox(width: 10), - - //Copy to clipboard button (user ID) - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - onPressed: () { - Clipboard.setData(ClipboardData(text: userID)); - }, - child: Icon( - Icons.copy, - color: themeProvider.lightTextColor, - ), - ), - ] - ), - const SizedBox(height: 20,), - - // -------------------- Group Specs -------------------- // - const Text( - 'Current Group', - style: TextStyle( - color: Colors.grey, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 30), - //Group Name - Row( - children: [ - const Text( - 'Group Name: ', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Text( - groupName, - style: const TextStyle( - fontSize: 24, - //fontWeight: FontWeight.bold, - ), - ), - ] - ), - Row( - children: [ - const Text( - 'Group ID: ', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Text( - groupID, - style: const TextStyle( - fontSize: 24, - //fontWeight: FontWeight.bold, - ), - ), - - //Copy to Clipboard (group ID) - const SizedBox(width: 10), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - onPressed: () { - Clipboard.setData(ClipboardData(text: groupID)); - }, - child: Icon( - Icons.copy, - color: themeProvider.lightTextColor, - ), - ), - ] - ), - const SizedBox(height: 20), - - // -------------------- Add Roomate -------------------- // - const Text( - 'Add Roommate', - style: TextStyle( - color: Colors.grey, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: TextField( - controller: invitedUser, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: 'Rommie Buddy User ID', - border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), - ), - ), - ), - - const SizedBox(width: 10), - - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - onPressed: () {}, - child: Text('Invite', style: TextStyle(color: themeProvider.lightTextColor, fontSize: 20)), - ), - ] - ), - const SizedBox(height: 20), - - // -------------------- Join Group -------------------- // - const Text( - 'Join Group', - style: TextStyle( - color: Colors.grey, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: TextField( - controller: invitedUser, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: 'Roomie Buddy Group ID', - border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), - ), - ), - ), - - const SizedBox(width: 10), - - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - onPressed: () {}, - child: Text('Request Join', style: TextStyle(color: themeProvider.lightTextColor, fontSize: 20)), - ), - ] - ), - const SizedBox(height: 20), - - // -------------------- Pending Requests -------------------- // - const Text( - 'Pending Requests', - style: TextStyle( - color: Colors.grey, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - - const SingleChildScrollView( - - child: Column( - - children: [ - Card( - child: ListTile( - title: Text('William wants to join House of Buddies'), - - ), - ), - - Card( - child: ListTile( - title: Text('William has invited you to Doofenshmirtz Evil Incorporated'), - - ), - ), - ] - ) - - - ), - - //No task display - const SizedBox(height: 50), - Text( - hasRequests? '':'No pending join requests or invites at this time.', - ), - - ] //Children - - ) - ) - ); - } -} \ No newline at end of file diff --git a/roomiebuddy/lib/pages/add_taskpage.dart b/roomiebuddy/lib/pages/add_taskpage.dart index 8ddefbe..016e2f2 100644 --- a/roomiebuddy/lib/pages/add_taskpage.dart +++ b/roomiebuddy/lib/pages/add_taskpage.dart @@ -25,7 +25,13 @@ class _AddTaskpageState extends State { return Scaffold( appBar: AppBar( - title: Text('Add Task', style: TextStyle(color: themeProvider.lightTextColor)), + title: Text( + 'Add Task', + style: TextStyle( + color: themeProvider.currentTextColor, + fontWeight: FontWeight.bold + ), + ), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), diff --git a/roomiebuddy/lib/pages/calendar_page.dart b/roomiebuddy/lib/pages/calendar_page.dart index 5d7a97e..a88182f 100644 --- a/roomiebuddy/lib/pages/calendar_page.dart +++ b/roomiebuddy/lib/pages/calendar_page.dart @@ -31,7 +31,13 @@ class _CalendarPageState extends State { return Scaffold( appBar: AppBar( - title: const Text('Task Calendar'), + title: Text( + 'Task Calendar', + style: TextStyle( + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), ), body: Padding( padding: const EdgeInsets.all(16.0), diff --git a/roomiebuddy/lib/pages/group_page.dart b/roomiebuddy/lib/pages/group_page.dart new file mode 100644 index 0000000..f76aca3 --- /dev/null +++ b/roomiebuddy/lib/pages/group_page.dart @@ -0,0 +1,815 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:roomiebuddy/providers/theme_provider.dart'; +import 'package:roomiebuddy/services/api_service.dart'; +import 'package:roomiebuddy/services/auth_storage.dart'; +import 'dart:math' as math; + +class GroupPage extends StatefulWidget { + const GroupPage({super.key}); + + @override + State createState() => _GroupPageState(); +} + +class _GroupPageState extends State { + final _groupIdController = TextEditingController(); + final _userIdController = TextEditingController(); + final _createGroupNameController = TextEditingController(); + final _createGroupDescController = TextEditingController(); + + final ApiService _apiService = ApiService(); + final AuthStorage _authStorage = AuthStorage(); + + final Map _loadingInvites = {}; + + String _username = ""; + String _userId = ""; + String _password = ""; + List> _userGroups = []; + String? _selectedGroupId; + List> _pendingInvites = []; + String _errorMessage = ""; + bool _isLoading = true; + bool _isSubmitting = false; + + @override + void initState() { + super.initState(); + _loadUserData(); + } + + @override + void dispose() { + _groupIdController.dispose(); + _userIdController.dispose(); + _createGroupNameController.dispose(); + _createGroupDescController.dispose(); + super.dispose(); + } + + // Loads user groups and pending invites + Future _loadUserData() async { + setState(() { + _isLoading = true; + _errorMessage = ""; + }); + + try { + + // Get user credentials + final userId = await _authStorage.getUserId(); + final password = await _authStorage.getPassword(); + final username = await _authStorage.getUsername(); + + // Ensure all credentials exist + if (userId == null || password == null || username == null) { + setState(() { + _isLoading = false; + _errorMessage = "User not logged in"; + }); + return; + } + _userId = userId; + _password = password; + _username = username; + + // Get user groups + final groupsResponse = await _apiService.getGroupList(userId, password); + if (!groupsResponse['success']) { + setState(() { + _errorMessage = "Failed to load groups: ${groupsResponse['message']}"; + }); + } else { + final groups = groupsResponse['message'] as Map; + _userGroups = groups.values.map((group) => group as Map).toList(); + _selectedGroupId = null; + } + + // Get pending invites + final invitesResponse = await _apiService.getPendingInvites(userId, password); + if (!invitesResponse['success']) { + setState(() { + _errorMessage = "Failed to load invites: ${invitesResponse['message']}"; + }); + } else { + final invites = invitesResponse['message'] as Map; + _pendingInvites = invites.values.map((invite) => invite as Map).toList(); + } + } catch (e) { + setState(() { + _errorMessage = "Error: $e"; + }); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + Future _createGroup() async { + final scaffoldMessenger = ScaffoldMessenger.of(context); + + // Ensure group name is entered + if (_createGroupNameController.text.isEmpty) { + scaffoldMessenger.showSnackBar( + const SnackBar(content: Text('Please enter a group name')), + ); + return; + } + + setState(() => _isSubmitting = true); + + try { + final response = await _apiService.createGroup( + _userId, + _password, + _createGroupNameController.text.trim(), + _createGroupDescController.text.trim(), + ); + + if (response['success']) { + + // Clear the form + _createGroupNameController.clear(); + _createGroupDescController.clear(); + + // Let the user know group was created successfully + if (!mounted) return; + scaffoldMessenger.showSnackBar( + const SnackBar(content: Text('Group created successfully')), + ); + + // Refresh the group list + try { + final groupsResponse = await _apiService.getGroupList(_userId, _password); + if (groupsResponse['success']) { + final groups = groupsResponse['message'] as Map; + if(mounted){ + setState(() { + _userGroups = groups.values.map((group) => group as Map).toList(); + }); + } + } + } catch (e) { + // Silently handle error + } + } else { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Failed to create group: ${response['message']}')), + ); + } + } catch (e) { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } finally { + if(mounted){ + setState(() => _isSubmitting = false); + } + } + } + + Future _inviteUser() async { + final scaffoldMessenger = ScaffoldMessenger.of(context); + + // Ensure user ID and group are entered + if (_userIdController.text.isEmpty || _selectedGroupId == null) { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + const SnackBar(content: Text('Please enter a user ID and select a group')), + ); + return; + } + + setState(() => _isSubmitting = true); + + try { + final response = await _apiService.inviteToGroup( + _userId, + _userIdController.text.trim(), + _selectedGroupId!, + _password, + ); + + if (response['success']) { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + const SnackBar(content: Text('Invitation sent successfully')), + ); + _userIdController.clear(); + } else { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Failed to send invitation: ${response['message']}')), + ); + } + } catch (e) { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } finally { + if(mounted){ + setState(() => _isSubmitting = false); + } + } + } + + Future _respondToInvite(String inviteId, String status) async { + setState(() { + _loadingInvites[inviteId] = true; + }); + + try { + final response = await _apiService.respondToInvite( + _userId, + inviteId, + status, + _password, + ); + + if (response['success']) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Successfully $status invitation')), + ); + + // Update local state + setState(() { + // Remove the invite locally + _pendingInvites.removeWhere((invite) => invite['invite_id'] == inviteId); + + // If accepted, refresh the groups list + if (status == 'accepted') { + // Get updated groups + _refreshGroupsList(); + } + }); + } else { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to $status invitation: ${response['message']}')), + ); + } + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } finally { + // Clear loading state for this invite + setState(() { + _loadingInvites.remove(inviteId); + }); + } + } + + Future _refreshGroupsList() async { + try { + final groupsResponse = await _apiService.getGroupList(_userId, _password); + if (groupsResponse['success']) { + final groups = groupsResponse['message'] as Map; + setState(() { + _userGroups = groups.values.map((group) => group as Map).toList(); + }); + } + } catch (e) { + // Silently handle error + } + } + + @override + Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + + if (_isLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + if (_errorMessage.isNotEmpty) { + return Scaffold( + appBar: AppBar(title: const Text('Group Management')), + body: Center(child: Text(_errorMessage)), + ); + } + + return Scaffold( + appBar: AppBar( + title: Text( + 'Group Management', + style: TextStyle( + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // -------------------- Profile Info -------------------- // + Text( + 'Profile Info', + style: TextStyle( + color: themeProvider.currentTextColor, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + + // Username + Row( + children: [ + Text( + 'Username: ', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), + Expanded( + child: Text( + _username, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + color: themeProvider.currentTextColor, + ), + ), + ), + ], + ), + + // User ID + Row( + children: [ + Text( + 'User ID: ', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), + Expanded( + child: Text( + _userId.length > 12 ? "${_userId.substring(0, 11)}..." : _userId, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + color: themeProvider.currentTextColor, + ), + ), + ), + const SizedBox(width: 8), + + // Copy to clipboard button (user ID) + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: themeProvider.themeColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.all(8), + ), + onPressed: () { + Clipboard.setData(ClipboardData(text: _userId)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('User ID copied to clipboard')), + ); + }, + child: Icon( + Icons.copy, + color: themeProvider.currentTextColor, + size: 20, + ), + ), + ], + ), + const SizedBox(height: 16), + + // -------------------- Create Group -------------------- // + Text( + 'Create Group', + style: TextStyle( + color: themeProvider.currentTextColor, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: _createGroupNameController, + decoration: InputDecoration( + hintText: 'Enter group name', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: themeProvider.themeColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + onPressed: _isSubmitting ? null : _createGroup, + child: _isSubmitting + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: themeProvider.currentTextColor, + ), + ) + : Text( + 'Create', + style: TextStyle(color: themeProvider.currentTextColor), + ), + ), + ], + ), + const SizedBox(height: 32), + + // -------------------- Invite Roommate -------------------- // + Text( + 'Invite Roommate', + style: TextStyle( + color: themeProvider.currentTextColor, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + + if (_userGroups.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: DropdownButtonFormField( + decoration: InputDecoration( + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + ), + value: _selectedGroupId, + hint: const Text('Select Group'), + items: _userGroups.map((group) => + DropdownMenuItem( + value: group['group_id'], + child: Text(group['name'], overflow: TextOverflow.ellipsis), + ) + ).toList(), + onChanged: (value) { + setState(() { + _selectedGroupId = value; + }); + }, + ), + ), + + Row( + children: [ + Expanded( + child: TextField( + controller: _userIdController, + decoration: InputDecoration( + hintText: 'Enter user ID to invite', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: themeProvider.themeColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + onPressed: _isSubmitting ? null : _inviteUser, + child: _isSubmitting + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: themeProvider.currentTextColor, + ), + ) + : Text( + 'Invite', + style: TextStyle(color: themeProvider.currentTextColor), + ), + ), + ], + ), + ], + ) + else + const Text('You must be in a group to invite a roommate.'), + + const SizedBox(height: 32), + + // -------------------- Pending Invites -------------------- // + Text( + 'Pending Invites', + style: TextStyle( + color: themeProvider.currentTextColor, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + + if (_pendingInvites.isNotEmpty) + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _pendingInvites.length, + itemBuilder: (context, index) { + final invite = _pendingInvites[index]; + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: Stack( + children: [ + // Main content + Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Group Name + Text( + invite['group_name'], + style: TextStyle( + fontSize: 18, + color: themeProvider.currentTextColor, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + + // Inviter name + Text( + '${invite['inviter_name']} invited you', + style: TextStyle( + fontSize: 14, + fontStyle: FontStyle.italic, + color: themeProvider.currentSecondaryTextColor, + ), + ), + const SizedBox(height: 8), + + + const SizedBox(height: 4), + ], + ), + ), + + // Action buttons for accepting/declining + Positioned( + bottom: 8, + right: 8, + child: _loadingInvites[invite['invite_id']] == true + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + color: themeProvider.themeColor, + ), + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon(Icons.check_circle_outline, color: themeProvider.successColor), + onPressed: () => _respondToInvite(invite['invite_id'], 'accepted'), + tooltip: 'Accept', + ), + IconButton( + icon: Icon(Icons.cancel_outlined, color: themeProvider.errorColor), + onPressed: () => _respondToInvite(invite['invite_id'], 'rejected'), + tooltip: 'Decline', + ), + ], + ), + ), + ], + ), + ); + }, + ) + else + const Text('No pending invites at this time'), + + const SizedBox(height: 32), + + // -------------------- Current Groups -------------------- // + Text( + 'Current Groups', + style: TextStyle( + color: themeProvider.currentTextColor, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + + if (_userGroups.isNotEmpty) + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _userGroups.length, + itemBuilder: (context, index) { + final group = _userGroups[index]; + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: Stack( + children: [ + // Main content + Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Group Name + Text( + group['name'], + style: TextStyle( + fontSize: 18, + color: themeProvider.currentTextColor, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + + // Members Section + Text( + 'Members (${group['members'].length})', + style: TextStyle( + fontSize: 14, + fontStyle: FontStyle.italic, + color: themeProvider.currentSecondaryTextColor, + ), + ), + const SizedBox(height: 16), + + // Member list + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (var i = 0; i < math.min(5, group['members'].length); i++) + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: themeProvider.themeColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + group['members'][i]['username'], + style: TextStyle( + fontSize: 14, + color: themeProvider.currentTextColor, + ), + ), + ), + + if (group['members'].length > 5) + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: themeProvider.currentSecondaryTextColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '+${group['members'].length - 5} more', + style: TextStyle( + fontSize: 14, + fontStyle: FontStyle.italic, + color: themeProvider.currentTextColor, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + ], + ), + ), + + // Leave group button + Positioned( + bottom: 8, + right: 8, + child: IconButton( + icon: Icon( + Icons.exit_to_app, + color: themeProvider.errorColor, + size: 24, + ), + tooltip: 'Leave Group', + onPressed: _isSubmitting ? null : () async { + final scaffoldMessenger = ScaffoldMessenger.of(context); + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Leave Group'), + content: const Text('Are you sure you want to leave this group?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Leave'), + ), + ], + ), + ); + + if (confirm == true) { + setState(() => _isSubmitting = true); + try { + final response = await _apiService.leaveGroup( + _userId, + _password, + group['group_id'], + ); + + if (response['success']) { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + const SnackBar(content: Text('Left group successfully')), + ); + setState(() { + _userGroups.removeWhere((g) => g['group_id'] == group['group_id']); + if (_selectedGroupId == group['group_id']) { + _selectedGroupId = null; + } + }); + } else { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Failed to leave group: ${response['message']}')), + ); + } + } catch (e) { + if (!mounted) return; + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } finally { + if(mounted){ + setState(() => _isSubmitting = false); + } + } + } + }, + ), + ), + + if (_isSubmitting) + Positioned.fill( + child: Container( + color: themeProvider.currentCardBackground.withAlpha(191), + child: Center( + child: CircularProgressIndicator( + color: themeProvider.themeColor, + ), + ), + ), + ), + ], + ), + ); + }, + ) + else + const Text('You are not currently in any groups'), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/roomiebuddy/lib/pages/home_page.dart b/roomiebuddy/lib/pages/home_page.dart index 4973869..ccf1c7a 100644 --- a/roomiebuddy/lib/pages/home_page.dart +++ b/roomiebuddy/lib/pages/home_page.dart @@ -165,7 +165,7 @@ class HomePageState extends State { style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, - color: themeProvider.lightTextColor, + color: themeProvider.currentTextColor, ), ), ), @@ -177,11 +177,16 @@ class HomePageState extends State { ], ), ), - const Padding( - padding: EdgeInsets.all(16.0), - child: Text('My Tasks', - style: - TextStyle(fontSize: 25, fontWeight: FontWeight.bold)), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'My Tasks', + style: TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), ), _isLoading ? const Center(child: CircularProgressIndicator()) diff --git a/roomiebuddy/lib/pages/login_screen.dart b/roomiebuddy/lib/pages/login_screen.dart index 1fdcbf4..d5033d5 100644 --- a/roomiebuddy/lib/pages/login_screen.dart +++ b/roomiebuddy/lib/pages/login_screen.dart @@ -112,12 +112,14 @@ class _LoginScreenState extends State { if (result['success']) { // Extract user ID from the response final String userId = result['data']['user_id']; + final String username = result['data']['username']; // Store credentials for future use await _authStorage.storeUserCredentials( userId, emailController.text.trim(), passwordController.text.trim(), + username, ); // Navigate to main screen @@ -159,7 +161,13 @@ class _LoginScreenState extends State { return Scaffold( appBar: AppBar( - title: const Text('Roomie Buddy'), + title: Text( + 'Roomie Buddy', + style: TextStyle( + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), ), body: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/roomiebuddy/lib/pages/settings_page.dart b/roomiebuddy/lib/pages/settings_page.dart index 94adb30..f95fe98 100644 --- a/roomiebuddy/lib/pages/settings_page.dart +++ b/roomiebuddy/lib/pages/settings_page.dart @@ -63,7 +63,13 @@ class _SettingsPageState extends State { return Scaffold( appBar: AppBar( - title: const Text('Settings'), + title: Text( + 'Settings', + style: TextStyle( + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), ), body: Padding( padding: const EdgeInsets.all(16.0), @@ -71,11 +77,12 @@ class _SettingsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Appearance Section - const Text( + Text( 'Appearance', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, ), ), const SizedBox(height: 12), @@ -83,7 +90,10 @@ class _SettingsPageState extends State { // Dark Mode Toggle Card( child: ListTile( - title: const Text('Dark Mode'), + title: Text( + 'Dark Mode', + style: TextStyle(color: themeProvider.currentTextColor), + ), trailing: Container( padding: const EdgeInsets.only(right: 0), width: 50, @@ -106,7 +116,10 @@ class _SettingsPageState extends State { // Theme Color Card( child: ListTile( - title: const Text('Theme Color'), + title: Text( + 'Theme Color', + style: TextStyle(color: themeProvider.currentTextColor), + ), trailing: Container( width: 50, height: 30, @@ -124,11 +137,12 @@ class _SettingsPageState extends State { // Account Section const SizedBox(height: 32), - const Text( + Text( 'Account', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, ), ), const SizedBox(height: 12), diff --git a/roomiebuddy/lib/pages/signup_screen.dart b/roomiebuddy/lib/pages/signup_screen.dart index 17cc106..06ae098 100644 --- a/roomiebuddy/lib/pages/signup_screen.dart +++ b/roomiebuddy/lib/pages/signup_screen.dart @@ -31,7 +31,13 @@ class _SignupScreenState extends State { return Scaffold( appBar: AppBar( - title: const Text('Roomie Buddy'), + title: Text( + 'Roomie Buddy', + style: TextStyle( + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), ), body: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/roomiebuddy/lib/providers/theme_provider.dart b/roomiebuddy/lib/providers/theme_provider.dart index f869aa0..d79c06c 100644 --- a/roomiebuddy/lib/providers/theme_provider.dart +++ b/roomiebuddy/lib/providers/theme_provider.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; class ThemeProvider extends ChangeNotifier { - bool _isDarkMode = false; // GLOBAL DARK MODE FLAG - Color _themeColor = Colors.greenAccent; // GLOBAL THEME COLOR + bool _isDarkMode = true; // GLOBAL DARK MODE FLAG + Color _themeColor = Colors.blueGrey; // GLOBAL THEME COLOR // Utility colors (Theme independent) Color get errorColor => Colors.red; @@ -73,19 +73,19 @@ class ThemeProvider extends ChangeNotifier { canvasColor: backgroundColor, appBarTheme: AppBarTheme( backgroundColor: appBarBgColor, - titleTextStyle: TextStyle(color: lightTextColor, fontSize: 20), - iconTheme: IconThemeData(color: lightTextColor), + titleTextStyle: TextStyle(color: textColor, fontSize: 20), + iconTheme: IconThemeData(color: textColor), ), bottomNavigationBarTheme: BottomNavigationBarThemeData( selectedItemColor: _themeColor, - unselectedItemColor: lightTextColor, - selectedLabelStyle: TextStyle(color: lightTextColor), - unselectedLabelStyle: TextStyle(color: lightTextColor), + unselectedItemColor: textColor, + selectedLabelStyle: TextStyle(color: textColor), + unselectedLabelStyle: TextStyle(color: textColor), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: _themeColor, - foregroundColor: lightTextColor, + foregroundColor: textColor, ), ), cardTheme: CardTheme( diff --git a/roomiebuddy/lib/services/auth_storage.dart b/roomiebuddy/lib/services/auth_storage.dart index 292369a..56273e5 100644 --- a/roomiebuddy/lib/services/auth_storage.dart +++ b/roomiebuddy/lib/services/auth_storage.dart @@ -5,6 +5,7 @@ class AuthStorage { static const String _userIdKey = 'user_id'; static const String _emailKey = 'email'; static const String _passwordKey = 'password'; + static const String _usernameKey = 'username'; // Singleton pattern implementation static final AuthStorage _instance = AuthStorage._internal(); @@ -16,12 +17,13 @@ class AuthStorage { AuthStorage._internal(); // Store user credentials - Future storeUserCredentials(String userId, String email, String password) async { + Future storeUserCredentials(String userId, String email, String password, String username) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_userIdKey, userId); await prefs.setString(_emailKey, email); await prefs.setString(_passwordKey, password); + await prefs.setString(_usernameKey, username); return true; } catch (e) { print('Error storing user credentials: $e'); @@ -62,6 +64,17 @@ class AuthStorage { } } + // Get username + Future getUsername() async { + try { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_usernameKey); + } catch (e) { + print('Error getting username: $e'); + return null; + } + } + // Check if user is logged in Future isLoggedIn() async { try { @@ -71,14 +84,16 @@ class AuthStorage { final userId = prefs.getString(_userIdKey); final email = prefs.getString(_emailKey); final password = prefs.getString(_passwordKey); + final username = prefs.getString(_usernameKey); // For development/debugging - remove or set to false in production - print("Auth check: UserId=$userId, Email=$email"); + print("Auth check: UserId=$userId, Email=$email, Username=$username"); // Ensure all values exist and aren't empty return userId != null && userId.isNotEmpty && email != null && email.isNotEmpty && - password != null && password.isNotEmpty; + password != null && password.isNotEmpty && + username != null && username.isNotEmpty; } catch (e) { print('Error checking login status: $e'); return false; @@ -92,6 +107,7 @@ class AuthStorage { await prefs.remove(_userIdKey); await prefs.remove(_emailKey); await prefs.remove(_passwordKey); + await prefs.remove(_usernameKey); return true; } catch (e) { print('Error clearing user credentials: $e'); From c94a770066c5a4ee56fae6f8df80279b2323da55 Mon Sep 17 00:00:00 2001 From: jacobecontreras Date: Fri, 25 Apr 2025 04:02:26 -0700 Subject: [PATCH 3/3] Removed profile/group section from group page, moved profile section to settings page --- backend/main.py | 2 +- roomiebuddy/lib/pages/group_page.dart | 516 +++++------------- roomiebuddy/lib/pages/settings_page.dart | 249 +++++++-- roomiebuddy/lib/providers/theme_provider.dart | 8 +- 4 files changed, 348 insertions(+), 427 deletions(-) diff --git a/backend/main.py b/backend/main.py index 67cc126..15b3cc5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -16,7 +16,7 @@ edit_task, delete_task, get_user_task, - get_group + get_group, create_group, leave_group, delete_group, diff --git a/roomiebuddy/lib/pages/group_page.dart b/roomiebuddy/lib/pages/group_page.dart index f76aca3..228b7a8 100644 --- a/roomiebuddy/lib/pages/group_page.dart +++ b/roomiebuddy/lib/pages/group_page.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:roomiebuddy/providers/theme_provider.dart'; import 'package:roomiebuddy/services/api_service.dart'; import 'package:roomiebuddy/services/auth_storage.dart'; -import 'dart:math' as math; class GroupPage extends StatefulWidget { const GroupPage({super.key}); @@ -14,17 +12,14 @@ class GroupPage extends StatefulWidget { } class _GroupPageState extends State { - final _groupIdController = TextEditingController(); final _userIdController = TextEditingController(); final _createGroupNameController = TextEditingController(); - final _createGroupDescController = TextEditingController(); final ApiService _apiService = ApiService(); final AuthStorage _authStorage = AuthStorage(); final Map _loadingInvites = {}; - String _username = ""; String _userId = ""; String _password = ""; List> _userGroups = []; @@ -42,14 +37,11 @@ class _GroupPageState extends State { @override void dispose() { - _groupIdController.dispose(); _userIdController.dispose(); _createGroupNameController.dispose(); - _createGroupDescController.dispose(); super.dispose(); } - // Loads user groups and pending invites Future _loadUserData() async { setState(() { _isLoading = true; @@ -57,14 +49,12 @@ class _GroupPageState extends State { }); try { - // Get user credentials final userId = await _authStorage.getUserId(); final password = await _authStorage.getPassword(); - final username = await _authStorage.getUsername(); - // Ensure all credentials exist - if (userId == null || password == null || username == null) { + // Ensure necessary credentials exist + if (userId == null || password == null) { setState(() { _isLoading = false; _errorMessage = "User not logged in"; @@ -73,7 +63,6 @@ class _GroupPageState extends State { } _userId = userId; _password = password; - _username = username; // Get user groups final groupsResponse = await _apiService.getGroupList(userId, password); @@ -126,14 +115,13 @@ class _GroupPageState extends State { _userId, _password, _createGroupNameController.text.trim(), - _createGroupDescController.text.trim(), + '', ); if (response['success']) { // Clear the form _createGroupNameController.clear(); - _createGroupDescController.clear(); // Let the user know group was created successfully if (!mounted) return; @@ -315,97 +303,16 @@ class _GroupPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // -------------------- Profile Info -------------------- // - Text( - 'Profile Info', - style: TextStyle( - color: themeProvider.currentTextColor, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - - // Username - Row( - children: [ - Text( - 'Username: ', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: themeProvider.currentTextColor, - ), - ), - Expanded( - child: Text( - _username, - style: TextStyle( - fontSize: 16, - overflow: TextOverflow.ellipsis, - color: themeProvider.currentTextColor, - ), - ), - ), - ], - ), - - // User ID - Row( - children: [ - Text( - 'User ID: ', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: themeProvider.currentTextColor, - ), - ), - Expanded( - child: Text( - _userId.length > 12 ? "${_userId.substring(0, 11)}..." : _userId, - style: TextStyle( - fontSize: 16, - overflow: TextOverflow.ellipsis, - color: themeProvider.currentTextColor, - ), - ), - ), - const SizedBox(width: 8), - - // Copy to clipboard button (user ID) - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - padding: const EdgeInsets.all(8), - ), - onPressed: () { - Clipboard.setData(ClipboardData(text: _userId)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('User ID copied to clipboard')), - ); - }, - child: Icon( - Icons.copy, - color: themeProvider.currentTextColor, - size: 20, - ), - ), - ], - ), - const SizedBox(height: 16), - // -------------------- Create Group -------------------- // Text( 'Create Group', style: TextStyle( color: themeProvider.currentTextColor, - fontSize: 20, + fontSize: 18, fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 8), + const SizedBox(height: 4), Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -421,41 +328,44 @@ class _GroupPageState extends State { ), ), const SizedBox(width: 8), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - onPressed: _isSubmitting ? null : _createGroup, - child: _isSubmitting - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: themeProvider.currentTextColor, + SizedBox( + width: 80, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: themeProvider.themeColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + onPressed: _isSubmitting ? null : _createGroup, + child: _isSubmitting + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: themeProvider.currentTextColor, + ), + ) + : Text( + 'Create', + style: TextStyle(color: themeProvider.currentTextColor), ), - ) - : Text( - 'Create', - style: TextStyle(color: themeProvider.currentTextColor), - ), + ), ), ], ), - const SizedBox(height: 32), + const SizedBox(height: 16), // -------------------- Invite Roommate -------------------- // Text( 'Invite Roommate', style: TextStyle( color: themeProvider.currentTextColor, - fontSize: 20, + fontSize: 18, fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 8), + const SizedBox(height: 4), if (_userGroups.isNotEmpty) Column( @@ -464,10 +374,15 @@ class _GroupPageState extends State { Padding( padding: const EdgeInsets.only(bottom: 8.0), child: DropdownButtonFormField( + isExpanded: true, decoration: InputDecoration( border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + filled: true, + fillColor: themeProvider.currentInputFill, ), + dropdownColor: themeProvider.currentInputFill, + borderRadius: BorderRadius.circular(8), value: _selectedGroupId, hint: const Text('Select Group'), items: _userGroups.map((group) => @@ -490,33 +405,36 @@ class _GroupPageState extends State { child: TextField( controller: _userIdController, decoration: InputDecoration( - hintText: 'Enter user ID to invite', + hintText: 'Enter user ID', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), ), ), ), const SizedBox(width: 8), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - onPressed: _isSubmitting ? null : _inviteUser, - child: _isSubmitting - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: themeProvider.currentTextColor, + SizedBox( + width: 80, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: themeProvider.themeColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + onPressed: _isSubmitting ? null : _inviteUser, + child: _isSubmitting + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: themeProvider.currentTextColor, + ), + ) + : Text( + 'Invite', + style: TextStyle(color: themeProvider.currentTextColor), ), - ) - : Text( - 'Invite', - style: TextStyle(color: themeProvider.currentTextColor), - ), + ), ), ], ), @@ -525,74 +443,69 @@ class _GroupPageState extends State { else const Text('You must be in a group to invite a roommate.'), - const SizedBox(height: 32), + const SizedBox(height: 16), // -------------------- Pending Invites -------------------- // Text( 'Pending Invites', style: TextStyle( color: themeProvider.currentTextColor, - fontSize: 20, + fontSize: 18, fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 8), - - if (_pendingInvites.isNotEmpty) - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _pendingInvites.length, - itemBuilder: (context, index) { - final invite = _pendingInvites[index]; - return Card( - margin: const EdgeInsets.only(bottom: 12), - child: Stack( - children: [ - // Main content - Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Group Name - Text( - invite['group_name'], - style: TextStyle( - fontSize: 18, - color: themeProvider.currentTextColor, - ), - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - - // Inviter name - Text( - '${invite['inviter_name']} invited you', - style: TextStyle( - fontSize: 14, - fontStyle: FontStyle.italic, - color: themeProvider.currentSecondaryTextColor, - ), + const SizedBox(height: 4), + + Container( + height: 425, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: themeProvider.currentInputFill, + borderRadius: BorderRadius.circular(8), + ), + child: _pendingInvites.isNotEmpty + ? ListView.builder( + itemCount: _pendingInvites.length, + itemBuilder: (context, index) { + final invite = _pendingInvites[index]; + final isLoadingInvite = _loadingInvites[invite['invite_id']] == true; + + return Card( + color: themeProvider.currentBackground, + margin: const EdgeInsets.only(bottom: 12), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + // Group Name + title: Text( + invite['group_name'], + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: themeProvider.currentTextColor, + ), + overflow: TextOverflow.ellipsis, + ), + // Inviter Name + subtitle: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + 'Invited by ${invite['inviter_name']}', + style: TextStyle( + fontSize: 14, + color: themeProvider.currentSecondaryTextColor, ), - const SizedBox(height: 8), - - - const SizedBox(height: 4), - ], + ), ), - ), - - // Action buttons for accepting/declining - Positioned( - bottom: 8, - right: 8, - child: _loadingInvites[invite['invite_id']] == true + trailing: isLoadingInvite ? SizedBox( width: 24, height: 24, child: CircularProgressIndicator( - strokeWidth: 2, + strokeWidth: 2.5, color: themeProvider.themeColor, ), ) @@ -600,212 +513,41 @@ class _GroupPageState extends State { mainAxisSize: MainAxisSize.min, children: [ IconButton( - icon: Icon(Icons.check_circle_outline, color: themeProvider.successColor), + icon: Icon(Icons.check_circle, + color: themeProvider.successColor, + size: 26, + ), onPressed: () => _respondToInvite(invite['invite_id'], 'accepted'), tooltip: 'Accept', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), ), + const SizedBox(width: 4), IconButton( - icon: Icon(Icons.cancel_outlined, color: themeProvider.errorColor), + icon: Icon(Icons.highlight_off, + color: themeProvider.errorColor, + size: 26, + ), onPressed: () => _respondToInvite(invite['invite_id'], 'rejected'), tooltip: 'Decline', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), ), ], ), ), - ], + ); + }, + ) + : Center( + child: Text( + 'No pending invites at this time', + style: TextStyle( + color: themeProvider.currentSecondaryTextColor, + ), ), - ); - }, - ) - else - const Text('No pending invites at this time'), - - const SizedBox(height: 32), - - // -------------------- Current Groups -------------------- // - Text( - 'Current Groups', - style: TextStyle( - color: themeProvider.currentTextColor, - fontSize: 20, - fontWeight: FontWeight.bold, - ), + ), ), - const SizedBox(height: 8), - - if (_userGroups.isNotEmpty) - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _userGroups.length, - itemBuilder: (context, index) { - final group = _userGroups[index]; - return Card( - margin: const EdgeInsets.only(bottom: 12), - child: Stack( - children: [ - // Main content - Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Group Name - Text( - group['name'], - style: TextStyle( - fontSize: 18, - color: themeProvider.currentTextColor, - ), - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 8), - - // Members Section - Text( - 'Members (${group['members'].length})', - style: TextStyle( - fontSize: 14, - fontStyle: FontStyle.italic, - color: themeProvider.currentSecondaryTextColor, - ), - ), - const SizedBox(height: 16), - - // Member list - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - for (var i = 0; i < math.min(5, group['members'].length); i++) - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: themeProvider.themeColor, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - group['members'][i]['username'], - style: TextStyle( - fontSize: 14, - color: themeProvider.currentTextColor, - ), - ), - ), - - if (group['members'].length > 5) - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: themeProvider.currentSecondaryTextColor, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - '+${group['members'].length - 5} more', - style: TextStyle( - fontSize: 14, - fontStyle: FontStyle.italic, - color: themeProvider.currentTextColor, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - ], - ), - ), - - // Leave group button - Positioned( - bottom: 8, - right: 8, - child: IconButton( - icon: Icon( - Icons.exit_to_app, - color: themeProvider.errorColor, - size: 24, - ), - tooltip: 'Leave Group', - onPressed: _isSubmitting ? null : () async { - final scaffoldMessenger = ScaffoldMessenger.of(context); - final confirm = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Leave Group'), - content: const Text('Are you sure you want to leave this group?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('Leave'), - ), - ], - ), - ); - - if (confirm == true) { - setState(() => _isSubmitting = true); - try { - final response = await _apiService.leaveGroup( - _userId, - _password, - group['group_id'], - ); - - if (response['success']) { - if (!mounted) return; - scaffoldMessenger.showSnackBar( - const SnackBar(content: Text('Left group successfully')), - ); - setState(() { - _userGroups.removeWhere((g) => g['group_id'] == group['group_id']); - if (_selectedGroupId == group['group_id']) { - _selectedGroupId = null; - } - }); - } else { - if (!mounted) return; - scaffoldMessenger.showSnackBar( - SnackBar(content: Text('Failed to leave group: ${response['message']}')), - ); - } - } catch (e) { - if (!mounted) return; - scaffoldMessenger.showSnackBar( - SnackBar(content: Text('Error: $e')), - ); - } finally { - if(mounted){ - setState(() => _isSubmitting = false); - } - } - } - }, - ), - ), - - if (_isSubmitting) - Positioned.fill( - child: Container( - color: themeProvider.currentCardBackground.withAlpha(191), - child: Center( - child: CircularProgressIndicator( - color: themeProvider.themeColor, - ), - ), - ), - ), - ], - ), - ); - }, - ) - else - const Text('You are not currently in any groups'), ], ), ), diff --git a/roomiebuddy/lib/pages/settings_page.dart b/roomiebuddy/lib/pages/settings_page.dart index f95fe98..82ba93c 100644 --- a/roomiebuddy/lib/pages/settings_page.dart +++ b/roomiebuddy/lib/pages/settings_page.dart @@ -3,6 +3,8 @@ import 'package:provider/provider.dart'; import 'package:roomiebuddy/providers/theme_provider.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:roomiebuddy/pages/login_screen.dart'; +import 'package:roomiebuddy/services/auth_storage.dart'; +import 'package:flutter/services.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @@ -12,6 +14,54 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { + final AuthStorage _authStorage = AuthStorage(); + String _username = ""; + String _userId = ""; + String _errorMessage = ""; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadUserData(); + } + + Future _loadUserData() async { + setState(() { + _isLoading = true; + _errorMessage = ""; + }); + + try { + final userId = await _authStorage.getUserId(); + final username = await _authStorage.getUsername(); + + if (userId == null || username == null) { + setState(() { + _isLoading = false; + _errorMessage = "User data not found"; + }); + return; + } + + if (mounted) { + setState(() { + _userId = userId; + _username = username; + _isLoading = false; + }); + } + + } catch (e) { + if (mounted) { + setState(() { + _errorMessage = "Error loading user data: $e"; + _isLoading = false; + }); + } + } + } + void _openColorPicker(BuildContext context, ThemeProvider themeProvider) { Color pickerColor = themeProvider.themeColor; @@ -19,33 +69,39 @@ class _SettingsPageState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text('Pick a theme color'), + backgroundColor: themeProvider.currentBackground, + contentPadding: const EdgeInsets.fromLTRB(20.0, 20.0, 20.0, 0.0), content: SingleChildScrollView( child: ColorPicker( pickerColor: pickerColor, onColorChanged: (Color color) { pickerColor = color; }, - pickerAreaHeightPercent: 0.8, + pickerAreaHeightPercent: 0.5, enableAlpha: false, displayThumbColor: true, - labelTypes: const [ - ColorLabelType.hex, - ColorLabelType.rgb, - ColorLabelType.hsv, - ], + labelTypes: const [], paletteType: PaletteType.hsv, ), ), + actionsPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 0.0), + actionsAlignment: MainAxisAlignment.center, actions: [ TextButton( - child: const Text('Cancel'), + child: Text( + 'Cancel', + style: TextStyle(color: themeProvider.currentTextColor), + ), onPressed: () { Navigator.of(context).pop(); }, ), + const SizedBox(width: 4), TextButton( - child: const Text('Apply'), + child: Text( + 'Apply', + style: TextStyle(color: themeProvider.themeColor), + ), onPressed: () { themeProvider.setThemeColor(pickerColor); Navigator.of(context).pop(); @@ -61,6 +117,21 @@ class _SettingsPageState extends State { Widget build(BuildContext context) { final themeProvider = Provider.of(context); + // Wait for the user data to load + if (_isLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + // If there is an error, show it + if (_errorMessage.isNotEmpty) { + return Scaffold( + appBar: AppBar(title: const Text('Settings')), + body: Center(child: Text(_errorMessage)), + ); + } + return Scaffold( appBar: AppBar( title: Text( @@ -76,7 +147,93 @@ class _SettingsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Appearance Section + // -------------------- Profile Section -------------------- // + Text( + 'Profile', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), + const SizedBox(height: 4), + Card( + color: themeProvider.currentInputFill, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: themeProvider.currentBorderColor, width: 1.0), + ), + margin: const EdgeInsets.only(bottom: 16.0), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + dense: true, + visualDensity: VisualDensity.compact, + contentPadding: EdgeInsets.zero, + leading: Text( + 'Username: ', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), + title: Text( + _username, + style: TextStyle( + fontSize: 15, + overflow: TextOverflow.ellipsis, + color: themeProvider.currentTextColor, + ), + ), + ), + ListTile( + dense: true, + visualDensity: VisualDensity.compact, + contentPadding: EdgeInsets.zero, + leading: Text( + 'User ID: ', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), + title: Text( + _userId.length > 12 ? "${_userId.substring(0, 11)}..." : _userId, + style: TextStyle( + fontSize: 15, + overflow: TextOverflow.ellipsis, + color: themeProvider.currentTextColor, + ), + ), + trailing: IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: Icon( + Icons.copy, + color: themeProvider.currentTextColor, + size: 20, + ), + tooltip: 'Copy User ID', + onPressed: () { + Clipboard.setData(ClipboardData(text: _userId)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('User ID copied to clipboard')), + ); + }, + ), + ), + ], + ), + ), + ), + + // -------------------- Appearance Section -------------------- // Text( 'Appearance', style: TextStyle( @@ -85,28 +242,33 @@ class _SettingsPageState extends State { color: themeProvider.currentTextColor, ), ), - const SizedBox(height: 12), + const SizedBox(height: 4), // Dark Mode Toggle Card( + color: themeProvider.currentInputFill, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: themeProvider.currentBorderColor, width: 1.0), + ), + margin: const EdgeInsets.only(bottom: 8.0), child: ListTile( + visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.only(left: 16.0, right: 6.0), title: Text( 'Dark Mode', - style: TextStyle(color: themeProvider.currentTextColor), + style: TextStyle(color: themeProvider.currentTextColor, fontSize: 15), ), - trailing: Container( - padding: const EdgeInsets.only(right: 0), - width: 50, - child: Switch( - value: themeProvider.isDarkMode, - onChanged: (value) { - themeProvider.toggleDarkMode(); - }, - activeColor: themeProvider.switchActiveThumb, - activeTrackColor: themeProvider.switchActiveTrack, - inactiveThumbColor: themeProvider.switchInactiveThumb, - inactiveTrackColor: themeProvider.switchInactiveTrack, - ), + trailing: Switch( + value: themeProvider.isDarkMode, + onChanged: (value) { + themeProvider.toggleDarkMode(); + }, + activeColor: themeProvider.switchActiveThumb, + activeTrackColor: themeProvider.switchActiveTrack, + inactiveThumbColor: themeProvider.switchInactiveThumb, + inactiveTrackColor: themeProvider.switchInactiveTrack, ), ), ), @@ -115,10 +277,19 @@ class _SettingsPageState extends State { // Theme Color Card( + color: themeProvider.currentInputFill, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: themeProvider.currentBorderColor, width: 1.0), + ), + margin: const EdgeInsets.only(bottom: 8.0), child: ListTile( + visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.only(left: 16.0, right: 12.0), title: Text( 'Theme Color', - style: TextStyle(color: themeProvider.currentTextColor), + style: TextStyle(color: themeProvider.currentTextColor, fontSize: 15), ), trailing: Container( width: 50, @@ -135,8 +306,8 @@ class _SettingsPageState extends State { ), ), - // Account Section - const SizedBox(height: 32), + // -------------------- Account Section -------------------- // + const SizedBox(height: 8), Text( 'Account', style: TextStyle( @@ -145,15 +316,25 @@ class _SettingsPageState extends State { color: themeProvider.currentTextColor, ), ), - const SizedBox(height: 12), + const SizedBox(height: 4), // Logout Card( + color: themeProvider.currentInputFill, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: themeProvider.currentBorderColor, width: 1.0), + ), + margin: const EdgeInsets.only(bottom: 8.0), child: ListTile( + visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.only(left: 16.0, right: 12.0), title: Text( 'Logout', style: TextStyle( - color: themeProvider.errorColor, + color: themeProvider.currentTextColor, + fontSize: 15, ), ), trailing: Icon( @@ -161,7 +342,6 @@ class _SettingsPageState extends State { color: themeProvider.errorColor, ), onTap: () { - // Show confirmation dialog showDialog( context: context, builder: (BuildContext context) { @@ -171,19 +351,18 @@ class _SettingsPageState extends State { actions: [ TextButton( onPressed: () { - Navigator.of(context).pop(); // Close dialog + Navigator.of(context).pop(); }, child: const Text('Cancel'), ), TextButton( onPressed: () { - // Close dialog Navigator.of(context).pop(); - + // Navigate to login screen Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute(builder: (context) => const LoginScreen(clearCredentials: true)), - (route) => false, // This removes all previous routes + (route) => false, ); }, child: Text( diff --git a/roomiebuddy/lib/providers/theme_provider.dart b/roomiebuddy/lib/providers/theme_provider.dart index d79c06c..b144ebc 100644 --- a/roomiebuddy/lib/providers/theme_provider.dart +++ b/roomiebuddy/lib/providers/theme_provider.dart @@ -10,12 +10,12 @@ class ThemeProvider extends ChangeNotifier { Color get successColor => Colors.green; // Global colors (Light Mode) - Color get lightBackground => Colors.white; - Color get lightCardBackground => Colors.white; + Color get lightBackground => Colors.grey[300]!; + Color get lightCardBackground => Colors.grey[50]!; Color get lightTextColor => Colors.black; Color get lightTextSecondary => Colors.grey[700]!; Color get lightBorder => Colors.grey[300]!; - Color get lightInputFill => Colors.white; + Color get lightInputFill => Colors.grey[100]!; // Global colors (Dark Mode) Color get darkBackground => Colors.grey[850]!; @@ -46,7 +46,7 @@ class ThemeProvider extends ChangeNotifier { bool get isDarkMode => _isDarkMode; Color get themeColor => _themeColor; Color get currentBackground => _isDarkMode ? darkBackground : lightBackground; - Color get currentCardBackground => _isDarkMode ? darkCardBackground : lightCardBackground; + Color get currentCardBackground => _isDarkMode ? darkCardBackground : lightBackground; Color get currentTextColor => _isDarkMode ? darkTextColor : lightTextColor; Color get currentSecondaryTextColor => _isDarkMode ? darkTextSecondary : lightTextSecondary; Color get currentBorderColor => _isDarkMode ? darkBorder : lightBorder;