-
Notifications
You must be signed in to change notification settings - Fork 0
Add community features (Phase 1 - API skeleton + client UI) #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
51a33e6
25d3387
8a0c5cb
e5ef1d2
f12fcd1
14f4ff9
194b129
55b422d
5cdd088
6bb702c
d1c81ff
183bda7
fbda728
c25d099
923a5b5
d048224
21f1040
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| docs/site/ | ||
| NOTES.md | ||
| firebase-debug.log | ||
| firebase-debug.log | ||
| .worktrees/ |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,73 @@ | ||||||
| /// User summary within an activity feed item. | ||||||
| class ActivityUser { | ||||||
| final String userId; | ||||||
| final String displayName; | ||||||
| final String? username; | ||||||
| final String? avatarUrl; | ||||||
|
|
||||||
| const ActivityUser({ | ||||||
| required this.userId, | ||||||
| required this.displayName, | ||||||
| this.username, | ||||||
| this.avatarUrl, | ||||||
| }); | ||||||
|
|
||||||
| factory ActivityUser.fromJson(Map<String, dynamic> json) => ActivityUser( | ||||||
| userId: json['user_id'] as String, | ||||||
| displayName: json['display_name'] as String, | ||||||
| username: json['username'] as String?, | ||||||
| avatarUrl: json['avatar_url'] as String?, | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| /// Book summary within an activity feed item. | ||||||
| class ActivityBook { | ||||||
| final String catalogBookId; | ||||||
| final String title; | ||||||
| final String author; | ||||||
| final String? coverImageUrl; | ||||||
|
|
||||||
| const ActivityBook({ | ||||||
| required this.catalogBookId, | ||||||
| required this.title, | ||||||
| required this.author, | ||||||
| this.coverImageUrl, | ||||||
| }); | ||||||
|
|
||||||
| factory ActivityBook.fromJson(Map<String, dynamic> json) => ActivityBook( | ||||||
| catalogBookId: json['catalog_book_id'] as String, | ||||||
| title: json['title'] as String, | ||||||
| author: json['author'] as String, | ||||||
| coverImageUrl: json['cover_url'] as String?, | ||||||
|
||||||
| coverImageUrl: json['cover_url'] as String?, | |
| coverImageUrl: (json['cover_image_url'] ?? json['cover_url']) as String?, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| /// Community book catalog model. | ||
| class CatalogBook { | ||
| final String catalogBookId; | ||
| final String? openLibraryId; | ||
| final String? isbn; | ||
| final String title; | ||
| final String author; | ||
| final List<String>? authors; | ||
| final String? coverImageUrl; | ||
| final String? description; | ||
| final int? pageCount; | ||
| final double? averageRating; | ||
| final int ratingCount; | ||
| final int reviewCount; | ||
|
|
||
| const CatalogBook({ | ||
| required this.catalogBookId, | ||
| this.openLibraryId, | ||
| this.isbn, | ||
| required this.title, | ||
| required this.author, | ||
| this.authors, | ||
| this.coverImageUrl, | ||
| this.description, | ||
| this.pageCount, | ||
| this.averageRating, | ||
| this.ratingCount = 0, | ||
| this.reviewCount = 0, | ||
| }); | ||
|
|
||
| factory CatalogBook.fromJson(Map<String, dynamic> json) { | ||
| final authors = json['authors'] as List?; | ||
| final authorStr = authors != null && authors.isNotEmpty | ||
| ? authors.first as String | ||
| : json['author'] as String? ?? 'Unknown'; | ||
|
|
||
| return CatalogBook( | ||
| catalogBookId: json['catalog_book_id'] as String, | ||
| openLibraryId: json['open_library_id'] as String?, | ||
| isbn: json['isbn'] as String?, | ||
| title: json['title'] as String, | ||
| author: authorStr, | ||
| authors: authors?.cast<String>(), | ||
| coverImageUrl: json['cover_url'] as String?, | ||
| description: json['description'] as String?, | ||
| pageCount: json['page_count'] as int?, | ||
|
Comment on lines
+42
to
+46
|
||
| averageRating: (json['average_rating'] as num?)?.toDouble(), | ||
| ratingCount: json['rating_count'] as int? ?? 0, | ||
| reviewCount: json['review_count'] as int? ?? 0, | ||
| ); | ||
| } | ||
|
|
||
| Map<String, dynamic> toJson() => { | ||
| 'catalog_book_id': catalogBookId, | ||
| 'open_library_id': openLibraryId, | ||
| 'isbn': isbn, | ||
| 'title': title, | ||
| 'authors': authors ?? [author], | ||
| 'cover_url': coverImageUrl, | ||
| 'description': description, | ||
| 'page_count': pageCount, | ||
| 'average_rating': averageRating, | ||
| 'rating_count': ratingCount, | ||
| 'review_count': reviewCount, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /// Community book review model. | ||
| class CommunityReview { | ||
| final String reviewId; | ||
| final String userId; | ||
| final String catalogBookId; | ||
| final String? authorDisplayName; | ||
| final String? authorUsername; | ||
| final String? authorAvatarUrl; | ||
| final String? title; | ||
| final String body; | ||
| final bool containsSpoilers; | ||
| final String visibility; | ||
| final int likeCount; | ||
| final int helpfulCount; | ||
| final DateTime? createdAt; | ||
|
|
||
| const CommunityReview({ | ||
| required this.reviewId, | ||
| required this.userId, | ||
| required this.catalogBookId, | ||
| this.authorDisplayName, | ||
| this.authorUsername, | ||
| this.authorAvatarUrl, | ||
| this.title, | ||
| required this.body, | ||
| this.containsSpoilers = false, | ||
| this.visibility = 'public', | ||
| this.likeCount = 0, | ||
| this.helpfulCount = 0, | ||
| this.createdAt, | ||
| }); | ||
|
|
||
| factory CommunityReview.fromJson(Map<String, dynamic> json) => | ||
| CommunityReview( | ||
| reviewId: json['review_id'] as String, | ||
| userId: json['user_id'] as String, | ||
| catalogBookId: json['catalog_book_id'] as String, | ||
| authorDisplayName: json['author_display_name'] as String?, | ||
| authorUsername: json['author_username'] as String?, | ||
| authorAvatarUrl: json['author_avatar_url'] as String?, | ||
| title: json['title'] as String?, | ||
| body: json['body'] as String, | ||
| containsSpoilers: json['contains_spoilers'] as bool? ?? false, | ||
| visibility: json['visibility'] as String? ?? 'public', | ||
| likeCount: json['like_count'] as int? ?? 0, | ||
| helpfulCount: json['helpful_count'] as int? ?? 0, | ||
| createdAt: json['created_at'] != null | ||
| ? DateTime.parse(json['created_at'] as String) | ||
| : null, | ||
| ); | ||
|
|
||
| Map<String, dynamic> toJson() => { | ||
| 'review_id': reviewId, | ||
| 'user_id': userId, | ||
| 'catalog_book_id': catalogBookId, | ||
| 'title': title, | ||
| 'body': body, | ||
| 'contains_spoilers': containsSpoilers, | ||
| 'visibility': visibility, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| /// Community user profile model. | ||
| class CommunityUser { | ||
| final String userId; | ||
| final String? username; | ||
| final String displayName; | ||
| final String? bio; | ||
| final String? avatarUrl; | ||
| final String profileVisibility; | ||
| final int followerCount; | ||
| final int followingCount; | ||
| final int bookCount; | ||
| final int reviewCount; | ||
| final bool isFollowing; | ||
| final bool isFriend; | ||
| final bool isBlocked; | ||
|
|
||
| const CommunityUser({ | ||
| required this.userId, | ||
| this.username, | ||
| required this.displayName, | ||
| this.bio, | ||
| this.avatarUrl, | ||
| this.profileVisibility = 'public', | ||
| required this.followerCount, | ||
| required this.followingCount, | ||
| required this.bookCount, | ||
| this.reviewCount = 0, | ||
| this.isFollowing = false, | ||
| this.isFriend = false, | ||
| this.isBlocked = false, | ||
| }); | ||
|
|
||
| CommunityUser copyWith({ | ||
| String? userId, | ||
| String? username, | ||
| String? displayName, | ||
| String? bio, | ||
| String? avatarUrl, | ||
| String? profileVisibility, | ||
| int? followerCount, | ||
| int? followingCount, | ||
| int? bookCount, | ||
| int? reviewCount, | ||
| bool? isFollowing, | ||
| bool? isFriend, | ||
| bool? isBlocked, | ||
| }) => CommunityUser( | ||
| userId: userId ?? this.userId, | ||
| username: username ?? this.username, | ||
| displayName: displayName ?? this.displayName, | ||
| bio: bio ?? this.bio, | ||
| avatarUrl: avatarUrl ?? this.avatarUrl, | ||
| profileVisibility: profileVisibility ?? this.profileVisibility, | ||
| followerCount: followerCount ?? this.followerCount, | ||
| followingCount: followingCount ?? this.followingCount, | ||
| bookCount: bookCount ?? this.bookCount, | ||
| reviewCount: reviewCount ?? this.reviewCount, | ||
| isFollowing: isFollowing ?? this.isFollowing, | ||
| isFriend: isFriend ?? this.isFriend, | ||
| isBlocked: isBlocked ?? this.isBlocked, | ||
| ); | ||
|
|
||
| factory CommunityUser.fromJson(Map<String, dynamic> json) => CommunityUser( | ||
| userId: json['user_id'] as String, | ||
| username: json['username'] as String?, | ||
| displayName: json['display_name'] as String, | ||
| bio: json['bio'] as String?, | ||
| avatarUrl: json['avatar_url'] as String?, | ||
| profileVisibility: json['profile_visibility'] as String? ?? 'public', | ||
| followerCount: json['follower_count'] as int? ?? 0, | ||
| followingCount: json['following_count'] as int? ?? 0, | ||
| bookCount: json['book_count'] as int? ?? 0, | ||
| reviewCount: json['review_count'] as int? ?? 0, | ||
| isFollowing: json['is_following'] as bool? ?? false, | ||
| isFriend: json['is_friend'] as bool? ?? false, | ||
| isBlocked: json['is_blocked'] as bool? ?? false, | ||
| ); | ||
|
|
||
| Map<String, dynamic> toJson() => { | ||
| 'user_id': userId, | ||
| 'username': username, | ||
| 'display_name': displayName, | ||
| 'bio': bio, | ||
| 'avatar_url': avatarUrl, | ||
| 'profile_visibility': profileVisibility, | ||
| 'follower_count': followerCount, | ||
| 'following_count': followingCount, | ||
| 'book_count': bookCount, | ||
| 'review_count': reviewCount, | ||
| 'is_following': isFollowing, | ||
| 'is_friend': isFriend, | ||
| 'is_blocked': isBlocked, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The client routes community profiles by
userId(/community/user/:userId), while the server only exposesGET /v1/profiles/{username}(server/src/papyrus/api/routes/profiles.py). This identifier mismatch means the profile flow will break as soon asSocialProvider.loadUserProfileis wired to the API, because UUID-based navigation and username-based lookup are not interchangeable. Pick one canonical identifier (username or user_id) and use it consistently in both route contracts.Useful? React with 👍 / 👎.