Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
P
PaperChase
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
h703249754
PaperChase
Commits
5ef5d4ce
Unverified
Commit
5ef5d4ce
authored
Apr 29, 2025
by
sxfzn
Committed by
GitHub
Apr 29, 2025
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add files via upload
parent
7e291a8b
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
2294 additions
and
874 deletions
+2294
-874
book_detail_page.dart
lib/book_detail_page.dart
+675
-210
chat_page.dart
lib/chat_page.dart
+52
-111
forgot_password.dart
lib/forgot_password.dart
+3
-1
inbox.dart
lib/inbox.dart
+249
-201
login.dart
lib/login.dart
+2
-2
main.dart
lib/main.dart
+500
-224
post.dart
lib/post.dart
+195
-111
profile.dart
lib/profile.dart
+247
-12
seller_profile_page.dart
lib/seller_profile_page.dart
+369
-0
settings.dart
lib/settings.dart
+2
-2
No files found.
lib/book_detail_page.dart
View file @
5ef5d4ce
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
State
<
BookDetailsPage
>
createState
()
=>
_BookDetailsPageState
();
}
class
_BookDetailsPageState
extends
State
<
BookDetailsPage
>
{
late
Future
<
DocumentSnapshot
>
_sellerFuture
;
bool
_hasReviewed
=
false
;
int
_userRating
=
0
;
final
_reviewController
=
TextEditingController
();
bool
_isSubmitting
=
false
;
bool
_isContactingSellerLoading
=
false
;
bool
_isDeleting
=
false
;
// Track deletion status
@override
void
initState
()
{
super
.
initState
();
_sellerFuture
=
FirebaseFirestore
.
instance
.
collection
(
'users'
)
.
doc
(
widget
.
book
[
'userId'
])
.
get
();
_checkExistingReview
();
}
Future
<
void
>
_checkExistingReview
()
async
{
final
currentUser
=
FirebaseAuth
.
instance
.
currentUser
;
if
(
currentUser
==
null
)
return
;
try
{
final
reviewDoc
=
await
FirebaseFirestore
.
instance
.
collection
(
'users'
)
.
doc
(
widget
.
book
[
'userId'
])
.
collection
(
'reviews'
)
.
doc
(
currentUser
.
uid
)
.
get
();
if
(
reviewDoc
.
exists
)
{
if
(
mounted
)
{
setState
(()
{
_hasReviewed
=
true
;
_userRating
=
reviewDoc
.
data
()?[
'rating'
]
??
0
;
_reviewController
.
text
=
reviewDoc
.
data
()?[
'comment'
]
??
''
;
});
}
}
}
catch
(
e
)
{
print
(
'Error checking existing review:
$e
'
);
}
}
Future
<
void
>
_contactSeller
(
BuildContext
context
,
String
sellerName
)
async
{
final
currentUser
=
FirebaseAuth
.
instance
.
currentUser
;
if
(
currentUser
==
null
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
'Please log in to contact the seller'
)),
);
return
;
}
setState
(()
{
_isContactingSellerLoading
=
true
;
});
try
{
// Get current user info
final
userDoc
=
await
FirebaseFirestore
.
instance
.
collection
(
'users'
)
.
doc
(
currentUser
.
uid
)
.
get
();
final
buyerName
=
userDoc
.
exists
?
"
${userDoc.data()?['first_name'] ?? ''}
${userDoc.data()?['last_name'] ?? ''}
"
.
trim
()
:
"Anonymous User"
;
// Check if a chat already exists between these users for this book
final
existingChatQuery
=
await
FirebaseFirestore
.
instance
.
collection
(
'chats'
)
.
where
(
'bookId'
,
isEqualTo:
widget
.
bookId
)
.
where
(
'users'
,
arrayContains:
currentUser
.
uid
)
.
get
();
String
chatId
;
if
(
existingChatQuery
.
docs
.
isNotEmpty
)
{
// Chat already exists, use existing chat
chatId
=
existingChatQuery
.
docs
.
first
.
id
;
}
else
{
// Create a new chat document
final
chatRef
=
FirebaseFirestore
.
instance
.
collection
(
'chats'
).
doc
();
chatId
=
chatRef
.
id
;
await
chatRef
.
set
({
'users'
:
[
currentUser
.
uid
,
widget
.
book
[
'userId'
]],
'buyerId'
:
currentUser
.
uid
,
'sellerId'
:
widget
.
book
[
'userId'
],
'bookId'
:
widget
.
bookId
,
'bookTitle'
:
widget
.
book
[
'title'
]
??
'Unknown Book'
,
'createdAt'
:
FieldValue
.
serverTimestamp
(),
'lastMessage'
:
'No messages yet'
,
'lastMessageTime'
:
FieldValue
.
serverTimestamp
(),
'lastMessageSenderId'
:
''
,
});
}
if
(
mounted
)
{
setState
(()
{
_isContactingSellerLoading
=
false
;
});
// Navigate to the chat page
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
StrictChatPage
(
chatId:
chatId
,
otherUserName:
sellerName
,
predefinedMessages:
const
[
"Is this still available?"
,
"When can we meet?"
,
"I'll take it"
,
"Thanks!"
,
"Hello"
,
"Can you hold it for me?"
,
"What's your lowest price?"
,
],
),
),
);
}
}
catch
(
e
)
{
if
(
mounted
)
{
setState
(()
{
_isContactingSellerLoading
=
false
;
});
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
'Error contacting seller:
$e
'
)),
);
}
}
}
@override
void
dispose
()
{
_reviewController
.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
final
bool
isDarkMode
=
Theme
.
of
(
context
).
brightness
==
Brightness
.
dark
;
final
currentUser
=
FirebaseAuth
.
instance
.
currentUser
;
final
isMyBook
=
currentUser
?.
uid
==
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
isMyBook
=
currentUser
?.
uid
==
widget
.
book
[
'userId'
];
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
,
),
title:
Text
(
title
,
style:
TextStyle
(
fontFamily:
'Impact'
,
fontSize:
24
,
fontStyle:
FontStyle
.
italic
,
fontWeight:
FontWeight
.
bold
,
color:
kPrimaryColor
,
),
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
),
),
actions:
[
if
(
isMyBook
)
IconButton
(
icon:
Icon
(
Icons
.
more_vert
,
color:
isDarkMode
?
Colors
.
white
:
Colors
.
black
),
onPressed:
()
=>
_showOptionsMenu
(
context
),
),
],
),
body:
SingleChildScrollView
(
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Center
(
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
,
height:
200
,
fit:
BoxFit
.
cover
)
:
Icon
(
Icons
.
book
,
size:
100
),
?
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
])),
),
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
),
),
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
(
fontSize:
24
,
fontWeight:
FontWeight
.
bold
,
color:
isDarkMode
?
Colors
.
white
:
Colors
.
black
,
),
),
),
child:
const
Text
(
'Delete Book'
,
style:
TextStyle
(
fontSize:
18
,
color:
Colors
.
white
),
],
),
const
SizedBox
(
height:
8
),
Text
(
title
,
style:
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
w500
,
color:
isDarkMode
?
Colors
.
white
:
Colors
.
black
,
),
),
)
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:
4
),
],
),
),
const
SizedBox
(
height:
8
),
// Seller Section with Rating
GestureDetector
(
onTap:
()
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
SellerProfilePage
(
sellerId:
widget
.
book
[
'userId'
],
),
child:
const
Text
(
'Contact Seller'
,
style:
TextStyle
(
fontSize:
18
,
color:
Colors
.
white
),
),
);
},
child:
Container
(
color:
isDarkMode
?
Colors
.
black
:
Colors
.
white
,
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
FutureBuilder
<
DocumentSnapshot
>(
future:
_sellerFuture
,
builder:
(
context
,
snapshot
)
{
if
(
snapshot
.
connectionState
==
ConnectionState
.
waiting
)
{
return
const
Center
(
child:
CircularProgressIndicator
());
}
String
sellerName
=
'Unknown Seller'
;
String
sellerAvatar
=
''
;
double
averageRating
=
0
;
int
reviewCount
=
0
;
if
(
snapshot
.
hasData
&&
snapshot
.
data
!.
exists
)
{
final
userData
=
snapshot
.
data
!.
data
()
as
Map
<
String
,
dynamic
>?;
if
(
userData
!=
null
)
{
sellerName
=
"
${userData['first_name'] ?? ''}
${userData['last_name'] ?? ''}
"
.
trim
();
sellerAvatar
=
userData
[
'avatar_url'
]
??
''
;
averageRating
=
userData
[
'average_rating'
]?.
toDouble
()
??
0.0
;
reviewCount
=
userData
[
'review_count'
]?.
toInt
()
??
0
;
}
}
return
Row
(
children:
[
CircleAvatar
(
radius:
20
,
backgroundColor:
Colors
.
grey
[
300
],
backgroundImage:
sellerAvatar
.
isNotEmpty
?
NetworkImage
(
sellerAvatar
)
:
null
,
child:
sellerAvatar
.
isEmpty
?
const
Icon
(
Icons
.
person
,
color:
Colors
.
white
)
:
null
,
),
const
SizedBox
(
width:
12
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
sellerName
,
style:
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
w500
,
color:
isDarkMode
?
Colors
.
white
:
Colors
.
black
,
),
),
Text
(
isMyBook
?
'You'
:
'Seller'
,
style:
TextStyle
(
fontSize:
14
,
color:
Colors
.
grey
[
600
],
),
),
Row
(
children:
[
_buildRatingStars
(
averageRating
,
size:
16
),
Text
(
' (
${reviewCount.toString()}
)'
,
style:
TextStyle
(
fontSize:
14
,
color:
Colors
.
grey
[
600
],
),
),
],
),
],
),
),
const
Icon
(
Icons
.
arrow_forward_ios
,
size:
16
,
color:
Colors
.
grey
),
],
);
},
),
),
),
const
SizedBox
(
height:
8
),
// Description section
Container
(
color:
isDarkMode
?
Colors
.
black
:
Colors
.
white
,
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'Description'
,
style:
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
bold
,
color:
isDarkMode
?
Colors
.
white
:
Colors
.
black
,
),
),
)
else
if
(
currentUser
==
null
)
Center
(
child:
TextButton
(
onPressed:
()
=>
Navigator
.
pushNamed
(
context
,
'/login'
),
child:
const
Text
(
'Log in to contact the seller'
,
style:
TextStyle
(
fontSize:
16
),
const
SizedBox
(
height:
12
),
Text
(
description
,
style:
TextStyle
(
fontSize:
16
,
color:
isDarkMode
?
Colors
.
white70
:
Colors
.
black87
,
),
),
],
),
),
const
SizedBox
(
height:
8
),
// Review section - only visible to buyers (not the seller's own book)
if
(!
isMyBook
)
Container
(
color:
isDarkMode
?
Colors
.
black
:
Colors
.
white
,
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
_hasReviewed
?
'Your Review'
:
'Rate this Seller'
,
style:
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
bold
,
color:
isDarkMode
?
Colors
.
white
:
Colors
.
black
,
),
),
const
SizedBox
(
height:
12
),
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
List
.
generate
(
5
,
(
index
)
{
return
IconButton
(
icon:
Icon
(
index
<
_userRating
?
Icons
.
star
:
Icons
.
star_border
,
color:
index
<
_userRating
?
Colors
.
amber
:
Colors
.
grey
,
size:
36
,
),
onPressed:
_hasReviewed
&&
!
currentUser
!.
isAnonymous
?
null
:
()
{
setState
(()
{
_userRating
=
index
+
1
;
});
},
);
}),
),
const
SizedBox
(
height:
16
),
TextField
(
controller:
_reviewController
,
maxLines:
3
,
readOnly:
_hasReviewed
&&
!
currentUser
!.
isAnonymous
,
decoration:
InputDecoration
(
hintText:
'Write your review (optional)'
,
border:
OutlineInputBorder
(),
filled:
true
,
fillColor:
isDarkMode
?
Colors
.
grey
[
900
]
:
Colors
.
grey
[
100
],
),
),
const
SizedBox
(
height:
16
),
if
(!
_hasReviewed
)
Center
(
child:
ElevatedButton
(
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
Colors
.
blue
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
32
,
vertical:
12
),
),
onPressed:
_isSubmitting
||
_userRating
==
0
||
currentUser
==
null
?
null
:
()
=>
_submitReview
(
context
),
child:
_isSubmitting
?
const
SizedBox
(
width:
20
,
height:
20
,
child:
CircularProgressIndicator
(
color:
Colors
.
white
),
)
:
const
Text
(
'Submit Review'
,
style:
TextStyle
(
fontSize:
16
,
color:
Colors
.
white
),
),
),
),
if
(
_hasReviewed
)
Center
(
child:
ElevatedButton
(
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
Colors
.
red
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
32
,
vertical:
12
),
),
onPressed:
currentUser
==
null
?
null
:
()
=>
_editReview
(
context
),
child:
const
Text
(
'Edit Review'
,
style:
TextStyle
(
fontSize:
16
,
color:
Colors
.
white
),
),
),
),
if
(
currentUser
==
null
)
Padding
(
padding:
const
EdgeInsets
.
only
(
top:
8.0
),
child:
Center
(
child:
Text
(
'Sign in to leave a review'
,
style:
TextStyle
(
color:
Colors
.
grey
[
600
],
fontStyle:
FontStyle
.
italic
,
),
),
),
),
],
),
],
),
),
const
SizedBox
(
height:
80
),
],
),
),
bottomNavigationBar:
BottomNavigationBar
(
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'
);
}
},
),
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
),
),
],
),
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
;
try
{
final
sellerDoc
=
await
FirebaseFirestore
.
instance
.
collection
(
'users'
)
.
doc
(
sellerId
)
.
get
();
setState
(()
{
_isSubmitting
=
true
;
});
final
sellerName
=
sellerDoc
.
exists
?
"
${sellerDoc['first_name']}
${sellerDoc['last_name']}
"
:
"Unknown Seller"
;
final
chatRef
=
FirebaseFirestore
.
instance
.
collection
(
'chats'
).
doc
(
chatRoomId
);
final
chatData
=
{
'users'
:
users
,
'bookId'
:
bookId
,
'bookTitle'
:
book
[
'title'
],
'lastMessage'
:
'Hi! Is this book still available?'
,
'lastMessageTime'
:
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
);
try
{
// Get current user info
final
userDoc
=
await
FirebaseFirestore
.
instance
.
collection
(
'users'
)
.
doc
(
currentUser
.
uid
)
.
get
();
final
userName
=
userDoc
.
exists
?
"
${userDoc.data()?['first_name'] ?? ''}
${userDoc.data()?['last_name'] ?? ''}
"
.
trim
()
:
"Anonymous User"
;
final
userAvatar
=
userDoc
.
data
()?[
'avatar_url'
]
??
''
;
// 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
(),
});
// Update seller's average rating
final
sellerReviewsRef
=
FirebaseFirestore
.
instance
.
collection
(
'users'
)
.
doc
(
widget
.
book
[
'userId'
])
.
collection
(
'reviews'
);
final
reviewsSnapshot
=
await
sellerReviewsRef
.
get
();
final
reviews
=
reviewsSnapshot
.
docs
;
double
totalRating
=
0
;
for
(
var
doc
in
reviews
)
{
totalRating
+=
doc
.
data
()[
'rating'
]
??
0
;
}
final
newAverageRating
=
reviews
.
isEmpty
?
0
:
totalRating
/
reviews
.
length
;
// Update the seller's user document with new average rating
await
FirebaseFirestore
.
instance
.
collection
(
'users'
)
.
doc
(
widget
.
book
[
'userId'
])
.
update
({
'average_rating'
:
newAverageRating
,
'review_count'
:
reviews
.
length
,
});
if
(
mounted
)
{
setState
(()
{
_hasReviewed
=
true
;
_isSubmitting
=
false
;
});
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
'Thank you for your review!'
)),
);
}
}
catch
(
e
)
{
if
(
mounted
)
{
setState
(()
{
_isSubmitting
=
false
;
});
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
'Error submitting review:
$e
'
)),
);
}
}
}
await
chatRef
.
collection
(
'messages'
).
add
({
'senderId'
:
currentUser
.
uid
,
'message'
:
'Hi! Is this book still available?'
,
'timestamp'
:
FieldValue
.
serverTimestamp
(),
'read'
:
false
,
Future
<
void
>
_editReview
(
BuildContext
context
)
async
{
setState
(()
{
_hasReviewed
=
false
;
});
}
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
StrictChatPage
(
chatId:
chatRoomId
,
otherUserName:
sellerName
,
currentUserId:
currentUser
.
uid
,
sellerId:
sellerId
,
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
);
},
),
],
),
),
);
}
catch
(
e
)
{
debugPrint
(
'Error starting chat:
$e
'
);
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
'Failed to contact seller. Please try again.'
)),
);
}
}
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
_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'
;
String
_formatDate
(
Timestamp
timestamp
)
{
if
(
timestamp
==
null
)
return
'Date not available'
;
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
lib/chat_page.dart
View file @
5ef5d4ce
...
...
@@ -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
()
{
...
...
@@ -75,11 +52,10 @@ List<String> get predefinedMessages {
.
collection
(
'chats'
)
.
doc
(
widget
.
chatId
)
.
get
();
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
,
...
...
@@ -245,78 +216,48 @@ List<String> get predefinedMessages {
),
),
Container
(
padding:
const
EdgeInsets
.
all
(
16
),
margin:
const
EdgeInsets
.
only
(
top:
2
),
width:
double
.
infinity
,
decoration:
BoxDecoration
(
color:
backgroundColor2
,
border:
Border
(
top:
BorderSide
(
color:
backgroundColor
.
withOpacity
(
0.2
),
),
),
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'Quick replies:'
,
style:
TextStyle
(
color:
textColor2
.
withOpacity
(
0.7
),
fontSize:
14
,
),
),
// Only show the Confirmed button if the current user is the seller
if
(
widget
.
currentUserId
==
widget
.
sellerId
)
SizedBox
(
width:
double
.
infinity
,
child:
ElevatedButton
(
onPressed:
()
=>
_confirmAndCompletePurchase
(
context
),
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
Colors
.
lightGreenAccent
,
padding:
const
EdgeInsets
.
symmetric
(
vertical:
16
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
12
),
padding:
const
EdgeInsets
.
all
(
16
),
margin:
const
EdgeInsets
.
only
(
top:
2
),
width:
double
.
infinity
,
decoration:
BoxDecoration
(
color:
backgroundColor2
,
border:
Border
(
top:
BorderSide
(
color:
backgroundColor
.
withOpacity
(
0.2
),
),
),
),
child:
const
Text
(
'Confirmed!'
,
style:
TextStyle
(
fontSize:
18
,
color:
kLightText
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'Quick replies:'
,
style:
TextStyle
(
color:
textColor2
.
withOpacity
(
0.7
),
fontSize:
14
,
),
),
const
SizedBox
(
height:
12
),
Wrap
(
spacing:
12
,
runSpacing:
12
,
children:
widget
.
predefinedMessages
.
map
((
msg
)
{
return
ActionChip
(
label:
Text
(
msg
),
onPressed:
()
=>
_sendMessage
(
msg
),
backgroundColor:
backgroundColor
,
labelStyle:
TextStyle
(
color:
textColor
,
),
);
}).
toList
(),
),
const
SizedBox
(
height:
12
),
],
),
),
),
if
(
widget
.
currentUserId
==
widget
.
sellerId
)
const
SizedBox
(
height:
12
),
Wrap
(
spacing:
12
,
runSpacing:
12
,
children:
predefinedMessages
.
map
((
msg
)
{
return
ActionChip
(
label:
Text
(
msg
),
onPressed:
()
=>
_sendMessage
(
msg
),
backgroundColor:
backgroundColor
,
labelStyle:
TextStyle
(
color:
textColor
,),
);
}).
toList
(),
),
const
SizedBox
(
height:
12
),
],
),
),
],
),
);
}
void
_confirmAndCompletePurchase
(
BuildContext
context
)
async
{
await
FirebaseFirestore
.
instance
.
collection
(
'books'
).
doc
(
_bookId
).
delete
();
//await FirebaseFirestore.instance.collection('chats').doc(widget.chatId).collection('messages').doc().delete();
await
FirebaseFirestore
.
instance
.
collection
(
'chats'
).
doc
(
widget
.
chatId
).
delete
();
Navigator
.
pop
(
context
);
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
'Transaction completed!'
)),
);
}
}
\ No newline at end of file
lib/forgot_password.dart
View file @
5ef5d4ce
...
...
@@ -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"
),
),
],
),
...
...
lib/inbox.dart
View file @
5ef5d4ce
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,21 +102,75 @@ class _InboxPageState extends State<InboxPage> {
),
),
),
bottomNavigationBar:
_buildBottomNavigationBar
(
isDarkMode
,
textColor2
),
bottomNavigationBar:
BottomNavigationBar
(
backgroundColor:
isDarkMode
?
kLightBackground
:
kDarkBackground
,
selectedItemColor:
kPrimaryColor
,
unselectedItemColor:
isDarkMode
?
kDarkBackground
:
kLightBackground
,
currentIndex:
1
,
items:
const
[
BottomNavigationBarItem
(
icon:
Icon
(
Icons
.
home
),
label:
"Home"
),
BottomNavigationBarItem
(
icon:
Icon
(
Icons
.
add
),
label:
"Post"
),
BottomNavigationBarItem
(
icon:
Icon
(
Icons
.
mail
),
label:
"Inbox"
),
],
onTap:
(
index
)
{
if
(
index
==
0
)
{
Navigator
.
pushNamedAndRemoveUntil
(
context
,
'/'
,
(
route
)
=>
false
);
}
else
if
(
index
==
1
)
{
Navigator
.
pushNamed
(
context
,
'/post'
);
}
else
if
(
index
==
2
)
{
Navigator
.
pushNamed
(
context
,
'/inbox'
);
}
},
),
);
}
return
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'Inbox'
,
style:
TextStyle
(
fontFamily:
'Impact'
,
fontSize:
24
,
fontStyle:
FontStyle
.
italic
,
fontWeight:
FontWeight
.
bold
,
color:
kPrimaryColor
,
),
title:
Row
(
children:
[
const
Text
(
'Inbox'
,
style:
TextStyle
(
fontFamily:
'Impact'
,
fontSize:
24
,
fontStyle:
FontStyle
.
italic
,
fontWeight:
FontWeight
.
bold
,
color:
kPrimaryColor
,
),
),
const
SizedBox
(
width:
16
),
Expanded
(
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
),
decoration:
BoxDecoration
(
color:
isDarkMode
?
Colors
.
grey
[
800
]
:
Colors
.
grey
[
200
],
borderRadius:
BorderRadius
.
circular
(
20
),
),
child:
DropdownButtonHideUnderline
(
child:
DropdownButton
<
BookFilter
>(
value:
_currentFilter
,
icon:
Icon
(
Icons
.
arrow_drop_down
,
color:
textColor
),
style:
TextStyle
(
color:
textColor
,
fontSize:
14
),
dropdownColor:
isDarkMode
?
Colors
.
grey
[
800
]
:
Colors
.
grey
[
200
],
items:
BookFilter
.
values
.
map
((
filter
)
{
return
DropdownMenuItem
<
BookFilter
>(
value:
filter
,
child:
Text
(
_getFilterName
(
filter
)),
);
}).
toList
(),
onChanged:
(
BookFilter
?
newValue
)
{
if
(
newValue
!=
null
)
{
setState
(()
{
_currentFilter
=
newValue
;
});
}
},
),
),
),
),
],
),
backgroundColor:
scaffoldColor
,
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}
'
,
...
...
@@ -106,22 +210,39 @@ class _InboxPageState extends State<InboxPage> {
),
);
}
if
(
snapshot
.
connectionState
==
ConnectionState
.
waiting
)
{
return
const
Center
(
child:
CircularProgressIndicator
());
}
return
_buildChatList
(
snapshot
,
currentUser
,
isDarkMode
,
textColor
);
},
),
),
bottomNavigationBar:
_buildBottomNavigationBar
(
isDarkMode
,
textColor2
),
bottomNavigationBar:
BottomNavigationBar
(
backgroundColor:
scaffoldColor
,
selectedItemColor:
kPrimaryColor
,
unselectedItemColor:
textColor2
,
currentIndex:
2
,
items:
const
[
BottomNavigationBarItem
(
icon:
Icon
(
Icons
.
home
),
label:
'Home'
),
BottomNavigationBarItem
(
icon:
Icon
(
Icons
.
add
),
label:
'Post'
),
BottomNavigationBarItem
(
icon:
Icon
(
Icons
.
mail
),
label:
'Inbox'
),
],
onTap:
(
index
)
{
if
(
index
==
0
)
{
Navigator
.
pushReplacementNamed
(
context
,
'/'
);
}
else
if
(
index
==
1
)
{
Navigator
.
pushReplacementNamed
(
context
,
'/post'
);
}
},
),
);
}
Widget
_buildChatList
(
AsyncSnapshot
<
QuerySnapshot
>
snapshot
,
User
currentUser
,
bool
isDarkMode
,
Color
textColor
)
{
final
chats
=
snapshot
.
data
?.
docs
??
[];
if
(
chats
.
isEmpty
)
{
return
Center
(
child:
Column
(
...
...
@@ -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,16 +286,29 @@ 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
(),
builder:
(
context
,
userSnapshot
)
{
...
...
@@ -171,77 +318,79 @@ class _InboxPageState extends State<InboxPage> {
userName
=
'
${userData['first_name'] ?? ''}
${userData['last_name'] ?? ''}
'
.
trim
();
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
Card
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
8
,
vertical:
4
),
color:
isDarkMode
?
Colors
.
grey
[
900
]
:
Colors
.
white
,
child:
ListTile
(
onTap:
()
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
StrictChatPage
(
chatId:
chat
.
id
,
otherUserName:
userName
,
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?"
,
],
),
),
);
}
return
_buildChatListItem
(
context
,
chatId
,
},
leading:
CircleAvatar
(
backgroundColor:
isDarkMode
?
Colors
.
grey
[
800
]
:
Colors
.
grey
[
200
],
child:
Text
(
userName
[
0
].
toUpperCase
(),
style:
TextStyle
(
color:
textColor
,
fontWeight:
FontWeight
.
bold
,
),
),
),
title:
Text
(
userName
,
currentUser
.
uid
,
sellerId
,
bookTitle
,
lastMessage
,
lastMessageTime
,
isDarkMode
,
textColor
,
);
},
style:
TextStyle
(
color:
textColor
,
fontWeight:
FontWeight
.
bold
,
),
),
subtitle:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
if
(
bookTitle
!=
null
)
Text
(
'Re:
$bookTitle
'
,
style:
TextStyle
(
color:
textColor
.
withOpacity
(
0.7
),
fontSize:
12
,
),
),
Text
(
lastMessage
??
'No messages yet'
,
style:
TextStyle
(
color:
textColor
.
withOpacity
(
0.7
),
),
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
),
],
),
trailing:
lastMessageTime
!=
null
?
Text
(
_formatTimestamp
(
lastMessageTime
),
style:
TextStyle
(
color:
textColor
.
withOpacity
(
0.5
),
fontSize:
12
,
),
)
:
null
,
),
);
},
);
...
...
@@ -249,107 +398,6 @@ class _InboxPageState extends State<InboxPage> {
);
}
Widget
_buildChatListItem
(
BuildContext
context
,
String
chatId
,
String
userName
,
String
currentUserId
,
String
sellerId
,
String
?
bookTitle
,
String
?
lastMessage
,
DateTime
?
lastMessageTime
,
bool
isDarkMode
,
Color
textColor
,
)
{
return
Card
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
8
,
vertical:
4
),
color:
isDarkMode
?
Colors
.
grey
[
900
]
:
Colors
.
white
,
child:
ListTile
(
onTap:
()
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
StrictChatPage
(
chatId:
chatId
,
otherUserName:
userName
,
currentUserId:
currentUserId
,
sellerId:
sellerId
,
),
),
);
},
leading:
CircleAvatar
(
backgroundColor:
isDarkMode
?
Colors
.
grey
[
800
]
:
Colors
.
grey
[
200
],
child:
Text
(
userName
.
isNotEmpty
?
userName
[
0
].
toUpperCase
()
:
'?'
,
style:
TextStyle
(
color:
textColor
,
fontWeight:
FontWeight
.
bold
,
),
),
),
title:
Text
(
userName
,
style:
TextStyle
(
color:
textColor
,
fontWeight:
FontWeight
.
bold
,
),
),
subtitle:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
if
(
bookTitle
!=
null
)
Text
(
'Re:
$bookTitle
'
,
style:
TextStyle
(
color:
textColor
.
withOpacity
(
0.7
),
fontSize:
12
,
),
),
Text
(
lastMessage
??
'No messages yet'
,
style:
TextStyle
(
color:
textColor
.
withOpacity
(
0.7
),
),
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
),
],
),
trailing:
lastMessageTime
!=
null
?
Text
(
_formatTimestamp
(
lastMessageTime
),
style:
TextStyle
(
color:
textColor
.
withOpacity
(
0.5
),
fontSize:
12
,
),
)
:
null
,
),
);
}
BottomNavigationBar
_buildBottomNavigationBar
(
bool
isDarkMode
,
Color
textColor2
)
{
return
BottomNavigationBar
(
backgroundColor:
isDarkMode
?
kLightBackground
:
kDarkBackground
,
selectedItemColor:
kPrimaryColor
,
unselectedItemColor:
textColor2
,
currentIndex:
2
,
items:
const
[
BottomNavigationBarItem
(
icon:
Icon
(
Icons
.
home
),
label:
'Home'
),
BottomNavigationBarItem
(
icon:
Icon
(
Icons
.
add
),
label:
'Post'
),
BottomNavigationBarItem
(
icon:
Icon
(
Icons
.
mail
),
label:
'Inbox'
),
],
onTap:
(
index
)
{
if
(
index
==
0
)
{
Navigator
.
pushReplacementNamed
(
context
,
'/'
);
}
else
if
(
index
==
1
)
{
Navigator
.
pushReplacementNamed
(
context
,
'/post'
);
}
},
);
}
String
_formatTimestamp
(
DateTime
timestamp
)
{
final
now
=
DateTime
.
now
();
final
difference
=
now
.
difference
(
timestamp
);
...
...
lib/login.dart
View file @
5ef5d4ce
...
...
@@ -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
(
...
...
lib/main.dart
View file @
5ef5d4ce
...
...
@@ -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:
2
0
,
fontWeight:
FontWeight
.
bold
),
titleTextStyle:
TextStyle
(
color:
Colors
.
white
,
fontSize:
2
4
,
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,19 +88,17 @@ class _MyAppState extends State<MyApp> {
),
),
themeMode:
_isDarkMode
?
ThemeMode
.
dark
:
ThemeMode
.
light
,
home:
HomePage
(
toggleTheme:
_toggleTheme
,
isDarkMode:
_isDarkMode
),
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
(),
'/post'
:
(
context
)
=>
PostBookPage
(),
'/inbox'
:
(
context
)
=>
InboxPage
(),
'/settings'
:
(
context
)
=>
SettingsPage
(
isDarkMode:
_isDarkMode
,
toggleTheme:
_toggleTheme
,
isDarkMode:
_isDarkMode
,
toggleTheme:
_toggleTheme
,
),
},
);
...
...
@@ -109,7 +108,7 @@ class _MyAppState extends State<MyApp> {
class
HomePage
extends
StatefulWidget
{
final
VoidCallback
toggleTheme
;
final
bool
isDarkMode
;
const
HomePage
({
super
.
key
,
required
this
.
toggleTheme
,
required
this
.
isDarkMode
});
@override
...
...
@@ -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
return
;
}
final
query
=
_searchController
.
text
.
trim
().
toLowerCase
();
if
(
query
.
isEmpty
)
{
setState
(()
{
_filteredBooks
=
_books
;
});
return
;
}
try
{
final
QuerySnapshot
snapshot
=
await
FirebaseFirestore
.
instance
.
collection
(
'books'
)
.
orderBy
(
'timestamp'
,
descending:
true
)
.
get
();
try
{
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
();
final
filteredBooks
=
snapshot
.
docs
.
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
();
setState
(()
{
_filteredBooks
=
filteredBooks
;
});
}
catch
(
e
)
{
print
(
"Error searching books:
$e
"
);
}
}
return
title
.
contains
(
query
)
||
author
.
contains
(
query
)
||
isbn
.
contains
(
query
);
}).
toList
();
Future
<
void
>
_loadRecentBooks
()
async
{
try
{
final
QuerySnapshot
snapshot
=
await
FirebaseFirestore
.
instance
.
collection
(
'books'
)
.
orderBy
(
'timestamp'
,
descending:
true
)
.
limit
(
20
)
// Increased limit to show more books
.
get
();
setState
(()
{
_books
=
snapshot
.
docs
;
_filteredBooks
=
snapshot
.
docs
;
});
}
catch
(
e
)
{
print
(
"Error fetching recent books:
$e
"
);
}
}
// Apply filters to the current book collection
void
_applyFilters
()
{
setState
(()
{
_books
=
filteredBooks
;
_filteredBooks
=
_books
.
where
((
doc
)
{
final
data
=
doc
.
data
()
as
Map
<
String
,
dynamic
>;
// Condition filter only
final
condition
=
(
data
[
'condition'
]
??
''
).
toString
();
final
isConditionSelected
=
_selectedConditions
.
isEmpty
||
_selectedConditions
.
contains
(
condition
);
return
isConditionSelected
;
}).
toList
();
_filtersActive
=
true
;
});
}
catch
(
e
)
{
print
(
"Error searching books:
$e
"
);
}
}
Future
<
void
>
_loadRecentBooks
()
async
{
try
{
final
QuerySnapshot
snapshot
=
await
FirebaseFirestore
.
instance
.
collection
(
'books'
)
.
orderBy
(
'timestamp'
,
descending:
true
)
// Sort by the most recent posts
.
limit
(
10
)
// Optionally limit to the latest 10 books
.
get
();
// Reset filters and show all books
void
_resetFilters
()
{
setState
(()
{
_books
=
snapshot
.
docs
;
_selectedConditions
=
[];
_filteredBooks
=
_books
;
_filtersActive
=
false
;
});
}
catch
(
e
)
{
print
(
"Error fetching recent books:
$e
"
);
}
}
void
_filterBooks
(
)
{
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'
])));
}
});
}
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,156 +423,275 @@ String _filterBy = 'Latest Posted'; // Default filter option
body:
Padding
(
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
TextField
(
controller:
_searchController
,
decoration:
InputDecoration
(
hintText:
"Search for books by title, author, or ISBN"
,
border:
OutlineInputBorder
(
borderRadius:
BorderRadius
.
circular
(
10
)),
suffixIcon:
IconButton
(
icon:
const
Icon
(
Icons
.
search
),
onPressed:
_searchBooks
,
),
),
),
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
,
// Search and Filter Row
Row
(
children:
[
// Search Field
Expanded
(
child:
TextField
(
controller:
_searchController
,
decoration:
InputDecoration
(
hintText:
"Search for books"
,
border:
OutlineInputBorder
(
borderRadius:
BorderRadius
.
circular
(
10
)),
suffixIcon:
IconButton
(
icon:
const
Icon
(
Icons
.
search
),
onPressed:
_searchBooks
,
),
]
,
)
,
),
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
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:
()
{
),
// Filter Button (updated)
const
SizedBox
(
width:
8
),
_buildFilterButton
(),
],
),
// Active Filters Chips
if
(
_filtersActive
)
...[
const
SizedBox
(
height:
12
),
SingleChildScrollView
(
scrollDirection:
Axis
.
horizontal
,
child:
Row
(
children:
[
// 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
;
_
filterBook
s
();
_
selectedConditions
.
remove
(
condition
)
;
_
applyFilter
s
();
});
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
,
),
],
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
,
),
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
(
child:
ListView
.
builder
(
itemCount:
_books
.
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'
];
return
ListTile
(
leading:
Image
.
network
(
thumbnail
,
width:
50
,
height:
50
,
fit:
BoxFit
.
cover
),
title:
Text
(
title
),
subtitle:
Text
(
'
$author
-
\$
$price
-
${book.data()['condition'] ?? 'Condition not available'}
'
),
onTap:
()
{
if
(
_isLoggedIn
)
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
BookDetailsPage
(
book:
book
.
data
()
as
Map
<
String
,
dynamic
>,
bookId:
bookId
),
// Pass book data
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
],
),
),
);
}
else
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
"You need to log in to view book details."
),
duration:
Duration
(
seconds:
2
),
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
doc
=
_filteredBooks
[
index
];
final
data
=
doc
.
data
()
as
Map
<
String
,
dynamic
>;
final
bookId
=
doc
.
id
;
final
title
=
data
[
'title'
]
??
'Unknown Title'
;
final
condition
=
data
[
'condition'
]
??
'Unknown'
;
// Extract price and format it with currency
final
price
=
data
[
'price'
]
!=
null
?
'
\$
${data['price'].toString()}
'
:
'Price not listed'
;
// Handle image URLs more robustly
String
?
imageUrl
;
// First try the imageUrls field (array of images)
if
(
data
.
containsKey
(
'imageUrls'
)
&&
data
[
'imageUrls'
]
!=
null
)
{
final
images
=
data
[
'imageUrls'
];
if
(
images
is
List
&&
images
.
isNotEmpty
)
{
imageUrl
=
images
[
0
].
toString
();
}
}
// If that doesn't work, try imageUrl field (single image)
if
((
imageUrl
==
null
||
imageUrl
.
isEmpty
)
&&
data
.
containsKey
(
'imageUrl'
))
{
imageUrl
=
data
[
'imageUrl'
]?.
toString
();
}
// If that doesn't work, try coverImageUrl field
if
((
imageUrl
==
null
||
imageUrl
.
isEmpty
)
&&
data
.
containsKey
(
'coverImageUrl'
))
{
imageUrl
=
data
[
'coverImageUrl'
]?.
toString
();
}
return
GestureDetector
(
onTap:
()
{
if
(
_isLoggedIn
)
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
BookDetailsPage
(
book:
data
,
bookId:
bookId
,
),
),
);
}
else
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
"You need to log in to view book details."
),
duration:
Duration
(
seconds:
2
),
),
);
}
},
child:
Container
(
decoration:
BoxDecoration
(
color:
widget
.
isDarkMode
?
kDarkCard
:
kLightCard
,
borderRadius:
BorderRadius
.
circular
(
12
),
boxShadow:
[
BoxShadow
(
color:
Colors
.
black
.
withOpacity
(
0.1
),
blurRadius:
8
,
spreadRadius:
2
,
),
],
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
// Image section - takes up most of the space
Expanded
(
flex:
5
,
child:
ClipRRect
(
borderRadius:
const
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
12
)),
child:
imageUrl
!=
null
&&
imageUrl
.
isNotEmpty
?
FadeInImage
.
assetNetwork
(
placeholder:
'assets/placeholder.png'
,
image:
imageUrl
,
fit:
BoxFit
.
cover
,
width:
double
.
infinity
,
height:
double
.
infinity
,
imageErrorBuilder:
(
context
,
error
,
stackTrace
)
{
return
Container
(
color:
Colors
.
grey
[
300
],
child:
const
Center
(
child:
Icon
(
Icons
.
broken_image
,
size:
40
)),
);
},
)
:
Container
(
color:
Colors
.
grey
[
300
],
child:
const
Center
(
child:
Icon
(
Icons
.
book
,
size:
40
)),
),
),
),
// Info section below the image
Expanded
(
flex:
3
,
child:
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
8.0
,
vertical:
4.0
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisAlignment:
MainAxisAlignment
.
start
,
children:
[
// Price in bold
Text
(
price
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
16
,
color:
kPrimaryColor
,
),
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
),
const
SizedBox
(
height:
2
),
// Title in bold
Text
(
title
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
13
,
color:
widget
.
isDarkMode
?
kDarkText
:
kLightText
,
),
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
),
const
SizedBox
(
height:
2
),
// Condition not in bold
Text
(
condition
,
style:
TextStyle
(
fontSize:
12
,
color:
widget
.
isDarkMode
?
Colors
.
grey
[
300
]
:
Colors
.
grey
[
700
],
),
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
),
],
),
),
),
],
),
),
);
}
},
);
},
),
},
),
),
],
),
),
bottomNavigationBar:
BottomNavigationBar
(
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
,
),
);
}
}
}
\ No newline at end of file
lib/post.dart
View file @
5ef5d4ce
...
...
@@ -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,62 +23,94 @@ 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
);
if
(
pickedFile
!=
null
)
{
setState
(()
{
_imageFile
=
File
(
pickedFile
.
path
);
});
print
(
'Image picked:
${_imageFile!.path}
'
);
}
else
{
print
(
'No image selected.'
);
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
);
});
print
(
'Image picked:
${_imageFile!.path}
'
);
}
else
{
print
(
'No image selected.'
);
}
}
catch
(
e
)
{
print
(
'Error picking image:
$e
'
);
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
'Error accessing
${source == ImageSource.camera ? 'camera' : 'gallery'}
:
$e
'
))
);
}
}
}
Future
<
String
?>
fetchBookDescription
(
String
isbn
)
async
{
final
String
url
=
"https://www.googleapis.com/books/v1/volumes?q=isbn:
$isbn
"
;
Future
<
String
?>
fetchBookDescription
(
String
isbn
)
async
{
final
String
url
=
"https://www.googleapis.com/books/v1/volumes?q=isbn:
$isbn
"
;
try
{
final
response
=
await
http
.
get
(
Uri
.
parse
(
url
));
if
(
response
.
statusCode
==
200
)
{
final
data
=
jsonDecode
(
response
.
body
);
if
(
data
[
'totalItems'
]
>
0
)
{
return
data
[
'items'
][
0
][
'volumeInfo'
][
'description'
]
??
'No description available'
;
try
{
final
response
=
await
http
.
get
(
Uri
.
parse
(
url
));
if
(
response
.
statusCode
==
200
)
{
final
data
=
jsonDecode
(
response
.
body
);
if
(
data
[
'totalItems'
]
>
0
)
{
return
data
[
'items'
][
0
][
'volumeInfo'
][
'description'
]
??
'No description available'
;
}
}
}
catch
(
e
)
{
print
(
"Error fetching book details:
$e
"
);
}
}
catch
(
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
{
try
{
var
request
=
http
.
MultipartRequest
(
'POST'
,
Uri
.
parse
(
'https://api.imgur.com/3/upload'
)
);
Future
<
String
?>
uploadImageToImgur
(
File
imageFile
)
async
{
try
{
var
request
=
http
.
MultipartRequest
(
'POST'
,
Uri
.
parse
(
'https://api.imgur.com/3/upload'
)
);
request
.
headers
[
'Authorization'
]
=
'Client-ID 00caf989adf38fa'
;
request
.
headers
[
'Authorization'
]
=
'Client-ID 00caf989adf38fa'
;
var
pic
=
await
http
.
MultipartFile
.
fromPath
(
'image'
,
imageFile
.
path
);
request
.
files
.
add
(
pic
);
var
pic
=
await
http
.
MultipartFile
.
fromPath
(
'image'
,
imageFile
.
path
);
request
.
files
.
add
(
pic
);
var
response
=
await
request
.
send
();
if
(
response
.
statusCode
==
200
)
{
final
responseData
=
await
response
.
stream
.
bytesToString
();
final
jsonData
=
json
.
decode
(
responseData
);
return
jsonData
[
'data'
][
'link'
];
// Image URL from Imgur
}
else
{
print
(
'Failed to upload image:
${response.reasonPhrase}
'
);
var
response
=
await
request
.
send
();
if
(
response
.
statusCode
==
200
)
{
final
responseData
=
await
response
.
stream
.
bytesToString
();
final
jsonData
=
json
.
decode
(
responseData
);
return
jsonData
[
'data'
][
'link'
];
// Image URL from Imgur
}
else
{
print
(
'Failed to upload image:
${response.reasonPhrase}
'
);
return
null
;
}
}
catch
(
e
)
{
print
(
'Error uploading image:
$e
'
);
return
null
;
}
}
catch
(
e
)
{
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
;
...
...
@@ -111,53 +164,67 @@ Future<String?> uploadImageToImgur(File imageFile) async {
// Function to handle book posting
Future
<
void
>
_postBook
()
async
{
if
(
titleController
.
text
.
isEmpty
||
priceController
.
text
.
isEmpty
||
isbnController
.
text
.
isEmpty
||
authorController
.
text
.
isEmpty
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
'All fields are required.'
))
if
(
titleController
.
text
.
isEmpty
||
priceController
.
text
.
isEmpty
||
isbnController
.
text
.
isEmpty
||
authorController
.
text
.
isEmpty
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
'All fields are required.'
))
);
return
;
}
// Show loading indicator while fetching description
showDialog
(
context:
context
,
barrierDismissible:
false
,
builder:
(
BuildContext
context
)
{
return
const
Center
(
child:
CircularProgressIndicator
(),
);
},
);
return
;
}
String
?
description
=
await
fetchBookDescription
(
isbnController
.
text
);
descriptionController
.
text
=
description
??
'No description available'
;
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
)
{
Navigator
.
pushReplacementNamed
(
context
,
'/profile'
);
}
else
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
'Failed to post book. Try again.'
))
);
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
(
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
,
),
color:
isDarkMode
?
kDarkBackground
:
kLightBackground
,
),
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:
1
0
),
const
SizedBox
(
height:
2
0
),
// Display Selected Image
if
(
_imageFile
!=
null
)
if
(
_imageFile
!=
null
)
...[
Container
(
height:
15
0
,
height:
20
0
,
width:
double
.
infinity
,
child:
Image
.
file
(
_imageFile
!,
fit:
BoxFit
.
cover
),
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
Colors
.
grey
),
borderRadius:
BorderRadius
.
circular
(
8
),
),
child:
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
8
),
child:
Image
.
file
(
_imageFile
!,
fit:
BoxFit
.
cover
),
),
),
const
SizedBox
(
height:
10
),
Center
(
child:
TextButton
.
icon
(
onPressed:
()
{
setState
(()
{
_imageFile
=
null
;
});
},
icon:
const
Icon
(
Icons
.
delete
,
color:
Colors
.
red
),
label:
const
Text
(
'Remove Image'
,
style:
TextStyle
(
color:
Colors
.
red
)),
),
),
SizedBox
(
height:
20
),
],
const
SizedBox
(
height:
20
),
// Post Book Button
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
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
10
),
// Rounded corners
),
backgroundColor:
isDarkMode
?
kLightBackground
:
kDarkBackground
,
foregroundColor:
isDarkMode
?
kDarkBackground
:
kLightBackground
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
30
,
vertical:
15
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
10
),
),
),
child:
const
Text
(
'Post Book'
),
),
),
],
...
...
@@ -297,10 +381,10 @@ 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'
);
}
},
),
);
}
}
}
\ No newline at end of file
lib/profile.dart
View file @
5ef5d4ce
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'
;
...
...
@@ -54,8 +51,8 @@ class ProfilePage extends StatelessWidget {
return
Scaffold
(
appBar:
AppBar
(
iconTheme:
IconThemeData
(
color:
isDarkMode
?
kDarkBackground
:
kLightBackground
,
),
color:
isDarkMode
?
kDarkBackground
:
kLightBackground
,
),
title:
const
Text
(
"Profile"
,
style:
TextStyle
(
...
...
@@ -67,8 +64,7 @@ class ProfilePage extends StatelessWidget {
),
),
foregroundColor:
isDarkMode
?
kDarkBackground
:
kLightBackground
,
),
),
drawer:
const
NavBar
(),
body:
Container
(
color:
backgroundColor
,
...
...
@@ -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,13 +204,11 @@ 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
),
),
);
},
...
...
@@ -226,6 +220,247 @@ class ProfilePage extends StatelessWidget {
);
},
),
const
SizedBox
(
height:
32
),
// Reviews section
StreamBuilder
<
QuerySnapshot
>(
stream:
FirebaseFirestore
.
instance
.
collection
(
'reviews'
)
.
where
(
'sellerId'
,
isEqualTo:
currentUser
.
uid
)
.
snapshots
(),
builder:
(
context
,
reviewsSnapshot
)
{
if
(
reviewsSnapshot
.
hasError
)
{
return
Text
(
'Error loading reviews'
,
style:
TextStyle
(
color:
textColor
),
);
}
if
(
reviewsSnapshot
.
connectionState
==
ConnectionState
.
waiting
)
{
return
const
Center
(
child:
CircularProgressIndicator
());
}
final
reviews
=
reviewsSnapshot
.
data
!.
docs
;
if
(
reviews
.
isEmpty
)
{
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'Reviews'
,
style:
TextStyle
(
fontSize:
20
,
fontWeight:
FontWeight
.
bold
,
color:
textColor
,
),
),
const
SizedBox
(
height:
8
),
Text
(
'No reviews yet'
,
textAlign:
TextAlign
.
center
,
style:
TextStyle
(
color:
textColor
.
withOpacity
(
0.7
)),
),
],
);
}
// Calculate average rating
double
totalRating
=
0
;
for
(
var
review
in
reviews
)
{
totalRating
+=
(
review
.
data
()
as
Map
<
String
,
dynamic
>)[
'rating'
]
??
0
;
}
double
averageRating
=
totalRating
/
reviews
.
length
;
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Text
(
'Reviews'
,
style:
TextStyle
(
fontSize:
20
,
fontWeight:
FontWeight
.
bold
,
color:
textColor
,
),
),
Row
(
children:
[
Text
(
averageRating
.
toStringAsFixed
(
1
),
style:
TextStyle
(
color:
textColor
,
fontWeight:
FontWeight
.
bold
,
fontSize:
16
,
),
),
const
SizedBox
(
width:
4
),
Icon
(
Icons
.
star
,
color:
Colors
.
amber
,
size:
20
,
),
Text
(
' (
${reviews.length}
)'
,
style:
TextStyle
(
color:
textColor
.
withOpacity
(
0.7
),
fontSize:
14
,
),
),
],
),
],
),
const
SizedBox
(
height:
16
),
ListView
.
builder
(
shrinkWrap:
true
,
physics:
const
NeverScrollableScrollPhysics
(),
itemCount:
reviews
.
length
,
itemBuilder:
(
context
,
index
)
{
final
reviewDoc
=
reviews
[
index
];
final
review
=
reviewDoc
.
data
()
as
Map
<
String
,
dynamic
>;
final
reviewId
=
reviewDoc
.
id
;
final
rating
=
review
[
'rating'
]
??
0
;
final
comment
=
review
[
'comment'
]
??
'No comment'
;
final
buyerName
=
review
[
'buyerName'
]
??
'Anonymous'
;
final
timestamp
=
review
[
'timestamp'
]
as
Timestamp
?;
final
date
=
timestamp
!=
null
?
'
${timestamp.toDate().day}
/
${timestamp.toDate().month}
/
${timestamp.toDate().year}
'
:
'Unknown date'
;
return
Card
(
color:
isDarkMode
?
Colors
.
grey
[
900
]
:
Colors
.
white
,
margin:
const
EdgeInsets
.
only
(
bottom:
8
),
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
buyerName
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
color:
textColor
,
),
),
Text
(
date
,
style:
TextStyle
(
fontSize:
12
,
color:
textColor
.
withOpacity
(
0.7
),
),
),
],
),
),
Row
(
children:
[
// Rating stars
Row
(
children:
List
.
generate
(
5
,
(
i
)
{
return
Icon
(
i
<
rating
?
Icons
.
star
:
Icons
.
star_border
,
color:
Colors
.
amber
,
size:
16
,
);
}),
),
const
SizedBox
(
width:
8
),
// Delete button
IconButton
(
icon:
Icon
(
Icons
.
delete
,
color:
Colors
.
red
.
withOpacity
(
0.7
),
size:
20
,
),
onPressed:
()
{
// Show confirmation dialog
showDialog
(
context:
context
,
builder:
(
BuildContext
context
)
{
return
AlertDialog
(
backgroundColor:
isDarkMode
?
Colors
.
grey
[
900
]
:
Colors
.
white
,
title:
Text
(
'Delete Review'
,
style:
TextStyle
(
color:
textColor
),
),
content:
Text
(
'Are you sure you want to delete this review?'
,
style:
TextStyle
(
color:
textColor
),
),
actions:
[
TextButton
(
child:
Text
(
'Cancel'
,
style:
TextStyle
(
color:
kPrimaryColor
),
),
onPressed:
()
{
Navigator
.
of
(
context
).
pop
();
},
),
TextButton
(
child:
const
Text
(
'Delete'
,
style:
TextStyle
(
color:
Colors
.
red
),
),
onPressed:
()
{
// Delete the review
FirebaseFirestore
.
instance
.
collection
(
'reviews'
)
.
doc
(
reviewId
)
.
delete
()
.
then
((
_
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
'Review deleted successfully'
),
backgroundColor:
Colors
.
green
,
),
);
}).
catchError
((
error
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
'Failed to delete review:
$error
'
),
backgroundColor:
Colors
.
red
,
),
);
});
Navigator
.
of
(
context
).
pop
();
},
),
],
);
},
);
},
),
],
),
],
),
const
SizedBox
(
height:
8
),
Text
(
comment
,
style:
TextStyle
(
color:
textColor
),
),
],
),
),
);
},
),
],
);
},
),
],
);
},
...
...
lib/seller_profile_page.dart
0 → 100644
View file @
5ef5d4ce
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
lib/settings.dart
View file @
5ef5d4ce
...
...
@@ -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
();
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment