Pricing page implemented.

This commit is contained in:
bala 2025-11-29 22:28:02 +05:30
parent a978699dfc
commit 6e9ffdb9dd
18 changed files with 692 additions and 206 deletions

View File

@ -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/dashboard/dashboard_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/store/create_location_screen.dart';
import 'package:autos/presentation/screens/store/store.dart';
@ -65,6 +66,9 @@ class AppRouter {
case AppRoutePaths.products:
return slideRoute(ProductsScreen());
case AppRoutePaths.pricing:
return slideRoute(PricingScreen());
default:
return _defaultFallback(settings);
}

View File

@ -12,4 +12,5 @@ class AppRoutePaths {
static const createStoreLocation = '/createStoreLocation';
static const brands = '/brands';
static const products = '/products';
static const pricing = '/pricing';
}

View File

@ -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),
);
}

View File

@ -13,15 +13,16 @@ class AppTypo {
);
static const TextStyle h2 = TextStyle(
color: Color(0xFF3B81F9),
color: Color(0xFF00BFFF),
fontFamily: fontFamily,
fontSize: 28,
fontWeight: FontWeight.w700,
);
static const TextStyle h3 = TextStyle(
color: Colors.white,
fontFamily: fontFamily,
fontSize: 24,
fontSize: 22,
fontWeight: FontWeight.w600,
);

View File

@ -9,7 +9,7 @@ class HamburgerButton extends StatefulWidget {
super.key,
required this.scaffoldKey,
this.backgroundColor = const Color(0xFFEAF1FF), // light background
this.iconColor = const Color(0xFF3B81F9), // dark blue lines
this.iconColor = const Color(0xFF00BFFF), // dark blue lines
});
@override

View File

