-
Notifications
You must be signed in to change notification settings - Fork 4
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
#157 感想投稿ページの実装 #201
base: main
Are you sure you want to change the base?
#157 感想投稿ページの実装 #201
Changes from all commits
bad77be
4dd4f18
b797a5a
05a7978
b25defa
d693dcf
4868894
5aa42f2
399451b
170b850
aef553e
bdf6e7a
f36dc0a
46d427d
075ee07
c75ea41
f55b19e
b40d11f
4e62a7f
3c41528
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 |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import 'package:auto_route/auto_route.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||
|
||
import '../../../auth/ui/auth_dependent_builder.dart'; | ||
import 'review_form.dart'; | ||
|
||
/// `Job` に紐づく `Review` (=感想)の投稿画面。 | ||
@RoutePage() | ||
class ReviewCreatePage extends ConsumerWidget { | ||
const ReviewCreatePage({ | ||
@PathParam('jobId') required this.jobId, | ||
super.key, | ||
}); | ||
|
||
/// [AutoRoute] で指定するパス文字列。 | ||
static const path = '/jobs/:jobId/reviews/create'; | ||
|
||
/// [ReviewCreatePage] に遷移する際に `context.router.pushNamed` で指定する文字列。 | ||
static String location({required String jobId}) => | ||
'/jobs/$jobId/reviews/create'; | ||
|
||
final String jobId; | ||
|
||
@override | ||
Widget build(BuildContext context, WidgetRef ref) { | ||
return Scaffold( | ||
appBar: AppBar(title: const Text('感想を投稿する')), | ||
body: AuthDependentBuilder( | ||
onAuthenticated: (userId) { | ||
return ReviewForm.create( | ||
workerId: userId, | ||
jobId: jobId, | ||
); | ||
}, | ||
), | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import 'package:dart_flutter_common/dart_flutter_common.dart'; | ||
import 'package:firebase_common/firebase_common.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:gap/gap.dart'; | ||
import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||
|
||
import '../../../review/ui/review_controller.dart'; | ||
import '../../../widgets/text_input_section.dart'; | ||
import '../../firebase_storage/firebase_storage.dart'; | ||
import '../../firebase_storage/ui/firebase_storage_controller.dart'; | ||
|
||
/// - `create` の場合、ログイン済みの `workerId`(ユーザー ID)と、対象の `jobId` | ||
/// - `update` の場合、更新対象の [Review] と、対象の`jobId`、本人であることが確認された `workerId`(ユーザー ID) | ||
/// | ||
/// を受け取り、それに応じた [Review] の作成または更新を行うフォーム。 | ||
class ReviewForm extends ConsumerStatefulWidget { | ||
const ReviewForm.create({ | ||
required String workerId, | ||
required String jobId, | ||
super.key, | ||
}) : _jobId = jobId, | ||
_workerId = workerId, | ||
_review = null; | ||
|
||
const ReviewForm.update({ | ||
required String workerId, | ||
required String jobId, | ||
required ReadReview review, | ||
super.key, | ||
}) : _jobId = jobId, | ||
_workerId = workerId, | ||
_review = review; | ||
|
||
final ReadReview? _review; | ||
|
||
final String _workerId; | ||
|
||
final String _jobId; | ||
|
||
@override | ||
ReviewFormState createState() => ReviewFormState(); | ||
} | ||
|
||
class ReviewFormState extends ConsumerState<ReviewForm> { | ||
/// フォームのグローバルキー | ||
final formKey = GlobalKey<FormState>(); | ||
|
||
/// [Review.title] のテキストフィールド用コントローラー | ||
late final TextEditingController _titleController; | ||
|
||
/// [Review.content] のテキストフィールド用コントローラー | ||
late final TextEditingController _contentController; | ||
|
||
/// 画像の高さ。 | ||
static const double _imageHeight = 300; | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
_titleController = TextEditingController(text: widget._review?.title); | ||
_contentController = TextEditingController(text: widget._review?.content); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final firebaseStorageController = | ||
ref.watch(firebaseStorageControllerProvider); | ||
final pickedImageFile = ref.watch(pickedImageFileStateProvider); | ||
final controller = ref.watch(reviewControllerProvider); | ||
return SingleChildScrollView( | ||
child: Column( | ||
crossAxisAlignment: CrossAxisAlignment.start, | ||
children: [ | ||
if (pickedImageFile != null) | ||
GestureDetector( | ||
onTap: firebaseStorageController.pickImageFromGallery, | ||
child: SizedBox( | ||
height: _imageHeight, | ||
child: Center( | ||
child: Image.file(pickedImageFile), | ||
), | ||
), | ||
) | ||
else if ((widget._review?.imageUrl ?? '').isNotEmpty) | ||
GenericImage.rectangle( | ||
onTap: firebaseStorageController.pickImageFromGallery, | ||
showDetailOnTap: false, | ||
imageUrl: pickedImageFile?.path ?? widget._review!.imageUrl, | ||
maxHeight: _imageHeight, | ||
) | ||
else | ||
GestureDetector( | ||
onTap: firebaseStorageController.pickImageFromGallery, | ||
child: Container( | ||
height: _imageHeight, | ||
decoration: BoxDecoration( | ||
border: Border.all(color: Colors.black38), | ||
), | ||
child: const Center(child: Icon(Icons.image)), | ||
), | ||
), | ||
const Gap(32), | ||
Padding( | ||
padding: const EdgeInsets.symmetric(horizontal: 24), | ||
child: Form( | ||
key: formKey, | ||
child: Column( | ||
crossAxisAlignment: CrossAxisAlignment.start, | ||
children: [ | ||
TextInputSection( | ||
title: 'タイトル', | ||
description: '感想のタイトルを最大2行程度で入力してください。', | ||
maxLines: 2, | ||
defaultDisplayLines: 2, | ||
controller: _titleController, | ||
isRequired: true, | ||
), | ||
TextInputSection( | ||
title: '本文', | ||
description: '感想の本文を入力してください。', | ||
defaultDisplayLines: 5, | ||
maxLines: 12, | ||
controller: _contentController, | ||
isRequired: true, | ||
), | ||
Padding( | ||
padding: const EdgeInsets.only(top: 16, bottom: 32), | ||
child: Center( | ||
child: ElevatedButton( | ||
onPressed: () { | ||
final isValidate = formKey.currentState?.validate(); | ||
if (!(isValidate ?? true)) { | ||
return; | ||
} | ||
|
||
final review = widget._review; | ||
|
||
if (review != null) { | ||
controller.updateReview( | ||
reviewId: review.reviewId, | ||
workerId: widget._workerId, | ||
title: _titleController.text, | ||
content: _contentController.text, | ||
imageFile: pickedImageFile, | ||
); | ||
} else { | ||
controller.create( | ||
workerId: widget._workerId, | ||
jobId: widget._jobId, | ||
title: _titleController.text, | ||
content: _contentController.text, | ||
imageFile: pickedImageFile, | ||
); | ||
} | ||
//TODO: 登録 or 更新完了の旨をユーザーに示すUIが必要か? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 現状だと、登録 or 更新完了した際に何もアクションがないので、ユーザーに何かしら通知する There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @haterain0203 ありがとうございます!いったん OK です!たんに SnackBar を出せば良さそうかなと思います! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kosukesaigusa |
||
}, | ||
child: const Text('この内容で登録する'), | ||
), | ||
), | ||
), | ||
], | ||
), | ||
), | ||
), | ||
], | ||
), | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import 'package:auto_route/auto_route.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||
|
||
import '../../../auth/ui/auth_dependent_builder.dart'; | ||
import '../../../review/review.dart'; | ||
import 'review_form.dart'; | ||
|
||
/// `Job` に紐づく `Review` (=感想)の更新画面。 | ||
@RoutePage() | ||
class ReviewUpdatePage extends ConsumerWidget { | ||
const ReviewUpdatePage({ | ||
@PathParam('jobId') required this.jobId, | ||
@PathParam('reviewId') required this.reviewId, | ||
super.key, | ||
}); | ||
|
||
/// [AutoRoute] で指定するパス文字列。 | ||
static const path = '/jobs/:jobId/reviews/:reviewId/update'; | ||
|
||
/// [ReviewUpdatePage] に遷移する際に `context.router.pushNamed` で指定する文字列。 | ||
static String location({required String jobId, required String reviewId}) => | ||
'/jobs/$jobId/reviews/$reviewId/update'; | ||
|
||
final String jobId; | ||
|
||
final String reviewId; | ||
|
||
@override | ||
Widget build(BuildContext context, WidgetRef ref) { | ||
final asyncValue = ref.watch(reviewFutureProvider(reviewId)); | ||
final review = asyncValue.valueOrNull; | ||
final isLoading = asyncValue.isLoading; | ||
Comment on lines
+31
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 更新時はパスパラメータの There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. インスタンスごと渡すという考え方ですね!読み取り回数を減らすという意味ではいいアイディアですし、実際にそうするプロジェクトもよくあると思います!が、
という理由でこれでいいと思います! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kosukesaigusa |
||
if (review == null) { | ||
return Scaffold( | ||
appBar: AppBar(title: const Text('感想を更新する')), | ||
body: isLoading | ||
? const Center(child: CircularProgressIndicator()) | ||
: const Center(child: Text('感想が存在しません。')), | ||
); | ||
} | ||
return Scaffold( | ||
appBar: AppBar(title: const Text('感想を更新する')), | ||
body: AuthDependentBuilder( | ||
onAuthenticated: (userId) { | ||
return ReviewForm.update( | ||
workerId: userId, | ||
jobId: jobId, | ||
review: review, | ||
); | ||
}, | ||
), | ||
); | ||
} | ||
} |
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.
Job などを参考にしていただいたと思うのですが、簡便化のために辞める予定です!いまはこのままでいいです!