mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-04 20:12:56 +00:00
feat: Complete Phase 1 - Full Flutter app implementation with comprehensive features
Version: 1.1.0 Major changes: - Implemented complete Flutter app structure with all core features - Added comprehensive UI screens for auth, countdown, goals, profile, settings, and social features - Integrated Supabase backend with authentication and data repositories - Added offline support with Hive caching and local storage - Implemented comprehensive routing with go_router - Added location services with Google Maps integration - Implemented notifications and home widget support - Added voice recording capabilities and AI chat features - Created comprehensive test suite and documentation - Added Android and iOS platform configurations - Implemented achievements system and social features - Added calendar integration and bucket list functionality This represents a complete Phase 1 milestone with 3,775 additions across 31 files.
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class Achievement extends Equatable {
|
||||
final String id;
|
||||
final String title;
|
||||
final String description;
|
||||
final String icon;
|
||||
final AchievementType type;
|
||||
final int? threshold;
|
||||
final DateTime unlockedAt;
|
||||
final bool isUnlocked;
|
||||
|
||||
const Achievement({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.icon,
|
||||
required this.type,
|
||||
this.threshold,
|
||||
required this.unlockedAt,
|
||||
this.isUnlocked = false,
|
||||
});
|
||||
|
||||
Achievement copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? description,
|
||||
String? icon,
|
||||
AchievementType? type,
|
||||
int? threshold,
|
||||
DateTime? unlockedAt,
|
||||
bool? isUnlocked,
|
||||
}) {
|
||||
return Achievement(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
icon: icon ?? this.icon,
|
||||
type: type ?? this.type,
|
||||
threshold: threshold ?? this.threshold,
|
||||
unlockedAt: unlockedAt ?? this.unlockedAt,
|
||||
isUnlocked: isUnlocked ?? this.isUnlocked,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
type,
|
||||
threshold,
|
||||
unlockedAt,
|
||||
isUnlocked,
|
||||
];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'icon': icon,
|
||||
'type': type.toString(),
|
||||
'threshold': threshold,
|
||||
'unlocked_at': unlockedAt.toIso8601String(),
|
||||
'is_unlocked': isUnlocked,
|
||||
};
|
||||
}
|
||||
|
||||
factory Achievement.fromJson(Map<String, dynamic> json) {
|
||||
return Achievement(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
icon: json['icon'] as String,
|
||||
type: AchievementType.values.firstWhere(
|
||||
(e) => e.toString() == json['type'],
|
||||
orElse: () => AchievementType.custom,
|
||||
),
|
||||
threshold: json['threshold'] as int?,
|
||||
unlockedAt: json['unlocked_at'] != null
|
||||
? DateTime.parse(json['unlocked_at'] as String)
|
||||
: DateTime.now(),
|
||||
isUnlocked: json['is_unlocked'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum AchievementType {
|
||||
firstGoal,
|
||||
goalsCompleted5,
|
||||
goalsCompleted10,
|
||||
goalsCompleted20,
|
||||
streak7Days,
|
||||
streak30Days,
|
||||
countdownStarted,
|
||||
countdown25Percent,
|
||||
countdown50Percent,
|
||||
countdown75Percent,
|
||||
countdownCompleted,
|
||||
earlyBird,
|
||||
nightOwl,
|
||||
socialButterfly,
|
||||
custom,
|
||||
}
|
||||
|
||||
extension AchievementTypeExtension on AchievementType {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case AchievementType.firstGoal:
|
||||
return 'First Goal';
|
||||
case AchievementType.goalsCompleted5:
|
||||
return '5 Goals';
|
||||
case AchievementType.goalsCompleted10:
|
||||
return '10 Goals';
|
||||
case AchievementType.goalsCompleted20:
|
||||
return '20 Goals';
|
||||
case AchievementType.streak7Days:
|
||||
return '7 Day Streak';
|
||||
case AchievementType.streak30Days:
|
||||
return '30 Day Streak';
|
||||
case AchievementType.countdownStarted:
|
||||
return 'Challenge Started';
|
||||
case AchievementType.countdown25Percent:
|
||||
return '25% Complete';
|
||||
case AchievementType.countdown50Percent:
|
||||
return '50% Complete';
|
||||
case AchievementType.countdown75Percent:
|
||||
return '75% Complete';
|
||||
case AchievementType.countdownCompleted:
|
||||
return 'Challenge Complete';
|
||||
case AchievementType.earlyBird:
|
||||
return 'Early Bird';
|
||||
case AchievementType.nightOwl:
|
||||
return 'Night Owl';
|
||||
case AchievementType.socialButterfly:
|
||||
return 'Social Butterfly';
|
||||
case AchievementType.custom:
|
||||
return 'Custom';
|
||||
}
|
||||
}
|
||||
|
||||
String get iconEmoji {
|
||||
switch (this) {
|
||||
case AchievementType.firstGoal:
|
||||
return '🎯';
|
||||
case AchievementType.goalsCompleted5:
|
||||
return '⭐';
|
||||
case AchievementType.goalsCompleted10:
|
||||
return '🌟';
|
||||
case AchievementType.goalsCompleted20:
|
||||
return '💫';
|
||||
case AchievementType.streak7Days:
|
||||
return '🔥';
|
||||
case AchievementType.streak30Days:
|
||||
return '🏆';
|
||||
case AchievementType.countdownStarted:
|
||||
return '🚀';
|
||||
case AchievementType.countdown25Percent:
|
||||
return '📊';
|
||||
case AchievementType.countdown50Percent:
|
||||
return '📈';
|
||||
case AchievementType.countdown75Percent:
|
||||
return '📉';
|
||||
case AchievementType.countdownCompleted:
|
||||
return '🎉';
|
||||
case AchievementType.earlyBird:
|
||||
return '🌅';
|
||||
case AchievementType.nightOwl:
|
||||
return '🌙';
|
||||
case AchievementType.socialButterfly:
|
||||
return '🦋';
|
||||
case AchievementType.custom:
|
||||
return '🏅';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
part of 'cached_goal_model.dart';
|
||||
|
||||
class CachedGoalAdapter extends TypeAdapter<CachedGoal> {
|
||||
@override
|
||||
final int typeId = 0;
|
||||
|
||||
@override
|
||||
CachedGoal read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return CachedGoal(
|
||||
id: fields[0] as String,
|
||||
ownerId: fields[1] as String,
|
||||
title: fields[2] as String,
|
||||
description: fields[3] as String?,
|
||||
progress: fields[4] as int,
|
||||
locationLat: fields[5] as double?,
|
||||
locationLng: fields[6] as double?,
|
||||
locationName: fields[7] as String?,
|
||||
imageUrl: fields[8] as String?,
|
||||
completed: fields[9] as bool,
|
||||
createdAt: fields[10] as DateTime,
|
||||
updatedAt: fields[11] as DateTime,
|
||||
isDirty: fields[12] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, CachedGoal obj) {
|
||||
writer.writeByte(13);
|
||||
writer.writeByte(0);
|
||||
writer.write(obj.id);
|
||||
writer.writeByte(1);
|
||||
writer.write(obj.ownerId);
|
||||
writer.writeByte(2);
|
||||
writer.write(obj.title);
|
||||
writer.writeByte(3);
|
||||
writer.write(obj.description);
|
||||
writer.writeByte(4);
|
||||
writer.write(obj.progress);
|
||||
writer.writeByte(5);
|
||||
writer.write(obj.locationLat);
|
||||
writer.writeByte(6);
|
||||
writer.write(obj.locationLng);
|
||||
writer.writeByte(7);
|
||||
writer.write(obj.locationName);
|
||||
writer.writeByte(8);
|
||||
writer.write(obj.imageUrl);
|
||||
writer.writeByte(9);
|
||||
writer.write(obj.completed);
|
||||
writer.writeByte(10);
|
||||
writer.write(obj.createdAt);
|
||||
writer.writeByte(11);
|
||||
writer.write(obj.updatedAt);
|
||||
writer.writeByte(12);
|
||||
writer.write(obj.isDirty);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is CachedGoalAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'cached_goal.g.dart';
|
||||
|
||||
@HiveType(typeId: 0)
|
||||
class CachedGoal extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
@HiveField(1)
|
||||
final String ownerId;
|
||||
|
||||
@HiveField(2)
|
||||
final String title;
|
||||
|
||||
@HiveField(3)
|
||||
final String? description;
|
||||
|
||||
@HiveField(4)
|
||||
final int progress;
|
||||
|
||||
@HiveField(5)
|
||||
final double? locationLat;
|
||||
|
||||
@HiveField(6)
|
||||
final double? locationLng;
|
||||
|
||||
@HiveField(7)
|
||||
final String? locationName;
|
||||
|
||||
@HiveField(8)
|
||||
final String? imageUrl;
|
||||
|
||||
@HiveField(9)
|
||||
final bool completed;
|
||||
|
||||
@HiveField(10)
|
||||
final DateTime createdAt;
|
||||
|
||||
@HiveField(11)
|
||||
final DateTime updatedAt;
|
||||
|
||||
@HiveField(12)
|
||||
final bool isDirty;
|
||||
|
||||
CachedGoal({
|
||||
required this.id,
|
||||
required this.ownerId,
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.progress,
|
||||
this.locationLat,
|
||||
this.locationLng,
|
||||
this.locationName,
|
||||
this.imageUrl,
|
||||
required this.completed,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.isDirty = false,
|
||||
});
|
||||
|
||||
CachedGoal copyWith({
|
||||
String? id,
|
||||
String? ownerId,
|
||||
String? title,
|
||||
String? description,
|
||||
int? progress,
|
||||
double? locationLat,
|
||||
double? locationLng,
|
||||
String? locationName,
|
||||
String? imageUrl,
|
||||
bool? completed,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
bool? isDirty,
|
||||
}) {
|
||||
return CachedGoal(
|
||||
id: id ?? this.id,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
progress: progress ?? this.progress,
|
||||
locationLat: locationLat ?? this.locationLat,
|
||||
locationLng: locationLng ?? this.locationLng,
|
||||
locationName: locationName ?? this.locationName,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
completed: completed ?? this.completed,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
isDirty: isDirty ?? this.isDirty,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'owner_id': ownerId,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'progress': progress,
|
||||
'location_lat': locationLat,
|
||||
'location_lng': locationLng,
|
||||
'location_name': locationName,
|
||||
'image_url': imageUrl,
|
||||
'completed': completed,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
factory CachedGoal.fromJson(Map<String, dynamic> json) {
|
||||
return CachedGoal(
|
||||
id: json['id'] as String,
|
||||
ownerId: json['owner_id'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String?,
|
||||
progress: json['progress'] as int,
|
||||
locationLat: json['location_lat'] as double?,
|
||||
locationLng: json['location_lng'] as double?,
|
||||
locationName: json['location_name'] as String?,
|
||||
imageUrl: json['image_url'] as String?,
|
||||
completed: json['completed'] as bool,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class CalendarEntry extends Equatable {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String? goalId;
|
||||
final DateTime entryDate;
|
||||
final String title;
|
||||
final String? note;
|
||||
final String entryType; // e.g. progress, milestone, reflection
|
||||
final DateTime createdAt;
|
||||
|
||||
const CalendarEntry({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
this.goalId,
|
||||
required this.entryDate,
|
||||
required this.title,
|
||||
this.note,
|
||||
required this.entryType,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
CalendarEntry copyWith({
|
||||
String? id,
|
||||
String? userId,
|
||||
String? goalId,
|
||||
DateTime? entryDate,
|
||||
String? title,
|
||||
String? note,
|
||||
String? entryType,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return CalendarEntry(
|
||||
id: id ?? this.id,
|
||||
userId: userId ?? this.userId,
|
||||
goalId: goalId ?? this.goalId,
|
||||
entryDate: entryDate ?? this.entryDate,
|
||||
title: title ?? this.title,
|
||||
note: note ?? this.note,
|
||||
entryType: entryType ?? this.entryType,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'user_id': userId,
|
||||
'goal_id': goalId,
|
||||
'entry_date': entryDate.toIso8601String().split('T').first,
|
||||
'title': title,
|
||||
'note': note,
|
||||
'entry_type': entryType,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
factory CalendarEntry.fromJson(Map<String, dynamic> json) {
|
||||
return CalendarEntry(
|
||||
id: json['id'] as String,
|
||||
userId: json['user_id'] as String,
|
||||
goalId: json['goal_id'] as String?,
|
||||
entryDate: DateTime.parse(json['entry_date'] as String),
|
||||
title: json['title'] as String,
|
||||
note: json['note'] as String?,
|
||||
entryType: json['entry_type'] as String? ?? 'note',
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
userId,
|
||||
goalId,
|
||||
entryDate,
|
||||
title,
|
||||
note,
|
||||
entryType,
|
||||
createdAt,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
enum MutationType {
|
||||
createGoal,
|
||||
updateGoal,
|
||||
deleteGoal,
|
||||
updateGoalProgress,
|
||||
}
|
||||
|
||||
class OfflineMutation {
|
||||
final String id;
|
||||
final MutationType type;
|
||||
final String? goalId;
|
||||
final Map<String, dynamic>? data;
|
||||
final DateTime createdAt;
|
||||
final DateTime? syncedAt;
|
||||
final bool isSynced;
|
||||
|
||||
OfflineMutation({
|
||||
required this.id,
|
||||
required this.type,
|
||||
this.goalId,
|
||||
this.data,
|
||||
required this.createdAt,
|
||||
this.syncedAt,
|
||||
this.isSynced = false,
|
||||
});
|
||||
|
||||
OfflineMutation copyWith({
|
||||
String? id,
|
||||
MutationType? type,
|
||||
String? goalId,
|
||||
Map<String, dynamic>? data,
|
||||
DateTime? createdAt,
|
||||
DateTime? syncedAt,
|
||||
bool? isSynced,
|
||||
}) {
|
||||
return OfflineMutation(
|
||||
id: id ?? this.id,
|
||||
type: type ?? this.type,
|
||||
goalId: goalId ?? this.goalId,
|
||||
data: data ?? this.data,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
syncedAt: syncedAt ?? this.syncedAt,
|
||||
isSynced: isSynced ?? this.isSynced,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'type': type.name,
|
||||
'goal_id': goalId,
|
||||
'data': data,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'synced_at': syncedAt?.toIso8601String(),
|
||||
'is_synced': isSynced,
|
||||
};
|
||||
}
|
||||
|
||||
factory OfflineMutation.fromJson(Map<String, dynamic> json) {
|
||||
return OfflineMutation(
|
||||
id: json['id'] as String,
|
||||
type: MutationType.values.firstWhere(
|
||||
(e) => e.name == json['type'] as String,
|
||||
),
|
||||
goalId: json['goal_id'] as String?,
|
||||
data: json['data'] as Map<String, dynamic>?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
syncedAt: json['synced_at'] != null
|
||||
? DateTime.parse(json['synced_at'] as String)
|
||||
: null,
|
||||
isSynced: json['is_synced'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
static OfflineMutation createGoalMutation({
|
||||
required String goalId,
|
||||
required Map<String, dynamic> goalData,
|
||||
}) {
|
||||
return OfflineMutation(
|
||||
id: const Uuid().v4(),
|
||||
type: MutationType.createGoal,
|
||||
goalId: goalId,
|
||||
data: goalData,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
static OfflineMutation updateGoalMutation({
|
||||
required String goalId,
|
||||
required Map<String, dynamic> goalData,
|
||||
}) {
|
||||
return OfflineMutation(
|
||||
id: const Uuid().v4(),
|
||||
type: MutationType.updateGoal,
|
||||
goalId: goalId,
|
||||
data: goalData,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
static OfflineMutation deleteGoalMutation({
|
||||
required String goalId,
|
||||
}) {
|
||||
return OfflineMutation(
|
||||
id: const Uuid().v4(),
|
||||
type: MutationType.deleteGoal,
|
||||
goalId: goalId,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
static OfflineMutation updateProgressMutation({
|
||||
required String goalId,
|
||||
required int progress,
|
||||
}) {
|
||||
return OfflineMutation(
|
||||
id: const Uuid().v4(),
|
||||
type: MutationType.updateGoalProgress,
|
||||
goalId: goalId,
|
||||
data: {'progress': progress},
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ class User extends Equatable {
|
||||
final String? avatarUrl;
|
||||
final String? bio;
|
||||
final bool isPublicProfile;
|
||||
final String? twitterHandle;
|
||||
final String? instagramHandle;
|
||||
final String? tiktokHandle;
|
||||
final String? websiteUrl;
|
||||
final DateTime? countdownStartDate;
|
||||
final DateTime? countdownEndDate;
|
||||
final DateTime createdAt;
|
||||
@@ -19,6 +23,10 @@ class User extends Equatable {
|
||||
this.avatarUrl,
|
||||
this.bio,
|
||||
this.isPublicProfile = false,
|
||||
this.twitterHandle,
|
||||
this.instagramHandle,
|
||||
this.tiktokHandle,
|
||||
this.websiteUrl,
|
||||
this.countdownStartDate,
|
||||
this.countdownEndDate,
|
||||
required this.createdAt,
|
||||
@@ -44,6 +52,10 @@ class User extends Equatable {
|
||||
String? avatarUrl,
|
||||
String? bio,
|
||||
bool? isPublicProfile,
|
||||
String? twitterHandle,
|
||||
String? instagramHandle,
|
||||
String? tiktokHandle,
|
||||
String? websiteUrl,
|
||||
DateTime? countdownStartDate,
|
||||
DateTime? countdownEndDate,
|
||||
DateTime? createdAt,
|
||||
@@ -56,6 +68,10 @@ class User extends Equatable {
|
||||
avatarUrl: avatarUrl ?? this.avatarUrl,
|
||||
bio: bio ?? this.bio,
|
||||
isPublicProfile: isPublicProfile ?? this.isPublicProfile,
|
||||
twitterHandle: twitterHandle ?? this.twitterHandle,
|
||||
instagramHandle: instagramHandle ?? this.instagramHandle,
|
||||
tiktokHandle: tiktokHandle ?? this.tiktokHandle,
|
||||
websiteUrl: websiteUrl ?? this.websiteUrl,
|
||||
countdownStartDate: countdownStartDate ?? this.countdownStartDate,
|
||||
countdownEndDate: countdownEndDate ?? this.countdownEndDate,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
@@ -71,6 +87,10 @@ class User extends Equatable {
|
||||
avatarUrl,
|
||||
bio,
|
||||
isPublicProfile,
|
||||
twitterHandle,
|
||||
instagramHandle,
|
||||
tiktokHandle,
|
||||
websiteUrl,
|
||||
countdownStartDate,
|
||||
countdownEndDate,
|
||||
createdAt,
|
||||
@@ -85,6 +105,10 @@ class User extends Equatable {
|
||||
'avatar_url': avatarUrl,
|
||||
'bio': bio,
|
||||
'is_public_profile': isPublicProfile,
|
||||
'twitter_handle': twitterHandle,
|
||||
'instagram_handle': instagramHandle,
|
||||
'tiktok_handle': tiktokHandle,
|
||||
'website_url': websiteUrl,
|
||||
'countdown_start_date': countdownStartDate?.toIso8601String(),
|
||||
'countdown_end_date': countdownEndDate?.toIso8601String(),
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
@@ -100,6 +124,10 @@ class User extends Equatable {
|
||||
avatarUrl: json['avatar_url'] as String?,
|
||||
bio: json['bio'] as String?,
|
||||
isPublicProfile: json['is_public_profile'] as bool? ?? false,
|
||||
twitterHandle: json['twitter_handle'] as String?,
|
||||
instagramHandle: json['instagram_handle'] as String?,
|
||||
tiktokHandle: json['tiktok_handle'] as String?,
|
||||
websiteUrl: json['website_url'] as String?,
|
||||
countdownStartDate: json['countdown_start_date'] != null
|
||||
? DateTime.parse(json['countdown_start_date'] as String)
|
||||
: null,
|
||||
|
||||
Reference in New Issue
Block a user