update pre commit skript, and responsive menu

This commit is contained in:
Matthias Lotz 2025-11-24 20:55:33 +01:00
parent b7acc01e90
commit e48cf69b5d
6 changed files with 195 additions and 43 deletions

View File

@ -15,7 +15,7 @@ RUN npm install --production
COPY backend/src ./src COPY backend/src ./src
# Copy production environment configuration # Copy production environment configuration
#COPY docker/prod/backend/config/.env ./.env # COPY docker/prod/backend/config/.env ./.env
# Create data directories for file storage # Create data directories for file storage
RUN mkdir -p src/data/images src/data/previews src/data/groups RUN mkdir -p src/data/images src/data/previews src/data/groups

View File

@ -36,7 +36,7 @@ services:
environment: environment:
- REMOVE_IMAGES=false - REMOVE_IMAGES=false
- NODE_ENV=production - NODE_ENV=production
- ADMIN_SESSION_SECRET=MvFhivVIPIXvSGvWGfGOiQCkUJrmUsjWQTNGUgnSmtpsGHQlKruTBEBZgbVvOHHr - ADMIN_SESSION_SECRET=${ADMIN_SESSION_SECRET}
- ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions - ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions
# ⚠️ Für HTTP-only Labs per Override auf "false" setzen (nicht im Repo committen) # ⚠️ Für HTTP-only Labs per Override auf "false" setzen (nicht im Repo committen)
- ADMIN_SESSION_COOKIE_SECURE=true - ADMIN_SESSION_COOKIE_SECURE=true

View File

@ -77,6 +77,38 @@ header {
.menu { .menu {
display: none; display: none;
flex-direction: column;
justify-content: center;
gap: 6px;
background: none;
border: none;
padding: 10px;
cursor: pointer;
}
.menu span {
width: 28px;
height: 3px;
background-color: #edf0f1;
transition: transform 0.3s ease, opacity 0.3s ease;
display: block;
}
.menu:focus-visible {
outline: 2px solid #edf0f1;
border-radius: 4px;
}
.menu--open span:nth-child(1) {
transform: translateY(9px) rotate(45deg);
}
.menu--open span:nth-child(2) {
opacity: 0;
}
.menu--open span:nth-child(3) {
transform: translateY(-9px) rotate(-45deg);
} }
.overlay { .overlay {
@ -121,6 +153,8 @@ header {
font-size: 60px; font-size: 60px;
color: #edf0f1; color: #edf0f1;
cursor: pointer; cursor: pointer;
background: none;
border: none;
} }
@media screen and (max-height: 450px) { @media screen and (max-height: 450px) {
@ -140,6 +174,6 @@ header {
display: none; display: none;
} }
.menu { .menu {
display: initial; display: flex;
} }
} }

View File

@ -1,4 +1,4 @@
import React from 'react' import React, { useEffect, useState } from 'react'
import { NavLink, useLocation } from 'react-router-dom' import { NavLink, useLocation } from 'react-router-dom'
import '../Css/Navbar.css' import '../Css/Navbar.css'
@ -9,11 +9,25 @@ import { Lock as LockIcon } from '@mui/icons-material';
function Navbar() { function Navbar() {
const location = useLocation(); const location = useLocation();
const isManagementPage = location.pathname.startsWith('/manage/'); const isManagementPage = location.pathname.startsWith('/manage/');
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
setMenuOpen(false);
}, [location.pathname]);
const toggleMenu = () => setMenuOpen(prev => !prev);
const closeMenu = () => setMenuOpen(false);
return ( return (
<>
<header> <header>
<div className="logo"><NavLink className="logo" exact to="/"><img src={logo} className="imageNav" alt="Logo"/><p className="logo">Upload your Project Images</p></NavLink></div> <div className="logo">
<nav> <NavLink className="logo" exact to="/">
<img src={logo} className="imageNav" alt="Logo" />
<p className="logo">Upload your Project Images</p>
</NavLink>
</div>
<nav aria-label="Hauptnavigation">
<ul className="nav__links"> <ul className="nav__links">
<li><NavLink to="/groups" activeClassName="active">Groups</NavLink></li> <li><NavLink to="/groups" activeClassName="active">Groups</NavLink></li>
<li><NavLink to="/moderation" activeClassName="active"><LockIcon style={{ fontSize: 18, verticalAlign: 'text-bottom', marginRight: 6 }} aria-hidden="true" />Moderation</NavLink></li> <li><NavLink to="/moderation" activeClassName="active"><LockIcon style={{ fontSize: 18, verticalAlign: 'text-bottom', marginRight: 6 }} aria-hidden="true" />Moderation</NavLink></li>
@ -23,8 +37,39 @@ function Navbar() {
)} )}
<li><a href="https://gitea.lan.hobbyhimmel.de/hobbyhimmel/Project-Image-Uploader" target="_blank" rel="noopener noreferrer">About</a></li> <li><a href="https://gitea.lan.hobbyhimmel.de/hobbyhimmel/Project-Image-Uploader" target="_blank" rel="noopener noreferrer">About</a></li>
</ul> </ul>
<button
type="button"
className={`menu${menuOpen ? ' menu--open' : ''}`}
onClick={toggleMenu}
aria-label="Navigation umschalten"
aria-expanded={menuOpen}
aria-controls="mobile-nav"
>
<span />
<span />
<span />
</button>
</nav> </nav>
</header> </header>
<div
id="mobile-nav"
className={`overlay${menuOpen ? ' overlay--active' : ''}`}
aria-hidden={!menuOpen}
>
<button type="button" className="close" onClick={closeMenu} aria-label="Navigation schließen">&times;</button>
<div className="overlay__content">
<NavLink to="/groups" activeClassName="active" onClick={closeMenu}>Groups</NavLink>
<NavLink to="/moderation" activeClassName="active" onClick={closeMenu}>
<LockIcon style={{ fontSize: 24, verticalAlign: 'text-bottom', marginRight: 12 }} aria-hidden="true" />Moderation
</NavLink>
<NavLink exact to="/" activeClassName="active" onClick={closeMenu}>Upload</NavLink>
{isManagementPage && (
<NavLink to={location.pathname} activeClassName="active" onClick={closeMenu}>Mein Upload</NavLink>
)}
<a href="https://gitea.lan.hobbyhimmel.de/hobbyhimmel/Project-Image-Uploader" target="_blank" rel="noopener noreferrer" onClick={closeMenu}>About</a>
</div>
</div>
</>
) )
} }

