feat: Refactor code structure and optimize performance across multiple modules

This commit is contained in:
Achintha Isuru
2025-11-17 23:29:28 -05:00
parent 831570f2e0
commit a64cbd9edf
1508 changed files with 105319 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:injectable/injectable.dart';
import 'package:path_provider/path_provider.dart';
class GraphQLConfig {
static HttpLink httpLink = HttpLink(
dotenv.env['BASE_URL']!,
defaultHeaders: {
'Content-Type': 'application/json',
},
);
static AuthLink authLink = AuthLink(
getToken: () async {
User? user = FirebaseAuth.instance.currentUser;
if (user != null) {
return 'Bearer ${await user.getIdToken()}';
}
return null;
},
);
static Link link = authLink.concat(httpLink);
Future<GraphQLClient> configClient() async {
var path = await getApplicationDocumentsDirectory();
final store = await HiveStore.open(path: '${path.path}/gql_cache');
return GraphQLClient(
link: link,
cache: GraphQLCache(store: store, ),
defaultPolicies: DefaultPolicies(
mutate: Policies(
cacheReread: CacheRereadPolicy.ignoreOptimisitic,
error: ErrorPolicy.all,
fetch: FetchPolicy.networkOnly,
),
query: Policies(
cacheReread: CacheRereadPolicy.ignoreOptimisitic,
error: ErrorPolicy.all,
fetch: FetchPolicy.networkOnly,
),
));
}
}
@Singleton()
class ApiClient {
static final GraphQLConfig _graphQLConfig = GraphQLConfig();
final Future<GraphQLClient> _client = _graphQLConfig.configClient();
Future<QueryResult> query(
{required String schema, Map<String, dynamic>? body}) async {
final QueryOptions options = QueryOptions(
document: gql(schema),
variables: body ?? {},
);
return (await _client).query(options).timeout(Duration(seconds: 30));
}
Stream<QueryResult?> queryWithCache({
required String schema,
Map<String, dynamic>? body,
}) async* {
final WatchQueryOptions options = WatchQueryOptions(
document: gql(schema),
variables: body ?? {},
fetchPolicy: FetchPolicy.cacheAndNetwork);
var result = (await _client).watchQuery(options).fetchResults();
yield result.eagerResult;
yield await result.networkResult;
}
Future<QueryResult> mutate(
{required String schema, Map<String, dynamic>? body}) async {
final MutationOptions options = MutationOptions(
document: gql(schema),
variables: body ?? {},
);
return (await _client).mutate(options);
}
void dropCache() async {
(await _client).cache.store.reset();
}
}

View File

@@ -0,0 +1,54 @@
import 'package:graphql_flutter/graphql_flutter.dart';
interface class DisplayableException {
final String message;
DisplayableException(this.message);
}
class NetworkException implements Exception, DisplayableException {
@override
final String message;
NetworkException([this.message = 'No internet connection']);
@override
String toString() {
return message;
}
}
class GraphQLException implements Exception, DisplayableException {
@override
final String message;
GraphQLException(this.message);
@override
String toString() {
return message;
}
}
class UnknownException implements Exception, DisplayableException {
@override
final String message;
UnknownException([this.message = 'Something went wrong']);
@override
String toString() {
return message;
}
}
Exception parseBackendError(dynamic error) {
if (error is OperationException) {
if (error.graphqlErrors.isNotEmpty) {
return GraphQLException(
error.graphqlErrors.firstOrNull?.message ?? 'GraphQL error occurred');
}
return NetworkException('Network error occurred');
}
return UnknownException();
}

View File

@@ -0,0 +1,27 @@
extension IntExtensions on int {
String toOrdinal() {
if (this >= 11 && this <= 13) {
return '${this}th';
}
switch (this % 10) {
case 1:
return '${this}st';
case 2:
return '${this}nd';
case 3:
return '${this}rd';
default:
return '${this}th';
}
}
String getWeekdayId() => switch (this) {
0 => 'Monday',
1 => 'Tuesday',
2 => 'Wednesday',
3 => 'Thursday',
4 => 'Friday',
5 => 'Saturday',
_ => 'Sunday',
};
}

