Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
F
FinSim
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Fares
FinSim
Commits
7636a1d3
Commit
7636a1d3
authored
Apr 08, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 27 files via Son of Anton
parent
2a9d7f25
Changes
27
Hide whitespace changes
Inline
Side-by-side
Showing
27 changed files
with
2144 additions
and
21 deletions
+2144
-21
app.dart
finsim_flutter/lib/app.dart
+28
-7
router.dart
finsim_flutter/lib/core/config/router.dart
+185
-13
auth_local_datasource.dart
...features/auth/data/datasources/auth_local_datasource.dart
+23
-0
auth_remote_datasource.dart
...eatures/auth/data/datasources/auth_remote_datasource.dart
+53
-0
user_model.dart
finsim_flutter/lib/features/auth/data/models/user_model.dart
+43
-0
auth_repository_impl.dart
...features/auth/data/repositories/auth_repository_impl.dart
+67
-0
auth_repository.dart
...ib/features/auth/domain/repositories/auth_repository.dart
+8
-0
auth_cubit.dart
...utter/lib/features/auth/presentation/bloc/auth_cubit.dart
+69
-0
auth_state.dart
...utter/lib/features/auth/presentation/bloc/auth_state.dart
+39
-0
login_screen.dart
.../lib/features/auth/presentation/screens/login_screen.dart
+180
-0
register_screen.dart
...b/features/auth/presentation/screens/register_screen.dart
+180
-0
splash_screen.dart
...lib/features/auth/presentation/screens/splash_screen.dart
+119
-0
dashboard_remote_datasource.dart
...shboard/data/datasources/dashboard_remote_datasource.dart
+14
-0
user_stats_model.dart
.../lib/features/dashboard/data/models/user_stats_model.dart
+95
-0
dashboard_repository_impl.dart
...ashboard/data/repositories/dashboard_repository_impl.dart
+22
-0
dashboard_repository.dart
...s/dashboard/domain/repositories/dashboard_repository.dart
+5
-0
dashboard_cubit.dart
...features/dashboard/presentation/bloc/dashboard_cubit.dart
+21
-0
dashboard_state.dart
...features/dashboard/presentation/bloc/dashboard_state.dart
+35
-0
dashboard_screen.dart
...ures/dashboard/presentation/screens/dashboard_screen.dart
+218
-0
profile_screen.dart
...atures/dashboard/presentation/screens/profile_screen.dart
+257
-0
quick_action_card.dart
...res/dashboard/presentation/widgets/quick_action_card.dart
+60
-0
recent_attempts_list.dart
.../dashboard/presentation/widgets/recent_attempts_list.dart
+82
-0
main.dart
finsim_flutter/lib/main.dart
+13
-1
bottom_nav_shell.dart
finsim_flutter/lib/shared/bottom_nav_shell.dart
+55
-0
more_menu_screen.dart
finsim_flutter/lib/shared/more_menu_screen.dart
+102
-0
placeholder_screen.dart
finsim_flutter/lib/shared/placeholder_screen.dart
+53
-0
settings_screen.dart
finsim_flutter/lib/shared/settings_screen.dart
+118
-0
No files found.
finsim_flutter/lib/app.dart
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'core/config/router.dart'
;
import
'core/config/router.dart'
;
import
'core/theme/app_theme.dart'
;
import
'core/theme/app_theme.dart'
;
import
'features/auth/presentation/bloc/auth_cubit.dart'
;
class
FinSimApp
extends
StatefulWidget
{
final
AuthCubit
authCubit
;
const
FinSimApp
({
super
.
key
,
required
this
.
authCubit
});
@override
State
<
FinSimApp
>
createState
()
=>
_FinSimAppState
();
}
class
_FinSimAppState
extends
State
<
FinSimApp
>
{
late
final
AppRouter
_appRouter
;
class
FinSimApp
extends
StatelessWidget
{
@override
const
FinSimApp
({
super
.
key
});
void
initState
()
{
super
.
initState
();
_appRouter
=
AppRouter
(
widget
.
authCubit
);
}
@override
@override
Widget
build
(
BuildContext
context
)
{
Widget
build
(
BuildContext
context
)
{
return
MaterialApp
.
router
(
return
BlocProvider
<
AuthCubit
>.
value
(
title:
'FinSim'
,
value:
widget
.
authCubit
,
debugShowCheckedModeBanner:
false
,
child:
MaterialApp
.
router
(
theme:
AppTheme
.
dark
,
title:
'FinSim'
,
routerConfig:
appRouter
,
debugShowCheckedModeBanner:
false
,
theme:
AppTheme
.
dark
,
routerConfig:
_appRouter
.
router
,
),
);
);
}
}
}
}
\ No newline at end of file
finsim_flutter/lib/core/config/router.dart
View file @
7636a1d3
import
'dart:async'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:go_router/go_router.dart'
;
import
'package:go_router/go_router.dart'
;
import
'../../shared/phase1_showcase_screen.dart'
;
import
'../../features/auth/presentation/bloc/auth_cubit.dart'
;
/// GoRouter configuration — Phase 1 skeleton.
import
'../../features/auth/presentation/bloc/auth_state.dart'
;
/// Phase 2 will replace this with auth guards + full route tree.
import
'../../features/auth/presentation/screens/splash_screen.dart'
;
final
GoRouter
appRouter
=
GoRouter
(
import
'../../features/auth/presentation/screens/login_screen.dart'
;
initialLocation:
'/'
,
import
'../../features/auth/presentation/screens/register_screen.dart'
;
routes:
[
import
'../../features/dashboard/presentation/screens/dashboard_screen.dart'
;
GoRoute
(
import
'../../features/dashboard/presentation/screens/profile_screen.dart'
;
path:
'/'
,
import
'../../shared/bottom_nav_shell.dart'
;
builder:
(
context
,
state
)
=>
const
Phase1ShowcaseScreen
(),
import
'../../shared/more_menu_screen.dart'
;
),
import
'../../shared/settings_screen.dart'
;
],
import
'../../shared/placeholder_screen.dart'
;
);
\ No newline at end of file
class
AppRouter
{
final
AuthCubit
authCubit
;
late
final
GoRouter
router
;
AppRouter
(
this
.
authCubit
)
{
router
=
GoRouter
(
initialLocation:
'/splash'
,
refreshListenable:
_GoRouterAuthRefresh
(
authCubit
.
stream
),
redirect:
_redirect
,
routes:
[
GoRoute
(
path:
'/splash'
,
builder:
(
_
,
__
)
=>
const
SplashScreen
(),
),
GoRoute
(
path:
'/login'
,
builder:
(
_
,
__
)
=>
const
LoginScreen
(),
),
GoRoute
(
path:
'/register'
,
builder:
(
_
,
__
)
=>
const
RegisterScreen
(),
),
StatefulShellRoute
.
indexedStack
(
builder:
(
_
,
__
,
navigationShell
)
=>
BottomNavShell
(
navigationShell:
navigationShell
),
branches:
[
// ── Tab 0: Home ──
StatefulShellBranch
(
routes:
[
GoRoute
(
path:
'/app/dashboard'
,
builder:
(
_
,
__
)
=>
const
DashboardScreen
(),
routes:
[
GoRoute
(
path:
'profile'
,
builder:
(
_
,
__
)
=>
const
ProfileScreen
(),
),
],
),
],
),
// ── Tab 1: Chat ──
StatefulShellBranch
(
routes:
[
GoRoute
(
path:
'/app/chat'
,
builder:
(
_
,
__
)
=>
const
PlaceholderScreen
(
icon:
'🤖'
,
title:
'AI Investment Advisor'
,
subtitle:
'Coming in Phase 3'
,
),
),
],
),
// ── Tab 2: Akinator ──
StatefulShellBranch
(
routes:
[
GoRoute
(
path:
'/app/akinator'
,
builder:
(
_
,
__
)
=>
const
PlaceholderScreen
(
icon:
'🔮'
,
title:
'Akinator 2.0'
,
subtitle:
'Coming in Phase 3'
,
),
),
],
),
// ── Tab 3: Quiz ──
StatefulShellBranch
(
routes:
[
GoRoute
(
path:
'/app/quiz'
,
builder:
(
_
,
__
)
=>
const
PlaceholderScreen
(
icon:
'📝'
,
title:
'Practice MCQ'
,
subtitle:
'Coming in Phase 4'
,
),
),
],
),
// ── Tab 4: More ──
StatefulShellBranch
(
routes:
[
GoRoute
(
path:
'/app/more'
,
builder:
(
_
,
__
)
=>
const
MoreMenuScreen
(),
routes:
[
GoRoute
(
path:
'scenarios'
,
builder:
(
_
,
__
)
=>
const
PlaceholderScreen
(
icon:
'📚'
,
title:
'Scenario Browser'
,
subtitle:
'Coming in Phase 4'
,
),
),
GoRoute
(
path:
'generator'
,
builder:
(
_
,
__
)
=>
const
PlaceholderScreen
(
icon:
'⚡'
,
title:
'Scenario Generator'
,
subtitle:
'Coming in Phase 3'
,
),
),
GoRoute
(
path:
'kb'
,
builder:
(
_
,
__
)
=>
const
PlaceholderScreen
(
icon:
'🧠'
,
title:
'Knowledge Base'
,
subtitle:
'Coming in Phase 5'
,
),
),
GoRoute
(
path:
'leaderboard'
,
builder:
(
_
,
__
)
=>
const
PlaceholderScreen
(
icon:
'🏆'
,
title:
'Leaderboard'
,
subtitle:
'Coming in Phase 5'
,
),
),
GoRoute
(
path:
'settings'
,
builder:
(
_
,
__
)
=>
const
SettingsScreen
(),
),
],
),
],
),
],
),
],
);
}
String
?
_redirect
(
BuildContext
context
,
GoRouterState
state
)
{
final
authState
=
authCubit
.
state
;
final
location
=
state
.
matchedLocation
;
final
isSplash
=
location
==
'/splash'
;
final
isAuthScreen
=
location
==
'/login'
||
location
==
'/register'
;
// Still loading — stay on splash
if
(
authState
is
AuthInitial
||
authState
is
AuthLoading
)
{
return
isSplash
?
null
:
'/splash'
;
}
// Authenticated — get off auth screens
if
(
authState
is
Authenticated
)
{
if
(
isSplash
||
isAuthScreen
)
return
'/app/dashboard'
;
return
null
;
}
// Not authenticated — must be on auth screen
if
(
isSplash
||
isAuthScreen
)
return
null
;
return
'/login'
;
}
}
/// Converts AuthCubit's stream into a Listenable for GoRouter.
class
_GoRouterAuthRefresh
extends
ChangeNotifier
{
late
final
StreamSubscription
<
dynamic
>
_sub
;
_GoRouterAuthRefresh
(
Stream
<
dynamic
>
stream
)
{
_sub
=
stream
.
asBroadcastStream
().
listen
((
_
)
=>
notifyListeners
());
}
@override
void
dispose
()
{
_sub
.
cancel
();
super
.
dispose
();
}
}
\ No newline at end of file
finsim_flutter/lib/features/auth/data/datasources/auth_local_datasource.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter_secure_storage/flutter_secure_storage.dart'
;
class
AuthLocalDatasource
{
static
const
_storage
=
FlutterSecureStorage
();
static
const
_tokenKey
=
'auth_token'
;
Future
<
void
>
saveToken
(
String
token
)
async
{
await
_storage
.
write
(
key:
_tokenKey
,
value:
token
);
}
Future
<
String
?>
getToken
()
async
{
return
await
_storage
.
read
(
key:
_tokenKey
);
}
Future
<
void
>
clearToken
()
async
{
await
_storage
.
delete
(
key:
_tokenKey
);
}
Future
<
bool
>
hasToken
()
async
{
final
token
=
await
getToken
();
return
token
!=
null
&&
token
.
isNotEmpty
;
}
}
\ No newline at end of file
finsim_flutter/lib/features/auth/data/datasources/auth_remote_datasource.dart
0 → 100644
View file @
7636a1d3
import
'package:dio/dio.dart'
;
import
'../../../../core/network/api_endpoints.dart'
;
import
'../models/user_model.dart'
;
class
AuthRemoteDatasource
{
final
Dio
_dio
;
AuthRemoteDatasource
(
this
.
_dio
);
/// POST /api/login → { token, user }
Future
<({
String
token
,
UserModel
user
})>
login
(
String
email
,
String
password
)
async
{
final
response
=
await
_dio
.
post
(
ApiEndpoints
.
login
,
data:
{
'email'
:
email
,
'password'
:
password
},
);
final
data
=
response
.
data
as
Map
<
String
,
dynamic
>;
return
(
token:
data
[
'token'
]
as
String
,
user:
UserModel
.
fromJson
(
data
[
'user'
]
as
Map
<
String
,
dynamic
>),
);
}
/// POST /api/register → { token, user }
Future
<({
String
token
,
UserModel
user
})>
register
(
String
username
,
String
email
,
String
password
)
async
{
final
response
=
await
_dio
.
post
(
ApiEndpoints
.
register
,
data:
{
'username'
:
username
,
'email'
:
email
,
'password'
:
password
},
);
final
data
=
response
.
data
as
Map
<
String
,
dynamic
>;
return
(
token:
data
[
'token'
]
as
String
,
user:
UserModel
.
fromJson
(
data
[
'user'
]
as
Map
<
String
,
dynamic
>),
);
}
/// GET /api/me → { user }
Future
<
UserModel
>
getMe
()
async
{
final
response
=
await
_dio
.
get
(
ApiEndpoints
.
me
);
final
data
=
response
.
data
as
Map
<
String
,
dynamic
>;
return
UserModel
.
fromJson
(
data
[
'user'
]
as
Map
<
String
,
dynamic
>);
}
/// POST /api/logout
Future
<
void
>
logout
()
async
{
try
{
await
_dio
.
post
(
ApiEndpoints
.
logout
);
}
catch
(
_
)
{
// Ignore logout errors — we clear local token regardless
}
}
}
\ No newline at end of file
finsim_flutter/lib/features/auth/data/models/user_model.dart
0 → 100644
View file @
7636a1d3
class
UserModel
{
final
int
userId
;
final
String
username
;
final
String
email
;
final
String
role
;
final
String
?
avatar
;
final
int
totalScore
;
final
int
quizzesTaken
;
const
UserModel
({
required
this
.
userId
,
required
this
.
username
,
required
this
.
email
,
required
this
.
role
,
this
.
avatar
,
this
.
totalScore
=
0
,
this
.
quizzesTaken
=
0
,
});
factory
UserModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
UserModel
(
userId:
json
[
'user_id'
]
as
int
,
username:
json
[
'username'
]
as
String
,
email:
json
[
'email'
]
as
String
,
role:
(
json
[
'role'
]
as
String
?)
??
'user'
,
avatar:
json
[
'avatar'
]
as
String
?,
totalScore:
(
json
[
'total_score'
]
as
num
?)?.
toInt
()
??
0
,
quizzesTaken:
(
json
[
'quizzes_taken'
]
as
num
?)?.
toInt
()
??
0
,
);
}
UserModel
copyWith
({
int
?
totalScore
,
int
?
quizzesTaken
})
{
return
UserModel
(
userId:
userId
,
username:
username
,
email:
email
,
role:
role
,
avatar:
avatar
,
totalScore:
totalScore
??
this
.
totalScore
,
quizzesTaken:
quizzesTaken
??
this
.
quizzesTaken
,
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/auth/data/repositories/auth_repository_impl.dart
0 → 100644
View file @
7636a1d3
import
'package:dio/dio.dart'
;
import
'../../../../core/error/exceptions.dart'
;
import
'../../domain/repositories/auth_repository.dart'
;
import
'../datasources/auth_remote_datasource.dart'
;
import
'../datasources/auth_local_datasource.dart'
;
import
'../models/user_model.dart'
;
class
AuthRepositoryImpl
implements
AuthRepository
{
final
AuthRemoteDatasource
remote
;
final
AuthLocalDatasource
local
;
AuthRepositoryImpl
({
required
this
.
remote
,
required
this
.
local
});
@override
Future
<
UserModel
>
login
(
String
email
,
String
password
)
async
{
try
{
final
result
=
await
remote
.
login
(
email
,
password
);
await
local
.
saveToken
(
result
.
token
);
return
result
.
user
;
}
on
DioException
catch
(
e
)
{
throw
_unwrap
(
e
);
}
}
@override
Future
<
UserModel
>
register
(
String
username
,
String
email
,
String
password
)
async
{
try
{
final
result
=
await
remote
.
register
(
username
,
email
,
password
);
await
local
.
saveToken
(
result
.
token
);
return
result
.
user
;
}
on
DioException
catch
(
e
)
{
throw
_unwrap
(
e
);
}
}
@override
Future
<
UserModel
?>
checkAuth
()
async
{
final
hasToken
=
await
local
.
hasToken
();
if
(!
hasToken
)
return
null
;
try
{
return
await
remote
.
getMe
();
}
on
DioException
catch
(
_
)
{
await
local
.
clearToken
();
return
null
;
}
catch
(
_
)
{
await
local
.
clearToken
();
return
null
;
}
}
@override
Future
<
void
>
logout
()
async
{
await
remote
.
logout
();
await
local
.
clearToken
();
}
Exception
_unwrap
(
DioException
e
)
{
final
error
=
e
.
error
;
if
(
error
is
AuthException
)
return
error
;
if
(
error
is
ConflictException
)
return
error
;
if
(
error
is
NetworkException
)
return
error
;
if
(
error
is
ServerException
)
return
error
;
return
ServerException
(
e
.
message
??
'An unknown error occurred'
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/auth/domain/repositories/auth_repository.dart
0 → 100644
View file @
7636a1d3
import
'../../data/models/user_model.dart'
;
abstract
class
AuthRepository
{
Future
<
UserModel
>
login
(
String
email
,
String
password
);
Future
<
UserModel
>
register
(
String
username
,
String
email
,
String
password
);
Future
<
UserModel
?>
checkAuth
();
Future
<
void
>
logout
();
}
\ No newline at end of file
finsim_flutter/lib/features/auth/presentation/bloc/auth_cubit.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'../../../../core/error/exceptions.dart'
;
import
'../../domain/repositories/auth_repository.dart'
;
import
'auth_state.dart'
;
class
AuthCubit
extends
Cubit
<
AuthState
>
{
final
AuthRepository
_repo
;
AuthCubit
(
this
.
_repo
)
:
super
(
const
AuthInitial
());
/// Auto-check on app launch — reads stored token, validates with API
Future
<
void
>
checkAuth
()
async
{
emit
(
const
AuthLoading
());
try
{
final
user
=
await
_repo
.
checkAuth
();
if
(
user
!=
null
)
{
emit
(
Authenticated
(
user
));
}
else
{
emit
(
const
Unauthenticated
());
}
}
catch
(
_
)
{
emit
(
const
Unauthenticated
());
}
}
/// Login with email + password
Future
<
void
>
login
(
String
email
,
String
password
)
async
{
emit
(
const
AuthLoading
());
try
{
final
user
=
await
_repo
.
login
(
email
,
password
);
emit
(
Authenticated
(
user
));
}
on
AuthException
catch
(
e
)
{
emit
(
AuthError
(
e
.
message
));
}
on
NetworkException
catch
(
e
)
{
emit
(
AuthError
(
e
.
message
));
}
catch
(
e
)
{
emit
(
AuthError
(
e
.
toString
()));
}
}
/// Register new account
Future
<
void
>
register
(
String
username
,
String
email
,
String
password
)
async
{
emit
(
const
AuthLoading
());
try
{
final
user
=
await
_repo
.
register
(
username
,
email
,
password
);
emit
(
Authenticated
(
user
));
}
on
ConflictException
catch
(
e
)
{
emit
(
AuthError
(
e
.
message
));
}
on
NetworkException
catch
(
e
)
{
emit
(
AuthError
(
e
.
message
));
}
catch
(
e
)
{
emit
(
AuthError
(
e
.
toString
()));
}
}
/// Logout — clear token, reset state
Future
<
void
>
logout
()
async
{
try
{
await
_repo
.
logout
();
}
catch
(
_
)
{}
emit
(
const
Unauthenticated
());
}
/// Convenience getter for current user
UserModel
?
get
currentUser
{
final
s
=
state
;
return
s
is
Authenticated
?
s
.
user
:
null
;
}
}
\ No newline at end of file
finsim_flutter/lib/features/auth/presentation/bloc/auth_state.dart
0 → 100644
View file @
7636a1d3
import
'package:equatable/equatable.dart'
;
import
'../../data/models/user_model.dart'
;
sealed
class
AuthState
extends
Equatable
{
const
AuthState
();
@override
List
<
Object
?>
get
props
=>
[];
}
class
AuthInitial
extends
AuthState
{
const
AuthInitial
();
}
class
AuthLoading
extends
AuthState
{
const
AuthLoading
();
}
class
Authenticated
extends
AuthState
{
final
UserModel
user
;
const
Authenticated
(
this
.
user
);
@override
List
<
Object
?>
get
props
=>
[
user
.
userId
,
user
.
username
];
}
class
Unauthenticated
extends
AuthState
{
const
Unauthenticated
();
}
class
AuthError
extends
AuthState
{
final
String
message
;
const
AuthError
(
this
.
message
);
@override
List
<
Object
?>
get
props
=>
[
message
];
}
\ No newline at end of file
finsim_flutter/lib/features/auth/presentation/screens/login_screen.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:go_router/go_router.dart'
;
import
'../../../../core/theme/app_colors.dart'
;
import
'../../../../core/theme/app_text_styles.dart'
;
import
'../../../../core/theme/app_spacing.dart'
;
import
'../../../../core/theme/app_gradients.dart'
;
import
'../../../../core/utils/validators.dart'
;
import
'../../../../core/widgets/fs_button.dart'
;
import
'../../../../core/widgets/fs_text_field.dart'
;
import
'../bloc/auth_cubit.dart'
;
import
'../bloc/auth_state.dart'
;
class
LoginScreen
extends
StatefulWidget
{
const
LoginScreen
({
super
.
key
});
@override
State
<
LoginScreen
>
createState
()
=>
_LoginScreenState
();
}
class
_LoginScreenState
extends
State
<
LoginScreen
>
{
final
_formKey
=
GlobalKey
<
FormState
>();
final
_emailCtrl
=
TextEditingController
();
final
_passwordCtrl
=
TextEditingController
();
bool
_obscurePassword
=
true
;
@override
void
dispose
()
{
_emailCtrl
.
dispose
();
_passwordCtrl
.
dispose
();
super
.
dispose
();
}
void
_submit
()
{
if
(!
_formKey
.
currentState
!.
validate
())
return
;
context
.
read
<
AuthCubit
>().
login
(
_emailCtrl
.
text
.
trim
(),
_passwordCtrl
.
text
,
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
body:
SafeArea
(
child:
Center
(
child:
SingleChildScrollView
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
28
),
child:
BlocListener
<
AuthCubit
,
AuthState
>(
listener:
(
context
,
state
)
{
// Navigation is handled by GoRouter redirect — no need here
},
child:
Form
(
key:
_formKey
,
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
// ── Logo ──
const
Text
(
'📊'
,
style:
TextStyle
(
fontSize:
48
)),
const
SizedBox
(
height:
12
),
ShaderMask
(
shaderCallback:
(
b
)
=>
AppGradients
.
textAccent
.
createShader
(
b
),
child:
Text
(
'FinSim'
,
style:
AppTextStyles
.
headlineLarge
.
copyWith
(
color:
Colors
.
white
),
),
),
const
SizedBox
(
height:
4
),
Text
(
'Investment Scenario Engine'
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
textTertiary
),
),
const
SizedBox
(
height:
36
),
// ── Error Banner ──
BlocBuilder
<
AuthCubit
,
AuthState
>(
buildWhen:
(
prev
,
curr
)
=>
curr
is
AuthError
||
prev
is
AuthError
,
builder:
(
context
,
state
)
{
if
(
state
is
!
AuthError
)
{
return
const
SizedBox
.
shrink
();
}
return
Container
(
width:
double
.
infinity
,
margin:
const
EdgeInsets
.
only
(
bottom:
16
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
14
,
vertical:
10
),
decoration:
BoxDecoration
(
color:
AppColors
.
errorBg
,
border:
Border
.
all
(
color:
AppColors
.
error
.
withOpacity
(
0.25
)),
borderRadius:
BorderRadius
.
circular
(
8
),
),
child:
Text
(
state
.
message
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
errorLight
),
),
);
},
),
// ── Fields ──
FSTextField
(
label:
'Email'
,
hint:
'your@email.com'
,
controller:
_emailCtrl
,
validator:
Validators
.
email
,
keyboardType:
TextInputType
.
emailAddress
,
textInputAction:
TextInputAction
.
next
,
),
AppSpacing
.
vMd
,
FSTextField
(
label:
'Password'
,
hint:
'••••••••'
,
controller:
_passwordCtrl
,
validator:
Validators
.
password
,
obscureText:
_obscurePassword
,
textInputAction:
TextInputAction
.
done
,
onEditingComplete:
_submit
,
suffixIcon:
IconButton
(
icon:
Icon
(
_obscurePassword
?
Icons
.
visibility_off_outlined
:
Icons
.
visibility_outlined
,
color:
AppColors
.
textTertiary
,
size:
20
,
),
onPressed:
()
=>
setState
(()
=>
_obscurePassword
=
!
_obscurePassword
),
),
),
AppSpacing
.
vLg
,
// ── Submit ──
BlocBuilder
<
AuthCubit
,
AuthState
>(
builder:
(
context
,
state
)
{
return
FSButton
(
label:
'Sign In'
,
isLoading:
state
is
AuthLoading
,
onPressed:
state
is
AuthLoading
?
null
:
_submit
,
);
},
),
AppSpacing
.
vLg
,
// ── Toggle ──
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
"Don't have an account? "
,
style:
AppTextStyles
.
bodySmall
,
),
GestureDetector
(
onTap:
()
=>
context
.
go
(
'/register'
),
child:
Text
(
'Create one'
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
accentLight
,
fontWeight:
FontWeight
.
w600
,
),
),
),
],
),
],
),
),
),
),
),
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/auth/presentation/screens/register_screen.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:go_router/go_router.dart'
;
import
'../../../../core/theme/app_colors.dart'
;
import
'../../../../core/theme/app_text_styles.dart'
;
import
'../../../../core/theme/app_spacing.dart'
;
import
'../../../../core/theme/app_gradients.dart'
;
import
'../../../../core/utils/validators.dart'
;
import
'../../../../core/widgets/fs_button.dart'
;
import
'../../../../core/widgets/fs_text_field.dart'
;
import
'../bloc/auth_cubit.dart'
;
import
'../bloc/auth_state.dart'
;
class
RegisterScreen
extends
StatefulWidget
{
const
RegisterScreen
({
super
.
key
});
@override
State
<
RegisterScreen
>
createState
()
=>
_RegisterScreenState
();
}
class
_RegisterScreenState
extends
State
<
RegisterScreen
>
{
final
_formKey
=
GlobalKey
<
FormState
>();
final
_usernameCtrl
=
TextEditingController
();
final
_emailCtrl
=
TextEditingController
();
final
_passwordCtrl
=
TextEditingController
();
bool
_obscurePassword
=
true
;
@override
void
dispose
()
{
_usernameCtrl
.
dispose
();
_emailCtrl
.
dispose
();
_passwordCtrl
.
dispose
();
super
.
dispose
();
}
void
_submit
()
{
if
(!
_formKey
.
currentState
!.
validate
())
return
;
context
.
read
<
AuthCubit
>().
register
(
_usernameCtrl
.
text
.
trim
(),
_emailCtrl
.
text
.
trim
(),
_passwordCtrl
.
text
,
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
body:
SafeArea
(
child:
Center
(
child:
SingleChildScrollView
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
28
),
child:
Form
(
key:
_formKey
,
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
const
Text
(
'📊'
,
style:
TextStyle
(
fontSize:
48
)),
const
SizedBox
(
height:
12
),
ShaderMask
(
shaderCallback:
(
b
)
=>
AppGradients
.
textAccent
.
createShader
(
b
),
child:
Text
(
'FinSim'
,
style:
AppTextStyles
.
headlineLarge
.
copyWith
(
color:
Colors
.
white
),
),
),
const
SizedBox
(
height:
4
),
Text
(
'Create your account'
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
textTertiary
),
),
const
SizedBox
(
height:
36
),
// ── Error Banner ──
BlocBuilder
<
AuthCubit
,
AuthState
>(
buildWhen:
(
prev
,
curr
)
=>
curr
is
AuthError
||
prev
is
AuthError
,
builder:
(
context
,
state
)
{
if
(
state
is
!
AuthError
)
return
const
SizedBox
.
shrink
();
return
Container
(
width:
double
.
infinity
,
margin:
const
EdgeInsets
.
only
(
bottom:
16
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
14
,
vertical:
10
),
decoration:
BoxDecoration
(
color:
AppColors
.
errorBg
,
border:
Border
.
all
(
color:
AppColors
.
error
.
withOpacity
(
0.25
)),
borderRadius:
BorderRadius
.
circular
(
8
),
),
child:
Text
(
state
.
message
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
errorLight
),
),
);
},
),
FSTextField
(
label:
'Username'
,
hint:
'johndoe'
,
controller:
_usernameCtrl
,
validator:
Validators
.
username
,
textInputAction:
TextInputAction
.
next
,
),
AppSpacing
.
vMd
,
FSTextField
(
label:
'Email'
,
hint:
'your@email.com'
,
controller:
_emailCtrl
,
validator:
Validators
.
email
,
keyboardType:
TextInputType
.
emailAddress
,
textInputAction:
TextInputAction
.
next
,
),
AppSpacing
.
vMd
,
FSTextField
(
label:
'Password'
,
hint:
'Min 4 characters'
,
controller:
_passwordCtrl
,
validator:
Validators
.
password
,
obscureText:
_obscurePassword
,
textInputAction:
TextInputAction
.
done
,
onEditingComplete:
_submit
,
suffixIcon:
IconButton
(
icon:
Icon
(
_obscurePassword
?
Icons
.
visibility_off_outlined
:
Icons
.
visibility_outlined
,
color:
AppColors
.
textTertiary
,
size:
20
,
),
onPressed:
()
=>
setState
(()
=>
_obscurePassword
=
!
_obscurePassword
),
),
),
AppSpacing
.
vLg
,
BlocBuilder
<
AuthCubit
,
AuthState
>(
builder:
(
context
,
state
)
{
return
FSButton
(
label:
'Create Account'
,
isLoading:
state
is
AuthLoading
,
onPressed:
state
is
AuthLoading
?
null
:
_submit
,
);
},
),
AppSpacing
.
vLg
,
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
'Already have an account? '
,
style:
AppTextStyles
.
bodySmall
,
),
GestureDetector
(
onTap:
()
=>
context
.
go
(
'/login'
),
child:
Text
(
'Sign in'
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
accentLight
,
fontWeight:
FontWeight
.
w600
,
),
),
),
],
),
],
),
),
),
),
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/auth/presentation/screens/splash_screen.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'../../../../core/theme/app_colors.dart'
;
import
'../../../../core/theme/app_text_styles.dart'
;
import
'../../../../core/theme/app_gradients.dart'
;
import
'../bloc/auth_cubit.dart'
;
class
SplashScreen
extends
StatefulWidget
{
const
SplashScreen
({
super
.
key
});
@override
State
<
SplashScreen
>
createState
()
=>
_SplashScreenState
();
}
class
_SplashScreenState
extends
State
<
SplashScreen
>
with
SingleTickerProviderStateMixin
{
late
final
AnimationController
_controller
;
late
final
Animation
<
double
>
_fadeAnim
;
late
final
Animation
<
double
>
_scaleAnim
;
@override
void
initState
()
{
super
.
initState
();
_controller
=
AnimationController
(
vsync:
this
,
duration:
const
Duration
(
milliseconds:
1000
),
);
_fadeAnim
=
Tween
<
double
>(
begin:
0
,
end:
1
).
animate
(
CurvedAnimation
(
parent:
_controller
,
curve:
Curves
.
easeOut
),
);
_scaleAnim
=
Tween
<
double
>(
begin:
0.8
,
end:
1.0
).
animate
(
CurvedAnimation
(
parent:
_controller
,
curve:
Curves
.
easeOutBack
),
);
_controller
.
forward
();
// Trigger auth check after a short delay for visual polish
Future
.
delayed
(
const
Duration
(
milliseconds:
600
),
()
{
if
(
mounted
)
{
context
.
read
<
AuthCubit
>().
checkAuth
();
}
});
}
@override
void
dispose
()
{
_controller
.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
body:
Container
(
decoration:
BoxDecoration
(
color:
AppColors
.
bg
,
gradient:
RadialGradient
(
center:
Alignment
.
center
,
radius:
0.8
,
colors:
[
AppColors
.
accent
.
withOpacity
(
0.06
),
AppColors
.
bg
,
],
),
),
child:
Center
(
child:
AnimatedBuilder
(
animation:
_controller
,
builder:
(
_
,
__
)
=>
Opacity
(
opacity:
_fadeAnim
.
value
,
child:
Transform
.
scale
(
scale:
_scaleAnim
.
value
,
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
const
Text
(
'📊'
,
style:
TextStyle
(
fontSize:
64
)),
const
SizedBox
(
height:
16
),
ShaderMask
(
shaderCallback:
(
bounds
)
=>
AppGradients
.
textAccent
.
createShader
(
bounds
),
child:
Text
(
'FinSim'
,
style:
AppTextStyles
.
displayLarge
.
copyWith
(
color:
Colors
.
white
,
),
),
),
const
SizedBox
(
height:
8
),
Text
(
'AI-Powered Investment Simulation'
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
textTertiary
,
),
),
const
SizedBox
(
height:
48
),
SizedBox
(
width:
24
,
height:
24
,
child:
CircularProgressIndicator
(
strokeWidth:
2
,
valueColor:
AlwaysStoppedAnimation
(
AppColors
.
accent
.
withOpacity
(
0.5
),
),
),
),
],
),
),
),
),
),
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/dashboard/data/datasources/dashboard_remote_datasource.dart
0 → 100644
View file @
7636a1d3
import
'package:dio/dio.dart'
;
import
'../../../../core/network/api_endpoints.dart'
;
import
'../models/user_stats_model.dart'
;
class
DashboardRemoteDatasource
{
final
Dio
_dio
;
DashboardRemoteDatasource
(
this
.
_dio
);
Future
<
UserStatsModel
>
getStats
()
async
{
final
response
=
await
_dio
.
get
(
ApiEndpoints
.
stats
);
return
UserStatsModel
.
fromJson
(
response
.
data
as
Map
<
String
,
dynamic
>);
}
}
\ No newline at end of file
finsim_flutter/lib/features/dashboard/data/models/user_stats_model.dart
0 → 100644
View file @
7636a1d3
class
UserStatsModel
{
final
int
totalScore
;
final
int
quizzesTaken
;
final
int
avgScore
;
final
int
totalQuestionsAnswered
;
final
int
totalCorrect
;
final
double
accuracy
;
final
List
<
QuizAttemptModel
>
recentAttempts
;
final
List
<
CategoryBreakdownModel
>
categories
;
const
UserStatsModel
({
this
.
totalScore
=
0
,
this
.
quizzesTaken
=
0
,
this
.
avgScore
=
0
,
this
.
totalQuestionsAnswered
=
0
,
this
.
totalCorrect
=
0
,
this
.
accuracy
=
0
,
this
.
recentAttempts
=
const
[],
this
.
categories
=
const
[],
});
factory
UserStatsModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
UserStatsModel
(
totalScore:
(
json
[
'total_score'
]
as
num
?)?.
toInt
()
??
0
,
quizzesTaken:
(
json
[
'quizzes_taken'
]
as
num
?)?.
toInt
()
??
0
,
avgScore:
(
json
[
'avg_score'
]
as
num
?)?.
toInt
()
??
0
,
totalQuestionsAnswered:
(
json
[
'total_questions_answered'
]
as
num
?)?.
toInt
()
??
0
,
totalCorrect:
(
json
[
'total_correct'
]
as
num
?)?.
toInt
()
??
0
,
accuracy:
(
json
[
'accuracy'
]
as
num
?)?.
toDouble
()
??
0
,
recentAttempts:
(
json
[
'recent_attempts'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
QuizAttemptModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
()
??
[],
categories:
(
json
[
'categories'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
CategoryBreakdownModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
()
??
[],
);
}
}
class
QuizAttemptModel
{
final
int
id
;
final
int
?
score
;
final
int
?
correctAnswers
;
final
int
?
totalQuestions
;
final
String
status
;
final
String
?
startedAt
;
final
String
?
completedAt
;
const
QuizAttemptModel
({
required
this
.
id
,
this
.
score
,
this
.
correctAnswers
,
this
.
totalQuestions
,
required
this
.
status
,
this
.
startedAt
,
this
.
completedAt
,
});
factory
QuizAttemptModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
QuizAttemptModel
(
id:
(
json
[
'id'
]
as
num
).
toInt
(),
score:
(
json
[
'score'
]
as
num
?)?.
toInt
(),
correctAnswers:
(
json
[
'correct_answers'
]
as
num
?)?.
toInt
(),
totalQuestions:
(
json
[
'total_questions'
]
as
num
?)?.
toInt
(),
status:
(
json
[
'status'
]
as
String
?)
??
'unknown'
,
startedAt:
json
[
'started_at'
]
as
String
?,
completedAt:
json
[
'completed_at'
]
as
String
?,
);
}
}
class
CategoryBreakdownModel
{
final
String
category
;
final
int
attempts
;
final
int
correct
;
const
CategoryBreakdownModel
({
required
this
.
category
,
required
this
.
attempts
,
this
.
correct
=
0
,
});
factory
CategoryBreakdownModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
CategoryBreakdownModel
(
category:
json
[
'category'
]
as
String
,
attempts:
(
json
[
'attempts'
]
as
num
).
toInt
(),
correct:
(
json
[
'correct'
]
as
num
?)?.
toInt
()
??
0
,
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/dashboard/data/repositories/dashboard_repository_impl.dart
0 → 100644
View file @
7636a1d3
import
'package:dio/dio.dart'
;
import
'../../../../core/error/exceptions.dart'
;
import
'../../domain/repositories/dashboard_repository.dart'
;
import
'../datasources/dashboard_remote_datasource.dart'
;
import
'../models/user_stats_model.dart'
;
class
DashboardRepositoryImpl
implements
DashboardRepository
{
final
DashboardRemoteDatasource
remote
;
DashboardRepositoryImpl
({
required
this
.
remote
});
@override
Future
<
UserStatsModel
>
getStats
()
async
{
try
{
return
await
remote
.
getStats
();
}
on
DioException
catch
(
e
)
{
final
error
=
e
.
error
;
if
(
error
is
Exception
)
throw
error
;
throw
ServerException
(
e
.
message
??
'Failed to load stats'
);
}
}
}
\ No newline at end of file
finsim_flutter/lib/features/dashboard/domain/repositories/dashboard_repository.dart
0 → 100644
View file @
7636a1d3
import
'../../data/models/user_stats_model.dart'
;
abstract
class
DashboardRepository
{
Future
<
UserStatsModel
>
getStats
();
}
\ No newline at end of file
finsim_flutter/lib/features/dashboard/presentation/bloc/dashboard_cubit.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'../../domain/repositories/dashboard_repository.dart'
;
import
'dashboard_state.dart'
;
class
DashboardCubit
extends
Cubit
<
DashboardState
>
{
final
DashboardRepository
_repo
;
DashboardCubit
(
this
.
_repo
)
:
super
(
const
DashboardInitial
());
Future
<
void
>
loadStats
()
async
{
emit
(
const
DashboardLoading
());
try
{
final
stats
=
await
_repo
.
getStats
();
emit
(
DashboardLoaded
(
stats
));
}
catch
(
e
)
{
emit
(
DashboardError
(
e
.
toString
()));
}
}
Future
<
void
>
refresh
()
async
=>
loadStats
();
}
\ No newline at end of file
finsim_flutter/lib/features/dashboard/presentation/bloc/dashboard_state.dart
0 → 100644
View file @
7636a1d3
import
'package:equatable/equatable.dart'
;
import
'../../data/models/user_stats_model.dart'
;
sealed
class
DashboardState
extends
Equatable
{
const
DashboardState
();
@override
List
<
Object
?>
get
props
=>
[];
}
class
DashboardInitial
extends
DashboardState
{
const
DashboardInitial
();
}
class
DashboardLoading
extends
DashboardState
{
const
DashboardLoading
();
}
class
DashboardLoaded
extends
DashboardState
{
final
UserStatsModel
stats
;
const
DashboardLoaded
(
this
.
stats
);
@override
List
<
Object
?>
get
props
=>
[
stats
.
totalScore
,
stats
.
quizzesTaken
];
}
class
DashboardError
extends
DashboardState
{
final
String
message
;
const
DashboardError
(
this
.
message
);
@override
List
<
Object
?>
get
props
=>
[
message
];
}
\ No newline at end of file
finsim_flutter/lib/features/dashboard/presentation/screens/dashboard_screen.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:go_router/go_router.dart'
;
import
'../../../../core/network/dio_client.dart'
;
import
'../../../../core/theme/app_colors.dart'
;
import
'../../../../core/theme/app_text_styles.dart'
;
import
'../../../../core/theme/app_spacing.dart'
;
import
'../../../../core/theme/app_radius.dart'
;
import
'../../../../core/widgets/stat_card.dart'
;
import
'../../../../core/widgets/fs_shimmer.dart'
;
import
'../../../../core/widgets/fs_error_state.dart'
;
import
'../../../auth/presentation/bloc/auth_cubit.dart'
;
import
'../../data/datasources/dashboard_remote_datasource.dart'
;
import
'../../data/repositories/dashboard_repository_impl.dart'
;
import
'../bloc/dashboard_cubit.dart'
;
import
'../bloc/dashboard_state.dart'
;
import
'../widgets/quick_action_card.dart'
;
import
'../widgets/recent_attempts_list.dart'
;
class
DashboardScreen
extends
StatelessWidget
{
const
DashboardScreen
({
super
.
key
});
@override
Widget
build
(
BuildContext
context
)
{
return
BlocProvider
(
create:
(
_
)
{
final
remote
=
DashboardRemoteDatasource
(
DioClient
.
instance
);
final
repo
=
DashboardRepositoryImpl
(
remote:
remote
);
return
DashboardCubit
(
repo
)..
loadStats
();
},
child:
const
_DashboardView
(),
);
}
}
class
_DashboardView
extends
StatelessWidget
{
const
_DashboardView
();
@override
Widget
build
(
BuildContext
context
)
{
final
user
=
context
.
read
<
AuthCubit
>().
currentUser
;
final
username
=
user
?.
username
??
'User'
;
final
initial
=
username
.
isNotEmpty
?
username
[
0
].
toUpperCase
()
:
'?'
;
return
Scaffold
(
body:
SafeArea
(
child:
BlocBuilder
<
DashboardCubit
,
DashboardState
>(
builder:
(
context
,
state
)
{
return
RefreshIndicator
(
color:
AppColors
.
accent
,
backgroundColor:
AppColors
.
panel
,
onRefresh:
()
=>
context
.
read
<
DashboardCubit
>().
refresh
(),
child:
ListView
(
padding:
AppSpacing
.
screenAll
,
children:
[
// ── Greeting ──
Row
(
children:
[
Container
(
width:
44
,
height:
44
,
decoration:
BoxDecoration
(
gradient:
const
LinearGradient
(
colors:
[
AppColors
.
accent
,
Color
(
0xFFA855F7
)],
),
borderRadius:
BorderRadius
.
circular
(
22
),
),
alignment:
Alignment
.
center
,
child:
Text
(
initial
,
style:
AppTextStyles
.
titleMedium
.
copyWith
(
color:
Colors
.
white
),
),
),
const
SizedBox
(
width:
12
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'Welcome back,'
,
style:
AppTextStyles
.
bodySmall
),
Text
(
username
,
style:
AppTextStyles
.
titleMedium
),
],
),
),
IconButton
(
icon:
const
Icon
(
Icons
.
person_outline
,
color:
AppColors
.
textSecondary
),
onPressed:
()
=>
context
.
go
(
'/app/dashboard/profile'
),
),
],
),
AppSpacing
.
vLg
,
// ── Stats ──
if
(
state
is
DashboardLoading
)
_buildStatsShimmer
()
else
if
(
state
is
DashboardLoaded
)
...[
GridView
.
count
(
crossAxisCount:
2
,
shrinkWrap:
true
,
physics:
const
NeverScrollableScrollPhysics
(),
crossAxisSpacing:
12
,
mainAxisSpacing:
12
,
childAspectRatio:
1.35
,
children:
[
StatCard
(
icon:
'🎯'
,
value:
'
${state.stats.totalScore}
'
,
label:
'Total Score'
,
),
StatCard
(
icon:
'📝'
,
value:
'
${state.stats.quizzesTaken}
'
,
label:
'Quizzes'
,
),
StatCard
(
icon:
'✅'
,
value:
'
${state.stats.accuracy.toStringAsFixed(1)}
%'
,
label:
'Accuracy'
,
),
StatCard
(
icon:
'📊'
,
value:
'
${state.stats.avgScore}
'
,
label:
'Avg Score'
,
),
],
),
]
else
if
(
state
is
DashboardError
)
FSErrorState
(
message:
state
.
message
,
onRetry:
()
=>
context
.
read
<
DashboardCubit
>().
loadStats
(),
),
AppSpacing
.
vLg
,
// ── Quick Actions ──
Text
(
'Quick Actions'
,
style:
AppTextStyles
.
titleMedium
),
AppSpacing
.
vSm
,
Row
(
children:
[
QuickActionCard
(
icon:
'⚡'
,
label:
'Generate'
,
accentColor:
AppColors
.
warning
,
onTap:
()
=>
context
.
go
(
'/app/more/generator'
),
),
const
SizedBox
(
width:
10
),
QuickActionCard
(
icon:
'📝'
,
label:
'Quiz'
,
accentColor:
AppColors
.
success
,
onTap:
()
=>
context
.
go
(
'/app/quiz'
),
),
const
SizedBox
(
width:
10
),
QuickActionCard
(
icon:
'🔮'
,
label:
'Akinator'
,
accentColor:
AppColors
.
purple
,
onTap:
()
=>
context
.
go
(
'/app/akinator'
),
),
],
),
AppSpacing
.
vLg
,
// ── Recent Attempts ──
Container
(
decoration:
BoxDecoration
(
color:
AppColors
.
panel
,
border:
Border
.
all
(
color:
AppColors
.
border
),
borderRadius:
AppRadius
.
mdAll
,
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Padding
(
padding:
const
EdgeInsets
.
fromLTRB
(
16
,
16
,
16
,
8
),
child:
Text
(
'Recent Quiz Attempts'
,
style:
AppTextStyles
.
titleSmall
),
),
if
(
state
is
DashboardLoading
)
const
Padding
(
padding:
EdgeInsets
.
all
(
16
),
child:
FSShimmerList
(
itemCount:
3
),
)
else
if
(
state
is
DashboardLoaded
)
RecentAttemptsList
(
attempts:
state
.
stats
.
recentAttempts
),
const
SizedBox
(
height:
8
),
],
),
),
],
),
);
},
),
),
);
}
Widget
_buildStatsShimmer
()
{
return
GridView
.
count
(
crossAxisCount:
2
,
shrinkWrap:
true
,
physics:
const
NeverScrollableScrollPhysics
(),
crossAxisSpacing:
12
,
mainAxisSpacing:
12
,
childAspectRatio:
1.35
,
children:
List
.
generate
(
4
,
(
_
)
=>
FSShimmer
.
card
(
height:
100
),
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/dashboard/presentation/screens/profile_screen.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:fl_chart/fl_chart.dart'
;
import
'../../../../core/network/dio_client.dart'
;
import
'../../../../core/theme/app_colors.dart'
;
import
'../../../../core/theme/app_text_styles.dart'
;
import
'../../../../core/theme/app_spacing.dart'
;
import
'../../../../core/theme/app_radius.dart'
;
import
'../../../../core/theme/app_gradients.dart'
;
import
'../../../../core/widgets/fs_shimmer.dart'
;
import
'../../../../core/widgets/fs_error_state.dart'
;
import
'../../../../core/widgets/fs_button.dart'
;
import
'../../../auth/presentation/bloc/auth_cubit.dart'
;
import
'../../data/datasources/dashboard_remote_datasource.dart'
;
import
'../../data/repositories/dashboard_repository_impl.dart'
;
import
'../../data/models/user_stats_model.dart'
;
import
'../bloc/dashboard_cubit.dart'
;
import
'../bloc/dashboard_state.dart'
;
class
ProfileScreen
extends
StatelessWidget
{
const
ProfileScreen
({
super
.
key
});
@override
Widget
build
(
BuildContext
context
)
{
return
BlocProvider
(
create:
(
_
)
{
final
remote
=
DashboardRemoteDatasource
(
DioClient
.
instance
);
final
repo
=
DashboardRepositoryImpl
(
remote:
remote
);
return
DashboardCubit
(
repo
)..
loadStats
();
},
child:
const
_ProfileView
(),
);
}
}
class
_ProfileView
extends
StatelessWidget
{
const
_ProfileView
();
@override
Widget
build
(
BuildContext
context
)
{
final
authCubit
=
context
.
read
<
AuthCubit
>();
final
user
=
authCubit
.
currentUser
;
return
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'Profile'
)),
body:
BlocBuilder
<
DashboardCubit
,
DashboardState
>(
builder:
(
context
,
state
)
{
if
(
state
is
DashboardLoading
)
{
return
const
Center
(
child:
CircularProgressIndicator
());
}
if
(
state
is
DashboardError
)
{
return
FSErrorState
(
message:
state
.
message
,
onRetry:
()
=>
context
.
read
<
DashboardCubit
>().
loadStats
(),
);
}
if
(
state
is
!
DashboardLoaded
)
{
return
const
SizedBox
.
shrink
();
}
final
stats
=
state
.
stats
;
return
ListView
(
padding:
AppSpacing
.
screenAll
,
children:
[
// ── User Header ──
Center
(
child:
Column
(
children:
[
Container
(
width:
72
,
height:
72
,
decoration:
BoxDecoration
(
gradient:
AppGradients
.
primaryButton
,
shape:
BoxShape
.
circle
,
),
alignment:
Alignment
.
center
,
child:
Text
(
(
user
?.
username
??
'?'
)[
0
].
toUpperCase
(),
style:
AppTextStyles
.
headlineLarge
.
copyWith
(
color:
Colors
.
white
),
),
),
AppSpacing
.
vSm
,
Text
(
user
?.
username
??
'User'
,
style:
AppTextStyles
.
titleLarge
),
Text
(
user
?.
email
??
''
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
textTertiary
)),
AppSpacing
.
vXs
,
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
4
),
decoration:
BoxDecoration
(
color:
AppColors
.
accentGlow
,
borderRadius:
AppRadius
.
pill
,
),
child:
Text
(
user
?.
role
.
toUpperCase
()
??
'USER'
,
style:
AppTextStyles
.
labelSmall
.
copyWith
(
color:
AppColors
.
accentLight
),
),
),
],
),
),
AppSpacing
.
vXl
,
// ── Accuracy Donut ──
_buildAccuracyChart
(
stats
),
AppSpacing
.
vLg
,
// ── Category Breakdown ──
if
(
stats
.
categories
.
isNotEmpty
)
...[
Text
(
'Category Breakdown'
,
style:
AppTextStyles
.
titleMedium
),
AppSpacing
.
vSm
,
...
stats
.
categories
.
map
((
c
)
=>
_buildCategoryBar
(
c
)),
AppSpacing
.
vLg
,
],
// ── Logout ──
FSButton
(
label:
'Sign Out'
,
variant:
FSButtonVariant
.
danger
,
icon:
Icons
.
logout
,
onPressed:
()
=>
authCubit
.
logout
(),
),
AppSpacing
.
vXl
,
],
);
},
),
);
}
Widget
_buildAccuracyChart
(
UserStatsModel
stats
)
{
final
correct
=
stats
.
totalCorrect
.
toDouble
();
final
wrong
=
(
stats
.
totalQuestionsAnswered
-
stats
.
totalCorrect
).
toDouble
();
final
hasData
=
stats
.
totalQuestionsAnswered
>
0
;
return
Container
(
padding:
AppSpacing
.
cardInnerLarge
,
decoration:
BoxDecoration
(
color:
AppColors
.
panel
,
border:
Border
.
all
(
color:
AppColors
.
border
),
borderRadius:
AppRadius
.
mdAll
,
),
child:
Column
(
children:
[
Text
(
'Overall Accuracy'
,
style:
AppTextStyles
.
titleSmall
),
AppSpacing
.
vMd
,
SizedBox
(
height:
160
,
child:
hasData
?
PieChart
(
PieChartData
(
sectionsSpace:
3
,
centerSpaceRadius:
40
,
sections:
[
PieChartSectionData
(
value:
correct
,
color:
AppColors
.
success
,
title:
'
${stats.accuracy.toStringAsFixed(0)}
%'
,
titleStyle:
AppTextStyles
.
titleSmall
.
copyWith
(
color:
Colors
.
white
,
fontSize:
13
),
radius:
35
,
),
PieChartSectionData
(
value:
wrong
>
0
?
wrong
:
0.01
,
color:
AppColors
.
error
.
withOpacity
(
0.4
),
title:
''
,
radius:
28
,
),
],
),
)
:
Center
(
child:
Text
(
'No data yet'
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
textTertiary
),
),
),
),
AppSpacing
.
vSm
,
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
_legendDot
(
AppColors
.
success
,
'Correct (
${stats.totalCorrect}
)'
),
const
SizedBox
(
width:
20
),
_legendDot
(
AppColors
.
error
.
withOpacity
(
0.4
),
'Wrong (
${stats.totalQuestionsAnswered - stats.totalCorrect}
)'
,
),
],
),
],
),
);
}
Widget
_legendDot
(
Color
color
,
String
label
)
{
return
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
Container
(
width:
10
,
height:
10
,
decoration:
BoxDecoration
(
color:
color
,
shape:
BoxShape
.
circle
),
),
const
SizedBox
(
width:
6
),
Text
(
label
,
style:
AppTextStyles
.
caption
),
],
);
}
Widget
_buildCategoryBar
(
CategoryBreakdownModel
cat
)
{
final
pct
=
cat
.
attempts
>
0
?
(
cat
.
correct
/
cat
.
attempts
*
100
).
round
()
:
0
;
final
fill
=
cat
.
attempts
>
0
?
cat
.
correct
/
cat
.
attempts
:
0.0
;
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
6
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Text
(
cat
.
category
,
style:
AppTextStyles
.
bodySmall
),
Text
(
'
$pct
% (
${cat.correct}
/
${cat.attempts}
)'
,
style:
AppTextStyles
.
caption
,
),
],
),
const
SizedBox
(
height:
4
),
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
3
),
child:
LinearProgressIndicator
(
value:
fill
,
minHeight:
6
,
backgroundColor:
AppColors
.
border
,
valueColor:
AlwaysStoppedAnimation
(
pct
>=
70
?
AppColors
.
success
:
pct
>=
40
?
AppColors
.
warning
:
AppColors
.
error
,
),
),
),
],
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/dashboard/presentation/widgets/quick_action_card.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'../../../../core/theme/app_colors.dart'
;
import
'../../../../core/theme/app_text_styles.dart'
;
import
'../../../../core/theme/app_radius.dart'
;
class
QuickActionCard
extends
StatelessWidget
{
final
String
icon
;
final
String
label
;
final
Color
accentColor
;
final
VoidCallback
onTap
;
const
QuickActionCard
({
super
.
key
,
required
this
.
icon
,
required
this
.
label
,
required
this
.
accentColor
,
required
this
.
onTap
,
});
@override
Widget
build
(
BuildContext
context
)
{
return
Expanded
(
child:
Material
(
color:
AppColors
.
panel
,
borderRadius:
AppRadius
.
mdAll
,
child:
InkWell
(
onTap:
onTap
,
borderRadius:
AppRadius
.
mdAll
,
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
16
,
horizontal:
8
),
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
AppColors
.
border
),
borderRadius:
AppRadius
.
mdAll
,
),
child:
Column
(
children:
[
Container
(
width:
44
,
height:
44
,
decoration:
BoxDecoration
(
color:
accentColor
.
withOpacity
(
0.12
),
borderRadius:
AppRadius
.
smAll
,
),
alignment:
Alignment
.
center
,
child:
Text
(
icon
,
style:
const
TextStyle
(
fontSize:
22
)),
),
const
SizedBox
(
height:
8
),
Text
(
label
,
style:
AppTextStyles
.
labelMedium
,
textAlign:
TextAlign
.
center
,
),
],
),
),
),
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/dashboard/presentation/widgets/recent_attempts_list.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'../../../../core/theme/app_colors.dart'
;
import
'../../../../core/theme/app_text_styles.dart'
;
import
'../../../../core/theme/app_radius.dart'
;
import
'../../../../core/utils/formatters.dart'
;
import
'../../../../core/widgets/fs_badge.dart'
;
import
'../../data/models/user_stats_model.dart'
;
class
RecentAttemptsList
extends
StatelessWidget
{
final
List
<
QuizAttemptModel
>
attempts
;
const
RecentAttemptsList
({
super
.
key
,
required
this
.
attempts
});
@override
Widget
build
(
BuildContext
context
)
{
if
(
attempts
.
isEmpty
)
{
return
Container
(
padding:
const
EdgeInsets
.
all
(
24
),
alignment:
Alignment
.
center
,
child:
Text
(
'No quiz attempts yet.
\n
Start practicing to see your results!'
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
textTertiary
),
textAlign:
TextAlign
.
center
,
),
);
}
return
Column
(
children:
attempts
.
map
((
a
)
{
final
score
=
a
.
score
??
0
;
final
badgeVariant
=
score
>=
70
?
FSBadgeVariant
.
green
:
score
>=
40
?
FSBadgeVariant
.
orange
:
FSBadgeVariant
.
red
;
return
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
14
,
vertical:
10
),
decoration:
BoxDecoration
(
border:
Border
(
bottom:
BorderSide
(
color:
AppColors
.
border
.
withOpacity
(
0.5
)),
),
),
child:
Row
(
children:
[
Expanded
(
flex:
3
,
child:
Text
(
Formatters
.
date
(
a
.
startedAt
),
style:
AppTextStyles
.
bodySmall
,
),
),
Expanded
(
flex:
2
,
child:
FSBadge
(
text:
'
$score
%'
,
variant:
badgeVariant
,
),
),
Expanded
(
flex:
2
,
child:
Text
(
'
${a.correctAnswers ?? 0}
/
${a.totalQuestions ?? 0}
'
,
style:
AppTextStyles
.
bodySmall
,
textAlign:
TextAlign
.
center
,
),
),
Expanded
(
flex:
2
,
child:
Text
(
a
.
status
,
style:
AppTextStyles
.
caption
,
textAlign:
TextAlign
.
end
,
),
),
],
),
);
}).
toList
(),
);
}
}
\ No newline at end of file
finsim_flutter/lib/main.dart
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_dotenv/flutter_dotenv.dart'
;
import
'package:flutter_dotenv/flutter_dotenv.dart'
;
import
'app.dart'
;
import
'app.dart'
;
import
'core/network/dio_client.dart'
;
import
'features/auth/data/datasources/auth_remote_datasource.dart'
;
import
'features/auth/data/datasources/auth_local_datasource.dart'
;
import
'features/auth/data/repositories/auth_repository_impl.dart'
;
import
'features/auth/presentation/bloc/auth_cubit.dart'
;
void
main
(
)
async
{
void
main
(
)
async
{
WidgetsFlutterBinding
.
ensureInitialized
();
WidgetsFlutterBinding
.
ensureInitialized
();
...
@@ -21,5 +27,11 @@ void main() async {
...
@@ -21,5 +27,11 @@ void main() async {
DeviceOrientation
.
portraitDown
,
DeviceOrientation
.
portraitDown
,
]);
]);
runApp
(
const
FinSimApp
());
final
dio
=
DioClient
.
instance
;
final
authLocal
=
AuthLocalDatasource
();
final
authRemote
=
AuthRemoteDatasource
(
dio
);
final
authRepo
=
AuthRepositoryImpl
(
remote:
authRemote
,
local:
authLocal
);
final
authCubit
=
AuthCubit
(
authRepo
);
runApp
(
FinSimApp
(
authCubit:
authCubit
));
}
}
\ No newline at end of file
finsim_flutter/lib/shared/bottom_nav_shell.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'package:go_router/go_router.dart'
;
import
'../core/theme/app_colors.dart'
;
class
BottomNavShell
extends
StatelessWidget
{
final
StatefulNavigationShell
navigationShell
;
const
BottomNavShell
({
super
.
key
,
required
this
.
navigationShell
});
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
body:
navigationShell
,
bottomNavigationBar:
Container
(
decoration:
const
BoxDecoration
(
border:
Border
(
top:
BorderSide
(
color:
AppColors
.
border
,
width:
1
)),
),
child:
BottomNavigationBar
(
currentIndex:
navigationShell
.
currentIndex
,
onTap:
(
index
)
=>
navigationShell
.
goBranch
(
index
,
initialLocation:
index
==
navigationShell
.
currentIndex
,
),
items:
const
[
BottomNavigationBarItem
(
icon:
Text
(
'🏠'
,
style:
TextStyle
(
fontSize:
20
)),
activeIcon:
Text
(
'🏠'
,
style:
TextStyle
(
fontSize:
22
)),
label:
'Home'
,
),
BottomNavigationBarItem
(
icon:
Text
(
'🤖'
,
style:
TextStyle
(
fontSize:
20
)),
activeIcon:
Text
(
'🤖'
,
style:
TextStyle
(
fontSize:
22
)),
label:
'Chat'
,
),
BottomNavigationBarItem
(
icon:
Text
(
'🔮'
,
style:
TextStyle
(
fontSize:
20
)),
activeIcon:
Text
(
'🔮'
,
style:
TextStyle
(
fontSize:
22
)),
label:
'Akinator'
,
),
BottomNavigationBarItem
(
icon:
Text
(
'📝'
,
style:
TextStyle
(
fontSize:
20
)),
activeIcon:
Text
(
'📝'
,
style:
TextStyle
(
fontSize:
22
)),
label:
'Quiz'
,
),
BottomNavigationBarItem
(
icon:
Text
(
'☰'
,
style:
TextStyle
(
fontSize:
20
)),
activeIcon:
Text
(
'☰'
,
style:
TextStyle
(
fontSize:
22
)),
label:
'More'
,
),
],
),
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/shared/more_menu_screen.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'package:go_router/go_router.dart'
;
import
'../core/theme/app_colors.dart'
;
import
'../core/theme/app_text_styles.dart'
;
import
'../core/theme/app_spacing.dart'
;
import
'../core/theme/app_radius.dart'
;
class
MoreMenuScreen
extends
StatelessWidget
{
const
MoreMenuScreen
({
super
.
key
});
static
const
_items
=
[
_MenuItem
(
'📚'
,
'Scenarios'
,
'Browse all scenarios'
,
'scenarios'
),
_MenuItem
(
'⚡'
,
'Generator'
,
'Generate new scenarios'
,
'generator'
),
_MenuItem
(
'🧠'
,
'Knowledge Base'
,
'Manage documents'
,
'kb'
),
_MenuItem
(
'🏆'
,
'Leaderboard'
,
'Top performers'
,
'leaderboard'
),
_MenuItem
(
'👤'
,
'Profile'
,
'Your stats & history'
,
null
),
_MenuItem
(
'⚙️'
,
'Settings'
,
'App configuration'
,
'settings'
),
];
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
body:
SafeArea
(
child:
ListView
(
padding:
AppSpacing
.
screenAll
,
children:
[
Text
(
'More'
,
style:
AppTextStyles
.
headlineLarge
),
AppSpacing
.
vXs
,
Text
(
'Additional features & settings'
,
style:
AppTextStyles
.
bodySmall
),
AppSpacing
.
vLg
,
GridView
.
builder
(
shrinkWrap:
true
,
physics:
const
NeverScrollableScrollPhysics
(),
gridDelegate:
const
SliverGridDelegateWithFixedCrossAxisCount
(
crossAxisCount:
2
,
crossAxisSpacing:
12
,
mainAxisSpacing:
12
,
childAspectRatio:
1.15
,
),
itemCount:
_items
.
length
,
itemBuilder:
(
context
,
index
)
{
final
item
=
_items
[
index
];
return
_buildTile
(
context
,
item
);
},
),
],
),
),
);
}
Widget
_buildTile
(
BuildContext
context
,
_MenuItem
item
)
{
return
Material
(
color:
AppColors
.
panel
,
borderRadius:
AppRadius
.
mdAll
,
child:
InkWell
(
onTap:
()
{
if
(
item
.
route
==
null
)
{
// Profile — navigate to dashboard's profile
context
.
go
(
'/app/dashboard/profile'
);
}
else
{
context
.
go
(
'/app/more/
${item.route}
'
);
}
},
borderRadius:
AppRadius
.
mdAll
,
child:
Container
(
padding:
const
EdgeInsets
.
all
(
16
),
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
AppColors
.
border
),
borderRadius:
AppRadius
.
mdAll
,
),
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
item
.
icon
,
style:
const
TextStyle
(
fontSize:
32
)),
const
SizedBox
(
height:
10
),
Text
(
item
.
label
,
style:
AppTextStyles
.
titleSmall
),
const
SizedBox
(
height:
4
),
Text
(
item
.
subtitle
,
style:
AppTextStyles
.
caption
,
textAlign:
TextAlign
.
center
,
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
),
],
),
),
),
);
}
}
class
_MenuItem
{
final
String
icon
;
final
String
label
;
final
String
subtitle
;
final
String
?
route
;
const
_MenuItem
(
this
.
icon
,
this
.
label
,
this
.
subtitle
,
this
.
route
);
}
\ No newline at end of file
finsim_flutter/lib/shared/placeholder_screen.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'../core/theme/app_colors.dart'
;
import
'../core/theme/app_text_styles.dart'
;
import
'../core/theme/app_spacing.dart'
;
import
'../core/theme/app_radius.dart'
;
class
PlaceholderScreen
extends
StatelessWidget
{
final
String
icon
;
final
String
title
;
final
String
subtitle
;
const
PlaceholderScreen
({
super
.
key
,
required
this
.
icon
,
required
this
.
title
,
required
this
.
subtitle
,
});
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
body:
Center
(
child:
Padding
(
padding:
AppSpacing
.
screenAll
,
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
Text
(
icon
,
style:
const
TextStyle
(
fontSize:
64
)),
AppSpacing
.
vMd
,
Text
(
title
,
style:
AppTextStyles
.
headlineMedium
),
AppSpacing
.
vSm
,
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
decoration:
BoxDecoration
(
color:
AppColors
.
accentGlow
,
border:
Border
.
all
(
color:
AppColors
.
accent
.
withOpacity
(
0.2
)),
borderRadius:
AppRadius
.
smAll
,
),
child:
Text
(
subtitle
,
style:
AppTextStyles
.
labelMedium
.
copyWith
(
color:
AppColors
.
accentLight
),
),
),
],
),
),
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/shared/settings_screen.dart
0 → 100644
View file @
7636a1d3
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'../core/config/app_config.dart'
;
import
'../core/theme/app_colors.dart'
;
import
'../core/theme/app_text_styles.dart'
;
import
'../core/theme/app_spacing.dart'
;
import
'../core/theme/app_radius.dart'
;
import
'../core/widgets/fs_button.dart'
;
import
'../features/auth/presentation/bloc/auth_cubit.dart'
;
class
SettingsScreen
extends
StatelessWidget
{
const
SettingsScreen
({
super
.
key
});
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'Settings'
)),
body:
ListView
(
padding:
AppSpacing
.
screenAll
,
children:
[
// ── Server Info ──
_SectionHeader
(
'Server Configuration'
),
AppSpacing
.
vSm
,
Container
(
padding:
AppSpacing
.
cardInner
,
decoration:
BoxDecoration
(
color:
AppColors
.
panel
,
border:
Border
.
all
(
color:
AppColors
.
border
),
borderRadius:
AppRadius
.
mdAll
,
),
child:
Row
(
children:
[
const
Icon
(
Icons
.
dns_outlined
,
color:
AppColors
.
textTertiary
,
size:
20
),
const
SizedBox
(
width:
12
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'Base URL'
,
style:
AppTextStyles
.
labelMedium
),
Text
(
AppConfig
.
baseUrl
,
style:
AppTextStyles
.
monoSmall
.
copyWith
(
color:
AppColors
.
accentLight
),
),
],
),
),
],
),
),
AppSpacing
.
vLg
,
// ── About ──
_SectionHeader
(
'About'
),
AppSpacing
.
vSm
,
Container
(
padding:
AppSpacing
.
cardInner
,
decoration:
BoxDecoration
(
color:
AppColors
.
panel
,
border:
Border
.
all
(
color:
AppColors
.
border
),
borderRadius:
AppRadius
.
mdAll
,
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
_infoRow
(
'App'
,
'
${AppConfig.appName}
v
${AppConfig.appVersion}
'
),
const
Divider
(
height:
20
),
_infoRow
(
'Platform'
,
'Flutter + FastAPI + LangGraph'
),
const
Divider
(
height:
20
),
_infoRow
(
'AI Model'
,
'LLaMA 3.3 70B (Groq)'
),
],
),
),
AppSpacing
.
vXl
,
// ── Logout ──
FSButton
(
label:
'Sign Out'
,
variant:
FSButtonVariant
.
danger
,
icon:
Icons
.
logout
,
onPressed:
()
=>
context
.
read
<
AuthCubit
>().
logout
(),
),
AppSpacing
.
vXl
,
],
),
);
}
Widget
_infoRow
(
String
label
,
String
value
)
{
return
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Text
(
label
,
style:
AppTextStyles
.
bodySmall
),
Text
(
value
,
style:
AppTextStyles
.
labelMedium
.
copyWith
(
color:
AppColors
.
textSecondary
)),
],
);
}
}
class
_SectionHeader
extends
StatelessWidget
{
final
String
title
;
const
_SectionHeader
(
this
.
title
);
@override
Widget
build
(
BuildContext
context
)
{
return
Text
(
title
.
toUpperCase
(),
style:
AppTextStyles
.
labelSmall
.
copyWith
(
color:
AppColors
.
textTertiary
,
letterSpacing:
1.5
,
),
);
}
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment