Skip to content

Commit 91d9c83

Browse files
authored
Merge pull request #20 from jnnabugwu/dev
feat: complete!
2 parents f15217a + c38cbed commit 91d9c83

File tree

13 files changed

+511
-152
lines changed

13 files changed

+511
-152
lines changed

lib/core/config/supabase_config.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class SupabaseConfig {
1313
} catch (e) {
1414
debugPrint('❌ Error getting Supabase URL: $e');
1515
// Return fallback directly here as a last resort
16-
return 'https://kszcjniwbqxyndpsajhr.supabase.co/';
16+
return 'https://kszcjniwbqxyndpsajhr.supabase.co';
1717
}
1818
}
1919

lib/core/di/injection.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import '../../features/profile/domain/usecases/update_profile_usecase.dart';
1414
import '../../features/profile/domain/usecases/upload_profile_image_usecase.dart';
1515
import '../../features/profile/presentation/bloc/profile_bloc.dart';
1616
import '../../features/profile/presentation/bloc/edit_profile_bloc.dart';
17+
import '../../features/profile/presentation/bloc/upload_image_bloc.dart';
18+
import '../../features/profile/data/services/image_picker_service.dart';
1719
import '../network/network_info.dart';
1820

1921
// Feature - Athletes
@@ -87,6 +89,13 @@ Future<void> _initProfile() async {
8789
updateProfileUseCase: sl<UpdateProfileUseCase>(),
8890
));
8991

92+
sl.registerFactory(() => UploadImageBloc(
93+
uploadProfileImageUseCase: sl<UploadProfileImageUseCase>(),
94+
));
95+
96+
// Services
97+
sl.registerLazySingleton(() => ImagePickerService());
98+
9099
// Use cases
91100
sl.registerLazySingleton(() => GetProfileUseCase());
92101
sl.registerLazySingleton(() => UpdateProfileUseCase(sl<ProfileRepository>()));

lib/features/profile/data/datasources/profile_remote_data_source.dart

Whitespace-only changes.

lib/features/profile/data/datasources/profile_remote_datasource.dart

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ class ProfileRemoteDataSourceImpl implements ProfileRemoteDataSource {
213213
debugPrint('ProfileRemoteDataSource: Error updating profile: $e');
214214
throw ServerException(message: e.toString(), statusCode: 500);
215215
}
216-
}
216+
}
217217

