Add role selection

This commit is contained in:
vergiLgood1 2025-05-19 13:35:57 +07:00
parent 1fd17ee0af
commit 867efe0bc9
28 changed files with 17340 additions and 235 deletions

View File

@ -15,7 +15,7 @@
# DIRECT_URL="postgresql://postgres.bhfzrlgxqkbkjepvqeva:TA-SIGAP2024@aws-0-ap-southeast-1.pooler.supabase.com:5432/postgres"
# Supabase Local URL
SUPABASE_URL=http://host.docker.internal:54321
SUPABASE_URL=http://192.168.1.8:54321
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
SERVICE_ROLE_SECRET=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,10 +12,9 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: TTexts.appName,
themeMode: ThemeMode.system,
themeMode: ThemeMode.system, // This will follow system theme settings
theme: TAppTheme.lightTheme,
darkTheme: TAppTheme.darkTheme,
debugShowCheckedModeBanner: false,

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';
@ -6,9 +7,12 @@ import 'package:sigap/app.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
// Ensure that the Flutter binding is initialized before calling any Flutter
// WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
// FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
WidgetsFlutterBinding.ensureInitialized();
// Make sure status bar is properly set
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(statusBarColor: Colors.transparent),
);
// Load environment variables from the .env file
await dotenv.load(fileName: ".env");
@ -37,4 +41,3 @@ Future<void> main() async {
runApp(const App());
}

View File

@ -26,6 +26,9 @@ class AnimatedSplashScreenWidget extends StatelessWidget {
splash: Center(
child: Lottie.asset(
isDark ? TImages.darkSplashApp : TImages.lightSplashApp,
frameRate: FrameRate.max,
repeat: true,
),
),
splashIconSize: 300,

View File

@ -9,6 +9,6 @@ class UtilityBindings extends Bindings {
void dependencies() {
// Get.put(BackgroundService.instance, permanent: true);
Get.put(NetworkManager());
Get.put(logger);
Get.put(logger, permanent: true);
}
}

View File

@ -10,7 +10,7 @@ class RolesRepository extends GetxController {
static RolesRepository get instance => Get.find();
final _supabase = SupabaseService.instance.client;
final _logger = Get.find<Logger>();
// final Logger() = Get.find<Logger>();
// Get all roles
Future<List<RoleModel>> getAllRoles() async {
@ -22,10 +22,10 @@ class RolesRepository extends GetxController {
return (roles as List).map((role) => RoleModel.fromJson(role)).toList();
} on PostgrestException catch (error) {
_logger.e('PostgrestException in getAllRoles: ${error.message}');
Logger().e('PostgrestException in getAllRoles: ${error.message}');
throw TExceptions.fromCode(error.code ?? 'unknown-error');
} catch (e) {
_logger.e('Exception in getAllRoles: $e');
Logger().e('Exception in getAllRoles: $e');
throw 'Failed to fetch roles: ${e.toString()}';
}
}
@ -42,10 +42,10 @@ class RolesRepository extends GetxController {
return RoleModel.fromJson(role);
} on PostgrestException catch (error) {
_logger.e('PostgrestException in getRoleById: ${error.message}');
Logger().e('PostgrestException in getRoleById: ${error.message}');
throw TExceptions.fromCode(error.code ?? 'unknown-error');
} catch (e) {
_logger.e('Exception in getRoleById: $e');
Logger().e('Exception in getRoleById: $e');
throw 'Failed to fetch role data: ${e.toString()}';
}
}
@ -62,10 +62,10 @@ class RolesRepository extends GetxController {
return RoleModel.fromJson(role);
} on PostgrestException catch (error) {
_logger.e('PostgrestException in getRoleByName: ${error.message}');
Logger().e('PostgrestException in getRoleByName: ${error.message}');
throw TExceptions.fromCode(error.code ?? 'unknown-error');
} catch (e) {
_logger.e('Exception in getRoleByName: $e');
Logger().e('Exception in getRoleByName: $e');
throw 'Failed to fetch role data: ${e.toString()}';
}
}
@ -84,10 +84,10 @@ class RolesRepository extends GetxController {
return RoleModel.fromJson(createdRole);
} on PostgrestException catch (error) {
_logger.e('PostgrestException in createRole: ${error.message}');
Logger().e('PostgrestException in createRole: ${error.message}');
throw TExceptions.fromCode(error.code ?? 'unknown-error');
} catch (e) {
_logger.e('Exception in createRole: $e');
Logger().e('Exception in createRole: $e');
throw 'Failed to create role: ${e.toString()}';
}
}
@ -105,10 +105,10 @@ class RolesRepository extends GetxController {
return RoleModel.fromJson(updatedRole);
} on PostgrestException catch (error) {
_logger.e('PostgrestException in updateRole: ${error.message}');
Logger().e('PostgrestException in updateRole: ${error.message}');
throw TExceptions.fromCode(error.code ?? 'unknown-error');
} catch (e) {
_logger.e('Exception in updateRole: $e');
Logger().e('Exception in updateRole: $e');
throw 'Failed to update role: ${e.toString()}';
}
}
@ -125,10 +125,10 @@ class RolesRepository extends GetxController {
.map((permission) => PermissionModel.fromJson(permission))
.toList();
} on PostgrestException catch (error) {
_logger.e('PostgrestException in getRolePermissions: ${error.message}');
Logger().e('PostgrestException in getRolePermissions: ${error.message}');
throw TExceptions.fromCode(error.code ?? 'unknown-error');
} catch (e) {
_logger.e('Exception in getRolePermissions: $e');
Logger().e('Exception in getRolePermissions: $e');
throw 'Failed to fetch role permissions: ${e.toString()}';
}
}

