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:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:paperchase_app/chat_page.dart';
import 'colors.dart';
import 'package:paperchase_app/seller_profile_page.dart';
import 'package:paperchase_app/chat_page.dart'; // Import chat page
class BookDetailsPage extends StatelessWidget {
class BookDetailsPage extends StatefulWidget {
final Map<String, dynamic> book;
final String bookId;
const BookDetailsPage({super.key, required this.book, required this.bookId});
@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 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 author = book['author'] ?? 'No author available';
final isbn = book['isbn'] ?? 'No ISBN available';
final price = book['price'] is String
? double.tryParse(book['price']) ?? 0.0
: book['price'] ?? 0.0;
final condition = book['condition'] ?? 'Condition not available';
final description = book['description'] ?? 'No description available';
final imageUrl = book['imageUrl'] ?? 'https://via.placeholder.com/200';
final title = widget.book['title'] ?? 'No title available';
final author = widget.book['author'] ?? 'No author available';
final isbn = widget.book['isbn'] ?? 'No ISBN available';
final price = widget.book['price'] is String
? double.tryParse(widget.book['price']) ?? 0.0
: widget.book['price'] ?? 0.0;
final condition = widget.book['condition'] ?? 'Condition not available';
final description = widget.book['description'] ?? 'No description available';
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(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[100],
appBar: AppBar(
automaticallyImplyLeading: true,
iconTheme: IconThemeData(
color: isDarkMode ? kDarkBackground : kLightBackground,
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: isDarkMode ? Colors.white : Colors.black),
onPressed: () => Navigator.pop(context),
),
title: Text(
title,
actions: [
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(
fontFamily: 'Impact',
fontSize: 24,
fontStyle: FontStyle.italic,
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(
child: Padding(
const SizedBox(height: 8),
// 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),
child: FutureBuilder<DocumentSnapshot>(
future: _sellerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
String sellerName = 'Unknown Seller';
String sellerAvatar = '';
double averageRating = 0;
int reviewCount = 0;
if (snapshot.hasData && snapshot.data!.exists) {
final userData = snapshot.data!.data() as Map<String, dynamic>?;
if (userData != null) {
sellerName = "${userData['first_name'] ?? ''} ${userData['last_name'] ?? ''}".trim();
sellerAvatar = userData['avatar_url'] ?? '';
averageRating = userData['average_rating']?.toDouble() ?? 0.0;
reviewCount = userData['review_count']?.toInt() ?? 0;
}
}
return Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: Colors.grey[300],
backgroundImage: sellerAvatar.isNotEmpty ? NetworkImage(sellerAvatar) : null,
child: sellerAvatar.isEmpty ? const Icon(Icons.person, color: Colors.white) : null,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: imageUrl.isNotEmpty
? Image.network(imageUrl, height: 200, fit: BoxFit.cover)
: Icon(Icons.book, size: 100),
),
const SizedBox(height: 20),
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),
Text(
sellerName,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black,
),
),
child: const Text(
'Delete Book',
style: TextStyle(fontSize: 18, color: Colors.white),
Text(
isMyBook ? 'You' : 'Seller',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
Row(
children: [
_buildRatingStars(averageRating, size: 16),
Text(
' (${reviewCount.toString()})',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
],
),
),
const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
],
);
},
),
),
),
)
else if (!isMyBook && currentUser != null)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () =>
_contactSeller(context, book, bookId),
style: ElevatedButton.styleFrom(
backgroundColor: kPrimaryColor,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
const SizedBox(height: 8),
// Description section
Container(
color: isDarkMode ? Colors.black : Colors.white,
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Description',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
child: const Text(
'Contact Seller',
style: TextStyle(fontSize: 18, color: Colors.white),
const SizedBox(height: 12),
Text(
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(
child: TextButton(
onPressed: () =>
Navigator.pushNamed(context, '/login'),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
),
onPressed: _isSubmitting || _userRating == 0 || currentUser == null
? null
: () => _submitReview(context),
child: _isSubmitting
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(color: Colors.white),
)
: const Text(
'Submit Review',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
if (_hasReviewed)
Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
),
onPressed: currentUser == null ? null : () => _editReview(context),
child: const Text(
'Log in to contact the seller',
style: TextStyle(fontSize: 16),
'Edit Review',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
if (currentUser == null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Center(
child: Text(
'Sign in to leave a review',
style: TextStyle(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
),
),
],
),
),
const SizedBox(height: 80),
],
),
),
bottomNavigationBar: !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) {
Navigator.pushNamedAndRemoveUntil(context, '/', (_) => false);
} else if (index == 1) {
Navigator.pushNamed(context, '/post');
} else if (index == 2) {
Navigator.pushNamed(context, '/inbox');
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: FutureBuilder<DocumentSnapshot>(
future: _sellerFuture,
builder: (context, snapshot) {
String sellerName = 'Unknown Seller';
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,
);
}
Future<void> _contactSeller(BuildContext context, Map<String, dynamic> book, String bookId) async {
final currentUser = FirebaseAuth.instance.currentUser;
if (currentUser == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please log in to contact the seller')),
Widget _buildRatingStars(double rating, {double size = 24}) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(5, (index) {
if (index < rating.floor()) {
// Full star
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
final isBuyer = currentUser.uid != sellerId;
final rolePrefix = isBuyer ? 'buyer' : 'seller';
final users = [currentUser.uid, sellerId]..sort();
final chatRoomId = "${rolePrefix}_${bookId}_${users.join('_')}";
Future<void> _submitReview(BuildContext context) async {
final currentUser = FirebaseAuth.instance.currentUser;
if (currentUser == null) return;
setState(() {
_isSubmitting = true;
});
try {
final sellerDoc = await FirebaseFirestore.instance
// Get current user info
final userDoc = await FirebaseFirestore.instance
.collection('users')
.doc(sellerId)
.doc(currentUser.uid)
.get();
final sellerName = sellerDoc.exists
? "${sellerDoc['first_name']} ${sellerDoc['last_name']}"
: "Unknown Seller";
final userName = userDoc.exists
? "${userDoc.data()?['first_name'] ?? ''} ${userDoc.data()?['last_name'] ?? ''}".trim()
: "Anonymous User";
final chatRef = FirebaseFirestore.instance.collection('chats').doc(chatRoomId);
final userAvatar = userDoc.data()?['avatar_url'] ?? '';
final chatData = {
'users': users,
'bookId': bookId,
'bookTitle': book['title'],
'lastMessage': 'Hi! Is this book still available?',
'lastMessageTime': FieldValue.serverTimestamp(),
// Save the review to seller's reviews subcollection
await FirebaseFirestore.instance
.collection('users')
.doc(widget.book['userId'])
.collection('reviews')
.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(),
'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({
'senderId': currentUser.uid,
'message': 'Hi! Is this book still available?',
'timestamp': FieldValue.serverTimestamp(),
'read': false,
final newAverageRating = reviews.isEmpty ? 0 : totalRating / reviews.length;
// Update the seller's user document with new average rating
await FirebaseFirestore.instance
.collection('users')
.doc(widget.book['userId'])
.update({
'average_rating': newAverageRating,
'review_count': reviews.length,
});
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StrictChatPage(
chatId: chatRoomId,
otherUserName: sellerName,
currentUserId: currentUser.uid,
sellerId: sellerId,
),
),
if (mounted) {
setState(() {
_hasReviewed = true;
_isSubmitting = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Thank you for your review!')),
);
}
} catch (e) {
debugPrint('Error starting chat: $e');
if (mounted) {
setState(() {
_isSubmitting = false;
});
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>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirm Deletion'),
content: const Text('Are you sure you want to delete this book?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')),
TextButton(
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) {
await FirebaseFirestore.instance.collection('books').doc(bookId).delete();
if (context.mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Book removed successfully')),
setState(() {
_isDeleting = true; // Show loading state
});
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) {
if (price == null) return '0.00';
if (price is num) return price.toStringAsFixed(2);
if (price is String) {
try {
return double.parse(price).toStringAsFixed(2);
} catch (_) {
return price;
}
}
return '0.00';
final date = timestamp.toDate();
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays == 0) {
return 'Today';
} else if (difference.inDays == 1) {
return 'Yesterday';
} else if (difference.inDays < 7) {
return '${difference.inDays} days ago';
} else {
return '${date.day}/${date.month}/${date.year}';
}
}
\ No newline at end of file
......@@ -2,24 +2,27 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:paperchase_app/book_detail_page.dart';
import 'colors.dart';
class StrictChatPage extends StatefulWidget {
final String chatId;
//final String bookId;
final String otherUserName;
final String currentUserId;
final String sellerId;
final List<String> predefinedMessages;
const StrictChatPage({
Key? key,
super.key,
required this.chatId,
required this.otherUserName,
required this.currentUserId,
required this.sellerId,
}) : super(key: key);
this.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?",
],
});
@override
_StrictChatPageState createState() => _StrictChatPageState();
......@@ -29,32 +32,6 @@ class _StrictChatPageState extends State<StrictChatPage> {
final ScrollController _scrollController = ScrollController();
final TextEditingController _messageController = TextEditingController();
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
void initState() {
......@@ -79,7 +56,6 @@ List<String> get predefinedMessages {
if (doc.exists) {
setState(() {
_bookTitle = doc.data()?['bookTitle'] as String?;
_bookId = doc.data()?['bookId'] as String?;
});
}
} catch (e) {
......@@ -135,8 +111,7 @@ List<String> get predefinedMessages {
final backgroundColor2 = isDarkMode ? kLightBackground : kDarkBackground;
final textColor = isDarkMode ? kDarkText : kLightText;
final textColor2 = isDarkMode ? kLightText : kDarkText;
final messageBackgroundOther = isDarkMode ? Colors.grey[800] : Colors
.grey[200];
final messageBackgroundOther = isDarkMode ? Colors.grey[800] : Colors.grey[200];
return Scaffold(
appBar: AppBar(
......@@ -201,8 +176,7 @@ List<String> get predefinedMessages {
final text = data['message'] as String? ?? '';
final senderId = data['senderId'] as String? ?? '';
final currentUser = FirebaseAuth.instance.currentUser;
final isMe = currentUser != null &&
senderId == currentUser.uid;
final isMe = currentUser != null && senderId == currentUser.uid;
return Container(
margin: const EdgeInsets.symmetric(vertical: 4),
......@@ -213,10 +187,7 @@ List<String> get predefinedMessages {
children: [
Container(
constraints: BoxConstraints(
maxWidth: MediaQuery
.of(context)
.size
.width * 0.75,
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
......@@ -266,57 +237,27 @@ List<String> get predefinedMessages {
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),
Wrap(
spacing: 12,
runSpacing: 12,
children: predefinedMessages.map((msg) {
children: widget.predefinedMessages.map((msg) {
return ActionChip(
label: Text(msg),
onPressed: () => _sendMessage(msg),
backgroundColor: backgroundColor,
labelStyle: TextStyle(color: textColor,),
labelStyle: TextStyle(
color: textColor,
),
);
}).toList(),
),
const SizedBox(height: 12),
],
),
),
),
],
),
);
}
void _confirmAndCompletePurchase(BuildContext context) async {
await FirebaseFirestore.instance.collection('books').doc(_bookId).delete();
//await FirebaseFirestore.instance.collection('chats').doc(widget.chatId).collection('messages').doc().delete();
await FirebaseFirestore.instance.collection('chats').doc(widget.chatId).delete();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Transaction completed!')),
);
}
}
\ No newline at end of file
......@@ -3,6 +3,8 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'colors.dart';
class ForgotPasswordPage extends StatefulWidget {
const ForgotPasswordPage({super.key});
@override
_ForgotPasswordPageState createState() => _ForgotPasswordPageState();
}
......@@ -67,7 +69,6 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
SizedBox(height: 20),
ElevatedButton(
onPressed: _resetPassword,
child: Text("Reset Password"),
style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, // Background color
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, // Text color
......@@ -76,6 +77,7 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
borderRadius: BorderRadius.circular(10), // Rounded corners
),
),
child: Text("Reset Password"),
),
],
),
......
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:paperchase_app/NavBar.dart';
import 'package:paperchase_app/chat_page.dart';
import 'package:paperchase_app/colors.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'colors.dart';
import 'NavBar.dart';
import 'chat_page.dart';
enum BookFilter {
all,
sold,
bought,
}
class InboxPage extends StatefulWidget {
const InboxPage({super.key});
......@@ -14,10 +20,33 @@ class InboxPage extends StatefulWidget {
}
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) {
return FirebaseFirestore.instance
.collection('chats')
.where('users', arrayContains: userId);
final baseQuery = FirebaseFirestore.instance.collection('chats');
switch (_currentFilter) {
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
......@@ -32,7 +61,9 @@ class _InboxPageState extends State<InboxPage> {
if (currentUser == null) {
return Scaffold(
appBar: AppBar(
iconTheme: IconThemeData(color: textColor),
iconTheme: IconThemeData(
color: isDarkMode ? kDarkBackground : kLightBackground,
),
title: const Text(
"Inbox",
style: TextStyle(
......@@ -43,7 +74,7 @@ class _InboxPageState extends State<InboxPage> {
color: kPrimaryColor,
),
),
backgroundColor: scaffoldColor,
foregroundColor: isDarkMode ? kLightBackground : kDarkBackground,
),
drawer: const NavBar(),
body: Container(
......@@ -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(
appBar: AppBar(
title: const Text(
title: Row(
children: [
const Text(
'Inbox',
style: TextStyle(
fontFamily: 'Impact',
......@@ -87,6 +139,39 @@ class _InboxPageState extends State<InboxPage> {
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,
iconTheme: IconThemeData(color: textColor2),
),
......@@ -98,7 +183,26 @@ class _InboxPageState extends State<InboxPage> {
.orderBy('lastMessageTime', descending: true)
.snapshots(),
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) {
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(
child: Text(
'Error loading conversations: ${snapshot.error}',
......@@ -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> {
children: [
Icon(Icons.chat_bubble_outline, size: 64, color: textColor.withOpacity(0.5)),
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 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> {
itemBuilder: (context, index) {
final chat = sortedChats[index];
final data = chat.data() as Map<String, dynamic>;
final chatId = chat.id;
// We'll determine real seller in FutureBuilder
final bookId = data['bookId'] as String? ?? '';
if (kDebugMode) {
print('Chat data: $data');
}
final lastMessage = data['lastMessage'] as String?;
final lastMessageTime = (data['lastMessageTime'] as Timestamp?)?.toDate();
final bookTitle = data['bookTitle'] as 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>(
future: FirebaseFirestore.instance.collection('users').doc(otherUserId).get(),
......@@ -172,95 +319,6 @@ class _InboxPageState extends State<InboxPage> {
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(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: isDarkMode ? Colors.grey[900] : Colors.white,
......@@ -270,10 +328,17 @@ class _InboxPageState extends State<InboxPage> {
context,
MaterialPageRoute(
builder: (context) => StrictChatPage(
chatId: chatId,
chatId: chat.id,
otherUserName: userName,
currentUserId: currentUserId,
sellerId: sellerId,
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?",
],
),
),
);
......@@ -281,7 +346,7 @@ class _InboxPageState extends State<InboxPage> {
leading: CircleAvatar(
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200],
child: Text(
userName.isNotEmpty ? userName[0].toUpperCase() : '?',
userName[0].toUpperCase(),
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
......@@ -327,25 +392,8 @@ class _InboxPageState extends State<InboxPage> {
: 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> {
_isLoading
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: _login, child: const Text('Login'),
onPressed: _login,
style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, // Background color
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, // Text color
......@@ -105,7 +105,7 @@ class _LoginPageState extends State<LoginPage> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // Rounded corners
),
),
), child: const Text('Login'),
),
TextButton(
style: TextButton.styleFrom(
......
......@@ -32,7 +32,6 @@ void main() async {
if (isFirstLaunch) {
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));
runApp(MyApp(isFirstLaunch: isFirstLaunch));
}
......@@ -46,11 +45,11 @@ class MyApp extends StatefulWidget {
}
class _MyAppState extends State<MyApp> {
bool _isDarkMode = false; // Default to Light Mode
bool _isDarkMode = false;
void _toggleTheme() {
setState(() {
_isDarkMode = !_isDarkMode; // Toggle between Light & Dark Mode
_isDarkMode = !_isDarkMode;
});
}
......@@ -58,7 +57,6 @@ class _MyAppState extends State<MyApp> {
Widget build(BuildContext context) {
return MaterialApp(
title: 'PaperChase',
theme: ThemeData(
primaryColor: kPrimaryColor,
brightness: Brightness.light,
......@@ -68,7 +66,7 @@ class _MyAppState extends State<MyApp> {
),
appBarTheme: const AppBarTheme(
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(
backgroundColor: kDarkBackground,
......@@ -79,7 +77,10 @@ class _MyAppState extends State<MyApp> {
darkTheme: ThemeData(
brightness: Brightness.dark,
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(
backgroundColor: kLightBackground,
selectedItemColor: kPrimaryColor,
......@@ -87,11 +88,9 @@ class _MyAppState extends State<MyApp> {
),
),
themeMode: _isDarkMode ? ThemeMode.dark : ThemeMode.light,
home: HomePage(toggleTheme: _toggleTheme, isDarkMode: _isDarkMode),
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(),
'/signup': (context) => const SignupPage(),
'/profile': (context) => const ProfilePage(),
......@@ -118,9 +117,20 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> {
final TextEditingController _searchController = TextEditingController();
List<dynamic> _books = [];
List<DocumentSnapshot> _books = [];
List<DocumentSnapshot> _filteredBooks = [];
bool _isLoggedIn = false;
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
void initState() {
......@@ -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) {
if (_user != null) {
Navigator.pushNamed(context, route);
......@@ -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 {
final query = _searchController.text.trim().toLowerCase();
if (query.isEmpty) {
_loadRecentBooks(); // If search is empty, reload recent books
setState(() {
_filteredBooks = _books;
});
return;
}
try {
final QuerySnapshot snapshot = await FirebaseFirestore.instance
.collection('books')
.orderBy('timestamp', descending: true)
.get();
final filteredBooks = snapshot.docs.where((doc) {
final filteredBooks = _books.where((doc) {
final data = doc.data() as Map<String, dynamic>;
final title = (data['title'] ?? '').toString().toLowerCase();
final author = (data['author'] ?? '').toString().toLowerCase();
final isbn = (data['isbn'] ?? '').toString();
return title.contains(query) || author.contains(query) || isbn.contains(query);
}).toList();
setState(() {
_books = filteredBooks;
_filteredBooks = filteredBooks;
});
} catch (e) {
print("Error searching books: $e");
}
}
}
Future<void> _loadRecentBooks() async {
try {
final QuerySnapshot snapshot = await FirebaseFirestore.instance
.collection('books')
.orderBy('timestamp', descending: true) // Sort by the most recent posts
.limit(10) // Optionally limit to the latest 10 books
.orderBy('timestamp', descending: true)
.limit(20) // Increased limit to show more books
.get();
setState(() {
_books = snapshot.docs;
_filteredBooks = snapshot.docs;
});
} catch (e) {
print("Error fetching recent books: $e");
}
}
}
void _filterBooks() {
// Apply filters to the current book collection
void _applyFilters() {
setState(() {
if (_filterBy == 'Latest Posted') {
_books.sort((a, b) => (b['timestamp'] as Timestamp).compareTo(a['timestamp'] as Timestamp));
} else if (_filterBy == 'Price: Low to High') {
_books.sort((a, b) => (a['price'] ?? 0).compareTo(b['price'] ?? 0));
} else if (_filterBy == 'Price: High to Low') {
_books.sort((a, b) => (b['price'] ?? 0).compareTo(a['price'] ?? 0));
} else if (_filterBy == 'Condition: Best to Worst') {
_books.sort((a, b) => _conditionRanking(a['condition']).compareTo(_conditionRanking(b['condition'])));
} else if (_filterBy == 'Condition: Worst to Best') {
_books.sort((a, b) => _conditionRanking(b['condition']).compareTo(_conditionRanking(a['condition'])));
_filteredBooks = _books.where((doc) {
final data = doc.data() as Map<String, dynamic>;
// Condition filter only
final condition = (data['condition'] ?? '').toString();
final isConditionSelected = _selectedConditions.isEmpty ||
_selectedConditions.contains(condition);
return isConditionSelected;
}).toList();
_filtersActive = true;
});
}
// Reset filters and show all books
void _resetFilters() {
setState(() {
_selectedConditions = [];
_filteredBooks = _books;
_filtersActive = false;
});
}
}
int _conditionRanking(String? condition) {
const conditionOrder = {
'Like New': 1,
'Good': 2,
'Fair': 3,
'Poor': 4
};
return conditionOrder[condition] ?? 0;
}
// Improved filter button
Widget _buildFilterButton() {
final bool isActive = _filtersActive;
return InkWell(
onTap: _showFilterDialog,
child: AnimatedContainer(
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
Widget build(BuildContext context) {
bool darkMode = isDarkMode(context); // Call the utility function
final query = _searchController.text.trim().toLowerCase();
return Scaffold(
drawer: _isLoggedIn ? NavBar() : null,
drawer: const NavBar(),
appBar: AppBar(
iconTheme: IconThemeData(
color: widget.isDarkMode ? kDarkBackground : kLightBackground,
),
title: Image.asset('assets/title-text.png'),
title: Image.asset('assets/title-text.png', height: 70), // Increased height from 60 to 70
centerTitle: true,
actions: [
IconButton(
icon: Icon(widget.isDarkMode ? Icons.wb_sunny : Icons.nightlight_round),
color: widget.isDarkMode ? kDarkBackground : kLightBackground,
onPressed: widget.toggleTheme,
),
if (!_isLoggedIn) ...[
TextButton(
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(
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
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TextField(
// Search and Filter Row
Row(
children: [
// Search Field
Expanded(
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Search for books by title, author, or ISBN",
hintText: "Search for books",
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
suffixIcon: IconButton(
icon: const Icon(Icons.search),
......@@ -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: [
for (var filterOption in [
'Latest Posted',
'Price: Low to High',
'Price: High to Low',
'Condition: Best to Worst',
'Condition: Worst to Best'
])
ListTile(
title: Text(
filterOption,
style: TextStyle(color: widget.isDarkMode ? kDarkText : kLightText),
),
onTap: () {
// Condition Chips
..._selectedConditions.map((condition) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: Chip(
label: Text(condition),
deleteIcon: const Icon(Icons.close, size: 18),
onDeleted: () {
setState(() {
_filterBy = filterOption;
_filterBooks();
_selectedConditions.remove(condition);
_applyFilters();
});
Navigator.pop(context);
},
),
],
);
}).toList(),
// Clear All Filters
if (_filtersActive)
TextButton.icon(
icon: const Icon(Icons.clear_all, size: 18),
label: const Text('Clear All'),
onPressed: _resetFilters,
),
],
),
),
],
// 🔥 Compact Container with Border around Icon on the Left
child: Container(
padding: const EdgeInsets.all(8), // Compact padding
decoration: BoxDecoration(
color: widget.isDarkMode ? kDarkBackground : kLightBackground,
borderRadius: BorderRadius.circular(12), // ✅ Rounded border
border: Border.all(
color: widget.isDarkMode ? Colors.grey : Colors.black12, // Light border
width: 1,
),
),
child:
Icon(Icons.sort_rounded, color: widget.isDarkMode ? kDarkText : kLightText), // ✅ Only the icon inside
const SizedBox(height: 15),
// Results count
Align(
alignment: Alignment.centerLeft,
child: Text(
'${_filteredBooks.length} books found',
style: TextStyle(
color: widget.isDarkMode ? kLightText : kDarkText,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 10),
const SizedBox(height: 8),
// Books Grid
Expanded(
child: ListView.builder(
itemCount: _books.length,
child: _filteredBooks.isEmpty
? Center(
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) {
final book = _books[index];
final bookId = book.id;
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'];
final doc = _filteredBooks[index];
final data = doc.data() as Map<String, dynamic>;
return ListTile(
leading: Image.network(thumbnail, width: 50, height: 50, fit: BoxFit.cover),
title: Text(title),
subtitle: Text('$author - \$$price - ${book.data()['condition'] ?? 'Condition not available'}'),
final bookId = doc.id;
final title = data['title'] ?? 'Unknown Title';
final condition = data['condition'] ?? 'Unknown';
// Extract price and format it with currency
final price = data['price'] != null
? '\$${data['price'].toString()}'
: 'Price not listed';
// Handle image URLs more robustly
String? imageUrl;
// First try the imageUrls field (array of images)
if (data.containsKey('imageUrls') && data['imageUrls'] != null) {
final images = data['imageUrls'];
if (images is List && images.isNotEmpty) {
imageUrl = images[0].toString();
}
}
// If that doesn't work, try imageUrl field (single image)
if ((imageUrl == null || imageUrl.isEmpty) && data.containsKey('imageUrl')) {
imageUrl = data['imageUrl']?.toString();
}
// If that doesn't work, try coverImageUrl field
if ((imageUrl == null || imageUrl.isEmpty) && data.containsKey('coverImageUrl')) {
imageUrl = data['coverImageUrl']?.toString();
}
return GestureDetector(
onTap: () {
if (_isLoggedIn) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BookDetailsPage(book: book.data() as Map<String, dynamic>, bookId: bookId), // Pass book data
builder: (context) => BookDetailsPage(
book: data,
bookId: bookId,
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
const SnackBar(
content: Text("You need to log in to view book details."),
duration: Duration(seconds: 2),
),
);
}
},
child: Container(
decoration: BoxDecoration(
color: widget.isDarkMode ? kDarkCard : kLightCard,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
spreadRadius: 2,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image section - takes up most of the space
Expanded(
flex: 5,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: imageUrl != null && imageUrl.isNotEmpty
? FadeInImage.assetNetwork(
placeholder: 'assets/placeholder.png',
image: imageUrl,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
imageErrorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[300],
child: const Center(child: Icon(Icons.broken_image, size: 40)),
);
},
)
: Container(
color: Colors.grey[300],
child: const Center(child: Icon(Icons.book, size: 40)),
),
),
),
// Info section below the image
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// Price in bold
Text(
price,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: kPrimaryColor,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
// Title in bold
Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
color: widget.isDarkMode ? kDarkText : kLightText,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
// Condition not in bold
Text(
condition,
style: TextStyle(
fontSize: 12,
color: widget.isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
],
),
),
);
},
),
......@@ -398,23 +683,14 @@ String _filterBy = 'Latest Posted'; // Default filter option
),
),
bottomNavigationBar: BottomNavigationBar(
backgroundColor: widget.isDarkMode ? kLightBackground : kDarkBackground,
selectedItemColor: kPrimaryColor,
unselectedItemColor: widget.isDarkMode ? kDarkBackground : kLightBackground,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
BottomNavigationBarItem(icon: Icon(Icons.add), label: "Post"),
BottomNavigationBarItem(icon: Icon(Icons.mail), label: "Inbox"),
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.post_add), label: 'Post'),
BottomNavigationBarItem(icon: Icon(Icons.inbox), label: 'Inbox'),
],
onTap: (index) {
if (index == 0) {
Navigator.pushNamed(context, '/');
} else if (index == 1) {
_navigateIfAuthenticated(context, '/post');
} else if (index == 2) {
_navigateIfAuthenticated(context, '/inbox');
}
},
currentIndex: _selectedIndex,
selectedItemColor: kPrimaryColor,
onTap: _onItemTapped,
),
);
}
......
......@@ -5,11 +5,12 @@ import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.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 'NavBar.dart';
class PostBookPage extends StatefulWidget {
const PostBookPage({super.key});
@override
_PostBookPageState createState() => _PostBookPageState();
}
......@@ -22,10 +23,36 @@ class _PostBookPageState extends State<PostBookPage> {
final TextEditingController descriptionController = TextEditingController();
File? _imageFile;
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
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) {
setState(() {
_imageFile = File(pickedFile.path);
......@@ -34,9 +61,15 @@ class _PostBookPageState extends State<PostBookPage> {
} else {
print('No image selected.');
}
}
} catch (e) {
print('Error picking image: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error accessing ${source == ImageSource.camera ? 'camera' : 'gallery'}: $e'))
);
}
}
Future<String?> fetchBookDescription(String isbn) async {
Future<String?> fetchBookDescription(String isbn) async {
final String url = "https://www.googleapis.com/books/v1/volumes?q=isbn:$isbn";
try {
......@@ -51,9 +84,9 @@ Future<String?> fetchBookDescription(String isbn) async {
print("Error fetching book details: $e");
}
return null; // Return null if no description is found
}
}
Future<String?> uploadImageToImgur(File imageFile) async {
Future<String?> uploadImageToImgur(File imageFile) async {
try {
var request = http.MultipartRequest(
'POST', Uri.parse('https://api.imgur.com/3/upload')
......@@ -77,7 +110,7 @@ Future<String?> uploadImageToImgur(File imageFile) async {
print('Error uploading image: $e');
return null;
}
}
}
// Function to upload book data to Firebase
Future<bool> uploadBook() async {
......@@ -87,8 +120,28 @@ Future<String?> uploadImageToImgur(File imageFile) async {
String? imageUrl;
if (_imageFile != null) {
// Show loading indicator
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
},
);
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({
......@@ -98,8 +151,8 @@ Future<String?> uploadImageToImgur(File imageFile) async {
'price': priceController.text,
'description': descriptionController.text,
'condition': _selectedCondition,
'userId': user.uid, // 🔹 Save logged-in user's ID
'imageUrl': imageUrl ?? "", // Optional image
'userId': user.uid,
'imageUrl': imageUrl ?? "",
'timestamp': FieldValue.serverTimestamp(),
});
return true;
......@@ -115,30 +168,45 @@ Future<String?> uploadImageToImgur(File imageFile) async {
priceController.text.isEmpty || isbnController.text.isEmpty ||
authorController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('All fields are required.'))
const SnackBar(content: Text('All fields are required.'))
);
return;
}
// Show loading indicator while fetching description
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
},
);
String? description = await fetchBookDescription(isbnController.text);
descriptionController.text = description ?? 'No description available';
// Hide loading indicator
Navigator.of(context).pop();
bool success = await uploadBook();
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Book posted successfully!'))
);
Navigator.pushReplacementNamed(context, '/profile');
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to post book. Try again.'))
const SnackBar(content: Text('Failed to post book. Try again.'))
);
}
}
}
@override
Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
drawer: NavBar(),
appBar: AppBar(
iconTheme: IconThemeData(
color: isDarkMode ? kDarkBackground : kLightBackground,
......@@ -146,18 +214,17 @@ Future<String?> uploadImageToImgur(File imageFile) async {
title: const Text(
"Post a Book",
style: TextStyle(
fontFamily: 'Impact', // Ensure "Impact" is available in your fonts
fontSize: 24, // Adjust size as needed
fontFamily: 'Impact',
fontSize: 24,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold,
color: kPrimaryColor,
),
),
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground,
),
body: Padding(
padding: EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
......@@ -165,34 +232,32 @@ Future<String?> uploadImageToImgur(File imageFile) async {
// Title Input
TextField(
controller: titleController,
decoration: InputDecoration(labelText: 'Title'),
decoration: const InputDecoration(labelText: 'Title'),
),
SizedBox(height: 10),
const SizedBox(height: 10),
// Price Input
TextField(
controller: priceController,
decoration: InputDecoration(labelText: 'Price'),
decoration: const InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.number,
),
SizedBox(height: 10),
const SizedBox(height: 10),
// ISBN Input
TextField(
controller: isbnController,
decoration: InputDecoration(labelText: 'ISBN Number'),
decoration: const InputDecoration(labelText: 'ISBN Number'),
keyboardType: TextInputType.number,
),
SizedBox(height: 10),
const SizedBox(height: 10),
// Author Input
TextField(
controller: authorController,
decoration: InputDecoration(labelText: 'Author'),
decoration: const InputDecoration(labelText: 'Author'),
),
SizedBox(height: 10),
const SizedBox(height: 10),
DropdownButtonFormField<String>(
value: _selectedCondition,
......@@ -207,17 +272,16 @@ Future<String?> uploadImageToImgur(File imageFile) async {
_selectedCondition = value!;
});
},
// Set the dropdown color
dropdownColor: isDarkMode ? kDarkBackground : kLightBackground,
decoration: InputDecoration(
labelText: 'Condition',
filled: true,
fillColor: isDarkMode ? kDarkBackground : kLightBackground, // Match scaffold color
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10),
fillColor: isDarkMode ? kDarkBackground : kLightBackground,
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
),
),
SizedBox(height: 50),
const SizedBox(height: 50),
// Image Picker Buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
......@@ -225,56 +289,76 @@ Future<String?> uploadImageToImgur(File imageFile) async {
ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.camera),
icon: Icon(Icons.camera, color: isDarkMode ? kLightText: kDarkText),
label: Text('Camera'),
label: const Text('Camera'),
style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, // Background color
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, // Text color
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), // Padding
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground,
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // Rounded corners
borderRadius: BorderRadius.circular(10),
),
),
),
SizedBox(width: 20),
const SizedBox(width: 20),
ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.gallery),
icon: Icon(Icons.photo_library,color: isDarkMode ? kLightText: kDarkText),
label: Text('Gallery'),
icon: Icon(Icons.photo_library, color: isDarkMode ? kLightText: kDarkText),
label: const Text('Gallery'),
style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, // Background color
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, // Text color
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), // Padding
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground,
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // Rounded corners
borderRadius: BorderRadius.circular(10),
),
),
),
],
),
SizedBox(height: 10),
const SizedBox(height: 20),
// Display Selected Image
if (_imageFile != null)
if (_imageFile != null) ...[
Container(
height: 150,
height: 200,
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),
),
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
Center(
child: ElevatedButton(
onPressed: _postBook,
child: Text('Post Book'),
style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground, // Background color
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground, // Text color
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), // Padding
backgroundColor: isDarkMode ? kLightBackground : kDarkBackground,
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
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 {
} else if (index == 1) {
Navigator.pushNamed(context, '/post');
} else if (index == 2) {
Navigator.pushNamed(context, '/inbox'); // Stay on the same page
Navigator.pushNamed(context, '/inbox');
}
},
),
......
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.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:permission_handler/permission_handler.dart';
import 'dart:io';
import 'colors.dart';
import 'NavBar.dart';
......@@ -67,7 +64,6 @@ class ProfilePage extends StatelessWidget {
),
),
foregroundColor: isDarkMode ? kDarkBackground : kLightBackground,
),
drawer: const NavBar(),
body: Container(
......@@ -139,6 +135,8 @@ class ProfilePage extends StatelessWidget {
),
),
const SizedBox(height: 32),
// Books section
StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('books')
......@@ -158,8 +156,6 @@ class ProfilePage extends StatelessWidget {
final books = booksSnapshot.data!.docs;
if (books.isEmpty) {
return Text(
'No books posted yet',
......@@ -208,17 +204,256 @@ class ProfilePage extends StatelessWidget {
fontWeight: FontWeight.bold,
),
),
onTap: () {
Navigator.push(
context,
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 {
final VoidCallback toggleTheme;
const SettingsPage({
Key? key,
super.key,
required this.isDarkMode,
required this.toggleTheme,
}) : super(key: key);
});
@override
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