feat: add foodsharing manager branding

Add supplied logos, favicons, PWA metadata, branded login and navigation,
and apply the Foodsharing Manager green palette across existing views.
This commit is contained in:
2026-08-09 00:57:03 +02:00
parent fcbf6bb949
commit 9e0f581e47
47 changed files with 253 additions and 34 deletions

View File

@@ -1,3 +1,4 @@
feat: rename login to foodsharing manager feat: add foodsharing manager branding
Use the application name consistently in the login view and its test. Add supplied logos, favicons, PWA metadata, branded login and navigation,
and apply the Foodsharing Manager green palette across existing views.

View File

@@ -0,0 +1,41 @@
# Foodsharing Manager Logo-Assets
## Empfohlene Dateien
### Klassische Webseite
- `favicon/favicon.ico`
- `favicon/favicon-32x32.png`
- `mobile/apple-touch-icon-180x180.png`
- `pwa/site.webmanifest`
### PWA / Android
- `pwa/android-chrome-192x192.png`
- `pwa/android-chrome-512x512.png`
- `pwa/maskable-icon-512x512.png`
### Desktop-App
- Windows: `desktop/app-icon.ico`
- macOS: `desktop/app-icon.icns`
- Linux / Electron / Tauri: `desktop/app-icon-512x512.png` oder `app-icon-1024x1024.png`
### Website-Header
- `web/logo-horizontal-original.png`
- alternativ `web/logo-horizontal-512px.png` oder `web/logo-horizontal-1024px.png`
### Social Media / Link-Vorschau
- `social/og-image-1200x630.png`
- kleinere Dateigröße: `social/og-image-1200x630.jpg`
## Beispiel für HTML
```html
<link rel="icon" href="/favicon/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="apple-touch-icon" href="/mobile/apple-touch-icon-180x180.png">
<link rel="manifest" href="/pwa/site.webmanifest">
<meta name="theme-color" content="#248A3D">
<meta property="og:image" content="/social/og-image-1200x630.png">
```
Hinweis: Das Ausgangslogo ist ein Rasterbild. Für sehr große Druckprodukte wäre später eine echte SVG-/Vektorversion sinnvoll.

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -0,0 +1,27 @@
{
"name": "Foodsharing Manager",
"short_name": "FS Manager",
"icons": [
{
"src": "/pwa/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/pwa/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/pwa/maskable-icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#248A3D",
"background_color": "#F7FBF5",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/branding/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -1,16 +1,17 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="de">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/branding/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/branding/favicon-32x32.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#248a3d" />
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<meta <meta name="description" content="Foodsharing Manager für Slot-Buchungen, Betriebsmonitoring und Abhol-Journal." />
name="description" <link rel="apple-touch-icon" href="%PUBLIC_URL%/branding/apple-touch-icon.png" />
content="Web site created using create-react-app" <meta property="og:title" content="Foodsharing Manager" />
/> <meta property="og:description" content="Slot-Buchungen, Betriebsmonitoring und Abhol-Journal für Foodsharing." />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <meta property="og:image" content="%PUBLIC_URL%/branding/og-image.png" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

View File

@@ -1,25 +1,25 @@
{ {
"short_name": "React App", "short_name": "Foodsharing Manager",
"name": "Create React App Sample", "name": "Foodsharing Manager",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "branding/favicon.ico",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon" "type": "image/x-icon"
}, },
{ {
"src": "logo192.png", "src": "branding/android-chrome-192x192.png",
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
}, },
{ {
"src": "logo512.png", "src": "branding/android-chrome-512x512.png",
"type": "image/png", "type": "image/png",
"sizes": "512x512" "sizes": "512x512"
} }
], ],
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"theme_color": "#000000", "theme_color": "#248a3d",
"background_color": "#ffffff" "background_color": "#f4f8f1"
} }

View File

@@ -1,12 +1,129 @@
/* App.css - Zusätzliche Stilregeln für die Pickup-Konfigurations-App */ /* App.css - gemeinsame Oberflaechen- und Markenstile */
:root {
--brand-green: #248a3d;
--brand-green-dark: #17652e;
--brand-green-soft: #edf7ed;
--brand-orange: #f6a51b;
--brand-ink: #39424e;
--brand-surface: #f4f8f1;
}
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: #f5f7fa; background-color: var(--brand-surface);
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.foodsharing-login {
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
padding: 1.5rem;
background: var(--brand-surface);
}
.foodsharing-login__card {
display: grid;
gap: 1.5rem;
width: 100%;
max-width: 28rem;
padding: 2.25rem;
border: 1px solid #d8e7d6;
border-top: 4px solid var(--brand-green);
border-radius: 10px;
background: #fff;
box-shadow: 0 18px 42px rgba(36, 80, 39, 0.12);
}
.foodsharing-login__logo {
display: block;
width: min(100%, 21rem);
height: auto;
margin: 0 auto 1.5rem;
}
.foodsharing-login__title {
margin: 0 0 0.45rem;
color: var(--brand-ink);
font-size: 1.35rem;
font-weight: 700;
text-align: center;
}
.foodsharing-login__intro {
margin: 0 0 1.5rem;
color: #64706a;
font-size: 0.875rem;
line-height: 1.5;
text-align: center;
}
.foodsharing-login__label {
display: block;
margin-bottom: 0.35rem;
color: var(--brand-ink);
font-size: 0.875rem;
font-weight: 600;
}
.foodsharing-login__input {
box-sizing: border-box;
width: 100%;
padding: 0.65rem 0.75rem;
border: 1px solid #cddacb;
border-radius: 5px;
color: var(--brand-ink);
background: #fff;
}
.foodsharing-login__input:focus {
border-color: var(--brand-green);
outline: none;
box-shadow: 0 0 0 3px rgba(36, 138, 61, 0.18);
}
.foodsharing-login__submit {
width: 100%;
padding: 0.7rem 1rem;
border: 0;
border-radius: 5px;
color: #fff;
background: var(--brand-green);
font-weight: 700;
}
.foodsharing-login__submit:hover {
background: var(--brand-green-dark);
}
.foodsharing-login__error {
margin-bottom: 1.25rem;
padding: 0.7rem 0.85rem;
border: 1px solid #edb4a8;
border-radius: 5px;
color: #9b2c1f;
background: #fff1ed;
font-size: 0.875rem;
}
/* Keep the existing Tailwind-based views on the same brand palette. */
*[class~="bg-blue-50"] { background-color: var(--brand-green-soft) !important; }
*[class~="bg-gray-100"] { background-color: var(--brand-surface) !important; }
*[class~="bg-blue-100"] { background-color: #dcefdc !important; }
*[class~="bg-blue-500"], *[class~="bg-blue-600"] { background-color: var(--brand-green) !important; }
*[class~="text-blue-600"], *[class~="text-blue-700"], *[class~="text-blue-800"], *[class~="text-blue-900"] { color: var(--brand-green-dark) !important; }
*[class~="border-blue-100"], *[class~="border-blue-200"] { border-color: #cce2ca !important; }
*[class~="border-blue-500"], *[class~="border-blue-600"] { border-color: var(--brand-green) !important; }
*[class~="hover:bg-blue-600"]:hover, *[class~="hover:bg-blue-700"]:hover { background-color: var(--brand-green-dark) !important; }
*[class~="hover:bg-blue-50"]:hover, *[class~="hover:bg-blue-100"]:hover { background-color: var(--brand-green-soft) !important; }
*[class~="hover:text-blue-600"]:hover, *[class~="hover:text-blue-700"]:hover, *[class~="hover:text-blue-800"]:hover { color: var(--brand-green-dark) !important; }
*[class~="hover:border-blue-400"]:hover, *[class~="hover:border-blue-500"]:hover { border-color: var(--brand-green) !important; }
*[class~="focus:border-blue-500"]:focus { border-color: var(--brand-green) !important; }
*[class~="focus:ring-blue-500"]:focus, *[class~="focus:ring-blue-400"]:focus, *[class~="focus:ring-blue-300"]:focus, *[class~="focus:ring-blue-200"]:focus { --tw-ring-color: rgba(36, 138, 61, 0.35) !important; }
.App { .App {
min-height: 100vh; min-height: 100vh;
padding: 1rem; padding: 1rem;

View File

@@ -3,6 +3,6 @@ import App from './App';
test('zeigt das Login-Formular an, wenn keine Session aktiv ist', () => { test('zeigt das Login-Formular an, wenn keine Session aktiv ist', () => {
render(<App />); render(<App />);
expect(screen.getByText(/Foodsharing Manager Login/i)).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'Anmelden' })).toBeInTheDocument();
expect(screen.getByLabelText(/E-Mail/i)).toBeInTheDocument(); expect(screen.getByLabelText(/E-Mail/i)).toBeInTheDocument();
}); });

View File

@@ -4,9 +4,9 @@ const LoginView = ({ credentials, onCredentialsChange, error, loading, initializ
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-100"> <div className="foodsharing-login">
<form <form
className="bg-white shadow-lg rounded-lg p-8 w-full max-w-md space-y-6" className="foodsharing-login__card"
onSubmit={(event) => { onSubmit={(event) => {
if (initializing) { if (initializing) {
event.preventDefault(); event.preventDefault();
@@ -16,20 +16,21 @@ const LoginView = ({ credentials, onCredentialsChange, error, loading, initializ
}} }}
> >
<div> <div>
<h1 className="text-2xl font-bold mb-2 text-center text-gray-800">Foodsharing Manager Login</h1> <img className="foodsharing-login__logo" src="/branding/logo-horizontal.png" alt="Foodsharing Manager" />
<p className="text-sm text-gray-500 text-center"> <h1 className="foodsharing-login__title">Anmelden</h1>
<p className="foodsharing-login__intro">
Die Anwendung authentifiziert sich direkt bei foodsharing.de. Bitte verwende deine persönlichen Login-Daten. Die Anwendung authentifiziert sich direkt bei foodsharing.de. Bitte verwende deine persönlichen Login-Daten.
</p> </p>
</div> </div>
{error && ( {error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"> <div className="foodsharing-login__error">
<span className="block sm:inline">{error}</span> <span>{error}</span>
</div> </div>
)} )}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1" htmlFor="email"> <label className="foodsharing-login__label" htmlFor="email">
E-Mail E-Mail
</label> </label>
<input <input
@@ -38,13 +39,13 @@ const LoginView = ({ credentials, onCredentialsChange, error, loading, initializ
name="email" name="email"
value={credentials.email} value={credentials.email}
onChange={handleChange('email')} onChange={handleChange('email')}
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="foodsharing-login__input"
required required
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1" htmlFor="password"> <label className="foodsharing-login__label" htmlFor="password">
Passwort Passwort
</label> </label>
<input <input
@@ -53,7 +54,7 @@ const LoginView = ({ credentials, onCredentialsChange, error, loading, initializ
name="password" name="password"
value={credentials.password} value={credentials.password}
onChange={handleChange('password')} onChange={handleChange('password')}
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="foodsharing-login__input"
required required
/> />
</div> </div>
@@ -61,7 +62,7 @@ const LoginView = ({ credentials, onCredentialsChange, error, loading, initializ
<button <button
type="submit" type="submit"
disabled={loading || initializing} disabled={loading || initializing}
className="w-full bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60" className="foodsharing-login__submit"
> >
{loading || initializing ? 'Anmeldung läuft...' : 'Anmelden'} {loading || initializing ? 'Anmeldung läuft...' : 'Anmelden'}
</button> </button>

View File

@@ -21,6 +21,26 @@
background: #fff; background: #fff;
} }
.app-navigation__brand {
display: inline-flex;
align-items: center;
gap: 0.45rem;
margin-right: 0.5rem;
padding-right: 1rem;
border-right: 1px solid #d8e7d6;
color: #17652e;
font-size: 0.875rem;
font-weight: 700;
text-decoration: none;
white-space: nowrap;
}
.app-navigation__brand-mark {
width: 1.9rem;
height: 1.9rem;
object-fit: contain;
}
.app-navigation__desktop-link { .app-navigation__desktop-link {
border-radius: 4px; border-radius: 4px;
padding: 0.45rem 0.65rem; padding: 0.45rem 0.65rem;
@@ -67,6 +87,12 @@
white-space: nowrap; white-space: nowrap;
} }
.app-navigation__mobile-header .app-navigation__brand-mark {
width: 1.75rem;
height: 1.75rem;
margin-right: -0.25rem;
}
.app-navigation__menu-toggle { .app-navigation__menu-toggle {
display: grid; display: grid;
flex: 0 0 auto; flex: 0 0 auto;

View File

@@ -69,6 +69,7 @@ const NavigationTabs = ({
return ( return (
<nav className={`app-navigation ${hasNavigation ? '' : 'app-navigation--compact'}`} aria-label="Navigation"> <nav className={`app-navigation ${hasNavigation ? '' : 'app-navigation--compact'}`} aria-label="Navigation">
<div className="app-navigation__mobile-header"> <div className="app-navigation__mobile-header">
<img className="app-navigation__brand-mark" src="/branding/logo-mark.png" alt="" aria-hidden="true" />
<span className="app-navigation__product-name">Foodsharing Manager</span> <span className="app-navigation__product-name">Foodsharing Manager</span>
<button <button
type="button" type="button"
@@ -142,6 +143,10 @@ const NavigationTabs = ({
{hasNavigation && ( {hasNavigation && (
<div className="app-navigation__desktop"> <div className="app-navigation__desktop">
<Link to="/" className="app-navigation__brand" aria-label="Foodsharing Manager Startseite">
<img className="app-navigation__brand-mark" src="/branding/logo-mark.png" alt="" aria-hidden="true" />
<span>Foodsharing Manager</span>
</Link>
{tabs.map((tab) => renderLink(tab, 'app-navigation__desktop-link'))} {tabs.map((tab) => renderLink(tab, 'app-navigation__desktop-link'))}
</div> </div>
)} )}

View File

@@ -112,7 +112,7 @@ const RangePickerModal = ({
<span>{date.getDate()}</span> <span>{date.getDate()}</span>
{(hasFoodsharingPickup || hasManagerPlan) && ( {(hasFoodsharingPickup || hasManagerPlan) && (
<span className="absolute bottom-0 flex gap-0.5"> <span className="absolute bottom-0 flex gap-0.5">
{hasFoodsharingPickup && <span className="h-1.5 w-1.5 rounded-full bg-blue-600" />} {hasFoodsharingPickup && <span className="h-1.5 w-1.5 rounded-full bg-green-600" />}
{hasManagerPlan && <span className="h-1.5 w-1.5 rounded-full bg-emerald-600" />} {hasManagerPlan && <span className="h-1.5 w-1.5 rounded-full bg-emerald-600" />}
</span> </span>
)} )}
@@ -154,7 +154,7 @@ const RangePickerModal = ({
/> />
<div className="mt-3 flex flex-wrap items-center gap-3 px-3 text-xs text-gray-600"> <div className="mt-3 flex flex-wrap items-center gap-3 px-3 text-xs text-gray-600">
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
<span className="h-2 w-2 rounded-full bg-blue-600" /> <span className="h-2 w-2 rounded-full bg-green-600" />
Foodsharing eingetragen Foodsharing eingetragen
</span> </span>
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
@@ -173,7 +173,7 @@ const RangePickerModal = ({
</button> </button>
<button <button
type="button" type="button"
className="text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-md" className="text-sm font-semibold text-white bg-green-600 hover:bg-green-700 px-4 py-2 rounded-md"
onClick={onClose} onClick={onClose}
> >
Fertig Fertig