diff --git a/lib/router.dart b/lib/router.dart index 4f5069d7..e0c9100e 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -2,11 +2,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:trip_app_nativeapp/core/exception/app_exception.dart'; import 'package:trip_app_nativeapp/features/user/controller/app_user_controller.dart'; import 'package:trip_app_nativeapp/view/pages/debug_page.dart'; import 'package:trip_app_nativeapp/view/pages/error_page.dart'; import 'package:trip_app_nativeapp/view/pages/loading_page.dart'; import 'package:trip_app_nativeapp/view/pages/login_page.dart'; +import 'package:trip_app_nativeapp/view/pages/trips/trip_detail_page.dart'; import 'package:trip_app_nativeapp/view/pages/trips/trips_list_page.dart'; part 'router.g.dart'; @@ -59,6 +61,21 @@ GoRouter router(RouterRef ref) { GoRoute( path: TripListPage.path, builder: (context, state) => const TripListPage(), + routes: [ + GoRoute( + path: TripDetailPage.pathParam, + builder: (context, state) { + final id = int.tryParse(state.params['id'] ?? ''); + if (id != null) { + return TripDetailPage(id); + } else { + return const ErrorPage( + exception: AppException(message: '旅の選択に失敗しました。'), + ); + } + }, + ), + ], ), GoRoute( path: LoginPage.path, diff --git a/lib/view/pages/trips/trip_detail_page.dart b/lib/view/pages/trips/trip_detail_page.dart new file mode 100644 index 00000000..f12ca554 --- /dev/null +++ b/lib/view/pages/trips/trip_detail_page.dart @@ -0,0 +1,121 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:trip_app_nativeapp/core/exception/app_exception.dart'; +import 'package:trip_app_nativeapp/core/extensions/build_context.dart'; +import 'package:trip_app_nativeapp/features/trips/controller/trip_controller.dart'; +import 'package:trip_app_nativeapp/view/pages/trips/trips_list_page.dart'; +import 'package:trip_app_nativeapp/view/widgets/common/car_driving_loading.dart'; +import 'package:trip_app_nativeapp/view/widgets/common/error_cat.dart'; +import 'package:trip_app_nativeapp/view/widgets/trips/trip_belonging_list.dart'; +import 'package:trip_app_nativeapp/view/widgets/trips/trip_overview_card.dart'; +import 'package:trip_app_nativeapp/view/widgets/trips/trip_schedule.dart'; + +class TripDetailPage extends HookConsumerWidget { + const TripDetailPage(this.id, {super.key}); + + static String path({required int id}) => '${TripListPage.path}/$id'; + static const pathParam = ':id'; + static const scheduleTabIndex = 0; + final int id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabIndex = useState(scheduleTabIndex); + final backgroundImageHeight = context.displaySize.width / 16 * 9; + return Scaffold( + body: SafeArea( + top: false, + child: ref.watch(tripsProvider).when( + data: (trips) { + final trip = trips.firstWhereOrNull((trip) => trip.id == id); + if (trip == null) { + return const Center( + child: ErrorCat( + AppException( + code: 'not_found', + message: '選択した旅の予定が見つかりませんでした。', + ), + null, + ), + ); + } + + return NestedScrollView( + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + // TODO(seigi0714): スクロールした場合のみ表示したい。 + title: Text( + trip.title.value, + style: context.textTheme.titleLarge, + ), + expandedHeight: + backgroundImageHeight + TripOverviewCard.height / 2, + flexibleSpace: FlexibleSpaceBar( + background: SizedBox( + height: backgroundImageHeight + + TripOverviewCard.height / 2, + child: Stack( + alignment: Alignment.topCenter, + children: [ + SizedBox( + height: backgroundImageHeight, + child: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: NetworkImage( + 'https://tsunagutabi.com/wp-content/uploads/2020/04/%E6%97%85%E8%A1%8C%E3%83%96%E3%83%AD%E3%82%B0%E3%81%AB%E3%81%8A%E3%81%99%E3%81%99%E3%82%81%E3%81%AE%E3%83%95%E3%83%AA%E3%83%BC%E7%94%BB%E5%83%8F%EF%BC%86%E7%B4%A0%E6%9D%90%E3%82%B5%E3%82%A4%E3%83%88%E3%81%BE%E3%81%A8%E3%82%81.jpg', + ), + fit: BoxFit.cover, + ), + ), + ), + ), + Positioned( + top: backgroundImageHeight - + TripOverviewCard.height / 2, + child: TripOverviewCard(trip), + ), + ], + ), + ), + ), + pinned: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(48), + child: TabBar( + controller: TabController( + length: 2, + vsync: Scaffold.of(context), + initialIndex: tabIndex.value, + ), + labelColor: Colors.black, + tabs: const [ + Tab( + text: '🗓️日程', + ), + Tab( + text: '🧳持ち物', + ) + ], + onTap: (i) => tabIndex.value = i, + ), + ), + ), + ]; + }, + body: tabIndex.value == scheduleTabIndex + ? const TripSchedule() + : const TripBelongingList(), + ); + }, + error: ErrorCat.new, + loading: CarDrivingLoading.new, + ), + ), + ); + } +} diff --git a/lib/view/widgets/trips/trip_belonging_list.dart b/lib/view/widgets/trips/trip_belonging_list.dart new file mode 100644 index 00000000..2abf5e8a --- /dev/null +++ b/lib/view/widgets/trips/trip_belonging_list.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class TripBelongingList extends StatelessWidget { + const TripBelongingList({super.key}); + + @override + Widget build(BuildContext context) { + return const Center( + child: Text('持ち物リスト'), + ); + } +} diff --git a/lib/view/widgets/trips/trip_card.dart b/lib/view/widgets/trips/trip_card.dart index 6c4103f7..86151c44 100644 --- a/lib/view/widgets/trips/trip_card.dart +++ b/lib/view/widgets/trips/trip_card.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; import 'package:lottie/lottie.dart'; import 'package:trip_app_nativeapp/core/extensions/build_context.dart'; import 'package:trip_app_nativeapp/core/extensions/datetime.dart'; import 'package:trip_app_nativeapp/core/gen/assets.gen.dart'; import 'package:trip_app_nativeapp/features/trips/domain/entity/trip/trip.dart'; +import 'package:trip_app_nativeapp/view/pages/trips/trip_detail_page.dart'; class TripCard extends StatelessWidget { const TripCard(this.trip, {super.key}); @@ -13,39 +15,42 @@ class TripCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 4, - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisSize: MainAxisSize.min, // Columnの高さを最小限にする - children: [ - Expanded( - child: Lottie.asset( - Assets.lotties.tripCard, - height: context.displaySize.height * 0.14, + return GestureDetector( + onTap: () => context.push(TripDetailPage.path(id: trip.id)), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisSize: MainAxisSize.min, // Columnの高さを最小限にする + children: [ + Expanded( + child: Lottie.asset( + Assets.lotties.tripCard, + height: context.displaySize.height * 0.14, + ), + ), + Text( + trip.title.value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.titleLarge, + ), + const Gap(8), + Text( + '🛫 ${trip.period.fromDate.toJsonDateString()}', + style: context.textTheme.titleMedium, + ), + const Gap(8), + Text( + '${trip.period.endDate.toJsonDateString()} 🔚', + style: context.textTheme.titleMedium, ), - ), - Text( - trip.title.value, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleLarge, - ), - const Gap(8), - Text( - '🛫 ${trip.period.fromDate.toJsonDateString()}', - style: context.textTheme.titleMedium, - ), - const Gap(8), - Text( - '${trip.period.endDate.toJsonDateString()} 🔚', - style: context.textTheme.titleMedium, - ), - ], + ], + ), ), ), ); diff --git a/lib/view/widgets/trips/trip_overview_card.dart b/lib/view/widgets/trips/trip_overview_card.dart new file mode 100644 index 00000000..d27af3ff --- /dev/null +++ b/lib/view/widgets/trips/trip_overview_card.dart @@ -0,0 +1,89 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:trip_app_nativeapp/core/extensions/build_context.dart'; +import 'package:trip_app_nativeapp/core/extensions/datetime.dart'; +import 'package:trip_app_nativeapp/features/trips/domain/entity/trip/trip.dart'; + +class TripOverviewCard extends StatelessWidget { + const TripOverviewCard(this.trip, {super.key}); + + final ExistingTrip trip; + static const height = 150.0; + + @override + Widget build(BuildContext context) { + return Center( + child: SizedBox( + height: height, + width: context.displaySize.width * 0.95, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 4, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + alignment: Alignment.centerLeft, + child: Text( + trip.title.value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.headlineMedium, + ), + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '🛫 ${trip.period.fromDate.toJsonDateString()}', + style: context.textTheme.titleMedium, + ), + Text( + '${trip.period.endDate.toJsonDateString()} 🔚', + style: context.textTheme.titleMedium, + ), + ], + ), + ), + SizedBox( + width: 36, + height: 36, + child: IconButton( + onPressed: () => log('share'), + icon: const Icon( + Icons.share, + ), + ), + ), + SizedBox( + width: 36, + height: 36, + child: IconButton( + onPressed: () => log('edit'), + icon: const Icon( + Icons.edit, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/view/widgets/trips/trip_schedule.dart b/lib/view/widgets/trips/trip_schedule.dart new file mode 100644 index 00000000..b331bdfb --- /dev/null +++ b/lib/view/widgets/trips/trip_schedule.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class TripSchedule extends StatelessWidget { + const TripSchedule({super.key}); + + @override + Widget build(BuildContext context) { + return const Center( + child: Text('スケジュール'), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index e1d40319..a39bdcbd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -153,14 +153,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" cli_util: dependency: transitive description: name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.4.0" clock: dependency: transitive description: @@ -178,7 +186,7 @@ packages: source: hosted version: "4.4.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 @@ -245,26 +253,26 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "32648ef4f1dda618d98a22bc958fa79d479895891061e63338bec510b67e821a" + sha256: e87176016465263daf10c209df1f50a52e9e46e7612fab7462da1e6d984638f6 url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.3.4" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "5d472a901d07ab5ba0239262c340d29930aa2cfd0c73068aa4d1142a353ffee4" + sha256: "6b5e20a249f2f7f15d098f802fe736b72cac14cddccdfcb46027e1b401deec9f" url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.3.4" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: cc20ce83432675abcc109b766aad2e420946eaeecc32ffd34bada389cdaa9a74 + sha256: f42f688bc26bdf4c081e011ba27a00439f17c20d9aeca4312f8022e577f8363f url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.3.4" dart_jsonwebtoken: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8e8e2729..4109a586 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ environment: sdk: '>=2.18.0 <3.0.0' dependencies: + collection: ^1.17.0 connectivity_plus: ^3.0.2 cupertino_icons: ^1.0.2 device_preview: ^1.1.0 @@ -42,7 +43,7 @@ dependencies: dev_dependencies: build_runner: ^2.3.0 - custom_lint: ^0.3.2 + custom_lint: ^0.3.4 flutter_lints: ^2.0.0 flutter_test: sdk: flutter