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.

wordpress-plugin/includes/class-content-types.phpPHP
<?php
declare(strict_types=1);
namespace MeinWeimar;
final class ContentTypes
{
private const PLACE_CATEGORY_MIGRATION_VERSION = '2';
public static function hooks(): void
{
add_action('init', [self::class, 'register']);
add_action('init', [self::class, 'register_meta']);
add_action('admin_init', [self::class, 'maybe_repair_place_category_hierarchy']);
}
/**
* Stable public resource names shared by WordPress, the website and Flutter.
* Internal post type names stay an implementation detail of the plugin.
*/
public static function definitions(): array
{
return [
'places' => ['post_type' => 'mw_place', 'label' => __('Places', 'meinweimar-core'), 'singular' => __('Place', 'meinweimar-core'), 'add_new' => __('Place hinzufügen', 'meinweimar-core'), 'edit' => __('Place bearbeiten', 'meinweimar-core'), 'slug' => 'places', 'icon' => 'dashicons-location-alt', 'category' => 'mw_place_category', 'geo' => true],
'events' => ['post_type' => 'mw_event', 'label' => __('Events', 'meinweimar-core'), 'singular' => __('Event', 'meinweimar-core'), 'add_new' => __('Event hinzufügen', 'meinweimar-core'), 'edit' => __('Event bearbeiten', 'meinweimar-core'), 'slug' => 'events', 'icon' => 'dashicons-calendar-alt', 'category' => 'mw_event_category', 'geo' => true],
'services' => ['post_type' => 'mw_service', 'label' => __('Services und Einrichtungen', 'meinweimar-core'), 'singular' => __('Service oder Einrichtung', 'meinweimar-core'), 'add_new' => __('Service hinzufügen', 'meinweimar-core'), 'edit' => __('Service bearbeiten', 'meinweimar-core'), 'slug' => 'services', 'icon' => 'dashicons-building', 'category' => 'mw_service_category', 'geo' => true],
'classifieds' => ['post_type' => 'mw_classified', 'label' => __('Kleinanzeigen', 'meinweimar-core'), 'singular' => __('Kleinanzeige', 'meinweimar-core'), 'add_new' => __('Kleinanzeige hinzufügen', 'meinweimar-core'), 'edit' => __('Kleinanzeige bearbeiten', 'meinweimar-core'), 'slug' => 'kleinanzeigen', 'icon' => 'dashicons-megaphone', 'category' => 'mw_classified_category', 'geo' => true],
'jobs' => ['post_type' => 'mw_job', 'label' => __('Jobs', 'meinweimar-core'), 'singular' => __('Job', 'meinweimar-core'), 'add_new' => __('Job hinzufügen', 'meinweimar-core'), 'edit' => __('Job bearbeiten', 'meinweimar-core'), 'slug' => 'jobs', 'icon' => 'dashicons-businessperson', 'category' => 'mw_job_category', 'geo' => true],
'real-estate' => ['post_type' => 'mw_property', 'label' => __('Immobilien', 'meinweimar-core'), 'singular' => __('Immobilie', 'meinweimar-core'), 'add_new' => __('Immobilie hinzufügen', 'meinweimar-core'), 'edit' => __('Immobilie bearbeiten', 'meinweimar-core'), 'slug' => 'immobilien', 'icon' => 'dashicons-admin-home', 'category' => 'mw_property_category', 'geo' => true],
'news' => ['post_type' => 'mw_news', 'label' => __('News', 'meinweimar-core'), 'singular' => __('News-Beitrag', 'meinweimar-core'), 'add_new' => __('News-Beitrag hinzufügen', 'meinweimar-core'), 'edit' => __('News-Beitrag bearbeiten', 'meinweimar-core'), 'slug' => 'news', 'icon' => 'dashicons-media-document', 'category' => 'mw_news_category', 'geo' => false],
'guides' => ['post_type' => 'mw_guide', 'label' => __('Ratgeber', 'meinweimar-core'), 'singular' => __('Ratgeber', 'meinweimar-core'), 'add_new' => __('Ratgeber hinzufügen', 'meinweimar-core'), 'edit' => __('Ratgeber bearbeiten', 'meinweimar-core'), 'slug' => 'guides', 'icon' => 'dashicons-book-alt', 'category' => 'mw_guide_category', 'geo' => false],
];
}
public static function public_post_types(): array
{
$post_types = [];
foreach (self::definitions() as $route => $definition) {
$post_types[$route] = $definition['post_type'];
}
return $post_types;
}
/**
* Legacy Place CPT labels become the first level of the shared Place
* taxonomy. The legacy post types themselves are intentionally not
* recreated in the target installation.
*
* @return array<string, array{name:string, source_post_type:string}>
*/
public static function place_category_roots(): array
{
return [
'essen-trinken' => [
'name' => 'Essen & Trinken',
'source_post_type' => 'gd_place',
],
'kunst-kultur' => [
'name' => 'Kunst & Kultur',
'source_post_type' => 'gd_kunstkultur',
],
'freizeit-erleben' => [
'name' => 'Freizeit & Erleben',
'source_post_type' => 'gd_freizeiterlebe',
],
];
}
/**
* Canonical Kleinanzeigen category tree used by the local site and fresh
* installations. Labels are German presentation values; slugs remain
* stable machine identifiers for REST, Flutter and conditional fields.
*
* @return array<string, array<string, string>>
*/
public static function classified_category_tree(): array
{
return [
'auto-rad-boot' => [
'name' => 'Auto, Rad & Boot',
'children' => [
'autos' => 'Autos',
'autoteile-reifen' => 'Autoteile & Reifen',
'boote-bootszubehoer' => 'Boote & Bootszubehör',
'fahrrad-zubehoer' => 'Fahrräder & Zubehör',
'motorraeder-motorroller' => 'Motorräder & Motorroller',
'motorradteile-zubehoer' => 'Motorradteile & Zubehör',
'nutzfahrzeuge-anhaenger' => 'Nutzfahrzeuge & Anhänger',
'reparaturen-dienstleistungen' => 'Reparaturen & Dienstleistungen',
'wohnwagen-wohnmobile' => 'Wohnwagen & -mobile',
'weiteres-auto-rad-boot' => 'Weiteres Auto, Rad & Boot',
],
],
'elektronik' => [
'name' => 'Elektronik',
'children' => [
'audio-hifi' => 'Audio & HiFi',
'dienstleistungen-elektronik' => 'Dienstleistungen Elektronik',
'foto' => 'Foto',
'handy-telefon' => 'Handy & Telefon',
'haushaltsgeraete' => 'Haushaltsgeräte',
'konsolen' => 'Konsolen',
'pc-zubehoer-software' => 'PC-Zubehör & Software',
'tablets-reader' => 'Tablets & Reader',
'tv-video' => 'TV & Video',
'videospiele' => 'Videospiele',
'weiteres-elektronik' => 'Weiteres Elektronik',
],
],
'haus-garten' => [
'name' => 'Haus & Garten',
'children' => [
'badezimmer' => 'Badezimmer',
'buero' => 'Büro',
'dekoration' => 'Dekoration',
'dienstleistungen-haus-garten' => 'Dienstleistungen Haus & Garten',
'gartenzubehoer-pflanzen' => 'Gartenzubehör & Pflanzen',
'heimtextilien' => 'Heimtextilien',
'heimwerken' => 'Heimwerken',
'kueche-esszimmer' => 'Küche & Esszimmer',
'lampen-licht' => 'Lampen & Licht',
'schlafzimmer' => 'Schlafzimmer',
'wohnzimmer' => 'Wohnzimmer',
'weiteres-haus-garten' => 'Weiteres Haus & Garten',
],
],
'familie-kind-baby' => [
'name' => 'Familie, Kind & Baby',
'children' => [
'baby-kinderschuhe' => 'Baby- & Kinderschuhe',
'baby-kinderkleidung' => 'Baby- & Kinderkleidung',
'babyausstattung' => 'Babyausstattung',
'kinderwagen-tragen' => 'Kinderwagen & Tragen',
'kinderzimmer' => 'Kinderzimmer',
'spielzeug' => 'Spielzeug',
'weiteres-familie-kind-baby' => 'Weiteres Familie, Kind & Baby',
],
],
'mode-beauty' => [
'name' => 'Mode & Beauty',
'children' => [
'beauty-gesundheit' => 'Beauty & Gesundheit',
'damenschuhe' => 'Damenschuhe',
'damenkleidung' => 'Damenkleidung',
'herrenschuhe' => 'Herrenschuhe',
'herrenkleidung' => 'Herrenkleidung',
'schmuck-uhren' => 'Schmuck & Uhren',
'taschen-accessoires' => 'Taschen & Accessoires',
'weiteres-mode-beauty' => 'Weiteres Mode & Beauty',
],
],
'dienstleistungen' => [
'name' => 'Dienstleistungen',
'children' => [
'dienstleistung-altenpflege' => 'Altenpflege',
'dienstleistung-auto-rad-boot' => 'Auto, Rad & Boot',
'dienstleistung-babysitter' => 'Babysitter/-in',
'dienstleistung-elektronik' => 'Elektronik',
'dienstleistung-haus-garten' => 'Haus & Garten',
'dienstleistung-kuenstler-musiker' => 'Künstler & Musiker',
'dienstleistung-reise-event' => 'Reise & Event',
'dienstleistung-tierbetreuung-training' => 'Tierbetreuung & Training',
'dienstleistung-umzug-transport' => 'Umzug & Transport',
'weiteres-dienstleistungen' => 'Weitere Dienstleistungen',
],
],
'musik-filme-buecher' => [
'name' => 'Musik, Filme & Bücher',
'children' => [
'buecher-zeitschriften' => 'Bücher & Zeitschriften',
'filme-dvds' => 'Filme & DVDs',
'musik-cds' => 'Musik & CDs',
'musikinstrumente' => 'Musikinstrumente',
'weiteres-musik-filme-buecher' => 'Weiteres Musik, Filme & Bücher',
],
],
'freizeit-hobby-nachbarschaft' => [
'name' => 'Freizeit, Hobby & Nachbarschaft',
'children' => [
'esoterik-spirituelles' => 'Esoterik & Spirituelles',
'essen-trinken' => 'Essen & Trinken',
'handarbeit' => 'Handarbeit',
'basteln-kunsthandwerk' => 'Basteln & Kunsthandwerk',
'kunst-antiquitaeten' => 'Kunst & Antiquitäten',
'modellbau' => 'Modellbau',
'reise-urlaub' => 'Reise & Urlaub',
'sammeln' => 'Sammeln',
'sport-camping' => 'Sport & Camping',
'weiteres-freizeit-hobby-nachbarschaft' => 'Weiteres Freizeit, Hobby & Nachbarschaft',
],
],
'haustiere' => [
'name' => 'Haustiere',
'children' => [
'hunde' => 'Hunde',
'katzen' => 'Katzen',
'pferde-zubehoer' => 'Pferde & Zubehör',
'kleintiere' => 'Kleintiere',
'voegel' => 'Vögel',
'fische' => 'Fische',
'reptilien' => 'Reptilien',
'tierzubehoer' => 'Tierzubehör',
'vermisste-tiere' => 'Vermisste Tiere',
'weitere-haustiere' => 'Weitere Haustiere',
],
],
'unterricht-kurse' => [
'name' => 'Unterricht & Kurse',
'children' => [
'unterricht-beauty-gesundheit' => 'Beauty & Gesundheit',
'computerkurse' => 'Computerkurse',
'nachhilfe' => 'Nachhilfe',
'musikunterricht' => 'Musikunterricht',
'sportkurse' => 'Sportkurse',
'sprachkurse' => 'Sprachkurse',
'weitere-unterricht-kurse' => 'Weitere Unterricht & Kurse',
],
],
'verschenken-tauschen' => [
'name' => 'Verschenken & Tauschen',
'children' => [
'verschenken' => 'Verschenken',
'tauschen' => 'Tauschen',
'verleihen' => 'Verleihen',
],
],
];
}
/**
* Canonical German job-field hierarchy shared by WordPress, the website
* and Flutter. Employment type, work mode and qualification remain fields
* and filters; they are deliberately not mixed into this taxonomy.
*
* @return array<string, array{name:string, children:array<string,string>}>
*/
public static function job_category_tree(): array
{
return [
'vertrieb-verkauf' => [
'name' => 'Vertrieb & Verkauf',
'children' => [
'einzelhandel' => 'Einzelhandel',
'aussendienst' => 'Außendienst',
'account-management' => 'Account Management',
'weiteres-vertrieb-verkauf' => 'Weitere Tätigkeiten im Vertrieb & Verkauf',
],
],
'verwaltung-buero' => [
'name' => 'Verwaltung & Büro',
'children' => [
'sekretariat' => 'Sekretariat',
'sachbearbeitung' => 'Sachbearbeitung',
'assistenz' => 'Assistenz',
'office-management' => 'Office Management',
'weiteres-verwaltung-buero' => 'Weitere Tätigkeiten in Verwaltung & Büro',
],
],
'it-softwareentwicklung' => [
'name' => 'IT & Softwareentwicklung',
'children' => [
'programmierung' => 'Programmierung',
'systemadministration' => 'Systemadministration',
'it-support' => 'IT-Support',
'daten-ki' => 'Daten & KI',
'webentwicklung' => 'Webentwicklung',
'weiteres-it-softwareentwicklung' => 'Weitere Tätigkeiten in IT & Softwareentwicklung',
],
],
'gesundheit-pflege' => [
'name' => 'Gesundheitswesen & Pflege',
'children' => [
'medizin' => 'Medizin',
'krankenpflege' => 'Krankenpflege',
'therapie' => 'Therapie',
'medizinische-verwaltung' => 'Medizinische Verwaltung',
'weiteres-gesundheitswesen-pflege' => 'Weitere Tätigkeiten im Gesundheitswesen & in der Pflege',
],
],
'handwerk-produktion' => [
'name' => 'Handwerk & Produktion',
'children' => [
'anlagenmechanik' => 'Anlagenmechanik',
'elektro-mechatronik' => 'Elektro & Mechatronik',
'fertigung' => 'Fertigung',
'qualitaetssicherung' => 'Qualitätssicherung',
'weiteres-handwerk-produktion' => 'Weitere Tätigkeiten in Handwerk & Produktion',
],
],
'gastronomie-hotellerie-tourismus' => [
'name' => 'Gastronomie, Hotellerie & Tourismus',
'children' => [
'kueche' => 'Küche',
'service' => 'Service',
'hotellerie' => 'Hotellerie',
'tourismus-reise' => 'Tourismus & Reise',
'weiteres-gastronomie-hotellerie-tourismus' => 'Weitere Tätigkeiten in Gastronomie, Hotellerie & Tourismus',
],
],
'marketing-pr-design' => [
'name' => 'Marketing, PR & Design',
'children' => [
'marketing' => 'Marketing',
'pr-kommunikation' => 'PR & Kommunikation',
'grafikdesign' => 'Grafik & Design',
'content-redaktion' => 'Content & Redaktion',
'weiteres-marketing-pr-design' => 'Weitere Tätigkeiten in Marketing, PR & Design',
],
],
'logistik-transport' => [
'name' => 'Logistik & Transport',
'children' => [
'fahrer' => 'Fahrer',
'disposition' => 'Disposition',
'spedition' => 'Spedition',
'lager-versand' => 'Lager & Versand',
'weiteres-logistik-transport' => 'Weitere Tätigkeiten in Logistik & Transport',
],
],
'finanzen-controlling-rechnungswesen' => [
'name' => 'Finanzen, Controlling & Rechnungswesen',
'children' => [
'buchhaltung' => 'Buchhaltung',
'controlling' => 'Controlling',
'banken-versicherungen' => 'Banken & Versicherungen',
'steuern' => 'Steuern',
'weiteres-finanzen-controlling-rechnungswesen' => 'Weitere Tätigkeiten in Finanzen, Controlling & Rechnungswesen',
],
],
'personalwesen-recruiting' => [
'name' => 'Personalwesen & Recruiting',
'children' => [
'recruiting' => 'Recruiting',
'personalbetreuung' => 'Personalbetreuung',
'personalentwicklung' => 'Personalentwicklung',
'lohn-gehaltsabrechnung' => 'Lohn- & Gehaltsabrechnung',
'weiteres-personalwesen-recruiting' => 'Weitere Tätigkeiten im Personalwesen & Recruiting',
],
],
'bildung-erziehung-wissenschaft' => [
'name' => 'Bildung, Erziehung & Wissenschaft',
'children' => [
'kita-schule' => 'Kita & Schule',
'hochschule' => 'Hochschule',
'forschung' => 'Forschung',
'nachhilfe-unterricht' => 'Nachhilfe & Unterricht',
'weiteres-bildung-erziehung-wissenschaft' => 'Weitere Tätigkeiten in Bildung, Erziehung & Wissenschaft',
],
],
'kultur-medien-veranstaltungen' => [
'name' => 'Kultur, Medien & Veranstaltungen',
'children' => [
'kultur' => 'Kultur',
'medien' => 'Medien',
'veranstaltungsmanagement' => 'Veranstaltungsmanagement',
'weiteres-kultur-medien-veranstaltungen' => 'Weitere Tätigkeiten in Kultur, Medien & Veranstaltungen',
],
],
'bau-architektur-immobilien' => [
'name' => 'Bau, Architektur & Immobilien',
'children' => [
'bau' => 'Bau',
'architektur' => 'Architektur',
'bauleitung' => 'Bauleitung',
'immobilienverwaltung' => 'Immobilienverwaltung',
'weiteres-bau-architektur-immobilien' => 'Weitere Tätigkeiten in Bau, Architektur & Immobilien',
],
],
'recht-versicherungen' => [
'name' => 'Recht & Versicherungen',
'children' => [
'juristische-taetigkeiten' => 'Juristische Tätigkeiten',
'notariat' => 'Notariat',
'versicherungsberatung' => 'Versicherungsberatung',
'weiteres-recht-versicherungen' => 'Weitere Tätigkeiten in Recht & Versicherungen',
],
],
'soziales-paedagogik' => [
'name' => 'Soziales & Pädagogik',
'children' => [
'sozialarbeit' => 'Sozialarbeit',
'beratung' => 'Beratung',
'jugendhilfe' => 'Jugendhilfe',
'weiteres-soziales-paedagogik' => 'Weitere Tätigkeiten in Soziales & Pädagogik',
],
],
'naturwissenschaften-umwelt-landwirtschaft' => [
'name' => 'Naturwissenschaften, Umwelt & Landwirtschaft',
'children' => [
'naturwissenschaften' => 'Naturwissenschaften',
'umwelt-nachhaltigkeit' => 'Umwelt & Nachhaltigkeit',
'landwirtschaft' => 'Landwirtschaft',
'weiteres-naturwissenschaften-umwelt-landwirtschaft' => 'Weitere Tätigkeiten in Naturwissenschaften, Umwelt & Landwirtschaft',
],
],
'reinigung-facility-hauswirtschaft' => [
'name' => 'Reinigung, Facility Management & Hauswirtschaft',
'children' => [
'reinigung' => 'Reinigung',
'facility-management' => 'Facility Management',
'hauswirtschaft' => 'Hauswirtschaft',
'weiteres-reinigung-facility-hauswirtschaft' => 'Weitere Tätigkeiten in Reinigung, Facility Management & Hauswirtschaft',
],
],
'sicherheit-bewachung' => [
'name' => 'Sicherheit & Bewachung',
'children' => [
'objektschutz' => 'Objektschutz',
'veranstaltungsschutz' => 'Veranstaltungsschutz',
'sicherheitsmanagement' => 'Sicherheitsmanagement',
'weiteres-sicherheit-bewachung' => 'Weitere Tätigkeiten in Sicherheit & Bewachung',
],
],
'oeffentlicher-dienst' => [
'name' => 'Öffentlicher Dienst & Verwaltung',
'children' => [
'kommunalverwaltung' => 'Kommunalverwaltung',
'landesbehoerden' => 'Landesbehörden',
'oeffentliche-betriebe' => 'Öffentliche Betriebe',
'weiteres-oeffentlicher-dienst' => 'Weitere Tätigkeiten im öffentlichen Dienst',
],
],
];
}
/**
* Create or repair the canonical job-field hierarchy.
*
* @return array{created:int,updated:int}
*/
public static function seed_job_categories(): array
{
$taxonomy = 'mw_job_category';
$created = 0;
$updated = 0;
foreach (self::job_category_tree() as $parent_slug => $parent_definition) {
$parent = term_exists($parent_slug, $taxonomy);
if (is_array($parent)) {
$parent_id = (int) $parent['term_id'];
wp_update_term($parent_id, $taxonomy, ['name' => $parent_definition['name']]);
$updated++;
} elseif (is_int($parent)) {
$parent_id = $parent;
$updated++;
} else {
$inserted = wp_insert_term($parent_definition['name'], $taxonomy, ['slug' => $parent_slug]);
if (is_wp_error($inserted)) {
continue;
}
$parent_id = (int) $inserted['term_id'];
$created++;
}
foreach ($parent_definition['children'] as $child_slug => $child_name) {
$child = term_exists($child_slug, $taxonomy);
if (is_array($child)) {
wp_update_term((int) $child['term_id'], $taxonomy, ['parent' => $parent_id, 'name' => $child_name]);
$updated++;
continue;
}
if (is_int($child)) {
wp_update_term($child, $taxonomy, ['parent' => $parent_id, 'name' => $child_name]);
$updated++;
continue;
}
$inserted = wp_insert_term($child_name, $taxonomy, ['slug' => $child_slug, 'parent' => $parent_id]);
if (!is_wp_error($inserted)) {
$created++;
}
}
}
return ['created' => $created, 'updated' => $updated];
}
/**
* Canonical event hierarchy for local culture, leisure, student and
* visitor discovery. Ticketing remains a separate event field.
*
* @return array<string, array{name:string, children:array<string,string> }>
*/
public static function event_category_tree(): array
{
return [
'konzerte' => [
'name' => 'Konzerte',
'children' => [
'rock-pop' => 'Rock & Pop',
'hard-heavy' => 'Hard & Heavy',
'hiphop-rnb' => "HipHop & R'n'B",
'electronic-dance' => 'Electronic & Dance',
'festivals' => 'Festivals',
'schlager-volksmusik' => 'Schlager & Volksmusik',
'clubkonzerte' => 'Clubkonzerte',
'country-folk' => 'Country & Folk',
'jazz-blues' => 'Jazz & Blues',
'weitere-konzerte' => 'Weitere Konzerte',
],
],
'kultur' => [
'name' => 'Kultur',
'children' => [
'theater' => 'Theater',
'klassische-konzerte' => 'Klassische Konzerte',
'oper-operette' => 'Oper & Operette',
'tanz-ballett' => 'Tanz / Ballett',
'festspiele' => 'Festspiele',
'kleinkunst' => 'Kleinkunst',
'lesungen-vortraege' => 'Lesungen & Vorträge',
'weitere-kultur' => 'Weitere Kultur',
],
],
'freizeit-events' => [
'name' => 'Freizeit & Events',
'children' => [
'zirkus' => 'Zirkus',
'ausstellungen' => 'Ausstellungen',
'fuehrungen-rundfahrten' => 'Führungen & Rundfahrten',
'kinder-familie' => 'Kinder & Familie',
'partys-silvester' => 'Partys & Silvester',
'karneval-oktoberfeste' => 'Karneval & Oktoberfeste',
'kino' => 'Kino',
'weitere-events' => 'Weitere Events',
],
],
'comedy' => [
'name' => 'Comedy',
'children' => [
'stand-up-comedy' => 'Stand-up-Comedy',
'kabarett' => 'Kabarett',
'comedy-shows' => 'Comedy-Shows',
],
],
'musicals-shows' => [
'name' => 'Musicals & Shows',
'children' => [
'musicals' => 'Musicals',
'show-events' => 'Show-Events',
],
],
'sport' => [
'name' => 'Sport',
'children' => [
'fussball' => 'Fußball',
'motorsport' => 'Motorsport',
'handball-basketball-eishockey' => 'Handball, Basketball & Eishockey',
'wintersport' => 'Wintersport',
'weitere-sportevents' => 'Weitere Sportevents',
],
],
'maerkte-feste-stadtleben' => [
'name' => 'Märkte, Feste & Stadtleben',
'children' => [
'wochenmaerkte' => 'Wochenmärkte',
'stadtfeste' => 'Stadtfeste',
'weihnachtsmaerkte' => 'Weihnachtsmärkte',
'flohmarkt' => 'Flohmärkte',
'weitere-maerkte-feste' => 'Weitere Märkte & Feste',
],
],
'workshops-kurse-bildung' => [
'name' => 'Workshops, Kurse & Bildung',
'children' => [
'workshops' => 'Workshops',
'kurse' => 'Kurse',
'seminare' => 'Seminare',
'hochschule-studierendenleben' => 'Hochschule & Studierendenleben',
'weitere-bildung' => 'Weitere Workshops, Kurse & Bildung',
],
],
];
}
/**
* Canonical real-estate hierarchy. Transaction and commission values are
* still stored in FieldSchemas; this taxonomy controls discovery and the
* progressive property-type choices only.
*
* @return array<string, array{name:string, children:array<string,mixed> }>
*/
public static function property_category_tree(): array
{
return [
'wohnen-zur-miete' => [
'name' => 'Wohnen zur Miete',
'children' => [
'mietwohnungen' => [
'name' => 'Mietwohnungen',
'children' => [
'etagenwohnung' => 'Etagenwohnung',
'dachgeschosswohnung' => 'Dachgeschosswohnung',
'souterrainwohnung' => 'Souterrainwohnung',
'penthouse' => 'Penthouse',
'maisonette' => 'Maisonette',
],
],
'haeuser-zur-miete' => [
'name' => 'Häuser zur Miete',
'children' => [
'einfamilienhaus-miete' => 'Einfamilienhaus',
'reihenhaus-miete' => 'Reihenhaus',
'doppelhaushaelfte-miete' => 'Doppelhaushälfte',
],
],
'wohnen-auf-zeit' => 'Wohnen auf Zeit',
'moebliert' => 'Möbliert',
'wg-zimmer' => 'WG-Zimmer',
],
],
'wohnen-zum-kauf' => [
'name' => 'Wohnen zum Kauf',
'children' => [
'eigentumswohnungen' => [
'name' => 'Eigentumswohnungen',
'children' => [
'etagenwohnung-kauf' => 'Etagenwohnung',
'dachgeschosswohnung-kauf' => 'Dachgeschosswohnung',
'penthouse-kauf' => 'Penthouse',
'maisonette-kauf' => 'Maisonette',
],
],
'haeuser-zum-kauf' => [
'name' => 'Häuser zum Kauf',
'children' => [
'bungalow' => 'Bungalow',
'villa' => 'Villa',
'mehrfamilienhaus' => 'Mehrfamilienhaus',
'massiv-fertighaus' => 'Massiv- & Fertighaus',
],
],
'grundstuecke' => [
'name' => 'Grundstücke',
'children' => [
'grundstueck-kauf' => 'Grundstück zum Kauf',
'grundstueck-pacht' => 'Grundstück zur Pacht',
],
],
'zwangsversteigerungen' => 'Zwangsversteigerungen',
'anlageobjekte-zinshaeuser' => 'Anlageobjekte / Zinshäuser',
],
],
'gewerbeimmobilien' => [
'name' => 'Gewerbeimmobilien',
'children' => [
'buero-praxen' => 'Büros & Praxen',
'einzelhandel-gastronomie' => 'Einzelhandel & Gastronomie',
'hallen-logistik-produktion' => 'Hallen, Logistik & Produktion',
'gewerbegrundstuecke' => 'Gewerbegrundstücke',
'spezialimmobilien' => 'Spezialimmobilien',
],
],
'stellplaetze-sonstiges' => [
'name' => 'Stellplätze & Sonstiges',
'children' => [
'garagen-stellplaetze' => 'Garagen & Stellplätze',
],
],
];
}
/**
* Seed a hierarchical tree with optional nested children.
*
* @param array<string,mixed> $tree
* @return array{created:int,updated:int}
*/
private static function seed_taxonomy_tree(string $taxonomy, array $tree): array
{
$created = 0;
$updated = 0;
$visit = function (array $nodes, int $parent_id = 0) use (&$visit, &$created, &$updated, $taxonomy): void {
foreach ($nodes as $slug => $definition) {
$name = is_array($definition) ? (string) ($definition['name'] ?? $slug) : (string) $definition;
$children = is_array($definition) ? (array) ($definition['children'] ?? []) : [];
$term = term_exists((string) $slug, $taxonomy);
if (is_array($term)) {
$term_id = (int) $term['term_id'];
wp_update_term($term_id, $taxonomy, ['name' => $name, 'parent' => $parent_id]);
$updated++;
} elseif (is_int($term)) {
$term_id = $term;
wp_update_term($term_id, $taxonomy, ['name' => $name, 'parent' => $parent_id]);
$updated++;
} else {
$inserted = wp_insert_term($name, $taxonomy, ['slug' => (string) $slug, 'parent' => $parent_id]);
if (is_wp_error($inserted)) {
continue;
}
$term_id = (int) $inserted['term_id'];
$created++;
}
if ($children) {
$visit($children, $term_id);
}
}
};
$visit($tree);
return ['created' => $created, 'updated' => $updated];
}
/**
* Create the shared Place roots used by fresh installations and imports.
* Legacy child categories are added below these roots by the importer.
*
* @return array{created:int,updated:int}
*/
public static function seed_place_categories(): array
{
$taxonomy = 'mw_place_category';
$created = 0;
$updated = 0;
foreach (self::place_category_roots() as $slug => $definition) {
$term = term_exists($slug, $taxonomy);
if (is_array($term)) {
$term_id = (int) ($term['term_id'] ?? 0);
if ($term_id) {
$result = wp_update_term($term_id, $taxonomy, [
'name' => $definition['name'],
'parent' => 0,
]);
if (!is_wp_error($result)) {
$updated++;
}
}
continue;
}
if (is_int($term)) {
$result = wp_update_term($term, $taxonomy, [
'name' => $definition['name'],
'parent' => 0,
]);
if (!is_wp_error($result)) {
$updated++;
}
continue;
}
$inserted = wp_insert_term($definition['name'], $taxonomy, [
'slug' => $slug,
'parent' => 0,
]);
if (!is_wp_error($inserted)) {
$created++;
}
}
return ['created' => $created, 'updated' => $updated];
}
/**
* Repair the already imported Place posts without touching unrelated
* hand-created Places. A legacy source CPT determines the root; imported
* source categories remain as children below that root.
*
* @return array{posts_repaired:int,terms_reparented:int,errors:list<string>}
*/
public static function repair_place_category_hierarchy(): array
{
$taxonomy = 'mw_place_category';
$roots = self::place_category_roots();
$root_ids = [];
foreach ($roots as $slug => $definition) {
$term = term_exists($slug, $taxonomy);
$root_ids[$definition['source_post_type']] = is_array($term)
? (int) ($term['term_id'] ?? 0)
: (is_int($term) ? $term : 0);
}
$posts = get_posts([
'post_type' => 'mw_place',
'post_status' => 'any',
'posts_per_page' => -1,
'fields' => 'ids',
'meta_key' => '_mw_legacy_source_key',
]);
$stats = [
'posts_repaired' => 0,
'terms_reparented' => 0,
'errors' => [],
];
foreach (array_map('absint', (array) $posts) as $post_id) {
$source_key = (string) get_post_meta($post_id, '_mw_legacy_source_key', true);
if (preg_match('/:(gd_place|gd_kunstkultur|gd_freizeiterlebe):\d+$/', $source_key, $matches) !== 1) {
continue;
}
$root_id = (int) ($root_ids[$matches[1]] ?? 0);
if (!$root_id) {
$stats['errors'][] = 'Place ' . $post_id . ': root category missing';
continue;
}
$term_ids = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']);
if (is_wp_error($term_ids)) {
$stats['errors'][] = 'Place ' . $post_id . ': categories could not be read';
continue;
}
$assigned = [$root_id];
foreach (array_map('absint', (array) $term_ids) as $term_id) {
if (!$term_id || $term_id === $root_id) {
continue;
}
$updated = wp_update_term($term_id, $taxonomy, ['parent' => $root_id]);
if (is_wp_error($updated)) {
$stats['errors'][] = 'Place ' . $post_id . ': category ' . $term_id . ' could not be nested';
continue;
}
$stats['terms_reparented']++;
$assigned[] = $term_id;
}
$assigned = array_values(array_unique(array_filter(array_map('absint', $assigned))));
$result = wp_set_post_terms($post_id, $assigned, $taxonomy, false);
if (is_wp_error($result)) {
$stats['errors'][] = 'Place ' . $post_id . ': categories could not be assigned';
continue;
}
$stats['posts_repaired']++;
}
return $stats;
}
/** Run the one-time repair for existing local imports. */
public static function maybe_repair_place_category_hierarchy(): void
{
$option = 'meinweimar_place_category_hierarchy_version';
if (self::PLACE_CATEGORY_MIGRATION_VERSION === (string) get_option($option, '')) {
return;
}
self::seed_place_categories();
$stats = self::repair_place_category_hierarchy();
if (empty($stats['errors'])) {
update_option($option, self::PLACE_CATEGORY_MIGRATION_VERSION, false);
}
}
public static function seed_event_categories(): array
{
return self::seed_taxonomy_tree('mw_event_category', self::event_category_tree());
}
public static function seed_property_categories(): array
{
return self::seed_taxonomy_tree('mw_property_category', self::property_category_tree());
}
/**
* Create or repair the canonical Kleinanzeigen hierarchy.
*
* @return array{created:int,updated:int}
*/
public static function seed_classified_categories(): array
{
$taxonomy = 'mw_classified_category';
$created = 0;
$updated = 0;
foreach (self::classified_category_tree() as $parent_slug => $parent_definition) {
$parent = term_exists($parent_slug, $taxonomy);
if (is_array($parent)) {
$parent_id = (int) $parent['term_id'];
$updated++;
} elseif (is_int($parent)) {
$parent_id = $parent;
$updated++;
} else {
$inserted = wp_insert_term($parent_definition['name'], $taxonomy, ['slug' => $parent_slug]);
if (is_wp_error($inserted)) {
continue;
}
$parent_id = (int) $inserted['term_id'];
$created++;
}
foreach ($parent_definition['children'] as $child_slug => $child_name) {
$child = term_exists($child_slug, $taxonomy);
if (is_array($child)) {
$child_id = (int) $child['term_id'];
wp_update_term($child_id, $taxonomy, ['parent' => $parent_id, 'name' => $child_name]);
$updated++;
continue;
}
if (is_int($child)) {
wp_update_term($child, $taxonomy, ['parent' => $parent_id, 'name' => $child_name]);
$updated++;
continue;
}
$inserted = wp_insert_term($child_name, $taxonomy, ['slug' => $child_slug, 'parent' => $parent_id]);
if (!is_wp_error($inserted)) {
$created++;
}
}
}
return ['created' => $created, 'updated' => $updated];
}
public static function post_type_for_route(string $route): ?string
{
$definition = self::definitions()[$route] ?? null;
return is_array($definition) ? $definition['post_type'] : null;
}
public static function route_for_post_type(string $post_type): string
{
foreach (self::definitions() as $route => $definition) {
if ($definition['post_type'] === $post_type) {
return $route;
}
}
return $post_type;
}
public static function category_taxonomy(string $post_type): ?string
{
foreach (self::definitions() as $definition) {
if ($definition['post_type'] === $post_type) {
return $definition['category'];
}
}
return null;
}
public static function geo_post_types(): array
{
$post_types = [];
foreach (self::definitions() as $definition) {
if (!empty($definition['geo'])) {
$post_types[] = $definition['post_type'];
}
}
return $post_types;
}
/**
* Public resource ids that belong to the shared map/discovery surface.
* Editorial resources such as News and Guides deliberately stay outside
* this list even when legacy posts happen to contain location metadata.
*
* @return list<string>
*/
public static function geo_routes(): array
{
$routes = [];
foreach (self::definitions() as $route => $definition) {
if (!empty($definition['geo'])) {
$routes[] = $route;
}
}
return $routes;
}
public static function register(): void
{
register_post_type('mw_place', [
'labels' => [
'name' => __('Places', 'meinweimar-core'),
'singular_name' => __('Place', 'meinweimar-core'),
'add_new_item' => __('Place hinzufügen', 'meinweimar-core'),
'edit_item' => __('Place bearbeiten', 'meinweimar-core'),
],
'public' => true,
'show_in_rest' => true,
'menu_icon' => 'dashicons-location-alt',
'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'revisions'],
'has_archive' => true,
'rewrite' => ['slug' => 'places', 'with_front' => false],
'map_meta_cap' => true,
]);
register_post_type('mw_event', [
'labels' => [
'name' => __('Events', 'meinweimar-core'),
'singular_name' => __('Event', 'meinweimar-core'),
'add_new_item' => __('Event hinzufügen', 'meinweimar-core'),
'edit_item' => __('Event bearbeiten', 'meinweimar-core'),
],
'public' => true,
'show_in_rest' => true,
'menu_icon' => 'dashicons-calendar-alt',
'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'revisions'],
'has_archive' => true,
'rewrite' => ['slug' => 'events', 'with_front' => false],
'map_meta_cap' => true,
]);
foreach (['services', 'classifieds', 'jobs', 'real-estate', 'news', 'guides'] as $route) {
$definition = self::definitions()[$route];
register_post_type($definition['post_type'], [
'labels' => [
'name' => $definition['label'],
'singular_name' => $definition['singular'],
'add_new_item' => $definition['add_new'],
'edit_item' => $definition['edit'],
],
'public' => true,
'show_in_rest' => true,
'menu_icon' => $definition['icon'],
'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'revisions', 'author'],
'has_archive' => true,
'rewrite' => ['slug' => $definition['slug'], 'with_front' => false],
'map_meta_cap' => true,
]);
}
register_taxonomy('mw_place_category', ['mw_place'], [
'labels' => [
'name' => __('Place-Kategorien', 'meinweimar-core'),
'singular_name' => __('Place-Kategorie', 'meinweimar-core'),
],
'public' => true,
'show_in_rest' => true,
'hierarchical' => true,
'rewrite' => ['slug' => 'place-kategorie', 'with_front' => false],
]);
register_taxonomy('mw_event_category', ['mw_event'], [
'labels' => [
'name' => __('Event-Kategorien', 'meinweimar-core'),
'singular_name' => __('Event-Kategorie', 'meinweimar-core'),
],
'public' => true,
'show_in_rest' => true,
'hierarchical' => true,
'rewrite' => ['slug' => 'event-kategorie', 'with_front' => false],
]);
foreach (['services', 'classifieds', 'jobs', 'real-estate', 'news', 'guides'] as $route) {
$definition = self::definitions()[$route];
register_taxonomy($definition['category'], [$definition['post_type']], [
'labels' => [
'name' => sprintf(__('%s-Kategorien', 'meinweimar-core'), $definition['label']),
'singular_name' => sprintf(__('%s-Kategorie', 'meinweimar-core'), $definition['singular']),
],
'public' => true,
'show_in_rest' => true,
'hierarchical' => true,
'rewrite' => ['slug' => sanitize_title($definition['label']) . '-kategorie', 'with_front' => false],
]);
}
register_taxonomy('mw_tag', array_values(self::public_post_types()), [
'labels' => [
'name' => __('Gemeinsame Tags', 'meinweimar-core'),
'singular_name' => __('Tag', 'meinweimar-core'),
],
'public' => true,
'show_in_rest' => true,
'hierarchical' => false,
'rewrite' => ['slug' => 'tag', 'with_front' => false],
]);
register_taxonomy('mw_region', self::geo_post_types(), [
'labels' => [
'name' => __('Regionen', 'meinweimar-core'),
'singular_name' => __('Region', 'meinweimar-core'),
],
'public' => true,
'show_in_rest' => true,
'hierarchical' => true,
'rewrite' => ['slug' => 'region', 'with_front' => false],
]);
}
public static function register_meta(): void
{
EventOccurrences::register_meta();
foreach (self::geo_post_types() as $post_type) {
$route = self::route_for_post_type($post_type);
foreach (FieldSchemas::meta_for_route($route) as $key => $schema) {
register_post_meta($post_type, $key, [
'single' => true,
'type' => $schema['wp_type'],
'show_in_rest' => $schema['show_in_rest'],
'sanitize_callback' => FieldSchemas::sanitize_callback($key, $route),
'auth_callback' => static fn (): bool => current_user_can('edit_posts'),
]);
}
}
foreach (array_values(self::public_post_types()) as $post_type) {
register_post_meta($post_type, 'mw_content_locale', [
'single' => true,
'type' => 'string',
'show_in_rest' => true,
'sanitize_callback' => [Settings::class, 'sanitize_locale'],
'auth_callback' => static fn (): bool => current_user_can('edit_posts'),
]);
register_post_meta($post_type, 'mw_gallery_ids', [
'single' => true,
'type' => 'array',
'show_in_rest' => [
'schema' => [
'type' => 'array',
'items' => ['type' => 'integer'],
],
],
'sanitize_callback' => [self::class, 'sanitize_gallery_ids'],
'auth_callback' => static fn (): bool => current_user_can('edit_posts'),
]);
}
}
public static function sanitize_gallery_ids($value): array
{
if (!is_array($value)) {
$value = is_scalar($value) ? preg_split('/[,\s]+/', (string) $value) : [];
}
$ids = [];
foreach (array_slice((array) $value, 0, 20) as $item) {
$attachment_id = absint($item);
if ($attachment_id && wp_attachment_is_image($attachment_id)) {
$ids[] = $attachment_id;
}
}
return array_values(array_unique($ids));
}
}

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%