View File

@ -2,8 +2,10 @@ import 'package:get/get.dart';
import 'package:sigap/src/features/auth/screens/forgot-password/forgot_password.dart';
import 'package:sigap/src/features/auth/screens/signin/signin_screen.dart';
import 'package:sigap/src/features/auth/screens/signup/signup_screen.dart';
import 'package:sigap/src/features/auth/screens/step-form/step_form_screen.dart';
import 'package:sigap/src/features/onboarding/screens/onboarding/onboarding_screen.dart';
import 'package:sigap/src/features/onboarding/screens/role-selection/role_selection_screen.dart';
import 'package:sigap/src/features/onboarding/screens/welcome/welcome_screen.dart';
import 'package:sigap/src/utils/constants/app_routes.dart';
class AppPages {
@ -13,12 +15,19 @@ class AppPages {
// Onboarding
GetPage(name: AppRoutes.onboarding, page: () => const OnboardingScreen()),
GetPage(name: AppRoutes.welcome, page: () => const WelcomeScreen()),
// Auth
GetPage(
name: AppRoutes.roleSelection,
page: () => const RoleSelectionScreen(),
),
// Auth
GetPage(
name: AppRoutes.formRegistration,
page: () => const FormRegistrationScreen(),
),
GetPage(name: AppRoutes.signIn, page: () => const SignInScreen()),
GetPage(name: AppRoutes.signUp, page: () => const SignUpScreen()),
@ -27,5 +36,9 @@ class AppPages {
name: AppRoutes.forgotPassword,
page: () => const ForgotPasswordScreen(),
),
];
}

View File