View File

@@ -0,0 +1,27 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
class Logger {
static void error(e, {String? message}) {
log(
e.toString(),
error: message ?? e,
level: 2000,
stackTrace: StackTrace.current,
name: 'ERROR',
);
}
static debug(dynamic msg, {String? message}) {
if (kDebugMode) {
log(
msg.toString(),
name: message ?? msg.runtimeType.toString(),
);
}
}
}
const error = Logger.error;
const debug = Logger.debug;

View File

@@ -0,0 +1,6 @@
extension StringExtensions on String {
String capitalize() {
if (isEmpty) return this;
return this[0].toUpperCase() + substring(1).toLowerCase();
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/services.dart';
class DateTextFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) {
if (oldValue.text.length >= newValue.text.length) {
return newValue;
}
String filteredText = newValue.text.replaceAll(RegExp(r'[^0-9.]'), '');
var dateText = _addSeparators(filteredText, '.');
return newValue.copyWith(
text: dateText, selection: updateCursorPosition(dateText));
}
String _addSeparators(String value, String separator) {
value = value.replaceAll('.', '');
var newString = '';
for (int i = 0; i < value.length; i++) {
newString += value[i];
if (i == 1) {
newString += separator;
}
if (i == 3) {
newString += separator;
}
}
return newString;
}
TextSelection updateCursorPosition(String text) {
return TextSelection.fromPosition(TextPosition(offset: text.length));
}
}

View File

@@ -0,0 +1,15 @@
abstract class EmailValidator {
static String? validate(String? email, {bool isRequired = true}) {
// Regular expression for validating emails
final RegExp phoneRegExp = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
if (isRequired && (email == null || email.isEmpty)) {
return 'Email is required';
} else if (email != null && !phoneRegExp.hasMatch(email)) {
return 'Invalid email';
}
return null;
}
}

View File

@@ -0,0 +1,13 @@
abstract class PhoneValidator {
static String? validate(String? phoneNumber, {bool isRequired = true}) {
// Regular expression for validating phone numbers
final RegExp phoneRegExp = RegExp(r'^\+?[1-9]\d{1,14}$');
if (isRequired && (phoneNumber==null || phoneNumber.isEmpty)) {
return 'Phone number is required';
} else if (phoneNumber !=null && !phoneRegExp.hasMatch(phoneNumber)) {
return 'Invalid phone number';
}
return null;
}
}

View File

@@ -0,0 +1,19 @@
class SkillExpValidator {
SkillExpValidator._();
static String? validate(String value) {
if (value.isEmpty) {
return 'Experience is required';
}
var parsed = int.tryParse(value);
if (value.isNotEmpty && parsed == null) {
return 'Experience must be a number';
}
if (value.isNotEmpty && parsed! > 20 || parsed! < 1) {
return 'Experience must be between 1 and 20';
}
return null;
}
}

View File

@@ -0,0 +1,9 @@
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injectable.config.dart';
final getIt = GetIt.instance;
@InjectableInit()
void configureDependencies(String env) => GetIt.instance.init(environment: env);

View File

@@ -0,0 +1,41 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/sevices/auth_state_service/auth_service.dart';
class SplashRedirectGuard extends AutoRouteGuard {
final AuthService authService;
SplashRedirectGuard(this.authService);
@override
Future<void> onNavigation(
NavigationResolver resolver, StackRouter router) async {
final status = await authService.getAuthStatus();
switch (status) {
case AuthStatus.authenticated:
router.replace(const HomeRoute());
break;
case AuthStatus.adminValidation:
// router.replace(const WaitingValidationRoute());
break;
case AuthStatus.prepareProfile:
if(kDebugMode) {
// router.replace(const HomeRoute());
}else {
// router.replace(const CheckListFlowRoute());
}
break;
case AuthStatus.unauthenticated:
// router.replace(const HomeRoute());
router.replace( const SignInFlowRoute());
break;
case AuthStatus.error:
//todo(Artem):show error screen
router.replace( const SignInFlowRoute());
}
// resolver.next(false);
// resolver.next(true);
}
}

