Add role selection
This commit is contained in:
parent
1fd17ee0af
commit
867efe0bc9
|
@ -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
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
|
||||
|
||||
|
||||
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:sigap/src/features/onboarding/models/onboarding_model.dart';
|
||||
|
||||
|
||||
final List<OnboardingContent> contents = [
|
||||
OnboardingContent(
|
||||
title: 'Crime Mapping & Analysis',
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,8 +52,74 @@ class RoleSelectionScreen extends StatelessWidget {
|
|||
|
||||
// Role list
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => ListView.builder(
|
||||
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];
|
||||
|
@ -65,8 +132,9 @@ class RoleSelectionScreen extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
// Continue button
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
@ -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
Loading…
Reference in New Issue