From 5a75c188015e9059b76e23d91c39067e21ac1bd3 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Tue, 28 Jan 2025 09:56:46 +0100 Subject: [PATCH 1/6] chore: using timezone_date_time_text in proposal_card --- .../lib/common/formatters/date_formatter.dart | 14 ++ .../lib/pages/discovery/discovery_page.dart | 2 +- .../widgets/cards/pending_proposal_card.dart | 125 +++++++++++------- .../lib/widgets/scrollbar/voices_slider.dart | 32 +---- .../lib/widgets/text/day_month_time_text.dart | 28 ++++ .../widgets/text/timezone_date_time_text.dart | 6 +- .../lib/src/themes/catalyst.dart | 2 + .../lib/src/themes/widgets/slider_theme.dart | 20 +++ .../lib/l10n/intl_en.arb | 13 +- 9 files changed, 164 insertions(+), 78 deletions(-) create mode 100644 catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/slider_theme.dart diff --git a/catalyst_voices/apps/voices/lib/common/formatters/date_formatter.dart b/catalyst_voices/apps/voices/lib/common/formatters/date_formatter.dart index 29b7f7dbabc..9ac642b947a 100644 --- a/catalyst_voices/apps/voices/lib/common/formatters/date_formatter.dart +++ b/catalyst_voices/apps/voices/lib/common/formatters/date_formatter.dart @@ -119,4 +119,18 @@ abstract class DateFormatter { return ''; } + + static String formatDayMonthTime(DateTime dateTime) { + return DateFormat('d MMM HH:mm').format(dateTime); + } + + static String formatFullDateWithoutDayname( + VoicesLocalizations l10n, + DateTime dateTime, + ) { + final date = DateFormat('d MMMM yyyy').format(dateTime); + final time = DateFormat('HH:mm').format(dateTime); + + return l10n.publishedOn(date, time); + } } diff --git a/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart b/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart index 686538aedd3..6e8750c01db 100644 --- a/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart @@ -62,7 +62,7 @@ class _GuestVisitorBody extends StatelessWidget { ), _CampaignCategories( List.filled( - 7, + 6, CampaignCategoryCardViewModel.dummy(), ), ), diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart index f6de845fda9..0a764872faf 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart @@ -1,5 +1,6 @@ import 'package:catalyst_voices/common/ext/ext.dart'; import 'package:catalyst_voices/common/formatters/date_formatter.dart'; +import 'package:catalyst_voices/widgets/text/day_month_time_text.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; @@ -9,7 +10,7 @@ import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; /// Displays a proposal in pending state on a card. -class PendingProposalCard extends StatelessWidget { +class PendingProposalCard extends StatefulWidget { final PendingProposal proposal; final bool showStatus; final bool showLastUpdate; @@ -25,54 +26,82 @@ class PendingProposalCard extends StatelessWidget { this.onFavoriteChanged, }); + @override + State createState() => _PendingProposalCardState(); +} + +class _PendingProposalCardState extends State { + bool isHovered = false; @override Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints(maxWidth: 326), - decoration: BoxDecoration( - color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Topbar( - showStatus: showStatus, - isFavorite: isFavorite, - onFavoriteChanged: onFavoriteChanged, - ), - _Category( - category: proposal.category, - ), - const SizedBox(height: 4), - _Title(text: proposal.title), - _Author(author: proposal.author), - _FundsAndDuration( - funds: proposal.fundsRequested, - duration: proposal.duration, - ), - const SizedBox(height: 12), - _Description(text: proposal.description), - const SizedBox(height: 24), - _ProposalInfo( - proposalStage: proposal.publishStage, - version: proposal.version, - lastUpdate: proposal.lastUpdateDate, - commentsCount: proposal.commentsCount, - showLastUpdate: showLastUpdate, - ), - ], - ), + return MouseRegion( + onEnter: (_) => _changeHoverState(value: true), + onExit: (_) => _changeHoverState(value: false), + child: Container( + constraints: const BoxConstraints(maxWidth: 326), + decoration: BoxDecoration( + color: context.colors.elevationsOnSurfaceNeutralLv1White, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isHovered + ? _hoverBorderColor(context) + : context.colors.elevationsOnSurfaceNeutralLv1White, ), - ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Topbar( + showStatus: widget.showStatus, + isFavorite: widget.isFavorite, + onFavoriteChanged: widget.onFavoriteChanged, + ), + _Category( + category: widget.proposal.category, + ), + const SizedBox(height: 4), + _Title(text: widget.proposal.title), + _Author(author: widget.proposal.author), + _FundsAndDuration( + funds: widget.proposal.fundsRequested, + duration: widget.proposal.duration, + ), + const SizedBox(height: 12), + _Description(text: widget.proposal.description), + const SizedBox(height: 24), + _ProposalInfo( + proposalStage: widget.proposal.publishStage, + version: widget.proposal.version, + lastUpdate: widget.proposal.lastUpdateDate, + commentsCount: widget.proposal.commentsCount, + showLastUpdate: widget.showLastUpdate, + ), + ], + ), + ), + ], + ), ), ); } + + void _changeHoverState({required bool value}) { + setState(() { + isHovered = value; + }); + } + + Color _hoverBorderColor(BuildContext context) { + return switch (widget.proposal.publishStage) { + ProposalPublish.draft => context.colorScheme.secondary, + ProposalPublish.published => context.colorScheme.primary, + }; + } } class _Topbar extends StatelessWidget { @@ -327,13 +356,13 @@ class _ProposalInfo extends StatelessWidget { ), if (showLastUpdate) ...[ const SizedBox(width: 4), - Text( - DateFormatter.formatShortMonth( + VoicesPlainTooltip( + message: DateFormatter.formatFullDateWithoutDayname( context.l10n, lastUpdate, ), - style: context.textTheme.labelLarge?.copyWith( - color: context.colors.textOnPrimaryLevel1, + child: DayMonthTimeText( + dateTime: lastUpdate, ), ), ], @@ -356,7 +385,7 @@ class _ProposalInfo extends StatelessWidget { ) { return switch (proposalStage) { ProposalPublish.draft => l10n.draft, - ProposalPublish.published => l10n.published, + ProposalPublish.published => l10n.finalProposal, }; } } diff --git a/catalyst_voices/apps/voices/lib/widgets/scrollbar/voices_slider.dart b/catalyst_voices/apps/voices/lib/widgets/scrollbar/voices_slider.dart index 2a657a51d25..5264a6a4945 100644 --- a/catalyst_voices/apps/voices/lib/widgets/scrollbar/voices_slider.dart +++ b/catalyst_voices/apps/voices/lib/widgets/scrollbar/voices_slider.dart @@ -1,4 +1,3 @@ -import 'package:catalyst_voices/common/ext/ext.dart'; import 'package:flutter/material.dart'; class VoicesSlider extends StatelessWidget { @@ -7,7 +6,6 @@ class VoicesSlider extends StatelessWidget { final double max; final int divisions; final ValueChanged? onChanged; - final SliderThemeData? sliderTheme; const VoicesSlider({ super.key, @@ -16,34 +14,16 @@ class VoicesSlider extends StatelessWidget { this.max = 1, this.divisions = 25, this.onChanged, - this.sliderTheme, }); @override Widget build(BuildContext context) { - return SliderTheme( - data: sliderTheme ?? - SliderThemeData( - trackHeight: 6, - activeTrackColor: Colors.white, - secondaryActiveTrackColor: Colors.tealAccent, - inactiveTrackColor: context.colors.onSurfaceNeutral016, - thumbColor: Colors.white, - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 4, - elevation: 0, - pressedElevation: 0, - ), - overlayColor: Colors.transparent, - inactiveTickMarkColor: Colors.transparent, - ), - child: Slider( - value: value, - onChanged: onChanged, - min: min, - max: max, - divisions: divisions, - ), + return Slider( + value: value, + onChanged: onChanged, + min: min, + max: max, + divisions: divisions, ); } } diff --git a/catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart b/catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart new file mode 100644 index 00000000000..8dae6e034ec --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart @@ -0,0 +1,28 @@ +import 'package:catalyst_voices/common/ext/build_context_ext.dart'; +import 'package:catalyst_voices/common/formatters/date_formatter.dart'; +import 'package:catalyst_voices/widgets/text/timezone_date_time_text.dart'; +import 'package:flutter/material.dart'; + +class DayMonthTimeText extends StatelessWidget { + final DateTime dateTime; + final bool showTimezone; + const DayMonthTimeText({ + super.key, + required this.dateTime, + this.showTimezone = false, + }); + + @override + Widget build(BuildContext context) { + return TimezoneDateTimeText( + dateTime, + formatter: (context, lastUpdate) { + return DateFormatter.formatDayMonthTime(lastUpdate); + }, + style: context.textTheme.labelLarge?.copyWith( + color: context.colors.textOnPrimaryLevel1, + ), + showTimezone: showTimezone, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text/timezone_date_time_text.dart b/catalyst_voices/apps/voices/lib/widgets/text/timezone_date_time_text.dart index ea2b2a114b8..365b7b0542c 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text/timezone_date_time_text.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text/timezone_date_time_text.dart @@ -60,12 +60,14 @@ class TimezoneDateTimeText extends StatelessWidget { final DateTime data; final TimezoneDateTimeTextFormatter formatter; final TextStyle? style; + final bool showTimezone; const TimezoneDateTimeText( this.data, { super.key, required this.formatter, this.style, + this.showTimezone = true, }); @override @@ -90,8 +92,8 @@ class TimezoneDateTimeText extends StatelessWidget { final effectiveStyle = style.merge(baseStyle); return AffixDecorator( - gap: 6, - suffix: _TimezoneCard(timezone), + gap: showTimezone ? 6 : 0, + suffix: showTimezone ? _TimezoneCard(timezone) : null, child: Text( string, style: effectiveStyle, diff --git a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/catalyst.dart b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/catalyst.dart index 8cc5e442bb9..b9dab962c4e 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/catalyst.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/catalyst.dart @@ -3,6 +3,7 @@ import 'package:catalyst_voices_brands/src/brands/brand.dart'; import 'package:catalyst_voices_brands/src/theme_extensions/brand_assets.dart'; import 'package:catalyst_voices_brands/src/theme_extensions/voices_color_scheme.dart'; import 'package:catalyst_voices_brands/src/themes/widgets/buttons_theme.dart'; +import 'package:catalyst_voices_brands/src/themes/widgets/slider_theme.dart'; import 'package:catalyst_voices_brands/src/themes/widgets/toggles_theme.dart'; import 'package:catalyst_voices_brands/src/themes/widgets/voices_dialog_theme.dart'; import 'package:catalyst_voices_brands/src/themes/widgets/voices_input_decoration_theme.dart'; @@ -343,6 +344,7 @@ ThemeData _buildThemeData( circularTrackColor: colorScheme.secondaryContainer, refreshBackgroundColor: colorScheme.secondaryContainer, ), + sliderTheme: VoicesSliderThemeData(colors: voicesColorScheme), textTheme: textTheme, colorScheme: colorScheme, iconTheme: IconThemeData( diff --git a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/slider_theme.dart b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/slider_theme.dart new file mode 100644 index 00000000000..8d9f59a6370 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/slider_theme.dart @@ -0,0 +1,20 @@ +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +class VoicesSliderThemeData extends SliderThemeData { + VoicesSliderThemeData({ + required VoicesColorScheme colors, + }) : super( + trackHeight: 6, + activeTrackColor: Colors.white, + inactiveTrackColor: colors.onSurfaceNeutral016, + thumbColor: Colors.white, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 4, + elevation: 0, + pressedElevation: 0, + ), + overlayColor: Colors.transparent, + inactiveTickMarkColor: Colors.transparent, + ); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb index 38c989c3bb9..df0f3eef39b 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -1389,5 +1389,16 @@ }, "finalProposal": "Final", "mostRecent": "Most Recent", - "viewAllProposals": "View All Proposals" + "viewAllProposals": "View All Proposals", + "publishedOn": "Published on {date} at {time}", + "@publishedOn": { + "placeholders": { + "date": { + "type": "String" + }, + "time": { + "type": "String" + } + } + } } \ No newline at end of file From 031ac49dca16b923eccbc66bebcf1dfb69bef0b7 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Tue, 28 Jan 2025 10:06:53 +0100 Subject: [PATCH 2/6] test: for new dateformatter methods --- .../formatters/date_formatter_test.dart | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/catalyst_voices/apps/voices/test/common/formatters/date_formatter_test.dart b/catalyst_voices/apps/voices/test/common/formatters/date_formatter_test.dart index 5bbe3e6b37b..c1fe5a6bf11 100644 --- a/catalyst_voices/apps/voices/test/common/formatters/date_formatter_test.dart +++ b/catalyst_voices/apps/voices/test/common/formatters/date_formatter_test.dart @@ -20,6 +20,10 @@ class _FakeVoicesLocalizations extends Fake implements VoicesLocalizations { String get from => 'From'; @override String get to => 'To'; + @override + String publishedOn(String date, String time) { + return 'Published on $date at $time'; + } } class _FakeMaterialLocalizations extends Fake implements MaterialLocalizations { @@ -185,5 +189,23 @@ void main() { expect(result, ''); }); + + test('should return translation for publishedOn', () { + final date = DateTime(2025, 1, 28, 14); + const expectedValue = 'Published on 28 January 2025 at 14:00'; + + final result = DateFormatter.formatFullDateWithoutDayname(l10n, date); + + expect(result, expectedValue); + }); + + test('should return formatted day month time string', () { + final date = DateTime(2025, 1, 28, 14); + const expectedValue = '28 Jan 14:00'; + + final result = DateFormatter.formatDayMonthTime(date); + + expect(result, expectedValue); + }); }); } From 2615135548c37b4002f2f02d56aaf5eb02d4d969 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Tue, 28 Jan 2025 16:18:19 +0100 Subject: [PATCH 3/6] chore: changing tooltip message --- .../lib/common/formatters/date_formatter.dart | 58 ++++++++++++++----- .../widgets/cards/campaign_stage_card.dart | 5 +- .../widgets/cards/pending_proposal_card.dart | 11 ++-- .../formatters/date_formatter_test.dart | 24 +++++--- 4 files changed, 69 insertions(+), 29 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/common/formatters/date_formatter.dart b/catalyst_voices/apps/voices/lib/common/formatters/date_formatter.dart index 9ac642b947a..e51c069e8cb 100644 --- a/catalyst_voices/apps/voices/lib/common/formatters/date_formatter.dart +++ b/catalyst_voices/apps/voices/lib/common/formatters/date_formatter.dart @@ -41,13 +41,44 @@ abstract class DateFormatter { return l10n.inXDays(days); } - static (String date, String time) formatDateTimeParts( - DateTime date, - ) { - final dayMonthFormatter = DateFormat('d MMMM').format(date); + /// Formats a given DateTime into separate date and time strings. + /// + /// The method formats the date part in either 'd MMMM yyyy' or + /// 'd MMMM' format depending on the value of [includeYear]. + /// The time part is always formatted in 'HH:mm' format. + /// + /// Parameters: + /// - [date]: The DateTime object to format. + /// - [includeYear]: A boolean indicating whether the year should be included + /// in the formatted date string. Defaults to `false`. + /// + /// Returns a map with: + /// - `date`: The formatted date string. + /// - `time`: The formatted time string. + /// + /// Examples: + /// - For `DateTime(2023, 10, 14, 9, 30)` with `includeYear = false`: + /// - date: "14 October" + /// - time: "09:30" + /// - For `DateTime(2023, 10, 14, 9, 30)` with `includeYear = true`: + /// - date: "14 October 2023" + /// - time: "09:30" + /// - For `DateTime(2022, 2, 5, 15, 45)` with `includeYear = false`: + /// - date: "5 February" + /// - time: "15:45" + /// - For `DateTime(2022, 2, 5, 15, 45)` with `includeYear = true`: + /// - date: "5 February 2022" + /// - time: "15:45" + static ({String date, String time}) formatDateTimeParts( + DateTime date, { + bool includeYear = false, + }) { + final formatter = + includeYear ? DateFormat('d MMMM yyyy') : DateFormat('d MMMM'); + final dayMonthFormatter = formatter.format(date); final timeFormatter = DateFormat('HH:mm').format(date); - return (dayMonthFormatter, timeFormatter); + return (date: dayMonthFormatter, time: timeFormatter); } static String formatShortMonth( @@ -120,17 +151,14 @@ abstract class DateFormatter { return ''; } + /// Formats a given [DateTime] object into a string + /// with the format "d MMM HH:mm". + /// + /// The format consists of: + /// - Day of the month as a number without leading zeros (e.g., 4) + /// - Abbreviated month name (e.g., Jan, Feb, Mar) + /// - 24-hour time format with hours and minutes (e.g., 14:30) static String formatDayMonthTime(DateTime dateTime) { return DateFormat('d MMM HH:mm').format(dateTime); } - - static String formatFullDateWithoutDayname( - VoicesLocalizations l10n, - DateTime dateTime, - ) { - final date = DateFormat('d MMMM yyyy').format(dateTime); - final time = DateFormat('HH:mm').format(dateTime); - - return l10n.publishedOn(date, time); - } } diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart index 8e2014cb738..a45e8b833b3 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart @@ -88,11 +88,12 @@ class CampaignStageCard extends StatelessWidget { final formattedDate = DateFormatter.formatDateTimeParts(campaign.startDate); return context.l10n - .campaignBeginsOn(formattedDate.$1, formattedDate.$2); + .campaignBeginsOn(formattedDate.date, formattedDate.time); case CampaignStage.live: final formattedDate = DateFormatter.formatDateTimeParts(campaign.endDate); - return context.l10n.campaignEndsOn(formattedDate.$1, formattedDate.$2); + return context.l10n + .campaignEndsOn(formattedDate.date, formattedDate.time); case CampaignStage.completed: return ''; } diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart index 0a764872faf..7d403a1dcb9 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart @@ -357,10 +357,7 @@ class _ProposalInfo extends StatelessWidget { if (showLastUpdate) ...[ const SizedBox(width: 4), VoicesPlainTooltip( - message: DateFormatter.formatFullDateWithoutDayname( - context.l10n, - lastUpdate, - ), + message: _tooltipMessage(context), child: DayMonthTimeText( dateTime: lastUpdate, ), @@ -388,4 +385,10 @@ class _ProposalInfo extends StatelessWidget { ProposalPublish.published => l10n.finalProposal, }; } + + String _tooltipMessage(BuildContext context) { + final dt = DateFormatter.formatDateTimeParts(lastUpdate, includeYear: true); + + return context.l10n.publishedOn(dt.date, dt.time); + } } diff --git a/catalyst_voices/apps/voices/test/common/formatters/date_formatter_test.dart b/catalyst_voices/apps/voices/test/common/formatters/date_formatter_test.dart index c1fe5a6bf11..b903eaade3f 100644 --- a/catalyst_voices/apps/voices/test/common/formatters/date_formatter_test.dart +++ b/catalyst_voices/apps/voices/test/common/formatters/date_formatter_test.dart @@ -190,22 +190,30 @@ void main() { expect(result, ''); }); - test('should return translation for publishedOn', () { + test('should return formatted day month time string', () { final date = DateTime(2025, 1, 28, 14); - const expectedValue = 'Published on 28 January 2025 at 14:00'; + const expectedValue = '28 Jan 14:00'; - final result = DateFormatter.formatFullDateWithoutDayname(l10n, date); + final result = DateFormatter.formatDayMonthTime(date); expect(result, expectedValue); }); - test('should return formatted day month time string', () { - final date = DateTime(2025, 1, 28, 14); - const expectedValue = '28 Jan 14:00'; + test('should format date without year when includeYear is false', () { + final date = DateTime(2023, 10, 25, 14, 30); // 25th October 2023, 14:30 + final result = + DateFormatter.formatDateTimeParts(date, includeYear: false); - final result = DateFormatter.formatDayMonthTime(date); + expect(result.date, '25 October'); + expect(result.time, '14:30'); + }); - expect(result, expectedValue); + test('should format date with year when includeYear is true', () { + final date = DateTime(2025, 10, 25, 14, 30); // 25th October 2023, 14:30 + final result = DateFormatter.formatDateTimeParts(date, includeYear: true); + + expect(result.date, '25 October 2025'); + expect(result.time, '14:30'); }); }); } From adb47487b1e2d1c2a27ab559998c2bae21983a5f Mon Sep 17 00:00:00 2001 From: Ryszard Schossler <51096731+LynxLynxx@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:47:19 +0100 Subject: [PATCH 4/6] Update catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart Co-authored-by: Dominik Toton <166132265+dtscalac@users.noreply.github.com> --- .../apps/voices/lib/widgets/text/day_month_time_text.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart b/catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart index 8dae6e034ec..74b4c6e8cb8 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; class DayMonthTimeText extends StatelessWidget { final DateTime dateTime; final bool showTimezone; + const DayMonthTimeText({ super.key, required this.dateTime, From fc27d362eb3d3a0a72414d0c33cecfeab805ae6a Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Wed, 29 Jan 2025 17:54:58 +0100 Subject: [PATCH 5/6] refactor: Improve PendingProposalCard and add ProposalPublish.isPublished method - Updated PendingProposalCard for better state management. - Added isPublished method to ProposalPublish enum --- .../widgets/cards/pending_proposal_card.dart | 158 +++++++++++------- .../lib/widgets/text/day_month_time_text.dart | 2 +- .../lib/src/proposal/proposal_base.dart | 1 + 3 files changed, 101 insertions(+), 60 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart index 7d403a1dcb9..5bc9ce26225 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart @@ -31,76 +31,116 @@ class PendingProposalCard extends StatefulWidget { } class _PendingProposalCardState extends State { + late final WidgetStatesController _statesController; + late final _ProposalBorderColor border; + + @override + void initState() { + super.initState(); + _statesController = WidgetStatesController(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + border = _ProposalBorderColor( + publishStage: widget.proposal.publishStage, + colorScheme: context.colorScheme, + colors: context.colors, + ); + } + + @override + void dispose() { + _statesController.dispose(); + super.dispose(); + } + bool isHovered = false; @override Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => _changeHoverState(value: true), - onExit: (_) => _changeHoverState(value: false), - child: Container( - constraints: const BoxConstraints(maxWidth: 326), - decoration: BoxDecoration( - color: context.colors.elevationsOnSurfaceNeutralLv1White, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isHovered - ? _hoverBorderColor(context) - : context.colors.elevationsOnSurfaceNeutralLv1White, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Topbar( - showStatus: widget.showStatus, - isFavorite: widget.isFavorite, - onFavoriteChanged: widget.onFavoriteChanged, - ), - _Category( - category: widget.proposal.category, - ), - const SizedBox(height: 4), - _Title(text: widget.proposal.title), - _Author(author: widget.proposal.author), - _FundsAndDuration( - funds: widget.proposal.fundsRequested, - duration: widget.proposal.duration, - ), - const SizedBox(height: 12), - _Description(text: widget.proposal.description), - const SizedBox(height: 24), - _ProposalInfo( - proposalStage: widget.proposal.publishStage, - version: widget.proposal.version, - lastUpdate: widget.proposal.lastUpdateDate, - commentsCount: widget.proposal.commentsCount, - showLastUpdate: widget.showLastUpdate, - ), - ], + return Material( + color: Colors.transparent, + child: InkWell( + statesController: _statesController, + onTap: () {}, + child: ValueListenableBuilder( + valueListenable: _statesController, + builder: (context, value, child) => Container( + constraints: const BoxConstraints(maxWidth: 326), + decoration: BoxDecoration( + color: context.colors.elevationsOnSurfaceNeutralLv1White, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: border.resolve(_statesController.value), ), ), - ], + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Topbar( + showStatus: widget.showStatus, + isFavorite: widget.isFavorite, + onFavoriteChanged: widget.onFavoriteChanged, + ), + _Category( + category: widget.proposal.category, + ), + const SizedBox(height: 4), + _Title(text: widget.proposal.title), + _Author(author: widget.proposal.author), + _FundsAndDuration( + funds: widget.proposal.fundsRequested, + duration: widget.proposal.duration, + ), + const SizedBox(height: 12), + _Description(text: widget.proposal.description), + const SizedBox(height: 24), + _ProposalInfo( + proposalStage: widget.proposal.publishStage, + version: widget.proposal.version, + lastUpdate: widget.proposal.lastUpdateDate, + commentsCount: widget.proposal.commentsCount, + showLastUpdate: widget.showLastUpdate, + ), + ], + ), + ), + ], + ), + ), ), ), ); } +} - void _changeHoverState({required bool value}) { - setState(() { - isHovered = value; - }); - } +final class _ProposalBorderColor extends WidgetStateColor { + final ProposalPublish publishStage; + final VoicesColorScheme colors; + final ColorScheme colorScheme; - Color _hoverBorderColor(BuildContext context) { - return switch (widget.proposal.publishStage) { - ProposalPublish.draft => context.colorScheme.secondary, - ProposalPublish.published => context.colorScheme.primary, - }; + _ProposalBorderColor({ + required this.publishStage, + required this.colors, + required this.colorScheme, + }) : super(colors.outlineBorder.value); + + @override + Color resolve(Set states) { + if (states.contains(WidgetState.hovered)) { + return switch (publishStage) { + ProposalPublish.draft => colorScheme.secondary, + ProposalPublish.published => colorScheme.primary, + }; + } + + return colors.elevationsOnSurfaceNeutralLv1White; } } diff --git a/catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart b/catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart index 74b4c6e8cb8..77cabdadd56 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text/day_month_time_text.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; class DayMonthTimeText extends StatelessWidget { final DateTime dateTime; final bool showTimezone; - + const DayMonthTimeText({ super.key, required this.dateTime, diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_base.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_base.dart index 1fdbcb20bf2..0c3411e546e 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_base.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_base.dart @@ -11,6 +11,7 @@ enum ProposalPublish { published; bool get isDraft => this == draft; + bool get isPublished => this == published; } enum ProposalAccess { private, public } From b85222aeafdf34d2696680001a580c82fbcff60a Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Thu, 30 Jan 2025 09:29:50 +0100 Subject: [PATCH 6/6] refactor(pending-proposal-card): update border color on publish stage change The `_ProposalBorderColor` is now updated when the proposal's publish stage changes. Also, the name of the border variable changed to `_border`. --- .../widgets/cards/pending_proposal_card.dart | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart index 5bc9ce26225..ab062589376 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart @@ -32,7 +32,7 @@ class PendingProposalCard extends StatefulWidget { class _PendingProposalCardState extends State { late final WidgetStatesController _statesController; - late final _ProposalBorderColor border; + late final _ProposalBorderColor _border; @override void initState() { @@ -43,13 +43,25 @@ class _PendingProposalCardState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - border = _ProposalBorderColor( + _border = _ProposalBorderColor( publishStage: widget.proposal.publishStage, colorScheme: context.colorScheme, colors: context.colors, ); } + @override + void didUpdateWidget(covariant PendingProposalCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.proposal.publishStage != oldWidget.proposal.publishStage) { + _border = _ProposalBorderColor( + publishStage: widget.proposal.publishStage, + colors: context.colors, + colorScheme: context.colorScheme, + ); + } + } + @override void dispose() { _statesController.dispose(); @@ -57,6 +69,7 @@ class _PendingProposalCardState extends State { } bool isHovered = false; + @override Widget build(BuildContext context) { return Material( @@ -72,7 +85,7 @@ class _PendingProposalCardState extends State { color: context.colors.elevationsOnSurfaceNeutralLv1White, borderRadius: BorderRadius.circular(12), border: Border.all( - color: border.resolve(_statesController.value), + color: _border.resolve(_statesController.value), ), ), child: Column( @@ -301,6 +314,7 @@ class _FundsAndDuration extends StatelessWidget { class _PropertyValue extends StatelessWidget { final String title; final String formattedValue; + const _PropertyValue({ required this.title, required this.formattedValue,