import 'package:flutter/material.dart'; /// Preset avatar options available for all riders without external image hosting. class PresetAvatar { final String id; final String label; final int primaryColorHex; final int secondaryColorHex; final IconData icon; const PresetAvatar({ required this.id, required this.label, required this.primaryColorHex, required this.secondaryColorHex, this.icon = Icons.two_wheeler, }); static const List availablePresets = [ PresetAvatar( id: 'helmet_cyan', label: 'Cyan Apex', primaryColorHex: 0xFF00E5FF, secondaryColorHex: 0xFF003840, icon: Icons.sports_motorsports, ), PresetAvatar( id: 'helmet_amber', label: 'Amber Sector', primaryColorHex: 0xFFFFB300, secondaryColorHex: 0xFF4A3200, icon: Icons.two_wheeler, ), PresetAvatar( id: 'tach_neon', label: 'Neon Tach', primaryColorHex: 0xFF00E676, secondaryColorHex: 0xFF00381B, icon: Icons.speed, ), PresetAvatar( id: 'speed_violet', label: 'Violet Velocity', primaryColorHex: 0xFFD500F9, secondaryColorHex: 0xFF3E004A, icon: Icons.electric_bolt, ), PresetAvatar( id: 'cruiser_red', label: 'Crimson Cruiser', primaryColorHex: 0xFFFF1744, secondaryColorHex: 0xFF4A0513, icon: Icons.motorcycle, ), PresetAvatar( id: 'adv_blue', label: 'Adventure Azure', primaryColorHex: 0xFF2979FF, secondaryColorHex: 0xFF0A224A, icon: Icons.terrain, ), PresetAvatar( id: 'cafe_gold', label: 'Cafe Gold', primaryColorHex: 0xFFFFD700, secondaryColorHex: 0xFF4A3E00, icon: Icons.workspace_premium, ), PresetAvatar( id: 'touring_slate', label: 'Touring Slate', primaryColorHex: 0xFF90A4AE, secondaryColorHex: 0xFF263238, icon: Icons.navigation, ), ]; static PresetAvatar byId(String? id) { if (id == null) return availablePresets.first; return availablePresets.firstWhere( (p) => p.id == id, orElse: () => availablePresets.first, ); } } /// Privacy controls for a rider profile. @immutable class ProfilePrivacySettings { final bool isSearchableByHandle; final bool isProfilePublic; final bool allowFriendRequests; const ProfilePrivacySettings({ this.isSearchableByHandle = true, this.isProfilePublic = true, this.allowFriendRequests = true, }); Map toJson() => { 'isSearchableByHandle': isSearchableByHandle, 'isProfilePublic': isProfilePublic, 'allowFriendRequests': allowFriendRequests, }; factory ProfilePrivacySettings.fromJson(Map? json) { if (json == null) return const ProfilePrivacySettings(); return ProfilePrivacySettings( isSearchableByHandle: json['isSearchableByHandle'] as bool? ?? true, isProfilePublic: json['isProfilePublic'] as bool? ?? true, allowFriendRequests: json['allowFriendRequests'] as bool? ?? true, ); } ProfilePrivacySettings copyWith({ bool? isSearchableByHandle, bool? isProfilePublic, bool? allowFriendRequests, }) { return ProfilePrivacySettings( isSearchableByHandle: isSearchableByHandle ?? this.isSearchableByHandle, isProfilePublic: isProfilePublic ?? this.isProfilePublic, allowFriendRequests: allowFriendRequests ?? this.allowFriendRequests, ); } @override bool operator ==(Object other) => identical(this, other) || other is ProfilePrivacySettings && runtimeType == other.runtimeType && isSearchableByHandle == other.isSearchableByHandle && isProfilePublic == other.isProfilePublic && allowFriendRequests == other.allowFriendRequests; @override int get hashCode => isSearchableByHandle.hashCode ^ isProfilePublic.hashCode ^ allowFriendRequests.hashCode; } /// Public rider profile decoupled from authentication credentials and cloud ride ownership. @immutable class RiderProfile { final String uid; final String handle; final String displayName; final String bio; final String region; final String? avatarPreset; final String? avatarUrl; final ProfilePrivacySettings privacySettings; final DateTime createdAt; final DateTime updatedAt; const RiderProfile({ required this.uid, required this.handle, required this.displayName, this.bio = '', this.region = '', this.avatarPreset, this.avatarUrl, this.privacySettings = const ProfilePrivacySettings(), required this.createdAt, required this.updatedAt, }); String get displayHandle => '@$handle'; PresetAvatar get preset => PresetAvatar.byId(avatarPreset); Map toJson() => { 'id': uid, 'username': handle, 'displayName': displayName, 'bio': bio, 'location': region, 'profilePhotoUrl': avatarUrl, 'avatarPreset': avatarPreset, 'privacySettings': privacySettings.toJson(), 'createdAt': createdAt.toIso8601String(), 'updatedAt': updatedAt.toIso8601String(), }; /// Serializes into the minimal, safe public profile projection for `public_profiles/{uid}`. /// Strictly isolates public fields from private user data. Map toPublicProfileJson() => { 'id': uid, 'username': handle, 'displayName': displayName, if (avatarUrl != null) 'profilePhotoUrl': avatarUrl, if (avatarPreset != null) 'avatarPreset': avatarPreset, if (bio.isNotEmpty) 'bio': bio, if (region.isNotEmpty) 'location': region, 'isSearchableByHandle': privacySettings.isSearchableByHandle, 'isProfilePublic': privacySettings.isProfilePublic, 'allowFriendRequests': privacySettings.allowFriendRequests, 'updatedAt': updatedAt.toIso8601String(), }; factory RiderProfile.fromPublicProfileJson(Map json) { final updated = json['updatedAt'] != null ? DateTime.tryParse(json['updatedAt'] as String) ?? DateTime.now() : DateTime.now(); return RiderProfile( uid: json['id'] as String? ?? '', handle: (json['username'] as String? ?? '').toLowerCase().trim(), displayName: json['displayName'] as String? ?? '', bio: json['bio'] as String? ?? '', region: json['location'] as String? ?? '', avatarPreset: json['avatarPreset'] as String?, avatarUrl: json['profilePhotoUrl'] as String?, privacySettings: ProfilePrivacySettings( isSearchableByHandle: json['isSearchableByHandle'] as bool? ?? true, isProfilePublic: json['isProfilePublic'] as bool? ?? true, allowFriendRequests: json['allowFriendRequests'] as bool? ?? true, ), createdAt: updated, updatedAt: updated, ); } factory RiderProfile.fromJson(Map json) { final created = json['createdAt'] != null ? DateTime.tryParse(json['createdAt'] as String) ?? DateTime.now() : DateTime.now(); final updated = json['updatedAt'] != null ? DateTime.tryParse(json['updatedAt'] as String) ?? created : created; return RiderProfile( uid: json['id'] as String? ?? '', handle: (json['username'] as String? ?? '').toLowerCase().trim(), displayName: json['displayName'] as String? ?? '', bio: json['bio'] as String? ?? '', region: json['location'] as String? ?? '', avatarPreset: json['avatarPreset'] as String?, avatarUrl: json['profilePhotoUrl'] as String?, privacySettings: ProfilePrivacySettings.fromJson( json['privacySettings'] as Map?, ), createdAt: created, updatedAt: updated, ); } RiderProfile copyWith({ String? uid, String? handle, String? displayName, String? bio, String? region, String? avatarPreset, String? avatarUrl, ProfilePrivacySettings? privacySettings, DateTime? createdAt, DateTime? updatedAt, }) { return RiderProfile( uid: uid ?? this.uid, handle: handle ?? this.handle, displayName: displayName ?? this.displayName, bio: bio ?? this.bio, region: region ?? this.region, avatarPreset: avatarPreset ?? this.avatarPreset, avatarUrl: avatarUrl ?? this.avatarUrl, privacySettings: privacySettings ?? this.privacySettings, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, ); } @override bool operator ==(Object other) => identical(this, other) || other is RiderProfile && runtimeType == other.runtimeType && uid == other.uid && handle == other.handle && displayName == other.displayName && bio == other.bio && region == other.region && avatarPreset == other.avatarPreset && avatarUrl == other.avatarUrl && privacySettings == other.privacySettings; @override int get hashCode => uid.hashCode ^ handle.hashCode ^ displayName.hashCode ^ bio.hashCode ^ region.hashCode ^ avatarPreset.hashCode ^ avatarUrl.hashCode ^ privacySettings.hashCode; } /// Relationship status between the active rider and another rider. enum FriendshipStatus { self, none, friend, requestIncoming, requestOutgoing, blocked, } /// Mutual friend summary. @immutable class FriendSummary { final String uid; final String handle; final String displayName; final String? avatarPreset; final String? avatarUrl; final DateTime friendedAt; const FriendSummary({ required this.uid, required this.handle, required this.displayName, this.avatarPreset, this.avatarUrl, required this.friendedAt, }); String get displayHandle => '@$handle'; Map toJson() => { 'uid': uid, 'handle': handle, 'displayName': displayName, 'avatarPreset': avatarPreset, 'avatarUrl': avatarUrl, 'friendedAt': friendedAt.toIso8601String(), }; factory FriendSummary.fromJson(Map json) => FriendSummary( uid: json['uid'] as String? ?? '', handle: json['handle'] as String? ?? '', displayName: json['displayName'] as String? ?? '', avatarPreset: json['avatarPreset'] as String?, avatarUrl: json['avatarUrl'] as String?, friendedAt: json['friendedAt'] != null ? DateTime.tryParse(json['friendedAt'] as String) ?? DateTime.now() : DateTime.now(), ); } /// Incoming or outgoing friend request. @immutable class FriendRequest { final String requestId; final String senderUid; final String senderHandle; final String senderDisplayName; final String? senderAvatarPreset; final String? senderAvatarUrl; final String receiverUid; final DateTime sentAt; const FriendRequest({ required this.requestId, required this.senderUid, required this.senderHandle, required this.senderDisplayName, this.senderAvatarPreset, this.senderAvatarUrl, required this.receiverUid, required this.sentAt, }); Map toJson() => { 'requestId': requestId, 'senderUid': senderUid, 'senderHandle': senderHandle, 'senderDisplayName': senderDisplayName, 'senderAvatarPreset': senderAvatarPreset, 'senderAvatarUrl': senderAvatarUrl, 'receiverUid': receiverUid, 'sentAt': sentAt.toIso8601String(), }; factory FriendRequest.fromJson(Map json) => FriendRequest( requestId: json['requestId'] as String? ?? '', senderUid: json['senderUid'] as String? ?? '', senderHandle: json['senderHandle'] as String? ?? '', senderDisplayName: json['senderDisplayName'] as String? ?? '', senderAvatarPreset: json['senderAvatarPreset'] as String?, senderAvatarUrl: json['senderAvatarUrl'] as String?, receiverUid: json['receiverUid'] as String? ?? '', sentAt: json['sentAt'] != null ? DateTime.tryParse(json['sentAt'] as String) ?? DateTime.now() : DateTime.now(), ); } /// Blocked rider record. @immutable class BlockedRider { final String blockedUid; final String? blockedHandle; final String? blockedDisplayName; final DateTime blockedAt; const BlockedRider({ required this.blockedUid, this.blockedHandle, this.blockedDisplayName, required this.blockedAt, }); Map toJson() => { 'blockedUid': blockedUid, 'blockedHandle': blockedHandle, 'blockedDisplayName': blockedDisplayName, 'blockedAt': blockedAt.toIso8601String(), }; factory BlockedRider.fromJson(Map json) => BlockedRider( blockedUid: json['blockedUid'] as String? ?? '', blockedHandle: json['blockedHandle'] as String?, blockedDisplayName: json['blockedDisplayName'] as String?, blockedAt: json['blockedAt'] != null ? DateTime.tryParse(json['blockedAt'] as String) ?? DateTime.now() : DateTime.now(), ); } /// Search result summary for public rider lookup. @immutable class RiderSearchResult { final String uid; final String handle; final String displayName; final String bio; final String region; final String? avatarPreset; final String? avatarUrl; final bool isProfilePublic; final bool allowFriendRequests; final FriendshipStatus relationship; const RiderSearchResult({ required this.uid, required this.handle, required this.displayName, this.bio = '', this.region = '', this.avatarPreset, this.avatarUrl, required this.isProfilePublic, required this.allowFriendRequests, required this.relationship, }); String get displayHandle => '@$handle'; }