Initialize project and update portal port configuration

Set default portal port to 8081, fix Dart build issue in cart screen, and update setup documentation.

Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
rbhat
2026-04-10 19:08:30 +05:30
commit 39a4f3283f
29 changed files with 1405 additions and 0 deletions

View File

@ -0,0 +1,30 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
import 'home_screen.dart';
import 'login_screen.dart';
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: AuthService.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasData) {
return const HomeScreen();
}
return const LoginScreen();
},
);
}
}

View File

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/cart_service.dart';
import 'checkout_screen.dart';
class CartScreen extends StatelessWidget {
const CartScreen({super.key});
@override
Widget build(BuildContext context) {
final cart = context.watch<CartService>();
return Scaffold(
appBar: AppBar(title: const Text('Your Cart')),
body: cart.items.isEmpty
? const Center(child: Text('Your cart is empty.'))
: Column(
children: [
Expanded(
child: ListView.separated(
itemCount: cart.items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final item = cart.items[index];
return ListTile(
leading: SizedBox(
width: 64,
child:
Image.network(item.product.imageUrl, fit: BoxFit.cover),
),
title: Text(item.product.name),
subtitle:
Text('\$${item.product.price.toStringAsFixed(2)} each'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: item.quantity > 1
? () => cart.updateQuantity(
item.product.id,
item.quantity - 1,
)
: null,
),
Text('${item.quantity}'),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () => cart.updateQuantity(
item.product.id,
item.quantity + 1,
),
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => cart.removeProduct(item.product.id),
),
],
),
);
},
),
),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey.shade300),
),
),
child: Row(
children: [
Text(
'Total: \$${cart.totalPrice.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
FilledButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const CheckoutScreen(),
),
);
},
child: const Text('Proceed to Checkout'),
),
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class CheckoutScreen extends StatelessWidget {
const CheckoutScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Checkout')),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: Card(
margin: const EdgeInsets.all(20),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Checkout (Skeleton)',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Text('This page is intentionally a placeholder for:'),
SizedBox(height: 8),
Text('- Shipping address collection'),
Text('- Payment gateway integration'),
Text('- Order review and confirmation flow'),
],
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@override
State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
bool _loading = false;
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _loading = true);
try {
await AuthService.instance.sendPasswordResetEmail(
_emailController.text.trim(),
);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Password reset email sent.')),
);
Navigator.of(context).pop();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString())),
);
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Reset Password')),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Card(
margin: const EdgeInsets.all(20),
child: Padding(
padding: const EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
validator: (value) =>
(value == null || value.trim().isEmpty)
? 'Email is required'
: null,
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: _loading ? null : _submit,
child: _loading
? const SizedBox(
width: 18,
height: 18,
child:
CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Send Reset Link'),
),
)
],
),
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/auth_service.dart';
import '../services/cart_service.dart';
import '../services/product_repository.dart';
import '../widgets/product_card.dart';
import 'cart_screen.dart';
import 'product_detail_screen.dart';
import 'profile_screen.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final cartCount = context.watch<CartService>().totalItems;
return Scaffold(
appBar: AppBar(
title: const Text('E-Commerce Portal'),
actions: [
IconButton(
tooltip: 'Profile',
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ProfileScreen()),
);
},
icon: const Icon(Icons.person),
),
Stack(
children: [
IconButton(
tooltip: 'Cart',
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const CartScreen()),
);
},
icon: const Icon(Icons.shopping_cart_outlined),
),
if (cartCount > 0)
Positioned(
right: 8,
top: 8,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
child: Text(
'$cartCount',
style: const TextStyle(
color: Colors.white,
fontSize: 11,
),
),
),
),
],
),
IconButton(
tooltip: 'Sign out',
onPressed: () async {
await AuthService.instance.signOut();
},
icon: const Icon(Icons.logout),
),
],
),
body: Padding(
padding: const EdgeInsets.all(12),
child: LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
int count = 1;
if (width > 1200) {
count = 4;
} else if (width > 900) {
count = 3;
} else if (width > 600) {
count = 2;
}
return GridView.builder(
itemCount: ProductRepository.products.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: count,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.72,
),
itemBuilder: (context, index) {
final product = ProductRepository.products[index];
return ProductCard(
product: product,
onView: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ProductDetailScreen(product: product),
),
);
},
onAddToCart: () {
context.read<CartService>().addProduct(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name} added to cart')),
);
},
);
},
);
},
),
),
);
}
}

