diff --git a/analysis_options.yaml b/analysis_options.yaml index 405c8ae..a5f616f 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,6 +10,7 @@ analyzer: errors: avoid_print: ignore + deprecated_member_use_from_same_package: ignore include: package:flutter_lints/flutter.yaml linter: diff --git a/assets/icon/outline/home.svg b/assets/icon/outline/home.svg new file mode 100644 index 0000000..4ce7443 --- /dev/null +++ b/assets/icon/outline/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/map.svg b/assets/icon/outline/map.svg new file mode 100644 index 0000000..428ac11 --- /dev/null +++ b/assets/icon/outline/map.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/notification-bing.svg b/assets/icon/outline/notification-bing.svg new file mode 100644 index 0000000..a13beb2 --- /dev/null +++ b/assets/icon/outline/notification-bing.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/search.svg b/assets/icon/outline/search.svg new file mode 100644 index 0000000..6c41ba7 --- /dev/null +++ b/assets/icon/outline/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/setting.svg b/assets/icon/outline/setting.svg new file mode 100644 index 0000000..180f297 --- /dev/null +++ b/assets/icon/outline/setting.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/user.svg b/assets/icon/outline/user.svg new file mode 100644 index 0000000..ce6f47f --- /dev/null +++ b/assets/icon/outline/user.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/image/onboarding/banner-1.png b/assets/image/onboarding/banner-1.png new file mode 100644 index 0000000..dacf803 Binary files /dev/null and b/assets/image/onboarding/banner-1.png differ diff --git a/assets/image/onboarding/banner-2.png b/assets/image/onboarding/banner-2.png new file mode 100644 index 0000000..b5044d2 Binary files /dev/null and b/assets/image/onboarding/banner-2.png differ diff --git a/assets/image/onboarding/banner-3.png b/assets/image/onboarding/banner-3.png new file mode 100644 index 0000000..db03c67 Binary files /dev/null and b/assets/image/onboarding/banner-3.png differ diff --git a/lib/core/gen/assets.gen.dart b/lib/core/gen/assets.gen.dart index bea6c4f..5537cbd 100644 --- a/lib/core/gen/assets.gen.dart +++ b/lib/core/gen/assets.gen.dart @@ -7,7 +7,24 @@ // ignore_for_file: type=lint // ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart' as _svg; +import 'package:vector_graphics/vector_graphics.dart' as _vg; + +class $AssetsIconGen { + const $AssetsIconGen(); + + /// Directory path: assets/icon/outline + $AssetsIconOutlineGen get outline => const $AssetsIconOutlineGen(); +} + +class $AssetsImageGen { + const $AssetsImageGen(); + + /// Directory path: assets/image/onboarding + $AssetsImageOnboardingGen get onboarding => const $AssetsImageOnboardingGen(); +} class $AssetsLogoGen { const $AssetsLogoGen(); @@ -20,9 +37,80 @@ class $AssetsLogoGen { List get values => [appLogo]; } +class $AssetsIconOutlineGen { + const $AssetsIconOutlineGen(); + + /// File path: assets/icon/outline/home.svg + SvgGenImage get home => + const SvgGenImage('assets/icon/outline/home.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/outline/map.svg + SvgGenImage get map => + const SvgGenImage('assets/icon/outline/map.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/outline/notification-bing.svg + SvgGenImage get notificationBing => const SvgGenImage( + 'assets/icon/outline/notification-bing.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/search.svg + SvgGenImage get search => const SvgGenImage( + 'assets/icon/outline/search.svg', + size: Size(18.0, 18.0), + ); + + /// File path: assets/icon/outline/setting.svg + SvgGenImage get setting => const SvgGenImage( + 'assets/icon/outline/setting.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/user.svg + SvgGenImage get user => + const SvgGenImage('assets/icon/outline/user.svg', size: Size(24.0, 24.0)); + + /// List of all assets + List get values => [ + home, + map, + notificationBing, + search, + setting, + user, + ]; +} + +class $AssetsImageOnboardingGen { + const $AssetsImageOnboardingGen(); + + /// File path: assets/image/onboarding/banner-1.png + AssetGenImage get banner1 => const AssetGenImage( + 'assets/image/onboarding/banner-1.png', + size: Size(888.0, 879.0), + ); + + /// File path: assets/image/onboarding/banner-2.png + AssetGenImage get banner2 => const AssetGenImage( + 'assets/image/onboarding/banner-2.png', + size: Size(888.0, 879.0), + ); + + /// File path: assets/image/onboarding/banner-3.png + AssetGenImage get banner3 => const AssetGenImage( + 'assets/image/onboarding/banner-3.png', + size: Size(888.0, 879.0), + ); + + /// List of all assets + List get values => [banner1, banner2, banner3]; +} + class Assets { const Assets._(); + static const $AssetsIconGen icon = $AssetsIconGen(); + static const $AssetsImageGen image = $AssetsImageGen(); static const $AssetsLogoGen logo = $AssetsLogoGen(); } @@ -95,3 +183,75 @@ class AssetGenImage { String get keyName => _assetName; } + +class SvgGenImage { + const SvgGenImage(this._assetName, {this.size, this.flavors = const {}}) + : _isVecFormat = false; + + const SvgGenImage.vec(this._assetName, {this.size, this.flavors = const {}}) + : _isVecFormat = true; + + final String _assetName; + final Size? size; + final Set flavors; + final bool _isVecFormat; + + _svg.SvgPicture svg({ + Key? key, + bool matchTextDirection = false, + AssetBundle? bundle, + String? package, + double? width, + double? height, + BoxFit fit = BoxFit.contain, + AlignmentGeometry alignment = Alignment.center, + bool allowDrawingOutsideViewBox = false, + WidgetBuilder? placeholderBuilder, + String? semanticsLabel, + bool excludeFromSemantics = false, + _svg.SvgTheme? theme, + ColorFilter? colorFilter, + Clip clipBehavior = Clip.hardEdge, + @deprecated Color? color, + @deprecated BlendMode colorBlendMode = BlendMode.srcIn, + @deprecated bool cacheColorFilter = false, + }) { + final _svg.BytesLoader loader; + if (_isVecFormat) { + loader = _vg.AssetBytesLoader( + _assetName, + assetBundle: bundle, + packageName: package, + ); + } else { + loader = _svg.SvgAssetLoader( + _assetName, + assetBundle: bundle, + packageName: package, + theme: theme, + ); + } + return _svg.SvgPicture( + loader, + key: key, + matchTextDirection: matchTextDirection, + width: width, + height: height, + fit: fit, + alignment: alignment, + allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, + placeholderBuilder: placeholderBuilder, + semanticsLabel: semanticsLabel, + excludeFromSemantics: excludeFromSemantics, + colorFilter: + colorFilter ?? + (color == null ? null : ColorFilter.mode(color, colorBlendMode)), + clipBehavior: clipBehavior, + cacheColorFilter: cacheColorFilter, + ); + } + + String get path => _assetName; + + String get keyName => _assetName; +} diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index ec268cf..a8e84c9 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -1,35 +1,78 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:go_router/go_router.dart'; -import 'package:proxibuy/presentation/ui/screens/home/details_page.dart'; +import 'package:proxibuy/presentation/providers/cubit/user_info_cubit.dart'; +import 'package:proxibuy/presentation/ui/screens/auth/auth_page.dart'; import 'package:proxibuy/presentation/ui/screens/home/home_page.dart'; class AppRouter { - static final home = '/'; - static final details = '/details'; + static final initial = '/'; + static final home = '/home'; + static final explore = '/explore'; + static final setting = '/settings'; + static final GlobalKey navigatorKey = GlobalKey(); - GoRouter createRouter() { - return GoRouter( - navigatorKey: navigatorKey, - routes: [ - // Define your routes here - GoRoute( - path: '/', - builder: (BuildContext context, GoRouterState state) { - return const MyHomePage(); - }, - routes: [ - GoRoute( - path: details, - builder: (BuildContext context, GoRouterState state) { - print(state.uri.queryParameters); - return const DetailsPage(); - }, - ), - ], - ), - ], - ); - } + GoRouter createRouter = GoRouter( + navigatorKey: navigatorKey, + initialLocation: home, + routes: [ + GoRoute( + path: home, + builder: (BuildContext context, GoRouterState state) { + return BlocConsumer( + listener: (context, state) { + if (state is UserInfoSuccess || state is UserInfoFail) { + FlutterNativeSplash.remove(); + } + }, + builder: (context, state) { + if (state is UserInfoSuccess) { + return const HomePage(); + } else { + return const AuthPage(); + } + }, + ); + }, + routes: [], + ), + // StatefulShellRoute.indexedStack( + // builder: (context, state, navigationShell) => + // BlocBuilder( + // builder: (context, state) { + // if (state is UserInfoSuccess) { + // return CustomBottomNav(child: navigationShell); + // } else { + // return AuthPage(); + // } + // }, + // ), + // branches: [ + // StatefulShellBranch(routes: [ + // GoRoute( + // path: initial, + // builder: (context, state) { + // return HomeScreen(); + // }, + // ), + // ]), + // StatefulShellBranch(routes: [ + // GoRoute( + // path: explore, + // builder: (context, state) => ExploreScreen(), + // ), + // ]), + // StatefulShellBranch(routes: [ + // GoRoute( + // path: setting, + // builder: (context, state) => SettingScreen(), + // ), + // ]), + // ], + // ), + ], + ); } diff --git a/lib/core/services/api/api_routes.dart b/lib/core/services/api/api_routes.dart new file mode 100644 index 0000000..c5f05b9 --- /dev/null +++ b/lib/core/services/api/api_routes.dart @@ -0,0 +1,3 @@ +class ApiRoutes { + static final userInfo = '/userinfo'; +} diff --git a/lib/core/services/api/api_service.dart b/lib/core/services/api/api_service.dart new file mode 100644 index 0000000..9afa80d --- /dev/null +++ b/lib/core/services/api/api_service.dart @@ -0,0 +1,94 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:pretty_dio_logger/pretty_dio_logger.dart'; + +final ApiService apiService = ApiService(); + +class ApiService { + final keycloak = 'https://keycloak.liara.run/'; + final clientID = 'frontend'; + final realm = 'lba'; + + late Dio _dio; + + void setAuthToken(String token) { + _dio.options.headers['Authorization'] = 'Bearer $token'; + } + + ApiService() { + _dio = Dio( + BaseOptions( + baseUrl: + '$keycloak/$realm/protocol/openid-connect', // 🔹 Set your API base URL + connectTimeout: const Duration(seconds: 10), // Timeout settings + receiveTimeout: const Duration(seconds: 10), + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + ), + )..interceptors.add(PrettyDioLogger( + enabled: kDebugMode, + )); + } + + /// 🔹 Handle GET requests + Future get(String endpoint, {Map? params}) async { + try { + Response response = await _dio.get(endpoint, queryParameters: params); + return response.data; + } catch (e) { + _handleError(e); + } + } + + /// 🔹 Handle POST requests + Future post(String endpoint, {dynamic data}) async { + try { + Response response = await _dio.post(endpoint, data: data); + return response.data; + } catch (e) { + _handleError(e); + } + } + + /// 🔹 Handle PUT requests + Future put(String endpoint, {dynamic data}) async { + try { + Response response = await _dio.put(endpoint, data: data); + return response.data; + } catch (e) { + _handleError(e); + } + } + + /// 🔹 Handle DELETE requests + Future delete(String endpoint, {dynamic data}) async { + try { + Response response = await _dio.delete(endpoint, data: data); + return response.data; + } catch (e) { + _handleError(e); + } + } + + /// 🔹 Error handling + void _handleError(dynamic error) { + if (error is DioException) { + switch (error.type) { + case DioExceptionType.connectionTimeout: + throw Exception("Connection timeout, please try again."); + case DioExceptionType.receiveTimeout: + throw Exception("Receive timeout, please try again."); + case DioExceptionType.badResponse: + throw Exception("Server error: ${error.response?.statusCode}"); + case DioExceptionType.cancel: + throw Exception("Request canceled."); + default: + throw Exception("Unexpected error occurred."); + } + } else { + throw Exception("Unknown error: $error"); + } + } +} diff --git a/lib/core/services/notification/notification_service_web.dart b/lib/core/services/notification/notification_service_web.dart new file mode 100644 index 0000000..69d6d12 --- /dev/null +++ b/lib/core/services/notification/notification_service_web.dart @@ -0,0 +1,14 @@ +import 'package:universal_html/html.dart'; + +class NotificationServiceWeb { + static void showNotification() { + if (Notification.supported) { + Notification('Handbags & Wallets | White Elm', + body: 'Leander, Texas (8.7 Km away)', + icon: + 'https://s3-alpha-sig.figma.com/img/652b/471b/a8059768c434c7abf4cfdc3c879e9293?Expires=1740355200&Key-Pair-Id=APKAQ4GOSFWCW27IBOMQ&Signature=EVePXMyyhpSNgasV3uH1d5qMrPSkjn5KzpSxFoySpBs9uzU1aNHQoab9G9iOliFDRRSZ62RggnoomQ52og0UTPnMBUG6c-5GKT~f-C~sGnqppDoNf9UhhCBXpbE-BJYvkdXyuc7YmcdVyljgcWfChHANQL~nz5egKp-nyoL6mRfa6z1AH5IVaD-hMcA~cgcM2iRiJxddCciGyRNyr0cS8XWilO55sA1blB-q2Bp9sKZJCezw5Nhn2S8SLSVnDrNjBAJv3ph4ArsCxHH5DhCNmPiDA~AT3LOz~lQwVVJZhkIEAK6zS1TpvrsPPfNp7gAgr4MG3GM2CGl19AwrSoxDdw__'); + } else { + print('Notifications are not supported on this browser.'); + } + } +} diff --git a/lib/core/services/notification/notification_srvice_android.dart b/lib/core/services/notification/notification_srvice_android.dart new file mode 100644 index 0000000..6fb47c7 --- /dev/null +++ b/lib/core/services/notification/notification_srvice_android.dart @@ -0,0 +1,26 @@ +import 'package:flutter/services.dart'; + +class NotificationSrviceAndroid { + static const platform = MethodChannel('custom_notification'); + + NotificationSrviceAndroid() { + platform.setMethodCallHandler(_handleMethod); + } + + Future _handleMethod(MethodCall call) async { + print(' method ${call.method}'); + } + + static Future showCustomNotification() async { + try { + await platform.invokeMethod('showCustomNotification', { + 'title': 'Handbags & Wallets | White Elm', + 'content': 'Leander, Texas (8.7 Km away)', + 'imageUrl': + 'https://s3-alpha-sig.figma.com/img/652b/471b/a8059768c434c7abf4cfdc3c879e9293?Expires=1740355200&Key-Pair-Id=APKAQ4GOSFWCW27IBOMQ&Signature=EVePXMyyhpSNgasV3uH1d5qMrPSkjn5KzpSxFoySpBs9uzU1aNHQoab9G9iOliFDRRSZ62RggnoomQ52og0UTPnMBUG6c-5GKT~f-C~sGnqppDoNf9UhhCBXpbE-BJYvkdXyuc7YmcdVyljgcWfChHANQL~nz5egKp-nyoL6mRfa6z1AH5IVaD-hMcA~cgcM2iRiJxddCciGyRNyr0cS8XWilO55sA1blB-q2Bp9sKZJCezw5Nhn2S8SLSVnDrNjBAJv3ph4ArsCxHH5DhCNmPiDA~AT3LOz~lQwVVJZhkIEAK6zS1TpvrsPPfNp7gAgr4MG3GM2CGl19AwrSoxDdw__', + }); + } on PlatformException catch (e) { + print("Failed to show notification: '${e.message}'."); + } + } +} diff --git a/lib/data/models/auth/user_info_model.dart b/lib/data/models/auth/user_info_model.dart new file mode 100644 index 0000000..1559c5f --- /dev/null +++ b/lib/data/models/auth/user_info_model.dart @@ -0,0 +1,40 @@ +class UserInfoModel { + String? sub; + bool? emailVerified; + String? name; + String? preferredUsername; + String? givenName; + String? familyName; + String? email; + + UserInfoModel( + {this.sub, + this.emailVerified, + this.name, + this.preferredUsername, + this.givenName, + this.familyName, + this.email}); + + UserInfoModel.fromJson(Map json) { + sub = json['sub']; + emailVerified = json['email_verified']; + name = json['name']; + preferredUsername = json['preferred_username']; + givenName = json['given_name']; + familyName = json['family_name']; + email = json['email']; + } + + Map toJson() { + final Map data = {}; + data['sub'] = sub; + data['email_verified'] = emailVerified; + data['name'] = name; + data['preferred_username'] = preferredUsername; + data['given_name'] = givenName; + data['family_name'] = familyName; + data['email'] = email; + return data; + } +} diff --git a/lib/data/models/onboarding_banner_model.dart b/lib/data/models/onboarding_banner_model.dart new file mode 100644 index 0000000..deebbbc --- /dev/null +++ b/lib/data/models/onboarding_banner_model.dart @@ -0,0 +1,8 @@ +import 'package:proxibuy/core/gen/assets.gen.dart'; + +class OnboardingBannerModel { + final String desc; + final AssetGenImage image; + + OnboardingBannerModel({required this.desc, required this.image}); +} diff --git a/lib/data/models/screen_model.dart b/lib/data/models/screen_model.dart new file mode 100644 index 0000000..d7923fd --- /dev/null +++ b/lib/data/models/screen_model.dart @@ -0,0 +1,8 @@ +import 'package:proxibuy/core/gen/assets.gen.dart'; + +class ScreenModel { + final String title; + final SvgGenImage icon; + + ScreenModel({required this.title, required this.icon}); +} diff --git a/lib/data/repositories/auth_repository.dart b/lib/data/repositories/auth_repository.dart new file mode 100644 index 0000000..489081f --- /dev/null +++ b/lib/data/repositories/auth_repository.dart @@ -0,0 +1,16 @@ +import 'package:proxibuy/core/services/api/api_routes.dart'; +import 'package:proxibuy/core/services/api/api_service.dart'; +import 'package:proxibuy/data/models/auth/user_info_model.dart'; + +class AuthRepository { + static Future fetchUserInfo() async { + try { + var response = await apiService.get(ApiRoutes.userInfo); + print("Users: $response"); + return UserInfoModel.fromJson(response); + } catch (e) { + print("Error: $e"); + rethrow; + } + } +} diff --git a/lib/data/storage/shared_preferences_helper.dart b/lib/data/storage/shared_preferences_helper.dart index 046698a..b094929 100644 --- a/lib/data/storage/shared_preferences_helper.dart +++ b/lib/data/storage/shared_preferences_helper.dart @@ -8,6 +8,7 @@ class SharedPreferencesHelper { } static const String themeMode = 'theme-mode'; + static const String token = 'auth_token'; } class ThemeModeStorage { @@ -26,3 +27,20 @@ class ThemeModeStorage { prefs.remove(SharedPreferencesHelper.themeMode); } } + +class AuthTokenStorage { + static String getToken() { + final prefs = SharedPreferencesHelper.preferences; + return prefs.getString(SharedPreferencesHelper.token) ?? ""; + } + + static void setToken(String token) { + final prefs = SharedPreferencesHelper.preferences; + prefs.setString(SharedPreferencesHelper.token, token); + } + + static void clearToken() { + final prefs = SharedPreferencesHelper.preferences; + prefs.remove(SharedPreferencesHelper.token); + } +} diff --git a/lib/main.dart b/lib/main.dart index c869195..5040048 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:proxibuy/core/routes/app_router.dart'; import 'package:proxibuy/data/storage/shared_preferences_helper.dart'; import 'package:proxibuy/presentation/providers/cubit/them_mode_cubit.dart'; +import 'package:proxibuy/presentation/providers/cubit/user_info_cubit.dart'; import 'package:proxibuy/presentation/ui/theme/theme.dart'; import 'package:url_strategy/url_strategy.dart'; @@ -15,7 +16,9 @@ void main() async { runApp(MultiBlocProvider( providers: [ - BlocProvider(create: (context) => ThemModeCubit()) + BlocProvider(create: (context) => ThemModeCubit()), + BlocProvider( + create: (context) => UserInfoCubit()..getUserInfo()), ], child: const MyApp(), )); @@ -38,13 +41,13 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { return BlocBuilder( - builder: (context, state) { + builder: (context, themeState) { return MaterialApp.router( - routerConfig: AppRouter().createRouter(), + routerConfig: AppRouter().createRouter, title: 'Proxibuy', theme: appTheme, darkTheme: darkTheme, - themeMode: state, + themeMode: themeState, debugShowCheckedModeBanner: false, ); }, diff --git a/lib/presentation/providers/cubit/user_info_cubit.dart b/lib/presentation/providers/cubit/user_info_cubit.dart new file mode 100644 index 0000000..b1b584b --- /dev/null +++ b/lib/presentation/providers/cubit/user_info_cubit.dart @@ -0,0 +1,27 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:proxibuy/data/models/auth/user_info_model.dart'; + +part 'user_info_state.dart'; + +class UserInfoCubit extends Cubit { + UserInfoCubit() : super(UserInfoInitial()); + + void getUserInfo() async { + emit(UserInfoLoadng()); + try { + // final response = await AuthRepository.fetchUserInfo(); + emit(UserInfoSuccess( + user: UserInfoModel.fromJson({ + "sub": "f39c8216-388a-44e1-85a8-4f996e66e604", + "email_verified": true, + "name": "demo demo", + "preferred_username": "demo", + "given_name": "demo", + "family_name": "demo", + "email": "demo@gmail.com" + }))); + } catch (e) { + emit(UserInfoFail(message: e)); + } + } +} diff --git a/lib/presentation/providers/cubit/user_info_state.dart b/lib/presentation/providers/cubit/user_info_state.dart new file mode 100644 index 0000000..f02db27 --- /dev/null +++ b/lib/presentation/providers/cubit/user_info_state.dart @@ -0,0 +1,19 @@ +part of 'user_info_cubit.dart'; + +sealed class UserInfoState {} + +final class UserInfoInitial extends UserInfoState {} + +final class UserInfoLoadng extends UserInfoState {} + +final class UserInfoSuccess extends UserInfoState { + final UserInfoModel user; + + UserInfoSuccess({required this.user}); +} + +final class UserInfoFail extends UserInfoState { + final Object? message; + + UserInfoFail({required this.message}); +} diff --git a/lib/presentation/ui/screens/auth/auth_page.dart b/lib/presentation/ui/screens/auth/auth_page.dart new file mode 100644 index 0000000..c8ff015 --- /dev/null +++ b/lib/presentation/ui/screens/auth/auth_page.dart @@ -0,0 +1,226 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:proxibuy/core/gen/assets.gen.dart'; +import 'package:proxibuy/core/routes/app_router.dart'; +import 'package:proxibuy/core/utils/empty_space.dart'; +import 'package:proxibuy/data/models/onboarding_banner_model.dart'; +import 'package:proxibuy/presentation/providers/cubit/them_mode_cubit.dart'; +import 'package:proxibuy/presentation/ui/theme/responsive.dart'; +import 'package:proxibuy/presentation/ui/widgets/carousel/carousel_slider_widget.dart'; +import 'package:proxibuy/presentation/ui/widgets/edit_text/phone_number_input.dart'; + +class AuthPage extends StatefulWidget { + const AuthPage({super.key}); + + @override + State createState() => _AuthPageState(); +} + +class _AuthPageState extends State { + bool onBoarding = true; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + 'Auth', + style: Theme.of(context).textTheme.titleLarge, + ), + backgroundColor: Colors.transparent, + surfaceTintColor: Colors.transparent, + actions: [ + IconButton( + icon: Icon(Icons.brightness_6), + onPressed: () { + context.read().changeTheme(); + }, + ), + ], + ), + body: Responsive(context).builder( + mobile: onBoarding + ? carousle( + withNavs: true, autoPlay: false, enableInfiniteScroll: false) + : auth(context), + tablet: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(50.0), + child: carousle( + withNavs: false, + autoPlay: true, + enableInfiniteScroll: true, + height: MediaQuery.sizeOf(context).height * 0.8), + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 50), + child: auth(context))) + ], + ), + desktop: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(80.0), + child: carousle( + withNavs: false, + autoPlay: true, + enableInfiniteScroll: true, + height: MediaQuery.sizeOf(context).height * 0.8), + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 160), + child: auth(context))) + ], + )), + ); + } + + Padding auth(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: FlutterLogo( + size: 120, + ), + ), + Text('Proxibuy Geofencing marketing'), + 36.h, + Row( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text('Login'), + Text('Please enter your phone number') + ], + ), + ], + ), + 36.h, + PhoneNumberInput(), + 36.h, + SizedBox( + width: double.infinity, + height: 46, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(64), + ), + ), + onPressed: () async { + context.go(AppRouter.initial); + }, + child: Text('Get OTP'), + ), + ), + 24.h, + Row( + children: [ + Expanded( + child: Divider( + color: Theme.of(context).colorScheme.onSurface, + )), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text('Or continue with'), + ), + Expanded(child: Builder(builder: (context) { + return Divider( + color: Theme.of(context).colorScheme.onSurface, + ); + })) + ], + ), + 24.h, + SizedBox( + width: double.infinity, + height: 46, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(64), + ), + side: BorderSide( + width: 2, + color: Theme.of(context).colorScheme.onSurface)), + onPressed: () async {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Image.network( + 'https://img.icons8.com/?size=100&id=110560&format=png&color=000000', + width: 32, // Adjust width + height: 32, // Adjust height + ), + ), + Text( + 'Login with google', + style: Theme.of(context).textTheme.labelLarge, + ), + ], + )), + ), + ], + ), + ); + } + + CarouselSliderWidget carousle( + {required final bool withNavs, + required final bool autoPlay, + required final bool enableInfiniteScroll, + double? height}) { + return CarouselSliderWidget( + height: height ?? MediaQuery.sizeOf(context).height, + items: [ + OnboardingBannerModel( + image: Assets.image.onboarding.banner1, + desc: + 'Proxibuy Geofencing marketing\n"Join the app to discover exclusive discounts and special offers in specific areas around you for a smarter shopping experience!"'), + OnboardingBannerModel( + image: Assets.image.onboarding.banner2, + desc: + 'Proxibuy Geofencing marketing\n"Join the app to discover exclusive discounts and special offers in specific areas around you for a smarter shopping experience!"'), + OnboardingBannerModel( + image: Assets.image.onboarding.banner3, + desc: + 'Proxibuy Geofencing marketing\n"Join the app to discover exclusive discounts and special offers in specific areas around you for a smarter shopping experience!"'), + ], + withNavs: withNavs, + autoPlay: autoPlay, + enableInfiniteScroll: enableInfiniteScroll, + onLastClick: () { + setState(() { + onBoarding = false; + }); + }, + onPageBuilder: (context, item) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded(child: item.image.image()), + if (Responsive(context).isDesktop()) 64.h, + Text(item.desc) + ], + ); + }, + ); + } +} diff --git a/lib/presentation/ui/screens/home/explore_screen.dart b/lib/presentation/ui/screens/home/explore_screen.dart new file mode 100644 index 0000000..84ad9a2 --- /dev/null +++ b/lib/presentation/ui/screens/home/explore_screen.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:proxibuy/core/routes/app_router.dart'; + +class ExploreScreen extends StatelessWidget { + const ExploreScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: InkWell( + onTap: () => context.go(AppRouter.setting), child: Text('Explore')), + ), + ); + } +} diff --git a/lib/presentation/ui/screens/home/home_page.dart b/lib/presentation/ui/screens/home/home_page.dart index 150cb15..5441acc 100644 --- a/lib/presentation/ui/screens/home/home_page.dart +++ b/lib/presentation/ui/screens/home/home_page.dart @@ -1,80 +1,223 @@ +import 'package:bottom_navy_bar/bottom_navy_bar.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; -import 'package:proxibuy/core/routes/app_router.dart'; -import 'package:proxibuy/core/services/permisions_services.dart'; +import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/utils/empty_space.dart'; +import 'package:proxibuy/data/models/screen_model.dart'; import 'package:proxibuy/presentation/providers/cubit/them_mode_cubit.dart'; +import 'package:proxibuy/presentation/ui/screens/home/explore_screen.dart'; +import 'package:proxibuy/presentation/ui/screens/home/home_screen.dart'; +import 'package:proxibuy/presentation/ui/screens/home/setting_screen.dart'; +import 'package:proxibuy/presentation/ui/theme/responsive.dart'; +import 'package:proxibuy/presentation/ui/theme/theme.dart'; -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key}); +class HomePage extends StatefulWidget { + const HomePage({super.key}); @override - State createState() => _MyHomePageState(); + State createState() => _HomePageState(); } -class _MyHomePageState extends State { - static const platform = MethodChannel('custom_notification'); - - Future showCustomNotification() async { - try { - final notifPer = await requestNotificationPermission(); - if (notifPer) { - await platform.invokeMethod('showCustomNotification', { - 'title': 'Handbags & Wallets | White Elm', - 'content': 'Leander, Texas (8.7 Km away)', - 'imageUrl': - 'https://s3-alpha-sig.figma.com/img/652b/471b/a8059768c434c7abf4cfdc3c879e9293?Expires=1740355200&Key-Pair-Id=APKAQ4GOSFWCW27IBOMQ&Signature=EVePXMyyhpSNgasV3uH1d5qMrPSkjn5KzpSxFoySpBs9uzU1aNHQoab9G9iOliFDRRSZ62RggnoomQ52og0UTPnMBUG6c-5GKT~f-C~sGnqppDoNf9UhhCBXpbE-BJYvkdXyuc7YmcdVyljgcWfChHANQL~nz5egKp-nyoL6mRfa6z1AH5IVaD-hMcA~cgcM2iRiJxddCciGyRNyr0cS8XWilO55sA1blB-q2Bp9sKZJCezw5Nhn2S8SLSVnDrNjBAJv3ph4ArsCxHH5DhCNmPiDA~AT3LOz~lQwVVJZhkIEAK6zS1TpvrsPPfNp7gAgr4MG3GM2CGl19AwrSoxDdw__', - }); - } - } on PlatformException catch (e) { - print("Failed to show notification: '${e.message}'."); - } - } - - @override - void initState() { - super.initState(); - platform.setMethodCallHandler(_handleMethod); - } - - Future _handleMethod(MethodCall call) async { - print(' method ${call.method}'); - } - +class _HomePageState extends State { + int selectedIndex = 0; + List screens = [ + ScreenModel(title: 'Home', icon: Assets.icon.outline.home), + ScreenModel(title: 'Explore', icon: Assets.icon.outline.map), + ScreenModel(title: 'Settings', icon: Assets.icon.outline.setting), + ]; @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("Proxibuy"), - actions: [ - IconButton( - icon: Icon(Icons.brightness_6), - onPressed: () { - context.read().changeTheme(); - }, - ), - ], - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () async { - await showCustomNotification(); - }, - child: Text('Show Notification')), - 18.h, - ElevatedButton( - onPressed: () async { - context.go(AppRouter.details); - }, - child: Text('Navigate to Details')), + return Responsive(context).builder( + desktop: Scaffold( + body: Column( + children: [ + navBar(context), + Expanded(child: body()), ], ), ), + mobile: Scaffold( + appBar: AppBar( + title: Text( + "Proxibuy", + style: Theme.of(context).textTheme.headlineSmall, + ), + actions: [ + IconButton( + icon: Assets.icon.outline.notificationBing + .svg(color: Theme.of(context).colorScheme.onSurface), + onPressed: () {}, + ), + 12.w, + IconButton( + icon: Icon(Icons.brightness_6), + onPressed: () { + context.read().changeTheme(); + }, + ), + ], + ), + body: body(), + bottomNavigationBar: bottomNavigationBar(context), + ), ); } + + Padding navBar(BuildContext context) { + final defaultBorder = OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: + BorderSide(color: Theme.of(context).colorScheme.surface, width: 2)); + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + SelectableText( + "Proxibuy", + style: Theme.of(context).textTheme.displaySmall, + ), + 32.w, + Row( + children: [ + ...List.generate( + screens.length, + (index) => Row( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: selectedIndex == index + ? themeColor(context) + ?.primaryLightSurface + .withAlpha(90) + : null), + child: IconButton( + splashRadius: 12, + onPressed: () { + _onItemTapped(context, index); + }, + icon: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + screens[index].icon.svg( + color: selectedIndex == index + ? Theme.of(context).primaryColor + : Theme.of(context) + .colorScheme + .onSurface), + 12.w, + Text( + screens[index].title, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: selectedIndex == index + ? Theme.of(context) + .primaryColor + : Theme.of(context) + .colorScheme + .onSurface), + ) + ], + )), + ), + 24.w + ], + ), + ) + ], + ), + 8.w, + Flexible( + child: Container( + constraints: BoxConstraints(maxWidth: 800), + child: TextField( + decoration: InputDecoration( + hintText: 'what are you looking for?', + suffixIcon: Padding( + padding: const EdgeInsets.all(8.0), + child: Assets.icon.outline.search.svg( + color: Theme.of(context).colorScheme.onSurface, + width: 16, + height: 16), + ), + enabledBorder: defaultBorder, + border: defaultBorder), + ), + )), + 32.w, + ], + ), + ), + Row( + children: [ + IconButton( + icon: Assets.icon.outline.notificationBing + .svg(color: Theme.of(context).colorScheme.onSurface), + onPressed: () {}, + ), + 12.w, + IconButton( + icon: Icon(Icons.brightness_6), + onPressed: () { + context.read().changeTheme(); + }, + ), + ], + ) + ], + ), + ); + } + + Padding bottomNavigationBar(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: BottomNavyBar( + selectedIndex: selectedIndex, + // landscapeLayout: BottomNavigationBarLandscapeLayout.centered, + // showUnselectedLabels: false, + onItemSelected: (index) => _onItemTapped(context, index), + backgroundColor: Theme.of(context).colorScheme.surface, + itemCornerRadius: 12, + itemPadding: EdgeInsets.symmetric(horizontal: 16), + borderRadius: BorderRadius.circular(16), + mainAxisAlignment: MainAxisAlignment.spaceAround, + showElevation: true, + + items: [ + ...List.generate( + screens.length, + (index) => BottomNavyBarItem( + icon: screens[index].icon.svg( + color: selectedIndex == index + ? Theme.of(context).primaryColor + : Theme.of(context).colorScheme.onSurface), + activeColor: Theme.of(context).primaryColor, + inactiveColor: Theme.of(context).colorScheme.onSurface, + title: Text(screens[index].title)), + ) + ], + ), + ); + } + + IndexedStack body() { + return IndexedStack( + sizing: StackFit.expand, + index: selectedIndex, + children: [HomeScreen(), ExploreScreen(), SettingScreen()], + ); + } + + void _onItemTapped(BuildContext context, int index) { + setState(() { + selectedIndex = index; + }); + } } diff --git a/lib/presentation/ui/screens/home/home_screen.dart b/lib/presentation/ui/screens/home/home_screen.dart new file mode 100644 index 0000000..21422d1 --- /dev/null +++ b/lib/presentation/ui/screens/home/home_screen.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:proxibuy/core/gen/assets.gen.dart'; +import 'package:proxibuy/presentation/ui/theme/responsive.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final defaultBorder = OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: + BorderSide(color: Theme.of(context).colorScheme.surface, width: 2)); + return Responsive(context).builder( + mobile: Scaffold( + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // ElevatedButton( + // onPressed: () async { + // final notifPer = await requestNotificationPermission(); + + // if (notifPer) { + // if (kIsWeb) { + // NotificationServiceWeb.showNotification(); + // } else { + // await NotificationSrviceAndroid.showCustomNotification(); + // } + // } + // }, + // child: Text('Show Notification')), + + Flexible( + child: Container( + constraints: BoxConstraints(maxWidth: 800), + child: TextField( + textInputAction: TextInputAction.search, + onSubmitted: (value) { + print(value); + }, + decoration: InputDecoration( + hintText: 'what are you looking for?', + suffixIcon: Padding( + padding: const EdgeInsets.all(12.0), + child: Assets.icon.outline.search.svg( + color: Theme.of(context).colorScheme.onSurface, + width: 16, + height: 16), + ), + enabledBorder: defaultBorder, + border: defaultBorder), + ), + )), + ], + ), + ), + ), + desktop: Scaffold( + body: SizedBox(), + )); + } +} diff --git a/lib/presentation/ui/screens/home/details_page.dart b/lib/presentation/ui/screens/home/setting_screen.dart similarity index 57% rename from lib/presentation/ui/screens/home/details_page.dart rename to lib/presentation/ui/screens/home/setting_screen.dart index 7cae96c..998fbaf 100644 --- a/lib/presentation/ui/screens/home/details_page.dart +++ b/lib/presentation/ui/screens/home/setting_screen.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -class DetailsPage extends StatelessWidget { - const DetailsPage({super.key}); +class SettingScreen extends StatelessWidget { + const SettingScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: Center( - child: Text('Detail Page'), + child: Text('Setting'), ), ); } diff --git a/lib/presentation/ui/theme/custom_colors.dart b/lib/presentation/ui/theme/custom_colors.dart new file mode 100644 index 0000000..2627a8e --- /dev/null +++ b/lib/presentation/ui/theme/custom_colors.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class CustomColors extends ThemeExtension { + final MaterialColor primarySwatch; + final MaterialColor secondrySwatch; + final Color primaryLightSurface; + + CustomColors({ + required this.primarySwatch, + required this.secondrySwatch, + required this.primaryLightSurface, + }); + + @override + CustomColors copyWith({ + MaterialColor? primarySwatch, + MaterialColor? secondrySwatch, + Color? primaryLightSurface, + }) { + return CustomColors( + primarySwatch: primarySwatch ?? this.primarySwatch, + secondrySwatch: secondrySwatch ?? this.secondrySwatch, + primaryLightSurface: primaryLightSurface ?? this.primaryLightSurface, + ); + } + + @override + ThemeExtension lerp( + ThemeExtension? other, double t) { + if (other is! CustomColors) return this; + return CustomColors( + primarySwatch: other.primarySwatch, + secondrySwatch: other.secondrySwatch, + primaryLightSurface: + Color.lerp(primaryLightSurface, other.primaryLightSurface, t)!, + ); + } +} + +extension AppColorsTheme on ThemeData { + CustomColors? get customColors => extension(); +} diff --git a/lib/presentation/ui/theme/responsive.dart b/lib/presentation/ui/theme/responsive.dart new file mode 100644 index 0000000..2fca6ad --- /dev/null +++ b/lib/presentation/ui/theme/responsive.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class Responsive { + final BuildContext context; + + Responsive(this.context); + + Widget builder( + {required final Widget mobile, + required final Widget desktop, + final Widget? tablet, + final bool tabletAndMobileSame = false}) { + if (isMobile() || (isTablet() && tabletAndMobileSame)) { + return mobile; + } else if (isTablet() && tablet != null) { + return tablet; + } else { + return desktop; + } + } + + Widget maxWidthInDesktop( + {final double maxWidth = 600, + required final Widget Function( + BuildContext contxet, + double mw, + ) child}) { + if (isDesktop() || isTablet()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: child(context, maxWidth), + ), + ], + ); + } else { + return child(context, MediaQuery.sizeOf(context).width); + } + } + + bool isMobile() => MediaQuery.sizeOf(context).width < 904; + bool isTablet() => + MediaQuery.sizeOf(context).width < 1280 && + MediaQuery.sizeOf(context).width >= 904; + bool isDesktop() => MediaQuery.sizeOf(context).width >= 1280; +} diff --git a/lib/presentation/ui/theme/theme.dart b/lib/presentation/ui/theme/theme.dart index 11ecdde..0834cbb 100644 --- a/lib/presentation/ui/theme/theme.dart +++ b/lib/presentation/ui/theme/theme.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:proxibuy/presentation/ui/theme/custom_colors.dart'; import 'colors.dart'; final ThemeData appTheme = ThemeData( @@ -6,9 +7,18 @@ final ThemeData appTheme = ThemeData( primaryColor: primarySwatch[500], scaffoldBackgroundColor: lightBackground, + extensions: >[ + CustomColors( + primarySwatch: primarySwatch, + secondrySwatch: secondarySwatch, + primaryLightSurface: primarySwatch[200]!, + ), + ], + // App Bar Theme appBarTheme: const AppBarTheme( - backgroundColor: primarySwatch, + surfaceTintColor: lightBackground, + backgroundColor: lightBackground, elevation: 2, titleTextStyle: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), @@ -76,10 +86,18 @@ final ThemeData darkTheme = ThemeData( primarySwatch: darkPrimarySwatch, primaryColor: darkPrimarySwatch[500], scaffoldBackgroundColor: darkBackground, + extensions: >[ + CustomColors( + primarySwatch: darkPrimarySwatch, + secondrySwatch: darkSecondarySwatch, + primaryLightSurface: darkPrimarySwatch[900]!, + ), + ], // App Bar Theme appBarTheme: const AppBarTheme( - backgroundColor: darkPrimarySwatch, + backgroundColor: darkBackground, + surfaceTintColor: darkBackground, elevation: 2, titleTextStyle: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), @@ -142,3 +160,7 @@ final ThemeData darkTheme = ThemeData( onSurface: fontLight, ), ); + +CustomColors? themeColor(BuildContext context) { + return Theme.of(context).extension(); +} diff --git a/lib/presentation/ui/widgets/carousel/carousel_slider_widget.dart b/lib/presentation/ui/widgets/carousel/carousel_slider_widget.dart new file mode 100644 index 0000000..fcca669 --- /dev/null +++ b/lib/presentation/ui/widgets/carousel/carousel_slider_widget.dart @@ -0,0 +1,124 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; + +class CarouselSliderWidget extends StatefulWidget { + final List items; + final Widget Function( + BuildContext context, + T item, + ) onPageBuilder; + final bool withNavs; + final double height; + final bool autoPlay; + final bool enableInfiniteScroll; + final Function()? onLastClick; + + const CarouselSliderWidget({ + super.key, + required this.items, + required this.onPageBuilder, + this.withNavs = false, + this.onLastClick, + this.autoPlay = true, + this.enableInfiniteScroll = true, + this.height = 200, + }); + + @override + State> createState() => + _CarouselSliderWidgetState(); +} + +class _CarouselSliderWidgetState extends State> { + int activeIndex = 0; + final CarouselSliderController _controller = CarouselSliderController(); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CarouselSlider.builder( + carouselController: _controller, + itemCount: widget.items.length, + itemBuilder: (context, index, realIndex) { + return widget.onPageBuilder(context, widget.items[index]); + }, + options: CarouselOptions( + height: widget.height * 0.75, + autoPlay: widget.autoPlay, + enableInfiniteScroll: widget.enableInfiniteScroll, + enlargeCenterPage: true, + viewportFraction: 0.9, + onPageChanged: (index, reason) => + setState(() => activeIndex = index), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: Center(child: buildIndicator())), + if (widget.withNavs) + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Opacity( + opacity: activeIndex != 0 ? 1 : 0, + child: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded), + onPressed: () => _controller.previousPage(), + ), + ), + Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).primaryColor), + child: InkWell( + highlightColor: Theme.of(context).primaryColor, + onTap: () { + if (activeIndex == widget.items.length - 1) { + if (widget.onLastClick != null) { + widget.onLastClick!(); + } + } else { + _controller.nextPage(); + } + }, + child: const Icon(Icons.arrow_forward_ios_rounded), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ); + } + + Widget buildImage(String urlImage, int index) => Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + color: Colors.grey, + child: Image.network( + urlImage, + fit: BoxFit.cover, + ), + ); + + Widget buildIndicator() => AnimatedSmoothIndicator( + activeIndex: activeIndex, + count: widget.items.length, + effect: ExpandingDotsEffect( + dotWidth: 10, + dotHeight: 10, + activeDotColor: Theme.of(context).primaryColor, + dotColor: Theme.of(context).colorScheme.onSurface, + ), + ); +} diff --git a/lib/presentation/ui/widgets/edit_text/phone_number_input.dart b/lib/presentation/ui/widgets/edit_text/phone_number_input.dart new file mode 100644 index 0000000..6d5b2f2 --- /dev/null +++ b/lib/presentation/ui/widgets/edit_text/phone_number_input.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:intl_phone_field/intl_phone_field.dart'; + +class PhoneNumberInput extends StatelessWidget { + const PhoneNumberInput({super.key}); + + @override + Widget build(BuildContext context) { + final border = OutlineInputBorder( + borderSide: + BorderSide(color: Theme.of(context).colorScheme.onSurface, width: 2), + ); + return IntlPhoneField( + decoration: InputDecoration( + labelText: 'Phone number', + counter: SizedBox(), + border: border, + enabledBorder: border), + initialCountryCode: 'US', + dropdownIconPosition: IconPosition.trailing, + flagsButtonMargin: EdgeInsets.symmetric(horizontal: 12), + onChanged: (phone) { + print(phone.completeNumber); + }, + ); + } +} diff --git a/lib/presentation/ui/widgets/navigations/custom_bottom_nav.dart b/lib/presentation/ui/widgets/navigations/custom_bottom_nav.dart new file mode 100644 index 0000000..0ac82c7 --- /dev/null +++ b/lib/presentation/ui/widgets/navigations/custom_bottom_nav.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:proxibuy/core/routes/app_router.dart'; + +class CustomBottomNav extends StatelessWidget { + final Widget child; + + const CustomBottomNav({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + child, + Positioned( + bottom: 16, + left: 16, + right: 16, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: BottomNavigationBar( + currentIndex: _getSelectedIndex(context), + landscapeLayout: BottomNavigationBarLandscapeLayout.centered, + onTap: (index) => _onItemTapped(context, index), + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.home), label: "Home"), + BottomNavigationBarItem( + icon: Icon(Icons.explore), label: "Explore"), + BottomNavigationBarItem( + icon: Icon(Icons.settings), label: "Settings"), + ], + ), + ), + ) + ], + ), + ); + } + + int _getSelectedIndex(BuildContext context) { + final String location = GoRouterState.of(context).uri.toString(); + if (location.startsWith(AppRouter.explore)) return 1; + if (location.startsWith(AppRouter.setting)) return 2; + return 0; + } + + void _onItemTapped(BuildContext context, int index) { + switch (index) { + case 0: + context.go(AppRouter.initial); + break; + case 1: + context.go(AppRouter.explore); + break; + case 2: + context.go(AppRouter.setting); + break; + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 5aba2d8..4d84498 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + bottom_navy_bar: + dependency: "direct main" + description: + name: bottom_navy_bar + sha256: ed54e0a18748106cc4d3648390bf94e88c0913db6bd3018dd30ab0810f0c26d6 + url: "https://pub.dev" + source: hosted + version: "6.1.0" build: dependency: transitive description: @@ -129,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.3" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae" + url: "https://pub.dev" + source: hosted + version: "5.0.0" characters: dependency: transitive description: @@ -137,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -249,6 +273,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + dropdown_textfield: + dependency: "direct main" + description: + name: dropdown_textfield + sha256: ef8a35c52c92a563773d3efead94e5a1c162d6fe6c53974d0986aab6249928a1 + url: "https://pub.dev" + source: hosted + version: "1.2.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -334,6 +374,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.4" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + url: "https://pub.dev" + source: hosted + version: "2.0.17" flutter_test: dependency: "direct dev" description: flutter @@ -432,6 +480,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + intl_phone_field: + dependency: "direct main" + description: + name: intl_phone_field + sha256: "73819d3dfcb68d2c85663606f6842597c3ddf6688ac777f051b17814fe767bbf" + url: "https://pub.dev" + source: hosted + version: "3.2.0" io: dependency: transitive description: @@ -672,6 +728,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.1" + pretty_dio_logger: + dependency: "direct main" + description: + name: pretty_dio_logger + sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" + url: "https://pub.dev" + source: hosted + version: "1.4.0" provider: dependency: transitive description: @@ -773,6 +837,14 @@ packages: description: flutter source: sdk version: "0.0.0" + smooth_page_indicator: + dependency: "direct main" + description: + name: smooth_page_indicator + sha256: "3b28b0c545fa67ed9e5997d9f9720d486f54c0c607e056a1094544e36934dff3" + url: "https://pub.dev" + source: hosted + version: "1.2.0+3" source_span: dependency: transitive description: @@ -853,6 +925,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_html: + dependency: "direct main" + description: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" universal_io: dependency: transitive description: @@ -869,6 +949,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + url: "https://pub.dev" + source: hosted + version: "1.1.18" vector_graphics_codec: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5c118ec..ad420f9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,14 @@ dependencies: url_strategy: ^0.3.0 flutter_native_splash: ^2.4.4 flutter_launcher_icons: ^0.14.3 + carousel_slider: ^5.0.0 + smooth_page_indicator: ^1.2.0+3 + intl_phone_field: ^3.2.0 + flutter_svg: ^2.0.17 + bottom_navy_bar: ^6.1.0 + pretty_dio_logger: ^1.4.0 + universal_html: ^2.2.4 + dropdown_textfield: ^1.2.0 flutter_launcher_icons: android: true @@ -106,6 +114,10 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/logo/ + - assets/image/ + - assets/icon/ + - assets/icon/outline/ + - assets/image/onboarding/ # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see diff --git a/web/index.html b/web/index.html index a442b53..556065d 100644 --- a/web/index.html +++ b/web/index.html @@ -1,4 +1,6 @@ - + + + - + - - - + + + - - - - + + + + - + proxibuy - + - - - - - - - - - - - + margin: 0; + min-height: 100%; + background-color: #ffffff; + background-size: 100% 100%; + } - \ No newline at end of file + .center { + margin: 0; + position: absolute; + top: 50%; + left: 50%; + -ms-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + } + + .contain { + display: block; + width: 100%; + height: 100%; + object-fit: contain; + } + + .stretch { + display: block; + width: 100%; + height: 100%; + } + + .cover { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + } + + .bottom { + position: absolute; + bottom: 0; + left: 50%; + -ms-transform: translate(-50%, 0); + transform: translate(-50%, 0); + } + + .bottomLeft { + position: absolute; + bottom: 0; + left: 0; + } + + .bottomRight { + position: absolute; + bottom: 0; + right: 0; + } + + @media (prefers-color-scheme: dark) { + body { + background-color: #121212; + } + } + + + + + + + + + + + + +