View File

@@ -0,0 +1,122 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/application/routing/guards.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/sevices/auth_state_service/auth_service.dart';
@AutoRouterConfig(
replaceInRouteName: 'Screen|Page,Route',
)
class AppRouter extends RootStackRouter {
@override
RouteType get defaultRouteType => const RouteType.material();
Future<DeepLink> deepLinkBuilder(PlatformDeepLink deepLink) async {
final uri = deepLink.uri;
if (uri.queryParameters.containsKey('oobCode')) {
final code = uri.queryParameters['oobCode'];
popUntil((route) => false);
return DeepLink([
const WelcomeRoute(),
const SignInRoute(),
EnterNewPassRoute(code: code),
]);
}
return deepLink;
}
@override
List<AutoRoute> get routes => [
AdaptiveRoute(
path: '/',
page: SplashRoute.page,
initial: true,
guards: [SplashRedirectGuard(getIt<AuthService>())],
),
createEventFlow,
authFlow,
homeFlow,
];
get authFlow => AdaptiveRoute(
path: '/auth',
page: SignInFlowRoute.page,
children: [
AutoRoute(path: 'welcome', page: WelcomeRoute.page, initial: true),
AutoRoute(
path: 'sign_in',
page: SignInRoute.page,
),
AutoRoute(
path: 'reset_pass',
page: ResetPassRoute.page,
),
AutoRoute(
path: 'pass',
page: EnterNewPassRoute.page,
),
],
);
get createEventFlow =>
AutoRoute(path: '/create', page: CreateEventFlowRoute.page, children: [
AutoRoute(path: 'edit', page: CreateEventRoute.page, initial: true),
AutoRoute(
path: 'preview',
page: EventDetailsRoute.page,
),
]);
get homeFlow => AdaptiveRoute(
path: '/home',
page: HomeRoute.page,
children: [eventsFlow, invoiceFlow, notificationsFlow, profileFlow]);
get eventsFlow =>
AdaptiveRoute(path: 'events', page: EventsFlowRoute.page, children: [
AutoRoute(path: 'list', page: EventsListMainRoute.page, initial: true),
AutoRoute(path: 'details', page: EventDetailsRoute.page),
AutoRoute(path: 'assigned', page: AssignedStaffRoute.page),
AutoRoute(path: 'assigned_manual', page: ClockManualRoute.page),
AutoRoute(path: 'rate_staff', page: RateStaffRoute.page),
]);
get invoiceFlow =>
AdaptiveRoute(path: 'invoice', page: InvoiceFlowRoute.page, children: [
AutoRoute(
path: 'list', page: InvoicesListMainRoute.page, initial: true),
AutoRoute(
path: 'details', page: InvoiceDetailsRoute.page),
AutoRoute(
path: 'event',
page: EventDetailsRoute.page,
),
]);
get notificationsFlow => AdaptiveRoute(
path: 'notifications',
page: NotificationFlowRoute.page,
children: [
AutoRoute(
path: 'list', page: NotificationsListRoute.page, initial: true),
AutoRoute(
path: 'details',
page: NotificationDetailsRoute.page,
),
]);
get profileFlow =>
AdaptiveRoute(path: 'profile', page: ProfileFlowRoute.page, children: [
AutoRoute(
path: 'preview', page: ProfilePreviewRoute.page, initial: true),
AutoRoute(
path: 'edit',
page: PersonalInfoRoute.page,
),
]);
@override
List<AutoRouteGuard> get guards => [];
}