@ -10,8 +10,8 @@ import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/validators/validation.dart';
class StepFormScreen extends StatelessWidget {
const StepFormScreen({super.key});
class FormRegistrationScreen extends StatelessWidget {
const FormRegistrationScreen({super.key});
@override
Widget build(BuildContext context) {

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:sigap/src/utils/constants/colors.dart';
class AuthHeader extends StatelessWidget {
final String title;
@ -14,16 +13,12 @@ class AuthHeader extends StatelessWidget {
children: [
Text(
title,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: TColors.textPrimary,
),
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8),
Text(
subtitle,
style: TextStyle(fontSize: 16, color: TColors.textSecondary),
style: Theme.of(context).textTheme.titleMedium
),
const SizedBox(height: 32),
],

View File

@ -1,4 +1,5 @@
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:sigap/src/cores/repositories/personalization/roles_repository.dart';
import 'package:sigap/src/features/personalization/models/models/roles_model.dart';
import 'package:sigap/src/utils/constants/app_routes.dart';
@ -20,16 +21,31 @@ class RoleSelectionController extends GetxController {
final RxBool isLoading = false.obs;
final RxBool isOfficer = false.obs;
// Flag to track role loading state
final isRolesLoading = true.obs;
@override
void onInit() {
super.onInit();
fetchRoles();
}
// Method to return placeholder roles
RoleModel getPlaceholderRole(int id) {
return RoleModel(
id: id.toString(),
name: "Loading...",
description: "Role information is loading",
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}
// Fetch available roles from repository
Future<void> fetchRoles() async {
try {
isLoading.value = true;
isRolesLoading.value = true;
// Get all roles from repository
final allRoles = await _rolesRepository.getAllRoles();
@ -50,6 +66,7 @@ class RoleSelectionController extends GetxController {
);
} finally {
isLoading.value = false;
isRolesLoading.value = false;
}
}
@ -71,8 +88,10 @@ class RoleSelectionController extends GetxController {
try {
isLoading.value = true;
Logger().i('Selected role: ${selectedRole.value?.name}');
// Check if the selected role is officer
if (selectedRole.value!.name.toLowerCase() == 'officer') {
if (selectedRole.value?.name.toLowerCase() == 'officer') {
isOfficer.value = true;
} else {
isOfficer.value = false;
@ -89,6 +108,7 @@ class RoleSelectionController extends GetxController {
message:
'An error occurred while selecting the role. Please try again.',
);
Logger().e('Error in continueWithRole: $e');
} finally {
isLoading.value = false;
}

View File

@ -1,5 +1,6 @@
import 'package:sigap/src/features/onboarding/models/onboarding_model.dart';
final List<OnboardingContent> contents = [
OnboardingContent(
title: 'Crime Mapping & Analysis',

View File

@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart';
import 'package:sigap/src/features/onboarding/datas/onboarding_data.dart';
import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.dart';
import 'package:sigap/src/utils/helpers/helper_functions.dart';
import '../../controllers/onboarding_controller.dart';
@ -16,22 +15,14 @@ class OnboardingScreen extends StatelessWidget {
// Get the controller
final controller = Get.find<OnboardingController>();
bool isDark = THelperFunctions.isDarkMode(context);
// Set system overlay style
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
),
);
// Get screen dimensions for responsive design
final size = MediaQuery.of(context).size;
final isSmallScreen = size.height < 700;
// Get Theme.of(context) data to apply consistent styling
bool isDark = THelperFunctions.isDarkMode(context);
return Scaffold(
backgroundColor: isDark ? TColors.dark : TColors.light,
body: SafeArea(
child: Column(
children: [
@ -39,16 +30,12 @@ class OnboardingScreen extends StatelessWidget {
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(TSizes.md),
child: TextButton(
onPressed: controller.skipToWelcomeScreen,
child: Text(
'Skip',
style: TextStyle(
color: TColors.textSecondary,
fontSize: 16,
fontWeight: FontWeight.w500,
),
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
@ -63,7 +50,7 @@ class OnboardingScreen extends StatelessWidget {
onPageChanged: controller.onPageChanged,
itemBuilder: (_, i) {
return Padding(
padding: const EdgeInsets.all(24.0),
padding: const EdgeInsets.all(TSizes.lg),
child: Column(
children: [
// Animated image
@ -77,12 +64,20 @@ class OnboardingScreen extends StatelessWidget {
contents[i].image,
fit: BoxFit.contain,
height: isSmallScreen ? 200 : 300,
// Make SVG adapt to dark Theme.of(context) if needed
colorFilter:
isDark
? const ColorFilter.mode(
Colors.white,
BlendMode.srcIn,
)
: null,
),
),
),
),
SizedBox(height: isSmallScreen ? 16 : 32),
SizedBox(height: isSmallScreen ? TSizes.md : TSizes.xl),
// Animated title
FadeTransition(
@ -92,16 +87,12 @@ class OnboardingScreen extends StatelessWidget {
child: Text(
contents[i].title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: isSmallScreen ? 22 : 28,
fontWeight: FontWeight.bold,
color: TColors.textPrimary,
),
style: Theme.of(context).textTheme.headlineMedium,
),
),
),
SizedBox(height: isSmallScreen ? 12 : 20),
SizedBox(height: isSmallScreen ? TSizes.sm : TSizes.lg),
// Animated description
FadeTransition(
@ -111,11 +102,7 @@ class OnboardingScreen extends StatelessWidget {
child: Text(
contents[i].description,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
color: TColors.textSecondary,
height: 1.5,
),
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
@ -137,37 +124,40 @@ class OnboardingScreen extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
contents.length,
(index) =>
buildDot(index, controller.currentIndex.value),
(index) => buildDot(
index,
controller.currentIndex.value,
Theme.of(context).primaryColor,
Theme.of(context).disabledColor,
),
),
),
),
const Spacer(),
// Next or Get Started button
// Next button
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 16.0,
horizontal: TSizes.lg,
vertical: TSizes.md,
),
child: Align(
alignment: Alignment.centerRight,
child: Container(
margin: const EdgeInsets.only(right: 8.0),
margin: const EdgeInsets.only(right: TSizes.sm),
child: FloatingActionButton(
onPressed: controller.nextPage,
backgroundColor: TColors.primary,
elevation: 2,
child: const Icon(
backgroundColor: Theme.of(context).primaryColor,
elevation: TSizes.cardElevation,
child: Icon(
Icons.chevron_right,
color: Colors.white,
size: 30,
),
color: Theme.of(context).colorScheme.onPrimary,
size: TSizes.iconLg - 2,
),
),
),
),
),
],
),
@ -179,15 +169,20 @@ class OnboardingScreen extends StatelessWidget {
}
// Animated dot indicator
Widget buildDot(int index, int currentIndex) {
Widget buildDot(
int index,
int currentIndex,
Color activeColor,
Color inactiveColor,
) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.only(right: 8),
height: 8,
width: currentIndex == index ? 24 : 8,
margin: const EdgeInsets.only(right: TSizes.sm),
height: TSizes.xs * 2,
width: currentIndex == index ? TSizes.md + 8 : TSizes.xs * 2,
decoration: BoxDecoration(
color: currentIndex == index ? TColors.primary : TColors.grey,
borderRadius: BorderRadius.circular(4),
color: currentIndex == index ? activeColor : inactiveColor,
borderRadius: BorderRadius.circular(TSizes.xs),
),
);
}

View File

@ -5,7 +5,8 @@ import 'package:sigap/src/features/auth/screens/widgets/auth_button.dart';
import 'package:sigap/src/features/auth/screens/widgets/auth_header.dart';
import 'package:sigap/src/features/onboarding/controllers/role_selection_controller.dart';
import 'package:sigap/src/features/onboarding/screens/role-selection/widgets/role_card.dart';
import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.dart';
import 'package:sigap/src/utils/loaders/shimmer.dart';
class RoleSelectionScreen extends StatelessWidget {
const RoleSelectionScreen({super.key});
@ -15,6 +16,9 @@ class RoleSelectionScreen extends StatelessWidget {
// Get the controller
final controller = Get.find<RoleSelectionController>();
// Get theme
final theme = Theme.of(context);
// Set system overlay style
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
@ -24,22 +28,19 @@ class RoleSelectionScreen extends StatelessWidget {
);
return Scaffold(
backgroundColor: TColors.light,
backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar(
backgroundColor: Colors.transparent,
backgroundColor: theme.appBarTheme.backgroundColor,
elevation: 0,
title: Text(
'Choose Role',
style: TextStyle(
color: TColors.textPrimary,
fontWeight: FontWeight.bold,
),
style: Theme.of(context).appBarTheme.titleTextStyle,
),
centerTitle: true,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
padding: const EdgeInsets.all(TSizes.defaultSpace),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -51,22 +52,89 @@ class RoleSelectionScreen extends StatelessWidget {
// Role list
Expanded(
child: Obx(
() => ListView.builder(
itemCount: controller.roles.length,
itemBuilder: (context, index) {
final role = controller.roles[index];
return Obx(
() => RoleCard(
role: role,
isSelected:
controller.selectedRole.value?.id == role.id,
onTap: () => controller.selectRole(role),
),
);
},
),
),
child: Obx(() {
// Check if roles are loading
if (controller.isRolesLoading.value) {
// Show shimmer placeholder cards
return ListView.builder(
itemCount: controller.roles.length,
itemBuilder:
(_, __) => Padding(
padding: const EdgeInsets.only(
bottom: TSizes.spaceBtwItems,
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
TSizes.borderRadiusMd,
),
border: Border.all(color: theme.dividerColor),
),
child: Padding(
padding: const EdgeInsets.all(TSizes.md),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icon shimmer
const TShimmerEffect(
width: 40,
height: 40,
radius: TSizes.borderRadiusSm,
),
const SizedBox(width: TSizes.md),
// Text shimmer
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
// Role title shimmer
const TShimmerEffect(
width: 150,
height: 24,
radius: TSizes.borderRadiusXs,
),
const SizedBox(height: TSizes.sm),
// Role description shimmer
const TShimmerEffect(
width: double.infinity,
height: 16,
radius: TSizes.borderRadiusXs,
),
const SizedBox(height: TSizes.xs),
const TShimmerEffect(
width: 200,
height: 16,
radius: TSizes.borderRadiusXs,
),
],
),
),
],
),
),
),
),
);
} else {
// Show dynamic cards from database
return ListView.builder(
itemCount: controller.roles.length,
itemBuilder: (context, index) {
final role = controller.roles[index];
return Obx(
() => RoleCard(
role: role,
isSelected:
controller.selectedRole.value?.id == role.id,
onTap: () => controller.selectRole(role),
),
);
},
);
}
}),
),
// Continue button

View File

@ -1,80 +1,87 @@
import 'package:flutter/material.dart';
import 'package:sigap/src/features/personalization/models/models/roles_model.dart';
import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.dart';
class RoleCard extends StatelessWidget {
final RoleModel role;
final bool isSelected;
final VoidCallback onTap;
final IconData? icon; // Add optional icon parameter
const RoleCard({
super.key,
required this.role,
required this.isSelected,
required this.onTap,
this.icon, // Optional icon parameter
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// Use provided icon or fall back to role.iconData
final IconData displayIcon = icon ?? role.iconData;
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: TSizes.spaceBtwItems),
padding: const EdgeInsets.all(TSizes.md),
decoration: BoxDecoration(
color:
isSelected
? TColors.primary.withOpacity(0.1)
: TColors.lightContainer,
borderRadius: BorderRadius.circular(16),
? theme.primaryColor.withOpacity(0.1)
: theme.cardColor,
borderRadius: BorderRadius.circular(TSizes.cardRadiusMd),
border: Border.all(
color: isSelected ? TColors.primary : TColors.borderPrimary,
color: isSelected ? theme.primaryColor : theme.dividerColor,
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Container(
width: 56,
height: 56,
width: TSizes.imageThumbSize - 24,
height: TSizes.imageThumbSize - 24,
decoration: BoxDecoration(
color: isSelected ? TColors.primary : TColors.secondary,
borderRadius: BorderRadius.circular(12),
color: isSelected ? theme.primaryColor : theme.cardColor,
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
),
child: Icon(
role.iconData,
color: isSelected ? TColors.white : TColors.primary,
size: 28,
displayIcon, // Use the display icon
color:
isSelected
? theme.colorScheme.onPrimary
: theme.primaryColor,
size: TSizes.iconLg - 4,
),
),
const SizedBox(width: 16),
const SizedBox(width: TSizes.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
role.name,
style: TextStyle(
fontSize: 18,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: TColors.textPrimary,
),
),
const SizedBox(height: 4),
const SizedBox(height: TSizes.xs),
Text(
role.description!,
style: TextStyle(
fontSize: 14,
color: TColors.textSecondary,
),
style: theme.textTheme.bodyMedium,
),
],
),
),
Icon(
isSelected ? Icons.check_circle : Icons.circle_outlined,
color: isSelected ? TColors.primary : TColors.textSecondary,
size: 24,
color:
isSelected
? theme.primaryColor
: theme.textTheme.bodySmall?.color,
size: TSizes.iconMd,
),
],
),

View File

@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import 'package:sigap/src/features/onboarding/controllers/onboarding_controller.dart';
import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/image_strings.dart';
import 'package:sigap/src/utils/constants/sizes.dart';
import 'package:sigap/src/utils/helpers/helper_functions.dart';
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
@ -10,89 +13,96 @@ class WelcomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.find<OnboardingController>();
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
),
);
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
final isSmallScreen = size.height < 700;
bool isDark = THelperFunctions.isDarkMode(context);
return Scaffold(
backgroundColor: TColors.light,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
padding: const EdgeInsets.all(TSizes.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 32),
const SizedBox(height: TSizes.xl),
// Logo section
Hero(
tag: 'app_logo',
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: TColors.primary.withOpacity(0.1),
color: theme.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.location_on,
size: 50,
color: TColors.primary,
child: SvgPicture.asset(
isDark ? TImages.darkAppBgLogo : TImages.lightAppBgLogo,
fit: BoxFit.contain,
height: isSmallScreen ? 60 : 80,
),
),
),
SizedBox(height: isSmallScreen ? 24 : 40),
SizedBox(height: isSmallScreen ? TSizes.lg : TSizes.xl + 8),
// Title and description
Text(
'Welcome to GIS Crime Cluster',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: isSmallScreen ? 24 : 28,
fontWeight: FontWeight.bold,
color: TColors.textPrimary,
),
style: theme.textTheme.headlineMedium,
),
SizedBox(height: isSmallScreen ? 12 : 16),
SizedBox(height: isSmallScreen ? TSizes.sm : TSizes.md),
Text(
'Visualize crime patterns and enhance safety across Jember Regency with our data-driven GIS application',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
color: TColors.textSecondary,
height: 1.5,
style: theme.textTheme.bodyMedium?.copyWith(height: 1.5),
),
// Top flexible space
const Spacer(flex: 1),
// Map image - now positioned in the middle
FadeTransition(
opacity: controller.fadeAnimation,
child: Lottie.asset(
isDark
? TImages.worldMapMarqueeDark
: TImages.worldMapMarquee,
fit: BoxFit.contain,
height: isSmallScreen ? 160 : 200,
repeat: true,
animate: true,
frameRate: FrameRate(60),
),
),
const Spacer(),
// Bottom flexible space
const Spacer(flex: 1),
// Get Started button
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: controller.getStarted,
style: ElevatedButton.styleFrom(
backgroundColor: TColors.primary,
foregroundColor: TColors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 0,
),
child: const Text(
style: theme.elevatedButtonTheme.style,
child: Text(
'Get Started',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.bold,
),
),
),
),
SizedBox(height: isSmallScreen ? 24 : 32),
SizedBox(height: isSmallScreen ? TSizes.lg : TSizes.xl),
Text(
'By continuing, you agree to our Terms of Service and Privacy Policy',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12, color: TColors.textSecondary),
style: theme.textTheme.labelMedium,
),
const SizedBox(height: 16),
const SizedBox(height: TSizes.md),
],
),
),

