Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,20 @@ async def post_reviews(
for review in reviews:
if current_user["id"] == review.get("reviewer_id"):
raise HTTPException(
status_code=404, detail="You have already left the user a review")
status_code=404, detail="You have already left the user a review")

# check if the seller and the current user have a conversation
try:
user_id = ObjectId(current_user["id"])
seller_id = ObjectId(body.seller_id)
except Exception:
raise HTTPException(status_code=400, detail="Invalid user or seller ID format.")

conversation_id = "_".join(sorted([str(user_id), str(seller_id)]))
conversation = await messages_collection.find_one({"conversation_id": conversation_id})
if not conversation:
raise HTTPException(
status_code=403, detail="You cannot leave a review without having a conversation with the seller")

review = {
"seller_id": ObjectId(body.seller_id),
Expand All @@ -831,6 +844,9 @@ async def post_reviews(

return ReviewPostResponse(message="Review submitted successfully")

except HTTPException as e:
raise HTTPException(
status_code=e.status_code, detail=f"Error: {e.detail}")
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Internal Server Error: {str(e)}")
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added backend/app/static/67f99f0867d759b3731b9c89_0.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added backend/app/static/67f99f2567d759b3731b9c8a_0.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added backend/app/static/67f99f3967d759b3731b9c8b_0.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
128 changes: 128 additions & 0 deletions frontend/lib/profile/components/profile_actions.component.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:utm_marketplace/profile/view_models/profile.viewmodel.dart';

class ProfileActions extends StatelessWidget {
final bool isOwnProfile;
final VoidCallback onToggleView;
final bool showListings;
final ProfileViewModel viewmodel;

const ProfileActions({
super.key,
required this.viewmodel,
required this.isOwnProfile,
required this.onToggleView,
required this.showListings,
Expand All @@ -30,6 +33,126 @@ class ProfileActions extends StatelessWidget {
),
);

final addReviewButton = ElevatedButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
final TextEditingController reviewController =
TextEditingController();
int selectedRating = 0;

return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: const Text('Write Review'),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return SizedBox(
width: MediaQuery.of(context).size.width * 1,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
return IconButton(
icon: Icon(
selectedRating > index
? Icons.star
: Icons.star_border,
color: Colors.amber,
),
onPressed: () {
setState(() {
selectedRating = index + 1;
});
},
);
}),
),
TextField(
controller: reviewController,
maxLines: 5,
decoration: const InputDecoration(
hintText: 'Enter your review here',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
],
),
);
},
),
actions: [
Center(
child: ElevatedButton(
onPressed: () {
if (selectedRating > 0) {
viewmodel
.submitReview(
reviewController.text,
selectedRating,
)
.then((_) {
if (context.mounted) {
if (viewmodel.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(viewmodel.errorMessage!),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Review submitted successfully'),
),
);
}
}
});
Navigator.of(context).pop();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please select a rating'),
),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1E3765),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
child: const Text(
'Submit',
style: TextStyle(color: Colors.white),
),
),
),
],
);
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1E3765),
minimumSize: const Size(double.infinity, 45),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
child: const Text(
'Add Review',
style: TextStyle(color: Colors.white),
),
);

