diff --git a/lib/core/api_services.dart b/lib/core/api_services.dart index e5bcdf2..fa61503 100644 --- a/lib/core/api_services.dart +++ b/lib/core/api_services.dart @@ -1,89 +1,162 @@ import 'dart:convert'; +import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; class ApiService { - final String baseUrl; + final String baseUrl = dotenv.get('BASE_URL'); + final String apiKey = dotenv.get('API_KEY'); - ApiService({this.baseUrl = ''}); - - static String get apiUrl => dotenv.env['BASE_URL'] ?? ''; + static const Map _headers = { + 'Content-Type': 'application/json', + }; Future> get(String endpoint) async { - final url = Uri.parse('$apiUrl/$endpoint'); - try { - final response = await http.get(url); + final url = Uri.parse('$baseUrl$endpoint'); + final response = await http.get( + url, + headers: {..._headers, 'API_KEY': apiKey}, + ); - if (response.statusCode == 200) { - return json.decode(response.body); - } else { - throw Exception('Failed to load data'); - } + return _processResponse(response); } catch (e) { - rethrow; + throw NetworkException( + 'Failed to connect to the server. Please check your internet connection.', + ); } } - Future> post( - String endpoint, - Map data, - ) async { - final url = Uri.parse('$apiUrl/$endpoint'); - + Future> post(String endpoint, Map body) async { try { + final url = Uri.parse('$baseUrl$endpoint'); + + // Debugging URL dan Body Request + debugPrint('Request URL: $url'); + debugPrint('Request Body: ${jsonEncode(body)}'); + debugPrint('API_KEY: $apiKey'); + final response = await http.post( url, - headers: {'Content-Type': 'application/json'}, - body: json.encode(data), + headers: { + ..._headers, // Menggunakan _headers untuk Content-Type + 'x-api-key': apiKey, // Pastikan API_KEY dimasukkan dengan benar di sini + }, + body: jsonEncode(body), ); - if (response.statusCode == 201) { - return json.decode(response.body); - } else { - throw Exception('Failed to submit data'); - } + debugPrint('Response: ${response.body}'); // Debugging Response + + return _processResponse(response); } catch (e) { - rethrow; + debugPrint('Error during API request: $e'); + throw NetworkException( + 'Failed to connect to the server. Please check your internet connection.', + ); } } Future> put( String endpoint, - Map data, + Map body, ) async { - final url = Uri.parse('$apiUrl/$endpoint'); - try { + final url = Uri.parse('$baseUrl$endpoint'); final response = await http.put( url, - headers: {'Content-Type': 'application/json'}, - body: json.encode(data), + headers: {..._headers, 'API_KEY': apiKey}, + body: jsonEncode(body), ); - if (response.statusCode == 200) { - return json.decode(response.body); - } else { - throw Exception('Failed to update data'); - } + return _processResponse(response); } catch (e) { - rethrow; + throw NetworkException( + 'Failed to connect to the server. Please check your internet connection.', + ); + } + } + + Future> patch( + String endpoint, + Map body, + ) async { + try { + final url = Uri.parse('$baseUrl$endpoint'); + final response = await http.patch( + url, + headers: {..._headers, 'API_KEY': apiKey}, + body: jsonEncode(body), + ); + + return _processResponse(response); + } catch (e) { + throw NetworkException( + 'Failed to connect to the server. Please check your internet connection.', + ); } } Future> delete(String endpoint) async { - final url = Uri.parse('$apiUrl/$endpoint'); - try { - final response = await http.delete(url); + final url = Uri.parse('$baseUrl$endpoint'); + final response = await http.delete( + url, + headers: {..._headers, 'API_KEY': apiKey}, + ); - if (response.statusCode == 200) { - return json.decode(response.body); - } else { - throw Exception('Failed to delete data'); - } + return _processResponse(response); } catch (e) { - rethrow; + throw NetworkException( + 'Failed to connect to the server. Please check your internet connection.', + ); + } + } + + Map _processResponse(http.Response response) { + switch (response.statusCode) { + case 200: + return jsonDecode(response.body); + case 400: + throw BadRequestException( + 'Bad request. The server could not process your request.', + ); + case 401: + throw UnauthorizedException( + 'Unauthorized. Please check your credentials.', + ); + case 404: + throw NotFoundException( + 'Not found. The requested resource could not be found.', + ); + case 500: + throw ServerException('Internal server error. Please try again later.'); + default: + throw Exception('Failed with status code: ${response.statusCode}'); } } } + +class NetworkException implements Exception { + final String message; + NetworkException(this.message); +} + +class BadRequestException implements Exception { + final String message; + BadRequestException(this.message); +} + +class UnauthorizedException implements Exception { + final String message; + UnauthorizedException(this.message); +} + +class NotFoundException implements Exception { + final String message; + NotFoundException(this.message); +} + +class ServerException implements Exception { + final String message; + ServerException(this.message); +} diff --git a/lib/core/navigation.dart b/lib/core/navigation.dart new file mode 100644 index 0000000..3ec9664 --- /dev/null +++ b/lib/core/navigation.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; +import 'package:iconsax_flutter/iconsax_flutter.dart'; +import 'package:rijig_mobile/core/guide.dart'; +import 'package:rijig_mobile/core/router.dart'; +import 'package:rijig_mobile/screen/app/activity/activity_screen.dart'; +import 'package:rijig_mobile/screen/app/cart/cart_screen.dart'; +import 'package:rijig_mobile/screen/app/home/home_screen.dart'; +import 'package:rijig_mobile/screen/app/profil/profil_screen.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class NavigationPage extends StatefulWidget { + final dynamic data; + const NavigationPage({super.key, this.data}); + + @override + State createState() => _NavigationPageState(); +} + +class _NavigationPageState extends State { + int _selectedIndex = 0; + + _loadSelectedIndex() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + setState(() { + _selectedIndex = prefs.getInt('last_selected_index') ?? 0; + }); + } + + _saveSelectedIndex(int index) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setInt('last_selected_index', index); + } + + @override + void initState() { + super.initState(); + _loadSelectedIndex(); + } + + void _onItemTapped(int index) { + if (index == 2) { + router.push("/requestpickup"); + } else { + setState(() { + _selectedIndex = index; + }); + _saveSelectedIndex(index); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBody: true, + backgroundColor: Colors.white, + body: IndexedStack( + index: _selectedIndex, + children: const [ + HomeScreen(), + ActivityScreen(), + Text(""), + CartScreen(), + ProfilScreen(), + ], + ), + bottomNavigationBar: Theme( + data: Theme.of(context).copyWith( + splashFactory: NoSplash.splashFactory, + highlightColor: Colors.transparent, + ), + child: Visibility( + visible: _selectedIndex != 2, + child: BottomAppBar( + shape: const CircularNotchedRectangle(), + padding: PaddingCustom().paddingHorizontal(2), + elevation: 0, + height: 67, + color: Colors.white, + clipBehavior: Clip.antiAlias, + notchMargin: 3.0, + child: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + backgroundColor: Colors.transparent, + elevation: 0, + showSelectedLabels: true, + showUnselectedLabels: true, + selectedItemColor: Colors.blue, + unselectedItemColor: Colors.grey, + currentIndex: _selectedIndex, + onTap: _onItemTapped, + items: [ + BottomNavigationBarItem( + icon: Icon(Iconsax.home_1, color: Colors.grey), + activeIcon: Icon(Iconsax.home_1, color: Colors.blue), + label: 'Beranda', + ), + BottomNavigationBarItem( + icon: Icon(Iconsax.home, color: Colors.grey), + activeIcon: Icon(Iconsax.home, color: Colors.blue), + label: 'Pesan', + ), + const BottomNavigationBarItem( + icon: SizedBox.shrink(), + label: '', + ), + BottomNavigationBarItem( + icon: Icon(Iconsax.document, color: Colors.grey), + activeIcon: Icon(Iconsax.document, color: Colors.blue), + label: 'Tutorial', + ), + BottomNavigationBarItem( + icon: Icon(Iconsax.home, color: Colors.grey), + activeIcon: Icon(Iconsax.home, color: Colors.blue), + label: 'Profil', + ), + ], + selectedLabelStyle: const TextStyle(fontSize: 14), + unselectedLabelStyle: const TextStyle(fontSize: 12), + ), + ), + ), + ), + + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + floatingActionButton: SizedBox( + width: 78, + height: 78, + child: FloatingActionButton( + onPressed: () { + router.push("/requestpickup"); + }, + backgroundColor: Colors.white, + shape: const CircleBorder( + side: BorderSide(color: Colors.white, width: 4), + ), + elevation: 0, + highlightElevation: 0, + hoverColor: Colors.blue, + splashColor: Colors.transparent, + foregroundColor: Colors.white, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Icon(Iconsax.home, color: primaryColor, size: 30), + Text("data", style: TextStyle(color: blackNavyColor)), + ], + ), + ), + ), + ); + } +} diff --git a/lib/core/router.dart b/lib/core/router.dart index fe06ff6..14f1eff 100644 --- a/lib/core/router.dart +++ b/lib/core/router.dart @@ -1,10 +1,37 @@ import 'package:go_router/go_router.dart'; -import 'package:rijig_mobile/screen/auth/login_screen.dart'; +import 'package:rijig_mobile/core/navigation.dart'; +import 'package:rijig_mobile/screen/app/home/home_screen.dart'; +import 'package:rijig_mobile/screen/app/requestpick/requestpickup_screen.dart'; +// import 'package:rijig_mobile/screen/auth/login_screen.dart'; import 'package:rijig_mobile/screen/auth/otp_screen.dart'; +import 'package:rijig_mobile/screen/launch/onboardingpage_screen.dart'; final router = GoRouter( routes: [ - GoRoute(path: '/', builder: (context, state) => LoginScreen()), - GoRoute(path: '/verif-otp', builder: (context, state) => VerifotpScreen()), + // GoRoute(path: '/', builder: (context, state) => SplashScreen()), + GoRoute(path: '/', builder: (context, state) => NavigationPage()), + GoRoute( + path: '/onboarding', + builder: (context, state) => OnboardongPageScreen(), + ), + GoRoute( + path: '/navigasi', + builder: (context, state) { + dynamic data = state.extra; + return NavigationPage(data: data); + }, + ), + GoRoute( + path: '/verif-otp', + builder: (context, state) { + final phone = state.extra as String?; + return VerifotpScreen(phone: phone!); + }, + ), + GoRoute(path: '/home', builder: (context, state) => HomeScreen()), + GoRoute( + path: '/requestpickup', + builder: (context, state) => RequestPickScreen(), + ), ], ); diff --git a/lib/main.dart b/lib/main.dart index 4547e96..0277a02 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,21 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:provider/provider.dart'; import 'package:rijig_mobile/core/router.dart'; -// import 'screen/auth/login_screen.dart'; +import 'package:rijig_mobile/viewmodel/auth_vmod.dart'; void main() async { await dotenv.load(fileName: "server/.env.dev"); - runApp(MyApp()); + HttpOverrides.global = MyHttpOverrides(); + WidgetsFlutterBinding.ensureInitialized(); + await initializeDateFormatting( + 'id_ID', + null, + ).then((_) => runApp(const MyApp())); } class MyApp extends StatelessWidget { @@ -16,11 +25,23 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return ScreenUtilInit( designSize: const Size(375, 812), - builder: (_, child) => MaterialApp.router( - // theme: ThemeData(textTheme: textTheme), - debugShowCheckedModeBanner: false, - routerConfig: router, - ), + builder: + (_, child) => ChangeNotifierProvider( + create: (_) => UserViewModel(), + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + routerConfig: router, + ), + ), ); } } + +class MyHttpOverrides extends HttpOverrides { + @override + HttpClient createHttpClient(SecurityContext? context) { + return super.createHttpClient(context) + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + } +} diff --git a/lib/model/auth_model.dart b/lib/model/auth_model.dart new file mode 100644 index 0000000..e205151 --- /dev/null +++ b/lib/model/auth_model.dart @@ -0,0 +1,13 @@ +class AuthModel { + final int status; + final String message; + + AuthModel({required this.status, required this.message}); + + factory AuthModel.fromJson(Map json) { + return AuthModel( + status: json['meta']['status'], + message: json['meta']['message'], + ); + } +} diff --git a/lib/model/product.dart b/lib/model/product.dart new file mode 100644 index 0000000..657ff23 --- /dev/null +++ b/lib/model/product.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; + +class Product { + final int id; + final String title, description; + final List images; + final List colors; + final double rating, price; + final bool isFavourite, isPopular; + + Product({ + required this.id, + required this.images, + required this.colors, + this.rating = 0.0, + this.isFavourite = false, + this.isPopular = false, + required this.title, + required this.price, + required this.description, + }); +} + +// Our demo Products + +List demoProducts = [ + Product( + id: 1, + images: ["assets/image/Image Popular Product 1.png"], + colors: [ + const Color(0xFFF6625E), + const Color(0xFF836DB8), + const Color(0xFFDECB9C), + Colors.white, + ], + title: "Wireless Controller for PS4™", + price: 64.99, + description: description, + rating: 4.8, + isFavourite: true, + isPopular: true, + ), + Product( + id: 2, + images: ["assets/image/Image Popular Product 1.png"], + colors: [ + const Color(0xFFF6625E), + const Color(0xFF836DB8), + const Color(0xFFDECB9C), + Colors.white, + ], + title: "Nike Sport White - Man Pant", + price: 50.5, + description: description, + rating: 4.1, + isPopular: true, + ), + Product( + id: 3, + images: ["assets/image/Image Popular Product 1.png"], + colors: [ + const Color(0xFFF6625E), + const Color(0xFF836DB8), + const Color(0xFFDECB9C), + Colors.white, + ], + title: "Gloves XC Omega - Polygon", + price: 36.55, + description: description, + rating: 4.1, + isFavourite: true, + isPopular: true, + ), + Product( + id: 4, + images: ["assets/image/Image Popular Product 1.png"], + colors: [ + const Color(0xFFF6625E), + const Color(0xFF836DB8), + const Color(0xFFDECB9C), + Colors.white, + ], + title: "Logitech Head", + price: 20.20, + description: description, + rating: 4.1, + isFavourite: true, + ), + // Product( + // id: 1, + // images: [ + // "assets/images/ps4_console_white_1.png", + // "assets/images/ps4_console_white_2.png", + // "assets/images/ps4_console_white_3.png", + // "assets/images/ps4_console_white_4.png", + // ], + // colors: [ + // const Color(0xFFF6625E), + // const Color(0xFF836DB8), + // const Color(0xFFDECB9C), + // Colors.white, + // ], + // title: "Wireless Controller for PS4™", + // price: 64.99, + // description: description, + // rating: 4.8, + // isFavourite: true, + // isPopular: true, + // ), + // Product( + // id: 2, + // images: ["assets/images/Image Popular Product 2.png"], + // colors: [ + // const Color(0xFFF6625E), + // const Color(0xFF836DB8), + // const Color(0xFFDECB9C), + // Colors.white, + // ], + // title: "Nike Sport White - Man Pant", + // price: 50.5, + // description: description, + // rating: 4.1, + // isPopular: true, + // ), + // Product( + // id: 3, + // images: ["assets/images/glap.png"], + // colors: [ + // const Color(0xFFF6625E), + // const Color(0xFF836DB8), + // const Color(0xFFDECB9C), + // Colors.white, + // ], + // title: "Gloves XC Omega - Polygon", + // price: 36.55, + // description: description, + // rating: 4.1, + // isFavourite: true, + // isPopular: true, + // ), + // Product( + // id: 4, + // images: ["assets/images/wireless headset.png"], + // colors: [ + // const Color(0xFFF6625E), + // const Color(0xFF836DB8), + // const Color(0xFFDECB9C), + // Colors.white, + // ], + // title: "Logitech Head", + // price: 20.20, + // description: description, + // rating: 4.1, + // isFavourite: true, + // ), +]; + +const String description = + "Wireless Controller for PS4™ gives you what you want in your gaming from over precision control your games to sharing …"; diff --git a/lib/screen/app/activity/activity_screen.dart b/lib/screen/app/activity/activity_screen.dart new file mode 100644 index 0000000..7fd2f14 --- /dev/null +++ b/lib/screen/app/activity/activity_screen.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class ActivityScreen extends StatefulWidget { + const ActivityScreen({super.key}); + + @override + State createState() => _ActivityScreenState(); +} + +class _ActivityScreenState extends State { + @override + Widget build(BuildContext context) { + final titleofscreen = "Aktivitas"; + return Scaffold( + body: Center(child: Text("ini adalah halaman $titleofscreen")), + ); + } +} diff --git a/lib/screen/app/cart/cart_screen.dart b/lib/screen/app/cart/cart_screen.dart new file mode 100644 index 0000000..1577922 --- /dev/null +++ b/lib/screen/app/cart/cart_screen.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class CartScreen extends StatefulWidget { + const CartScreen({super.key}); + + @override + State createState() => _CartScreenState(); +} + +class _CartScreenState extends State { + @override + Widget build(BuildContext context) { + final titleofscreen = "Cart"; + return Scaffold( + body: Center(child: Text("ini adalah halaman $titleofscreen")), + ); + } +} diff --git a/lib/screen/app/home/components/categories.dart b/lib/screen/app/home/components/categories.dart new file mode 100644 index 0000000..7392d79 --- /dev/null +++ b/lib/screen/app/home/components/categories.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class Categories extends StatelessWidget { + const Categories({super.key}); + + @override + Widget build(BuildContext context) { + List> categories = [ + {"icon": "assets/icons/Flash Icon.svg", "text": "Flash Deal"}, + {"icon": "assets/icons/Bill Icon.svg", "text": "Bill"}, + {"icon": "assets/icons/Game Icon.svg", "text": "Game"}, + {"icon": "assets/icons/Gift Icon.svg", "text": "Daily Gift"}, + {"icon": "assets/icons/Discover.svg", "text": "More"}, + ]; + return Padding( + padding: const EdgeInsets.all(20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + categories.length, + (index) => CategoryCard( + icon: categories[index]["icon"], + text: categories[index]["text"], + press: () {}, + ), + ), + ), + ); + } +} + +class CategoryCard extends StatelessWidget { + const CategoryCard({ + super.key, + required this.icon, + required this.text, + required this.press, + }); + + final String icon, text; + final GestureTapCallback press; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: press, + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + height: 56, + width: 56, + decoration: BoxDecoration( + color: const Color(0xFFFFECDF), + borderRadius: BorderRadius.circular(10), + ), + child: SvgPicture.asset(icon), + ), + const SizedBox(height: 4), + Text(text, textAlign: TextAlign.center) + ], + ), + ); + } +} diff --git a/lib/screen/app/home/components/discount_banner.dart b/lib/screen/app/home/components/discount_banner.dart new file mode 100644 index 0000000..3c06b40 --- /dev/null +++ b/lib/screen/app/home/components/discount_banner.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +class DiscountBanner extends StatelessWidget { + const DiscountBanner({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildCard( + 'Pendapatan', + 'Rp 35.000', + Icons.account_balance_wallet, + ), + _buildCard('Sampah', '10 Kg', Icons.delete), + ], + ), + ], + ), + ); + } + + Widget _buildCard(String title, String value, IconData icon) { + return Expanded( + child: Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + // Icon on the left + Icon(icon, color: Colors.blue, size: 40), + SizedBox(width: 10), + // Column for title and value on the right + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + SizedBox(height: 8), + Text( + value, + style: TextStyle(fontSize: 14, color: Colors.black54), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screen/app/home/components/home_header.dart b/lib/screen/app/home/components/home_header.dart new file mode 100644 index 0000000..c6f104a --- /dev/null +++ b/lib/screen/app/home/components/home_header.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:iconsax_flutter/iconsax_flutter.dart'; + +class HomeHeader extends StatelessWidget { + const HomeHeader({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Rijig", style: TextStyle(fontSize: 24)), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon(Iconsax.notification), + Gap(10), + Icon(Iconsax.message), + ], + ), + ], + ), + ), + const SizedBox(width: 16), + // IconBtnWithCounter( + // svgSrc: "assets/icons/Cart Icon.svg", + // press: () => Navigator.pushNamed(context, CartScreen.routeName), + // ), + // const SizedBox(width: 8), + // IconBtnWithCounter( + // svgSrc: "assets/icons/Bell.svg", + // numOfitem: 3, + // press: () {}, + // ), + ], + ), + ); + } +} diff --git a/lib/screen/app/home/components/icon_btn_with_counter.dart b/lib/screen/app/home/components/icon_btn_with_counter.dart new file mode 100644 index 0000000..2ac3a5e --- /dev/null +++ b/lib/screen/app/home/components/icon_btn_with_counter.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +// import '../../../constants.dart'; + +class IconBtnWithCounter extends StatelessWidget { + const IconBtnWithCounter({ + super.key, + required this.svgSrc, + this.numOfitem = 0, + required this.press, + }); + + final String svgSrc; + final int numOfitem; + final GestureTapCallback press; + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: BorderRadius.circular(100), + onTap: press, + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + padding: const EdgeInsets.all(12), + height: 46, + width: 46, + decoration: BoxDecoration( + // color: kSecondaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: SvgPicture.asset(svgSrc), + ), + if (numOfitem != 0) + Positioned( + top: -3, + right: 0, + child: Container( + height: 20, + width: 20, + decoration: BoxDecoration( + color: const Color(0xFFFF4848), + shape: BoxShape.circle, + border: Border.all(width: 1.5, color: Colors.white), + ), + child: Center( + child: Text( + "$numOfitem", + style: const TextStyle( + fontSize: 12, + height: 1, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/screen/app/home/components/popular_product.dart b/lib/screen/app/home/components/popular_product.dart new file mode 100644 index 0000000..58e39e3 --- /dev/null +++ b/lib/screen/app/home/components/popular_product.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:rijig_mobile/model/product.dart'; +import 'package:rijig_mobile/screen/app/home/components/product_card.dart'; + +import 'section_title.dart'; + +class PopularProducts extends StatelessWidget { + const PopularProducts({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: SectionTitle( + title: "Popular Products", + // press: () { + // Navigator.pushNamed(context, ProductsScreen.routeName); + // }, + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ...List.generate( + demoProducts.length, + (index) { + if (demoProducts[index].isPopular) { + return Padding( + padding: const EdgeInsets.only(left: 20), + child: ProductCard( + product: demoProducts[index], + onPress: (){}, + ), + ); + } + + return const SizedBox + .shrink(); // here by default width and height is 0 + }, + ), + const SizedBox(width: 20), + ], + ), + ) + ], + ); + } +} diff --git a/lib/screen/app/home/components/product_card.dart b/lib/screen/app/home/components/product_card.dart new file mode 100644 index 0000000..d9d9d7e --- /dev/null +++ b/lib/screen/app/home/components/product_card.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:rijig_mobile/core/guide.dart'; +import 'package:rijig_mobile/model/product.dart'; + +class ProductCard extends StatelessWidget { + const ProductCard({ + super.key, + this.width = 140, + this.aspectRetio = 1.02, + required this.product, + required this.onPress, + }); + + final double width, aspectRetio; + final Product product; + final VoidCallback onPress; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + child: GestureDetector( + onTap: onPress, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: 1.02, + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Image.asset(product.images[0]), + ), + ), + const SizedBox(height: 8), + Text( + product.title, + style: Theme.of(context).textTheme.bodyMedium, + maxLines: 2, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "\$${product.price}", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: primaryColor, + ), + ), + InkWell( + borderRadius: BorderRadius.circular(50), + onTap: () {}, + child: Container( + padding: const EdgeInsets.all(6), + height: 24, + width: 24, + decoration: BoxDecoration( + color: + product.isFavourite + ? primaryColor.withValues(alpha: 0.1) + : secondaryColor.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + "assets/icons/Heart Icon_2.svg", + colorFilter: ColorFilter.mode( + product.isFavourite + ? const Color(0xFFFF4848) + : const Color(0xFFDBDEE4), + BlendMode.srcIn, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen/app/home/components/search_field.dart b/lib/screen/app/home/components/search_field.dart new file mode 100644 index 0000000..f29df8d --- /dev/null +++ b/lib/screen/app/home/components/search_field.dart @@ -0,0 +1,34 @@ +// import 'package:flutter/material.dart'; + +// import '../../../constants.dart'; + +// class SearchField extends StatelessWidget { +// const SearchField({ +// Key? key, +// }) : super(key: key); + +// @override +// Widget build(BuildContext context) { +// return Form( +// child: TextFormField( +// onChanged: (value) {}, +// decoration: InputDecoration( +// filled: true, +// fillColor: kSecondaryColor.withOpacity(0.1), +// contentPadding: +// const EdgeInsets.symmetric(horizontal: 16, vertical: 8), +// border: searchOutlineInputBorder, +// focusedBorder: searchOutlineInputBorder, +// enabledBorder: searchOutlineInputBorder, +// hintText: "Search product", +// prefixIcon: const Icon(Icons.search), +// ), +// ), +// ); +// } +// } + +// const searchOutlineInputBorder = OutlineInputBorder( +// borderRadius: BorderRadius.all(Radius.circular(12)), +// borderSide: BorderSide.none, +// ); diff --git a/lib/screen/app/home/components/section_title.dart b/lib/screen/app/home/components/section_title.dart new file mode 100644 index 0000000..0f6ce83 --- /dev/null +++ b/lib/screen/app/home/components/section_title.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class SectionTitle extends StatelessWidget { + const SectionTitle({ + super.key, + required this.title, + // this.press, + }); + + final String title; + // GestureTapCallback press; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + TextButton( + onPressed: (){}, + style: TextButton.styleFrom(foregroundColor: Colors.grey), + child: const Text("See more"), + ), + ], + ); + } +} diff --git a/lib/screen/app/home/components/special_offers.dart b/lib/screen/app/home/components/special_offers.dart new file mode 100644 index 0000000..79325ac --- /dev/null +++ b/lib/screen/app/home/components/special_offers.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; + +import 'section_title.dart'; + +class SpecialOffers extends StatelessWidget { + const SpecialOffers({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: SectionTitle( + title: "Special for you", + // press: () {}, + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + SpecialOfferCard( + image: "assets/image/Image Banner 2.png", + category: "Smartphone", + numOfBrands: 18, + press: () { + // Navigator.pushNamed(context, ProductsScreen.routeName); + }, + ), + SpecialOfferCard( + image: "assets/image/Image Banner 3.png", + category: "Fashion", + numOfBrands: 24, + press: () { + // Navigator.pushNamed(context, ProductsScreen.routeName); + }, + ), + const SizedBox(width: 20), + ], + ), + ), + ], + ); + } +} + +class SpecialOfferCard extends StatelessWidget { + const SpecialOfferCard({ + super.key, + required this.category, + required this.image, + required this.numOfBrands, + required this.press, + }); + + final String category, image; + final int numOfBrands; + final GestureTapCallback press; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 20), + child: GestureDetector( + onTap: press, + child: SizedBox( + width: 242, + height: 100, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Stack( + children: [ + Image.asset(image, fit: BoxFit.cover), + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black54, + Colors.black38, + Colors.black26, + Colors.transparent, + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 10, + ), + child: Text.rich( + TextSpan( + style: const TextStyle(color: Colors.white), + children: [ + TextSpan( + text: "$category\n", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + TextSpan(text: "$numOfBrands Brands"), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screen/app/home/home_screen.dart b/lib/screen/app/home/home_screen.dart new file mode 100644 index 0000000..3c318b6 --- /dev/null +++ b/lib/screen/app/home/home_screen.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:rijig_mobile/screen/app/home/components/categories.dart'; +import 'package:rijig_mobile/screen/app/home/components/discount_banner.dart'; +import 'package:rijig_mobile/screen/app/home/components/home_header.dart'; +import 'package:rijig_mobile/screen/app/home/components/popular_product.dart'; +import 'package:rijig_mobile/screen/app/home/components/special_offers.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + @override + Widget build(BuildContext context) { + // final titleofscreen = "Home"; + return const Scaffold( + body: SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + HomeHeader(), + DiscountBanner(), + Categories(), + SpecialOffers(), + SizedBox(height: 20), + PopularProducts(), + SizedBox(height: 20), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screen/app/profil/profil_screen.dart b/lib/screen/app/profil/profil_screen.dart new file mode 100644 index 0000000..8c68628 --- /dev/null +++ b/lib/screen/app/profil/profil_screen.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class ProfilScreen extends StatefulWidget { + const ProfilScreen({super.key}); + + @override + State createState() => _ProfilScreenState(); +} + +class _ProfilScreenState extends State { + @override + Widget build(BuildContext context) { + final titleofscreen = "Profil"; + return Scaffold( + body: Center(child: Text("ini adalah halaman $titleofscreen")), + ); + } +} diff --git a/lib/screen/app/requestpick/requestpickup_screen.dart b/lib/screen/app/requestpick/requestpickup_screen.dart new file mode 100644 index 0000000..88d172e --- /dev/null +++ b/lib/screen/app/requestpick/requestpickup_screen.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class RequestPickScreen extends StatelessWidget { + const RequestPickScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("Request Pickup")), + body: Center( + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text("Back to Home"), + ), + ), + ); + } +} diff --git a/lib/screen/auth/login_screen.dart b/lib/screen/auth/login_screen.dart index 1f13934..e3cfcbf 100644 --- a/lib/screen/auth/login_screen.dart +++ b/lib/screen/auth/login_screen.dart @@ -1,81 +1,60 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:rijig_mobile/core/router.dart'; +import 'package:rijig_mobile/viewmodel/auth_vmod.dart'; class LoginScreen extends StatelessWidget { - final _formKey = GlobalKey(); + final _phoneController = TextEditingController(); + LoginScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - SizedBox(height: constraints.maxHeight * 0.1), - Text("Halo, Rijig"), - SizedBox(height: constraints.maxHeight * 0.1), - Text( - "Masukkan Nomor Whatsapp", - style: Theme.of(context).textTheme.headlineSmall!.copyWith( - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: constraints.maxHeight * 0.05), - Form( - key: _formKey, - child: Column( - children: [ - TextFormField( - decoration: const InputDecoration( - hintText: 'Phone', - filled: true, - fillColor: Color(0xFFF5FCF9), - contentPadding: EdgeInsets.symmetric( - horizontal: 16.0 * 1.5, - vertical: 16.0, - ), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all( - Radius.circular(50), - ), - ), - ), - keyboardType: TextInputType.phone, - onSaved: (phone) {}, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - ), + appBar: AppBar(title: Text('Login')), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Consumer( + builder: (context, userVM, child) { + if (userVM.authModel?.status == 200) { + Future.delayed(Duration.zero, () { + router.go('/verif-otp', extra: _phoneController.text); + }); + } - ElevatedButton( - onPressed: () { - debugPrint("klik send otp"); - router.push("/verif-otp"); - }, - style: ElevatedButton.styleFrom( - elevation: 0, - backgroundColor: const Color(0xFF00BF6D), - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 48), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(16), - ), - ), - ), - child: const Text("send otp"), - ), - ], + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _phoneController, + keyboardType: TextInputType.phone, + decoration: InputDecoration( + labelText: 'Phone Number', + errorText: userVM.errorMessage, + ), + ), + SizedBox(height: 20), + userVM.isLoading + ? CircularProgressIndicator() + : ElevatedButton( + onPressed: () { + if (_phoneController.text.isNotEmpty) { + userVM.login(_phoneController.text); + } + }, + child: Text('Send OTP'), + ), + if (userVM.authModel != null) + Text( + userVM.authModel!.message, + style: TextStyle( + color: + userVM.authModel!.status == 200 + ? Colors.green + : Colors.red, ), ), - ], - ), + ], ); }, ), diff --git a/lib/screen/auth/otp_screen.dart b/lib/screen/auth/otp_screen.dart index 3fc5060..59765cb 100644 --- a/lib/screen/auth/otp_screen.dart +++ b/lib/screen/auth/otp_screen.dart @@ -1,205 +1,68 @@ -import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:rijig_mobile/core/guide.dart'; +import 'package:provider/provider.dart'; +import 'package:rijig_mobile/core/router.dart'; +import 'package:rijig_mobile/viewmodel/auth_vmod.dart'; -class VerifotpScreen extends StatefulWidget { - const VerifotpScreen({super.key}); +class VerifotpScreen extends StatelessWidget { + final String phone; + final _otpController = TextEditingController(); - @override - State createState() => _VerifotpScreenState(); -} + VerifotpScreen({super.key, required this.phone}); -class _VerifotpScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: whiteColor, - body: SafeArea( - child: SizedBox( - width: double.infinity, - child: Padding( - padding: PaddingCustom().paddingHorizontalVertical(16, 30), - child: SingleChildScrollView( - child: Column( - children: [ - Gap(16), - Text("OTP Verification", style: Tulisan.heading), - Gap(8), - const Text( - "kode otp tela dikirim ke whatsapp 6287874****** \ndan akan kadaluarsa dalam 00:30", - textAlign: TextAlign.center, - style: TextStyle(color: Color(0xFF757575)), + appBar: AppBar(title: Text('Verify OTP')), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Consumer( + builder: (context, userVM, child) { + if (userVM.isLoading) { + return Center(child: CircularProgressIndicator()); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Phone: $phone', style: TextStyle(fontSize: 18)), + SizedBox(height: 20), + + TextField( + controller: _otpController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Enter OTP', + errorText: userVM.errorMessage, ), - // const SizedBox(height: 16), - GapCustom().gapValue(12, true), - SizedBox(height: MediaQuery.of(context).size.height * 0.1), - const OtpForm(), - SizedBox(height: MediaQuery.of(context).size.height * 0.2), - TextButton( - onPressed: () {}, - child: const Text( - "tidak menerima kode otp?\nkirim ulang kode otp", - style: TextStyle(color: Color(0xFF757575)), - textAlign: TextAlign.center, - ), + ), + SizedBox(height: 20), + ElevatedButton( + onPressed: () async { + String otp = _otpController.text; + + if (otp.isNotEmpty) { + await Future.delayed(Duration(milliseconds: 50)); + userVM.verifyOtp(phone, otp); + + if (userVM.authModel?.status == 200) { + debugPrint("routing ke halaman home"); + router.go('/home'); + } + } + }, + child: Text('Verify OTP'), + ), + + if (userVM.errorMessage != null) + Text( + userVM.errorMessage!, + style: TextStyle(color: Colors.red), ), - ], - ), - ), - ), + ], + ); + }, ), ), ); } } - -const authOutlineInputBorder = OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFF757575)), - borderRadius: BorderRadius.all(Radius.circular(12)), -); - -class OtpForm extends StatelessWidget { - const OtpForm({super.key}); - - @override - Widget build(BuildContext context) { - return Form( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - SizedBox( - height: 64, - width: 64, - child: TextFormField( - onSaved: (pin) {}, - onChanged: (pin) { - if (pin.isNotEmpty) { - FocusScope.of(context).nextFocus(); - } - }, - textInputAction: TextInputAction.next, - keyboardType: TextInputType.number, - inputFormatters: [ - LengthLimitingTextInputFormatter(1), - FilteringTextInputFormatter.digitsOnly, - ], - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - decoration: InputDecoration( - hintStyle: const TextStyle(color: Color(0xFF757575)), - border: authOutlineInputBorder, - enabledBorder: authOutlineInputBorder, - focusedBorder: authOutlineInputBorder.copyWith( - borderSide: const BorderSide(color: Color(0xFF00BF6D)), - ), - ), - ), - ), - SizedBox( - height: 64, - width: 64, - child: TextFormField( - onSaved: (pin) {}, - onChanged: (pin) { - if (pin.isNotEmpty) { - FocusScope.of(context).nextFocus(); - } - }, - textInputAction: TextInputAction.next, - keyboardType: TextInputType.number, - inputFormatters: [ - LengthLimitingTextInputFormatter(1), - FilteringTextInputFormatter.digitsOnly, - ], - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - decoration: InputDecoration( - hintStyle: const TextStyle(color: Color(0xFF757575)), - border: authOutlineInputBorder, - enabledBorder: authOutlineInputBorder, - focusedBorder: authOutlineInputBorder.copyWith( - borderSide: const BorderSide(color: Color(0xFF00BF6D)), - ), - ), - ), - ), - SizedBox( - height: 64, - width: 64, - child: TextFormField( - onSaved: (pin) {}, - onChanged: (pin) { - if (pin.isNotEmpty) { - FocusScope.of(context).nextFocus(); - } - }, - textInputAction: TextInputAction.next, - keyboardType: TextInputType.number, - inputFormatters: [ - LengthLimitingTextInputFormatter(1), - FilteringTextInputFormatter.digitsOnly, - ], - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - decoration: InputDecoration( - hintStyle: const TextStyle(color: Color(0xFF757575)), - border: authOutlineInputBorder, - enabledBorder: authOutlineInputBorder, - focusedBorder: authOutlineInputBorder.copyWith( - borderSide: const BorderSide(color: Color(0xFF00BF6D)), - ), - ), - ), - ), - SizedBox( - height: 64, - width: 64, - child: TextFormField( - onSaved: (pin) {}, - onChanged: (pin) { - if (pin.isNotEmpty) { - FocusScope.of(context).nextFocus(); - } - }, - textInputAction: TextInputAction.next, - keyboardType: TextInputType.number, - inputFormatters: [ - LengthLimitingTextInputFormatter(1), - FilteringTextInputFormatter.digitsOnly, - ], - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - decoration: InputDecoration( - hintStyle: const TextStyle(color: Color(0xFF757575)), - border: authOutlineInputBorder, - enabledBorder: authOutlineInputBorder, - focusedBorder: authOutlineInputBorder.copyWith( - borderSide: const BorderSide(color: Color(0xFF00BF6D)), - ), - ), - ), - ), - ], - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - elevation: 0, - backgroundColor: const Color(0xFF00BF6D), - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 48), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16)), - ), - ), - child: const Text("Continue"), - ), - ], - ), - ); - } -} diff --git a/lib/screen/launch/onboardingpage_screen.dart b/lib/screen/launch/onboardingpage_screen.dart new file mode 100644 index 0000000..aac54d3 --- /dev/null +++ b/lib/screen/launch/onboardingpage_screen.dart @@ -0,0 +1,104 @@ +import 'package:concentric_transition/concentric_transition.dart'; +import 'package:flutter/material.dart'; + +final pages = [ + const PageData( + icon: Icons.food_bank_outlined, + title: "Search for your favourite food", + bgColor: Color(0xff3b1791), + textColor: Colors.white, + ), + const PageData( + icon: Icons.shopping_bag_outlined, + title: "Add it to cart", + bgColor: Color(0xfffab800), + textColor: Color(0xff3b1790), + ), + const PageData( + icon: Icons.delivery_dining, + title: "Order and wait", + bgColor: Color(0xffffffff), + textColor: Color(0xff3b1790), + ), +]; + +class OnboardongPageScreen extends StatelessWidget { + const OnboardongPageScreen({super.key}); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + return Scaffold( + body: ConcentricPageView( + colors: pages.map((p) => p.bgColor).toList(), + radius: screenWidth * 0.1, + nextButtonBuilder: + (context) => Padding( + padding: const EdgeInsets.only(left: 3), // visual center + child: Icon(Icons.navigate_next, size: screenWidth * 0.08), + ), + // enable itemcount to disable infinite scroll + // itemCount: pages.length, + // opacityFactor: 2.0, + scaleFactor: 2, + duration: Duration(milliseconds: 500), + // verticalPosition: 0.7, + // direction: Axis.vertical, + // itemCount: pages.length, + // physics: NeverScrollableScrollPhysics(), + itemBuilder: (index) { + final page = pages[index % pages.length]; + return SafeArea(child: _Page(page: page)); + }, + ), + ); + } +} + +class PageData { + final String? title; + final IconData? icon; + final Color bgColor; + final Color textColor; + + const PageData({ + this.title, + this.icon, + this.bgColor = Colors.white, + this.textColor = Colors.black, + }); +} + +class _Page extends StatelessWidget { + final PageData page; + + const _Page({required this.page}); + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(16.0), + margin: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: page.textColor, + ), + child: Icon(page.icon, size: screenHeight * 0.1, color: page.bgColor), + ), + Text( + page.title ?? "", + style: TextStyle( + color: page.textColor, + fontSize: screenHeight * 0.035, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], + ); + } +} diff --git a/lib/screen/launch/splash_screen.dart b/lib/screen/launch/splash_screen.dart new file mode 100644 index 0000000..c667ec8 --- /dev/null +++ b/lib/screen/launch/splash_screen.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:rijig_mobile/core/guide.dart'; +import 'package:rijig_mobile/core/router.dart'; + +class SplashScreen extends StatelessWidget { + const SplashScreen({super.key}); + + @override + Widget build(BuildContext context) { + Future.delayed(Duration(seconds: 3), () { + router.go('/onboarding'); + }); + + return Scaffold( + backgroundColor: whiteColor, + body: Stack( + children: [ + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Image.asset('assets/image/Go_Ride.png', height: 200), + ), + + Align( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.only(bottom: 250.0), + child: Text( + 'Rijig', + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: primaryColor, + fontFamily: 'Roboto', + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/viewmodel/auth_vmod.dart b/lib/viewmodel/auth_vmod.dart new file mode 100644 index 0000000..302a43f --- /dev/null +++ b/lib/viewmodel/auth_vmod.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:rijig_mobile/core/api_services.dart'; +import 'package:rijig_mobile/model/auth_model.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class UserViewModel extends ChangeNotifier { + final ApiService _apiService = ApiService(); + bool isLoading = false; + String? errorMessage; + AuthModel? authModel; + + Future login(String phone) async { + try { + isLoading = true; + errorMessage = null; + notifyListeners(); + + var response = await _apiService.post('/authmasyarakat/auth', { + 'phone': phone, + }); + + authModel = AuthModel.fromJson(response); + + if (authModel?.status == 200) { + } else { + errorMessage = authModel?.message ?? 'Failed to send OTP'; + } + } catch (e) { + if (e is NetworkException) { + errorMessage = e.message; + } else { + errorMessage = 'Something went wrong. Please try again later.'; + } + } finally { + isLoading = false; + notifyListeners(); + } + } + + Future verifyOtp(String phone, String otp) async { + try { + isLoading = true; + errorMessage = null; + notifyListeners(); + + var response = await _apiService.post('/authmasyarakat/verify-otp', { + 'phone': phone, + 'otp': otp, + }); + + if (response['meta']['status'] == 200) { + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('token', response['data']['token']); + await prefs.setString('user_id', response['data']['user_id']); + await prefs.setString('user_role', response['data']['user_role']); + + debugPrint("berhasil login"); + } else { + errorMessage = response['meta']['message'] ?? 'Failed to verify OTP'; + } + } catch (e) { + if (e is NetworkException) { + errorMessage = e.message; + } else { + errorMessage = 'Something went wrong. Please try again later.'; + } + } finally { + isLoading = false; + notifyListeners(); + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 18b4465..9ef4610 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -41,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + concentric_transition: + dependency: "direct main" + description: + name: concentric_transition + sha256: "825191221e4bc6a0cfaf00adbc5cd2cc1333970f61311bce52021f1f68e0a891" + url: "https://pub.dev" + source: hosted + version: "1.0.3" crypto: dependency: transitive description: @@ -110,6 +126,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.9.3" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 + url: "https://pub.dev" + source: hosted + version: "2.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -160,6 +184,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + iconsax_flutter: + dependency: "direct main" + description: + name: iconsax_flutter + sha256: "95b65699da8ea98f87c5d232f06b0debaaf1ec1332b697e4d90969ec9a93037d" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" leak_tracker: dependency: transitive description: @@ -240,6 +280,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: transitive description: @@ -288,6 +336,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" platform: dependency: transitive description: @@ -429,6 +485,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.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: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + url: "https://pub.dev" + source: hosted + version: "1.1.16" vector_math: dependency: transitive description: @@ -461,6 +541,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" sdks: dart: ">=3.7.2 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index b0d8558..33d88ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,15 +8,19 @@ environment: sdk: ^3.7.2 dependencies: + concentric_transition: ^1.0.3 cupertino_icons: ^1.0.8 flutter: sdk: flutter flutter_dotenv: ^5.2.1 flutter_screenutil: ^5.9.3 + flutter_svg: ^2.1.0 gap: ^3.0.1 go_router: ^15.1.1 google_fonts: ^6.0.0 http: ^1.3.0 + iconsax_flutter: ^1.0.0 + intl: ^0.20.2 provider: ^6.1.4 shared_preferences: ^2.3.3 @@ -29,3 +33,4 @@ flutter: uses-material-design: true assets: - server/.env.dev + - assets/image/