View File

@ -116,6 +116,10 @@ class TImages {
static const String comingSoon2 =
"assets/images/animations/coming-soon2.json";
static const String loader = "assets/images/animations/loader.json";
static const String worldMapMarquee =
"assets/images/animations/Animation - 1747602248003.json";
static const String worldMapMarqueeDark =
"assets/images/animations/world-map-dark.json";
// static const String characterExplore2 =
// "assets/images/animations/char_explore2.json";
}

View File

@ -37,6 +37,7 @@ class TSizes {
static const double spaceBtwSections = 32.0;
// Border radius
static const double borderRadiusXs = 2.0;
static const double borderRadiusSm = 4.0;
static const double borderRadiusMd = 8.0;
static const double borderRadiusLg = 12.0;

View File

@ -18,6 +18,7 @@ import location
import path_provider_foundation
import shared_preferences_foundation
import url_launcher_macos
import webview_flutter_wkwebview
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
@ -33,4 +34,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
}

View File

@ -1586,6 +1586,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webview_flutter:
dependency: "direct main"
description:
name: webview_flutter
sha256: "62d763c27ce7f6cef04b3bec01c85a28d60149bffd155884aa4b8fd4941ea2e4"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: "5ece28a317a9f76ad5ee17c78dbacc8a491687cec85ee19c1643761bf8d678ef"
url: "https://pub.dev"
source: hosted
version: "4.6.0"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "7cb32b21825bd65569665c32bb00a34ded5779786d6201f5350979d2d529940d"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "6af7d1908c9c89311c2dffcc2c9b51b88a6f055ba16fa0aa8a04cbb1c3afc9ce"
url: "https://pub.dev"
source: hosted
version: "3.21.0"
win32:
dependency: transitive
description:

