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