View File

@ -1,24 +1,59 @@
import React from 'react' import React, { useEffect, useState } from 'react'
import { NavLink, useLocation } from 'react-router-dom' import { NavLink, useLocation } from 'react-router-dom'
import '../Css/Navbar.css' import '../Css/Navbar.css'
import logo from '../../../Images/logo.png' import logo from '../../../Images/logo.png'
import { Lock as LockIcon } from '@mui/icons-material';
function Navbar() { function Navbar() {
const location = useLocation(); const location = useLocation();
const isManagementPage = location.pathname.startsWith('/manage/'); const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
setMenuOpen(false);
}, [location.pathname]);
const toggleMenu = () => setMenuOpen(prev => !prev);
const closeMenu = () => setMenuOpen(false);
return ( return (
<>
<header> <header>
<div className="logo"><NavLink className="logo" exact to="/"><img src={logo} className="imageNav" alt="Logo"/><p className="logo">Upload your Project Images</p></NavLink></div> <div className="logo">
<nav> <NavLink className="logo" exact to="/">
<img src={logo} className="imageNav" alt="Logo" />
<p className="logo">Upload your Project Images</p>
</NavLink>
</div>
<nav aria-label="Hauptnavigation">
<ul className="nav__links"> <ul className="nav__links">
<li><a href="https://gitea.lan.hobbyhimmel.de/hobbyhimmel/Project-Image-Uploader" target="_blank" rel="noopener noreferrer">About</a></li> <li><a href="https://gitea.lan.hobbyhimmel.de/hobbyhimmel/Project-Image-Uploader" target="_blank" rel="noopener noreferrer">About</a></li>
</ul> </ul>
<button
type="button"
className={`menu${menuOpen ? ' menu--open' : ''}`}
onClick={toggleMenu}
aria-label="Navigation umschalten"
aria-expanded={menuOpen}
aria-controls="mobile-nav-upload"
>
<span />
<span />
<span />
</button>
</nav> </nav>
</header> </header>
<div
id="mobile-nav-upload"
className={`overlay${menuOpen ? ' overlay--active' : ''}`}
aria-hidden={!menuOpen}
>
<button type="button" className="close" onClick={closeMenu} aria-label="Navigation schließen">&times;</button>
<div className="overlay__content">
<a href="https://gitea.lan.hobbyhimmel.de/hobbyhimmel/Project-Image-Uploader" target="_blank" rel="noopener noreferrer" onClick={closeMenu}>About</a>
</div>
</div>
</>
) )
} }

View File

@ -5,6 +5,9 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TARGET_FILE="$ROOT_DIR/docker/prod/docker-compose.yml" TARGET_FILE="$ROOT_DIR/docker/prod/docker-compose.yml"
ANCHOR_LINE=" - ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions" ANCHOR_LINE=" - ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions"
EXPECTED_LINE=" - ADMIN_SESSION_COOKIE_SECURE=true" EXPECTED_LINE=" - ADMIN_SESSION_COOKIE_SECURE=true"
SECRET_ANCHOR_LINE=' - NODE_ENV=production'
SECRET_EXPECTED_LINE=' - ADMIN_SESSION_SECRET=${ADMIN_SESSION_SECRET}'
SECRET_VALUE='${ADMIN_SESSION_SECRET}'
if [[ ! -f "$TARGET_FILE" ]]; then if [[ ! -f "$TARGET_FILE" ]]; then
exit 0 exit 0
@ -13,6 +16,9 @@ fi
export TARGET_FILE export TARGET_FILE
export ANCHOR_LINE export ANCHOR_LINE
export EXPECTED_LINE export EXPECTED_LINE
export SECRET_ANCHOR_LINE
export SECRET_EXPECTED_LINE
export SECRET_VALUE
result=$(python3 <<'PY' result=$(python3 <<'PY'
import os import os
@ -23,25 +29,57 @@ import sys
path = pathlib.Path(os.environ['TARGET_FILE']) path = pathlib.Path(os.environ['TARGET_FILE'])
anchor = os.environ['ANCHOR_LINE'] anchor = os.environ['ANCHOR_LINE']
expected = os.environ['EXPECTED_LINE'] expected = os.environ['EXPECTED_LINE']
secret_anchor = os.environ['SECRET_ANCHOR_LINE']
secret_expected = os.environ['SECRET_EXPECTED_LINE']
secret_value = os.environ['SECRET_VALUE']
text = path.read_text() text = path.read_text()
new_text = text
changed = False changed = False
if 'ADMIN_SESSION_COOKIE_SECURE' in text: cookie_pattern = re.compile(r'(\-\s*ADMIN_SESSION_COOKIE_SECURE\s*=\s*)([^\n\r]+)')
pattern = re.compile(r'(\-\s*ADMIN_SESSION_COOKIE_SECURE\s*=\s*)([^\n\r]+)') secret_pattern = re.compile(r'(\-\s*ADMIN_SESSION_SECRET\s*=\s*)([^\n\r]+)')
new_text, count = pattern.subn(r'\1true', text, count=1)
if count: def ensure_entry(text, *, pattern, value, anchor_line, expected_line, label):
changed = new_text != text match = pattern.search(text)
else: if match:
if anchor not in text: desired = f"{match.group(1)}{value}"
print('ERROR: Anchor line not found for ADMIN_SESSION_COOKIE_SECURE insertion', file=sys.stderr) if match.group(0) == desired:
return text, False
return pattern.sub(lambda m: f"{m.group(1)}{value}", text, count=1), True
if anchor_line not in text:
print(f"ERROR: Anchor line not found for {label}", file=sys.stderr)
sys.exit(2) sys.exit(2)
new_text = text.replace(anchor, anchor + '\n' + expected, 1) return text.replace(anchor_line, anchor_line + '\n' + expected_line, 1), True
changed = True
new_text, cookie_changed = ensure_entry(
new_text,
pattern=cookie_pattern,
value='true',
anchor_line=anchor,
expected_line=expected,
label='ADMIN_SESSION_COOKIE_SECURE'
)
changed = changed or cookie_changed
if expected not in new_text: if expected not in new_text:
print('ERROR: Failed to ensure ADMIN_SESSION_COOKIE_SECURE=true in docker-compose.yml', file=sys.stderr) print('ERROR: Failed to ensure ADMIN_SESSION_COOKIE_SECURE=true in docker-compose.yml', file=sys.stderr)
sys.exit(3) sys.exit(3)
new_text, secret_changed = ensure_entry(
new_text,
pattern=secret_pattern,
value=secret_value,
anchor_line=secret_anchor,
expected_line=secret_expected,
label='ADMIN_SESSION_SECRET'
)
changed = changed or secret_changed
if secret_expected not in new_text:
print('ERROR: Failed to ensure ADMIN_SESSION_SECRET uses environment variable in docker-compose.yml', file=sys.stderr)
sys.exit(4)
if changed: if changed:
path.write_text(new_text) path.write_text(new_text)
print('UPDATED') print('UPDATED')