View File

@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
import 'forgot_password_screen.dart';
import 'register_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _loading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _run(Future<void> Function() action) async {
setState(() => _loading = true);
try {
await action();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString())),
);
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Card(
margin: const EdgeInsets.all(20),
child: Padding(
padding: const EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Welcome Back',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
validator: (value) =>
(value == null || value.trim().isEmpty)
? 'Email is required'
: null,
),
const SizedBox(height: 12),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
validator: (value) =>
(value == null || value.isEmpty)
? 'Password is required'
: null,
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: _loading
? null
: () {
if (!_formKey.currentState!.validate()) return;
_run(() async {
await AuthService.instance
.signInWithEmailPassword(
email: _emailController.text.trim(),
password: _passwordController.text,
);
});
},
child: _loading
? const SizedBox(
width: 18,
height: 18,
child:
CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Sign In'),
),
),
TextButton(
onPressed: _loading
? null
: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const ForgotPasswordScreen(),
),
);
},
child: const Text('Forgot Password?'),
),
const Divider(height: 24),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _loading
? null
: () => _run(() async {
await AuthService.instance.signInWithGoogle();
}),
icon: const Icon(Icons.login),
label: const Text('Continue with Google'),
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _loading
? null
: () => _run(() async {
await AuthService.instance.signInWithGithub();
}),
icon: const Icon(Icons.code),
label: const Text('Continue with GitHub'),
),
),
const SizedBox(height: 8),
TextButton(
onPressed: _loading
? null
: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const RegisterScreen(),
),
);
},
child: const Text('Create an account'),
),
],
),
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/product.dart';
import '../services/cart_service.dart';
class ProductDetailScreen extends StatelessWidget {
final Product product;
const ProductDetailScreen({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(product.name)),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1100),
child: Padding(
padding: const EdgeInsets.all(16),
child: Wrap(
spacing: 20,
runSpacing: 20,
children: [
SizedBox(
width: 480,
child: AspectRatio(
aspectRatio: 4 / 3,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(product.imageUrl, fit: BoxFit.cover),
),
),
),
SizedBox(
width: 460,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 10),
Text(
'\$${product.price.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(product.description),
const SizedBox(height: 20),
FilledButton.icon(
onPressed: () {
context.read<CartService>().addProduct(product);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Item added to cart')),
);
},
icon: const Icon(Icons.add_shopping_cart),
label: const Text('Add to Cart'),
),
],
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
final user = AuthService.instance.currentUser;
final providers = user?.providerData.map((p) => p.providerId).join(', ') ?? '';
return Scaffold(
appBar: AppBar(title: const Text('Your Profile')),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Card(
margin: const EdgeInsets.all(20),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Account Information',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Text('Display name: ${user?.displayName ?? 'Not set'}'),
Text('Email: ${user?.email ?? 'N/A'}'),
Text('UID: ${user?.uid ?? 'N/A'}'),
Text('Providers: $providers'),
const SizedBox(height: 16),
FilledButton.tonalIcon(
onPressed: () async {
await AuthService.instance.signOut();
if (!context.mounted) return;
Navigator.of(context).pop();
},
icon: const Icon(Icons.logout),
label: const Text('Sign out'),
),
],
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmController = TextEditingController();
bool _loading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_confirmController.dispose();
super.dispose();
}
Future<void> _register() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _loading = true);
try {
await AuthService.instance.registerWithEmailPassword(
email: _emailController.text.trim(),
password: _passwordController.text,
);
if (!mounted) return;
Navigator.of(context).pop();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString())),
);
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Register')),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Card(
margin: const EdgeInsets.all(20),
child: Padding(
padding: const EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
validator: (value) =>
(value == null || value.trim().isEmpty)
? 'Email is required'
: null,
),
const SizedBox(height: 12),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration:
const InputDecoration(labelText: 'Password (min 6)'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 6) {
return 'Must be at least 6 characters';
}
return null;
},
),
const SizedBox(height: 12),
TextFormField(
controller: _confirmController,
obscureText: true,
decoration:
const InputDecoration(labelText: 'Confirm Password'),
validator: (value) {
if (value != _passwordController.text) {
return 'Passwords do not match';
}
return null;
},
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: _loading ? null : _register,
child: _loading
? const SizedBox(
width: 18,
height: 18,
child:
CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Create Account'),
),
),
],
),
),
),
),
),
),
);
}
}