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

feat(cat-voices): In-page Information Cards #1242

Merged
merged 13 commits into from
Nov 26, 2024
Merged
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 @@ -44,6 +44,15 @@ abstract class DateFormatter {
return l10n.inXDays(days);
}

static (String date, String time) formatDateTimeParts(
DateTime date,
) {
final dayMonthFormatter = DateFormat('d MMMM').format(date);
final timeFormatter = DateFormat('HH:mm').format(date);

return (dayMonthFormatter, timeFormatter);
}

static String formatShortMonth(
VoicesLocalizations l10n,
DateTime dateTime,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import 'package:catalyst_voices/common/formatters/date_formatter.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';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
import 'package:flutter/material.dart';

class InPageInformationCard extends StatelessWidget {
final InPageInformation information;

const InPageInformationCard({
super.key,
required this.information,
});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
return DecoratedBox(
decoration: BoxDecoration(
color: theme.colors.elevationsOnSurfaceNeutralLv1White,
border: Border.all(
color: theme.colors.outlineBorderVariant!,
),
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
VoicesAvatar(
icon: VoicesAssets.icons.speakerphone.buildIcon(),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
information.stage.localizedName(context.l10n),
style: textTheme.titleMedium,
),
Text(
information.description,
style: textTheme.bodyMedium,
),
if (_getDateInformation(context).isNotEmpty)
_CampaignDateInformation(
value: _getDateInformation(context),
),
const SizedBox(height: 16),
],
),
),
],
),
if (!information.stage.isDraft) ...[
const SizedBox(height: 16),
OutlinedButton(
onPressed: () {}, // TODO(ryszard-schossler): add logic
child: Text(_getButtonText(context)),
),
],
],
),
),
);
}

String _getButtonText(BuildContext context) {
if (information.stage == CampaignStage.live) {
return context.l10n.viewProposals;
} else {
return context.l10n.viewVotingResults;
}
}

String _getDateInformation(BuildContext context) {
if (information is LiveCampaignInformation ||
information is DraftCampaignInformation) {
final dateMixin = (information as DateTimeMixin);
final formattedDate = DateFormatter.formatDateTimeParts(dateMixin.date);
return dateMixin.localizedDate(
context.l10n,
formattedDate,
);
} else {
return '';
}
}
}