View File

@ -54,6 +54,7 @@ dependencies:
dropdown_search:
dotted_border:
flutter_svg: ^2.1.0
webview_flutter: ^4.12.0
# --- Input & Forms ---
flutter_otp_text_field:

View File

@ -0,0 +1,93 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
import createGlobe from 'cobe';
import { cn } from '../_lib/utils';
interface EarthProps {
className?: string;
theta?: number;
dark?: number;
scale?: number;
diffuse?: number;
mapSamples?: number;
mapBrightness?: number;
baseColor?: [number, number, number];
markerColor?: [number, number, number];
glowColor?: [number, number, number];
}
const Earth: React.FC<EarthProps> = ({
className,
theta = 0.25,
dark = 1,
scale = 1.1,
diffuse = 1.2,
mapSamples = 40000,
mapBrightness = 6,
baseColor = [0.4, 0.6509, 1],
markerColor = [1, 0, 0],
glowColor = [0.2745, 0.5765, 0.898],
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
let width = 0;
const onResize = () =>
canvasRef.current && (width = canvasRef.current.offsetWidth);
window.addEventListener('resize', onResize);
onResize();
let phi = 0;
onResize();
const globe = createGlobe(canvasRef.current!, {
devicePixelRatio: 2,
width: width * 2,
height: width * 2,
phi: 0,
theta: theta,
dark: dark,
scale: scale,
diffuse: diffuse,
mapSamples: mapSamples,
mapBrightness: mapBrightness,
baseColor: baseColor,
markerColor: markerColor,
glowColor: glowColor,
opacity: 1,
offset: [0, 0],
markers: [
// longitude latitude
],
onRender: (state: Record<string, any>) => {
// Called on every animation frame.
// `state` will be an empty object, return updated params.\
state.phi = phi;
phi += 0.003;
},
});
return () => {
globe.destroy();
};
}, []);
return (
<div
className={cn(
'flex items-center justify-center z-[10] w-full max-w-[350px] mx-auto',
className
)}
>
<canvas
ref={canvasRef}
style={{
width: '100%',
height: '100%',
maxWidth: '100%',
aspectRatio: '1',
}}
/>
</div>
);
};
export default Earth;

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,8 @@
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",
"@react-three/drei": "^9.122.0",
"@react-three/fiber": "^8.18.0",
"@sentry/nextjs": "^9.5.0",
"@supabase/ssr": "^0.4.1",
"@supabase/supabase-js": "latest",
@ -47,16 +49,17 @@
"autoprefixer": "10.4.20",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cobe": "^0.6.3",
"csv-parse": "^5.6.0",
"csv-parser": "^3.2.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.5.2",
"framer-motion": "^12.12.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.468.0",
"mapbox-gl": "^3.11.0",
"mapbox-gl-animated-popup": "^0.4.0",
"ml-kmeans": "^6.0.0",
"motion": "^12.4.7",
"next": "latest",
"next-themes": "^0.4.4",
"prettier": "^3.3.3",
@ -69,6 +72,7 @@
"resend": "^4.1.2",
"sonner": "^2.0.1",
"three": "^0.176.0",
"three-globe": "^2.42.4",
"threebox-plugin": "^2.2.7",
"uuid": "^11.1.0",
"vaul": "^1.1.2",
@ -90,7 +94,7 @@
"postgres": "^3.4.5",
"prisma": "^6.4.1",
"react-email": "3.0.7",
"tailwind-merge": "^2.5.2",
"tailwind-merge": "^2.6.0",
"tailwindcss": "3.4.17",
"tailwindcss-animate": "^1.0.7",
"ts-node": "^10.9.2",

File diff suppressed because it is too large Load Diff