/// User search result with minimal info for display class SearchUser { final String id; final String username; final String displayName; final String? avatarUrl; final String harmonyTier; SearchUser({ required this.id, required this.username, required this.displayName, this.avatarUrl, this.harmonyTier = 'new', }); factory SearchUser.fromJson(Map json) { return SearchUser( id: json['id'] as String, username: json['username'] as String, displayName: json['display_name'] as String? ?? json['displayName'] as String? ?? json['username'] as String, avatarUrl: json['avatar_url'] as String?, harmonyTier: json['harmony_tier'] as String? ?? json['harmonyTier'] as String? ?? 'new', ); } Map toJson() { return { 'id': id, 'username': username, 'display_name': displayName, 'avatar_url': avatarUrl, 'harmony_tier': harmonyTier, }; } } /// Hashtag search result with post count class SearchTag { final String tag; final int count; SearchTag({ required this.tag, required this.count, }); factory SearchTag.fromJson(Map json) { return SearchTag( tag: json['tag'] as String, count: json['count'] as int? ?? 0, ); } Map toJson() { return { 'tag': tag, 'count': count, }; } String get withHash => '#$tag'; } /// Post search result with minimal info for display class SearchPost { final String id; final String body; final String authorId; final String authorHandle; final String authorDisplayName; final DateTime createdAt; SearchPost({ required this.id, required this.body, required this.authorId, required this.authorHandle, required this.authorDisplayName, required this.createdAt, }); factory SearchPost.fromJson(Map json) { return SearchPost( id: json['id'] as String, body: json['body'] as String, authorId: json['author_id'] as String, authorHandle: json['author_handle'] as String, authorDisplayName: json['author_display_name'] as String, createdAt: DateTime.parse(json['created_at'] as String), ); } Map toJson() { return { 'id': id, 'body': body, 'author_id': authorId, 'author_handle': authorHandle, 'author_display_name': authorDisplayName, 'created_at': createdAt.toIso8601String(), }; } } /// Search results model for discovery search /// Contains users, hashtags, and posts matching the query class SearchResults { final List users; final List tags; final List posts; SearchResults({ required this.users, required this.tags, required this.posts, }); factory SearchResults.fromJson(Map json) { final usersJson = json['users'] as List? ?? []; final tagsJson = json['tags'] as List? ?? []; final postsJson = json['posts'] as List? ?? []; return SearchResults( users: usersJson .map((u) => SearchUser.fromJson(u as Map)) .toList(), tags: tagsJson .map((t) => SearchTag.fromJson(t as Map)) .toList(), posts: postsJson .map((p) => SearchPost.fromJson(p as Map)) .toList(), ); } Map toJson() { return { 'users': users.map((u) => u.toJson()).toList(), 'tags': tags.map((t) => t.toJson()).toList(), 'posts': posts.map((p) => p.toJson()).toList(), }; } bool get isEmpty => users.isEmpty && tags.isEmpty && posts.isEmpty; bool get hasResults => users.isNotEmpty || tags.isNotEmpty || posts.isNotEmpty; } /// Recent search item (stored locally) class RecentSearch { final String id; final String text; final DateTime searchedAt; final RecentSearchType type; RecentSearch({ required this.id, required this.text, required this.searchedAt, required this.type, }); factory RecentSearch.fromJson(Map json) { return RecentSearch( id: json['id'] as String, text: json['text'] as String, searchedAt: DateTime.parse(json['searched_at'] as String), type: RecentSearchType.values.firstWhere( (e) => e.name == json['type'], orElse: () => RecentSearchType.text, ), ); } Map toJson() { return { 'id': id, 'text': text, 'searched_at': searchedAt.toIso8601String(), 'type': type.name, }; } } enum RecentSearchType { user, tag, text, }