Pricing page implemented.
This commit is contained in:
parent
a978699dfc
commit
6e9ffdb9dd
@ -5,6 +5,7 @@ import 'package:autos/presentation/screens/auth/sign_up_screen.dart';
|
|||||||
import 'package:autos/presentation/screens/brands/brands_screen.dart';
|
import 'package:autos/presentation/screens/brands/brands_screen.dart';
|
||||||
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
|
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
|
||||||
import 'package:autos/presentation/screens/ebay/ebay_screen.dart';
|
import 'package:autos/presentation/screens/ebay/ebay_screen.dart';
|
||||||
|
import 'package:autos/presentation/screens/pricing/pricing_screen.dart';
|
||||||
import 'package:autos/presentation/screens/products/products_screen.dart';
|
import 'package:autos/presentation/screens/products/products_screen.dart';
|
||||||
import 'package:autos/presentation/screens/store/create_location_screen.dart';
|
import 'package:autos/presentation/screens/store/create_location_screen.dart';
|
||||||
import 'package:autos/presentation/screens/store/store.dart';
|
import 'package:autos/presentation/screens/store/store.dart';
|
||||||
@ -65,6 +66,9 @@ class AppRouter {
|
|||||||
case AppRoutePaths.products:
|
case AppRoutePaths.products:
|
||||||
return slideRoute(ProductsScreen());
|
return slideRoute(ProductsScreen());
|
||||||
|
|
||||||
|
case AppRoutePaths.pricing:
|
||||||
|
return slideRoute(PricingScreen());
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return _defaultFallback(settings);
|
return _defaultFallback(settings);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,4 +12,5 @@ class AppRoutePaths {
|
|||||||
static const createStoreLocation = '/createStoreLocation';
|
static const createStoreLocation = '/createStoreLocation';
|
||||||
static const brands = '/brands';
|
static const brands = '/brands';
|
||||||
static const products = '/products';
|
static const products = '/products';
|
||||||
|
static const pricing = '/pricing';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,116 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppTheme {
|
||||||
|
// ✅ MAIN BRAND COLORS
|
||||||
|
static const Color primary = Color(0xFF00BFFF); // Main Blue
|
||||||
|
static const Color lightBlue = Color(0xFFE8F7FF); // Sidebar highlight
|
||||||
|
static const Color background = Color(0xFFF4FBFE); // App background
|
||||||
|
static const Color textDark = Color(0xFF1C1C1C);
|
||||||
|
static const Color textGrey = Color(0xFF6F6F6F);
|
||||||
|
static const Color cardBorder = Color(0xFFAEE9FF);
|
||||||
|
|
||||||
|
// ✅ GLOBAL APP THEME
|
||||||
|
static ThemeData lightTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
|
||||||
|
scaffoldBackgroundColor: background,
|
||||||
|
|
||||||
|
primaryColor: primary,
|
||||||
|
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: primary,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
),
|
||||||
|
|
||||||
|
fontFamily: 'Poppins',
|
||||||
|
|
||||||
|
// ✅ TEXT STYLES
|
||||||
|
textTheme: const TextTheme(
|
||||||
|
headlineLarge: TextStyle(
|
||||||
|
fontSize: 34,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textDark,
|
||||||
|
),
|
||||||
|
headlineMedium: TextStyle(
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textDark,
|
||||||
|
),
|
||||||
|
headlineSmall: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: textDark,
|
||||||
|
),
|
||||||
|
bodyLarge: TextStyle(fontSize: 16, color: textDark),
|
||||||
|
bodyMedium: TextStyle(fontSize: 14, color: textGrey),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ✅ ELEVATED BUTTON THEME (SUBSCRIBE BUTTONS)
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: primary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 6,
|
||||||
|
shadowColor: Colors.black.withOpacity(0.2),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ✅ OUTLINED BUTTON THEME (UNSELECTED BUTTONS)
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: primary,
|
||||||
|
side: const BorderSide(color: primary),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ✅ INPUT FIELD THEME
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: cardBorder),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: cardBorder),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: primary, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ✅ CARD THEME (PRICING BOXES)
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
color: Colors.white,
|
||||||
|
elevation: 12,
|
||||||
|
shadowColor: Colors.black.withValues(alpha: 0.06),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(26)),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ✅ APP BAR THEME
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: true,
|
||||||
|
iconTheme: IconThemeData(color: Colors.white),
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
color: textDark,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ✅ DIVIDER THEME
|
||||||
|
dividerTheme: const DividerThemeData(color: cardBorder, thickness: 1),
|
||||||
|
|
||||||
|
// ✅ ICON THEME
|
||||||
|
iconTheme: const IconThemeData(color: primary),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -13,15 +13,16 @@ class AppTypo {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static const TextStyle h2 = TextStyle(
|
static const TextStyle h2 = TextStyle(
|
||||||
color: Color(0xFF3B81F9),
|
color: Color(0xFF00BFFF),
|
||||||
fontFamily: fontFamily,
|
fontFamily: fontFamily,
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const TextStyle h3 = TextStyle(
|
static const TextStyle h3 = TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
fontFamily: fontFamily,
|
fontFamily: fontFamily,
|
||||||
fontSize: 24,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ class HamburgerButton extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.scaffoldKey,
|
required this.scaffoldKey,
|
||||||
this.backgroundColor = const Color(0xFFEAF1FF), // light background
|
this.backgroundColor = const Color(0xFFEAF1FF), // light background
|
||||||
this.iconColor = const Color(0xFF3B81F9), // dark blue lines
|
this.iconColor = const Color(0xFF00BFFF), // dark blue lines
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class SideMenu extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Header
|
// Header
|
||||||
DrawerHeader(
|
DrawerHeader(
|
||||||
decoration: BoxDecoration(color: Color(0xFF3B81F9)),
|
decoration: BoxDecoration(color: Color(0xFF00BFFF)),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -71,7 +71,7 @@ class SideMenu extends ConsumerWidget {
|
|||||||
// --- ACCOUNT ---
|
// --- ACCOUNT ---
|
||||||
_sectionHeader("ACCOUNT"),
|
_sectionHeader("ACCOUNT"),
|
||||||
_menuItem(context, "👤", "My Account", AppRoutePaths.auth),
|
_menuItem(context, "👤", "My Account", AppRoutePaths.auth),
|
||||||
_menuItem(context, "💰", "Pricing Plan", 'pricing'),
|
_menuItem(context, "💰", "Pricing Plan", AppRoutePaths.pricing),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -123,7 +123,7 @@ class SideMenu extends ConsumerWidget {
|
|||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
color: Color(0xFF3B81F9),
|
color: Color(0xFF00BFFF),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:autos/core/theme/app_theme.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class WaveBackground extends StatelessWidget {
|
class WaveBackground extends StatelessWidget {
|
||||||
@ -19,10 +20,7 @@ class WaveBackground extends StatelessWidget {
|
|||||||
height: 220,
|
height: 220,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [AppTheme.primary, AppTheme.primary],
|
||||||
Color(0xFF3B81F9), // blue
|
|
||||||
Color(0xFF5A96F9), // lighter blue
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
@ -31,31 +29,6 @@ class WaveBackground extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Positioned(
|
|
||||||
// top: 50,
|
|
||||||
// left: 0,
|
|
||||||
// right: 0,
|
|
||||||
// child: Row(
|
|
||||||
// children: [
|
|
||||||
// Image.asset(
|
|
||||||
// 'assets/auth/ebay.png',
|
|
||||||
// height: 40,
|
|
||||||
// fit: BoxFit.contain,
|
|
||||||
// ),
|
|
||||||
// // Image.asset(
|
|
||||||
// // 'assets/auth/data.png',
|
|
||||||
// // height: 40,
|
|
||||||
// // fit: BoxFit.contain,
|
|
||||||
// // ),
|
|
||||||
// // Image.asset(
|
|
||||||
// // 'assets/auth/ebay.png',
|
|
||||||
// // height: 40,
|
|
||||||
// // fit: BoxFit.contain,
|
|
||||||
// // ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// ✅ Optional bottom light curve (subtle, not dominant)
|
// ✅ Optional bottom light curve (subtle, not dominant)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
@ -65,7 +38,7 @@ class WaveBackground extends StatelessWidget {
|
|||||||
clipper: BottomSoftWaveClipper(),
|
clipper: BottomSoftWaveClipper(),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 80,
|
height: 80,
|
||||||
color: const Color(0xFF5A96F9),
|
color: AppTheme.primary,
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
import 'package:autos/domain/entities/product.dart';
|
import 'package:autos/domain/entities/product.dart';
|
||||||
|
|
||||||
class ProductModel extends ProductEntity {
|
class ProductModel extends ProductEntity {
|
||||||
@ -12,10 +10,18 @@ class ProductModel extends ProductEntity {
|
|||||||
|
|
||||||
factory ProductModel.fromJson(Map<String, dynamic> json) {
|
factory ProductModel.fromJson(Map<String, dynamic> json) {
|
||||||
return ProductModel(
|
return ProductModel(
|
||||||
id: json['id'],
|
id: json['id'] ?? 0,
|
||||||
name: json['name'],
|
|
||||||
image: json['image'],
|
/// ✅ SAFE STRING CONVERSION
|
||||||
price: (json['price'] as num).toDouble(),
|
name: json['name']?.toString() ?? '',
|
||||||
|
|
||||||
|
/// ✅ API sends `logo`, but entity expects `image`
|
||||||
|
image: json['logo']?.toString() ?? '',
|
||||||
|
|
||||||
|
/// ✅ API DOES NOT SEND PRICE → DEFAULT TO 0
|
||||||
|
price: json['price'] != null
|
||||||
|
? double.tryParse(json['price'].toString()) ?? 0.0
|
||||||
|
: 0.0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,8 @@ class BrandsRepositoryImpl implements BrandsRepository {
|
|||||||
await _storage.write(key: 'turn14_token', value: token);
|
await _storage.write(key: 'turn14_token', value: token);
|
||||||
} else if (data["code"] == "TOKEN_VALID") {
|
} else if (data["code"] == "TOKEN_VALID") {
|
||||||
// ✅ Token still valid, read from storage
|
// ✅ Token still valid, read from storage
|
||||||
|
token = data["access_token"];
|
||||||
|
await _storage.write(key: 'turn14_token', value: token);
|
||||||
token = await _storage.read(key: 'turn14_token') ?? '';
|
token = await _storage.read(key: 'turn14_token') ?? '';
|
||||||
if (token.isEmpty) {
|
if (token.isEmpty) {
|
||||||
throw Exception("Token missing in storage");
|
throw Exception("Token missing in storage");
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:autos/core/routing/app_router.dart';
|
import 'package:autos/core/routing/app_router.dart';
|
||||||
import 'package:autos/core/routing/navigation_service.dart';
|
import 'package:autos/core/routing/navigation_service.dart';
|
||||||
|
import 'package:autos/core/theme/app_theme.dart';
|
||||||
import 'package:autos/presentation/providers/user_provider.dart';
|
import 'package:autos/presentation/providers/user_provider.dart';
|
||||||
import 'package:autos/presentation/screens/auth/login_screen.dart';
|
import 'package:autos/presentation/screens/auth/login_screen.dart';
|
||||||
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
|
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
|
||||||
@ -52,9 +53,7 @@ class _MyAppState extends ConsumerState<MyApp> {
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: "Autos",
|
title: "Autos",
|
||||||
theme: ThemeData(
|
theme: AppTheme.lightTheme,
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Dynamic home based on session
|
// Dynamic home based on session
|
||||||
home: user != null ? const DashboardScreen() : const LoginScreen(),
|
home: user != null ? const DashboardScreen() : const LoginScreen(),
|
||||||
|
|||||||
@ -22,22 +22,30 @@ class BrandsNotifier extends StateNotifier<AsyncValue<List<BrandEntity>>> {
|
|||||||
BrandsNotifier(this.repository, this.ref) : super(const AsyncValue.loading());
|
BrandsNotifier(this.repository, this.ref) : super(const AsyncValue.loading());
|
||||||
|
|
||||||
/// Fetch brands every time page opens
|
/// Fetch brands every time page opens
|
||||||
Future<void> fetchBrands() async {
|
Future<void> fetchBrands() async {
|
||||||
state = const AsyncValue.loading();
|
state = const AsyncValue.loading();
|
||||||
try {
|
|
||||||
final userAsync = ref.read(userDetailsProvider);
|
|
||||||
final user = userAsync.value;
|
|
||||||
|
|
||||||
if (user == null) throw Exception("User not logged in");
|
try {
|
||||||
|
/// READ USER DIRECTLY FROM userProvider (NOT userDetailsProvider)
|
||||||
|
final userAsync = ref.read(userProvider);
|
||||||
|
|
||||||
final brands = await repository.getBrands();
|
final user = userAsync.value;
|
||||||
|
|
||||||
state = AsyncValue.data(brands);
|
if (user == null || user.id.isEmpty) {
|
||||||
} catch (e, st) {
|
throw Exception("User id is empty");
|
||||||
state = AsyncValue.error(e, st);
|
|
||||||
debugPrint("Brands fetch error: $e");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugPrint("✅ FETCHING BRANDS FOR USER ID: ${user.id}");
|
||||||
|
|
||||||
|
final brands = await repository.getBrands();
|
||||||
|
|
||||||
|
state = AsyncValue.data(brands);
|
||||||
|
} catch (e, st) {
|
||||||
|
state = AsyncValue.error(e, st);
|
||||||
|
debugPrint("❌ Brands fetch error: $e");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// SAVE SELECTED BRANDS TO SERVER (Bulk Insert)
|
/// SAVE SELECTED BRANDS TO SERVER (Bulk Insert)
|
||||||
Future<void> saveSelectedBrands(List<BrandEntity> selectedBrands) async {
|
Future<void> saveSelectedBrands(List<BrandEntity> selectedBrands) async {
|
||||||
|
|||||||
@ -9,95 +9,131 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:flutter_riverpod/legacy.dart';
|
import 'package:flutter_riverpod/legacy.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
// Provide a single ApiService instance across the app
|
/// ------------------------------------------------------------
|
||||||
|
/// API SERVICE
|
||||||
|
/// ------------------------------------------------------------
|
||||||
final apiServiceProvider = Provider<ApiService>((ref) => ApiService());
|
final apiServiceProvider = Provider<ApiService>((ref) => ApiService());
|
||||||
|
|
||||||
// Provide repository that depends on ApiService
|
/// ------------------------------------------------------------
|
||||||
|
/// USER REPOSITORY
|
||||||
|
/// ------------------------------------------------------------
|
||||||
final userRepositoryProvider = Provider<UserRepositoryImpl>(
|
final userRepositoryProvider = Provider<UserRepositoryImpl>(
|
||||||
(ref) => UserRepositoryImpl(ref.read(apiServiceProvider)),
|
(ref) => UserRepositoryImpl(ref.read(apiServiceProvider)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Manage user state
|
/// ------------------------------------------------------------
|
||||||
final loginProvider = StateNotifierProvider<UserNotifier, AsyncValue<User?>>((
|
/// LOGIN PROVIDER ✅ (UNCHANGED NAME)
|
||||||
ref,
|
/// ------------------------------------------------------------
|
||||||
) {
|
final loginProvider =
|
||||||
|
StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
|
||||||
final repo = ref.read(userRepositoryProvider);
|
final repo = ref.read(userRepositoryProvider);
|
||||||
return UserNotifier(repo);
|
return UserNotifier(repo);
|
||||||
});
|
});
|
||||||
|
|
||||||
final signupProvider = StateNotifierProvider<UserNotifier, AsyncValue<User?>>((
|
/// ------------------------------------------------------------
|
||||||
ref,
|
/// SIGNUP PROVIDER ✅ (UNCHANGED NAME)
|
||||||
) {
|
/// ------------------------------------------------------------
|
||||||
|
final signupProvider =
|
||||||
|
StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
|
||||||
final repo = ref.read(userRepositoryProvider);
|
final repo = ref.read(userRepositoryProvider);
|
||||||
return UserNotifier(repo);
|
return UserNotifier(repo);
|
||||||
});
|
});
|
||||||
|
|
||||||
final userProvider = StateNotifierProvider<UserNotifier, AsyncValue<User?>>((
|
/// ------------------------------------------------------------
|
||||||
ref,
|
/// MAIN USER PROVIDER ✅ (UNCHANGED NAME)
|
||||||
) {
|
/// AUTO LOADS FROM STORAGE
|
||||||
|
/// ------------------------------------------------------------
|
||||||
|
final userProvider =
|
||||||
|
StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
|
||||||
final repo = ref.read(userRepositoryProvider);
|
final repo = ref.read(userRepositoryProvider);
|
||||||
return UserNotifier(repo);
|
final notifier = UserNotifier(repo);
|
||||||
|
|
||||||
|
/// ✅ AUTO RESTORE SESSION ON APP START
|
||||||
|
notifier.loadUserFromStorage();
|
||||||
|
|
||||||
|
return notifier;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// ------------------------------------------------------------
|
||||||
|
/// USER DETAILS PROVIDER ✅ (UNCHANGED NAME)
|
||||||
|
/// ------------------------------------------------------------
|
||||||
final userDetailsProvider = FutureProvider<UserModel?>((ref) async {
|
final userDetailsProvider = FutureProvider<UserModel?>((ref) async {
|
||||||
final userAsync = ref.watch(userProvider);
|
final userAsync = ref.watch(userProvider);
|
||||||
|
|
||||||
// Waiting for login provider to finish
|
|
||||||
if (userAsync.isLoading) return null;
|
if (userAsync.isLoading) return null;
|
||||||
|
|
||||||
final user = userAsync.value;
|
final user = userAsync.value;
|
||||||
if (user == null) return null;
|
if (user == null) return null;
|
||||||
|
|
||||||
// Fetch Full Details
|
|
||||||
final repo = ref.read(userRepositoryProvider);
|
final repo = ref.read(userRepositoryProvider);
|
||||||
return await repo.getUserDetails(user.id);
|
return await repo.getUserDetails(user.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// ------------------------------------------------------------
|
||||||
|
/// AUTH STATE
|
||||||
|
/// ------------------------------------------------------------
|
||||||
enum AuthAction { idle, login, signup }
|
enum AuthAction { idle, login, signup }
|
||||||
|
|
||||||
|
/// ------------------------------------------------------------
|
||||||
|
/// USER NOTIFIER ✅
|
||||||
|
/// ------------------------------------------------------------
|
||||||
class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
||||||
final UserRepositoryImpl repository;
|
final UserRepositoryImpl repository;
|
||||||
final _storage = const FlutterSecureStorage();
|
final _storage = const FlutterSecureStorage();
|
||||||
static const _userKey = 'logged_in_user';
|
static const _userKey = 'logged_in_user';
|
||||||
|
|
||||||
AuthAction lastAction = AuthAction.idle;
|
AuthAction lastAction = AuthAction.idle;
|
||||||
|
|
||||||
UserNotifier(this.repository) : super(const AsyncValue.data(null));
|
UserNotifier(this.repository) : super(const AsyncValue.data(null));
|
||||||
|
|
||||||
///Load saved user from storage (auto-login)
|
/// ✅ LOAD USER FROM STORAGE (FIXED)
|
||||||
Future<void> loadUserFromStorage() async {
|
Future<void> loadUserFromStorage() async {
|
||||||
final jsonString = await _storage.read(key: _userKey);
|
try {
|
||||||
if (jsonString != null) {
|
final jsonString = await _storage.read(key: _userKey);
|
||||||
debugPrint("RESULT: $jsonString");
|
|
||||||
|
if (jsonString == null) {
|
||||||
|
debugPrint("🟡 No user found in storage");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final jsonData = jsonDecode(jsonString);
|
final jsonData = jsonDecode(jsonString);
|
||||||
final user = UserModel.fromJson(jsonData);
|
final user = UserModel.fromJson(jsonData);
|
||||||
await Future.microtask(() {});
|
|
||||||
state = AsyncValue.data(user);
|
state = AsyncValue.data(user);
|
||||||
|
|
||||||
|
debugPrint("✅ USER RESTORED → ID: ${user.id}");
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("❌ Storage restore failed: $e");
|
||||||
|
state = const AsyncValue.data(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///Login
|
/// ✅ LOGIN
|
||||||
Future<void> login(String email, String password) async {
|
Future<void> login(String email, String password) async {
|
||||||
lastAction = AuthAction.login;
|
lastAction = AuthAction.login;
|
||||||
state = const AsyncValue.loading();
|
state = const AsyncValue.loading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final user = await repository.login(email, password);
|
final user = await repository.login(email, password);
|
||||||
// ✅ Save user to secure storage
|
|
||||||
if (user is UserModel) {
|
if (user is UserModel) {
|
||||||
await _storage.write(key: _userKey, value: user.toRawJson());
|
await _storage.write(key: _userKey, value: user.toRawJson());
|
||||||
|
debugPrint("✅ USER SAVED → ID: ${user.id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
state = AsyncValue.data(user);
|
state = AsyncValue.data(user);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
state = AsyncValue.error(e, st);
|
state = AsyncValue.error(e, st);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Logout
|
/// ✅ LOGOUT
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
await _storage.delete(key: _userKey);
|
await _storage.delete(key: _userKey);
|
||||||
state = const AsyncValue.data(null);
|
state = const AsyncValue.data(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
///Sign up
|
/// ✅ SIGNUP
|
||||||
Future<void> signup(
|
Future<void> signup(
|
||||||
String name,
|
String name,
|
||||||
String email,
|
String email,
|
||||||
@ -106,6 +142,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
|||||||
) async {
|
) async {
|
||||||
lastAction = AuthAction.signup;
|
lastAction = AuthAction.signup;
|
||||||
state = const AsyncValue.loading();
|
state = const AsyncValue.loading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final user = await repository.signup(name, email, password, phone);
|
final user = await repository.signup(name, email, password, phone);
|
||||||
state = AsyncValue.data(user);
|
state = AsyncValue.data(user);
|
||||||
@ -114,9 +151,10 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///Reset password
|
/// ✅ RESET PASSWORD
|
||||||
Future<void> sendPasswordResetLink(String email) async {
|
Future<void> sendPasswordResetLink(String email) async {
|
||||||
state = const AsyncValue.loading();
|
state = const AsyncValue.loading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.sendPasswordResetLink(email);
|
await repository.sendPasswordResetLink(email);
|
||||||
state = const AsyncValue.data(null);
|
state = const AsyncValue.data(null);
|
||||||
@ -125,23 +163,20 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch user details from backend
|
/// ✅ FETCH FULL USER DETAILS
|
||||||
Future<void> getUserDetails(String userId) async {
|
Future<void> getUserDetails(String userId) async {
|
||||||
state = const AsyncValue.loading();
|
state = const AsyncValue.loading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final user = await repository.getUserDetails(userId);
|
final user = await repository.getUserDetails(userId);
|
||||||
|
|
||||||
// Save full details separately
|
|
||||||
await saveUserDetails(user);
|
await saveUserDetails(user);
|
||||||
|
|
||||||
state = AsyncValue.data(user);
|
state = AsyncValue.data(user);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
state = AsyncValue.error(e, st);
|
state = AsyncValue.error(e, st);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save full user details separately
|
/// ✅ SAVE FULL DETAILS
|
||||||
Future<void> saveUserDetails(UserModel user) async {
|
Future<void> saveUserDetails(UserModel user) async {
|
||||||
await _storage.write(
|
await _storage.write(
|
||||||
key: 'logged_in_user_details',
|
key: 'logged_in_user_details',
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:autos/core/theme/app_theme.dart';
|
||||||
|
import 'package:autos/core/theme/app_typography.dart';
|
||||||
import 'package:autos/data/models/user_model.dart';
|
import 'package:autos/data/models/user_model.dart';
|
||||||
import 'package:autos/presentation/providers/user_provider.dart';
|
import 'package:autos/presentation/providers/user_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -10,7 +12,8 @@ class ForgotPasswordScreen extends ConsumerStatefulWidget {
|
|||||||
const ForgotPasswordScreen({super.key});
|
const ForgotPasswordScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
|
ConsumerState<ForgotPasswordScreen> createState() =>
|
||||||
|
_ForgotPasswordScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||||
@ -51,8 +54,9 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Forgot Password"),
|
title: Text("Forgot Password", style: AppTypo.h3),
|
||||||
backgroundColor: const Color(0xFF3B81F9),
|
backgroundColor: AppTheme.primary,
|
||||||
|
iconTheme: Theme.of(context).appBarTheme.iconTheme,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
@ -62,10 +66,7 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
"Reset your password",
|
"Reset your password",
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text(
|
const Text(
|
||||||
@ -104,7 +105,7 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
|||||||
Fluttertoast.showToast(
|
Fluttertoast.showToast(
|
||||||
msg: "Reset link sent to $email",
|
msg: "Reset link sent to $email",
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
toastLength: Toast.LENGTH_LONG
|
toastLength: Toast.LENGTH_LONG,
|
||||||
);
|
);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -115,7 +116,7 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF3B81F9),
|
backgroundColor: AppTheme.primary,
|
||||||
minimumSize: const Size(double.infinity, 50),
|
minimumSize: const Size(double.infinity, 50),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(40),
|
borderRadius: BorderRadius.circular(40),
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:autos/core/routing/route_paths.dart';
|
||||||
|
import 'package:autos/core/theme/app_theme.dart';
|
||||||
import 'package:autos/core/widgets/sso_icon_button.dart';
|
import 'package:autos/core/widgets/sso_icon_button.dart';
|
||||||
import 'package:autos/core/widgets/wave_background.dart';
|
import 'package:autos/core/widgets/wave_background.dart';
|
||||||
import 'package:autos/presentation/providers/user_provider.dart';
|
import 'package:autos/presentation/providers/user_provider.dart';
|
||||||
@ -137,16 +139,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.push(
|
Navigator.pushNamed(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
AppRoutePaths.forgotPassword,
|
||||||
builder: (_) => const ForgotPasswordScreen(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: Text(
|
||||||
"Forgot Password?",
|
"Forgot Password?",
|
||||||
style: TextStyle(color: Color(0xFF3B81F9)),
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -167,7 +167,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF3B81F9),
|
backgroundColor: AppTheme.primary,
|
||||||
minimumSize: Size(
|
minimumSize: Size(
|
||||||
double.infinity,
|
double.infinity,
|
||||||
size.height * 0.05,
|
size.height * 0.05,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:autos/core/theme/app_typography.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:autos/core/widgets/hamburger_button.dart';
|
import 'package:autos/core/widgets/hamburger_button.dart';
|
||||||
import 'package:autos/core/widgets/side_menu.dart';
|
import 'package:autos/core/widgets/side_menu.dart';
|
||||||
@ -37,10 +38,7 @@ class _EbayScreenState extends State<EbayScreen> {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"eBay Settings",
|
"eBay Settings",
|
||||||
style: const TextStyle(
|
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
|
||||||
fontSize: 26,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
323
lib/presentation/screens/pricing/pricing_screen.dart
Normal file
323
lib/presentation/screens/pricing/pricing_screen.dart
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
import 'package:autos/core/widgets/hamburger_button.dart';
|
||||||
|
import 'package:autos/core/widgets/side_menu.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:autos/core/theme/app_typography.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class PricingScreen extends ConsumerStatefulWidget {
|
||||||
|
const PricingScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<PricingScreen> createState() => _PricingScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PricingScreenState extends ConsumerState<PricingScreen> {
|
||||||
|
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
String selected = 'pricing';
|
||||||
|
|
||||||
|
bool isMonthly = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final double topPadding = MediaQuery.of(context).padding.top + 16;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
drawer: SideMenu(
|
||||||
|
selected: selected,
|
||||||
|
onItemSelected: (key) {
|
||||||
|
setState(() => selected = key);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
backgroundColor: const Color(0xFFF6FDFF),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
// Top centered title (same style as your dashboard)
|
||||||
|
Positioned(
|
||||||
|
top: topPadding,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Choose Your Plan",
|
||||||
|
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Main content
|
||||||
|
SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, topPadding + 70, 16, 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"🚀 Subscribe to a plan and start automating your eBay listings instantly.",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 15, color: Colors.black54),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 22),
|
||||||
|
|
||||||
|
// Monthly / Yearly toggle
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(40),
|
||||||
|
border: Border.all(color: const Color(0xFF00BFFF)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_toggleButton("Monthly", isMonthly, () {
|
||||||
|
setState(() => isMonthly = true);
|
||||||
|
}),
|
||||||
|
_toggleButton("Yearly (Save 15%)", !isMonthly, () {
|
||||||
|
setState(() => isMonthly = false);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Pricing cards
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
_pricingCard(
|
||||||
|
title: "Starter Sync",
|
||||||
|
subtitle: "Upload up to 100 products per month",
|
||||||
|
price: isMonthly ? "\$49" : "\$499",
|
||||||
|
features: const [
|
||||||
|
"Auto price & inventory updates",
|
||||||
|
"Daily sync",
|
||||||
|
"Manual sync option",
|
||||||
|
"Basic reporting dashboard",
|
||||||
|
],
|
||||||
|
isPopular: false,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
_pricingCard(
|
||||||
|
title: "Growth Sync",
|
||||||
|
subtitle: "Upload up to 250 products per month",
|
||||||
|
price: isMonthly ? "\$99" : "\$999",
|
||||||
|
features: const [
|
||||||
|
"Everything in Starter",
|
||||||
|
"3-hour sync interval",
|
||||||
|
"Bulk product import",
|
||||||
|
"Priority email support",
|
||||||
|
],
|
||||||
|
isPopular: true,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
_pricingCard(
|
||||||
|
title: "Pro Sync",
|
||||||
|
subtitle: "Upload up to 1000 products per month",
|
||||||
|
price: isMonthly ? "\$249" : "\$2499",
|
||||||
|
features: const [
|
||||||
|
"Everything in Growth",
|
||||||
|
"Real-time sync",
|
||||||
|
"Advanced analytics dashboard",
|
||||||
|
"Dedicated account manager",
|
||||||
|
"API access",
|
||||||
|
],
|
||||||
|
isPopular: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
const Divider(thickness: 0.6),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
const Text(
|
||||||
|
"© 2025. Data4Autos. All rights reserved.",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.black45,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Hamburger button (keeps same behavior as other screens)
|
||||||
|
HamburgerButton(scaffoldKey: _scaffoldKey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle button
|
||||||
|
Widget _toggleButton(String text, bool active, VoidCallback onTap) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: active ? const Color(0xFF00BFFF) : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: active ? Colors.white : const Color(0xFF00BFFF),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pricing card widget
|
||||||
|
Widget _pricingCard({
|
||||||
|
required String title,
|
||||||
|
required String subtitle,
|
||||||
|
required String price,
|
||||||
|
required List<String> features,
|
||||||
|
required bool isPopular,
|
||||||
|
}) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
margin: const EdgeInsets.only(top: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: isPopular ? const Color(0xFF00BFFF) : Colors.transparent,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 12,
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
offset: const Offset(0, 6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(color: Colors.black54),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: price,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(
|
||||||
|
text: " / month",
|
||||||
|
style: TextStyle(color: Colors.black54),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
Column(
|
||||||
|
children: features
|
||||||
|
.map(
|
||||||
|
(e) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
color: Color(0xFF00BFFF),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(child: Text(e)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
|
||||||
|
// Subscribe button
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: isPopular
|
||||||
|
? const Color(0xFF00BFFF)
|
||||||
|
: Colors.white,
|
||||||
|
foregroundColor: isPopular
|
||||||
|
? Colors.white
|
||||||
|
: const Color(0xFF00BFFF),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: const BorderSide(color: Color(0xFF00BFFF)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
elevation: isPopular ? 6 : 0,
|
||||||
|
),
|
||||||
|
onPressed: () {},
|
||||||
|
child: Text(
|
||||||
|
isMonthly ? "Subscribe Monthly" : "Subscribe Yearly",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Most popular badge
|
||||||
|
if (isPopular)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF00BFFF),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
"MOST POPULAR",
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -54,7 +54,8 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
/// Filter products based on search text and stock
|
/// Filter products based on search text and stock
|
||||||
List productsFilter(List products) {
|
List productsFilter(List products) {
|
||||||
return products.where((product) {
|
return products.where((product) {
|
||||||
final matchesSearch = _searchText.isEmpty ||
|
final matchesSearch =
|
||||||
|
_searchText.isEmpty ||
|
||||||
product.name.toLowerCase().contains(_searchText.toLowerCase());
|
product.name.toLowerCase().contains(_searchText.toLowerCase());
|
||||||
final matchesStock = !_inStockOnly || (product.inStock ?? true);
|
final matchesStock = !_inStockOnly || (product.inStock ?? true);
|
||||||
return matchesSearch && matchesStock;
|
return matchesSearch && matchesStock;
|
||||||
@ -96,94 +97,61 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20),
|
padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20),
|
||||||
child: productsState.when(
|
child: Column(
|
||||||
/// ✅ LOADING
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
loading: () => const Center(
|
children: [
|
||||||
child: Padding(
|
/// ✅ SEARCH + FILTER + COUNT ROW (always visible)
|
||||||
padding: EdgeInsets.only(top: 120),
|
Row(
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
/// ✅ ERROR
|
|
||||||
error: (e, _) => Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 120),
|
|
||||||
child: Text(
|
|
||||||
(() {
|
|
||||||
final raw = e.toString();
|
|
||||||
// Extract backend message if exists
|
|
||||||
final match = RegExp(r'message[: ]+([^}]+)').firstMatch(raw);
|
|
||||||
if (match != null) return match.group(1)!.trim();
|
|
||||||
return raw.replaceFirst(RegExp(r'^Exception:\s*'), '');
|
|
||||||
})(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(color: Colors.red),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
/// ✅ SUCCESS
|
|
||||||
data: (products) {
|
|
||||||
if (products.isEmpty) {
|
|
||||||
return const Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(top: 120),
|
|
||||||
child: Text("No products found"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply search / stock filter
|
|
||||||
final filteredProducts = productsFilter(products);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
/// ✅ Search + Filter + Count Row
|
// Search bar
|
||||||
Row(
|
Expanded(
|
||||||
children: [
|
child: TextField(
|
||||||
// Search bar
|
onChanged: (value) {
|
||||||
Expanded(
|
setState(() => _searchText = value);
|
||||||
child: TextField(
|
},
|
||||||
onChanged: (value) {
|
decoration: InputDecoration(
|
||||||
setState(() => _searchText = value);
|
hintText: "Search products",
|
||||||
},
|
prefixIcon: const Icon(Icons.search),
|
||||||
decoration: InputDecoration(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
hintText: "Search products",
|
vertical: 0,
|
||||||
prefixIcon: const Icon(Icons.search),
|
horizontal: 16,
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: const BorderSide(color: Colors.grey),
|
|
||||||
),
|
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: Colors.grey),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
// In Stock Only button
|
// In Stock Only button
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() => _inStockOnly = !_inStockOnly);
|
setState(() => _inStockOnly = !_inStockOnly);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: _inStockOnly ? Colors.green : Colors.grey,
|
backgroundColor: _inStockOnly
|
||||||
shape: RoundedRectangleBorder(
|
? Colors.green
|
||||||
borderRadius: BorderRadius.circular(12),
|
: Colors.grey,
|
||||||
),
|
shape: RoundedRectangleBorder(
|
||||||
),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Text(
|
|
||||||
"In Stock",
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
),
|
||||||
|
child: Text(
|
||||||
|
"In Stock",
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
// Count button
|
// Count button
|
||||||
ElevatedButton(
|
productsState.when(
|
||||||
|
data: (products) {
|
||||||
|
final filteredProducts = productsFilter(products);
|
||||||
|
return ElevatedButton(
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.deepPurple,
|
backgroundColor: Colors.deepPurple,
|
||||||
@ -195,22 +163,72 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
"${filteredProducts.length} products",
|
"${filteredProducts.length} products",
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
|
loading: () => const SizedBox(),
|
||||||
|
error: (_, __) => const SizedBox(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
/// ✅ Products Grid
|
/// ✅ PRODUCTS GRID / LOADING / ERROR
|
||||||
GridView.builder(
|
productsState.when(
|
||||||
|
/// ✅ LOADING
|
||||||
|
loading: () => const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(top: 120),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ ERROR
|
||||||
|
error: (e, _) => Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 120),
|
||||||
|
child: Text(
|
||||||
|
(() {
|
||||||
|
final raw = e.toString();
|
||||||
|
// Extract backend message if exists
|
||||||
|
final match = RegExp(
|
||||||
|
r'message[: ]+([^}]+)',
|
||||||
|
).firstMatch(raw);
|
||||||
|
if (match != null) return match.group(1)!.trim();
|
||||||
|
return raw.replaceFirst(
|
||||||
|
RegExp(r'^Exception:\s*'),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ SUCCESS
|
||||||
|
data: (products) {
|
||||||
|
if (products.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(top: 120),
|
||||||
|
child: Text("No products found"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final filteredProducts = productsFilter(products);
|
||||||
|
|
||||||
|
return GridView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: filteredProducts.length,
|
itemCount: filteredProducts.length,
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate:
|
||||||
crossAxisCount: 2,
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisSpacing: 12,
|
crossAxisCount: 2,
|
||||||
mainAxisSpacing: 12,
|
crossAxisSpacing: 12,
|
||||||
childAspectRatio: 0.68,
|
mainAxisSpacing: 12,
|
||||||
),
|
childAspectRatio: 0.68,
|
||||||
|
),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final product = filteredProducts[index];
|
final product = filteredProducts[index];
|
||||||
|
|
||||||
@ -270,10 +288,10 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:autos/core/routing/route_paths.dart';
|
import 'package:autos/core/routing/route_paths.dart';
|
||||||
|
import 'package:autos/core/theme/app_typography.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:autos/core/widgets/hamburger_button.dart';
|
import 'package:autos/core/widgets/hamburger_button.dart';
|
||||||
import 'package:autos/core/widgets/side_menu.dart';
|
import 'package:autos/core/widgets/side_menu.dart';
|
||||||
@ -34,10 +35,10 @@ class _StoreScreenState extends State<StoreScreen> {
|
|||||||
top: topPadding,
|
top: topPadding,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: const Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"eBay Locations",
|
"eBay Locations",
|
||||||
style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700),
|
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user