class _CampaignDateInformation extends StatelessWidget {
final String value;

const _CampaignDateInformation({required this.value});

@override
Widget build(BuildContext context) {
return Column(
children: [
const SizedBox(height: 30),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
VoicesAssets.icons.calendar.buildIcon(),
const SizedBox(width: 8),
Text(
value,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
],
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import 'package:catalyst_voices/widgets/cards/in_page_information_card.dart';
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
import 'package:catalyst_voices_brands/catalyst_voices_brands.dart';
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import '../../helpers/helpers.dart';

void main() {
late VoicesColorScheme voicesColors;

final draftInformationTest = DraftCampaignInformation(
date: DateTime(2024, 11, 20, 13, 00, 00),
description: 'Draft Information Test',
);

final liveInformationTest = LiveCampaignInformation(
date: DateTime(2024, 11, 20, 13, 00, 00),
description: 'Live Information Test',
);

const completedInformationText = CompletedCampaignInformation(
description: 'Completed Information Test',
);

setUp(() {
voicesColors = const VoicesColorScheme.optional(
outlineBorderVariant: Colors.red,
elevationsOnSurfaceNeutralLv1White: Colors.blue,
);
});

Widget buildTestWidget(InPageInformation information) {
return Scaffold(
body: SizedBox(
width: 1000,
child: InPageInformationCard(
information: information,
),
),
);
}

group('InPageInformationCard', () {
testWidgets(
'Renders correctly always display elements which are always render',
(tester) async {
await tester.pumpApp(
buildTestWidget(completedInformationText),
voicesColors: voicesColors,
);

await tester.pumpAndSettle();

expect(find.byType(InPageInformationCard), findsOneWidget);
expect(find.byType(CatalystSvgIcon), findsOneWidget);
},
);

testWidgets('Renders correctly for draft information', (tester) async {
await tester.pumpApp(
buildTestWidget(draftInformationTest),
voicesColors: voicesColors,
);

await tester.pumpAndSettle();

expect(find.byType(InPageInformationCard), findsOneWidget);
expect(find.byType(Text), findsExactly(3));
expect(find.byType(CatalystSvgIcon), findsExactly(2));
expect(
find.text('Campaign Staring Soon (Ready to deploy)'),
findsOneWidget,
);
expect(find.text(draftInformationTest.description), findsOneWidget);
expect(find.byType(OutlinedButton), findsNothing);
});

testWidgets('Renders correctly for live information', (tester) async {
await tester.pumpApp(
buildTestWidget(liveInformationTest),
voicesColors: voicesColors,
);

await tester.pumpAndSettle();

expect(find.byType(InPageInformationCard), findsOneWidget);
expect(find.byType(Text), findsExactly(4));
expect(find.byType(CatalystSvgIcon), findsExactly(2));
expect(
find.text('Campaign Is Live (Published)'),
findsOneWidget,
);
expect(find.text(liveInformationTest.description), findsOneWidget);
expect(find.byType(OutlinedButton), findsOneWidget);
expect(find.text('View proposals'), findsOneWidget);
});

testWidgets('Renders correctly for completed information', (tester) async {
await tester.pumpApp(
buildTestWidget(completedInformationText),
voicesColors: voicesColors,
);

await tester.pumpAndSettle();

expect(find.byType(InPageInformationCard), findsOneWidget);
expect(find.byType(Text), findsExactly(3));
expect(find.byType(CatalystSvgIcon), findsOneWidget);
expect(
find.text('Campaign Concluded, Result are in!'),
findsOneWidget,
);
expect(find.text(completedInformationText.description), findsOneWidget);
expect(find.byType(OutlinedButton), findsOneWidget);
expect(find.text('View Voting Results'), findsOneWidget);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,50 @@
"@noProposalStateTitle": {
"description": "Title shown when there are no proposals in the proposals tab"
},
"campaignIsLive": "Campaign Is Live (Published)",
"@campaignIsLive": {
"description": "Title of the campaign is live (published) space"
},
"campaignStaringSoon": "Campaign Staring Soon (Ready to deploy)",
"@campaignStaringSoon": {
"description": "Title of the campaign staring soon space"
},
"campaignConcluded": "Campaign Concluded, Result are in!",
"@campaignConcluded": {
"description": "Title of the campaign concluded space"
},
"campaignBeginsOn": "Campaign begins on {date} at {time}",
"@campaignBeginsOn": {
"description": "Title of the campaign concluded space",
"placeholders": {
"date": {
"type": "String"
},
"time": {
"type": "String"
}
}
},
"campaignEndsOn": "Campaign ends on {date} at {time}",
"@campaignEndsOn": {
"description": "Title of the campaign concluded space",
"placeholders": {
"date": {
"type": "String"
},
"time": {
"type": "String"
}
}
},
"viewProposals": "View proposals",
"@viewProposals": {
"description": "Title of the view proposals space"
},
"viewVotingResults": "View Voting Results",
"@viewVotingResults": {
"description": "Title of the view voting results space"
},
"campaignDetails": "Campaign Details",
"description": "Description",
"startDate": "Start Date",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export 'navigation/sections_navigation.dart';
export 'proposal/comment.dart';
export 'proposal/guidance/guidance.dart';
export 'proposal/guidance/guidance_type.dart';
export 'proposal/in_page_information.dart';
export 'registration/exception/localized_registration_exception.dart';
export 'registration/registration.dart';
export 'treasury/treasury_sections.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:equatable/equatable.dart';

enum CampaignStage {
draft,
live,
completed;

bool get isCompleted => this == CampaignStage.completed;
bool get isDraft => this == CampaignStage.draft;

String localizedName(VoicesLocalizations l10n) => switch (this) {
CampaignStage.draft => l10n.campaignStaringSoon,
CampaignStage.live => l10n.campaignIsLive,
CampaignStage.completed => l10n.campaignConcluded,
};
}

sealed class InPageInformation extends Equatable {
final CampaignStage stage;
final String description;

const InPageInformation({
required this.stage,
required this.description,
});

@override
List<Object?> get props => [stage, description];
}

class CompletedCampaignInformation extends InPageInformation {
const CompletedCampaignInformation({required super.description})
: super(stage: CampaignStage.completed);
}

class DraftCampaignInformation extends InPageInformation with DateTimeMixin {
@override
final DateTime date;

const DraftCampaignInformation({
LynxLynxx marked this conversation as resolved.
Show resolved Hide resolved
required this.date,
required super.description,
}) : super(stage: CampaignStage.draft);

@override
List<Object?> get props => [date, description];
}

class LiveCampaignInformation extends InPageInformation with DateTimeMixin {
@override
final DateTime date;

const LiveCampaignInformation({
required this.date,
required super.description,
}) : super(stage: CampaignStage.live);

@override
List<Object?> get props => [date, description];
}

mixin DateTimeMixin {
DateTime get date;
CampaignStage get stage;

String localizedDate(
VoicesLocalizations l10n,
(String date, String time) formattedDate,
) {
if (stage == CampaignStage.draft) {
return l10n.campaignBeginsOn(formattedDate.$1, formattedDate.$2);
} else {
return l10n.campaignEndsOn(formattedDate.$1, formattedDate.$2);
}
}
}
LynxLynxx marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading