From e4e8c7ea211864414d4b0685c560db9ed67cea0b Mon Sep 17 00:00:00 2001 From: Namonay Date: Thu, 15 May 2025 23:37:39 +0200 Subject: [PATCH] Added profile screen with API interaction, better .env usage --- lib/main.dart | 3 +- lib/methods/api.dart | 27 +++++- lib/views/home_screen.dart | 16 +--- lib/views/init_screen.dart | 6 +- lib/views/login_screen.dart | 39 +++++--- lib/views/profile_screen.dart | 171 +++++++++++++++++++++++++++++++--- pubspec.yaml | 1 + 7 files changed, 215 insertions(+), 48 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f372473..8d021d0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'views/home_screen.dart'; import 'views/profile_screen.dart'; import 'views/login_screen.dart'; -import 'methods/api.dart'; import 'views/init_screen.dart'; void main() { @@ -15,7 +14,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: '42 API Client', + title: 'Fluty', initialRoute: '/', onGenerateRoute: (settings) { final uri = Uri.parse(settings.name ?? '/'); diff --git a/lib/methods/api.dart b/lib/methods/api.dart index be27714..2b16df8 100644 --- a/lib/methods/api.dart +++ b/lib/methods/api.dart @@ -1,5 +1,6 @@ import 'package:shared_preferences/shared_preferences.dart'; -import 'package:http/http.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; Future checkToken() async { @@ -9,10 +10,26 @@ Future checkToken() async return date.isAfter(DateTime.now()); } -// Future> fetchProfileData(String login) async -// { -// -// } +Future> fetchProfileData(String login) async +{ + final uri = Uri.https( + 'api.intra.42.fr', + '/v2/users/$login', + ); + final token = await getToken() ?? ''; + final response = await http.get( + uri, + headers: { + 'Authorization': 'Bearer $token', + 'Accept': 'application/json', + }, + ); + if (response.statusCode == 200) { + return jsonDecode(response.body) as Map; + } + else { throw Exception('Failed to fetch user: ${response.statusCode}'); } +} + Future saveToken(String token, String refresh, DateTime expiration) async { final prefs = await SharedPreferences.getInstance(); diff --git a/lib/views/home_screen.dart b/lib/views/home_screen.dart index 3435c66..d30cf77 100644 --- a/lib/views/home_screen.dart +++ b/lib/views/home_screen.dart @@ -13,11 +13,10 @@ class _HomeScreenState extends State { void _handleInput(BuildContext context, String input) { // Replace with your desired logic - print("Input received: $input"); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('You entered: $input')), + SnackBar(content: Text('Checking profile of: $input')), ); - Navigator.pushReplacementNamed(context, '/profile/${input}'); + Navigator.pushReplacementNamed(context, '/profile/$input'); } @override Widget build(BuildContext context) { @@ -39,7 +38,7 @@ class _HomeScreenState extends State { TextField( controller: _controller, decoration: const InputDecoration( - hintText: 'Enter something...', + hintText: 'Enter login', border: OutlineInputBorder(), filled: true, fillColor: Colors.white70, @@ -50,14 +49,7 @@ class _HomeScreenState extends State { onPressed: () { _handleInput(context, _controller.text); }, - child: const Text('Submit Input'), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () { - Navigator.pushNamed(context, '/profile'); - }, - child: const Text('Go to Profile'), + child: const Text('Submit'), ), ], ), diff --git a/lib/views/init_screen.dart b/lib/views/init_screen.dart index fda373e..80b4d7a 100644 --- a/lib/views/init_screen.dart +++ b/lib/views/init_screen.dart @@ -1,9 +1,11 @@ + import 'package:flutter/material.dart'; import '../methods/api.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; Future initApp() async { + await dotenv.load(); return await checkToken(); } class InitScreen extends StatelessWidget { @@ -25,7 +27,7 @@ class InitScreen extends StatelessWidget { switch (snapshot.connectionState) { case ConnectionState.waiting: - return Center( + return const Center( child: Image( image: AssetImage("assets/images/42_logo.png"), width: 100, @@ -53,7 +55,7 @@ class InitScreen extends StatelessWidget { Future.microtask(() { Navigator.pushReplacementNamed(context, '/login'); }); - return Center( + return const Center( child: Text( "Redirect" ) diff --git a/lib/views/login_screen.dart b/lib/views/login_screen.dart index e122dde..67ce522 100644 --- a/lib/views/login_screen.dart +++ b/lib/views/login_screen.dart @@ -2,21 +2,21 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:swifty/methods/api.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; const FlutterAppAuth _appAuth = FlutterAppAuth(); Future redirect_to_oauth(BuildContext context) async { - final String _clientId = "CLIENT TODO"; - final String _clientSecret = "SECRET TODO"; - final String _redirectUrl = 'swifty-companion://oauth2/callback'; - final String _authorizationEndpoint = 'https://api.intra.42.fr/oauth/authorize'; - final String _tokenEndpoint = 'https://api.intra.42.fr/oauth/token'; + String _clientId = dotenv.get('CLIENT-ID'); + String _clientSecret = dotenv.get('CLIENT-SECRET'); + const String _redirectUrl = 'swifty-companion://oauth2/callback'; + const String _authorizationEndpoint = 'https://api.intra.42.fr/oauth/authorize'; + const String _tokenEndpoint = 'https://api.intra.42.fr/oauth/token'; final request = AuthorizationRequest( _clientId, _redirectUrl, - serviceConfiguration: AuthorizationServiceConfiguration( + serviceConfiguration: const AuthorizationServiceConfiguration( authorizationEndpoint: _authorizationEndpoint, tokenEndpoint: _tokenEndpoint, )); @@ -29,7 +29,7 @@ Future redirect_to_oauth(BuildContext context) async { clientSecret: _clientSecret, authorizationCode: result.authorizationCode, grantType: 'authorization_code', - serviceConfiguration: AuthorizationServiceConfiguration( + serviceConfiguration: const AuthorizationServiceConfiguration( authorizationEndpoint: "https://api.intra.42.fr/oauth/authorize", tokenEndpoint: _tokenEndpoint, ), @@ -53,13 +53,22 @@ class LoginScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Login")), - body: Center( - child: ElevatedButton( - onPressed: () { - redirect_to_oauth(context); - }, - child: const Text('Login'), - ), + body: Stack( + fit: StackFit.expand, + children: [ + Image.asset( + 'assets/images/cluster-photo-00.jpg', + fit: BoxFit.cover, + ), + Center( + child: ElevatedButton( + onPressed: () { + redirect_to_oauth(context); + }, + child: const Text('Login into 42 API'), + ), + ), + ], ), ); } diff --git a/lib/views/profile_screen.dart b/lib/views/profile_screen.dart index e6336f9..1cae896 100644 --- a/lib/views/profile_screen.dart +++ b/lib/views/profile_screen.dart @@ -1,5 +1,6 @@ -// profile_screen.dart import 'package:flutter/material.dart'; +import 'package:swifty/methods/api.dart'; +import 'package:swifty/views/login_screen.dart'; class ProfileScreen extends StatelessWidget { final String login; @@ -7,16 +8,162 @@ class ProfileScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text("Profile")), - body: Center( - child: ElevatedButton( - onPressed: () { - Navigator.pushNamed(context, '/login'); - }, - child: Text('Go to ${login}'), - ), - ), + return FutureBuilder( + future: fetchProfileData(login), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + if (snapshot.hasError) { + if (snapshot.error.toString().contains('401')) { redirect_to_oauth(context); } + return Scaffold( + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("This user does not exists or is hidden from you !"), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.pushReplacementNamed(context, '/home'); + }, + child: const Text("Bring me back !!"), + ), + ], + ), + ), + ); + + } + + if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { + final Map list = snapshot.data ?? {}; + final name = list['usual_full_name'] ?? 'Unknown'; + final imageUrl = list['image']?['versions']?['medium'] ?? ''; + final correctionsPoints = list['correction_point'] ?? 0; + final level = list['cursus_users']?[1]?['level']?.toDouble() ?? 0.0; + final location = list['location'] ?? 'Unavailable'; + final skills = list['cursus_users']?[1]?['skills']; + final unfilteredProjects = list['projects_users']; + final projects = unfilteredProjects.where((p) => p['status'] != 'creating_group' && p['status'] != 'searching_a_group').toList(); + + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: const Text("Profile"), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pushReplacementNamed(context, '/home'), + ), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + if (imageUrl.isNotEmpty) + CircleAvatar( + radius: 40, + backgroundImage: NetworkImage(imageUrl), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + LinearProgressIndicator(value: level % 1, minHeight: 8), + const SizedBox(height: 4), + Text('Level: ${level.toStringAsFixed(2)}'), + Text('Correction Points: $correctionsPoints'), + Text('Location: $location'), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + const TabBar( + tabs: [ + Tab(text: 'Skills'), + Tab(text: 'Projects'), + ], + labelColor: Colors.blue, + unselectedLabelColor: Colors.grey, + ), + + Expanded( + child: TabBarView( + children: [ + ListView.builder( + itemCount: skills.length, + itemBuilder: (context, index) { + final skill = skills[index] as Map; + return ListTile( + title: Text(skill['name']), + trailing: Text( + skill['level'].toStringAsFixed(2), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18,), + ), + ); + }, + ), + ListView.builder( + itemCount: projects.length, + itemBuilder: (context, index) { + final project = projects[index] as Map; + Text project_grade; + if (project['final_mark'] == null) { + project_grade = const Text( + 'pending', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black38, + fontSize: 18 + ), + ); + } + else { + var color = (project['validated?']) ? Colors.green : Colors.red; + project_grade = Text( + project['final_mark'].toStringAsFixed(2), + style: TextStyle( + fontWeight: FontWeight.bold, + color: color, + fontSize: 18 + ), + ); + } + return ListTile( + title: Text(project['project']['name']), + trailing: project_grade + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } + + return const Scaffold( + body: Center(child: Text("Unexpected error.")), + ); + }, ); } -} \ No newline at end of file +} + diff --git a/pubspec.yaml b/pubspec.yaml index 0a9b3c5..b8b9ced 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,7 @@ flutter: assets: - assets/images/42_logo.png - assets/images/cluster-photo-00.jpg + - .env # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class.