Unverified Commit 5ef5d4ce authored by sxfzn's avatar sxfzn Committed by GitHub

Add files via upload

parent 7e291a8b
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart'; import 'package:paperchase_app/seller_profile_page.dart';
import 'package:paperchase_app/chat_page.dart'; import 'package:paperchase_app/chat_page.dart'; // Import chat page
import 'colors.dart';
class BookDetailsPage extends StatefulWidget {
class BookDetailsPage extends StatelessWidget {
final Map<String, dynamic> book; final Map<String, dynamic> book;
final String bookId; final String bookId;
const BookDetailsPage({super.key, required this.book, required this.bookId}); const BookDetailsPage({super.key, required this.book, required this.bookId});
@override
State<BookDetailsPage> createState() => _BookDetailsPageState();
}
class _BookDetailsPageState extends State<BookDetailsPage> {
late Future<DocumentSnapshot> _sellerFuture;
bool _hasReviewed = false;
int _userRating = 0;
final _reviewController = TextEditingController();
bool _isSubmitting = false;
bool _isContactingSellerLoading = false;
bool _isDeleting = false; // Track deletion status
@override
void initState() {
super.initState();
_sellerFuture = FirebaseFirestore.instance
.collection('users')
.doc(widget.book['userId'])
.get();
_checkExistingReview();
}
Future<void> _checkExistingReview() async {
final currentUser = FirebaseAuth.instance.currentUser;
if (currentUser == null) return;
try {
final reviewDoc = await FirebaseFirestore.instance
.collection('users')
.doc(widget.book['userId'])
.collection('reviews')
.doc(currentUser.uid)
.get();
if (reviewDoc.exists) {
if (mounted) {
setState(() {
_hasReviewed = true;
_userRating = reviewDoc.data()?['rating'] ?? 0;
_reviewController.text = reviewDoc.data()?['comment'] ?? '';
});
}
}
} catch (e) {
print('Error checking existing review: $e');
}
}
Future<void> _contactSeller(BuildContext context, String sellerName) async {
final currentUser = FirebaseAuth.instance.currentUser;
if (currentUser == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please log in to contact the seller')),
);
return;
}
setState(() {
_isContactingSellerLoading = true;
});
try {
// Get current user info
final userDoc = await FirebaseFirestore.instance
.collection('users')
.doc(currentUser.uid)
.get();
final buyerName = userDoc.exists
? "${userDoc.data()?['first_name'] ?? ''} ${userDoc.data()?['last_name'] ?? ''}".trim()
: "Anonymous User";
// Check if a chat already exists between these users for this book
final existingChatQuery = await FirebaseFirestore.instance
.collection('chats')
.where('bookId', isEqualTo: widget.bookId)
.where('users', arrayContains: currentUser.uid)
.get();
String chatId;
if (existingChatQuery.docs.isNotEmpty) {
// Chat already exists, use existing chat
chatId = existingChatQuery.docs.first.id;
} else {
// Create a new chat document
final chatRef = FirebaseFirestore.instance.collection('chats').doc();
chatId = chatRef.id;
await chatRef.set({
'users': [currentUser.uid, widget.book['userId']],
'buyerId': currentUser.uid,
'sellerId': widget.book['userId'],
'bookId': widget.bookId,
'bookTitle': widget.book['title'] ?? 'Unknown Book',
'createdAt': FieldValue.serverTimestamp(),
'lastMessage': 'No messages yet',
'lastMessageTime': FieldValue.serverTimestamp(),
'lastMessageSenderId': '',
});
}
if (mounted) {
setState(() {
_isContactingSellerLoading = false;
});
// Navigate to the chat page
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StrictChatPage(
chatId: chatId,
otherUserName: sellerName,
predefinedMessages: const [
"Is this still available?",
"When can we meet?",
"I'll take it",
"Thanks!",
"Hello",
"Can you hold it for me?",
"What's your lowest price?",
],
),
),
);
}
} catch (e) {
if (mounted) {
setState(() {
_isContactingSellerLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error contacting seller: $e')),
);
}
}
}
@override
void dispose() {
_reviewController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
final currentUser = FirebaseAuth.instance.currentUser; final currentUser = FirebaseAuth.instance.currentUser;
final isMyBook = currentUser?.uid == book['userId']; final isMyBook = currentUser?.uid == widget.book['userId'];
final title = widget.book['title'] ?? 'No title available';
final title = book['title'] ?? 'No title available'; final author = widget.book['author'] ?? 'No author available';
final author = book['author'] ?? 'No author available'; final isbn = widget.book['isbn'] ?? 'No ISBN available';
final isbn = book['isbn'] ?? 'No ISBN available'; final price = widget.book['price'] is String
final price = book['price'] is String ? double.tryParse(widget.book['price']) ?? 0.0
? double.tryParse(book['price']) ?? 0.0 : widget.book['price'] ?? 0.0;
: book['price'] ?? 0.0; final condition = widget.book['condition'] ?? 'Condition not available';
final condition = book['condition'] ?? 'Condition not available'; final description = widget.book['description'] ?? 'No description available';
final description = book['description'] ?? 'No description available'; final imageUrl = widget.book['imageUrl'] ?? 'https://via.placeholder.com/200';
final imageUrl = book['imageUrl'] ?? 'https://via.placeholder.com/200'; final postedDate = widget.book['createdAt'] != null
? _formatDate(widget.book['createdAt'])
: 'Date not available';
return Scaffold( return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[100],
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: true, backgroundColor: isDarkMode ? Colors.black : Colors.white,
iconTheme: IconThemeData( elevation: 0,
color: isDarkMode ? kDarkBackground : kLightBackground, leading: IconButton(
), icon: Icon(Icons.arrow_back, color: isDarkMode ? Colors.white : Colors.black),
title: Text( onPressed: () => Navigator.pop(context),
title,
style: TextStyle(
fontFamily: 'Impact',
fontSize: 24,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold,
color: kPrimaryColor,
),
), ),
actions: [
if (isMyBook)
IconButton(
icon: Icon(Icons.more_vert, color: isDarkMode ? Colors.white : Colors.black),
onPressed: () => _showOptionsMenu(context),
),
],
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Column(
padding: const EdgeInsets.all(16.0), crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, AspectRatio(
children: [ aspectRatio: 1.0,
Center( child: Container(
width: double.infinity,
color: Colors.grey[300],
child: imageUrl.isNotEmpty child: imageUrl.isNotEmpty
? Image.network(imageUrl, height: 200, fit: BoxFit.cover) ? Image.network(
: Icon(Icons.book, size: 100), imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Center(child: Icon(Icons.book, size: 100, color: Colors.grey[500]));
},
)
: Center(child: Icon(Icons.book, size: 100, color: Colors.grey[500])),
), ),
const SizedBox(height: 20), ),
Text("Title: $title",
style: Container(
const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), color: isDarkMode ? Colors.black : Colors.white,
Text("Author: $author", style: const TextStyle(fontSize: 18)), padding: const EdgeInsets.all(16.0),
Text("ISBN: $isbn", style: const TextStyle(fontSize: 16)), child: Column(
Text("Price: \$${price.toStringAsFixed(2)}", crossAxisAlignment: CrossAxisAlignment.start,
style: const TextStyle(fontSize: 16, color: Colors.green)), children: [
Text("Condition: $condition", style: const TextStyle(fontSize: 16)), Row(
const SizedBox(height: 10), mainAxisAlignment: MainAxisAlignment.spaceBetween,
const Text("Description:", children: [
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), Text(
Text(description, style: const TextStyle(fontSize: 16)), '\$${price.toStringAsFixed(2)}',
const SizedBox(height: 24), style: TextStyle(
if (isMyBook && currentUser != null) fontSize: 24,
SizedBox( fontWeight: FontWeight.bold,
width: double.infinity, color: isDarkMode ? Colors.white : Colors.black,
child: ElevatedButton( ),
onPressed: () => _confirmAndDeleteBook(context, bookId),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
), ),
), ],
child: const Text( ),
'Delete Book', const SizedBox(height: 8),
style: TextStyle(fontSize: 18, color: Colors.white), Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black,
), ),
), ),
) const SizedBox(height: 4),
],
else if (!isMyBook && currentUser != null) ),
SizedBox( ),
width: double.infinity,
child: ElevatedButton( const SizedBox(height: 8),
onPressed: () =>
_contactSeller(context, book, bookId), // Seller Section with Rating
style: ElevatedButton.styleFrom( GestureDetector(
backgroundColor: kPrimaryColor, onTap: () {
padding: const EdgeInsets.symmetric(vertical: 16), Navigator.push(
shape: RoundedRectangleBorder( context,
borderRadius: BorderRadius.circular(12), MaterialPageRoute(
), builder: (context) => SellerProfilePage(
sellerId: widget.book['userId'],
), ),
child: const Text( ),
'Contact Seller', );
style: TextStyle(fontSize: 18, color: Colors.white), },
child: Container(
color: isDarkMode ? Colors.black : Colors.white,
padding: const EdgeInsets.all(16.0),
child: FutureBuilder<DocumentSnapshot>(
future: _sellerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
String sellerName = 'Unknown Seller';
String sellerAvatar = '';
double averageRating = 0;
int reviewCount = 0;
if (snapshot.hasData && snapshot.data!.exists) {
final userData = snapshot.data!.data() as Map<String, dynamic>?;
if (userData != null) {
sellerName = "${userData['first_name'] ?? ''} ${userData['last_name'] ?? ''}".trim();
sellerAvatar = userData['avatar_url'] ?? '';
averageRating = userData['average_rating']?.toDouble() ?? 0.0;
reviewCount = userData['review_count']?.toInt() ?? 0;
}
}
return Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: Colors.grey[300],
backgroundImage: sellerAvatar.isNotEmpty ? NetworkImage(sellerAvatar) : null,
child: sellerAvatar.isEmpty ? const Icon(Icons.person, color: Colors.white) : null,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
sellerName,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black,
),
),
Text(
isMyBook ? 'You' : 'Seller',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
Row(
children: [
_buildRatingStars(averageRating, size: 16),
Text(
' (${reviewCount.toString()})',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
],
),
),
const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
],
);
},
),
),
),
const SizedBox(height: 8),
// Description section
Container(
color: isDarkMode ? Colors.black : Colors.white,
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Description',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
), ),
), ),
const SizedBox(height: 12),
) Text(
description,
else if (currentUser == null) style: TextStyle(
Center( fontSize: 16,
child: TextButton( color: isDarkMode ? Colors.white70 : Colors.black87,
onPressed: () =>
Navigator.pushNamed(context, '/login'),
child: const Text(
'Log in to contact the seller',
style: TextStyle(fontSize: 16),
), ),
), ),
],
),
),
const SizedBox(height: 8),
// Review section - only visible to buyers (not the seller's own book)
if (!isMyBook)
Container(
color: isDarkMode ? Colors.black : Colors.white,
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_hasReviewed ? 'Your Review' : 'Rate this Seller',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
return IconButton(
icon: Icon(
index < _userRating ? Icons.star : Icons.star_border,
color: index < _userRating ? Colors.amber : Colors.grey,
size: 36,
),
onPressed: _hasReviewed && !currentUser!.isAnonymous
? null
: () {
setState(() {
_userRating = index + 1;
});
},
);
}),
),
const SizedBox(height: 16),
TextField(
controller: _reviewController,
maxLines: 3,
readOnly: _hasReviewed && !currentUser!.isAnonymous,
decoration: InputDecoration(
hintText: 'Write your review (optional)',
border: OutlineInputBorder(),
filled: true,
fillColor: isDarkMode ? Colors.grey[900] : Colors.grey[100],
),
),
const SizedBox(height: 16),
if (!_hasReviewed)
Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
),
onPressed: _isSubmitting || _userRating == 0 || currentUser == null
? null
: () => _submitReview(context),
child: _isSubmitting
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(color: Colors.white),
)
: const Text(
'Submit Review',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
if (_hasReviewed)
Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
),
onPressed: currentUser == null ? null : () => _editReview(context),
child: const Text(
'Edit Review',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
if (currentUser == null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Center(
child: Text(
'Sign in to leave a review',
style: TextStyle(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
),
),
],
), ),
], ),
),
const SizedBox(height: 80),
],
), ),
), ),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: !isMyBook
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, ? Container(
selectedItemColor: decoration: BoxDecoration(
isDarkMode ? kDarkBackground : kLightBackground, color: isDarkMode ? Colors.black : Colors.white,
unselectedItemColor: boxShadow: [
isDarkMode ? kDarkBackground : kLightBackground, BoxShadow(
currentIndex: 2, color: Colors.black.withOpacity(0.1),
items: const [ blurRadius: 5,
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"), offset: const Offset(0, -3),
BottomNavigationBarItem(icon: Icon(Icons.add), label: "Post"), ),
BottomNavigationBarItem(icon: Icon(Icons.mail), label: "Inbox"), ],
], ),
onTap: (index) { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
if (index == 0) { child: FutureBuilder<DocumentSnapshot>(
Navigator.pushNamedAndRemoveUntil(context, '/', (_) => false); future: _sellerFuture,
} else if (index == 1) { builder: (context, snapshot) {
Navigator.pushNamed(context, '/post'); String sellerName = 'Unknown Seller';
} else if (index == 2) {
Navigator.pushNamed(context, '/inbox'); if (snapshot.hasData && snapshot.data!.exists) {
} final userData = snapshot.data!.data() as Map<String, dynamic>?;
}, if (userData != null) {
), sellerName = "${userData['first_name'] ?? ''} ${userData['last_name'] ?? ''}".trim();
}
}
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: _isContactingSellerLoading
? null
: () => _contactSeller(context, sellerName),
child: _isContactingSellerLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(color: Colors.white),
)
: const Text(
'Contact Seller',
style: TextStyle(fontSize: 18, color: Colors.white),
),
);
},
),
)
: null,
); );
} }
Widget _buildRatingStars(double rating, {double size = 24}) {
return Row(
Future<void> _contactSeller(BuildContext context, Map<String, dynamic> book, String bookId) async { mainAxisSize: MainAxisSize.min,
final currentUser = FirebaseAuth.instance.currentUser; children: List.generate(5, (index) {
if (currentUser == null) { if (index < rating.floor()) {
ScaffoldMessenger.of(context).showSnackBar( // Full star
const SnackBar(content: Text('Please log in to contact the seller')), return Icon(Icons.star, color: Colors.amber, size: size);
} else if (index == rating.floor() && rating % 1 > 0) {
// Half star
return Icon(Icons.star_half, color: Colors.amber, size: size);
} else {
// Empty star
return Icon(Icons.star_border, color: Colors.amber, size: size);
}
}),
); );
return;
} }
final sellerId = book['userId']; // 📌 This is the user who posted the book Future<void> _submitReview(BuildContext context) async {
final isBuyer = currentUser.uid != sellerId; final currentUser = FirebaseAuth.instance.currentUser;
final rolePrefix = isBuyer ? 'buyer' : 'seller'; if (currentUser == null) return;
final users = [currentUser.uid, sellerId]..sort();
final chatRoomId = "${rolePrefix}_${bookId}_${users.join('_')}";
try { setState(() {
final sellerDoc = await FirebaseFirestore.instance _isSubmitting = true;
.collection('users') });
.doc(sellerId)
.get();
final sellerName = sellerDoc.exists try {
? "${sellerDoc['first_name']} ${sellerDoc['last_name']}" // Get current user info
: "Unknown Seller"; final userDoc = await FirebaseFirestore.instance
.collection('users')
final chatRef = FirebaseFirestore.instance.collection('chats').doc(chatRoomId); .doc(currentUser.uid)
.get();
final chatData = {
'users': users, final userName = userDoc.exists
'bookId': bookId, ? "${userDoc.data()?['first_name'] ?? ''} ${userDoc.data()?['last_name'] ?? ''}".trim()
'bookTitle': book['title'], : "Anonymous User";
'lastMessage': 'Hi! Is this book still available?',
'lastMessageTime': FieldValue.serverTimestamp(), final userAvatar = userDoc.data()?['avatar_url'] ?? '';
'createdAt': FieldValue.serverTimestamp(),
'participants': { // Save the review to seller's reviews subcollection
currentUser.uid: true, await FirebaseFirestore.instance
sellerId: true, .collection('users')
}, .doc(widget.book['userId'])
'sellerId': sellerId, .collection('reviews')
'buyerId': isBuyer ? currentUser.uid : null, // null if seller is messaging .doc(currentUser.uid)
}; .set({
'userId': currentUser.uid,
final existingChat = await chatRef.get(); 'rating': _userRating,
if (existingChat.exists) { 'comment': _reviewController.text.trim(),
await chatRef.update({ 'userName': userName,
'lastMessage': chatData['lastMessage'], 'userAvatar': userAvatar,
'lastMessageTime': chatData['lastMessageTime'], 'bookId': widget.bookId,
}); 'bookTitle': widget.book['title'] ?? 'Unknown Book',
} else { 'createdAt': FieldValue.serverTimestamp(),
await chatRef.set(chatData); });
// Update seller's average rating
final sellerReviewsRef = FirebaseFirestore.instance
.collection('users')
.doc(widget.book['userId'])
.collection('reviews');
final reviewsSnapshot = await sellerReviewsRef.get();
final reviews = reviewsSnapshot.docs;
double totalRating = 0;
for (var doc in reviews) {
totalRating += doc.data()['rating'] ?? 0;
}
final newAverageRating = reviews.isEmpty ? 0 : totalRating / reviews.length;
// Update the seller's user document with new average rating
await FirebaseFirestore.instance
.collection('users')
.doc(widget.book['userId'])
.update({
'average_rating': newAverageRating,
'review_count': reviews.length,
});
if (mounted) {
setState(() {
_hasReviewed = true;
_isSubmitting = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Thank you for your review!')),
);
}
} catch (e) {
if (mounted) {
setState(() {
_isSubmitting = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error submitting review: $e')),
);
}
} }
}
await chatRef.collection('messages').add({ Future<void> _editReview(BuildContext context) async {
'senderId': currentUser.uid, setState(() {
'message': 'Hi! Is this book still available?', _hasReviewed = false;
'timestamp': FieldValue.serverTimestamp(),
'read': false,
}); });
}
Navigator.push( void _showOptionsMenu(BuildContext context) {
context, showModalBottomSheet(
MaterialPageRoute( context: context,
builder: (context) => StrictChatPage( builder: (context) => SafeArea(
chatId: chatRoomId, child: Column(
otherUserName: sellerName, mainAxisSize: MainAxisSize.min,
currentUserId: currentUser.uid, children: [
sellerId: sellerId, ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: const Text('Delete Book'),
onTap: () {
Navigator.pop(context); // Close the bottom sheet
_confirmAndDeleteBook(context);
},
),
],
), ),
), ),
); );
} catch (e) {
debugPrint('Error starting chat: $e');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to contact seller. Please try again.')),
);
} }
}
// Fixed method that prevents using context after async gap
void _confirmAndDeleteBook(BuildContext context) async {
void _confirmAndDeleteBook(BuildContext context, String bookId) async {
final shouldDelete = await showDialog<bool>( final shouldDelete = await showDialog<bool>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Confirm Deletion'), title: const Text('Confirm Deletion'),
content: const Text('Are you sure you want to delete this book?'), content: const Text('Are you sure you want to delete this book?'),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), TextButton(
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')), onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel')
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete')
),
], ],
), ),
); );
// Store context references before any async operations
final scaffoldMessenger = ScaffoldMessenger.of(context);
final navigator = Navigator.of(context);
if (shouldDelete == true) { if (shouldDelete == true) {
await FirebaseFirestore.instance.collection('books').doc(bookId).delete(); setState(() {
if (context.mounted) { _isDeleting = true; // Show loading state
Navigator.pop(context); });
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Book removed successfully')), try {
// Delete the book document from Firestore
await FirebaseFirestore.instance
.collection('books')
.doc(widget.bookId)
.delete();
// Check if widget is still mounted before updating state
if (mounted) {
setState(() {
_isDeleting = false;
});
}
// Show success message using stored scaffoldMessenger reference
scaffoldMessenger.showSnackBar(
const SnackBar(content: Text('Book deleted successfully')),
); );
// Navigate back using stored navigator reference
navigator.pop();
} catch (e) {
// Only update state if still mounted
if (mounted) {
setState(() {
_isDeleting = false;
});
// Show error using stored scaffoldMessenger
scaffoldMessenger.showSnackBar(
SnackBar(content: Text('Error deleting book: $e')),
);
}
} }
} }
} }
}
String _formatDate(Timestamp timestamp) {
String _formatPrice(dynamic price) { if (timestamp == null) return 'Date not available';
if (price == null) return '0.00';
if (price is num) return price.toStringAsFixed(2); final date = timestamp.toDate();
if (price is String) { final now = DateTime.now();
try { final difference = now.difference(date);
return double.parse(price).toStringAsFixed(2);
} catch (_) { if (difference.inDays == 0) {
return price; return 'Today';
} } else if (difference.inDays == 1) {
} return 'Yesterday';
return '0.00'; } else if (difference.inDays < 7) {
return '${difference.inDays} days ago';
} else {
return '${date.day}/${date.month}/${date.year}';
} }
} }
\ No newline at end of file
...@@ -2,24 +2,27 @@ import 'package:flutter/foundation.dart'; ...@@ -2,24 +2,27 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:paperchase_app/book_detail_page.dart';
import 'colors.dart'; import 'colors.dart';
class StrictChatPage extends StatefulWidget { class StrictChatPage extends StatefulWidget {
final String chatId; final String chatId;
//final String bookId;
final String otherUserName; final String otherUserName;
final String currentUserId; final List<String> predefinedMessages;
final String sellerId;
const StrictChatPage({ const StrictChatPage({
Key? key, super.key,
required this.chatId, required this.chatId,
required this.otherUserName, required this.otherUserName,
required this.currentUserId, this.predefinedMessages = const [
required this.sellerId, "Is this still available?",
}) : super(key: key); "When can we meet?",
"I'll take it",
"Thanks!",
"Hello",
"Can you hold it for me?",
"What's your lowest price?",
],
});
@override @override
_StrictChatPageState createState() => _StrictChatPageState(); _StrictChatPageState createState() => _StrictChatPageState();
...@@ -29,32 +32,6 @@ class _StrictChatPageState extends State<StrictChatPage> { ...@@ -29,32 +32,6 @@ class _StrictChatPageState extends State<StrictChatPage> {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
final TextEditingController _messageController = TextEditingController(); final TextEditingController _messageController = TextEditingController();
String? _bookTitle; String? _bookTitle;
String? _bookId;
List<String> get predefinedMessages {
final currentUser = FirebaseAuth.instance.currentUser;
final email = currentUser?.email ?? "your email";
if (widget.currentUserId == widget.sellerId) {
return [
"Yes, it's still available.",
"Thanks!",
"How about we meet this weekend?",
"That a deal!",
"Yes, I will hold it",
"Contact me at $email"
];
} else {
return [
"Is this still available?",
"When can we meet?",
"I'll take it",
"Thanks!",
"Can you hold it for me?",
"Contact me at $email",
];
}
}
@override @override
void initState() { void initState() {
...@@ -75,11 +52,10 @@ List<String> get predefinedMessages { ...@@ -75,11 +52,10 @@ List<String> get predefinedMessages {
.collection('chats') .collection('chats')
.doc(widget.chatId) .doc(widget.chatId)
.get(); .get();
if (doc.exists) { if (doc.exists) {
setState(() { setState(() {
_bookTitle = doc.data()?['bookTitle'] as String?; _bookTitle = doc.data()?['bookTitle'] as String?;
_bookId = doc.data()?['bookId'] as String?;
}); });
} }
} catch (e) { } catch (e) {
...@@ -135,8 +111,7 @@ List<String> get predefinedMessages { ...@@ -135,8 +111,7 @@ List<String> get predefinedMessages {
final backgroundColor2 = isDarkMode ? kLightBackground : kDarkBackground; final backgroundColor2 = isDarkMode ? kLightBackground : kDarkBackground;
final textColor = isDarkMode ? kDarkText : kLightText; final textColor = isDarkMode ? kDarkText : kLightText;
final textColor2 = isDarkMode ? kLightText : kDarkText; final textColor2 = isDarkMode ? kLightText : kDarkText;
final messageBackgroundOther = isDarkMode ? Colors.grey[800] : Colors final messageBackgroundOther = isDarkMode ? Colors.grey[800] : Colors.grey[200];
.grey[200];
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
...@@ -201,8 +176,7 @@ List<String> get predefinedMessages { ...@@ -201,8 +176,7 @@ List<String> get predefinedMessages {
final text = data['message'] as String? ?? ''; final text = data['message'] as String? ?? '';
final senderId = data['senderId'] as String? ?? ''; final senderId = data['senderId'] as String? ?? '';
final currentUser = FirebaseAuth.instance.currentUser; final currentUser = FirebaseAuth.instance.currentUser;
final isMe = currentUser != null && final isMe = currentUser != null && senderId == currentUser.uid;
senderId == currentUser.uid;
return Container( return Container(
margin: const EdgeInsets.symmetric(vertical: 4), margin: const EdgeInsets.symmetric(vertical: 4),
...@@ -213,10 +187,7 @@ List<String> get predefinedMessages { ...@@ -213,10 +187,7 @@ List<String> get predefinedMessages {
children: [ children: [
Container( Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery maxWidth: MediaQuery.of(context).size.width * 0.75,
.of(context)
.size
.width * 0.75,
), ),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
...@@ -245,78 +216,48 @@ List<String> get predefinedMessages { ...@@ -245,78 +216,48 @@ List<String> get predefinedMessages {
), ),
), ),
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(top: 2), margin: const EdgeInsets.only(top: 2),
width: double.infinity, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
color: backgroundColor2, color: backgroundColor2,
border: Border( border: Border(
top: BorderSide( top: BorderSide(
color: backgroundColor.withOpacity(0.2), color: backgroundColor.withOpacity(0.2),
), ),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Quick replies:',
style: TextStyle(
color: textColor2.withOpacity(0.7),
fontSize: 14,
),
),
// Only show the Confirmed button if the current user is the seller
if (widget.currentUserId == widget.sellerId)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _confirmAndCompletePurchase(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.lightGreenAccent,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
), ),
), ),
child: const Text( child: Column(
'Confirmed!', crossAxisAlignment: CrossAxisAlignment.start,
style: TextStyle(fontSize: 18, color: kLightText), children: [
Text(
'Quick replies:',
style: TextStyle(
color: textColor2.withOpacity(0.7),
fontSize: 14,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: widget.predefinedMessages.map((msg) {
return ActionChip(
label: Text(msg),
onPressed: () => _sendMessage(msg),
backgroundColor: backgroundColor,
labelStyle: TextStyle(
color: textColor,
),
);
}).toList(),
),
const SizedBox(height: 12),
],
), ),
), ),
),
if (widget.currentUserId == widget.sellerId)
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: predefinedMessages.map((msg) {
return ActionChip(
label: Text(msg),
onPressed: () => _sendMessage(msg),
backgroundColor: backgroundColor,
labelStyle: TextStyle(color: textColor,),
);
}).toList(),
),
const SizedBox(height: 12),
],
),
),
], ],
), ),
); );
} }
void _confirmAndCompletePurchase(BuildContext context) async {
await FirebaseFirestore.instance.collection('books').doc(_bookId).delete();
//await FirebaseFirestore.instance.collection('chats').doc(widget.chatId).collection('messages').doc().delete();
await FirebaseFirestore.instance.collection('chats').doc(widget.chatId).delete();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Transaction completed!')),
);
}
} }
\ No newline at end of file
...@@ -3,6 +3,8 @@ import 'package:firebase_auth/firebase_auth.dart'; ...@@ -3,6 +3,8 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'colors.dart'; import 'colors.dart';
class ForgotPasswordPage extends StatefulWidget { class ForgotPasswordPage extends StatefulWidget {
const ForgotPasswordPage({super.key});
@override @override
_ForgotPasswordPageState createState() => _ForgotPasswordPageState(); _ForgotPasswordPageState createState() => _ForgotPasswordPageState();
} }
...@@ -67,7 +69,6 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> { ...@@ -67,7 +69,6 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
SizedBox(height: 20), SizedBox(height: 20),
ElevatedButton( ElevatedButton(
onPressed: _resetPassword, onPressed: _resetPassword,
child: Text("Reset Password"),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, // Background color backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, // Background color
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, // Text color foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, // Text color
...@@ -76,6 +77,7 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> { ...@@ -76,6 +77,7 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
borderRadius: BorderRadius.circular(10), // Rounded corners borderRadius: BorderRadius.circular(10), // Rounded corners
), ),
), ),
child: Text("Reset Password"),
), ),
], ],
), ),
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:paperchase_app/NavBar.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:paperchase_app/chat_page.dart'; import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:paperchase_app/colors.dart'; import 'colors.dart';
import 'NavBar.dart';
import 'chat_page.dart';
enum BookFilter {
all,
sold,
bought,
}
class InboxPage extends StatefulWidget { class InboxPage extends StatefulWidget {
const InboxPage({super.key}); const InboxPage({super.key});
...@@ -14,10 +20,33 @@ class InboxPage extends StatefulWidget { ...@@ -14,10 +20,33 @@ class InboxPage extends StatefulWidget {
} }
class _InboxPageState extends State<InboxPage> { class _InboxPageState extends State<InboxPage> {
BookFilter _currentFilter = BookFilter.all;
String _getFilterName(BookFilter filter) {
switch (filter) {
case BookFilter.all:
return 'All Books';
case BookFilter.sold:
return 'Sold Books';
case BookFilter.bought:
return 'Bought Books';
}
}
Query<Map<String, dynamic>> _getFilteredQuery(String userId) { Query<Map<String, dynamic>> _getFilteredQuery(String userId) {
return FirebaseFirestore.instance final baseQuery = FirebaseFirestore.instance.collection('chats');
.collection('chats') switch (_currentFilter) {
.where('users', arrayContains: userId); case BookFilter.all:
return baseQuery.where('users', arrayContains: userId);
case BookFilter.sold:
return baseQuery
.where('users', arrayContains: userId)
.where('sellerId', isEqualTo: userId);
case BookFilter.bought:
return baseQuery
.where('users', arrayContains: userId)
.where('buyerId', isEqualTo: userId);
}
} }
@override @override
...@@ -32,7 +61,9 @@ class _InboxPageState extends State<InboxPage> { ...@@ -32,7 +61,9 @@ class _InboxPageState extends State<InboxPage> {
if (currentUser == null) { if (currentUser == null) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
iconTheme: IconThemeData(color: textColor), iconTheme: IconThemeData(
color: isDarkMode ? kDarkBackground : kLightBackground,
),
title: const Text( title: const Text(
"Inbox", "Inbox",
style: TextStyle( style: TextStyle(
...@@ -43,7 +74,7 @@ class _InboxPageState extends State<InboxPage> { ...@@ -43,7 +74,7 @@ class _InboxPageState extends State<InboxPage> {
color: kPrimaryColor, color: kPrimaryColor,
), ),
), ),
backgroundColor: scaffoldColor, foregroundColor: isDarkMode ? kLightBackground : kDarkBackground,
), ),
drawer: const NavBar(), drawer: const NavBar(),
body: Container( body: Container(
...@@ -71,21 +102,75 @@ class _InboxPageState extends State<InboxPage> { ...@@ -71,21 +102,75 @@ class _InboxPageState extends State<InboxPage> {
), ),
), ),
), ),
bottomNavigationBar: _buildBottomNavigationBar(isDarkMode, textColor2), bottomNavigationBar: BottomNavigationBar(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground,
selectedItemColor: kPrimaryColor,
unselectedItemColor: isDarkMode ? kDarkBackground : kLightBackground,
currentIndex: 1,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
BottomNavigationBarItem(icon: Icon(Icons.add), label: "Post"),
BottomNavigationBarItem(icon: Icon(Icons.mail), label: "Inbox"),
],
onTap: (index) {
if (index == 0) {
Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false);
} else if (index == 1) {
Navigator.pushNamed(context, '/post');
} else if (index == 2) {
Navigator.pushNamed(context, '/inbox');
}
},
),
); );
} }
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text( title: Row(
'Inbox', children: [
style: TextStyle( const Text(
fontFamily: 'Impact', 'Inbox',
fontSize: 24, style: TextStyle(
fontStyle: FontStyle.italic, fontFamily: 'Impact',
fontWeight: FontWeight.bold, fontSize: 24,
color: kPrimaryColor, fontStyle: FontStyle.italic,
), fontWeight: FontWeight.bold,
color: kPrimaryColor,
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
borderRadius: BorderRadius.circular(20),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<BookFilter>(
value: _currentFilter,
icon: Icon(Icons.arrow_drop_down, color: textColor),
style: TextStyle(color: textColor, fontSize: 14),
dropdownColor: isDarkMode ? Colors.grey[800] : Colors.grey[200],
items: BookFilter.values.map((filter) {
return DropdownMenuItem<BookFilter>(
value: filter,
child: Text(_getFilterName(filter)),
);
}).toList(),
onChanged: (BookFilter? newValue) {
if (newValue != null) {
setState(() {
_currentFilter = newValue;
});
}
},
),
),
),
),
],
), ),
backgroundColor: scaffoldColor, backgroundColor: scaffoldColor,
iconTheme: IconThemeData(color: textColor2), iconTheme: IconThemeData(color: textColor2),
...@@ -98,7 +183,26 @@ class _InboxPageState extends State<InboxPage> { ...@@ -98,7 +183,26 @@ class _InboxPageState extends State<InboxPage> {
.orderBy('lastMessageTime', descending: true) .orderBy('lastMessageTime', descending: true)
.snapshots(), .snapshots(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (kDebugMode) {
print('Current user ID in Inbox: ${currentUser.uid}');
print('Current filter: ${_getFilterName(_currentFilter)}');
print('Stream connection state: ${snapshot.connectionState}');
if (snapshot.hasError) {
print('Stream error: ${snapshot.error}');
print('Error stack trace: ${snapshot.stackTrace}');
}
}
if (snapshot.hasError) { if (snapshot.hasError) {
final error = snapshot.error.toString();
if (error.contains('failed-precondition') || error.contains('requires an index')) {
return StreamBuilder<QuerySnapshot>(
stream: _getFilteredQuery(currentUser.uid).snapshots(),
builder: (context, simpleSnapshot) {
return _buildChatList(simpleSnapshot, currentUser, isDarkMode, textColor);
},
);
}
return Center( return Center(
child: Text( child: Text(
'Error loading conversations: ${snapshot.error}', 'Error loading conversations: ${snapshot.error}',
...@@ -106,22 +210,39 @@ class _InboxPageState extends State<InboxPage> { ...@@ -106,22 +210,39 @@ class _InboxPageState extends State<InboxPage> {
), ),
); );
} }
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
return _buildChatList(snapshot, currentUser, isDarkMode, textColor); return _buildChatList(snapshot, currentUser, isDarkMode, textColor);
}, },
), ),
), ),
bottomNavigationBar: _buildBottomNavigationBar(isDarkMode, textColor2), bottomNavigationBar: BottomNavigationBar(
backgroundColor: scaffoldColor,
selectedItemColor: kPrimaryColor,
unselectedItemColor: textColor2,
currentIndex: 2,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.add), label: 'Post'),
BottomNavigationBarItem(icon: Icon(Icons.mail), label: 'Inbox'),
],
onTap: (index) {
if (index == 0) {
Navigator.pushReplacementNamed(context, '/');
} else if (index == 1) {
Navigator.pushReplacementNamed(context, '/post');
}
},
),
); );
} }
Widget _buildChatList(AsyncSnapshot<QuerySnapshot> snapshot, User currentUser, bool isDarkMode, Color textColor) { Widget _buildChatList(AsyncSnapshot<QuerySnapshot> snapshot, User currentUser, bool isDarkMode, Color textColor) {
final chats = snapshot.data?.docs ?? []; final chats = snapshot.data?.docs ?? [];
if (chats.isEmpty) { if (chats.isEmpty) {
return Center( return Center(
child: Column( child: Column(
...@@ -129,9 +250,22 @@ class _InboxPageState extends State<InboxPage> { ...@@ -129,9 +250,22 @@ class _InboxPageState extends State<InboxPage> {
children: [ children: [
Icon(Icons.chat_bubble_outline, size: 64, color: textColor.withOpacity(0.5)), Icon(Icons.chat_bubble_outline, size: 64, color: textColor.withOpacity(0.5)),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text('No conversations yet'), Text(
_currentFilter == BookFilter.all
? 'No conversations yet'
: _currentFilter == BookFilter.sold
? 'No sold books conversations'
: 'No bought books conversations',
style: TextStyle(fontSize: 18, color: textColor.withOpacity(0.7)),
),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text('Browse books and contact sellers to start chatting'), Text(
_currentFilter == BookFilter.all
? 'Browse books and contact sellers to start chatting'
: 'No messages found for this filter',
style: TextStyle(fontSize: 14, color: textColor.withOpacity(0.5)),
textAlign: TextAlign.center,
),
], ],
), ),
); );
...@@ -152,16 +286,29 @@ class _InboxPageState extends State<InboxPage> { ...@@ -152,16 +286,29 @@ class _InboxPageState extends State<InboxPage> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final chat = sortedChats[index]; final chat = sortedChats[index];
final data = chat.data() as Map<String, dynamic>; final data = chat.data() as Map<String, dynamic>;
final chatId = chat.id;
// We'll determine real seller in FutureBuilder if (kDebugMode) {
final bookId = data['bookId'] as String? ?? ''; print('Chat data: $data');
}
final lastMessage = data['lastMessage'] as String?; final lastMessage = data['lastMessage'] as String?;
final lastMessageTime = (data['lastMessageTime'] as Timestamp?)?.toDate(); final lastMessageTime = (data['lastMessageTime'] as Timestamp?)?.toDate();
final bookTitle = data['bookTitle'] as String?; final bookTitle = data['bookTitle'] as String?;
final usersList = (data['users'] as List?)?.cast<String>() ?? []; final usersList = (data['users'] as List?)?.cast<String>() ?? [];
String otherUserId = usersList.firstWhere((id) => id != currentUser.uid, orElse: () => 'unknown'); String otherUserId;
try {
otherUserId = usersList.firstWhere(
(id) => id != currentUser.uid,
orElse: () => 'unknown',
);
} catch (e) {
if (kDebugMode) {
print('Error finding other user: $e');
}
otherUserId = 'unknown';
}
return FutureBuilder<DocumentSnapshot>( return FutureBuilder<DocumentSnapshot>(
future: FirebaseFirestore.instance.collection('users').doc(otherUserId).get(), future: FirebaseFirestore.instance.collection('users').doc(otherUserId).get(),
builder: (context, userSnapshot) { builder: (context, userSnapshot) {
...@@ -171,77 +318,79 @@ class _InboxPageState extends State<InboxPage> { ...@@ -171,77 +318,79 @@ class _InboxPageState extends State<InboxPage> {
userName = '${userData['first_name'] ?? ''} ${userData['last_name'] ?? ''}'.trim(); userName = '${userData['first_name'] ?? ''} ${userData['last_name'] ?? ''}'.trim();
if (userName.isEmpty) userName = 'Unknown User'; if (userName.isEmpty) userName = 'Unknown User';
} }
return FutureBuilder<QuerySnapshot>( return Card(
future: FirebaseFirestore.instance margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
.collection('chats') color: isDarkMode ? Colors.grey[900] : Colors.white,
.doc(chatId) child: ListTile(
.collection('messages') onTap: () {
.orderBy('timestamp', descending: false) Navigator.push(
.limit(1) context,
.get(), MaterialPageRoute(
builder: (context, messagesSnapshot) { builder: (context) => StrictChatPage(
String sellerId = ''; chatId: chat.id,
String buyerId = ''; otherUserName: userName,
predefinedMessages: const [
// Check who sent first message to determine buyer "Is this still available?",
if (messagesSnapshot.hasData && messagesSnapshot.data!.docs.isNotEmpty) { "When can we meet?",
final firstMessage = messagesSnapshot.data!.docs.first; "I'll take it",
final firstMessageData = firstMessage.data() as Map<String, dynamic>; "Thanks!",
buyerId = firstMessageData['senderId'] as String? ?? ''; "Hello",
"Can you hold it for me?",
// If buyer is first message sender, seller is the other user "What's your lowest price?",
sellerId = usersList.firstWhere((id) => id != buyerId, orElse: () => ''); ],
} else { ),
// If no messages yet, use any seller ID from data if available ),
sellerId = data['sellerId'] as String? ?? '';
// If seller ID still not available, default to book owner from books collection
if (sellerId.isEmpty && bookId.isNotEmpty) {
// This will be handled later in the next FutureBuilder
}
}
// Return placeholder while loading book data if necessary
if (sellerId.isEmpty && bookId.isNotEmpty) {
return FutureBuilder<DocumentSnapshot>(
future: FirebaseFirestore.instance.collection('books').doc(bookId).get(),
builder: (context, bookSnapshot) {
if (bookSnapshot.hasData && bookSnapshot.data!.exists) {
final bookData = bookSnapshot.data!.data() as Map<String, dynamic>? ?? {};
sellerId = bookData['userId'] as String? ?? '';
}
// Now build the actual chat list item
return _buildChatListItem(
context,
chatId,
userName,
currentUser.uid,
sellerId,
bookTitle,
lastMessage,
lastMessageTime,
isDarkMode,
textColor,
);
},
); );
} },
leading: CircleAvatar(
return _buildChatListItem( backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200],
context, child: Text(
chatId, userName[0].toUpperCase(),
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
),
),
title: Text(
userName, userName,
currentUser.uid, style: TextStyle(
sellerId, color: textColor,
bookTitle, fontWeight: FontWeight.bold,
lastMessage, ),
lastMessageTime, ),
isDarkMode, subtitle: Column(
textColor, crossAxisAlignment: CrossAxisAlignment.start,
); children: [
}, if (bookTitle != null)
Text(
'Re: $bookTitle',
style: TextStyle(
color: textColor.withOpacity(0.7),
fontSize: 12,
),
),
Text(
lastMessage ?? 'No messages yet',
style: TextStyle(
color: textColor.withOpacity(0.7),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
trailing: lastMessageTime != null
? Text(
_formatTimestamp(lastMessageTime),
style: TextStyle(
color: textColor.withOpacity(0.5),
fontSize: 12,
),
)
: null,
),
); );
}, },
); );
...@@ -249,107 +398,6 @@ class _InboxPageState extends State<InboxPage> { ...@@ -249,107 +398,6 @@ class _InboxPageState extends State<InboxPage> {
); );
} }
Widget _buildChatListItem(
BuildContext context,
String chatId,
String userName,
String currentUserId,
String sellerId,
String? bookTitle,
String? lastMessage,
DateTime? lastMessageTime,
bool isDarkMode,
Color textColor,
) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: isDarkMode ? Colors.grey[900] : Colors.white,
child: ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StrictChatPage(
chatId: chatId,
otherUserName: userName,
currentUserId: currentUserId,
sellerId: sellerId,
),
),
);
},
leading: CircleAvatar(
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200],
child: Text(
userName.isNotEmpty ? userName[0].toUpperCase() : '?',
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
),
),
title: Text(
userName,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (bookTitle != null)
Text(
'Re: $bookTitle',
style: TextStyle(
color: textColor.withOpacity(0.7),
fontSize: 12,
),
),
Text(
lastMessage ?? 'No messages yet',
style: TextStyle(
color: textColor.withOpacity(0.7),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
trailing: lastMessageTime != null
? Text(
_formatTimestamp(lastMessageTime),
style: TextStyle(
color: textColor.withOpacity(0.5),
fontSize: 12,
),
)
: null,
),
);
}
BottomNavigationBar _buildBottomNavigationBar(bool isDarkMode, Color textColor2) {
return BottomNavigationBar(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground,
selectedItemColor: kPrimaryColor,
unselectedItemColor: textColor2,
currentIndex: 2,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.add), label: 'Post'),
BottomNavigationBarItem(icon: Icon(Icons.mail), label: 'Inbox'),
],
onTap: (index) {
if (index == 0) {
Navigator.pushReplacementNamed(context, '/');
} else if (index == 1) {
Navigator.pushReplacementNamed(context, '/post');
}
},
);
}
String _formatTimestamp(DateTime timestamp) { String _formatTimestamp(DateTime timestamp) {
final now = DateTime.now(); final now = DateTime.now();
final difference = now.difference(timestamp); final difference = now.difference(timestamp);
......
...@@ -97,7 +97,7 @@ class _LoginPageState extends State<LoginPage> { ...@@ -97,7 +97,7 @@ class _LoginPageState extends State<LoginPage> {
_isLoading _isLoading
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: ElevatedButton( : ElevatedButton(
onPressed: _login, child: const Text('Login'), onPressed: _login,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, // Background color backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, // Background color
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, // Text color foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, // Text color
...@@ -105,7 +105,7 @@ class _LoginPageState extends State<LoginPage> { ...@@ -105,7 +105,7 @@ class _LoginPageState extends State<LoginPage> {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // Rounded corners borderRadius: BorderRadius.circular(10), // Rounded corners
), ),
), ), child: const Text('Login'),
), ),
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
......
...@@ -32,7 +32,6 @@ void main() async { ...@@ -32,7 +32,6 @@ void main() async {
if (isFirstLaunch) { if (isFirstLaunch) {
await prefs.setBool('first_launch', false); await prefs.setBool('first_launch', false);
} }
// Add a delay to ensure the GIF plays after the native splash screen
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
runApp(MyApp(isFirstLaunch: isFirstLaunch)); runApp(MyApp(isFirstLaunch: isFirstLaunch));
} }
...@@ -46,11 +45,11 @@ class MyApp extends StatefulWidget { ...@@ -46,11 +45,11 @@ class MyApp extends StatefulWidget {
} }
class _MyAppState extends State<MyApp> { class _MyAppState extends State<MyApp> {
bool _isDarkMode = false; // Default to Light Mode bool _isDarkMode = false;
void _toggleTheme() { void _toggleTheme() {
setState(() { setState(() {
_isDarkMode = !_isDarkMode; // Toggle between Light & Dark Mode _isDarkMode = !_isDarkMode;
}); });
} }
...@@ -58,7 +57,6 @@ class _MyAppState extends State<MyApp> { ...@@ -58,7 +57,6 @@ class _MyAppState extends State<MyApp> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'PaperChase', title: 'PaperChase',
theme: ThemeData( theme: ThemeData(
primaryColor: kPrimaryColor, primaryColor: kPrimaryColor,
brightness: Brightness.light, brightness: Brightness.light,
...@@ -68,7 +66,7 @@ class _MyAppState extends State<MyApp> { ...@@ -68,7 +66,7 @@ class _MyAppState extends State<MyApp> {
), ),
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: kDarkBackground, backgroundColor: kDarkBackground,
titleTextStyle: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), titleTextStyle: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold),
), ),
bottomNavigationBarTheme: const BottomNavigationBarThemeData( bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: kDarkBackground, backgroundColor: kDarkBackground,
...@@ -79,7 +77,10 @@ class _MyAppState extends State<MyApp> { ...@@ -79,7 +77,10 @@ class _MyAppState extends State<MyApp> {
darkTheme: ThemeData( darkTheme: ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
scaffoldBackgroundColor: kDarkBackground, scaffoldBackgroundColor: kDarkBackground,
appBarTheme: const AppBarTheme(backgroundColor: kLightBackground), appBarTheme: const AppBarTheme(
backgroundColor: kLightBackground,
titleTextStyle: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData( bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: kLightBackground, backgroundColor: kLightBackground,
selectedItemColor: kPrimaryColor, selectedItemColor: kPrimaryColor,
...@@ -87,19 +88,17 @@ class _MyAppState extends State<MyApp> { ...@@ -87,19 +88,17 @@ class _MyAppState extends State<MyApp> {
), ),
), ),
themeMode: _isDarkMode ? ThemeMode.dark : ThemeMode.light, themeMode: _isDarkMode ? ThemeMode.dark : ThemeMode.light,
home: HomePage(toggleTheme: _toggleTheme, isDarkMode: _isDarkMode),
home: HomePage(toggleTheme: _toggleTheme, isDarkMode: _isDarkMode),
routes: { routes: {
'/home': (context) => HomePage(toggleTheme: _toggleTheme, isDarkMode: _isDarkMode), // Passing the flag and toggle method '/home': (context) => HomePage(toggleTheme: _toggleTheme, isDarkMode: _isDarkMode),
'/login': (context) => const LoginPage(), '/login': (context) => const LoginPage(),
'/signup': (context) => const SignupPage(), '/signup': (context) => const SignupPage(),
'/profile': (context) => const ProfilePage(), '/profile': (context) => const ProfilePage(),
'/post': (context) => PostBookPage(), '/post': (context) => PostBookPage(),
'/inbox': (context) => InboxPage(), '/inbox': (context) => InboxPage(),
'/settings': (context) => SettingsPage( '/settings': (context) => SettingsPage(
isDarkMode: _isDarkMode, isDarkMode: _isDarkMode,
toggleTheme: _toggleTheme, toggleTheme: _toggleTheme,
), ),
}, },
); );
...@@ -109,7 +108,7 @@ class _MyAppState extends State<MyApp> { ...@@ -109,7 +108,7 @@ class _MyAppState extends State<MyApp> {
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
final VoidCallback toggleTheme; final VoidCallback toggleTheme;
final bool isDarkMode; final bool isDarkMode;
const HomePage({super.key, required this.toggleTheme, required this.isDarkMode}); const HomePage({super.key, required this.toggleTheme, required this.isDarkMode});
@override @override
...@@ -118,9 +117,20 @@ class HomePage extends StatefulWidget { ...@@ -118,9 +117,20 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
List<dynamic> _books = []; List<DocumentSnapshot> _books = [];
List<DocumentSnapshot> _filteredBooks = [];
bool _isLoggedIn = false; bool _isLoggedIn = false;
User? _user; User? _user;
final String _filterBy = 'Latest Posted';
int _selectedIndex = 0;
// Filter state
List<String> _allConditions = ['Like New', 'Good', 'Fair', 'Poor'];
List<String> _selectedConditions = [];
bool _filtersActive = false;
get kDarkCard => const Color(0xFF2C2C2C);
get kLightCard => Colors.white;
@override @override
void initState() { void initState() {
...@@ -138,7 +148,18 @@ class _HomePageState extends State<HomePage> { ...@@ -138,7 +148,18 @@ class _HomePageState extends State<HomePage> {
}); });
} }
// Navigation with authentication check void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
if (index == 1) {
_navigateIfAuthenticated(context, '/post');
} else if (index == 2) {
_navigateIfAuthenticated(context, '/inbox');
}
}
void _navigateIfAuthenticated(BuildContext context, String route) { void _navigateIfAuthenticated(BuildContext context, String route) {
if (_user != null) { if (_user != null) {
Navigator.pushNamed(context, route); Navigator.pushNamed(context, route);
...@@ -150,115 +171,251 @@ class _HomePageState extends State<HomePage> { ...@@ -150,115 +171,251 @@ class _HomePageState extends State<HomePage> {
} }
} }
Future<void> _logout() async {
await FirebaseAuth.instance.signOut();
setState(() {
_isLoggedIn = false;
});
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
}
Future<void> _searchBooks() async { Future<void> _searchBooks() async {
final query = _searchController.text.trim().toLowerCase(); final query = _searchController.text.trim().toLowerCase();
if (query.isEmpty) { if (query.isEmpty) {
_loadRecentBooks(); // If search is empty, reload recent books setState(() {
return; _filteredBooks = _books;
} });
return;
}
try { try {
final QuerySnapshot snapshot = await FirebaseFirestore.instance final filteredBooks = _books.where((doc) {
.collection('books') final data = doc.data() as Map<String, dynamic>;
.orderBy('timestamp', descending: true) final title = (data['title'] ?? '').toString().toLowerCase();
.get(); final author = (data['author'] ?? '').toString().toLowerCase();
final isbn = (data['isbn'] ?? '').toString();
return title.contains(query) || author.contains(query) || isbn.contains(query);
}).toList();
final filteredBooks = snapshot.docs.where((doc) { setState(() {
final data = doc.data() as Map<String, dynamic>; _filteredBooks = filteredBooks;
final title = (data['title'] ?? '').toString().toLowerCase(); });
final author = (data['author'] ?? '').toString().toLowerCase(); } catch (e) {
final isbn = (data['isbn'] ?? '').toString(); print("Error searching books: $e");
}
}
return title.contains(query) || author.contains(query) || isbn.contains(query); Future<void> _loadRecentBooks() async {
}).toList(); try {
final QuerySnapshot snapshot = await FirebaseFirestore.instance
.collection('books')
.orderBy('timestamp', descending: true)
.limit(20) // Increased limit to show more books
.get();
setState(() {
_books = snapshot.docs;
_filteredBooks = snapshot.docs;
});
} catch (e) {
print("Error fetching recent books: $e");
}
}
// Apply filters to the current book collection
void _applyFilters() {
setState(() { setState(() {
_books = filteredBooks; _filteredBooks = _books.where((doc) {
final data = doc.data() as Map<String, dynamic>;
// Condition filter only
final condition = (data['condition'] ?? '').toString();
final isConditionSelected = _selectedConditions.isEmpty ||
_selectedConditions.contains(condition);
return isConditionSelected;
}).toList();
_filtersActive = true;
}); });
} catch (e) {
print("Error searching books: $e");
} }
}
// Reset filters and show all books
void _resetFilters() {
Future<void> _loadRecentBooks() async {
try {
final QuerySnapshot snapshot = await FirebaseFirestore.instance
.collection('books')
.orderBy('timestamp', descending: true) // Sort by the most recent posts
.limit(10) // Optionally limit to the latest 10 books
.get();
setState(() { setState(() {
_books = snapshot.docs; _selectedConditions = [];
_filteredBooks = _books;
_filtersActive = false;
}); });
} catch (e) {
print("Error fetching recent books: $e");
} }
}
// Improved filter button
void _filterBooks() { Widget _buildFilterButton() {
setState(() { final bool isActive = _filtersActive;
if (_filterBy == 'Latest Posted') {
_books.sort((a, b) => (b['timestamp'] as Timestamp).compareTo(a['timestamp'] as Timestamp)); return InkWell(
} else if (_filterBy == 'Price: Low to High') { onTap: _showFilterDialog,
_books.sort((a, b) => (a['price'] ?? 0).compareTo(b['price'] ?? 0)); child: AnimatedContainer(
} else if (_filterBy == 'Price: High to Low') { duration: const Duration(milliseconds: 200),
_books.sort((a, b) => (b['price'] ?? 0).compareTo(a['price'] ?? 0)); padding: const EdgeInsets.all(12),
} else if (_filterBy == 'Condition: Best to Worst') { decoration: BoxDecoration(
_books.sort((a, b) => _conditionRanking(a['condition']).compareTo(_conditionRanking(b['condition']))); color: isActive ? kPrimaryColor : Colors.transparent,
} else if (_filterBy == 'Condition: Worst to Best') { borderRadius: BorderRadius.circular(10),
_books.sort((a, b) => _conditionRanking(b['condition']).compareTo(_conditionRanking(a['condition']))); border: Border.all(
} color: isActive ? kPrimaryColor : widget.isDarkMode ? Colors.grey[600]! : Colors.grey[400]!,
}); width: 1.5,
} ),
),
int _conditionRanking(String? condition) { child: Stack(
const conditionOrder = { children: [
'Like New': 1, Icon(
'Good': 2, Icons.filter_list,
'Fair': 3, color: isActive ? Colors.white : (widget.isDarkMode ? Colors.grey[400] : Colors.grey[700]),
'Poor': 4 size: 22,
}; ),
return conditionOrder[condition] ?? 0; if (isActive)
} Positioned(
right: 0,
top: 0,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
),
],
),
),
);
}
// Show the filter dialog with only condition filters
void _showFilterDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) {
final bool isDarkMode = widget.isDarkMode;
final Color backgroundColor = isDarkMode ? kDarkBackground : kLightBackground;
final Color textColor = isDarkMode ? kLightText : kDarkText;
return AlertDialog(
backgroundColor: backgroundColor,
title: Text(
'Filter Books',
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Condition filter
Text(
'Condition:',
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: _allConditions.map((condition) {
final isSelected = _selectedConditions.contains(condition);
return FilterChip(
label: Text(condition),
selected: isSelected,
selectedColor: kPrimaryColor.withOpacity(0.2),
checkmarkColor: kPrimaryColor,
backgroundColor: backgroundColor,
shape: StadiumBorder(
side: BorderSide(
color: isSelected ? kPrimaryColor : Colors.grey,
),
),
onSelected: (bool selected) {
setState(() {
if (selected) {
_selectedConditions.add(condition);
} else {
_selectedConditions.remove(condition);
}
});
},
);
}).toList(),
),
],
),
),
actions: [
TextButton(
child: Text(
'Reset',
style: TextStyle(color: kPrimaryColor),
),
onPressed: () {
setState(() {
_selectedConditions = [];
});
},
),
TextButton(
child: Text(
'Cancel',
style: TextStyle(color: Colors.grey),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FilledButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(kPrimaryColor),
),
child: const Text(
'Apply',
style: TextStyle(color: Colors.white),
),
onPressed: () {
// Update the main state with current filter values
this._selectedConditions = List.from(_selectedConditions);
// Close the dialog
Navigator.of(context).pop();
// Apply the filters
_applyFilters();
},
),
],
);
},
);
},
);
}
String _filterBy = 'Latest Posted'; // Default filter option
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool darkMode = isDarkMode(context); // Call the utility function
final query = _searchController.text.trim().toLowerCase();
return Scaffold( return Scaffold(
drawer: _isLoggedIn ? NavBar() : null, drawer: const NavBar(),
appBar: AppBar( appBar: AppBar(
iconTheme: IconThemeData( title: Image.asset('assets/title-text.png', height: 70), // Increased height from 60 to 70
color: widget.isDarkMode ? kDarkBackground : kLightBackground, centerTitle: true,
),
title: Image.asset('assets/title-text.png'),
actions: [ actions: [
IconButton( IconButton(
icon: Icon(widget.isDarkMode ? Icons.wb_sunny : Icons.nightlight_round), icon: Icon(widget.isDarkMode ? Icons.wb_sunny : Icons.nightlight_round),
color: widget.isDarkMode ? kDarkBackground : kLightBackground,
onPressed: widget.toggleTheme, onPressed: widget.toggleTheme,
), ),
if (!_isLoggedIn) ...[ if (!_isLoggedIn) ...[
TextButton( TextButton(
onPressed: () => Navigator.pushNamed(context, '/login'), onPressed: () => Navigator.pushNamed(context, '/login'),
child: Text('Login', style: TextStyle(color: widget.isDarkMode ? kDarkBackground : kLightBackground)), child: const Text('Login', style: TextStyle(color: Colors.white)),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pushNamed(context, '/signup'), onPressed: () => Navigator.pushNamed(context, '/signup'),
child: Text('Sign Up', style: TextStyle(color: widget.isDarkMode ? kDarkBackground : kLightBackground)), child: const Text('Sign Up', style: TextStyle(color: Colors.white)),
), ),
], ],
], ],
...@@ -266,156 +423,275 @@ String _filterBy = 'Latest Posted'; // Default filter option ...@@ -266,156 +423,275 @@ String _filterBy = 'Latest Posted'; // Default filter option
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
TextField( // Search and Filter Row
controller: _searchController, Row(
decoration: InputDecoration( children: [
hintText: "Search for books by title, author, or ISBN", // Search Field
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), Expanded(
suffixIcon: IconButton( child: TextField(
icon: const Icon(Icons.search), controller: _searchController,
onPressed: _searchBooks, decoration: InputDecoration(
), hintText: "Search for books",
), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
), suffixIcon: IconButton(
const SizedBox(height: 15), icon: const Icon(Icons.search),
onPressed: _searchBooks,
if (_books.isNotEmpty && (query ?? '').isNotEmpty)
Align(
alignment: Alignment.centerLeft, // ✅ Align to the left
child: GestureDetector(
onTap: () => showModalBottomSheet(
context: context,
builder: (context) => Container(
decoration: BoxDecoration(
color: widget.isDarkMode ? kDarkBackground : kLightBackground,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16), // ✅ Rounded corners at the top
topRight: Radius.circular(16),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
spreadRadius: 2,
), ),
], ),
), ),
child: Column( ),
mainAxisSize: MainAxisSize.min,
children: [ // Filter Button (updated)
for (var filterOption in [ const SizedBox(width: 8),
'Latest Posted', _buildFilterButton(),
'Price: Low to High', ],
'Price: High to Low', ),
'Condition: Best to Worst',
'Condition: Worst to Best' // Active Filters Chips
]) if (_filtersActive) ...[
ListTile( const SizedBox(height: 12),
title: Text( SingleChildScrollView(
filterOption, scrollDirection: Axis.horizontal,
style: TextStyle(color: widget.isDarkMode ? kDarkText : kLightText), child: Row(
), children: [
onTap: () { // Condition Chips
..._selectedConditions.map((condition) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: Chip(
label: Text(condition),
deleteIcon: const Icon(Icons.close, size: 18),
onDeleted: () {
setState(() { setState(() {
_filterBy = filterOption; _selectedConditions.remove(condition);
_filterBooks(); _applyFilters();
}); });
Navigator.pop(context);
}, },
), ),
], );
), }).toList(),
// Clear All Filters
if (_filtersActive)
TextButton.icon(
icon: const Icon(Icons.clear_all, size: 18),
label: const Text('Clear All'),
onPressed: _resetFilters,
),
],
), ),
), ),
],
// 🔥 Compact Container with Border around Icon on the Left
child: Container( const SizedBox(height: 15),
padding: const EdgeInsets.all(8), // Compact padding
decoration: BoxDecoration( // Results count
color: widget.isDarkMode ? kDarkBackground : kLightBackground, Align(
borderRadius: BorderRadius.circular(12), // ✅ Rounded border alignment: Alignment.centerLeft,
border: Border.all( child: Text(
color: widget.isDarkMode ? Colors.grey : Colors.black12, // Light border '${_filteredBooks.length} books found',
width: 1, style: TextStyle(
), color: widget.isDarkMode ? kLightText : kDarkText,
fontWeight: FontWeight.bold,
), ),
child:
Icon(Icons.sort_rounded, color: widget.isDarkMode ? kDarkText : kLightText), // ✅ Only the icon inside
), ),
), ),
),
const SizedBox(height: 8),
const SizedBox(height: 10),
// Books Grid
Expanded( Expanded(
child: _filteredBooks.isEmpty
child: ListView.builder( ? Center(
itemCount: _books.length, child: Column(
itemBuilder: (context, index) { mainAxisAlignment: MainAxisAlignment.center,
final book = _books[index]; children: [
final bookId = book.id; Icon(Icons.search_off,
final data = book.data() as Map<String, dynamic>; size: 64,
color: widget.isDarkMode ? Colors.grey[400] : Colors.grey[600]),
final title = book.data()['title'] ?? "Unknown Title"; const SizedBox(height: 16),
final author = book.data()['author'] ?? "No author available"; Text(
final thumbnail = book.data()['imageUrl'] ?? "https://via.placeholder.com/50"; 'No books match your filters',
final price = book.data()['price']; style: TextStyle(
fontSize: 18,
color: widget.isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
return ListTile(
leading: Image.network(thumbnail, width: 50, height: 50, fit: BoxFit.cover),
title: Text(title),
subtitle: Text('$author - \$$price - ${book.data()['condition'] ?? 'Condition not available'}'),
onTap: () {
if (_isLoggedIn) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BookDetailsPage(book: book.data() as Map<String, dynamic>, bookId: bookId), // Pass book data
), ),
); const SizedBox(height: 8),
} else { TextButton(
ScaffoldMessenger.of(context).showSnackBar( onPressed: _resetFilters,
SnackBar( child: const Text('Reset Filters'),
content: Text("You need to log in to view book details."), ),
duration: Duration(seconds: 2), ],
),
)
: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 15,
crossAxisSpacing: 15,
childAspectRatio: 0.6,
),
itemCount: _filteredBooks.length,
itemBuilder: (context, index) {
final doc = _filteredBooks[index];
final data = doc.data() as Map<String, dynamic>;
final bookId = doc.id;
final title = data['title'] ?? 'Unknown Title';
final condition = data['condition'] ?? 'Unknown';
// Extract price and format it with currency
final price = data['price'] != null
? '\$${data['price'].toString()}'
: 'Price not listed';
// Handle image URLs more robustly
String? imageUrl;
// First try the imageUrls field (array of images)
if (data.containsKey('imageUrls') && data['imageUrls'] != null) {
final images = data['imageUrls'];
if (images is List && images.isNotEmpty) {
imageUrl = images[0].toString();
}
}
// If that doesn't work, try imageUrl field (single image)
if ((imageUrl == null || imageUrl.isEmpty) && data.containsKey('imageUrl')) {
imageUrl = data['imageUrl']?.toString();
}
// If that doesn't work, try coverImageUrl field
if ((imageUrl == null || imageUrl.isEmpty) && data.containsKey('coverImageUrl')) {
imageUrl = data['coverImageUrl']?.toString();
}
return GestureDetector(
onTap: () {
if (_isLoggedIn) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BookDetailsPage(
book: data,
bookId: bookId,
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("You need to log in to view book details."),
duration: Duration(seconds: 2),
),
);
}
},
child: Container(
decoration: BoxDecoration(
color: widget.isDarkMode ? kDarkCard : kLightCard,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
spreadRadius: 2,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image section - takes up most of the space
Expanded(
flex: 5,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: imageUrl != null && imageUrl.isNotEmpty
? FadeInImage.assetNetwork(
placeholder: 'assets/placeholder.png',
image: imageUrl,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
imageErrorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[300],
child: const Center(child: Icon(Icons.broken_image, size: 40)),
);
},
)
: Container(
color: Colors.grey[300],
child: const Center(child: Icon(Icons.book, size: 40)),
),
),
),
// Info section below the image
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// Price in bold
Text(
price,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: kPrimaryColor,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
// Title in bold
Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
color: widget.isDarkMode ? kDarkText : kLightText,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
// Condition not in bold
Text(
condition,
style: TextStyle(
fontSize: 12,
color: widget.isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
],
),
), ),
); );
} },
}, ),
);
},
),
), ),
], ],
), ),
), ),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
backgroundColor: widget.isDarkMode ? kLightBackground : kDarkBackground,
selectedItemColor: kPrimaryColor,
unselectedItemColor: widget.isDarkMode ? kDarkBackground : kLightBackground,
items: const [ items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"), BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.add), label: "Post"), BottomNavigationBarItem(icon: Icon(Icons.post_add), label: 'Post'),
BottomNavigationBarItem(icon: Icon(Icons.mail), label: "Inbox"), BottomNavigationBarItem(icon: Icon(Icons.inbox), label: 'Inbox'),
], ],
onTap: (index) { currentIndex: _selectedIndex,
if (index == 0) { selectedItemColor: kPrimaryColor,
Navigator.pushNamed(context, '/'); onTap: _onItemTapped,
} else if (index == 1) {
_navigateIfAuthenticated(context, '/post');
} else if (index == 2) {
_navigateIfAuthenticated(context, '/inbox');
}
},
), ),
); );
} }
} }
\ No newline at end of file
...@@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; ...@@ -5,11 +5,12 @@ import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart'; import 'package:permission_handler/permission_handler.dart'; // Add this import
import 'colors.dart'; import 'colors.dart';
import 'NavBar.dart';
class PostBookPage extends StatefulWidget { class PostBookPage extends StatefulWidget {
const PostBookPage({super.key});
@override @override
_PostBookPageState createState() => _PostBookPageState(); _PostBookPageState createState() => _PostBookPageState();
} }
...@@ -22,62 +23,94 @@ class _PostBookPageState extends State<PostBookPage> { ...@@ -22,62 +23,94 @@ class _PostBookPageState extends State<PostBookPage> {
final TextEditingController descriptionController = TextEditingController(); final TextEditingController descriptionController = TextEditingController();
File? _imageFile; File? _imageFile;
String _selectedCondition = "Like New"; String _selectedCondition = "Like New";
final ImagePicker _picker = ImagePicker(); // Create a single instance
// Function to request camera permission
Future<bool> _requestCameraPermission() async {
PermissionStatus status = await Permission.camera.status;
if (status.isDenied) {
status = await Permission.camera.request();
}
return status.isGranted;
}
// Function to pick an image from camera or gallery // Function to pick an image from camera or gallery
Future<void> _pickImage(ImageSource source) async { Future<void> _pickImage(ImageSource source) async {
final pickedFile = await ImagePicker().pickImage(source: source); try {
if (pickedFile != null) { // Request permission if using camera
setState(() { if (source == ImageSource.camera) {
_imageFile = File(pickedFile.path); bool hasPermission = await _requestCameraPermission();
}); if (!hasPermission) {
print('Image picked: ${_imageFile!.path}'); ScaffoldMessenger.of(context).showSnackBar(
} else { const SnackBar(content: Text('Camera permission is required to take photos'))
print('No image selected.'); );
return;
}
}
final XFile? pickedFile = await _picker.pickImage(
source: source,
imageQuality: 80, // Optimize image quality
);
if (pickedFile != null) {
setState(() {
_imageFile = File(pickedFile.path);
});
print('Image picked: ${_imageFile!.path}');
} else {
print('No image selected.');
}
} catch (e) {
print('Error picking image: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error accessing ${source == ImageSource.camera ? 'camera' : 'gallery'}: $e'))
);
}
} }
}
Future<String?> fetchBookDescription(String isbn) async { Future<String?> fetchBookDescription(String isbn) async {
final String url = "https://www.googleapis.com/books/v1/volumes?q=isbn:$isbn"; final String url = "https://www.googleapis.com/books/v1/volumes?q=isbn:$isbn";
try { try {
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
if (data['totalItems'] > 0) { if (data['totalItems'] > 0) {
return data['items'][0]['volumeInfo']['description'] ?? 'No description available'; return data['items'][0]['volumeInfo']['description'] ?? 'No description available';
}
} }
} catch (e) {
print("Error fetching book details: $e");
} }
} catch (e) { return null; // Return null if no description is found
print("Error fetching book details: $e");
} }
return null; // Return null if no description is found
}
Future<String?> uploadImageToImgur(File imageFile) async { Future<String?> uploadImageToImgur(File imageFile) async {
try { try {
var request = http.MultipartRequest( var request = http.MultipartRequest(
'POST', Uri.parse('https://api.imgur.com/3/upload') 'POST', Uri.parse('https://api.imgur.com/3/upload')
); );
request.headers['Authorization'] = 'Client-ID 00caf989adf38fa'; request.headers['Authorization'] = 'Client-ID 00caf989adf38fa';
var pic = await http.MultipartFile.fromPath('image', imageFile.path); var pic = await http.MultipartFile.fromPath('image', imageFile.path);
request.files.add(pic); request.files.add(pic);
var response = await request.send(); var response = await request.send();
if (response.statusCode == 200) { if (response.statusCode == 200) {
final responseData = await response.stream.bytesToString(); final responseData = await response.stream.bytesToString();
final jsonData = json.decode(responseData); final jsonData = json.decode(responseData);
return jsonData['data']['link']; // Image URL from Imgur return jsonData['data']['link']; // Image URL from Imgur
} else { } else {
print('Failed to upload image: ${response.reasonPhrase}'); print('Failed to upload image: ${response.reasonPhrase}');
return null;
}
} catch (e) {
print('Error uploading image: $e');
return null; return null;
} }
} catch (e) {
print('Error uploading image: $e');
return null;
} }
}
// Function to upload book data to Firebase // Function to upload book data to Firebase
Future<bool> uploadBook() async { Future<bool> uploadBook() async {
...@@ -87,8 +120,28 @@ Future<String?> uploadImageToImgur(File imageFile) async { ...@@ -87,8 +120,28 @@ Future<String?> uploadImageToImgur(File imageFile) async {
String? imageUrl; String? imageUrl;
if (_imageFile != null) { if (_imageFile != null) {
// Show loading indicator
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
},
);
imageUrl = await uploadImageToImgur(_imageFile!); imageUrl = await uploadImageToImgur(_imageFile!);
if (imageUrl == null) return false; // Upload and get URL
// Hide loading indicator
Navigator.of(context).pop();
if (imageUrl == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to upload image. Please try again.'))
);
return false;
}
} }
await FirebaseFirestore.instance.collection('books').add({ await FirebaseFirestore.instance.collection('books').add({
...@@ -98,8 +151,8 @@ Future<String?> uploadImageToImgur(File imageFile) async { ...@@ -98,8 +151,8 @@ Future<String?> uploadImageToImgur(File imageFile) async {
'price': priceController.text, 'price': priceController.text,
'description': descriptionController.text, 'description': descriptionController.text,
'condition': _selectedCondition, 'condition': _selectedCondition,
'userId': user.uid, // 🔹 Save logged-in user's ID 'userId': user.uid,
'imageUrl': imageUrl ?? "", // Optional image 'imageUrl': imageUrl ?? "",
'timestamp': FieldValue.serverTimestamp(), 'timestamp': FieldValue.serverTimestamp(),
}); });
return true; return true;
...@@ -111,53 +164,67 @@ Future<String?> uploadImageToImgur(File imageFile) async { ...@@ -111,53 +164,67 @@ Future<String?> uploadImageToImgur(File imageFile) async {
// Function to handle book posting // Function to handle book posting
Future<void> _postBook() async { Future<void> _postBook() async {
if (titleController.text.isEmpty || if (titleController.text.isEmpty ||
priceController.text.isEmpty || isbnController.text.isEmpty || priceController.text.isEmpty || isbnController.text.isEmpty ||
authorController.text.isEmpty) { authorController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('All fields are required.')) const SnackBar(content: Text('All fields are required.'))
);
return;
}
// Show loading indicator while fetching description
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
},
); );
return;
}
String? description = await fetchBookDescription(isbnController.text); String? description = await fetchBookDescription(isbnController.text);
descriptionController.text = description ?? 'No description available'; descriptionController.text = description ?? 'No description available';
// Hide loading indicator
Navigator.of(context).pop();
bool success = await uploadBook(); bool success = await uploadBook();
if (success) { if (success) {
Navigator.pushReplacementNamed(context, '/profile'); ScaffoldMessenger.of(context).showSnackBar(
} else { const SnackBar(content: Text('Book posted successfully!'))
ScaffoldMessenger.of(context).showSnackBar( );
SnackBar(content: Text('Failed to post book. Try again.')) Navigator.pushReplacementNamed(context, '/profile');
); } else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to post book. Try again.'))
);
}
} }
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return Scaffold(
drawer: NavBar(),
appBar: AppBar( appBar: AppBar(
iconTheme: IconThemeData( iconTheme: IconThemeData(
color: isDarkMode ? kDarkBackground : kLightBackground, color: isDarkMode ? kDarkBackground : kLightBackground,
), ),
title: const Text( title: const Text(
"Post a Book", "Post a Book",
style: TextStyle( style: TextStyle(
fontFamily: 'Impact', // Ensure "Impact" is available in your fonts fontFamily: 'Impact',
fontSize: 24, // Adjust size as needed fontSize: 24,
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: kPrimaryColor, color: kPrimaryColor,
), ),
), ),
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, foregroundColor: isDarkMode ? kDarkBackground : kLightBackground,
),
),
body: Padding( body: Padding(
padding: EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
...@@ -165,34 +232,32 @@ Future<String?> uploadImageToImgur(File imageFile) async { ...@@ -165,34 +232,32 @@ Future<String?> uploadImageToImgur(File imageFile) async {
// Title Input // Title Input
TextField( TextField(
controller: titleController, controller: titleController,
decoration: InputDecoration(labelText: 'Title'), decoration: const InputDecoration(labelText: 'Title'),
), ),
SizedBox(height: 10), const SizedBox(height: 10),
// Price Input // Price Input
TextField( TextField(
controller: priceController, controller: priceController,
decoration: InputDecoration(labelText: 'Price'), decoration: const InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
SizedBox(height: 10), const SizedBox(height: 10),
// ISBN Input // ISBN Input
TextField( TextField(
controller: isbnController, controller: isbnController,
decoration: InputDecoration(labelText: 'ISBN Number'), decoration: const InputDecoration(labelText: 'ISBN Number'),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
SizedBox(height: 10), const SizedBox(height: 10),
// Author Input // Author Input
TextField( TextField(
controller: authorController, controller: authorController,
decoration: InputDecoration(labelText: 'Author'), decoration: const InputDecoration(labelText: 'Author'),
), ),
SizedBox(height: 10), const SizedBox(height: 10),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: _selectedCondition, value: _selectedCondition,
...@@ -207,17 +272,16 @@ Future<String?> uploadImageToImgur(File imageFile) async { ...@@ -207,17 +272,16 @@ Future<String?> uploadImageToImgur(File imageFile) async {
_selectedCondition = value!; _selectedCondition = value!;
}); });
}, },
// Set the dropdown color
dropdownColor: isDarkMode ? kDarkBackground : kLightBackground, dropdownColor: isDarkMode ? kDarkBackground : kLightBackground,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Condition', labelText: 'Condition',
filled: true, filled: true,
fillColor: isDarkMode ? kDarkBackground : kLightBackground, // Match scaffold color fillColor: isDarkMode ? kDarkBackground : kLightBackground,
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
), ),
), ),
SizedBox(height: 50), const SizedBox(height: 50),
// Image Picker Buttons // Image Picker Buttons
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
...@@ -225,56 +289,76 @@ Future<String?> uploadImageToImgur(File imageFile) async { ...@@ -225,56 +289,76 @@ Future<String?> uploadImageToImgur(File imageFile) async {
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.camera), onPressed: () => _pickImage(ImageSource.camera),
icon: Icon(Icons.camera, color: isDarkMode ? kLightText: kDarkText), icon: Icon(Icons.camera, color: isDarkMode ? kLightText: kDarkText),
label: Text('Camera'), label: const Text('Camera'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, // Background color backgroundColor: isDarkMode ? kLightBackground : kDarkBackground,
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, // Text color foregroundColor: isDarkMode ? kDarkBackground : kLightBackground,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), // Padding padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // Rounded corners borderRadius: BorderRadius.circular(10),
), ),
), ),
), ),
SizedBox(width: 20), const SizedBox(width: 20),
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.gallery), onPressed: () => _pickImage(ImageSource.gallery),
icon: Icon(Icons.photo_library,color: isDarkMode ? kLightText: kDarkText), icon: Icon(Icons.photo_library, color: isDarkMode ? kLightText: kDarkText),
label: Text('Gallery'), label: const Text('Gallery'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, // Background color backgroundColor: isDarkMode ? kLightBackground : kDarkBackground,
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, // Text color foregroundColor: isDarkMode ? kDarkBackground : kLightBackground,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), // Padding padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // Rounded corners borderRadius: BorderRadius.circular(10),
), ),
), ),
), ),
], ],
), ),
SizedBox(height: 10), const SizedBox(height: 20),
// Display Selected Image // Display Selected Image
if (_imageFile != null) if (_imageFile != null) ...[
Container( Container(
height: 150, height: 200,
width: double.infinity, width: double.infinity,
child: Image.file(_imageFile!, fit: BoxFit.cover), decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(_imageFile!, fit: BoxFit.cover),
),
),
const SizedBox(height: 10),
Center(
child: TextButton.icon(
onPressed: () {
setState(() {
_imageFile = null;
});
},
icon: const Icon(Icons.delete, color: Colors.red),
label: const Text('Remove Image', style: TextStyle(color: Colors.red)),
),
), ),
SizedBox(height: 20), ],
const SizedBox(height: 20),
// Post Book Button // Post Book Button
Center( Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: _postBook, onPressed: _postBook,
child: Text('Post Book'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, // Background color backgroundColor: isDarkMode ? kLightBackground : kDarkBackground,
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, // Text color foregroundColor: isDarkMode ? kDarkBackground : kLightBackground,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), // Padding padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // Rounded corners borderRadius: BorderRadius.circular(10),
), ),
), ),
child: const Text('Post Book'),
), ),
), ),
], ],
...@@ -297,10 +381,10 @@ Future<String?> uploadImageToImgur(File imageFile) async { ...@@ -297,10 +381,10 @@ Future<String?> uploadImageToImgur(File imageFile) async {
} else if (index == 1) { } else if (index == 1) {
Navigator.pushNamed(context, '/post'); Navigator.pushNamed(context, '/post');
} else if (index == 2) { } else if (index == 2) {
Navigator.pushNamed(context, '/inbox'); // Stay on the same page Navigator.pushNamed(context, '/inbox');
} }
}, },
), ),
); );
} }
} }
\ No newline at end of file
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:image_picker/image_picker.dart';
import 'package:paperchase_app/book_detail_page.dart'; import 'package:paperchase_app/book_detail_page.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:io';
import 'colors.dart'; import 'colors.dart';
import 'NavBar.dart'; import 'NavBar.dart';
...@@ -54,8 +51,8 @@ class ProfilePage extends StatelessWidget { ...@@ -54,8 +51,8 @@ class ProfilePage extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
iconTheme: IconThemeData( iconTheme: IconThemeData(
color: isDarkMode ? kDarkBackground : kLightBackground, color: isDarkMode ? kDarkBackground : kLightBackground,
), ),
title: const Text( title: const Text(
"Profile", "Profile",
style: TextStyle( style: TextStyle(
...@@ -67,8 +64,7 @@ class ProfilePage extends StatelessWidget { ...@@ -67,8 +64,7 @@ class ProfilePage extends StatelessWidget {
), ),
), ),
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, foregroundColor: isDarkMode ? kDarkBackground : kLightBackground,
),
),
drawer: const NavBar(), drawer: const NavBar(),
body: Container( body: Container(
color: backgroundColor, color: backgroundColor,
...@@ -139,6 +135,8 @@ class ProfilePage extends StatelessWidget { ...@@ -139,6 +135,8 @@ class ProfilePage extends StatelessWidget {
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Books section
StreamBuilder<QuerySnapshot>( StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance stream: FirebaseFirestore.instance
.collection('books') .collection('books')
...@@ -158,8 +156,6 @@ class ProfilePage extends StatelessWidget { ...@@ -158,8 +156,6 @@ class ProfilePage extends StatelessWidget {
final books = booksSnapshot.data!.docs; final books = booksSnapshot.data!.docs;
if (books.isEmpty) { if (books.isEmpty) {
return Text( return Text(
'No books posted yet', 'No books posted yet',
...@@ -208,13 +204,11 @@ class ProfilePage extends StatelessWidget { ...@@ -208,13 +204,11 @@ class ProfilePage extends StatelessWidget {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => BookDetailsPage(book: book, bookId: doc.id ), builder: (context) => BookDetailsPage(book: book, bookId: doc.id),
), ),
); );
}, },
...@@ -226,6 +220,247 @@ class ProfilePage extends StatelessWidget { ...@@ -226,6 +220,247 @@ class ProfilePage extends StatelessWidget {
); );
}, },
), ),
const SizedBox(height: 32),
// Reviews section
StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('reviews')
.where('sellerId', isEqualTo: currentUser.uid)
.snapshots(),
builder: (context, reviewsSnapshot) {
if (reviewsSnapshot.hasError) {
return Text(
'Error loading reviews',
style: TextStyle(color: textColor),
);
}
if (reviewsSnapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final reviews = reviewsSnapshot.data!.docs;
if (reviews.isEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Reviews',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: textColor,
),
),
const SizedBox(height: 8),
Text(
'No reviews yet',
textAlign: TextAlign.center,
style: TextStyle(color: textColor.withOpacity(0.7)),
),
],
);
}
// Calculate average rating
double totalRating = 0;
for (var review in reviews) {
totalRating += (review.data() as Map<String, dynamic>)['rating'] ?? 0;
}
double averageRating = totalRating / reviews.length;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Reviews',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: textColor,
),
),
Row(
children: [
Text(
averageRating.toStringAsFixed(1),
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(width: 4),
Icon(
Icons.star,
color: Colors.amber,
size: 20,
),
Text(
' (${reviews.length})',
style: TextStyle(
color: textColor.withOpacity(0.7),
fontSize: 14,
),
),
],
),
],
),
const SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: reviews.length,
itemBuilder: (context, index) {
final reviewDoc = reviews[index];
final review = reviewDoc.data() as Map<String, dynamic>;
final reviewId = reviewDoc.id;
final rating = review['rating'] ?? 0;
final comment = review['comment'] ?? 'No comment';
final buyerName = review['buyerName'] ?? 'Anonymous';
final timestamp = review['timestamp'] as Timestamp?;
final date = timestamp != null
? '${timestamp.toDate().day}/${timestamp.toDate().month}/${timestamp.toDate().year}'
: 'Unknown date';
return Card(
color: isDarkMode ? Colors.grey[900] : Colors.white,
margin: const EdgeInsets.only(bottom: 8),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
buyerName,
style: TextStyle(
fontWeight: FontWeight.bold,
color: textColor,
),
),
Text(
date,
style: TextStyle(
fontSize: 12,
color: textColor.withOpacity(0.7),
),
),
],
),
),
Row(
children: [
// Rating stars
Row(
children: List.generate(5, (i) {
return Icon(
i < rating ? Icons.star : Icons.star_border,
color: Colors.amber,
size: 16,
);
}),
),
const SizedBox(width: 8),
// Delete button
IconButton(
icon: Icon(
Icons.delete,
color: Colors.red.withOpacity(0.7),
size: 20,
),
onPressed: () {
// Show confirmation dialog
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
title: Text(
'Delete Review',
style: TextStyle(color: textColor),
),
content: Text(
'Are you sure you want to delete this review?',
style: TextStyle(color: textColor),
),
actions: [
TextButton(
child: Text(
'Cancel',
style: TextStyle(color: kPrimaryColor),
),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text(
'Delete',
style: TextStyle(color: Colors.red),
),
onPressed: () {
// Delete the review
FirebaseFirestore.instance
.collection('reviews')
.doc(reviewId)
.delete()
.then((_) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Review deleted successfully'),
backgroundColor: Colors.green,
),
);
}).catchError((error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to delete review: $error'),
backgroundColor: Colors.red,
),
);
});
Navigator.of(context).pop();
},
),
],
);
},
);
},
),
],
),
],
),
const SizedBox(height: 8),
Text(
comment,
style: TextStyle(color: textColor),
),
],
),
),
);
},
),
],
);
},
),
], ],
); );
}, },
......
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:intl/intl.dart';
import 'book_detail_page.dart';
class SellerProfilePage extends StatelessWidget {
final String sellerId;
const SellerProfilePage({super.key, required this.sellerId});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(
title: const Text('Seller Profile'),
backgroundColor: isDarkMode ? Colors.black : Colors.white,
foregroundColor: isDarkMode ? Colors.white : Colors.black,
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Seller's Info
FutureBuilder<DocumentSnapshot>(
future: FirebaseFirestore.instance.collection('users').doc(sellerId).get(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final data = snapshot.data?.data() as Map<String, dynamic>?;
if (data == null) {
return const Center(child: Text('Seller not found'));
}
final sellerName = "${data['first_name']} ${data['last_name']}".trim();
final sellerAvatar = data['avatar_url'] ?? '';
final sellerRating = (data['average_rating'] ?? 0).toDouble();
final ratingCount = (data['review_count'] ?? 0);
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundImage: sellerAvatar.isNotEmpty ? NetworkImage(sellerAvatar) : null,
child: sellerAvatar.isEmpty ? const Icon(Icons.person, size: 40) : null,
),
const SizedBox(height: 12),
Text(
sellerName,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: List.generate(5, (index) {
if (index < sellerRating.floor()) {
return Icon(Icons.star, color: Colors.amber, size: 24);
} else if (index == sellerRating.floor() && sellerRating % 1 > 0) {
return Icon(Icons.star_half, color: Colors.amber, size: 24);
} else {
return Icon(Icons.star_border, color: Colors.amber, size: 24);
}
}),
),
const SizedBox(width: 8),
Text(
'${sellerRating.toStringAsFixed(1)} (${ratingCount})',
style: TextStyle(
fontSize: 16,
color: isDarkMode ? Colors.white70 : Colors.black87,
),
),
],
),
],
),
);
},
),
const Divider(),
// Reviews Section
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Reviews',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
_buildReviewsList(context, isDarkMode),
const Divider(),
// Seller's Listings
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
"Seller's Books",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
_buildSellerListings(context, isDarkMode),
],
),
),
);
}
Widget _buildReviewsList(BuildContext context, bool isDarkMode) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('users')
.doc(sellerId)
.collection('reviews')
.orderBy('createdAt', descending: true)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
);
}
final reviews = snapshot.data?.docs ?? [];
if (reviews.isEmpty) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text('No reviews yet'),
),
);
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: reviews.length,
itemBuilder: (context, index) {
final review = reviews[index].data() as Map<String, dynamic>;
final rating = review['rating'] ?? 0;
final comment = review['comment'] ?? '';
final userName = review['userName'] ?? 'Anonymous';
final userAvatar = review['userAvatar'] ?? '';
final bookTitle = review['bookTitle'] ?? 'Unknown Book';
// Format the timestamp
String formattedDate = 'Recently';
if (review['createdAt'] != null) {
final timestamp = review['createdAt'] as Timestamp;
final date = timestamp.toDate();
formattedDate = DateFormat('MMM d, yyyy').format(date);
}
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
color: isDarkMode ? Colors.grey[900] : Colors.white,
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 20,
backgroundImage: userAvatar.isNotEmpty ? NetworkImage(userAvatar) : null,
child: userAvatar.isEmpty ? const Icon(Icons.person, size: 20) : null,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
userName,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
Text(
formattedDate,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
const SizedBox(height: 4),
Row(
children: List.generate(5, (i) {
return Icon(
i < rating ? Icons.star : Icons.star_border,
color: Colors.amber,
size: 16,
);
}),
),
],
),
),
],
),
if (comment.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
comment,
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
),
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'Purchased: $bookTitle',
style: TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
color: Colors.grey[600],
),
),
),
],
),
),
);
},
);
},
);
}
Widget _buildSellerListings(BuildContext context, bool isDarkMode) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('books')
.where('userId', isEqualTo: sellerId)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final books = snapshot.data?.docs ?? [];
if (books.isEmpty) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: Text('No listings yet')),
);
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: books.length,
itemBuilder: (context, index) {
final book = books[index].data() as Map<String, dynamic>;
final bookId = books[index].id;
final title = book['title'] ?? 'No title';
final price = book['price'] is String
? double.tryParse(book['price']) ?? 0.0
: book['price'] ?? 0.0;
final imageUrl = book['imageUrl'] ?? '';
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
color: isDarkMode ? Colors.grey[900] : Colors.white,
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BookDetailsPage(
book: book,
bookId: bookId,
),
),
);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Container(
width: 60,
height: 80,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(4),
),
child: imageUrl.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Icon(Icons.book, size: 30, color: Colors.grey[500]);
},
),
)
: Icon(Icons.book, size: 30, color: Colors.grey[500]),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'\$${price.toStringAsFixed(2)}',
style: TextStyle(
color: Colors.green[700],
fontWeight: FontWeight.bold,
),
),
],
),
),
const Icon(Icons.chevron_right),
],
),
),
),
);
},
);
},
);
}
}
\ No newline at end of file
...@@ -6,10 +6,10 @@ class SettingsPage extends StatefulWidget { ...@@ -6,10 +6,10 @@ class SettingsPage extends StatefulWidget {
final VoidCallback toggleTheme; final VoidCallback toggleTheme;
const SettingsPage({ const SettingsPage({
Key? key, super.key,
required this.isDarkMode, required this.isDarkMode,
required this.toggleTheme, required this.toggleTheme,
}) : super(key: key); });
@override @override
State<SettingsPage> createState() => _SettingsPageState(); State<SettingsPage> createState() => _SettingsPageState();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment