glanzlicht / MeinWeimar_APP

Official MeinWeimar App

Verified secure showcase
204 Files 2 MB Public size 14 Redacted Dart · PHP · XML · GITIGNORE · YAML · PLIST

Project overview

MeinWeimar_APP is a verified code showcase with 204 visible project entries across 2 top-level modules. The primary detected language is Dart; protected files remain visible in the structure while their contents stay private.

README

MeinWeimar — Flutter App & WordPress Plugin

> Working in Progress · Private portfolio repository · Active development

MeinWeimar is a location-based local platform for residents, students and visitors. This repository intentionally contains only two product parts:

  1. the Flutter consumer app;
  2. the meinweimar-core WordPress plugin that provides the editorial/admin
  3. layer and the REST contract consumed by the app.

The complete public website, WordPress core, databases, private infrastructure and unrelated platform modules are not part of this repository.

MeinWeimar consumer app preview

Why this project is interesting

MeinWeimar combines a mobile/web Flutter client with WordPress used as a structured content and administration backend. The goal is a maintainable product boundary: content, branding, navigation, categories, media and configuration are managed in WordPress and delivered through a versioned REST contract instead of being hardcoded in the Flutter UI.

The project demonstrates work across:

  • Flutter/Dart application architecture for iOS, Android and Web;
  • WordPress plugin architecture with custom post types, taxonomies, admin
  • settings and REST endpoints;

  • localized German/English UI and backend-controlled labels;
  • discovery/list/detail flows for places, services, events, classifieds, jobs
  • and real estate;

  • map, GPS, geocoding, marker and navigation contracts;
  • caching, ETag/config-version handling and offline-safe fallbacks;
  • role-aware account, claim, favorites, moderation and messaging boundaries;
  • automated Flutter tests, PHP/REST contract checks and release builds.

Repository structure

flutter-app/       Flutter consumer application
wordpress-plugin/  meinweimar-core WordPress plugin
screenshots/       Curated portfolio preview only

Current status

This is an active Working in Progress project. The core architecture and many end-to-end flows are implemented and continuously tested. Native device polish, production content, final legal/privacy review and further product modules are still being developed. The repository does not claim to be a finished production release.

Local development

Flutter app

cd flutter-app
flutter pub get
flutter analyze
flutter test
flutter run --dart-define=MW_API_BASE_URL=https://example.test/wp-json/meinweimar/v1

The API base URL and other environment-specific values are supplied at build or run time. No credentials are stored in the repository.

WordPress plugin

Copy wordpress-plugin into a WordPress installation's wp-content/plugins/meinweimar-core directory and activate it in the admin area. The plugin owns the MeinWeimar content types, editorial settings, permissions, REST responses, media/configuration contracts and frontend integration points.

Privacy and repository boundary

This repository is private and is prepared for controlled technical review. Secrets, production database exports, user data, WordPress uploads, private keys and infrastructure configuration must remain outside version control. The public-facing portfolio page uses a separate, redacted presentation layer and does not expose the private repository directly.

For technical reviewers

The most relevant entry points are:

  • flutter-app/lib/core — shared configuration, networking, localization,
  • theme and location contracts;

  • flutter-app/lib/features — consumer-facing product surfaces;
  • flutter-app/lib/repositories — API boundary and data access;
  • wordpress-plugin/includes — plugin services, REST, admin and contracts;
  • wordpress-plugin/assets — admin/frontend integration assets.

MeinWeimar is deliberately presented as an evolving product rather than a finished demo: the repository shows the architectural decisions, the working implementation and the remaining engineering work transparently.

flutter-app/test/features/discovery_page_location_test.dartDart
import 'package:consumer_app/core/location/location_service.dart';
import 'package:consumer_app/core/location/location_controller.dart';
import 'package:consumer_app/core/config/app_config.dart';
import 'package:consumer_app/features/discovery/discovery_page.dart';
import 'package:consumer_app/l10n/app_localizations.dart';
import 'package:consumer_app/models/content_models.dart';
import 'package:consumer_app/repositories/discovery_repository.dart';
import 'package:consumer_app/repositories/geo_repository.dart';
import 'package:consumer_app/shared/map/map_surface.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:geolocator/geolocator.dart';
void main() {
testWidgets('location denial reloads discovery without private coordinates', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
titleBuilder: (l10n) => l10n.placesTitle,
locationService: LocationService(
provider: _DeniedLocationProvider(),
),
),
),
),
);
await tester.pumpAndSettle();
final locationButton = find.widgetWithText(
OutlinedButton,
'Meinen Standort verwenden',
);
await tester.ensureVisible(locationButton);
await tester.tap(locationButton);
await tester.pumpAndSettle();
expect(repository.calls, greaterThanOrEqualTo(2));
expect(repository.lastLatitude, isNull);
expect(repository.lastLongitude, isNull);
expect(find.textContaining('Standort nicht freigegeben'), findsOneWidget);
});
testWidgets('discovery map/list control renders only the active surface', (
tester,
) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: _RecordingDiscoveryRepository(),
titleBuilder: (l10n) => l10n.placesTitle,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(MapSurface), findsNothing);
await tester.tap(find.text('Karte'));
await tester.pumpAndSettle();
expect(find.byType(MapSurface), findsOneWidget);
await tester.tap(find.text('Liste'));
await tester.pumpAndSettle();
expect(find.byType(MapSurface), findsNothing);
});
testWidgets('Jobs route renders the reference listing and filter sheet', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
types: const ['jobs'],
allowMap: false,
titleBuilder: (l10n) => l10n.moduleJobs,
fieldSchemas: const {
'listing.employment_type': ContentFieldSchema(
type: 'string',
required: false,
input: 'enum',
readOnly: false,
values: ['full_time', 'part_time', 'minijob'],
filterEnabled: true,
filterKind: 'enum',
),
'listing.work_mode': ContentFieldSchema(
type: 'string',
required: false,
input: 'enum',
readOnly: false,
values: ['onsite', 'hybrid', 'remote'],
filterEnabled: true,
filterKind: 'enum',
),
'listing.salary_min': ContentFieldSchema(
type: 'number',
required: false,
input: 'number',
readOnly: false,
filterEnabled: true,
filterKind: 'range',
),
},
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Jobs in Weimar'), findsOneWidget);
expect(find.text('Vollzeit'), findsOneWidget);
expect(find.text('Filter'), findsOneWidget);
expect(repository.lastFilters['job_salary_interval'], 'month');
await tester.tap(find.text('Filter').last);
await tester.pumpAndSettle();
expect(find.text('Arbeitsmodell'), findsOneWidget);
expect(find.text('Beschäftigungsart'), findsOneWidget);
await tester.drag(find.byType(ListView).last, const Offset(0, -240));
await tester.pumpAndSettle();
expect(find.text('Gehalt (brutto)'), findsOneWidget);
expect(find.text('Brutto monatlich'), findsWidgets);
expect(find.text('Brutto jährlich'), findsWidgets);
final yearlyChoice = find.widgetWithText(ChoiceChip, 'Brutto jährlich');
await tester.tap(yearlyChoice);
await tester.pumpAndSettle();
expect(find.text('Jobs anzeigen'), findsOneWidget);
await tester.tap(find.text('Jobs anzeigen'));
await tester.pumpAndSettle();
expect(repository.lastFilters['job_salary_interval'], 'year');
});
testWidgets('Jobs filter can use GPS instead of entering an address', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
types: const ['jobs'],
allowMap: false,
titleBuilder: (l10n) => l10n.moduleJobs,
locationService: LocationService(
provider: _GrantedLocationProvider(),
),
),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Filter').last);
await tester.pumpAndSettle();
expect(find.text('Meinen Standort verwenden'), findsOneWidget);
await tester.tap(find.text('Meinen Standort verwenden'));
await tester.pumpAndSettle();
expect(find.text('Ausgewählter Standort: Mein Standort'), findsOneWidget);
expect(repository.lastLatitude, closeTo(50.98, 0.000001));
expect(repository.lastLongitude, closeTo(11.32, 0.000001));
});
testWidgets('Jobs uses the shared geo discovery shell', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: _RecordingDiscoveryRepository(),
types: const ['jobs'],
titleBuilder: (l10n) => l10n.moduleJobs,
geoRepository: _EmptyGeoRepository(),
geoConfig: GeoConfig.fallback(),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Meinen Standort verwenden'), findsOneWidget);
expect(find.text('Liste'), findsOneWidget);
expect(find.text('Karte'), findsOneWidget);
expect(find.textContaining('Radius:'), findsOneWidget);
expect(find.text('Filter'), findsOneWidget);
});
testWidgets('shared geo filter uses the route-specific apply label', (
tester,
) async {
const routes = <({String type, String label})>[
(type: 'events', label: 'Events anzeigen'),
(type: 'services', label: 'Services anzeigen'),
(type: 'classifieds', label: 'Kleinanzeigen anzeigen'),
(type: 'real-estate', label: 'Immobilien anzeigen'),
];
for (final route in routes) {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: _RecordingDiscoveryRepository(),
types: [route.type],
titleBuilder: (l10n) => l10n.discoveryTitle,
geoRepository: _EmptyGeoRepository(),
geoConfig: GeoConfig.fallback(),
),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Filter'));
await tester.pumpAndSettle();
expect(find.text(route.label), findsOneWidget);
await tester.tap(find.text(route.label));
await tester.pumpAndSettle();
}
});
testWidgets('renders schema-driven listing filters and forwards selection', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
titleBuilder: (l10n) => l10n.moduleJobs,
fieldSchemas: const {
'listing.offer_type': ContentFieldSchema(
type: 'string',
required: false,
input: 'enum',
readOnly: false,
values: ['offer', 'employment'],
filterEnabled: true,
filterKind: 'enum',
),
},
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Angebotsart'), findsOneWidget);
await tester.tap(find.byType(DropdownButtonFormField<String>));
await tester.pumpAndSettle();
await tester.tap(find.text('Stelle / Beschäftigung').last);
await tester.pumpAndSettle();
expect(repository.lastFilters['offer_type'], 'employment');
});
testWidgets('renders schema-driven range filters and forwards bounds', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
types: const ['real-estate'],
titleBuilder: (l10n) => l10n.moduleRealEstate,
fieldSchemas: const {
'listing.area': ContentFieldSchema(
type: 'number',
required: false,
input: 'number',
readOnly: false,
filterEnabled: true,
filterKind: 'range',
unit: 'm²',
),
},
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Fläche'), findsOneWidget);
expect(find.text('Von'), findsOneWidget);
expect(find.text('Bis'), findsOneWidget);
final fields = find.byType(TextFormField);
expect(fields, findsNWidgets(2));
await tester.enterText(fields.at(0), '50');
await tester.enterText(fields.at(1), '120');
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
expect(repository.lastFilters['area_min'], '50');
expect(repository.lastFilters['area_max'], '120');
});
testWidgets('category selection forwards the canonical taxonomy slug', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
const categorySchema = ContentCategorySchema(
taxonomy: 'mw_classified_category',
profiles: {
'electronics': ContentCategoryProfile(
slugs: ['electronics'],
labels: {'electronics': 'Elektronik'},
fields: ['brand'],
requiredFields: [],
),
},
);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
types: const ['classifieds'],
titleBuilder: (l10n) => l10n.moduleClassifieds,
categorySchema: categorySchema,
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Kategorie'), findsOneWidget);
await tester.tap(find.widgetWithText(ChoiceChip, 'Elektronik'));
await tester.pumpAndSettle();
expect(repository.lastCategory, 'electronics');
});
testWidgets('Places categories come from the API taxonomy hierarchy', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
const categorySchema = ContentCategorySchema(
taxonomy: 'mw_place_category',
profiles: {
'gastronomy': ContentCategoryProfile(
slugs: ['restaurants', 'imbiss'],
labels: {},
fields: [],
requiredFields: [],
),
},
);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
types: const ['places'],
titleBuilder: (l10n) => l10n.placesTitle,
categorySchema: categorySchema,
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Kunst & Kultur'), findsOneWidget);
expect(find.text('Museum'), findsNothing);
expect(find.text('Dienstleistungen'), findsNothing);
await tester.tap(find.text('Kunst & Kultur'));
await tester.pumpAndSettle();
expect(find.text('Museum'), findsOneWidget);
await tester.tap(find.text('Museum'));
await tester.pumpAndSettle();
expect(repository.lastCategory, 'museum-2');
});
testWidgets(
'Services do not render FieldSchemas profiles when the API has no categories',
(tester) async {
final repository = _RecordingDiscoveryRepository();
const categorySchema = ContentCategorySchema(
taxonomy: 'mw_service_category',
profiles: {
'services': ContentCategoryProfile(
slugs: ['services'],
labels: {'services': 'Dienstleistungen'},
fields: [],
requiredFields: [],
),
},
);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
types: const ['services'],
allowMap: true,
titleBuilder: (l10n) => l10n.moduleServices,
categorySchema: categorySchema,
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Kategorie'), findsNothing);
expect(find.text('Dienstleistungen'), findsNothing);
expect(find.text('Nichts gefunden'), findsOneWidget);
expect(repository.lastCategory, isNull);
},
);
testWidgets('Guides never render a legacy address in the discovery card', (
tester,
) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: _RecordingDiscoveryRepository(),
types: const ['guides'],
allowMap: false,
titleBuilder: (l10n) => l10n.moduleGuides,
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Guide mit Legacy-Adresse'), findsOneWidget);
expect(find.text('Alte Straße 1, 99423 Weimar'), findsNothing);
});
testWidgets('editorial resources never send geo query parameters', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
types: const ['news'],
allowMap: false,
titleBuilder: (l10n) => l10n.moduleNews,
),
),
),
);
await tester.pumpAndSettle();
expect(repository.lastLatitude, isNull);
expect(repository.lastLongitude, isNull);
expect(repository.lastRadiusKm, isNull);
});
testWidgets('editorial resources never render a geo access notice', (
tester,
) async {
final locationController = LocationController(
service: LocationService(provider: _DeniedLocationProvider()),
)..access = LocationAccess.denied;
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: _RecordingDiscoveryRepository(),
types: const ['news'],
allowMap: false,
locationController: locationController,
titleBuilder: (l10n) => l10n.moduleNews,
),
),
),
);
await tester.pumpAndSettle();
expect(find.textContaining('Standort nicht freigegeben'), findsNothing);
});
testWidgets(
'Places filter renders categories, typed labels and applies radius',
(tester) async {
final repository = _RecordingDiscoveryRepository();
const categorySchema = ContentCategorySchema(
taxonomy: 'mw_place_category',
profiles: {
'gastronomy': ContentCategoryProfile(
slugs: ['restaurants'],
labels: {},
fields: [],
requiredFields: [],
),
},
);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
types: const ['places'],
allowMap: true,
titleBuilder: (l10n) => l10n.placesTitle,
categorySchema: categorySchema,
fieldSchemas: const {
'place_details.rating_average': ContentFieldSchema(
type: 'number',
required: false,
input: 'number',
readOnly: false,
filterEnabled: true,
filterKind: 'range',
),
'place_details.featured': ContentFieldSchema(
type: 'boolean',
required: false,
input: 'boolean',
readOnly: false,
filterEnabled: true,
filterKind: 'boolean',
),
},
),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Filter'));
await tester.pumpAndSettle();
expect(find.text('Kunst & Kultur'), findsNWidgets(2));
expect(find.text('Dienstleistungen'), findsNothing);
expect(find.text('Bewertung'), findsOneWidget);
expect(find.text('Hervorgehoben'), findsOneWidget);
expect(find.text('Von'), findsOneWidget);
expect(find.text('Bis'), findsOneWidget);
final slider = find.byType(Slider);
// The shared discovery page keeps the same radius control visible in
// the page toolbar while the filter sheet is open. Interact with the
// sheet instance, not the covered page instance.
expect(slider, findsNWidgets(2));
await tester.drag(slider.last, const Offset(100, 0));
await tester.pumpAndSettle();
await tester.tap(find.text('Orte anzeigen'));
await tester.pumpAndSettle();
expect(repository.lastRadiusKm, greaterThan(10));
},
);
testWidgets('category-dependent filters follow the public category schema', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
const categorySchema = ContentCategorySchema(
taxonomy: 'mw_classified_category',
profiles: {
'electronics': ContentCategoryProfile(
slugs: ['electronics'],
labels: {'electronics': 'Elektronik'},
fields: ['brand'],
requiredFields: [],
),
'services': ContentCategoryProfile(
slugs: ['services'],
labels: {'services': 'Dienstleistungen'},
fields: [],
requiredFields: [],
),
},
);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
types: const ['classifieds'],
titleBuilder: (l10n) => l10n.moduleClassifieds,
categorySchema: categorySchema,
fieldSchemas: const {
'listing.brand': ContentFieldSchema(
type: 'string',
required: false,
input: 'enum',
readOnly: false,
values: ['brand-a'],
filterEnabled: true,
filterKind: 'enum',
categoryConditions: ['electronics'],
conditions: [
{
'field': 'category',
'operator': 'in',
'values': ['electronics'],
},
],
),
},
),
),
),
);
await tester.pumpAndSettle();
// Detail filters are progressive: choose a category first.
expect(find.text('Marke'), findsNothing);
await tester.tap(find.widgetWithText(ChoiceChip, 'Dienstleistungen'));
await tester.pumpAndSettle();
expect(find.text('Marke'), findsNothing);
await tester.tap(find.widgetWithText(ChoiceChip, 'Elektronik'));
await tester.pumpAndSettle();
expect(find.text('Marke'), findsOneWidget);
});
testWidgets('required category fields remain available to filters', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
const categorySchema = ContentCategorySchema(
taxonomy: 'mw_property_category',
profiles: {
'apartment': ContentCategoryProfile(
slugs: ['apartment'],
labels: {'apartment': 'Wohnung'},
fields: [],
requiredFields: ['area'],
),
'land': ContentCategoryProfile(
slugs: ['land'],
labels: {'land': 'Grundstück'},
fields: ['area'],
requiredFields: ['area'],
),
},
);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
types: const ['real-estate'],
titleBuilder: (l10n) => l10n.moduleRealEstate,
categorySchema: categorySchema,
fieldSchemas: const {
'listing.area': ContentFieldSchema(
type: 'number',
required: false,
input: 'number',
readOnly: false,
filterEnabled: true,
filterKind: 'range',
categoryConditions: ['apartment', 'land'],
conditions: [
{
'field': 'category',
'operator': 'in',
'values': ['apartment', 'land'],
},
],
),
},
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Fläche'), findsNothing);
await tester.tap(find.widgetWithText(ChoiceChip, 'Wohnung'));
await tester.pumpAndSettle();
expect(find.text('Fläche'), findsOneWidget);
});
testWidgets(
'real estate discovery uses transaction segments and category chips',
(tester) async {
final repository = _RecordingDiscoveryRepository();
const categorySchema = ContentCategorySchema(
taxonomy: 'mw_property_category',
profiles: {
'apartment': ContentCategoryProfile(
slugs: ['apartment'],
labels: {'apartment': 'Wohnung'},
fields: [],
requiredFields: [],
),
'land': ContentCategoryProfile(
slugs: ['land'],
labels: {'land': 'Grundstück'},
fields: [],
requiredFields: [],
),
'wg-room': ContentCategoryProfile(
slugs: ['room', 'wg-zimmer'],
labels: {},
fields: [],
requiredFields: [],
),
'temporary-living': ContentCategoryProfile(
slugs: ['temporary-living', 'wohnen-auf-zeit'],
labels: {},
fields: [],
requiredFields: [],
),
'office-practice': ContentCategoryProfile(
slugs: ['office-practice', 'buero-praxis'],
labels: {'office-practice': 'Büro / Praxis'},
fields: [],
requiredFields: [],
),
},
);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
types: const ['real-estate'],
titleBuilder: (l10n) => l10n.moduleRealEstate,
categorySchema: categorySchema,
fieldSchemas: const {
'listing.transaction_type': ContentFieldSchema(
type: 'string',
required: false,
input: 'enum',
readOnly: false,
// Legacy aliases may still be present in a cached/older
// bootstrap; they must not become duplicate UI segments.
values: ['sale', 'rent', 'lease', 'temporary'],
filterEnabled: true,
filterKind: 'enum',
),
'listing.commission_applicable': ContentFieldSchema(
type: 'string',
required: false,
input: 'enum',
readOnly: false,
values: ['no', 'yes'],
filterEnabled: true,
filterKind: 'enum',
categoryConditions: ['apartment', 'land'],
conditions: [
{
'field': 'category',
'operator': 'in',
'values': ['apartment', 'land'],
},
],
),
},
),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(SegmentedButton<String>), findsOneWidget);
expect(find.text('Kaufen'), findsOneWidget);
expect(find.text('Miete'), findsOneWidget);
expect(find.widgetWithText(ChoiceChip, 'Wohnung'), findsNothing);
expect(find.widgetWithText(ChoiceChip, 'Grundstück'), findsNothing);
expect(find.text('Provision fällig?'), findsNothing);
await tester.tap(find.text('Kaufen').first);
await tester.pumpAndSettle();
expect(repository.lastFilters['transaction_type'], 'sale');
expect(find.widgetWithText(ChoiceChip, 'Wohnung'), findsOneWidget);
expect(find.widgetWithText(ChoiceChip, 'Grundstück'), findsOneWidget);
expect(find.widgetWithText(ChoiceChip, 'WG-Zimmer'), findsOneWidget);
expect(
find.widgetWithText(ChoiceChip, 'Wohnen auf Zeit'),
findsOneWidget,
);
expect(find.widgetWithText(ChoiceChip, 'Büro / Praxis'), findsOneWidget);
await tester.tap(find.widgetWithText(ChoiceChip, 'Wohnung'));
await tester.pumpAndSettle();
expect(repository.lastCategory, 'apartment');
},
);
testWidgets('dedicated classifieds discovery uses category chips', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
const categorySchema = ContentCategorySchema(
taxonomy: 'mw_classified_category',
profiles: {
'electronics': ContentCategoryProfile(
slugs: ['electronics'],
labels: {'electronics': 'Elektronik'},
fields: [],
requiredFields: [],
),
},
);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
types: const ['classifieds'],
titleBuilder: (l10n) => l10n.moduleClassifieds,
categorySchema: categorySchema,
),
),
),
);
await tester.pumpAndSettle();
expect(find.widgetWithText(ChoiceChip, 'Elektronik'), findsOneWidget);
expect(find.byType(DropdownButtonFormField<String>), findsNothing);
await tester.tap(find.widgetWithText(ChoiceChip, 'Elektronik'));
await tester.pumpAndSettle();
expect(repository.lastCategory, 'electronics');
});
testWidgets('schema dependent filters support an in condition', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
titleBuilder: (l10n) => l10n.moduleJobs,
fieldSchemas: const {
'listing.offer_type': ContentFieldSchema(
type: 'string',
required: false,
input: 'enum',
readOnly: false,
values: ['offer', 'employment'],
filterEnabled: true,
filterKind: 'enum',
),
'listing.employment_type': ContentFieldSchema(
type: 'string',
required: false,
input: 'enum',
readOnly: false,
values: ['full_time', 'part_time'],
filterEnabled: true,
filterKind: 'enum',
conditions: [
{
'field': 'listing.offer_type',
'operator': 'in',
'values': ['employment'],
},
],
),
},
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Beschäftigungsart'), findsNothing);
final offerType = find.byType(DropdownButtonFormField<String>).first;
await tester.tap(offerType);
await tester.pumpAndSettle();
await tester.tap(find.text('Stelle / Beschäftigung').last);
await tester.pumpAndSettle();
expect(find.text('Beschäftigungsart'), findsOneWidget);
expect(repository.lastFilters['offer_type'], 'employment');
});
testWidgets('schema dependent filters normalize condition whitespace', (
tester,
) async {
final repository = _RecordingDiscoveryRepository();
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(
body: DiscoveryPage(
repository: repository,
titleBuilder: (l10n) => l10n.moduleJobs,
fieldSchemas: const {
'listing.offer_type': ContentFieldSchema(
type: 'string',
required: false,
input: 'enum',
readOnly: false,
values: ['employment'],
filterEnabled: true,
filterKind: 'enum',
),
'listing.application_method': ContentFieldSchema(
type: 'string',
required: false,
input: 'enum',
readOnly: false,
values: ['url'],
filterEnabled: true,
filterKind: 'enum',
conditions: [
{
'field': 'listing.offer_type',
'operator': 'equals',
'value': ' employment ',
},
],
),
},
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Bewerbungsweg'), findsNothing);
await tester.tap(find.byType(DropdownButtonFormField<String>).first);
await tester.pumpAndSettle();
await tester.tap(find.text('Stelle / Beschäftigung').last);
await tester.pumpAndSettle();
expect(find.text('Bewerbungsweg'), findsOneWidget);
});
}
class _RecordingDiscoveryRepository implements DiscoveryRepository {
int calls = 0;
double? lastLatitude;
double? lastLongitude;
double? lastRadiusKm = 10;
String? lastCategory;
Map<String, String> lastFilters = const {};
@override
Future<DiscoveryResponse> discover({
required List<String> types,
String? search,
double? latitude,
double? longitude,
double? radiusKm = 10,
int page = 1,
int perPage = 40,
String? category,
String? region,
String? status,
Map<String, String> filters = const {},
String sort = 'auto',
double? minLatitude,
double? maxLatitude,
double? minLongitude,
double? maxLongitude,
}) async {
calls++;
lastLatitude = latitude;
lastLongitude = longitude;
lastRadiusKm = radiusKm;
lastCategory = category;
lastFilters = filters;
final categories = types.length == 1 && types.first == 'places'
? [
{
'slug': 'freizeit-erleben',
'name': 'Freizeit & Erleben',
'parent_slug': '',
'count': 68,
'taxonomy': 'mw_place_category',
},
{
'slug': 'kunst-kultur',
'name': 'Kunst & Kultur',
'parent_slug': '',
'count': 32,
'taxonomy': 'mw_place_category',
},
{
'slug': 'spielplaetze',
'name': 'Spielplätze',
'parent_slug': '',
'count': 68,
'taxonomy': 'mw_place_category',
},
{
'slug': 'museum-2',
'name': 'Museum',
'parent_slug': 'kunst-kultur',
'count': 12,
'taxonomy': 'mw_place_category',
},
]
: types.length == 1 && types.first == 'classifieds'
? [
{
'slug': 'electronics',
'name': 'Elektronik',
'parent_slug': '',
'count': 4,
'taxonomy': 'mw_classified_category',
},
{
'slug': 'services',
'name': 'Dienstleistungen',
'parent_slug': '',
'count': 2,
'taxonomy': 'mw_classified_category',
},
]
: types.length == 1 && types.first == 'real-estate'
? [
{
'slug': 'apartment',
'name': 'Wohnung',
'parent_slug': '',
'count': 4,
'taxonomy': 'mw_property_category',
},
{
'slug': 'land',
'name': 'Grundstück',
'parent_slug': '',
'count': 2,
'taxonomy': 'mw_property_category',
},
{
'slug': 'wg-zimmer',
'name': 'WG-Zimmer',
'parent_slug': '',
'count': 2,
'taxonomy': 'mw_property_category',
},
{
'slug': 'wohnen-auf-zeit',
'name': 'Wohnen auf Zeit',
'parent_slug': '',
'count': 2,
'taxonomy': 'mw_property_category',
},
{
'slug': 'office-practice',
'name': 'Büro / Praxis',
'parent_slug': '',
'count': 2,
'taxonomy': 'mw_property_category',
},
]
: types.length == 1 && types.first == 'jobs'
? [
{
'slug': 'it-tech',
'name': 'IT & Technik',
'parent_slug': '',
'count': 3,
'taxonomy': 'mw_job_category',
},
]
: const [];
final items = types.length == 1 && types.first == 'guides'
? [
{
'id': 901,
'resource': 'guides',
'title': 'Guide mit Legacy-Adresse',
'excerpt': 'Redaktioneller Guide',
'geo': {'available': true, 'lat': 50.98, 'lng': 11.32},
'address': {
'line1': 'Alte Straße 1',
'locality': 'Weimar',
'postal_code': '99423',
},
},
]
: const [];
return DiscoveryResponse.fromJson({
'items': items,
'meta': {
'page': 1,
'per_page': 40,
'total': 0,
'total_pages': 0,
'categories': categories,
},
});
}
@override
Future<DiscoveryItem> detail({required String resource, required int id}) {
throw UnimplementedError();
}
}
class _DeniedLocationProvider implements LocationProvider {
@override
Future<bool> isLocationServiceEnabled() async => true;
@override
Future<LocationPermission> checkPermission() async =>
LocationPermission.denied;
@override
Future<LocationPermission> requestPermission() async =>
LocationPermission.denied;
@override
Future<Position> getCurrentPosition() async => throw StateError('denied');
}
class _EmptyGeoRepository implements GeoRepository {
@override
Future<List<GeoSearchResult>> searchAddress(
String query, {
int limit = 5,
}) async => const [];
}
class _GrantedLocationProvider implements LocationProvider {
@override
Future<bool> isLocationServiceEnabled() async => true;
@override
Future<LocationPermission> checkPermission() async =>
LocationPermission.whileInUse;
@override
Future<LocationPermission> requestPermission() async =>
LocationPermission.whileInUse;
@override
Future<Position> getCurrentPosition() async => Position(
latitude: 50.98,
longitude: 11.32,
timestamp: DateTime.utc(2026, 9, 21),
accuracy: 5,
altitude: 0,
altitudeAccuracy: 1,
heading: 0,
headingAccuracy: 1,
speed: 0,
speedAccuracy: 0,
);
}

Architecture

High-level modules detected from the verified public snapshot. Protected implementation details are intentionally omitted.

flutter-appmodule
wordpress-pluginmodule

Technology stack

DartPHPJavaScriptARBLOCKPBXPROJFlutter / Dart
Dart56.1%
PHP29.4%
JavaScript4.0%
ARB2.9%
LOCK1.7%
PBXPROJ1.7%
CSS1.5%
Markdown0.4%