@ -22,7 +22,7 @@ class SideMenu extends ConsumerWidget {
children: [
// Header
DrawerHeader(
decoration: BoxDecoration(color: Color(0xFF3B81F9)),
decoration: BoxDecoration(color: Color(0xFF00BFFF)),
child: SizedBox(
width: double.infinity,
child: Row(
@ -71,7 +71,7 @@ class SideMenu extends ConsumerWidget {
// --- ACCOUNT ---
_sectionHeader("ACCOUNT"),
_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,
fontSize: 15,
letterSpacing: 0.5,
color: Color(0xFF3B81F9),
color: Color(0xFF00BFFF),
),
),
),

View File

@ -1,3 +1,4 @@
import 'package:autos/core/theme/app_theme.dart';
import 'package:flutter/material.dart';
class WaveBackground extends StatelessWidget {
@ -19,10 +20,7 @@ class WaveBackground extends StatelessWidget {
height: 220,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF3B81F9), // blue
Color(0xFF5A96F9), // lighter blue
],
colors: [AppTheme.primary, AppTheme.primary],
begin: Alignment.topLeft,
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)
Positioned(
bottom: 0,
@ -65,7 +38,7 @@ class WaveBackground extends StatelessWidget {
clipper: BottomSoftWaveClipper(),
child: Container(
height: 80,
color: const Color(0xFF5A96F9),
color: AppTheme.primary,
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(

View File

@ -1,5 +1,3 @@
import 'package:autos/domain/entities/product.dart';
class ProductModel extends ProductEntity {
@ -12,10 +10,18 @@ class ProductModel extends ProductEntity {
factory ProductModel.fromJson(Map<String, dynamic> json) {
return ProductModel(
id: json['id'],
name: json['name'],
image: json['image'],
price: (json['price'] as num).toDouble(),
id: json['id'] ?? 0,
/// SAFE STRING CONVERSION
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,
);
}
}

View File

@ -42,6 +42,8 @@ class BrandsRepositoryImpl implements BrandsRepository {
await _storage.write(key: 'turn14_token', value: token);
} else if (data["code"] == "TOKEN_VALID") {
// 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') ?? '';
if (token.isEmpty) {
throw Exception("Token missing in storage");

View File

@ -1,5 +1,6 @@
import 'package:autos/core/routing/app_router.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/screens/auth/login_screen.dart';
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
@ -52,9 +53,7 @@ class _MyAppState extends ConsumerState<MyApp> {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: "Autos",
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
theme: AppTheme.lightTheme,
// Dynamic home based on session
home: user != null ? const DashboardScreen() : const LoginScreen(),

View File

@ -22,22 +22,30 @@ class BrandsNotifier extends StateNotifier<AsyncValue<List<BrandEntity>>> {
BrandsNotifier(this.repository, this.ref) : super(const AsyncValue.loading());
/// Fetch brands every time page opens
Future<void> fetchBrands() async {
state = const AsyncValue.loading();
try {
final userAsync = ref.read(userDetailsProvider);
final user = userAsync.value;
Future<void> fetchBrands() async {
state = const AsyncValue.loading();
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);
} catch (e, st) {
state = AsyncValue.error(e, st);
debugPrint("Brands fetch error: $e");
if (user == null || user.id.isEmpty) {
throw Exception("User id is empty");
}
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)
Future<void> saveSelectedBrands(List<BrandEntity> selectedBrands) async {

View File

@ -9,95 +9,131 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.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());
// Provide repository that depends on ApiService
/// ------------------------------------------------------------
/// USER REPOSITORY
/// ------------------------------------------------------------
final userRepositoryProvider = Provider<UserRepositoryImpl>(
(ref) => UserRepositoryImpl(ref.read(apiServiceProvider)),
);
// Manage user state
final loginProvider = StateNotifierProvider<UserNotifier, AsyncValue<User?>>((
ref,
) {
/// ------------------------------------------------------------
/// LOGIN PROVIDER (UNCHANGED NAME)
/// ------------------------------------------------------------
final loginProvider =
StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
final repo = ref.read(userRepositoryProvider);
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);
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);
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 userAsync = ref.watch(userProvider);
// Waiting for login provider to finish
if (userAsync.isLoading) return null;
final user = userAsync.value;
if (user == null) return null;
// Fetch Full Details
final repo = ref.read(userRepositoryProvider);
return await repo.getUserDetails(user.id);
});
/// ------------------------------------------------------------
/// AUTH STATE
/// ------------------------------------------------------------
enum AuthAction { idle, login, signup }
/// ------------------------------------------------------------
/// USER NOTIFIER
/// ------------------------------------------------------------
class UserNotifier extends StateNotifier<AsyncValue<User?>> {
final UserRepositoryImpl repository;
final _storage = const FlutterSecureStorage();
static const _userKey = 'logged_in_user';
AuthAction lastAction = AuthAction.idle;
UserNotifier(this.repository) : super(const AsyncValue.data(null));
///Load saved user from storage (auto-login)
/// LOAD USER FROM STORAGE (FIXED)
Future<void> loadUserFromStorage() async {
final jsonString = await _storage.read(key: _userKey);
if (jsonString != null) {
debugPrint("RESULT: $jsonString");
try {
final jsonString = await _storage.read(key: _userKey);
if (jsonString == null) {
debugPrint("🟡 No user found in storage");
return;
}
final jsonData = jsonDecode(jsonString);
final user = UserModel.fromJson(jsonData);
await Future.microtask(() {});
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 {
lastAction = AuthAction.login;
state = const AsyncValue.loading();
try {
final user = await repository.login(email, password);
// Save user to secure storage
if (user is UserModel) {
await _storage.write(key: _userKey, value: user.toRawJson());
debugPrint("✅ USER SAVED → ID: ${user.id}");
}
state = AsyncValue.data(user);
} catch (e, st) {
state = AsyncValue.error(e, st);
}
}
/// Logout
/// LOGOUT
Future<void> logout() async {
await _storage.delete(key: _userKey);
state = const AsyncValue.data(null);
}
///Sign up
/// SIGNUP
Future<void> signup(
String name,
String email,
@ -106,6 +142,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
) async {
lastAction = AuthAction.signup;
state = const AsyncValue.loading();
try {
final user = await repository.signup(name, email, password, phone);
state = AsyncValue.data(user);
@ -114,9 +151,10 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
}
}
///Reset password
/// RESET PASSWORD
Future<void> sendPasswordResetLink(String email) async {
state = const AsyncValue.loading();
try {
await repository.sendPasswordResetLink(email);
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 {
state = const AsyncValue.loading();
try {
final user = await repository.getUserDetails(userId);
// Save full details separately
await saveUserDetails(user);
state = AsyncValue.data(user);
} catch (e, st) {
state = AsyncValue.error(e, st);
}
}
/// Save full user details separately
/// SAVE FULL DETAILS
Future<void> saveUserDetails(UserModel user) async {
await _storage.write(
key: 'logged_in_user_details',

View File

@ -1,4 +1,6 @@
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/presentation/providers/user_provider.dart';
import 'package:flutter/material.dart';
@ -10,7 +12,8 @@ class ForgotPasswordScreen extends ConsumerStatefulWidget {
const ForgotPasswordScreen({super.key});
@override
ConsumerState<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
ConsumerState<ForgotPasswordScreen> createState() =>
_ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
@ -51,8 +54,9 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
return Scaffold(
appBar: AppBar(
title: const Text("Forgot Password"),
backgroundColor: const Color(0xFF3B81F9),
title: Text("Forgot Password", style: AppTypo.h3),
backgroundColor: AppTheme.primary,
iconTheme: Theme.of(context).appBarTheme.iconTheme,
),
body: Padding(
padding: const EdgeInsets.all(20.0),
@ -62,10 +66,7 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
children: [
const Text(
"Reset your password",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
const Text(
@ -104,7 +105,7 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
Fluttertoast.showToast(
msg: "Reset link sent to $email",
backgroundColor: Colors.green,
toastLength: Toast.LENGTH_LONG
toastLength: Toast.LENGTH_LONG,
);
Navigator.pop(context);
} catch (e) {
@ -115,7 +116,7 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3B81F9),
backgroundColor: AppTheme.primary,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),

View File

@ -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/wave_background.dart';
import 'package:autos/presentation/providers/user_provider.dart';
@ -137,16 +139,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
const Spacer(),
TextButton(
onPressed: () {
Navigator.push(
Navigator.pushNamed(
context,
MaterialPageRoute(
builder: (_) => const ForgotPasswordScreen(),
),
AppRoutePaths.forgotPassword,
);
},
child: const Text(
child: Text(
"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(
backgroundColor: const Color(0xFF3B81F9),
backgroundColor: AppTheme.primary,
minimumSize: Size(
double.infinity,
size.height * 0.05,

View File

@ -1,3 +1,4 @@
import 'package:autos/core/theme/app_typography.dart';
import 'package:flutter/material.dart';
import 'package:autos/core/widgets/hamburger_button.dart';
import 'package:autos/core/widgets/side_menu.dart';
@ -37,10 +38,7 @@ class _EbayScreenState extends State<EbayScreen> {
child: Center(
child: Text(
"eBay Settings",
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.w700,
),
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
),
),
),

View 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),
),
),
],
);
}
}

View File

@ -54,7 +54,8 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
/// Filter products based on search text and stock
List productsFilter(List products) {
return products.where((product) {
final matchesSearch = _searchText.isEmpty ||
final matchesSearch =
_searchText.isEmpty ||
product.name.toLowerCase().contains(_searchText.toLowerCase());
final matchesStock = !_inStockOnly || (product.inStock ?? true);
return matchesSearch && matchesStock;
@ -96,94 +97,61 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20),
child: 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"),
),
);
}
// Apply search / stock filter
final filteredProducts = productsFilter(products);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// SEARCH + FILTER + COUNT ROW (always visible)
Row(
children: [
/// Search + Filter + Count Row
Row(
children: [
// Search bar
Expanded(
child: TextField(
onChanged: (value) {
setState(() => _searchText = value);
},
decoration: InputDecoration(
hintText: "Search products",
prefixIcon: const Icon(Icons.search),
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,
),
// Search bar
Expanded(
child: TextField(
onChanged: (value) {
setState(() => _searchText = value);
},
decoration: InputDecoration(
hintText: "Search products",
prefixIcon: const Icon(Icons.search),
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,
),
const SizedBox(width: 12),
),
),
const SizedBox(width: 12),
// In Stock Only button
ElevatedButton(
onPressed: () {
setState(() => _inStockOnly = !_inStockOnly);
},
style: ElevatedButton.styleFrom(
backgroundColor: _inStockOnly ? Colors.green : Colors.grey,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
"In Stock",
style: const TextStyle(fontSize: 12),
),
// In Stock Only button
ElevatedButton(
onPressed: () {
setState(() => _inStockOnly = !_inStockOnly);
},
style: ElevatedButton.styleFrom(
backgroundColor: _inStockOnly
? Colors.green
: Colors.grey,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
const SizedBox(width: 12),
),
child: Text(
"In Stock",
style: const TextStyle(fontSize: 12),
),
),
const SizedBox(width: 12),
// Count button
ElevatedButton(
// Count button
productsState.when(
data: (products) {
final filteredProducts = productsFilter(products);
return ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
@ -195,22 +163,72 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
"${filteredProducts.length} products",
style: const TextStyle(fontSize: 12),
),
),
],
);
},
loading: () => const SizedBox(),
error: (_, __) => const SizedBox(),
),
const SizedBox(height: 16),
],
),
const SizedBox(height: 16),
/// Products Grid
GridView.builder(
/// PRODUCTS GRID / LOADING / ERROR
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,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredProducts.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.68,
),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.68,
),
itemBuilder: (context, index) {
final product = filteredProducts[index];
@ -270,10 +288,10 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
),
);
},
),
],
);
},
);
},
),
],
),
),

View File

@ -1,4 +1,5 @@
import 'package:autos/core/routing/route_paths.dart';
import 'package:autos/core/theme/app_typography.dart';
import 'package:flutter/material.dart';
import 'package:autos/core/widgets/hamburger_button.dart';
import 'package:autos/core/widgets/side_menu.dart';
@ -34,10 +35,10 @@ class _StoreScreenState extends State<StoreScreen> {
top: topPadding,
left: 0,
right: 0,
child: const Center(
child: Center(
child: Text(
"eBay Locations",
style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700),
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
),
),
),