Skip to content
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

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,42 @@ class ReviewRepository {
.orderBy('createdAt', descending: true),
);
}

/// 指定した [Review] を取得する。
Future<ReadReview?> fetchReview({required String reviewId}) =>
_query.fetchDocument(reviewId: reviewId);

/// [Review] を作成する。
Future<void> create({
required String workerId,
required String jobId,
required String imageUrl,
required String title,
required String content,
}) =>
_query.add(
createReview: CreateReview(
workerId: workerId,
jobId: jobId,
title: title,
content: content,
imageUrl: imageUrl,
),
);

/// [Review] を更新する。
Future<void> update({
required String reviewId,
String? title,
String? content,
String? imageUrl,
}) =>
_query.update(
reviewId: reviewId,
updateReview: UpdateReview(
title: title,
content: content,
imageUrl: imageUrl,
),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import '../../geoflutterfire_plus/geoflutterfire_plus.dart';
import '../../image_detail_view/ui/image_detail_view_stub.dart';
import '../../image_picker/ui/image_picker_sample.dart';
import '../../in_review/ui/in_review.dart';
import '../../review/ui/review_create.dart';
import '../../review/ui/review_update.dart';
import '../../sample_todo/ui/todos.dart';
import '../../sign_in/ui/sign_in.dart';
import '../../user_fcm_token/ui/user_fcm_token.dart';
Expand Down Expand Up @@ -138,6 +140,25 @@ class DevelopmentItemsPage extends ConsumerWidget {
HostUpdatePage.location(hostId: 'b1M4bcp7zEVpgHXYhOVWt8BMkq23'),
),
),
ListTile(
title: const Text(
'感想投稿ページ',
),
onTap: () => context.router.pushNamed(
ReviewCreatePage.location(jobId: 'PYRsrMSOApEgZ6lzMuUK'),
),
),
ListTile(
title: const Text(
'感想更新ページ',
),
onTap: () => context.router.pushNamed(
ReviewUpdatePage.location(
jobId: 'PYRsrMSOApEgZ6lzMuUK',
reviewId: '2TPCqigw8xTvju9t6hAh',
),
),
),
const Divider(),
Padding(
padding: const EdgeInsets.all(16),
Expand Down
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,
);
},
),
);
}
}
169 changes: 169 additions & 0 deletions packages/mottai_flutter_app/lib/development/review/ui/review_form.dart
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>();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Job などを参考にしていただいたと思うのですが、簡便化のために辞める予定です!いまはこのままでいいです!


/// [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が必要か?
Copy link
Collaborator Author

@haterain0203 haterain0203 Sep 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

現状だと、登録 or 更新完了した際に何もアクションがないので、ユーザーに何かしら通知する
といった対応が必要かと思うのですが、これは別issueで対応でも良さそうでしょうか?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@haterain0203 ありがとうございます!いったん OK です!たんに SnackBar を出せば良さそうかなと思います!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

更新時はパスパラメータの reviewId を使って該当の Review を取得していますが、
更新する場合、前の画面ですでに Review を取得済みの場合が多いかと思うので、Review ごと渡すことで都度の取得処理が不要になる?かと思ったのですが、どうでしょうか・・・?
(そもそもauto_routeでそういったことができないかもですが)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@haterain0203

インスタンスごと渡すという考え方ですね!読み取り回数を減らすという意味ではいいアイディアですし、実際にそうするプロジェクトもよくあると思います!が、

  • 一応ここで明示的に取得することで最新であることを担保する
  • このページは関係ないですが、auto_route を使ってどんな画面にもパスパラメータ、クエリパラメータで必要な情報とともに遷移できるようにしているので、プッシュ通知をタップして特定の画面に飛びたい時(つまり Dart のインスタンスは渡せない)や、Flutter Web で URL を直接入力して特定の画面を開きたい時(同じく Dart のインスタンスは渡せない)にも対応できるようにする方針で統一している

という理由でこれでいいと思います!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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,
);
},
),
);
}
}
Loading
Loading