final listingsTab = GestureDetector(
onTap: () {
if (!showListings) onToggleView();
Expand Down Expand Up @@ -95,6 +218,11 @@ class ProfileActions extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: savedItemsButton,
),
if (!isOwnProfile)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: addReviewButton,
),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
Expand Down
21 changes: 19 additions & 2 deletions frontend/lib/profile/model/profile.model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class ProfileModel {
final int ratingCount;
final String? location;
final List<String>? savedPosts;
final List<Review> reviews;
List<Review> reviews;
final List<ListingItem> listings;

ProfileModel({
Expand All @@ -33,10 +33,27 @@ class ProfileModel {
ratingCount: json['rating_count'] ?? 0,
location: json['location'],
savedPosts: (json['saved_posts'] as List?)?.cast<String>(),
reviews: [], // Reviews are not part of the current API response
reviews: [],
listings: [], // Listings are not part of the current API response
);
}

Future<void> fetchReviews(Map<String, dynamic> reviewsJson) async {
try {
reviews = (reviewsJson['reviews'] as List<dynamic>)
.map((reviewJson) => Review(
reviewerId: reviewJson['reviewer_id'],
reviewerName: '', // Name is not provided in the response
reviewerImage: '', // Image is not provided in the response
rating: reviewJson['rating'].toDouble(),
comment: reviewJson['comment'] ?? '',
date: DateTime.parse(reviewJson['timestamp']),
))
.toList();
} catch (e) {
throw Exception('Failed to fetch and set reviews: $e');
}
}
}

class Review {
Expand Down
67 changes: 67 additions & 0 deletions frontend/lib/profile/repository/profile.repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,73 @@ class ProfileRepository {
}
}

Future<Map<String, dynamic>> fetchReviews(String sellerId) async {
try {
// Get the JWT token from secure storage
final token = await secureStorage.read(key: 'jwt_token');

if (token == null) {
throw Exception('User is not authenticated');
}

// If userId is "me", get the user ID from the token
String targetUserId = sellerId;
if (sellerId == 'me') {
targetUserId = _getUserIdFromToken(token);
}

// Make API call to fetch reviews
final response = await dio.get(
'/reviews',
queryParameters: {'seller_id': targetUserId},
options: Options(
headers: {
'Authorization': 'Bearer $token',
},
),
);

return response.data;
} catch (e) {
throw Exception('Failed to fetch reviews: $e');
}
}

Future<int> submitReview(String sellerId, String review, int rating) async {
try {
// Get the JWT token from secure storage
final token = await secureStorage.read(key: 'jwt_token');

if (token == null) {
throw Exception('User is not authenticated');
}

// Prepare review data
final reviewData = {
'seller_id': sellerId,
'comment': review,
'rating': rating,
};

// Make API call to submit the review
final response = await dio.post(
'/reviews',
data: reviewData,
options: Options(
headers: {
'Authorization': 'Bearer $token',
},
),
);

return response.statusCode ?? 0;
} catch (e) {
debugPrint('Error submitting review: $e');
debugPrint("Error code: ${e is DioException ? (e).response?.statusCode : 0}");
return e is DioException ? (e).response?.statusCode ?? 0 : 0;
}
}

Future<ProfileModel> updateProfile({
required String userId,
String? displayName,
Expand Down
3 changes: 3 additions & 0 deletions frontend/lib/profile/view/profile.view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class _ProfileState extends State<Profile> {
),
const SizedBox(height: 16),
ProfileActions(
viewmodel: profileViewModel,
isOwnProfile: widget.isOwnProfile,
onToggleView: profileViewModel.toggleView,
showListings: profileViewModel.showListings,
Expand Down Expand Up @@ -244,6 +245,8 @@ class _ProfileState extends State<Profile> {
leading: CircleAvatar(
radius: 24,
backgroundImage: NetworkImage(review.reviewerImage),
onBackgroundImageError: (_, __) => setState(() {}),
child: Icon(Icons.person, size: 24),
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Expand Down
30 changes: 30 additions & 0 deletions frontend/lib/profile/view_models/profile.viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class ProfileViewModel extends LoadingViewModel {
isLoading = true;
notifyListeners();
final result = await repo.fetchUserProfileById(userId);

final reviews = await repo.fetchReviews(userId);
result.fetchReviews(reviews);
_profileModel = result;
notifyListeners();
} catch (e) {
Expand All @@ -52,11 +55,38 @@ class ProfileViewModel extends LoadingViewModel {
_imageVersion++;

await fetchUserProfileById(_profileModel!.id);

final reviews = await repo.fetchReviews(_profileModel!.id);
_profileModel?.fetchReviews(reviews);
} catch (e) {
debugPrint('Error refreshing user data: ${e.toString()}');
}
}

Future<void> submitReview(String review, int rating) async {
if (_profileModel == null) return;
int result = 0;

try {
_errorMessage = null;
isLoading = true;
notifyListeners();

result = await repo.submitReview(_profileModel!.id, review, rating);
if (result == 403) {
_errorMessage = "You must have spoken to the seller to leave a review.";
}
notifyListeners();
} catch (e) {
debugPrint('Error in submitReview: ${e.toString()} $result');
_errorMessage = e.toString();
notifyListeners();
} finally {
isLoading = false;
notifyListeners();
}
}

Future<bool> updateProfile({
required String userId,
String? displayName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class BottomNav extends StatelessWidget {
),
],
onTap: (index) {
if (index == currentIndex) return;
if (index == currentIndex && index != 0) return;

switch (index) {
case 0:
Expand Down
Loading