diff --git a/assets/imgs/common/icon_cal.png b/assets/imgs/common/icon_cal.png new file mode 100644 index 0000000..3d879ef Binary files /dev/null and b/assets/imgs/common/icon_cal.png differ diff --git a/assets/imgs/common/icon_pay.png b/assets/imgs/common/icon_pay.png new file mode 100644 index 0000000..c2219a6 Binary files /dev/null and b/assets/imgs/common/icon_pay.png differ diff --git a/assets/imgs/common/icon_pin.png b/assets/imgs/common/icon_pin.png new file mode 100644 index 0000000..34c773d Binary files /dev/null and b/assets/imgs/common/icon_pin.png differ diff --git a/assets/imgs/home/icon_chat.png b/assets/imgs/home/icon_chat.png new file mode 100644 index 0000000..8da8a75 Binary files /dev/null and b/assets/imgs/home/icon_chat.png differ diff --git a/assets/imgs/home/icon_community.png b/assets/imgs/home/icon_community.png new file mode 100644 index 0000000..e1158bb Binary files /dev/null and b/assets/imgs/home/icon_community.png differ diff --git a/assets/imgs/home/icon_home.png b/assets/imgs/home/icon_home.png new file mode 100644 index 0000000..937dbaa Binary files /dev/null and b/assets/imgs/home/icon_home.png differ diff --git a/assets/imgs/home/icon_pin.png b/assets/imgs/home/icon_pin.png new file mode 100644 index 0000000..b66ee77 Binary files /dev/null and b/assets/imgs/home/icon_pin.png differ diff --git a/assets/imgs/home/icon_user.png b/assets/imgs/home/icon_user.png new file mode 100644 index 0000000..d8a6565 Binary files /dev/null and b/assets/imgs/home/icon_user.png differ diff --git a/lib/core/constants/images.dart b/lib/core/constants/images.dart index ae606c2..e2bc61a 100644 --- a/lib/core/constants/images.dart +++ b/lib/core/constants/images.dart @@ -11,5 +11,8 @@ abstract class PNDImages { static const String chat = _homePath + 'icon_chat.png'; static const String user = _homePath + 'icon_user.png'; - static const String locationPin = _homePath + 'icon_pin.png'; + static const String _commonPath = _basePath + 'common/'; + static const String location = _commonPath + 'icon_pin.png'; + static const String payment = _commonPath + 'icon_pay.png'; + static const String calander = _commonPath + 'icon_cal.png'; } diff --git a/lib/core/constants/sizes.dart b/lib/core/constants/sizes.dart index 16a122f..007d084 100644 --- a/lib/core/constants/sizes.dart +++ b/lib/core/constants/sizes.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; /// Constant sizes to be used in the app (paddings, gaps, rounded corners etc.) class PNDSizes { + static const p2 = 2.0; static const p4 = 4.0; static const p8 = 8.0; static const p12 = 12.0; @@ -14,6 +15,7 @@ class PNDSizes { } /// Constant gap widths +const gapW2 = SizedBox(width: PNDSizes.p2); const gapW4 = SizedBox(width: PNDSizes.p4); const gapW8 = SizedBox(width: PNDSizes.p8); const gapW12 = SizedBox(width: PNDSizes.p12); diff --git a/lib/core/constants/text_style.dart b/lib/core/constants/text_style.dart index 4570b2c..1675830 100644 --- a/lib/core/constants/text_style.dart +++ b/lib/core/constants/text_style.dart @@ -28,20 +28,20 @@ abstract class AppTextStyle { fontWeight: FontWeight.w800, ); - static final TextStyle headlineBold1 = pretendardBoldStyle(24, 33); - static final TextStyle headlineBold2 = pretendardBoldStyle(20, 24); + static final TextStyle headlineBold1 = pretendardSemiBoldStyle(22, 29); + static final TextStyle headlineBold2 = pretendardSemiBoldStyle(20, 24); - static final TextStyle headlineRegular1 = pretendardRegularStyle(24, 33); - static final TextStyle headlineRegular2 = pretendardRegularStyle(20, 24); - - static final TextStyle headlineMedium1 = pretendardMediumStyle(24, 33); + static final TextStyle headlineMedium1 = pretendardMediumStyle(22, 29); static final TextStyle headlineMedium2 = pretendardMediumStyle(20, 24); + static final TextStyle headlineRegular1 = pretendardRegularStyle(22, 29); + static final TextStyle headlineRegular2 = pretendardRegularStyle(20, 24); + static final TextStyle bodyBold1 = pretendardSemiBoldStyle(16, 19); - static final TextStyle bodyBold2 = pretendardRegularStyle(14, 17); - static final TextStyle bodyBold3 = pretendardRegularStyle(12, 14); + static final TextStyle bodyBold2 = pretendardSemiBoldStyle(14, 17); + static final TextStyle bodyBold3 = pretendardSemiBoldStyle(12, 14); - static final TextStyle bodyRegular1 = pretendardSemiBoldStyle(16, 19); + static final TextStyle bodyRegular1 = pretendardRegularStyle(16, 19); static final TextStyle bodyRegular2 = pretendardRegularStyle(14, 17); static final TextStyle bodyRegular3 = pretendardRegularStyle(12, 14); } diff --git a/lib/presentation/pages/home/home_view.dart b/lib/presentation/pages/home/home_view.dart index f4f36a8..5170f3d 100644 --- a/lib/presentation/pages/home/home_view.dart +++ b/lib/presentation/pages/home/home_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:pets_next_door_flutter/core/constants/colors.dart'; +import 'package:pets_next_door_flutter/core/constants/images.dart'; import 'package:pets_next_door_flutter/core/constants/sizes.dart'; import 'package:pets_next_door_flutter/core/constants/text_style.dart'; import 'package:pets_next_door_flutter/presentation/pages/home/home_event.dart'; diff --git a/lib/presentation/pages/home/widgets/home_location_button.dart b/lib/presentation/pages/home/widgets/home_location_button.dart index 2a679d8..0c93f33 100644 --- a/lib/presentation/pages/home/widgets/home_location_button.dart +++ b/lib/presentation/pages/home/widgets/home_location_button.dart @@ -12,7 +12,11 @@ class _HomeLocationButton extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(Icons.location_on_sharp), + Image.asset( + PNDImages.location, + width: 24, + height: 24, + ), gapW4, Row( children: [ diff --git a/lib/presentation/pages/pet_sos/layouts/pet_filter.dart b/lib/presentation/pages/pet_sos/layouts/pet_filter.dart new file mode 100644 index 0000000..e9edebc --- /dev/null +++ b/lib/presentation/pages/pet_sos/layouts/pet_filter.dart @@ -0,0 +1,28 @@ +part of '../pet_sos_view.dart'; + +/// 돌봄급구 펫타입 필터 영역 +/// Radio 버튼 형식으로 구현되어 있음 +class _PetSosPetFilter extends ConsumerWidget with PetSosEvent { + const _PetSosPetFilter(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedPetFilter = ref.watch(petSosFilterProvider).petTypeFilter; + + return Wrap( + spacing: 8, + children: [ + ...PetTypeFilter.values + .map((petType) => GestureDetector( + onTap: () => onPetTypeChanged(ref, petType), + child: PndRadioButtonItem( + groupValue: selectedPetFilter, + value: petType, + text: petType.displayName, + ), + )) + .toList() + ], + ); + } +} diff --git a/lib/presentation/pages/pet_sos/layouts/pet_sos_list_view.dart b/lib/presentation/pages/pet_sos/layouts/pet_sos_list_view.dart new file mode 100644 index 0000000..6c89a4b --- /dev/null +++ b/lib/presentation/pages/pet_sos/layouts/pet_sos_list_view.dart @@ -0,0 +1,47 @@ +part of '../pet_sos_view.dart'; + +class _PetSosListView extends HookConsumerWidget { + const _PetSosListView({ + this.onScrollDirectionChanged, + }); + + final void Function(ScrollDirection)? onScrollDirectionChanged; + + @override + Widget build(BuildContext context, WidgetRef ref) { + useAutomaticKeepAlive(); + + final _scrollController = useScrollController(); + + useEffect(() { + void _callBack() { + onScrollDirectionChanged + ?.call(_scrollController.position.userScrollDirection); + } + + _scrollController.addListener(_callBack); + return () => _scrollController.removeListener(_callBack); + }, [_scrollController]); + + return Expanded( + child: ListView.separated( + itemCount: 10, + controller: _scrollController, + shrinkWrap: true, + itemBuilder: (context, index) => PndPostListTile.sosPage( + imageUrl: + 'https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP6btQ%2Fbtq2dCpzo9w%2F6KwuwdcKiC01N800kBegAk%2Fimg.jpg', + title: '안녕하세요 푸들 한마리 돌봄 급하게 구합니다.', + dateInfo: '23.03.21 ~ 23.03.28', + location: '용답동', + pay: '시급 9000원', + ), + separatorBuilder: (context, index) => Divider( + height: 1, + thickness: 1, + color: AppColor.of.gray20, + ), + ), + ); + } +} diff --git a/lib/presentation/pages/pet_sos/layouts/sort_filter.dart b/lib/presentation/pages/pet_sos/layouts/sort_filter.dart new file mode 100644 index 0000000..71afe88 --- /dev/null +++ b/lib/presentation/pages/pet_sos/layouts/sort_filter.dart @@ -0,0 +1,24 @@ +part of '../pet_sos_view.dart'; + +/// 돌봄급구 게시물 정렬 필터 영역 +/// 드롭다운 메뉴 형식으로 구현되어 있음 +class _PetSosSortFilter extends ConsumerWidget with PetSosEvent { + const _PetSosSortFilter(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedSortFilter = ref.watch(petSosFilterProvider).sortFilter; + return PndDropdownButton( + onSelected: (sort) => onSortChanged(ref, sort), + selectedValueStr: selectedSortFilter.displayName, + itemBuilder: (_) => SortTypeFilter.values + .map( + (sortType) => PndDropdownItem( + value: sortType, + valueStr: sortType.displayName, + ), + ) + .toList(), + ); + } +} diff --git a/lib/presentation/pages/pet_sos/pet_sos_view.dart b/lib/presentation/pages/pet_sos/pet_sos_view.dart index f8703ff..6e942be 100644 --- a/lib/presentation/pages/pet_sos/pet_sos_view.dart +++ b/lib/presentation/pages/pet_sos/pet_sos_view.dart @@ -1,17 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:pets_next_door_flutter/app/router/app_router.dart'; -import 'package:pets_next_door_flutter/core/constants/sizes.dart'; -import 'package:pets_next_door_flutter/core/constants/text_style.dart'; +import 'package:pets_next_door_flutter/core/constants/colors.dart'; import 'package:pets_next_door_flutter/core/enums/pet_type_filter.enum.dart'; import 'package:pets_next_door_flutter/core/enums/sort_type_filter.enum.dart'; import 'package:pets_next_door_flutter/presentation/pages/pet_sos/pet_sos_event.dart'; import 'package:pets_next_door_flutter/presentation/pages/pet_sos/providers/pet_sos_filter_provider.dart'; +import 'package:pets_next_door_flutter/presentation/widgets/button/radio_button.dart'; +import 'package:pets_next_door_flutter/presentation/widgets/dropdown/dropdown_button.dart'; +import 'package:pets_next_door_flutter/presentation/widgets/dropdown/dropdown_item.dart'; +import 'package:pets_next_door_flutter/presentation/widgets/list_tile/post_list_tile.dart'; -class PetSosView extends HookConsumerWidget { +part 'layouts/pet_filter.dart'; +part 'layouts/pet_sos_list_view.dart'; +part 'layouts/sort_filter.dart'; + +class PetSosView extends StatelessWidget { const PetSosView({ super.key, this.onScrollDirectionChanged, @@ -20,171 +25,29 @@ class PetSosView extends HookConsumerWidget { final void Function(ScrollDirection)? onScrollDirectionChanged; @override - Widget build(BuildContext context, WidgetRef ref) { - useAutomaticKeepAlive(); - - final _scrollController = useScrollController(); - - useEffect(() { - void _callBack() { - onScrollDirectionChanged - ?.call(_scrollController.position.userScrollDirection); - } - - _scrollController.addListener(_callBack); - return () => _scrollController.removeListener(_callBack); - }, [_scrollController]); - + Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const _SortFilter(), - const _PetFilter(), - ], - ), - ), - Expanded( - child: ListView.separated( - controller: _scrollController, - shrinkWrap: true, - itemBuilder: (context, index) => ContentsListTile(), - separatorBuilder: (context, index) => Divider(), - itemCount: 10), + _buildPetSosFilterGroup(), + _PetSosListView( + onScrollDirectionChanged: onScrollDirectionChanged, ) ], ); } -} -class ContentsListTile extends StatelessWidget { - const ContentsListTile({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () => context.goNamed(AppRoute.signIn.name), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 88, - width: 88, - color: Colors.yellow, - ), - gapW8, - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '푸들 한마리 급하게 ㅁㅇ너랴ㅐㅓㅁㅇㄴ래ㅑ머ㅔㅐㅑ', - softWrap: false, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), - ), - Text( - '23.03.21 ~ 23.03.28', - softWrap: false, - ), - Text( - '용답동', - softWrap: false, - ), - Text( - '시급 9000원', - softWrap: false, - ), - ], - ), - ) - ], - ), + Padding _buildPetSosFilterGroup() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + _PetSosSortFilter(), + _PetSosPetFilter(), + ], ), ); } } - -class _SortFilter extends ConsumerWidget with PetSosEvent { - const _SortFilter(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return PopupMenuButton( - surfaceTintColor: Colors.white, - constraints: const BoxConstraints.tightFor(width: 90), - position: PopupMenuPosition.under, - child: Container( - height: 45, - child: Row( - children: [ - Text(ref.watch(petSosFilterProvider).sortFilter.displayName), - Icon(Icons.keyboard_arrow_down_rounded) - ], - )), - onSelected: (sort) => onSortChanged(ref, sort), - itemBuilder: (context) => SortTypeFilter.values - .map((e) => PopupMenuItem( - height: 40, - padding: EdgeInsets.all(0), - value: e, - child: Container( - padding: EdgeInsets.only(left: 10), - child: Text( - e.displayName, - )), - )) - .toList(), - ); - } -} - -class _PetFilter extends ConsumerWidget with PetSosEvent { - const _PetFilter(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final filter = ref.watch(petSosFilterProvider); - return Wrap( - spacing: 8, - children: [ - ...PetTypeFilter.values - .map((petType) => GestureDetector( - onTap: () => onPetTypeChanged(ref, petType), - child: Container( - color: Colors.transparent, - height: 30, - child: Row( - children: [ - (filter.petTypeFilter == petType) - ? Icon( - Icons.check_box_sharp, - size: 20, - ) - : Icon( - Icons.check_box_outline_blank_outlined, - size: 20, - ), - gapW4, - Text( - petType.displayName, - style: AppTextStyle.bodyRegular3, - ), - ], - ), - ), - )) - .toList() - ], - ); - } -} diff --git a/lib/presentation/widgets/button/radio_button.dart b/lib/presentation/widgets/button/radio_button.dart new file mode 100644 index 0000000..0daa984 --- /dev/null +++ b/lib/presentation/widgets/button/radio_button.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:pets_next_door_flutter/core/constants/colors.dart'; +import 'package:pets_next_door_flutter/core/constants/sizes.dart'; +import 'package:pets_next_door_flutter/core/constants/text_style.dart'; + +class PndRadioButtonItem extends StatelessWidget { + const PndRadioButtonItem({ + super.key, + required this.groupValue, + required this.value, + required this.text, + }); + + final T groupValue; + final T value; + final String text; + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.transparent, + height: 30, + child: Row( + children: [ + if (groupValue == value) + Icon(Icons.check, size: 15, color: AppColor.of.black), + gapW2, + Text( + text, + style: (groupValue == value) + ? AppTextStyle.bodyBold3 + : AppTextStyle.bodyRegular3.copyWith(color: AppColor.of.gray90), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/dropdown/dropdown_button.dart b/lib/presentation/widgets/dropdown/dropdown_button.dart new file mode 100644 index 0000000..8b222a6 --- /dev/null +++ b/lib/presentation/widgets/dropdown/dropdown_button.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:pets_next_door_flutter/core/constants/text_style.dart'; + +class PndDropdownButton extends StatelessWidget { + const PndDropdownButton({ + super.key, + required this.selectedValueStr, + required this.itemBuilder, + this.onSelected, + }); + + final String selectedValueStr; + final List> Function(BuildContext) itemBuilder; + final void Function(T)? onSelected; + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + surfaceTintColor: Colors.white, + constraints: const BoxConstraints.tightFor(width: 90), + position: PopupMenuPosition.under, + child: Container( + height: 45, + child: Row( + children: [ + Text( + selectedValueStr, + style: AppTextStyle.bodyBold2, + ), + Icon(Icons.keyboard_arrow_down_rounded) + ], + )), + onSelected: onSelected, + itemBuilder: itemBuilder, + ); + } +} diff --git a/lib/presentation/widgets/dropdown/dropdown_item.dart b/lib/presentation/widgets/dropdown/dropdown_item.dart new file mode 100644 index 0000000..14c0212 --- /dev/null +++ b/lib/presentation/widgets/dropdown/dropdown_item.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class PndDropdownItem extends PopupMenuItem { + PndDropdownItem({ + super.key, + @override T? value, + String? valueStr, + }) : super( + padding: EdgeInsets.all(0), + height: 40, + value: value, + child: Container( + padding: EdgeInsets.only(left: 10), + child: Text( + valueStr ?? '', + ), + ), + ); +} diff --git a/lib/presentation/widgets/list_tile/post_list_tile.dart b/lib/presentation/widgets/list_tile/post_list_tile.dart new file mode 100644 index 0000000..63135a3 --- /dev/null +++ b/lib/presentation/widgets/list_tile/post_list_tile.dart @@ -0,0 +1,153 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:pets_next_door_flutter/core/constants/colors.dart'; +import 'package:pets_next_door_flutter/core/constants/images.dart'; +import 'package:pets_next_door_flutter/core/constants/sizes.dart'; +import 'package:pets_next_door_flutter/core/constants/text_style.dart'; + +typedef _ImgIconUrl = String; +typedef _TextWithIconImgUrl = ({_ImgIconUrl imgIconUrl, String text}); + +const double _kImageHeight = 88; +const double _kImageWidth = 88; +const double _kContentIconSize = 16; +const double _kContentSpacing = PNDSizes.p4; + +/// 앱에서 게시물 보여줄 때 사용하는 ListTile +class PndPostListTile extends StatelessWidget { + const PndPostListTile({ + super.key, + required this.title, + required this.imageUrl, + required this.contentsList, + this.imageHeight = _kImageHeight, + this.imageWidth = _kImageWidth, + this.contentsSpacing = _kContentSpacing, + }); + final String title; + final String imageUrl; + final double imageWidth; + final double imageHeight; + final List<_TextWithIconImgUrl> contentsList; + final double contentsSpacing; + + /// 돌봄급구에서 보여주는 ListTile + /// 나중에 돌봄급구와 메이트 디자인 변경될 수 있을 것 같아서 빼놓음 + factory PndPostListTile.sosPage({ + Key? key, + required String? imageUrl, + required String title, + required String dateInfo, + required String location, + required String pay, + }) { + return PndPostListTile( + key: key, + imageUrl: imageUrl ?? '', + title: title, + contentsList: [ + (imgIconUrl: PNDImages.calander, text: dateInfo), + (imgIconUrl: PNDImages.location, text: location), + (imgIconUrl: PNDImages.payment, text: pay), + ], + ); + } + + /// 돌봄메이트에서 보여주는 ListTile + factory PndPostListTile.matePage({ + Key? key, + required String? imageUrl, + required String title, + required String dateInfo, + required String location, + required String pay, + }) { + return PndPostListTile( + key: key, + imageUrl: imageUrl ?? '', + title: title, + contentsList: [ + (imgIconUrl: PNDImages.calander, text: dateInfo), + (imgIconUrl: PNDImages.location, text: location), + (imgIconUrl: PNDImages.payment, text: pay), + ], + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Container( + height: imageHeight, + width: imageWidth, + decoration: BoxDecoration( + color: AppColor.of.gray20, + ), + child: CachedNetworkImage( + imageUrl: imageUrl, + fit: BoxFit.cover, + errorWidget: (context, url, error) => Center( + child: Icon( + Icons.image, + size: 25, + color: AppColor.of.gray30, + ), + ), + )), + ), + gapW8, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + softWrap: false, + overflow: TextOverflow.ellipsis, + style: AppTextStyle.bodyBold1, + ), + gapH8, + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: contentsList.indexed + .map( + (contents) => Container( + margin: contents.$1 != contentsList.length - 1 + ? EdgeInsets.only(bottom: contentsSpacing) + : null, + child: Row( + children: [ + Image.asset( + contents.$2.imgIconUrl, + width: _kContentIconSize, + height: _kContentIconSize, + color: AppColor.of.gray90, + ), + gapW4, + Text( + contents.$2.text, + style: AppTextStyle.bodyRegular3, + softWrap: false, + ), + ], + ), + ), + ) + .toList(), + ) + ], + ), + ) + ], + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index c45a8e5..2940709 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,8 @@ flutter: - assets/imgs/ - assets/imgs/pet/ - assets/imgs/launch/ + - assets/imgs/home/ + - assets/imgs/common/ - assets/svgs/ - assets/svgs/common/ - assets/svgs/login/