diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..5f7f0169
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+ "FSharp.suggestGitignore": false,
+ "cSpell.words": [
+ "gruene"
+ ],
+ "files.eol": "\n"
+}
\ No newline at end of file
diff --git a/assets/icons/refresh.svg b/assets/icons/refresh.svg
new file mode 100644
index 00000000..5ac45b29
--- /dev/null
+++ b/assets/icons/refresh.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 00000000..fa0b357c
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
diff --git a/lib/app/theme/theme.dart b/lib/app/theme/theme.dart
index 6d3f46aa..46f32a6a 100644
--- a/lib/app/theme/theme.dart
+++ b/lib/app/theme/theme.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:google_fonts/google_fonts.dart';
class ThemeColors {
@@ -31,7 +32,7 @@ class ThemeColors {
}
class _ThemeTextStyles {
- // TOOD use GrueneType font
+ // TODO use GrueneType font
static TextStyle displayMedium = GoogleFonts.ptSans(
textStyle: TextStyle(
fontSize: 18,
@@ -78,12 +79,12 @@ final ThemeData appTheme = ThemeData.light().copyWith(
primaryColor: ThemeColors.primary,
disabledColor: ThemeColors.textDisabled,
colorScheme: ThemeData.light().colorScheme.copyWith(
- primary: ThemeColors.primary,
- secondary: ThemeColors.secondary,
- tertiary: ThemeColors.tertiary,
- surface: ThemeColors.background,
- surfaceDim: ThemeColors.backgroundSecondary,
- ),
+ primary: ThemeColors.primary,
+ secondary: ThemeColors.secondary,
+ tertiary: ThemeColors.tertiary,
+ surface: ThemeColors.background,
+ surfaceDim: ThemeColors.backgroundSecondary,
+ ),
textTheme: TextTheme(
displayLarge: _ThemeTextStyles.displayLarge,
displayMedium: _ThemeTextStyles.displayMedium,
@@ -101,4 +102,14 @@ final ThemeData appTheme = ThemeData.light().copyWith(
unselectedLabelStyle: _ThemeTextStyles.labelSmall,
),
scaffoldBackgroundColor: ThemeColors.backgroundSecondary,
+ actionIconTheme: ActionIconThemeData(
+ backButtonIconBuilder: (BuildContext context) => SvgPicture.asset('assets/icons/back.svg'),
+ ),
+ tabBarTheme: TabBarTheme(
+ indicatorColor: ThemeColors.primary,
+ indicatorSize: TabBarIndicatorSize.tab,
+ labelStyle: _ThemeTextStyles.titleMedium,
+ unselectedLabelStyle: _ThemeTextStyles.titleMedium,
+ labelColor: ThemeColors.primary,
+ ),
);
diff --git a/lib/app/widgets/app_bar.dart b/lib/app/widgets/app_bar.dart
index 6f29dfee..45345de2 100644
--- a/lib/app/widgets/app_bar.dart
+++ b/lib/app/widgets/app_bar.dart
@@ -12,14 +12,28 @@ class MainAppBar extends StatelessWidget implements PreferredSizeWidget {
final currentRoute = GoRouterState.of(context);
final theme = Theme.of(context);
return AppBar(
- title: Text(currentRoute.name ?? '', style: theme.textTheme.displayMedium?.apply(color: theme.colorScheme.surface)),
+ title: Text(
+ currentRoute.name ?? '',
+ style: theme.textTheme.displayMedium?.apply(color: theme.colorScheme.surface),
+ ),
foregroundColor: theme.colorScheme.surface,
backgroundColor: theme.primaryColor,
centerTitle: true,
actions: [
+ if (currentRoute.path == Routes.campaigns)
+ IconButton(
+ icon: CustomIcon(
+ path: 'assets/icons/refresh.svg',
+ color: ThemeColors.background,
+ ),
+ onPressed: null,
+ ),
if (currentRoute.path != Routes.settings)
IconButton(
- icon: CustomIcon(path: 'assets/icons/settings.svg', color: ThemeColors.background),
+ icon: CustomIcon(
+ path: 'assets/icons/settings.svg',
+ color: ThemeColors.background,
+ ),
onPressed: () => context.push(Routes.settings),
),
],
diff --git a/lib/features/campaigns/screens/campaigns_screen.dart b/lib/features/campaigns/screens/campaigns_screen.dart
index ad866d22..f5379917 100644
--- a/lib/features/campaigns/screens/campaigns_screen.dart
+++ b/lib/features/campaigns/screens/campaigns_screen.dart
@@ -1,12 +1,93 @@
import 'package:flutter/material.dart';
+import 'package:gruene_app/app/theme/theme.dart';
+import 'package:gruene_app/features/campaigns/screens/doors_screen.dart';
+import 'package:gruene_app/features/campaigns/screens/flyer_screen.dart';
+import 'package:gruene_app/features/campaigns/screens/posters_screen.dart';
+import 'package:gruene_app/features/campaigns/screens/statistics_screen.dart';
+import 'package:gruene_app/features/campaigns/screens/teams_screen.dart';
+import 'package:gruene_app/i18n/translations.g.dart';
-class CampaignsScreen extends StatelessWidget {
- const CampaignsScreen({super.key});
+class CampaignsScreen extends StatefulWidget {
+ CampaignsScreen({super.key});
+ @override
+ State createState() => _CampaignsScreen();
+}
+
+class _CampaignsScreen extends State with SingleTickerProviderStateMixin {
+ final List campaignTabs = [
+ CampaignMenuModel(t.campaigns.door.label, true, DoorsScreen()),
+ CampaignMenuModel(t.campaigns.posters.label, true, PostersScreen()),
+ CampaignMenuModel(t.campaigns.flyer.label, true, FlyerScreen()),
+ CampaignMenuModel(t.campaigns.team.label, false, TeamsScreen()),
+ CampaignMenuModel(t.campaigns.statistic.label, false, StatisticsScreen()),
+ ];
+ late TabController _tabController;
+
+ @override
+ void initState() {
+ _tabController = TabController(length: campaignTabs.length, vsync: this);
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ _tabController.dispose();
+ }
+
+ void onTap(int index) {
+ setState(() {
+ _tabController.index = !campaignTabs[index]._enabled ? _tabController.previousIndex : index;
+ });
+ }
@override
Widget build(BuildContext context) {
- return const Center(
- child: Text('Campaigns Screen'),
+ return DefaultTabController(
+ initialIndex: 0,
+ length: campaignTabs.length,
+ child: Scaffold(
+ backgroundColor: ThemeColors.background,
+ appBar: AppBar(
+ shape: Border(bottom: BorderSide(color: ThemeColors.textLight, width: 2)),
+ toolbarHeight: 0,
+ backgroundColor: ThemeColors.backgroundSecondary,
+ bottom: TabBar(
+ controller: _tabController,
+ tabs: campaignTabs
+ .map(
+ (CampaignMenuModel item) => Tab(
+ child: Semantics(
+ child: Text(
+ item._tabTitle,
+ style: item._enabled ? null : TextStyle(color: ThemeColors.textDisabled),
+ ),
+ ),
+ ),
+ )
+ .toList(),
+ isScrollable: true,
+ tabAlignment: TabAlignment.start,
+ indicatorWeight: 4,
+ onTap: (int index) => onTap(index),
+ ),
+ ),
+ body: TabBarView(
+ controller: _tabController,
+ physics: NeverScrollableScrollPhysics(),
+ children: campaignTabs.map((CampaignMenuModel tab) {
+ return tab._view;
+ }).toList(),
+ ),
+ ),
);
}
}
+
+class CampaignMenuModel {
+ final String _tabTitle;
+ final bool _enabled;
+ final Widget _view;
+
+ const CampaignMenuModel(this._tabTitle, this._enabled, this._view);
+}
diff --git a/lib/features/campaigns/screens/doors_screen.dart b/lib/features/campaigns/screens/doors_screen.dart
new file mode 100644
index 00000000..e6715a56
--- /dev/null
+++ b/lib/features/campaigns/screens/doors_screen.dart
@@ -0,0 +1,28 @@
+import 'package:flutter/material.dart';
+import 'package:gruene_app/features/campaigns/widgets/filter_chip_widget.dart';
+import 'package:gruene_app/i18n/translations.g.dart';
+
+class DoorsScreen extends StatelessWidget {
+ DoorsScreen({super.key});
+
+ final List doorsFilter = [
+ FilterChipModel(t.campaigns.filters.visited_areas, false),
+ FilterChipModel(t.campaigns.filters.routes, false),
+ FilterChipModel(t.campaigns.filters.focusAreas, true),
+ FilterChipModel(t.campaigns.filters.experience_areas, false),
+ ];
+ final Map> doorsExclusions = >{
+ t.campaigns.filters.focusAreas: [t.campaigns.filters.visited_areas],
+ t.campaigns.filters.visited_areas: [t.campaigns.filters.focusAreas],
+ };
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ FilterChipCampaign(doorsFilter, doorsExclusions),
+ Center(child: Text(t.campaigns.door.label)),
+ ],
+ );
+ }
+}
diff --git a/lib/features/campaigns/screens/flyer_screen.dart b/lib/features/campaigns/screens/flyer_screen.dart
new file mode 100644
index 00000000..1fd6569c
--- /dev/null
+++ b/lib/features/campaigns/screens/flyer_screen.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/widgets.dart';
+import 'package:gruene_app/features/campaigns/widgets/filter_chip_widget.dart';
+import 'package:gruene_app/i18n/translations.g.dart';
+
+class FlyerScreen extends StatelessWidget {
+ FlyerScreen({super.key});
+
+ final List flyerFilter = [
+ FilterChipModel(t.campaigns.filters.visited_areas, false),
+ FilterChipModel(t.campaigns.filters.routes, false),
+ FilterChipModel(t.campaigns.filters.experience_areas, false),
+ ];
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ FilterChipCampaign(flyerFilter, >{}),
+ Center(child: Text(t.campaigns.flyer.label)),
+ ],
+ );
+ }
+}
diff --git a/lib/features/campaigns/screens/posters_screen.dart b/lib/features/campaigns/screens/posters_screen.dart
new file mode 100644
index 00000000..48fe1458
--- /dev/null
+++ b/lib/features/campaigns/screens/posters_screen.dart
@@ -0,0 +1,22 @@
+import 'package:flutter/widgets.dart';
+import 'package:gruene_app/features/campaigns/widgets/filter_chip_widget.dart';
+import 'package:gruene_app/i18n/translations.g.dart';
+
+class PostersScreen extends StatelessWidget {
+ PostersScreen({super.key});
+
+ final List postersFilter = [
+ FilterChipModel(t.campaigns.filters.routes, false),
+ FilterChipModel(t.campaigns.filters.polling_stations, false),
+ FilterChipModel(t.campaigns.filters.experience_areas, false),
+ ];
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ FilterChipCampaign(postersFilter, >{}),
+ Center(child: Text(t.campaigns.posters.label)),
+ ],
+ );
+ }
+}
diff --git a/lib/features/campaigns/screens/statistics_screen.dart b/lib/features/campaigns/screens/statistics_screen.dart
new file mode 100644
index 00000000..83ecc3cb
--- /dev/null
+++ b/lib/features/campaigns/screens/statistics_screen.dart
@@ -0,0 +1,20 @@
+import 'package:flutter/material.dart';
+import 'package:gruene_app/app/theme/theme.dart';
+import 'package:gruene_app/i18n/translations.g.dart';
+
+class StatisticsScreen extends StatelessWidget {
+ const StatisticsScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Placeholder(
+ color: Colors.red,
+ child: Center(
+ child: Text(
+ t.campaigns.statistic.label,
+ style: TextStyle(fontSize: 20, color: ThemeColors.primary),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/campaigns/screens/teams_screen.dart b/lib/features/campaigns/screens/teams_screen.dart
new file mode 100644
index 00000000..470c5a62
--- /dev/null
+++ b/lib/features/campaigns/screens/teams_screen.dart
@@ -0,0 +1,20 @@
+import 'package:flutter/material.dart';
+import 'package:gruene_app/app/theme/theme.dart';
+import 'package:gruene_app/i18n/translations.g.dart';
+
+class TeamsScreen extends StatelessWidget {
+ const TeamsScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Placeholder(
+ color: Colors.red,
+ child: Center(
+ child: Text(
+ t.campaigns.team.label,
+ style: TextStyle(fontSize: 20, color: ThemeColors.primary),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/campaigns/widgets/filter_chip_widget.dart b/lib/features/campaigns/widgets/filter_chip_widget.dart
new file mode 100644
index 00000000..612585e4
--- /dev/null
+++ b/lib/features/campaigns/widgets/filter_chip_widget.dart
@@ -0,0 +1,96 @@
+import 'package:flutter/material.dart';
+import 'package:gruene_app/app/theme/theme.dart';
+
+class FilterChipModel {
+ final String _text;
+ final bool _isEnabled;
+
+ const FilterChipModel(this._text, this._isEnabled);
+}
+
+class FilterChipCampaign extends StatefulWidget {
+ final List filterOptions;
+ final Map> filterExclusions;
+
+ const FilterChipCampaign(
+ this.filterOptions,
+ this.filterExclusions, {
+ super.key,
+ });
+
+ @override
+ State createState() => _FilterChipCampaignState();
+}
+
+class _FilterChipCampaignState extends State {
+ Set currentFilters = {};
+
+ void unselect(String itemLabel) {
+ currentFilters.removeWhere((z) => z._text == itemLabel);
+ }
+
+ void unselectExclusions(FilterChipModel item) {
+ var filterExclusions = widget.filterExclusions;
+ filterExclusions.entries.where((x) => x.key == item._text).map((x) => x.value).forEach((x) => x.forEach(unselect));
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Align(
+ alignment: Alignment.centerLeft,
+ child: Padding(
+ padding: EdgeInsets.only(left: 12),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Wrap(
+ spacing: 15.0,
+ children: widget.filterOptions.map((filterItem) {
+ return FilterChip(
+ label: Text(filterItem._text),
+ backgroundColor: ThemeColors.background,
+ selectedColor: ThemeColors.primary,
+ padding: EdgeInsets.zero,
+ side: filterItem._isEnabled
+ ? BorderSide(color: ThemeColors.primary, width: 2)
+ : BorderSide(color: ThemeColors.textDisabled, width: 1),
+ shape: StadiumBorder(),
+ selected: currentFilters.contains(filterItem),
+ showCheckmark: false,
+ labelStyle: TextStyle(
+ color: filterItem._isEnabled ? ChipLabelColor() : ThemeColors.textDisabled,
+ ),
+ onSelected: (bool selected) {
+ setState(() {
+ if (!filterItem._isEnabled) return;
+ if (selected) {
+ unselectExclusions(filterItem);
+ currentFilters.add(filterItem);
+ } else {
+ currentFilters.remove(filterItem);
+ }
+ });
+ },
+ );
+ }).toList(),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class ChipLabelColor extends Color implements WidgetStateColor {
+ const ChipLabelColor() : super(_default);
+
+ static const int _default = 0xFF000000;
+
+ @override
+ Color resolve(Set states) {
+ if (states.contains(WidgetState.selected)) {
+ return Colors.white; // Selected text color
+ }
+ return Colors.black; // normal text color
+ }
+}
diff --git a/lib/i18n/app_de.json b/lib/i18n/app_de.json
index dd606ce6..2cc6304f 100644
--- a/lib/i18n/app_de.json
+++ b/lib/i18n/app_de.json
@@ -8,7 +8,29 @@
},
"campaigns": {
"campaigns": "Wahlkampf",
- "label": "Wahlkampf"
+ "label": "Wahlkampf",
+ "filters": {
+ "focusAreas": "Fokusgebiete",
+ "routes": "Routen",
+ "visited_areas": "Besuchte Gebiete",
+ "polling_stations": "Wahllokale",
+ "experience_areas": "Erfahrungsgebiete"
+ },
+ "posters": {
+ "label": "Plakate"
+ },
+ "door": {
+ "label": "Haustür"
+ },
+ "flyer": {
+ "label": "Flyer"
+ },
+ "team": {
+ "label": "Team"
+ },
+ "statistic": {
+ "label": "Statistik"
+ }
},
"profiles": {
"profiles": "Mitglieder",