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/lib/features/more/more_page.dartDart
import 'package:flutter/material.dart';
import '../../core/config/app_config.dart';
import '../../core/errors/api_error_code.dart';
import '../../core/theme/app_theme.dart';
import '../../l10n/app_localizations.dart';
import '../../l10n/locale_controller.dart';
import '../../models/account_models.dart';
import '../../models/content_models.dart';
import '../../models/image_suggestion_models.dart';
import '../../repositories/account_repository.dart';
import '../../repositories/claim_repository.dart';
import '../claim/claim_page.dart';
import '../discovery/resource_browser_page.dart';
import '../../repositories/discovery_repository.dart';
import '../../repositories/geo_repository.dart';
import '../../repositories/image_suggestion_repository.dart';
import '../../repositories/notifications_repository.dart';
import '../notifications/notification_inbox_page.dart';
import '../../shared/states/app_state_widgets.dart';
import '../image_suggestion/image_suggestion_labels.dart';
import '../image_suggestion/image_suggestion_status_page.dart';
class MorePage extends StatefulWidget {
const MorePage({
super.key,
required this.localeController,
required this.snapshot,
required this.accountRepository,
required this.claimRepository,
required this.discoveryRepository,
required this.geoRepository,
this.imageSuggestionRepository,
this.notificationsRepository,
required this.onOpenModule,
});
final LocaleController localeController;
final BootstrapSnapshot snapshot;
final AccountRepository accountRepository;
final ClaimRepository claimRepository;
final DiscoveryRepository discoveryRepository;
final GeoRepository geoRepository;
final ImageSuggestionRepository? imageSuggestionRepository;
final NotificationsRepository? notificationsRepository;
final Future<void> Function(String id) onOpenModule;
@override
State<MorePage> createState() => _MorePageState();
}
class _MorePageState extends State<MorePage> {
late Future<ProfileData> _profile;
@override
void initState() {
super.initState();
_profile = widget.accountRepository.me();
}
void _retryProfile() {
setState(() {
_profile = widget.accountRepository.me();
});
}
Future<void> _openProfileEditor(ProfileData profile) async {
final updated = await Navigator.of(context).push<ProfileData>(
MaterialPageRoute<ProfileData>(
builder: (_) => _EditProfilePage(
profile: profile,
accountRepository: widget.accountRepository,
),
),
);
if (updated != null && mounted) {
_retryProfile();
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final currentLanguage = _localeLabel(l10n, widget.localeController.locale);
final supportedLocales = AppLocalizations.supportedLocales;
final currentLocaleTag = localeLanguageTag(widget.localeController.locale);
final selectedLocale = supportedLocales.firstWhere(
(locale) => localeLanguageTag(locale) == currentLocaleTag,
orElse: () => supportedLocales.first,
);
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 860),
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
l10n.moreTitle,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
_ProfileCard(
profile: _profile,
onRetry: _retryProfile,
onEdit: _openProfileEditor,
),
const SizedBox(height: 16),
Card(
child: Column(
children: [
ListTile(
leading: const Icon(Icons.language),
title: Text(l10n.languageLabel),
subtitle: Text(currentLanguage),
trailing: DropdownButton<Locale>(
value: selectedLocale,
underline: const SizedBox.shrink(),
items: supportedLocales
.map(
(locale) => DropdownMenuItem(
value: locale,
child: Text(_localeLabel(l10n, locale)),
),
)
.toList(growable: false),
onChanged: (locale) {
if (locale != null) {
widget.localeController.select(locale);
}
},
),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.tune),
title: Text(l10n.settingsTitle),
onTap: () => _openInfo(
context,
title: l10n.settingsTitle,
body: l10n.settingsAvailabilityBody,
icon: Icons.tune,
),
),
ListTile(
leading: const Icon(Icons.privacy_tip_outlined),
title: Text(l10n.privacyTitle),
onTap: () => _openInfo(
context,
title: l10n.privacyTitle,
body: l10n.privacyAvailabilityBody,
icon: Icons.privacy_tip_outlined,
),
),
ListTile(
leading: const Icon(Icons.verified_user_outlined),
title: Text(l10n.claimTitle),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) =>
ClaimPage(repository: widget.claimRepository),
),
);
},
),
ListTile(
leading: const Icon(Icons.notifications_none_outlined),
title: Text(l10n.notificationsTitle),
onTap: widget.notificationsRepository == null
? null
: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => NotificationInboxPage(
repository: widget.notificationsRepository!,
imageSuggestionRepository:
widget.imageSuggestionRepository,
),
),
),
),
],
),
),
const SizedBox(height: 16),
_ModuleCard(
resources: widget.snapshot.resources,
repository: widget.discoveryRepository,
geoRepository: widget.geoRepository,
geoConfig: widget.snapshot.config.geo,
imageSuggestionRepository: widget.imageSuggestionRepository,
contentSchemas: widget.snapshot.config.contentSchemas,
onOpenModule: widget.onOpenModule,
),
if (widget.imageSuggestionRepository != null) ...[
const SizedBox(height: 16),
_ImageSuggestionStatusCard(
repository: widget.imageSuggestionRepository!,
),
],
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.apiContractTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(l10n.apiContractBody),
const SizedBox(height: 8),
Text(
widget.snapshot.config.apiBaseUrl.toString(),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
],
),
),
);
}
void _openInfo(
BuildContext context, {
required String title,
required String body,
required IconData icon,
}) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => _InfoPage(title: title, body: body, icon: icon),
),
);
}
String _localeLabel(AppLocalizations l10n, Locale locale) {
return switch (locale.languageCode.toLowerCase()) {
'de' => l10n.languageGerman,
'en' => l10n.languageEnglish,
_ => localeLanguageTag(locale),
};
}
}
class _ModuleCard extends StatelessWidget {
const _ModuleCard({
required this.resources,
required this.repository,
required this.geoRepository,
required this.geoConfig,
required this.imageSuggestionRepository,
required this.contentSchemas,
required this.onOpenModule,
});
final List<ResourceDefinition> resources;
final DiscoveryRepository repository;
final GeoRepository geoRepository;
final GeoConfig geoConfig;
final ImageSuggestionRepository? imageSuggestionRepository;
final Map<String, Map<String, ContentFieldSchema>> contentSchemas;
final Future<void> Function(String id) onOpenModule;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final visibleResources = resources
.where((resource) => !{'places', 'events'}.contains(resource.id))
.toList(growable: false);
if (visibleResources.isEmpty) return const SizedBox.shrink();
return Card(
child: Column(
children: [
ListTile(
leading: const Icon(Icons.apps_outlined),
title: Text(l10n.availableModulesTitle),
subtitle: Text(l10n.availableModulesBody),
),
const Divider(height: 1),
for (final resource in visibleResources)
ListTile(
leading: Icon(resourceIcon(resource.id)),
title: Text(localizedResourceLabel(l10n, resource)),
trailing: const Icon(Icons.chevron_right),
onTap: () => onOpenModule(resource.id),
),
],
),
);
}
}
class _InfoPage extends StatelessWidget {
const _InfoPage({
required this.title,
required this.body,
required this.icon,
});
final String title;
final String body;
final IconData icon;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 36, color: AppTheme.palette(context).brand),
const SizedBox(height: 16),
Text(body, style: Theme.of(context).textTheme.bodyLarge),
],
),
),
),
],
),
);
}
}
class _ImageSuggestionStatusCard extends StatefulWidget {
const _ImageSuggestionStatusCard({required this.repository});
final ImageSuggestionRepository repository;
@override
State<_ImageSuggestionStatusCard> createState() =>
_ImageSuggestionStatusCardState();
}
class _ImageSuggestionStatusCardState
extends State<_ImageSuggestionStatusCard> {
late Future<ImageSuggestionList> _suggestions;
@override
void initState() {
super.initState();
_suggestions = widget.repository.own();
}
void _retry() {
setState(() {
_suggestions = widget.repository.own();
});
}
Future<void> _openStatus({int? highlightedId}) async {
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => ImageSuggestionStatusPage(
repository: widget.repository,
highlightedId: highlightedId,
),
),
);
if (mounted) _retry();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: FutureBuilder<ImageSuggestionList>(
future: _suggestions,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox(height: 96, child: LoadingState());
}
if (snapshot.hasError) {
final error = snapshot.error;
final code = error is ApiException
? error.code
: ApiErrorCode.unknown;
if (code == ApiErrorCode.permissionDenied) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.imageSuggestMyTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
const SizedBox(height: 150, child: AuthRequiredState()),
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.imageSuggestMyTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
ErrorState(onRetry: _retry, code: code),
],
);
}
final items = snapshot.data?.items ?? const [];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.imageSuggestMyTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(l10n.imageSuggestMyBody),
const SizedBox(height: 12),
if (items.isEmpty)
Text(
l10n.imageSuggestMyEmpty,
style: TextStyle(color: AppTheme.palette(context).muted),
)
else
for (final item in items.take(5))
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.photo_outlined),
title: Text(
item.originalFilename.isEmpty
? l10n.imageSuggestTarget(
imageSuggestionTargetLabel(
l10n,
item.targetType,
),
)
: item.originalFilename,
),
subtitle: Text(
l10n.imageSuggestMyItemStatus(
_statusLabel(l10n, item.status),
),
),
trailing: item.status == 'needs_info'
? const Icon(Icons.reply_outlined)
: const Icon(Icons.chevron_right),
onTap: () => _openStatus(highlightedId: item.id),
),
TextButton.icon(
onPressed: _openStatus,
icon: const Icon(Icons.list_alt_outlined),
label: Text(l10n.imageSuggestViewAll),
),
],
);
},
),
),
);
}
String _statusLabel(AppLocalizations l10n, String status) {
return switch (status) {
'submitted' => l10n.imageSuggestStatusSubmitted,
'in_review' => l10n.imageSuggestStatusInReview,
'accepted' => l10n.imageSuggestStatusAccepted,
'rejected' => l10n.imageSuggestStatusRejected,
'needs_info' => l10n.imageSuggestStatusNeedsInfo,
_ => l10n.imageSuggestStatusPending,
};
}
}
class _ProfileCard extends StatelessWidget {
const _ProfileCard({
required this.profile,
required this.onRetry,
required this.onEdit,
});
final Future<ProfileData> profile;
final VoidCallback onRetry;
final Future<void> Function(ProfileData profile) onEdit;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: FutureBuilder<ProfileData>(
future: profile,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox(height: 100, child: LoadingState());
}
if (snapshot.hasError) {
final error = snapshot.error;
final code = error is ApiException
? error.code
: ApiErrorCode.unknown;
if (code == ApiErrorCode.permissionDenied) {
return const SizedBox(height: 190, child: AuthRequiredState());
}
return SizedBox(
height: 230,
child: ErrorState(onRetry: onRetry, code: code),
);
}
final data = snapshot.data;
if (data == null || !data.visible) {
return Text(l10n.profileUnavailable);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircleAvatar(
radius: 28,
foregroundImage:
data.avatarVisible && data.avatarUrl.trim().isNotEmpty
? NetworkImage(data.avatarUrl)
: null,
child:
data.avatarVisible && data.avatarUrl.trim().isNotEmpty
? null
: const Icon(Icons.person_outline, size: 28),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.profileTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
data.nickname.isNotEmpty
? data.nickname
: l10n.unknownValue,
style: Theme.of(context).textTheme.titleLarge,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
const SizedBox(height: 4),
Text(
l10n.profileSignedIn,
style: TextStyle(color: AppTheme.palette(context).muted),
),
if (data.status.isNotEmpty) ...[
const SizedBox(height: 12),
Text(
'${l10n.profileStatusLabel}: ${data.status}',
style: TextStyle(color: AppTheme.palette(context).muted),
),
],
const SizedBox(height: 12),
TextButton.icon(
onPressed: () => onEdit(data),
icon: const Icon(Icons.edit_outlined),
label: Text(l10n.profileEdit),
),
],
);
},
),
),
);
}
}
class _EditProfilePage extends StatefulWidget {
const _EditProfilePage({
required this.profile,
required this.accountRepository,
});
final ProfileData profile;
final AccountRepository accountRepository;
@override
State<_EditProfilePage> createState() => _EditProfilePageState();
}
class _EditProfilePageState extends State<_EditProfilePage> {
late final TextEditingController _nickname;
late final TextEditingController _status;
late final TextEditingController _age;
late final TextEditingController _gender;
late final TextEditingController _bio;
late String _profileVisibility;
late String _avatarVisibility;
bool _saving = false;
ApiErrorCode? _errorCode;
@override
void initState() {
super.initState();
final profile = widget.profile;
_nickname = TextEditingController(text: profile.nickname);
_status = TextEditingController(text: profile.status);
_age = TextEditingController(
text: profile.age > 0 ? profile.age.toString() : '',
);
_gender = TextEditingController(text: profile.gender);
_bio = TextEditingController(text: profile.bio);
_profileVisibility = _visibility(profile.profileVisibility);
_avatarVisibility = _visibility(profile.avatarVisibility);
}
String _visibility(String value) =>
value == 'public' || value == 'private' ? value : 'private';
@override
void dispose() {
_nickname.dispose();
_status.dispose();
_age.dispose();
_gender.dispose();
_bio.dispose();
super.dispose();
}
Future<void> _save() async {
if (_saving) return;
setState(() {
_saving = true;
_errorCode = null;
});
final age = int.tryParse(_age.text.trim()) ?? 0;
try {
final updated = await widget.accountRepository.updateMe({
'nickname': _nickname.text.trim(),
'status': _status.text.trim(),
'age': age,
'gender': _gender.text.trim(),
'bio': _bio.text.trim(),
'profile_visibility': _profileVisibility,
'avatar_visibility': _avatarVisibility,
});
if (!mounted) return;
Navigator.of(context).pop(updated);
} catch (error) {
if (!mounted) return;
setState(() {
_errorCode = error is ApiException ? error.code : ApiErrorCode.unknown;
_saving = false;
});
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: Text(l10n.profileEdit)),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
TextField(
controller: _nickname,
textInputAction: TextInputAction.next,
decoration: InputDecoration(labelText: l10n.profileNickname),
),
const SizedBox(height: 12),
TextField(
controller: _status,
textInputAction: TextInputAction.next,
decoration: InputDecoration(labelText: l10n.profileStatusLabel),
),
const SizedBox(height: 12),
TextField(
controller: _age,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
decoration: InputDecoration(labelText: l10n.profileAge),
),
const SizedBox(height: 12),
TextField(
controller: _gender,
textInputAction: TextInputAction.next,
decoration: InputDecoration(labelText: l10n.profileGender),
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: _profileVisibility,
decoration: InputDecoration(labelText: l10n.profileVisibility),
items: [
DropdownMenuItem(
value: 'public',
child: Text(l10n.profilePublic),
),
DropdownMenuItem(
value: 'private',
child: Text(l10n.profilePrivate),
),
],
onChanged: _saving
? null
: (value) {
if (value != null) {
setState(() => _profileVisibility = value);
}
},
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: _avatarVisibility,
decoration: InputDecoration(
labelText: l10n.profileAvatarVisibility,
),
items: [
DropdownMenuItem(
value: 'public',
child: Text(l10n.profilePublic),
),
DropdownMenuItem(
value: 'private',
child: Text(l10n.profilePrivate),
),
],
onChanged: _saving
? null
: (value) {
if (value != null) {
setState(() => _avatarVisibility = value);
}
},
),
const SizedBox(height: 12),
TextField(
controller: _bio,
minLines: 4,
maxLines: 8,
decoration: InputDecoration(labelText: l10n.profileBio),
),
if (_errorCode != null) ...[
const SizedBox(height: 16),
ErrorState(code: _errorCode!, onRetry: _save),
],
const SizedBox(height: 16),
FilledButton.icon(
onPressed: _saving ? null : _save,
icon: _saving
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.save_outlined),
label: Text(_saving ? l10n.profileSaving : l10n.profileSave),
),
],
),
);
}
}

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%