218218
/// Format data for Supabase compatibility
219219
Map<String, dynamic> _formatDataForSupabase(Map<String, dynamic> athleteData) {
@@ -242,17 +242,6 @@ class ProfileRemoteDataSourceImpl implements ProfileRemoteDataSource {
242242
'sport': athleteData['sport'],
243243
};
244244

245-
// Log field transformations
246-
debugPrint('_formatDataForSupabase: id = ${athleteData['id']}');
247-
debugPrint('_formatDataForSupabase: name -> full_name = ${athleteData['name']}');
248-
debugPrint('_formatDataForSupabase: email = ${athleteData['email']}');
249-
debugPrint('_formatDataForSupabase: username = $username');
250-
debugPrint('_formatDataForSupabase: status -> athlete_status = ${athleteData['status']?.toString()?.split('.')?.last}');
251-
debugPrint('_formatDataForSupabase: major = ${athleteData['major']}');
252-
debugPrint('_formatDataForSupabase: career = ${athleteData['career']}');
253-
debugPrint('_formatDataForSupabase: university -> college = ${athleteData['university']}');
254-
debugPrint('_formatDataForSupabase: sport = ${athleteData['sport']}');
255-
256245
// If no username, use a default based on auth ID to avoid database errors
257246
if (username == null) {
258247
String defaultUsername = "user_${athleteData['id'].toString().substring(0, 8)}";
@@ -328,25 +317,35 @@ class ProfileRemoteDataSourceImpl implements ProfileRemoteDataSource {
328317
try {
329318
debugPrint('ProfileRemoteDataSource: Uploading profile image for ID: $actualAthleteId');
330319

331-
// Format the file path: profiles/user-id/filename
332-
final filePath = 'profiles/$actualAthleteId/$fileName';
320+
// Format the file path without leading slash
321+
final filePath = '${actualAthleteId.trim()}/${fileName.trim()}'.replaceAll('//', '/');
322+
323+
debugPrint(filePath);
333324

334325
// Upload the image to Supabase Storage
335-
final response = await supabaseClient
326+
327+
await supabaseClient
336328
.storage
337-
.from('athlete_images') // Bucket name
329+
.from('athlete-images') // Bucket name
338330
.uploadBinary(filePath, imageBytes, fileOptions: const FileOptions(
339331
cacheControl: '3600',
340332
upsert: true
341333
));
342334

343-
// Get the public URL for the uploaded image
344-
final imageUrl = supabaseClient
335+
// Get the signed URL for the uploaded image
336+
final String imageUrl = await supabaseClient
345337
.storage
346-
.from('athlete_images')
347-
.getPublicUrl(filePath);
338+
.from('athlete-images')
339+
.createSignedUrl(filePath,60);
340+
// Ensure no double slashes in the path part
341+
342+
// Add https:// back to the start if it was removed
343+
// final String finalImageUrl = imageUrl.startsWith('https:/')
344+
// ? imageUrl.replaceFirst('https:/', 'https://')
345+
// : imageUrl;
348346

349-
debugPrint('ProfileRemoteDataSource: Image uploaded: $imageUrl');
347+
debugPrint('ProfileRemoteDataSource: Image uploaded. Raw URL: $imageUrl');
348+
// debugPrint('ProfileRemoteDataSource: Final URL: $finalImageUrl');
350349

351350
// Update the athlete's profile with the new image URL
352351
try {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import 'dart:typed_data';
2+
import 'package:flutter/foundation.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:image_picker/image_picker.dart';
5+
import 'dart:io';
6+
7+
class ImagePickerService {
8+
final ImagePicker _picker = ImagePicker();
9+
10+
// Pick image from gallery
11+
Future<ImageResult?> pickImageFromGallery() async {
12+
try {
13+
final XFile? pickedFile = await _picker.pickImage(
14+
source: ImageSource.gallery,
15+
maxWidth: 800,
16+
maxHeight: 800,
17+
imageQuality: 85,
18+
);
19+
20+
if (pickedFile == null) return null;
21+
22+
return _processPickedImage(pickedFile);
23+
} catch (e) {
24+
debugPrint('Error picking image from gallery: $e');
25+
return null;
26+
}
27+
}
28+
29+
// Take image from camera
30+
Future<ImageResult?> takeImageFromCamera() async {
31+
try {
32+
final XFile? pickedFile = await _picker.pickImage(
33+
source: ImageSource.camera,
34+
maxWidth: 800,
35+
maxHeight: 800,
36+
imageQuality: 85,
37+
);
38+
39+
if (pickedFile == null) return null;
40+
41+
return _processPickedImage(pickedFile);
42+
} catch (e) {
43+
debugPrint('Error taking image with camera: $e');
44+
return null;
45+
}
46+
}
47+
48+
// Process the picked image into a format we can use
49+
Future<ImageResult> _processPickedImage(XFile pickedFile) async {
50+
// Get the file name
51+
final String fileName = pickedFile.name.isNotEmpty
52+
? pickedFile.name
53+
: 'profile_${DateTime.now().millisecondsSinceEpoch}.jpg';
54+
55+
Uint8List imageBytes;
56+
// For web, we already have bytes from XFile
57+
if (kIsWeb) {
58+
imageBytes = await pickedFile.readAsBytes();
59+
} else {
60+
// For mobile, read the file
61+
final File imageFile = File(pickedFile.path);
62+
imageBytes = await imageFile.readAsBytes();
63+
}
64+
65+
return ImageResult(
66+
fileName: fileName,
67+
imageBytes: imageBytes,
68+
filePath: pickedFile.path,
69+
);
70+
}
71+
72+
// Show a dialog to choose between gallery and camera
73+
Future<ImageResult?> showImageSourceDialog(BuildContext context) async {
74+
final ImageSource? source = await showDialog<ImageSource>(
75+
context: context,
76+
builder: (BuildContext context) {
77+
return AlertDialog(
78+
title: const Text('Select Image Source'),
79+
content: Column(
80+
mainAxisSize: MainAxisSize.min,
81+
children: [
82+
ListTile(
83+
leading: const Icon(Icons.photo_library),
84+
title: const Text('Gallery'),
85+
onTap: () => Navigator.of(context).pop(ImageSource.gallery),
86+
),
87+
ListTile(
88+
leading: const Icon(Icons.camera_alt),
89+
title: const Text('Camera'),
90+
onTap: () => Navigator.of(context).pop(ImageSource.camera),
91+
),
92+
],
93+
),
94+
);
95+
},
96+
);
97+
98+
if (source == null) return null;
99+
100+
return source == ImageSource.gallery
101+
? await pickImageFromGallery()
102+
: await takeImageFromCamera();
103+
}
104+
}
105+
106+
// Class to hold the result of image picking
107+
class ImageResult {
108+
final String fileName;
109+
final Uint8List imageBytes;
110+
final String filePath;
111+
112+
ImageResult({
113+
required this.fileName,
114+
required this.imageBytes,
115+
required this.filePath,
116+
});
117+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import 'package:bloc/bloc.dart';
2+
import 'package:equatable/equatable.dart';
3+
import 'package:flutter/material.dart';
4+
import 'dart:typed_data';
5+
import '../../domain/usecases/upload_profile_image_usecase.dart';
6+
7+
part 'upload_image_event.dart';
8+
part 'upload_image_state.dart';
9+
10+
class UploadImageBloc extends Bloc<UploadImageEvent, UploadImageState> {
11+
final UploadProfileImageUseCase uploadProfileImageUseCase;
12+
13+
UploadImageBloc({
14+
required this.uploadProfileImageUseCase,
15+
}) : super(UploadImageInitial()) {
16+
on<UploadProfileImageEvent>(_onUploadProfileImage);
17+
on<ResetUploadStateEvent>(_onResetUploadState);
18+
}
19+
20+
Future<void> _onUploadProfileImage(
21+
UploadProfileImageEvent event,
22+
Emitter<UploadImageState> emit,
23+
) async {
24+
emit(UploadImageLoading());
25+
26+
try {
27+
final params = UploadImageParams(
28+
athleteId: event.athleteId,
29+
imageBytes: event.imageBytes,
30+
fileName: event.fileName,
31+
);
32+
33+
final result = await uploadProfileImageUseCase(params);
34+
35+
result.fold(
36+
(failure) => emit(UploadImageFailure(message: failure.message)),
37+
(imageUrl) => emit(UploadImageSuccess(imageUrl: imageUrl)),
38+
);
39+
} catch (e) {
40+
emit(UploadImageFailure(message: e.toString()));
41+
}
42+
}
43+
44+
void _onResetUploadState(
45+
ResetUploadStateEvent event,
46+
Emitter<UploadImageState> emit,
47+
) {
48+
emit(UploadImageInitial());
49+
}
50+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
part of 'upload_image_bloc.dart';
2+
3+
abstract class UploadImageEvent extends Equatable {
4+
const UploadImageEvent();
5+
6+
@override
7+
List<Object> get props => [];
8+
}
9+
10+
class UploadProfileImageEvent extends UploadImageEvent {
11+
final String athleteId;
12+
final Uint8List imageBytes;
13+
final String fileName;
14+
15+
const UploadProfileImageEvent({
16+
required this.athleteId,
17+
required this.imageBytes,
18+
required this.fileName,
19+
});
20+
21+
@override
22+
List<Object> get props => [athleteId, imageBytes, fileName];
23+
}
24+
25+
class ResetUploadStateEvent extends UploadImageEvent {}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
part of 'upload_image_bloc.dart';
2+
3+
abstract class UploadImageState extends Equatable {
4+
const UploadImageState();
5+
6+
@override
7+
List<Object?> get props => [];
8+
}
9+
10+
class UploadImageInitial extends UploadImageState {}
11+
12+
class UploadImageLoading extends UploadImageState {}
13+
14+
class UploadImageSuccess extends UploadImageState {
15+
final String imageUrl;
16+
17+
const UploadImageSuccess({required this.imageUrl});
18+
19+
@override
20+
List<Object?> get props => [imageUrl];
21+
}
22+
23+
class UploadImageFailure extends UploadImageState {
24+
final String message;
25+
26+
const UploadImageFailure({required this.message});
27+
28+
@override
29+
List<Object?> get props => [message];
30+
}

0 commit comments

Comments
 (0)