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 @override
Widget build(BuildContext context) { State<BookDetailsPage> createState() => _BookDetailsPageState();
}
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; 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; final currentUser = FirebaseAuth.instance.currentUser;
final isMyBook = currentUser?.uid == book['userId']; 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
Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
final currentUser = FirebaseAuth.instance.currentUser;
final isMyBook = currentUser?.uid == widget.book['userId'];
final title = book['title'] ?? 'No title available'; final title = widget.book['title'] ?? 'No title available';
final author = book['author'] ?? 'No author available'; final author = widget.book['author'] ?? 'No author available';
final isbn = book['isbn'] ?? 'No ISBN available'; final isbn = widget.book['isbn'] ?? 'No ISBN available';
final price = book['price'] is String final price = widget.book['price'] is String
? double.tryParse(book['price']) ?? 0.0 ? double.tryParse(widget.book['price']) ?? 0.0
: book['price'] ?? 0.0; : widget.book['price'] ?? 0.0;
final condition = book['condition'] ?? 'Condition not available'; final condition = widget.book['condition'] ?? 'Condition not available';
final description = book['description'] ?? 'No description available'; final description = widget.book['description'] ?? 'No description available';
final imageUrl = book['imageUrl'] ?? 'https://via.placeholder.com/200'; final imageUrl = widget.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),
onPressed: () => Navigator.pop(context),
), ),
title: Text( actions: [
title, if (isMyBook)
IconButton(
icon: Icon(Icons.more_vert, color: isDarkMode ? Colors.white : Colors.black),
onPressed: () => _showOptionsMenu(context),
),
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 1.0,
child: Container(
width: double.infinity,
color: Colors.grey[300],
child: imageUrl.isNotEmpty
? Image.network(
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])),
),
),
Container(
color: isDarkMode ? Colors.black : Colors.white,
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'\$${price.toStringAsFixed(2)}',
style: TextStyle( style: TextStyle(
fontFamily: 'Impact',
fontSize: 24, fontSize: 24,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: kPrimaryColor, color: isDarkMode ? Colors.white : Colors.black,
),
),
],
), ),
const SizedBox(height: 8),
Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black,
),
),
const SizedBox(height: 4),
],
), ),
), ),
body: SingleChildScrollView( const SizedBox(height: 8),
child: Padding(
// Seller Section with Rating
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SellerProfilePage(
sellerId: widget.book['userId'],
),
),
);
},
child: Container(
color: isDarkMode ? Colors.black : Colors.white,
padding: const EdgeInsets.all(16.0), 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Center( Text(
child: imageUrl.isNotEmpty sellerName,
? Image.network(imageUrl, height: 200, fit: BoxFit.cover) style: TextStyle(
: Icon(Icons.book, size: 100), fontSize: 16,
), fontWeight: FontWeight.w500,
const SizedBox(height: 20), color: isDarkMode ? Colors.white : Colors.black,
Text("Title: $title",
style:
const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
Text("Author: $author", style: const TextStyle(fontSize: 18)),
Text("ISBN: $isbn", style: const TextStyle(fontSize: 16)),
Text("Price: \$${price.toStringAsFixed(2)}",
style: const TextStyle(fontSize: 16, color: Colors.green)),
Text("Condition: $condition", style: const TextStyle(fontSize: 16)),
const SizedBox(height: 10),
const Text("Description:",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Text(description, style: const TextStyle(fontSize: 16)),
const SizedBox(height: 24),
if (isMyBook && currentUser != null)
SizedBox(
width: double.infinity,
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( Text(
'Delete Book', isMyBook ? 'You' : 'Seller',
style: TextStyle(fontSize: 18, color: Colors.white), 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),
],
);
},
),
), ),
), ),
)
else if (!isMyBook && currentUser != null) const SizedBox(height: 8),
SizedBox(
width: double.infinity, // Description section
child: ElevatedButton( Container(
onPressed: () => color: isDarkMode ? Colors.black : Colors.white,
_contactSeller(context, book, bookId), padding: const EdgeInsets.all(16.0),
style: ElevatedButton.styleFrom( child: Column(
backgroundColor: kPrimaryColor, crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.symmetric(vertical: 16), children: [
shape: RoundedRectangleBorder( Text(
borderRadius: BorderRadius.circular(12), 'Description',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
), ),
), ),
child: const Text( const SizedBox(height: 12),
'Contact Seller', Text(
style: TextStyle(fontSize: 18, color: Colors.white), description,
style: TextStyle(
fontSize: 16,
color: isDarkMode ? Colors.white70 : Colors.black87,
),
),
],
), ),
), ),
) const SizedBox(height: 8),
else if (currentUser == null) // 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( Center(
child: TextButton( child: ElevatedButton(
onPressed: () => style: ElevatedButton.styleFrom(
Navigator.pushNamed(context, '/login'), 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( child: const Text(
'Log in to contact the seller', 'Edit Review',
style: TextStyle(fontSize: 16), 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: !isMyBook
? Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.black : Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 5,
offset: const Offset(0, -3),
), ),
bottomNavigationBar: BottomNavigationBar(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground,
selectedItemColor:
isDarkMode ? kDarkBackground : kLightBackground,
unselectedItemColor:
isDarkMode ? kDarkBackground : kLightBackground,
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) { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
Navigator.pushNamedAndRemoveUntil(context, '/', (_) => false); child: FutureBuilder<DocumentSnapshot>(
} else if (index == 1) { future: _sellerFuture,
Navigator.pushNamed(context, '/post'); builder: (context, snapshot) {
} else if (index == 2) { String sellerName = 'Unknown Seller';
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('_')}"; setState(() {
_isSubmitting = true;
});
try { try {
final sellerDoc = await FirebaseFirestore.instance // Get current user info
final userDoc = await FirebaseFirestore.instance
.collection('users') .collection('users')
.doc(sellerId) .doc(currentUser.uid)
.get(); .get();
final sellerName = sellerDoc.exists final userName = userDoc.exists
? "${sellerDoc['first_name']} ${sellerDoc['last_name']}" ? "${userDoc.data()?['first_name'] ?? ''} ${userDoc.data()?['last_name'] ?? ''}".trim()
: "Unknown Seller"; : "Anonymous User";
final chatRef = FirebaseFirestore.instance.collection('chats').doc(chatRoomId); final userAvatar = userDoc.data()?['avatar_url'] ?? '';
final chatData = { // Save the review to seller's reviews subcollection
'users': users, await FirebaseFirestore.instance
'bookId': bookId, .collection('users')
'bookTitle': book['title'], .doc(widget.book['userId'])
'lastMessage': 'Hi! Is this book still available?', .collection('reviews')
'lastMessageTime': FieldValue.serverTimestamp(), .doc(currentUser.uid)
.set({
'userId': currentUser.uid,
'rating': _userRating,
'comment': _reviewController.text.trim(),
'userName': userName,
'userAvatar': userAvatar,
'bookId': widget.bookId,
'bookTitle': widget.book['title'] ?? 'Unknown Book',
'createdAt': FieldValue.serverTimestamp(), 'createdAt': FieldValue.serverTimestamp(),
'participants': {
currentUser.uid: true,
sellerId: true,
},
'sellerId': sellerId,
'buyerId': isBuyer ? currentUser.uid : null, // null if seller is messaging
};
final existingChat = await chatRef.get();
if (existingChat.exists) {
await chatRef.update({
'lastMessage': chatData['lastMessage'],
'lastMessageTime': chatData['lastMessageTime'],
}); });
} else {
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;
} }
await chatRef.collection('messages').add({ final newAverageRating = reviews.isEmpty ? 0 : totalRating / reviews.length;
'senderId': currentUser.uid,
'message': 'Hi! Is this book still available?', // Update the seller's user document with new average rating
'timestamp': FieldValue.serverTimestamp(), await FirebaseFirestore.instance
'read': false, .collection('users')
.doc(widget.book['userId'])
.update({
'average_rating': newAverageRating,
'review_count': reviews.length,
}); });
Navigator.push( if (mounted) {
context, setState(() {
MaterialPageRoute( _hasReviewed = true;
builder: (context) => StrictChatPage( _isSubmitting = false;
chatId: chatRoomId, });
otherUserName: sellerName,
currentUserId: currentUser.uid, ScaffoldMessenger.of(context).showSnackBar(
sellerId: sellerId, const SnackBar(content: Text('Thank you for your review!')),
),
),
); );
}
} catch (e) { } catch (e) {
debugPrint('Error starting chat: $e'); if (mounted) {
setState(() {
_isSubmitting = false;
});
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to contact seller. Please try again.')), SnackBar(content: Text('Error submitting review: $e')),
); );
} }
} }
}
Future<void> _editReview(BuildContext context) async {
setState(() {
_hasReviewed = false;
});
}
void _showOptionsMenu(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: const Text('Delete Book'),
onTap: () {
Navigator.pop(context); // Close the bottom sheet
_confirmAndDeleteBook(context);
},
),
],
),
),
);
}
void _confirmAndDeleteBook(BuildContext context, String bookId) async { // Fixed method that prevents using context after async gap
void _confirmAndDeleteBook(BuildContext context) 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) {
if (timestamp == null) return 'Date not available';
String _formatPrice(dynamic price) { final date = timestamp.toDate();
if (price == null) return '0.00'; final now = DateTime.now();
if (price is num) return price.toStringAsFixed(2); final difference = now.difference(date);
if (price is String) {
try { if (difference.inDays == 0) {
return double.parse(price).toStringAsFixed(2); return 'Today';
} catch (_) { } else if (difference.inDays == 1) {
return price; return 'Yesterday';
} } else if (difference.inDays < 7) {
} return '${difference.inDays} days ago';
return '0.00'; } 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() {
...@@ -79,7 +56,6 @@ List<String> get predefinedMessages { ...@@ -79,7 +56,6 @@ List<String> get predefinedMessages {
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,
...@@ -266,57 +237,27 @@ List<String> get predefinedMessages { ...@@ -266,57 +237,27 @@ List<String> get predefinedMessages {
fontSize: 14, 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(
'Confirmed!',
style: TextStyle(fontSize: 18, color: kLightText),
),
),
),
if (widget.currentUserId == widget.sellerId)
const SizedBox(height: 12), const SizedBox(height: 12),
Wrap( Wrap(
spacing: 12, spacing: 12,
runSpacing: 12, runSpacing: 12,
children: predefinedMessages.map((msg) { children: widget.predefinedMessages.map((msg) {
return ActionChip( return ActionChip(
label: Text(msg), label: Text(msg),
onPressed: () => _sendMessage(msg), onPressed: () => _sendMessage(msg),
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
labelStyle: TextStyle(color: textColor,), labelStyle: TextStyle(
color: textColor,
),
); );
}).toList(), }).toList(),
), ),
const SizedBox(height: 12), 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,13 +102,34 @@ class _InboxPageState extends State<InboxPage> { ...@@ -71,13 +102,34 @@ 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(
children: [
const Text(
'Inbox', 'Inbox',
style: TextStyle( style: TextStyle(
fontFamily: 'Impact', fontFamily: 'Impact',
...@@ -87,6 +139,39 @@ class _InboxPageState extends State<InboxPage> { ...@@ -87,6 +139,39 @@ class _InboxPageState extends State<InboxPage> {
color: kPrimaryColor, 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}',
...@@ -115,7 +219,24 @@ class _InboxPageState extends State<InboxPage> { ...@@ -115,7 +219,24 @@ class _InboxPageState extends State<InboxPage> {
}, },
), ),
), ),
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');
}
},
),
); );
} }
...@@ -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,15 +286,28 @@ class _InboxPageState extends State<InboxPage> { ...@@ -152,15 +286,28 @@ 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(),
...@@ -172,95 +319,6 @@ class _InboxPageState extends State<InboxPage> { ...@@ -172,95 +319,6 @@ class _InboxPageState extends State<InboxPage> {
if (userName.isEmpty) userName = 'Unknown User'; if (userName.isEmpty) userName = 'Unknown User';
} }
return FutureBuilder<QuerySnapshot>(
future: FirebaseFirestore.instance
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timestamp', descending: false)
.limit(1)
.get(),
builder: (context, messagesSnapshot) {
String sellerId = '';
String buyerId = '';
// Check who sent first message to determine buyer
if (messagesSnapshot.hasData && messagesSnapshot.data!.docs.isNotEmpty) {
final firstMessage = messagesSnapshot.data!.docs.first;
final firstMessageData = firstMessage.data() as Map<String, dynamic>;
buyerId = firstMessageData['senderId'] as String? ?? '';
// If buyer is first message sender, seller is the other user
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,
);
},
);
}
return _buildChatListItem(
context,
chatId,
userName,
currentUser.uid,
sellerId,
bookTitle,
lastMessage,
lastMessageTime,
isDarkMode,
textColor,
);
},
);
},
);
},
);
}
Widget _buildChatListItem(
BuildContext context,
String chatId,
String userName,
String currentUserId,
String sellerId,
String? bookTitle,
String? lastMessage,
DateTime? lastMessageTime,
bool isDarkMode,
Color textColor,
) {
return Card( return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: isDarkMode ? Colors.grey[900] : Colors.white, color: isDarkMode ? Colors.grey[900] : Colors.white,
...@@ -270,10 +328,17 @@ class _InboxPageState extends State<InboxPage> { ...@@ -270,10 +328,17 @@ class _InboxPageState extends State<InboxPage> {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StrictChatPage( builder: (context) => StrictChatPage(
chatId: chatId, chatId: chat.id,
otherUserName: userName, otherUserName: userName,
currentUserId: currentUserId, predefinedMessages: const [
sellerId: sellerId, "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?",
],
), ),
), ),
); );
...@@ -281,7 +346,7 @@ class _InboxPageState extends State<InboxPage> { ...@@ -281,7 +346,7 @@ class _InboxPageState extends State<InboxPage> {
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200], backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200],
child: Text( child: Text(
userName.isNotEmpty ? userName[0].toUpperCase() : '?', userName[0].toUpperCase(),
style: TextStyle( style: TextStyle(
color: textColor, color: textColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
...@@ -327,25 +392,8 @@ class _InboxPageState extends State<InboxPage> { ...@@ -327,25 +392,8 @@ class _InboxPageState extends State<InboxPage> {
: null, : 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');
}
}, },
); );
} }
......
...@@ -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,11 +88,9 @@ class _MyAppState extends State<MyApp> { ...@@ -87,11 +88,9 @@ 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(),
...@@ -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(() {
_filteredBooks = _books;
});
return; return;
} }
try { try {
final QuerySnapshot snapshot = await FirebaseFirestore.instance final filteredBooks = _books.where((doc) {
.collection('books')
.orderBy('timestamp', descending: true)
.get();
final filteredBooks = snapshot.docs.where((doc) {
final data = doc.data() as Map<String, dynamic>; final data = doc.data() as Map<String, dynamic>;
final title = (data['title'] ?? '').toString().toLowerCase(); final title = (data['title'] ?? '').toString().toLowerCase();
final author = (data['author'] ?? '').toString().toLowerCase(); final author = (data['author'] ?? '').toString().toLowerCase();
final isbn = (data['isbn'] ?? '').toString(); final isbn = (data['isbn'] ?? '').toString();
return title.contains(query) || author.contains(query) || isbn.contains(query); return title.contains(query) || author.contains(query) || isbn.contains(query);
}).toList(); }).toList();
setState(() { setState(() {
_books = filteredBooks; _filteredBooks = filteredBooks;
}); });
} catch (e) { } catch (e) {
print("Error searching books: $e"); print("Error searching books: $e");
} }
} }
Future<void> _loadRecentBooks() async { Future<void> _loadRecentBooks() async {
try { try {
final QuerySnapshot snapshot = await FirebaseFirestore.instance final QuerySnapshot snapshot = await FirebaseFirestore.instance
.collection('books') .collection('books')
.orderBy('timestamp', descending: true) // Sort by the most recent posts .orderBy('timestamp', descending: true)
.limit(10) // Optionally limit to the latest 10 books .limit(20) // Increased limit to show more books
.get(); .get();
setState(() { setState(() {
_books = snapshot.docs; _books = snapshot.docs;
_filteredBooks = snapshot.docs;
}); });
} catch (e) { } catch (e) {
print("Error fetching recent books: $e"); print("Error fetching recent books: $e");
} }
} }
void _filterBooks() { // Apply filters to the current book collection
void _applyFilters() {
setState(() { setState(() {
if (_filterBy == 'Latest Posted') { _filteredBooks = _books.where((doc) {
_books.sort((a, b) => (b['timestamp'] as Timestamp).compareTo(a['timestamp'] as Timestamp)); final data = doc.data() as Map<String, dynamic>;
} else if (_filterBy == 'Price: Low to High') {
_books.sort((a, b) => (a['price'] ?? 0).compareTo(b['price'] ?? 0)); // Condition filter only
} else if (_filterBy == 'Price: High to Low') { final condition = (data['condition'] ?? '').toString();
_books.sort((a, b) => (b['price'] ?? 0).compareTo(a['price'] ?? 0)); final isConditionSelected = _selectedConditions.isEmpty ||
} else if (_filterBy == 'Condition: Best to Worst') { _selectedConditions.contains(condition);
_books.sort((a, b) => _conditionRanking(a['condition']).compareTo(_conditionRanking(b['condition'])));
} else if (_filterBy == 'Condition: Worst to Best') { return isConditionSelected;
_books.sort((a, b) => _conditionRanking(b['condition']).compareTo(_conditionRanking(a['condition']))); }).toList();
_filtersActive = true;
});
} }
// Reset filters and show all books
void _resetFilters() {
setState(() {
_selectedConditions = [];
_filteredBooks = _books;
_filtersActive = false;
}); });
} }
int _conditionRanking(String? condition) { // Improved filter button
const conditionOrder = { Widget _buildFilterButton() {
'Like New': 1, final bool isActive = _filtersActive;
'Good': 2,
'Fair': 3, return InkWell(
'Poor': 4 onTap: _showFilterDialog,
}; child: AnimatedContainer(
return conditionOrder[condition] ?? 0; duration: const Duration(milliseconds: 200),
} padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isActive ? kPrimaryColor : Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isActive ? kPrimaryColor : widget.isDarkMode ? Colors.grey[600]! : Colors.grey[400]!,
width: 1.5,
),
),
child: Stack(
children: [
Icon(
Icons.filter_list,
color: isActive ? Colors.white : (widget.isDarkMode ? Colors.grey[400] : Colors.grey[700]),
size: 22,
),
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,12 +423,16 @@ String _filterBy = 'Latest Posted'; // Default filter option ...@@ -266,12 +423,16 @@ 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
Row(
children: [
// Search Field
Expanded(
child: TextField(
controller: _searchController, controller: _searchController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: "Search for books by title, author, or ISBN", hintText: "Search for books",
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
suffixIcon: IconButton( suffixIcon: IconButton(
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
...@@ -279,117 +440,241 @@ String _filterBy = 'Latest Posted'; // Default filter option ...@@ -279,117 +440,241 @@ String _filterBy = 'Latest Posted'; // Default filter option
), ),
), ),
), ),
const SizedBox(height: 15),
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,
), ),
// Filter Button (updated)
const SizedBox(width: 8),
_buildFilterButton(),
], ],
), ),
child: Column(
mainAxisSize: MainAxisSize.min, // Active Filters Chips
if (_filtersActive) ...[
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [ children: [
for (var filterOption in [ // Condition Chips
'Latest Posted', ..._selectedConditions.map((condition) {
'Price: Low to High', return Padding(
'Price: High to Low', padding: const EdgeInsets.only(right: 8),
'Condition: Best to Worst', child: Chip(
'Condition: Worst to Best' label: Text(condition),
]) deleteIcon: const Icon(Icons.close, size: 18),
ListTile( onDeleted: () {
title: Text(
filterOption,
style: TextStyle(color: widget.isDarkMode ? kDarkText : kLightText),
),
onTap: () {
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 const SizedBox(height: 15),
child: Container(
padding: const EdgeInsets.all(8), // Compact padding // Results count
decoration: BoxDecoration( Align(
color: widget.isDarkMode ? kDarkBackground : kLightBackground, alignment: Alignment.centerLeft,
borderRadius: BorderRadius.circular(12), // ✅ Rounded border child: Text(
border: Border.all( '${_filteredBooks.length} books found',
color: widget.isDarkMode ? Colors.grey : Colors.black12, // Light border style: TextStyle(
width: 1, 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: 10), const SizedBox(height: 8),
// Books Grid
Expanded( Expanded(
child: _filteredBooks.isEmpty
child: ListView.builder( ? Center(
itemCount: _books.length, child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off,
size: 64,
color: widget.isDarkMode ? Colors.grey[400] : Colors.grey[600]),
const SizedBox(height: 16),
Text(
'No books match your filters',
style: TextStyle(
fontSize: 18,
color: widget.isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
const SizedBox(height: 8),
TextButton(
onPressed: _resetFilters,
child: const Text('Reset Filters'),
),
],
),
)
: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 15,
crossAxisSpacing: 15,
childAspectRatio: 0.6,
),
itemCount: _filteredBooks.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final book = _books[index]; final doc = _filteredBooks[index];
final bookId = book.id; final data = doc.data() as Map<String, dynamic>;
final data = book.data() as Map<String, dynamic>;
final title = book.data()['title'] ?? "Unknown Title";
final author = book.data()['author'] ?? "No author available";
final thumbnail = book.data()['imageUrl'] ?? "https://via.placeholder.com/50";
final price = book.data()['price'];
return ListTile( final bookId = doc.id;
leading: Image.network(thumbnail, width: 50, height: 50, fit: BoxFit.cover), final title = data['title'] ?? 'Unknown Title';
title: Text(title), final condition = data['condition'] ?? 'Unknown';
subtitle: Text('$author - \$$price - ${book.data()['condition'] ?? 'Condition not available'}'), // 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: () { onTap: () {
if (_isLoggedIn) { if (_isLoggedIn) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => BookDetailsPage(book: book.data() as Map<String, dynamic>, bookId: bookId), // Pass book data builder: (context) => BookDetailsPage(
book: data,
bookId: bookId,
),
), ),
); );
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( const SnackBar(
content: Text("You need to log in to view book details."), content: Text("You need to log in to view book details."),
duration: Duration(seconds: 2), 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,
),
],
),
),
),
],
),
),
); );
}, },
), ),
...@@ -398,23 +683,14 @@ String _filterBy = 'Latest Posted'; // Default filter option ...@@ -398,23 +683,14 @@ String _filterBy = 'Latest Posted'; // Default filter option
), ),
), ),
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');
}
},
), ),
); );
} }
......
...@@ -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,10 +23,36 @@ class _PostBookPageState extends State<PostBookPage> { ...@@ -22,10 +23,36 @@ 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 {
// Request permission if using camera
if (source == ImageSource.camera) {
bool hasPermission = await _requestCameraPermission();
if (!hasPermission) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Camera permission is required to take photos'))
);
return;
}
}
final XFile? pickedFile = await _picker.pickImage(
source: source,
imageQuality: 80, // Optimize image quality
);
if (pickedFile != null) { if (pickedFile != null) {
setState(() { setState(() {
_imageFile = File(pickedFile.path); _imageFile = File(pickedFile.path);
...@@ -34,9 +61,15 @@ class _PostBookPageState extends State<PostBookPage> { ...@@ -34,9 +61,15 @@ class _PostBookPageState extends State<PostBookPage> {
} else { } else {
print('No image selected.'); 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 {
...@@ -51,9 +84,9 @@ Future<String?> fetchBookDescription(String isbn) async { ...@@ -51,9 +84,9 @@ Future<String?> fetchBookDescription(String isbn) async {
print("Error fetching book details: $e"); print("Error fetching book details: $e");
} }
return null; // Return null if no description is found 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')
...@@ -77,7 +110,7 @@ Future<String?> uploadImageToImgur(File imageFile) async { ...@@ -77,7 +110,7 @@ Future<String?> uploadImageToImgur(File imageFile) async {
print('Error uploading image: $e'); print('Error uploading image: $e');
return null; 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;
...@@ -115,30 +168,45 @@ Future<String?> uploadImageToImgur(File imageFile) async { ...@@ -115,30 +168,45 @@ Future<String?> uploadImageToImgur(File imageFile) async {
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; return;
} }
// Show loading indicator while fetching description
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
},
);
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) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Book posted successfully!'))
);
Navigator.pushReplacementNamed(context, '/profile'); Navigator.pushReplacementNamed(context, '/profile');
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to post book. Try again.')) 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,
...@@ -146,18 +214,17 @@ Future<String?> uploadImageToImgur(File imageFile) async { ...@@ -146,18 +214,17 @@ Future<String?> uploadImageToImgur(File imageFile) async {
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,
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), child: Image.file(_imageFile!, fit: BoxFit.cover),
), ),
SizedBox(height: 20), ),
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)),
),
),
],
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,7 +381,7 @@ Future<String?> uploadImageToImgur(File imageFile) async { ...@@ -297,7 +381,7 @@ 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');
} }
}, },
), ),
......
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';
...@@ -67,7 +64,6 @@ class ProfilePage extends StatelessWidget { ...@@ -67,7 +64,6 @@ class ProfilePage extends StatelessWidget {
), ),
), ),
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, foregroundColor: isDarkMode ? kDarkBackground : kLightBackground,
), ),
drawer: const NavBar(), drawer: const NavBar(),
body: Container( body: Container(
...@@ -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,17 +204,256 @@ class ProfilePage extends StatelessWidget { ...@@ -208,17 +204,256 @@ 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),
),
);
},
),
);
},
),
],
);
},
),
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