Merge pull request 'Add sidebar' (#260) from sidebar into main
Some checks failed
continuous-integration/drone/push Build is failing

Reviewed-on: #260
This commit is contained in:
simon 2026-01-12 15:17:28 +01:00
commit a1b0f65233
18 changed files with 6367 additions and 427 deletions

View file

@ -99,4 +99,213 @@
/* Make LiveView wrapper divs transparent for layout */ /* Make LiveView wrapper divs transparent for layout */
[data-phx-session] { display: contents } [data-phx-session] { display: contents }
/* ============================================
Sidebar Base Styles
============================================ */
/* Desktop Sidebar Base */
.sidebar {
@apply flex flex-col bg-base-200 min-h-screen;
@apply transition-[width] duration-300 ease-in-out;
@apply relative;
width: 16rem; /* Expanded: w-64 */
z-index: 40;
}
/* Collapsed State */
[data-sidebar-expanded="false"] .sidebar {
width: 4rem; /* Collapsed: w-16 */
}
/* ============================================
Header - Logo Centering
============================================ */
/* Header container with smooth transition for gap */
.sidebar > div:first-child {
@apply transition-all duration-300;
}
/* ============================================
Text Labels - Hide in Collapsed State
============================================ */
.menu-label {
@apply transition-all duration-200 whitespace-nowrap;
transition-delay: 0ms; /* Expanded: sofort sichtbar */
}
[data-sidebar-expanded="false"] .sidebar .menu-label {
@apply opacity-0 w-0 overflow-hidden pointer-events-none;
transition-delay: 300ms; /* Warte bis Sidebar eingeklappt ist (300ms = duration der Sidebar width transition) */
}
/* ============================================
Toggle Button Icon Swap
============================================ */
.sidebar-collapsed-icon {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar .sidebar-expanded-icon {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar .sidebar-collapsed-icon {
@apply block;
}
/* ============================================
Menu Groups - Show/Hide Based on State
============================================ */
.expanded-menu-group {
@apply block;
}
.collapsed-menu-group {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar .expanded-menu-group {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar .collapsed-menu-group {
@apply block;
}
/* Collapsed menu group button: center icon under logo */
.sidebar .collapsed-menu-group button {
padding-left: 14px;
}
/* ============================================
Elements Only Visible in Expanded State
============================================ */
.expanded-only {
@apply block transition-opacity duration-200;
}
[data-sidebar-expanded="false"] .sidebar .expanded-only {
@apply hidden;
}
/* ============================================
Tooltip - Only Show in Collapsed State
============================================ */
.sidebar .tooltip::before,
.sidebar .tooltip::after {
@apply opacity-0 pointer-events-none;
}
[data-sidebar-expanded="false"] .sidebar .tooltip:hover::before,
[data-sidebar-expanded="false"] .sidebar .tooltip:hover::after {
@apply opacity-100;
}
/* ============================================
Menu Item Alignment - Icons Centered Under Logo
============================================ */
/* Base alignment: Icons centered under logo (32px from left edge)
- Logo center: 16px padding + 16px (half of 32px) = 32px
- Icon center should be at 32px: 22px start + 10px (half of 20px) = 32px
- Menu has p-2 (8px), so links need 14px additional padding-left */
.sidebar .menu > li > a,
.sidebar .menu > li > button {
@apply transition-all duration-300;
padding-left: 14px;
}
/* Collapsed state: same padding to keep icons at same position
- Remove gap so label (which is opacity-0 w-0) doesn't create space
- Keep padding-left at 14px so icons stay centered under logo */
[data-sidebar-expanded="false"] .sidebar .menu > li > a,
[data-sidebar-expanded="false"] .sidebar .menu > li > button {
@apply gap-0;
padding-left: 14px;
padding-right: 14px; /* Center icon horizontally in 64px sidebar */
}
/* ============================================
Footer Button Alignment - Left Aligned in Collapsed State
============================================ */
[data-sidebar-expanded="false"] .sidebar .dropdown > button {
@apply px-0;
/* Buttons stay at left position, only label disappears */
}
/* ============================================
User Menu Button - Focus Ring on Avatar
============================================ */
/* Focus ring appears on the avatar when button is focused */
.user-menu-button:focus .avatar > div {
@apply ring-2 ring-primary ring-offset-2 ring-offset-base-200;
}
/* ============================================
User Menu Button - Smooth Centering Transition
============================================ */
/* User menu button transitions smoothly to center */
.user-menu-button {
@apply transition-all duration-300;
}
/* In collapsed state, center avatar under logo
- Avatar is 32px (w-8), center it in 64px sidebar
- (64px - 32px) / 2 = 16px padding avatar center at 32px (same as logo center) */
[data-sidebar-expanded="false"] .sidebar .user-menu-button {
@apply gap-0;
padding-left: 16px;
padding-right: 16px;
justify-content: center;
}
/* ============================================
User Menu Button - Hover Ring on Avatar
============================================ */
/* Smooth transition for avatar ring effects */
.user-menu-button .avatar > div {
@apply transition-all duration-200;
}
/* Hover ring appears on the avatar when button is hovered */
.user-menu-button:hover .avatar > div {
@apply ring-1 ring-neutral ring-offset-1 ring-offset-base-200;
}
/* ============================================
Mobile Drawer Width
============================================ */
/* Auf Mobile (< 1024px) ist die Sidebar immer w-64 (16rem) wenn geöffnet */
@media (max-width: 1023px) {
.drawer-side .sidebar {
width: 16rem; /* w-64 auch auf Mobile */
}
}
/* ============================================
Drawer Side Overflow Fix für Desktop
============================================ */
/* Im Desktop-Modus (lg:drawer-open) overflow auf visible setzen
damit Dropdowns und Tooltips über Main Content erscheinen können */
@media (min-width: 1024px) {
.drawer.lg\:drawer-open .drawer-side {
overflow: visible !important;
overflow-x: visible !important;
overflow-y: visible !important;
}
}
/* This file is for your main application CSS */ /* This file is for your main application CSS */

View file

@ -73,6 +73,43 @@ Hooks.ComboBox = {
} }
} }
// SidebarState hook: Manages sidebar expanded/collapsed state
Hooks.SidebarState = {
mounted() {
// Restore state from localStorage
const expanded = localStorage.getItem('sidebar-expanded') !== 'false'
this.setSidebarState(expanded)
// Expose toggle function globally
window.toggleSidebar = () => {
const current = this.el.dataset.sidebarExpanded === 'true'
this.setSidebarState(!current)
}
},
setSidebarState(expanded) {
// Convert boolean to string for consistency
const expandedStr = expanded ? 'true' : 'false'
// Update data-attribute (CSS reacts to this)
this.el.dataset.sidebarExpanded = expandedStr
// Persist to localStorage
localStorage.setItem('sidebar-expanded', expandedStr)
// Update ARIA for accessibility
const toggleBtn = document.getElementById('sidebar-toggle')
if (toggleBtn) {
toggleBtn.setAttribute('aria-expanded', expandedStr)
}
},
destroyed() {
// Cleanup
delete window.toggleSidebar
}
}
let liveSocket = new LiveSocket("/live", Socket, { let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500, longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken}, params: {_csrf_token: csrfToken},
@ -102,3 +139,170 @@ liveSocket.connect()
// >> liveSocket.disableLatencySim() // >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket window.liveSocket = liveSocket
// Sidebar accessibility improvements
document.addEventListener("DOMContentLoaded", () => {
const drawerToggle = document.getElementById("mobile-drawer")
const sidebarToggle = document.getElementById("sidebar-toggle")
const sidebar = document.getElementById("main-sidebar")
if (!drawerToggle || !sidebarToggle || !sidebar) return
// Manage tabindex for sidebar elements based on open/closed state
const updateSidebarTabIndex = (isOpen) => {
// Find all potentially focusable elements (including those with tabindex="-1")
const allFocusableElements = sidebar.querySelectorAll(
'a[href], button, select, input:not([type="hidden"]), [tabindex]'
)
allFocusableElements.forEach(el => {
// Skip the overlay button
if (el.closest('.drawer-overlay')) return
if (isOpen) {
// Remove tabindex="-1" to make focusable when open
if (el.hasAttribute('tabindex') && el.getAttribute('tabindex') === '-1') {
el.removeAttribute('tabindex')
}
} else {
// Set tabindex="-1" to remove from tab order when closed
if (!el.hasAttribute('tabindex')) {
el.setAttribute('tabindex', '-1')
} else if (el.getAttribute('tabindex') !== '-1') {
// Store original tabindex in data attribute before setting to -1
if (!el.hasAttribute('data-original-tabindex')) {
el.setAttribute('data-original-tabindex', el.getAttribute('tabindex'))
}
el.setAttribute('tabindex', '-1')
}
}
})
}
// Find first focusable element in sidebar
// Priority: first navigation link (menuitem) > other links > other focusable elements
const getFirstFocusableElement = () => {
// First, try to find the first navigation link (menuitem)
const firstNavLink = sidebar.querySelector('a[href][role="menuitem"]:not([tabindex="-1"])')
if (firstNavLink && !firstNavLink.closest('.drawer-overlay')) {
return firstNavLink
}
// Fallback: any navigation link
const firstLink = sidebar.querySelector('a[href]:not([tabindex="-1"])')
if (firstLink && !firstLink.closest('.drawer-overlay')) {
return firstLink
}
// Last resort: any other focusable element
const focusableSelectors = [
'button:not([tabindex="-1"]):not([disabled])',
'select:not([tabindex="-1"]):not([disabled])',
'input:not([tabindex="-1"]):not([disabled]):not([type="hidden"])',
'[tabindex]:not([tabindex="-1"])'
]
for (const selector of focusableSelectors) {
const element = sidebar.querySelector(selector)
if (element && !element.closest('.drawer-overlay')) {
return element
}
}
return null
}
// Update aria-expanded when drawer state changes
const updateAriaExpanded = () => {
const isOpen = drawerToggle.checked
sidebarToggle.setAttribute("aria-expanded", isOpen.toString())
// Update dropdown aria-expanded if present
const userMenuButton = sidebar.querySelector('button[aria-haspopup="true"]')
if (userMenuButton) {
const dropdown = userMenuButton.closest('.dropdown')
const isDropdownOpen = dropdown?.classList.contains('dropdown-open')
if (userMenuButton) {
userMenuButton.setAttribute("aria-expanded", (isDropdownOpen || false).toString())
}
}
}
// Listen for changes to the drawer checkbox
drawerToggle.addEventListener("change", () => {
const isOpen = drawerToggle.checked
updateAriaExpanded()
updateSidebarTabIndex(isOpen)
if (!isOpen) {
// When closing, return focus to toggle button
sidebarToggle.focus()
}
})
// Update on initial load
updateAriaExpanded()
updateSidebarTabIndex(drawerToggle.checked)
// Close sidebar with ESC key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && drawerToggle.checked) {
drawerToggle.checked = false
updateAriaExpanded()
updateSidebarTabIndex(false)
// Return focus to toggle button
sidebarToggle.focus()
}
})
// Improve keyboard navigation for sidebar toggle
sidebarToggle.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
const wasOpen = drawerToggle.checked
drawerToggle.checked = !drawerToggle.checked
updateAriaExpanded()
// If opening, move focus to first element in sidebar
if (!wasOpen && drawerToggle.checked) {
updateSidebarTabIndex(true)
// Use setTimeout to ensure DOM is updated
setTimeout(() => {
const firstElement = getFirstFocusableElement()
if (firstElement) {
firstElement.focus()
}
}, 50)
} else if (wasOpen && !drawerToggle.checked) {
updateSidebarTabIndex(false)
}
}
})
// Also handle click events to update tabindex and focus
sidebarToggle.addEventListener("click", () => {
setTimeout(() => {
const isOpen = drawerToggle.checked
updateSidebarTabIndex(isOpen)
if (isOpen) {
const firstElement = getFirstFocusableElement()
if (firstElement) {
firstElement.focus()
}
}
}, 50)
})
// Handle dropdown keyboard navigation
const userMenuButton = sidebar?.querySelector('button[aria-haspopup="true"]')
if (userMenuButton) {
userMenuButton.addEventListener("click", () => {
setTimeout(updateAriaExpanded, 0)
})
userMenuButton.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
userMenuButton.click()
}
})
}
})

View file

@ -10,11 +10,7 @@ services:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_DB: mv_dev POSTGRES_DB: mv_dev
volumes: volumes:
- type: volume - postgres-data:/var/lib/postgresql/data
source: postgres-data
target: /var/lib/postgresql/data
volume:
nocopy: true
ports: ports:
- "5000:5432" - "5000:5432"
networks: networks:
@ -49,9 +45,7 @@ services:
- rauthy-dev - rauthy-dev
- local - local
volumes: volumes:
- type: volume - rauthy-data:/app/data
source: rauthy-data
target: /app/data
volumes: volumes:
postgres-data: postgres-data:

View file

@ -0,0 +1,533 @@
# DaisyUI Drawer Pattern - Standard Implementation
This document describes the standard DaisyUI drawer pattern for implementing responsive sidebars. It covers mobile overlay drawers, desktop persistent sidebars, and their combination.
## Core Concept
DaisyUI's drawer component uses a **checkbox-based toggle mechanism** combined with CSS to create accessible, responsive sidebars without custom JavaScript.
### Key Components
1. **`drawer`** - Container element
2. **`drawer-toggle`** - Hidden checkbox that controls open/close state
3. **`drawer-content`** - Main content area
4. **`drawer-side`** - Sidebar content (menu, navigation)
5. **`drawer-overlay`** - Optional overlay for mobile (closes drawer on click)
## HTML Structure
```html
<div class="drawer">
<!-- Hidden checkbox controls the drawer state -->
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
<!-- Main content area -->
<div class="drawer-content">
<!-- Page content goes here -->
<label for="my-drawer" class="btn btn-primary">Open drawer</label>
</div>
<!-- Sidebar content -->
<div class="drawer-side">
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu p-4 w-80 min-h-full bg-base-200 text-base-content">
<!-- Sidebar content goes here -->
<li><a>Sidebar Item 1</a></li>
<li><a>Sidebar Item 2</a></li>
</ul>
</div>
</div>
```
## How drawer-toggle Works
### Mechanism
The `drawer-toggle` is a **hidden checkbox** that serves as the state controller:
```html
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
```
### Toggle Behavior
1. **Label Connection**: Any `<label for="my-drawer">` element can toggle the drawer
2. **Checkbox State**:
- `checked` → drawer is open
- `unchecked` → drawer is closed
3. **CSS Targeting**: DaisyUI uses CSS sibling selectors to show/hide the drawer based on checkbox state
4. **Accessibility**: Native checkbox provides keyboard accessibility (Space/Enter to toggle)
### Toggle Examples
```html
<!-- Button to open drawer -->
<label for="my-drawer" class="btn btn-primary drawer-button">
Open Menu
</label>
<!-- Close button inside drawer -->
<label for="my-drawer" class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></label>
<!-- Overlay to close (click outside) -->
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
```
## Mobile Drawer (Overlay)
### Characteristics
- Drawer slides in from the side (usually left)
- Overlays the main content
- Dark overlay (drawer-overlay) behind drawer
- Clicking overlay closes the drawer
- Typically used on mobile/tablet screens
### Implementation
```html
<div class="drawer">
<input id="mobile-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
<!-- Toggle button in header -->
<div class="navbar bg-base-100">
<div class="flex-none">
<label for="mobile-drawer" class="btn btn-square btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-5 h-5 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</label>
</div>
<div class="flex-1">
<a class="btn btn-ghost text-xl">My App</a>
</div>
</div>
<!-- Main content -->
<div class="p-4">
<h1>Main Content</h1>
</div>
</div>
<div class="drawer-side">
<!-- Overlay - clicking it closes the drawer -->
<label for="mobile-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<!-- Sidebar menu -->
<ul class="menu p-4 w-80 min-h-full bg-base-200">
<li><a>Home</a></li>
<li><a>About</a></li>
<li><a>Contact</a></li>
</ul>
</div>
</div>
```
### Styling Notes
- **Width**: Default `w-80` (320px), adjust with Tailwind width utilities
- **Background**: Use DaisyUI color classes like `bg-base-200`
- **Height**: Always use `min-h-full` to ensure full height
- **Padding**: Add `p-4` or similar for inner spacing
## Desktop Sidebar (Persistent)
### Characteristics
- Always visible (no overlay)
- Does not overlay main content
- Main content adjusts to sidebar width
- No toggle button needed
- Used on desktop screens
### Implementation with drawer-open
```html
<div class="drawer lg:drawer-open">
<input id="desktop-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
<!-- Main content -->
<div class="p-4">
<h1>Main Content</h1>
<p>The sidebar is always visible on desktop (lg and above)</p>
</div>
</div>
<div class="drawer-side">
<!-- No overlay needed for persistent sidebar -->
<label for="desktop-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<!-- Sidebar menu -->
<ul class="menu p-4 w-80 min-h-full bg-base-200">
<li><a>Dashboard</a></li>
<li><a>Settings</a></li>
<li><a>Profile</a></li>
</ul>
</div>
</div>
```
### How drawer-open Works
The `drawer-open` class forces the drawer to be **permanently open**:
```html
<div class="drawer drawer-open">
```
- Drawer is always visible
- Cannot be toggled closed
- `drawer-toggle` checkbox is ignored
- `drawer-overlay` is not shown
- Main content automatically shifts to accommodate sidebar width
### Responsive Usage
Use Tailwind breakpoint modifiers for responsive behavior:
```html
<!-- Open on large screens and above -->
<div class="drawer lg:drawer-open">
<!-- Open on medium screens and above -->
<div class="drawer md:drawer-open">
<!-- Open on extra-large screens and above -->
<div class="drawer xl:drawer-open">
```
## Combined Mobile + Desktop Pattern (Recommended)
This is the **most common pattern** for responsive applications: mobile overlay + desktop persistent.
### Complete Implementation
```html
<div class="drawer lg:drawer-open">
<!-- Checkbox for mobile toggle -->
<input id="app-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col">
<!-- Navbar with mobile menu button -->
<div class="navbar bg-base-100 lg:hidden">
<div class="flex-none">
<label for="app-drawer" class="btn btn-square btn-ghost">
<!-- Hamburger icon -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-5 h-5 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</label>
</div>
<div class="flex-1">
<a class="btn btn-ghost text-xl">My App</a>
</div>
</div>
<!-- Main content -->
<div class="flex-1 p-6">
<h1 class="text-3xl font-bold mb-4">Welcome</h1>
<p>This is the main content area.</p>
<p>On mobile (< lg): sidebar is hidden, hamburger menu visible</p>
<p>On desktop (≥ lg): sidebar is persistent, hamburger menu hidden</p>
</div>
</div>
<div class="drawer-side">
<!-- Overlay only shows on mobile -->
<label for="app-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<!-- Sidebar navigation -->
<aside class="bg-base-200 w-80 min-h-full">
<!-- Logo/Header area -->
<div class="p-4 font-bold text-xl border-b border-base-300">
My App Logo
</div>
<!-- Navigation menu -->
<ul class="menu p-4">
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
Dashboard
</a></li>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Documents
</a></li>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Settings
</a></li>
</ul>
</aside>
</div>
</div>
```
### Behavior Breakdown
#### On Mobile (< 1024px / < lg)
1. Sidebar is hidden by default
2. Hamburger button visible in navbar
3. Clicking hamburger opens sidebar as overlay
4. Clicking overlay or close button closes sidebar
5. Sidebar slides in from left with animation
#### On Desktop (≥ 1024px / ≥ lg)
1. `lg:drawer-open` keeps sidebar permanently visible
2. Hamburger button hidden via `lg:hidden`
3. Sidebar takes up fixed width (320px)
4. Main content area adjusts automatically
5. No overlay, no toggle needed
## Tailwind Breakpoints Reference
```css
/* Default (mobile-first) */
/* < 640px */
sm: /* ≥ 640px */
md: /* ≥ 768px */
lg: /* ≥ 1024px */ ← Common desktop breakpoint
xl: /* ≥ 1280px */
2xl: /* ≥ 1536px */
```
## Key Classes Summary
| Class | Purpose |
|-------|---------|
| `drawer` | Main container |
| `drawer-toggle` | Hidden checkbox for state control |
| `drawer-content` | Main content area |
| `drawer-side` | Sidebar container |
| `drawer-overlay` | Clickable overlay (closes drawer) |
| `drawer-open` | Forces drawer to stay open |
| `drawer-end` | Positions drawer on the right side |
| `lg:drawer-open` | Opens drawer on large screens only |
## Positioning Variants
### Left Side Drawer (Default)
```html
<div class="drawer">
<!-- Drawer appears on the left -->
</div>
```
### Right Side Drawer
```html
<div class="drawer drawer-end">
<!-- Drawer appears on the right -->
</div>
```
## Best Practices
### 1. Accessibility
- Always include `aria-label` on overlay: `<label for="drawer" aria-label="close sidebar" class="drawer-overlay"></label>`
- Use semantic HTML (`<nav>`, `<aside>`)
- Ensure keyboard navigation works (native checkbox provides this)
### 2. Responsive Design
- Use `lg:drawer-open` for desktop persistence
- Hide mobile toggle button on desktop: `lg:hidden`
- Adjust sidebar width for mobile if needed: `w-64 md:w-80`
### 3. Performance
- DaisyUI drawer is pure CSS (no JavaScript needed)
- Animations are handled by CSS transitions
- No performance overhead
### 4. Styling
- Use DaisyUI theme colors: `bg-base-200`, `text-base-content`
- Maintain consistent spacing: `p-4`, `gap-2`
- Use DaisyUI menu component for navigation: `<ul class="menu">`
### 5. Content Structure
```html
<div class="drawer-content flex flex-col">
<!-- Navbar (if needed) -->
<div class="navbar">...</div>
<!-- Main content with flex-1 to fill space -->
<div class="flex-1 p-6">
<!-- Your content -->
</div>
<!-- Footer (if needed) -->
<footer>...</footer>
</div>
```
## Common Patterns
### Pattern 1: Drawer with Close Button
```html
<div class="drawer-side">
<label for="drawer" class="drawer-overlay"></label>
<aside class="bg-base-200 w-80 min-h-full relative">
<!-- Close button (mobile only) -->
<label for="drawer" class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2 lg:hidden"></label>
<!-- Sidebar content -->
<ul class="menu p-4 pt-12">
<li><a>Item 1</a></li>
</ul>
</aside>
</div>
```
### Pattern 2: Drawer with User Profile
```html
<aside class="bg-base-200 w-80 min-h-full flex flex-col">
<!-- Logo -->
<div class="p-4 font-bold text-xl">My App</div>
<!-- Navigation (flex-1 to push footer down) -->
<ul class="menu flex-1 p-4">
<li><a>Dashboard</a></li>
<li><a>Settings</a></li>
</ul>
<!-- User profile footer -->
<div class="p-4 border-t border-base-300">
<div class="flex items-center gap-2">
<div class="avatar">
<div class="w-10 rounded-full">
<img src="/avatar.jpg" alt="User" />
</div>
</div>
<div>
<div class="font-semibold">John Doe</div>
<div class="text-sm opacity-70">john@example.com</div>
</div>
</div>
</div>
</aside>
```
### Pattern 3: Nested Menu with Submenu
```html
<ul class="menu p-4 w-80 min-h-full bg-base-200">
<li><a>Dashboard</a></li>
<!-- Submenu -->
<li>
<details>
<summary>Products</summary>
<ul>
<li><a>Electronics</a></li>
<li><a>Clothing</a></li>
<li><a>Books</a></li>
</ul>
</details>
</li>
<li><a>Settings</a></li>
</ul>
```
## Troubleshooting
### Issue: Drawer doesn't open on mobile
**Solution**: Check that:
1. Checkbox `id` matches label `for` attribute
2. Checkbox has class `drawer-toggle`
3. You're not using `drawer-open` on mobile breakpoints
### Issue: Drawer overlaps content on desktop
**Solution**:
- Remove `drawer-open` or use responsive variant `lg:drawer-open`
- Ensure you want overlay behavior, not persistent sidebar
### Issue: Overlay not clickable
**Solution**:
- Ensure overlay label has correct `for` attribute
- Check that overlay is not behind other elements (z-index)
### Issue: Content jumps when drawer opens
**Solution**:
- Add `flex flex-col` to `drawer-content`
- Ensure drawer-side width is fixed (e.g., `w-80`)
## Migration from Custom Solutions
If migrating from a custom sidebar implementation:
### Replace custom JavaScript
❌ Before:
```javascript
function toggleDrawer() {
document.getElementById('sidebar').classList.toggle('open');
}
```
✅ After:
```html
<input id="drawer" type="checkbox" class="drawer-toggle" />
<label for="drawer">Toggle</label>
```
### Replace custom CSS
❌ Before:
```css
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s;
}
.sidebar.open {
transform: translateX(0);
}
```
✅ After:
```html
<div class="drawer">
<!-- DaisyUI handles all transitions -->
</div>
```
### Replace media query logic
❌ Before:
```css
@media (min-width: 1024px) {
.sidebar { display: block; }
.toggle-button { display: none; }
}
```
✅ After:
```html
<div class="drawer lg:drawer-open">
<label for="drawer" class="lg:hidden">Toggle</label>
</div>
```
## Summary
The DaisyUI drawer pattern provides:
**Zero JavaScript** - Pure CSS solution
**Accessible** - Built-in keyboard support via checkbox
**Responsive** - Easy mobile/desktop variants with Tailwind
**Themeable** - Uses DaisyUI theme colors
**Flexible** - Supports left/right positioning
**Standard** - No custom CSS needed
**Recommended approach**: Use `lg:drawer-open` for desktop with hidden mobile toggle for best responsive experience.

View file

@ -0,0 +1,747 @@
# Sidebar Analysis - Current State
**Erstellt:** 2025-12-16
**Status:** Analyse für Neuimplementierung
**Autor:** Cursor AI Assistant
---
## Executive Summary
Die aktuelle Sidebar-Implementierung verwendet **nicht existierende Custom-CSS-Variants** (`is-drawer-close:` und `is-drawer-open:`), was zu einer defekten Implementierung führt. Die Sidebar ist strukturell basierend auf DaisyUI's Drawer-Komponente, aber die responsive und state-basierte Funktionalität ist nicht funktionsfähig.
**Kritisches Problem:** Die im Code verwendeten Variants `is-drawer-close:*` und `is-drawer-open:*` sind **nicht in Tailwind konfiguriert**, was bedeutet, dass diese Klassen beim Build ignoriert werden.
---
## 1. Dateien-Übersicht
### 1.1 Hauptdateien
| Datei | Zweck | Zeilen | Status |
|-------|-------|--------|--------|
| `lib/mv_web/components/layouts/sidebar.ex` | Sidebar-Komponente (Elixir) | 198 | ⚠️ Verwendet nicht existierende Variants |
| `lib/mv_web/components/layouts/navbar.ex` | Navbar mit Sidebar-Toggle | 48 | ✅ Funktional |
| `lib/mv_web/components/layouts.ex` | Layout-Wrapper mit Drawer | 121 | ✅ Funktional |
| `assets/js/app.js` | JavaScript für Sidebar-Interaktivität | 272 | ✅ Umfangreiche Accessibility-Logik |
| `assets/css/app.css` | CSS-Konfiguration | 103 | ⚠️ Keine Drawer-Variants definiert |
| `assets/tailwind.config.js` | Tailwind-Konfiguration | 75 | ⚠️ Keine Drawer-Variants definiert |
### 1.2 Verwandte Dateien
- `lib/mv_web/components/layouts/root.html.heex` - Root-Layout (minimal, keine Sidebar-Logik)
- `priv/static/images/logo.svg` - Logo (wird vermutlich für Sidebar benötigt)
---
## 2. Aktuelle Struktur
### 2.1 HTML-Struktur (DaisyUI Drawer Pattern)
```html
<!-- In layouts.ex -->
<div class="drawer">
<input id="main-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
<!-- Navbar mit Toggle-Button -->
<navbar with sidebar-toggle button />
<!-- Hauptinhalt -->
<main>...</main>
</div>
<!-- Sidebar -->
<div class="drawer-side">
<button class="drawer-overlay" onclick="close drawer"></button>
<nav id="main-sidebar">
<!-- Navigation Items -->
</nav>
</div>
</div>
```
**Bewertung:** ✅ Korrekte DaisyUI Drawer-Struktur
### 2.2 Sidebar-Komponente (`sidebar.ex`)
**Struktur:**
```elixir
defmodule MvWeb.Layouts.Sidebar do
attr :current_user, :map
attr :club_name, :string
def sidebar(assigns) do
# Rendert Sidebar mit Navigation, Locale-Selector, Theme-Toggle, User-Menu
end
end
```
**Hauptelemente:**
1. **Drawer Overlay** - Button zum Schließen (Mobile)
2. **Navigation Container** (`<nav id="main-sidebar">`)
3. **Menü-Items** - Members, Users, Contributions (nested), Settings
4. **Footer-Bereich** - Locale-Selector, Theme-Toggle, User-Menu
---
## 3. Custom CSS Variants - KRITISCHES PROBLEM
### 3.1 Verwendete Variants im Code
Die Sidebar verwendet folgende Custom-Variants **extensiv**:
```elixir
# Beispiele aus sidebar.ex
"is-drawer-close:overflow-visible"
"is-drawer-close:w-14 is-drawer-open:w-64"
"is-drawer-close:hidden"
"is-drawer-close:tooltip is-drawer-close:tooltip-right"
"is-drawer-close:w-auto"
"is-drawer-close:justify-center"
"is-drawer-close:dropdown-end"
```
**Gefundene Verwendungen:**
- `is-drawer-close:` - 13 Instanzen in sidebar.ex
- `is-drawer-open:` - 1 Instanz in sidebar.ex
### 3.2 Definition der Variants
**❌ NICHT GEFUNDEN in:**
- `assets/css/app.css` - Enthält nur `phx-*-loading` Variants
- `assets/tailwind.config.js` - Enthält nur `phx-*-loading` Variants
**Fazit:** Diese Variants existieren **nicht** und werden beim Tailwind-Build **ignoriert**!
### 3.3 Vorhandene Variants
Nur folgende Custom-Variants sind tatsächlich definiert:
```css
/* In app.css (Tailwind CSS 4.x Syntax) */
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
```
```javascript
/* In tailwind.config.js (Tailwind 3.x Kompatibilität) */
plugin(({addVariant}) => addVariant("phx-click-loading", [...])),
plugin(({addVariant}) => addVariant("phx-submit-loading", [...])),
plugin(({addVariant}) => addVariant("phx-change-loading", [...])),
```
---
## 4. JavaScript-Implementierung
### 4.1 Übersicht
Die JavaScript-Implementierung ist **sehr umfangreich** und fokussiert auf Accessibility:
**Datei:** `assets/js/app.js` (Zeilen 106-270)
**Hauptfunktionalitäten:**
1. ✅ Tabindex-Management für fokussierbare Elemente
2. ✅ ARIA-Attribut-Management (`aria-expanded`)
3. ✅ Keyboard-Navigation (Enter, Space, Escape)
4. ✅ Focus-Management beim Öffnen/Schließen
5. ✅ Dropdown-Integration
### 4.2 Wichtige JavaScript-Funktionen
#### 4.2.1 Tabindex-Management
```javascript
const updateSidebarTabIndex = (isOpen) => {
const allFocusableElements = sidebar.querySelectorAll(
'a[href], button, select, input:not([type="hidden"]), [tabindex]'
)
allFocusableElements.forEach(el => {
if (isOpen) {
// Make focusable when open
el.removeAttribute('tabindex')
} else {
// Remove from tab order when closed
el.setAttribute('tabindex', '-1')
}
})
}
```
**Zweck:** Verhindert, dass Nutzer mit Tab zu unsichtbaren Sidebar-Elementen springen können.
#### 4.2.2 ARIA-Expanded Management
```javascript
const updateAriaExpanded = () => {
const isOpen = drawerToggle.checked
sidebarToggle.setAttribute("aria-expanded", isOpen.toString())
}
```
**Zweck:** Informiert Screen-Reader über den Sidebar-Status.
#### 4.2.3 Focus-Management
```javascript
const getFirstFocusableElement = () => {
// Priority: navigation link > other links > other focusable
const firstNavLink = sidebar.querySelector('a[href][role="menuitem"]')
// ... fallback logic
}
// On open: focus first element
// On close: focus toggle button
```
**Zweck:** Logische Fokus-Reihenfolge für Keyboard-Navigation.
#### 4.2.4 Keyboard-Shortcuts
```javascript
// ESC to close
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && drawerToggle.checked) {
drawerToggle.checked = false
sidebarToggle.focus()
}
})
// Enter/Space on toggle button
sidebarToggle.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
// Toggle drawer and manage focus
}
})
```
### 4.3 LiveView Hooks
**Definierte Hooks:**
```javascript
Hooks.CopyToClipboard = { ... } // Clipboard-Funktionalität
Hooks.ComboBox = { ... } // Dropdown-Prävention bei Enter
```
**Sidebar-spezifisch:** Keine Hooks, nur native DOM-Events.
---
## 5. DaisyUI Dependencies
### 5.1 Verwendete DaisyUI-Komponenten
| Komponente | Verwendung | Klassen |
|------------|-----------|---------|
| **Drawer** | Basis-Layout | `drawer`, `drawer-toggle`, `drawer-side`, `drawer-content`, `drawer-overlay` |
| **Menu** | Navigation | `menu`, `menu-title`, `w-64` |
| **Button** | Toggle, User-Menu | `btn`, `btn-ghost`, `btn-square`, `btn-circle` |
| **Avatar** | User-Menu | `avatar`, `avatar-placeholder` |
| **Dropdown** | User-Menu | `dropdown`, `dropdown-top`, `dropdown-end`, `dropdown-content` |
| **Tooltip** | Icon-Tooltips | `tooltip`, `tooltip-right` (via `data-tip`) |
| **Select** | Locale-Selector | `select`, `select-sm` |
| **Toggle** | Theme-Switch | `toggle`, `theme-controller` |
### 5.2 Standard Tailwind-Klassen
**Layout:**
- `flex`, `flex-col`, `items-start`, `justify-center`
- `gap-2`, `gap-4`, `p-4`, `mt-auto`, `w-full`, `w-64`, `min-h-full`
**Sizing:**
- `size-4`, `size-5`, `w-12`, `w-52`
**Colors:**
- `bg-base-100`, `bg-base-200`, `text-neutral-content`
**Typography:**
- `text-lg`, `text-sm`, `font-bold`
**Accessibility:**
- `sr-only`, `focus:outline-none`, `focus:ring-2`, `focus:ring-primary`
---
## 6. Toggle-Button (Navbar)
### 6.1 Implementierung
**Datei:** `lib/mv_web/components/layouts/navbar.ex`
```elixir
<button
type="button"
onclick="document.getElementById('main-drawer').checked = !document.getElementById('main-drawer').checked"
aria-label={gettext("Toggle navigation menu")}
aria-expanded="false"
aria-controls="main-sidebar"
id="sidebar-toggle"
class="mr-2 btn btn-square btn-ghost"
>
<svg><!-- Layout-Panel-Left Icon --></svg>
</button>
```
**Funktionalität:**
- ✅ Togglet Drawer-Checkbox
- ✅ ARIA-Labels vorhanden
- ✅ Keyboard-accessible
- ⚠️ `aria-expanded` wird durch JavaScript aktualisiert
**Icon:** Custom SVG (Layout-Panel-Left mit Chevron-Right)
---
## 7. Responsive Verhalten
### 7.1 Aktuelles Konzept (nicht funktional)
**Versuchte Implementierung:**
- **Desktop (collapsed):** Sidebar mit 14px Breite (`is-drawer-close:w-14`)
- **Desktop (expanded):** Sidebar mit 64px Breite (`is-drawer-open:w-64`)
- **Mobile:** Overlay-Drawer (DaisyUI Standard)
### 7.2 Problem
Da die `is-drawer-*` Variants nicht existieren, gibt es **kein responsives Verhalten**:
- Die Sidebar hat immer eine feste Breite von `w-64`
- Die conditional hiding (`:hidden`, etc.) funktioniert nicht
- Tooltips werden nicht conditional angezeigt
---
## 8. Accessibility-Features
### 8.1 Implementierte Features
| Feature | Status | Implementierung |
|---------|--------|-----------------|
| **ARIA Labels** | ✅ | Alle interaktiven Elemente haben Labels |
| **ARIA Roles** | ✅ | `menubar`, `menuitem`, `menu`, `button` |
| **ARIA Expanded** | ✅ | Wird durch JS dynamisch gesetzt |
| **ARIA Controls** | ✅ | Toggle → Sidebar verknüpft |
| **Keyboard Navigation** | ✅ | Enter, Space, Escape, Tab |
| **Focus Management** | ✅ | Logische Focus-Reihenfolge |
| **Tabindex Management** | ✅ | Verhindert Focus auf hidden Elements |
| **Screen Reader Only** | ✅ | `.sr-only` für visuelle Labels |
| **Focus Indicators** | ✅ | `focus:ring-2 focus:ring-primary` |
| **Skip Links** | ❌ | Nicht vorhanden |
### 8.2 Accessibility-Score
**Geschätzt:** 90/100 (WCAG 2.1 Level AA konform)
**Verbesserungspotenzial:**
- Skip-Link zur Hauptnavigation hinzufügen
- High-Contrast-Mode testen
---
## 9. Menü-Struktur
### 9.1 Navigation Items
```
📋 Main Menu
├── 👥 Members (/members)
├── 👤 Users (/users)
├── 💰 Contributions (collapsed submenu)
│ ├── Plans (/contribution_types)
│ └── Settings (/contribution_settings)
└── ⚙️ Settings (/settings)
🔽 Footer Area (logged in only)
├── 🌐 Locale Selector (DE/EN)
├── 🌓 Theme Toggle (Light/Dark)
└── 👤 User Menu (Dropdown)
├── Profile (/users/:id)
└── Logout (/sign-out)
```
### 9.2 Conditional Rendering
**Nicht eingeloggt:**
- Sidebar ist leer (nur Struktur)
- Keine Menü-Items
**Eingeloggt:**
- Vollständige Navigation
- Footer-Bereich mit User-Menu
### 9.3 Nested Menu (Contributions)
**Problem:** Das Contributions-Submenu ist **immer versteckt** im collapsed State:
```elixir
<li class="is-drawer-close:hidden" role="none">
<h2 class="flex items-center gap-2 menu-title">
<.icon name="hero-currency-dollar" />
{gettext("Contributions")}
</h2>
<ul role="menu">
<li class="is-drawer-close:hidden">...</li>
<li class="is-drawer-close:hidden">...</li>
</ul>
</li>
```
Da `:hidden` nicht funktioniert, wird das Submenu immer angezeigt.
---
## 10. Theme-Funktionalität
### 10.1 Theme-Toggle
```elixir
<input
type="checkbox"
value="dark"
class="toggle theme-controller"
aria-label={gettext("Toggle dark mode")}
/>
```
**Funktionalität:**
- ✅ DaisyUI `theme-controller` - automatische Theme-Umschaltung
- ✅ Persistence durch `localStorage` (siehe root.html.heex Script)
- ✅ Icon-Wechsel (Sun ↔ Moon)
### 10.2 Definierte Themes
**Datei:** `assets/css/app.css`
1. **Light Theme** (default)
- Base: `oklch(98% 0 0)`
- Primary: `oklch(70% 0.213 47.604)` (Orange/Phoenix-inspiriert)
2. **Dark Theme**
- Base: `oklch(30.33% 0.016 252.42)`
- Primary: `oklch(58% 0.233 277.117)` (Purple/Elixir-inspiriert)
---
## 11. Locale-Funktionalität
### 11.1 Locale-Selector
```elixir
<form method="post" action="/set_locale">
<select
id="locale-select-sidebar"
name="locale"
onchange="this.form.submit()"
class="select select-sm w-full is-drawer-close:w-auto"
>
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
</form>
```
**Funktionalität:**
- ✅ POST zu `/set_locale` Endpoint
- ✅ CSRF-Token included
- ✅ Auto-Submit on change
- ✅ Accessible Label (`.sr-only`)
---
## 12. Probleme und Defekte
### 12.1 Kritische Probleme
| Problem | Schweregrad | Details |
|---------|-------------|---------|
| **Nicht existierende CSS-Variants** | 🔴 Kritisch | `is-drawer-close:*` und `is-drawer-open:*` sind nicht definiert |
| **Keine responsive Funktionalität** | 🔴 Kritisch | Sidebar verhält sich nicht wie geplant |
| **Conditional Styles funktionieren nicht** | 🔴 Kritisch | Hidden/Tooltip/Width-Changes werden ignoriert |
### 12.2 Mittlere Probleme
| Problem | Schweregrad | Details |
|---------|-------------|---------|
| **Kein Logo** | 🟡 Mittel | Logo-Element fehlt komplett in der Sidebar |
| **Submenu immer sichtbar** | 🟡 Mittel | Contributions-Submenu sollte in collapsed State versteckt sein |
| **Toggle-Icon statisch** | 🟡 Mittel | Icon ändert sich nicht zwischen expanded/collapsed |
### 12.3 Kleinere Probleme
| Problem | Schweregrad | Details |
|---------|-------------|---------|
| **Code-Redundanz** | 🟢 Klein | Variants in beiden Tailwind-Configs (3.x und 4.x) |
| **Inline-onclick Handler** | 🟢 Klein | Sollten durch JS-Events ersetzt werden |
| **Keine Skip-Links** | 🟢 Klein | Accessibility-Verbesserung |
---
## 13. Abhängigkeiten
### 13.1 Externe Abhängigkeiten
| Dependency | Version | Verwendung |
|------------|---------|------------|
| **DaisyUI** | Latest (vendor) | Drawer, Menu, Button, etc. |
| **Tailwind CSS** | 4.0.9 | Utility-Klassen |
| **Heroicons** | v2.2.0 | Icons in Navigation |
| **Phoenix LiveView** | ~> 1.1.0 | Backend-Integration |
### 13.2 Interne Abhängigkeiten
| Modul | Verwendung |
|-------|-----------|
| `MvWeb.Gettext` | Internationalisierung |
| `Mv.Membership.get_settings()` | Club-Name abrufen |
| `MvWeb.CoreComponents` | Icons, Links |
---
## 14. Code-Qualität
### 14.1 Positives
- ✅ **Sehr gute Accessibility-Implementierung**
- ✅ **Saubere Modulstruktur** (Separation of Concerns)
- ✅ **Gute Dokumentation** (Moduledocs, Attribute docs)
- ✅ **Internationalisierung** vollständig implementiert
- ✅ **ARIA-Best-Practices** befolgt
- ✅ **Keyboard-Navigation** umfassend
### 14.2 Verbesserungsbedarf
- ❌ **Broken CSS-Variants** (Hauptproblem)
- ❌ **Fehlende Tests** (keine Component-Tests gefunden)
- ⚠️ **Inline-JavaScript** in onclick-Attributen
- ⚠️ **Magic-IDs** (`main-drawer`, `sidebar-toggle`) hardcoded
- ⚠️ **Komplexe JavaScript-Logik** ohne Dokumentation
---
## 15. Empfehlungen für Neuimplementierung
### 15.1 Sofort-Maßnahmen
1. **CSS-Variants entfernen**
- Alle `is-drawer-close:*` und `is-drawer-open:*` entfernen
- Durch Standard-Tailwind oder DaisyUI-Mechanismen ersetzen
2. **Logo hinzufügen**
- Logo-Element als erstes Element in Sidebar
- Konsistente Größe (32px / size-8)
3. **Toggle-Icon implementieren**
- Icon-Swap zwischen Chevron-Left und Chevron-Right
- Nur auf Desktop sichtbar
### 15.2 Architektur-Entscheidungen
1. **Responsive Strategie:**
- **Mobile:** Standard DaisyUI Drawer (Overlay)
- **Desktop:** Persistent Sidebar mit fester Breite
- **Kein collapsing auf Desktop** (einfacher, wartbarer)
2. **State-Management:**
- Drawer-Checkbox für Mobile
- Keine zusätzlichen Custom-Variants
- Standard DaisyUI-Mechanismen verwenden
3. **JavaScript-Refactoring:**
- Hooks statt inline-onclick
- Dokumentierte Funktionen
- Unit-Tests für kritische Logik
### 15.3 Prioritäten
**High Priority:**
1. CSS-Variants-Problem lösen
2. Logo implementieren
3. Basic responsive Funktionalität
**Medium Priority:**
4. Toggle-Icon implementieren
5. Tests schreiben
6. JavaScript refactoren
**Low Priority:**
7. Skip-Links hinzufügen
8. Code-Optimierung
9. Performance-Tuning
---
## 16. Checkliste für Neuimplementierung
### 16.1 Vorbereitung
- [ ] Alle `is-drawer-*` Klassen aus Code entfernen
- [ ] Keine Custom-Variants in CSS/Tailwind definieren
- [ ] DaisyUI-Dokumentation für Drawer studieren
### 16.2 Implementation
- [ ] Logo-Element hinzufügen (size-8, persistent)
- [ ] Toggle-Button mit Icon-Swap (nur Desktop)
- [ ] Mobile: Overlay-Drawer (DaisyUI Standard)
- [ ] Desktop: Persistent Sidebar (w-64)
- [ ] Menü-Items mit korrekten Klassen
- [ ] Submenu-Handling (nested `<ul>`)
### 16.3 Funktionalität
- [ ] Toggle-Funktionalität auf Mobile
- [ ] Accessibility: ARIA, Focus, Keyboard
- [ ] Theme-Toggle funktional
- [ ] Locale-Selector funktional
- [ ] User-Menu-Dropdown funktional
### 16.4 Testing
- [ ] Component-Tests schreiben
- [ ] Accessibility-Tests (axe-core)
- [ ] Keyboard-Navigation testen
- [ ] Screen-Reader testen
- [ ] Responsive Breakpoints testen
### 16.5 Dokumentation
- [ ] Code-Kommentare aktualisieren
- [ ] Component-Docs schreiben
- [ ] README aktualisieren
---
## 17. Technische Details
### 17.1 CSS-Selektoren
**Verwendete IDs:**
- `#main-drawer` - Drawer-Toggle-Checkbox
- `#main-sidebar` - Sidebar-Navigation-Container
- `#sidebar-toggle` - Toggle-Button in Navbar
- `#locale-select-sidebar` - Locale-Dropdown
**Verwendete Klassen:**
- `.drawer-side` - DaisyUI Sidebar-Container
- `.drawer-overlay` - DaisyUI Overlay-Button
- `.drawer-content` - DaisyUI Content-Container
- `.menu` - DaisyUI Menu-Container
- `.is-drawer-close:*` - ❌ NICHT DEFINIERT
- `.is-drawer-open:*` - ❌ NICHT DEFINIERT
### 17.2 Event-Handler
**JavaScript:**
```javascript
drawerToggle.addEventListener("change", ...)
sidebarToggle.addEventListener("click", ...)
sidebarToggle.addEventListener("keydown", ...)
document.addEventListener("keydown", ...) // ESC handler
```
**Inline (zu migrieren):**
```elixir
onclick="document.getElementById('main-drawer').checked = false"
onclick="document.getElementById('main-drawer').checked = !..."
onchange="this.form.submit()"
```
---
## 18. Metriken
### 18.1 Code-Metriken
| Metrik | Wert |
|--------|------|
| **Zeilen Code (Sidebar)** | 198 |
| **Zeilen JavaScript** | 165 (Sidebar-spezifisch) |
| **Zeilen CSS** | 0 (nur Tailwind-Klassen) |
| **Anzahl Komponenten** | 1 (Sidebar) + 1 (Navbar) |
| **Anzahl Menü-Items** | 6 (inkl. Submenu) |
| **Anzahl Footer-Controls** | 3 (Locale, Theme, User) |
### 18.2 Abhängigkeits-Metriken
| Kategorie | Anzahl |
|-----------|--------|
| **DaisyUI-Komponenten** | 7 |
| **Tailwind-Utility-Klassen** | ~50 |
| **Custom-Variants (broken)** | 2 (`is-drawer-close`, `is-drawer-open`) |
| **JavaScript-Event-Listener** | 6 |
| **ARIA-Attribute** | 12 |
---
## 19. Zusammenfassung
### 19.1 Was funktioniert
✅ **Sehr gute Grundlage:**
- DaisyUI Drawer-Pattern korrekt implementiert
- Exzellente Accessibility (ARIA, Keyboard, Focus)
- Saubere Modulstruktur
- Internationalisierung
- Theme-Switching
- JavaScript-Logik ist robust
### 19.2 Was nicht funktioniert
❌ **Kritische Defekte:**
- CSS-Variants existieren nicht → keine responsive Funktionalität
- Kein Logo
- Kein Toggle-Icon-Swap
- Submenu-Handling defekt
### 19.3 Nächste Schritte
1. **CSS-Variants entfernen** (alle `is-drawer-*` Klassen)
2. **Standard DaisyUI-Pattern verwenden** (ohne Custom-Variants)
3. **Logo hinzufügen** (persistent, size-8)
4. **Simplify:** Mobile = Overlay, Desktop = Persistent (keine collapsed State)
5. **Tests schreiben** (Component + Accessibility)
---
## 20. Anhang
### 20.1 Verwendete CSS-Klassen (alphabetisch)
```
avatar, avatar-placeholder, bg-base-100, bg-base-200, bg-neutral,
btn, btn-circle, btn-ghost, btn-square, cursor-pointer, drawer,
drawer-content, drawer-overlay, drawer-side, drawer-toggle, dropdown,
dropdown-content, dropdown-end, dropdown-top, flex, flex-col,
focus:outline-none, focus:ring-2, focus:ring-primary,
focus-within:outline-none, focus-within:ring-2, gap-2, gap-4,
is-drawer-close:*, is-drawer-open:*, items-center, items-start,
mb-2, menu, menu-sm, menu-title, min-h-full, mr-2, mt-3, mt-auto,
p-2, p-4, rounded-box, rounded-full, select, select-sm, shadow,
shadow-sm, size-4, size-5, sr-only, text-lg, text-neutral-content,
text-sm, theme-controller, toggle, tooltip, tooltip-right, w-12,
w-52, w-64, w-full, z-1
```
### 20.2 Verwendete ARIA-Attribute
```
aria-busy, aria-controls, aria-describedby, aria-expanded,
aria-haspopup, aria-hidden, aria-label, aria-labelledby,
aria-live, role="alert", role="button", role="menu",
role="menubar", role="menuitem", role="none", role="status"
```
### 20.3 Relevante Links
- [DaisyUI Drawer Docs](https://daisyui.com/components/drawer/)
- [Tailwind CSS Custom Variants](https://tailwindcss.com/docs/adding-custom-styles#adding-custom-variants)
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
- [Phoenix LiveView Docs](https://hexdocs.pm/phoenix_live_view/)
---
**Ende des Berichts**

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,233 @@
# Analyse der fehlschlagenden Tests
## Übersicht
**Gesamtanzahl fehlschlagender Tests:** 5
- **show_test.exs:** 1 Fehler
- **sidebar_test.exs:** 4 Fehler
---
## Kategorisierung
### Kategorie 1: Test-Assertions passen nicht zur Implementierung (4 Tests)
Diese Tests erwarten bestimmte Werte/Attribute, die in der aktuellen Implementierung anders sind oder fehlen.
### Kategorie 2: Datenbank-Isolation Problem (1 Test)
Ein Test schlägt fehl, weil die Datenbank nicht korrekt isoliert ist.
---
## Detaillierte Analyse
### 1. `show_test.exs` - Custom Fields Sichtbarkeit
**Test:** `does not display Custom Fields section when no custom fields exist` (Zeile 112)
**Problem:**
- Der Test erwartet, dass die "Custom Fields" Sektion NICHT angezeigt wird, wenn keine Custom Fields existieren
- Die Sektion wird aber angezeigt, weil in der Datenbank noch Custom Fields von anderen Tests vorhanden sind
**Ursache:**
- Die LiveView lädt alle Custom Fields aus der Datenbank (Zeile 238-242 in `show.ex`)
- Die Test-Datenbank wird nicht zwischen Tests geleert
- Da `async: false` verwendet wird, sollten die Tests sequenziell laufen, aber Custom Fields bleiben in der Datenbank
**Kategorie:** Datenbank-Isolation Problem
---
### 2. `sidebar_test.exs` - Settings Link
**Test:** `T3.1: renders flat menu items with icons and labels` (Zeile 174)
**Problem:**
- Test erwartet `href="#"` für Settings
- Tatsächlicher Wert: `href="/settings"`
**Ursache:**
- Die Implementierung verwendet einen echten Link `~p"/settings"` (Zeile 100 in `sidebar.ex`)
- Der Test erwartet einen Placeholder-Link `href="#"`
**Kategorie:** Test-Assertion passt nicht zur Implementierung
---
### 3. `sidebar_test.exs` - Drawer Overlay CSS-Klasse
**Test:** `drawer overlay is present` (Zeile 747)
**Problem:**
- Test sucht nach exakt `class="drawer-overlay"`
- Tatsächlicher Wert: `class="drawer-overlay lg:hidden focus:outline-none focus:ring-2 focus:ring-primary"`
**Ursache:**
- Der Test verwendet eine exakte String-Suche (`~s(class="drawer-overlay")`)
- Die Implementierung hat mehrere CSS-Klassen
**Kategorie:** Test-Assertion passt nicht zur Implementierung
---
### 4. `sidebar_test.exs` - Toggle Button ARIA-Attribut
**Test:** `T5.2: toggle button has correct ARIA attributes` (Zeile 324)
**Problem:**
- Test erwartet `aria-controls="main-sidebar"` am Toggle-Button
- Das Attribut fehlt in der Implementierung (Zeile 45-65 in `sidebar.ex`)
**Ursache:**
- Das `aria-controls` Attribut wurde nicht in der Implementierung hinzugefügt
- Der Test erwartet es für bessere Accessibility
**Kategorie:** Test-Assertion passt nicht zur Implementierung (Accessibility-Feature fehlt)
---
### 5. `sidebar_test.exs` - Contribution Settings Link
**Test:** `sidebar structure is complete with all sections` (Zeile 501)
**Problem:**
- Test erwartet Link `/contribution_settings`
- Tatsächlicher Link: `/membership_fee_settings`
**Ursache:**
- Der Test hat eine veraltete/inkorrekte Erwartung
- Die Implementierung verwendet `/membership_fee_settings` (Zeile 96 in `sidebar.ex`)
**Kategorie:** Test-Assertion passt nicht zur Implementierung (veralteter Test)
---
## Lösungsvorschläge
### Lösung 1: `show_test.exs` - Custom Fields Sichtbarkeit
**Option A: Test-Datenbank bereinigen (Empfohlen)**
- Im `setup` Block alle Custom Fields löschen, bevor der Test läuft
- Oder: Explizit prüfen, dass keine Custom Fields existieren
**Option B: Test anpassen**
- Den Test so anpassen, dass er explizit alle Custom Fields löscht
- Oder: Die LiveView-Logik ändern, um nur Custom Fields zu laden, die tatsächlich existieren
**Empfehlung:** Option A - Im Test-Setup alle Custom Fields löschen
```elixir
setup do
# Clean up any existing custom fields
Mv.Membership.CustomField
|> Ash.read!()
|> Enum.each(&Ash.destroy!/1)
# Create test member
{:ok, member} = ...
%{member: member}
end
```
---
### Lösung 2: `sidebar_test.exs` - Settings Link
**Option A: Test anpassen (Empfohlen)**
- Test ändern, um `href="/settings"` zu erwarten statt `href="#"`
**Option B: Implementierung ändern**
- Settings-Link zu `href="#"` ändern (nicht empfohlen, da es ein echter Link sein sollte)
**Empfehlung:** Option A - Test anpassen
```elixir
# Zeile 190 ändern von:
assert html =~ ~s(href="#")
# zu:
assert html =~ ~s(href="/settings")
```
---
### Lösung 3: `sidebar_test.exs` - Drawer Overlay CSS-Klasse
**Option A: Test anpassen (Empfohlen)**
- Test ändern, um nach der Klasse in der Klasse-Liste zu suchen (mit `has_class?` Helper)
**Option B: Regex verwenden**
- Regex verwenden, um die Klasse zu finden
**Empfehlung:** Option A - Test anpassen
```elixir
# Zeile 752 ändern von:
assert html =~ ~s(class="drawer-overlay")
# zu:
assert has_class?(html, "drawer-overlay")
```
---
### Lösung 4: `sidebar_test.exs` - Toggle Button ARIA-Attribut
**Option A: Implementierung anpassen (Empfohlen)**
- `aria-controls="main-sidebar"` zum Toggle-Button hinzufügen
**Option B: Test anpassen**
- Test entfernen oder als optional markieren (nicht empfohlen für Accessibility)
**Empfehlung:** Option A - Implementierung anpassen
```elixir
# In sidebar.ex Zeile 45-52, aria-controls hinzufügen:
<button
type="button"
id="sidebar-toggle"
class="hidden lg:flex ml-auto btn btn-ghost btn-sm btn-square"
aria-label={gettext("Toggle sidebar")}
aria-controls="main-sidebar"
aria-expanded="true"
onclick="toggleSidebar()"
>
```
---
### Lösung 5: `sidebar_test.exs` - Contribution Settings Link
**Option A: Test anpassen (Empfohlen)**
- Test ändern, um `/membership_fee_settings` statt `/contribution_settings` zu erwarten
**Option B: Link hinzufügen**
- Einen neuen Link `/contribution_settings` hinzufügen (nicht empfohlen, da redundant)
**Empfehlung:** Option A - Test anpassen
```elixir
# Zeile 519 ändern von:
"/contribution_settings",
# zu:
# Entfernen oder durch "/membership_fee_settings" ersetzen
# (da "/membership_fee_settings" bereits in Zeile 518 vorhanden ist)
```
---
## Zusammenfassung der empfohlenen Änderungen
1. **show_test.exs:** Custom Fields im Setup löschen
2. **sidebar_test.exs (T3.1):** Settings-Link Assertion anpassen
3. **sidebar_test.exs (drawer overlay):** CSS-Klasse-Suche mit Helper-Funktion
4. **sidebar_test.exs (T5.2):** `aria-controls` Attribut zur Implementierung hinzufügen
5. **sidebar_test.exs (edge cases):** Falschen Link aus erwarteter Liste entfernen
---
## Priorisierung
1. **Hoch:** Lösung 1 (show_test.exs) - Datenbank-Isolation ist wichtig
2. **Mittel:** Lösung 4 (ARIA-Attribut) - Accessibility-Verbesserung
3. **Niedrig:** Lösungen 2, 3, 5 - Einfache Test-Anpassungen

1576
docs/umsetzung-sidebar.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@ defmodule MvWeb.Layouts do
""" """
use MvWeb, :html use MvWeb, :html
use Gettext, backend: MvWeb.Gettext use Gettext, backend: MvWeb.Gettext
import MvWeb.Layouts.Navbar import MvWeb.Layouts.Sidebar
embed_templates "layouts/*" embed_templates "layouts/*"
@ -43,20 +43,66 @@ defmodule MvWeb.Layouts do
slot :inner_block, required: true slot :inner_block, required: true
def app(assigns) do def app(assigns) do
club_name = get_club_name()
assigns = assign(assigns, :club_name, club_name)
~H""" ~H"""
<%= if @current_user do %> <%= if @current_user do %>
<.navbar current_user={@current_user} club_name={@club_name} /> <div
<% end %> id="app-layout"
<main class="px-4 py-20 sm:px-6 lg:px-16"> class="drawer lg:drawer-open"
<div class="mx-auto max-full space-y-4"> data-sidebar-expanded="true"
{render_slot(@inner_block)} phx-hook="SidebarState"
>
<input id="mobile-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col relative z-0">
<!-- Mobile Header (only visible on mobile) -->
<header class="lg:hidden sticky top-0 z-10 navbar bg-base-100 shadow-sm">
<label
for="mobile-drawer"
class="btn btn-square btn-ghost"
aria-label={gettext("Open navigation menu")}
>
<.icon name="hero-bars-3" class="size-6" aria-hidden="true" />
</label>
<span class="font-bold">{@club_name}</span>
</header>
<!-- Main Content (shared between mobile and desktop) -->
<main class="px-4 py-8 sm:px-6 lg:px-8">
<div class="mx-auto space-y-4 max-full">
{render_slot(@inner_block)}
</div>
</main>
</div>
<div class="drawer-side z-40">
<.sidebar current_user={@current_user} club_name={@club_name} mobile={false} />
</div>
</div> </div>
</main> <% else %>
<!-- Not logged in -->
<main class="px-4 py-8 sm:px-6">
<div class="mx-auto space-y-4 max-full">
{render_slot(@inner_block)}
</div>
</main>
<% end %>
<.flash_group flash={@flash} /> <.flash_group flash={@flash} />
""" """
end end
# Helper function to get club name from settings
# Falls back to "Mitgliederverwaltung" if settings can't be loaded
defp get_club_name do
case Mv.Membership.get_settings() do
{:ok, settings} -> settings.club_name
_ -> "Mitgliederverwaltung"
end
end
@doc """ @doc """
Shows the flash group with standard titles and content. Shows the flash group with standard titles and content.
@ -69,7 +115,7 @@ defmodule MvWeb.Layouts do
def flash_group(assigns) do def flash_group(assigns) do
~H""" ~H"""
<div id={@id} aria-live="polite" class="toast toast-top toast-end z-50 flex flex-col gap-2"> <div id={@id} aria-live="polite" class="z-50 flex flex-col gap-2 toast toast-top toast-end">
<.flash kind={:success} flash={@flash} /> <.flash kind={:success} flash={@flash} />
<.flash kind={:warning} flash={@flash} /> <.flash kind={:warning} flash={@flash} />
<.flash kind={:info} flash={@flash} /> <.flash kind={:info} flash={@flash} />

View file

@ -1,152 +0,0 @@
defmodule MvWeb.Layouts.Navbar do
@moduledoc """
Navbar that is used in the rootlayout shown on every page
"""
use Phoenix.Component
use Gettext, backend: MvWeb.Gettext
use MvWeb, :verified_routes
alias Mv.Membership
import MvWeb.Authorization
attr :current_user, :map,
required: true,
doc: "The current user - navbar is only shown when user is present"
attr :club_name, :string,
default: nil,
doc: "Optional club name - if not provided, will be loaded from database"
def navbar(assigns) do
club_name = assigns[:club_name] || get_club_name()
assigns = assign(assigns, :club_name, club_name)
~H"""
<header class="navbar bg-base-100 shadow-sm">
<div class="flex-1">
<a href="/members" class="btn btn-ghost text-xl">{@club_name}</a>
<ul class="menu menu-horizontal bg-base-200">
<li><.link navigate="/members">{gettext("Members")}</.link></li>
<li>
<details>
<summary>{gettext("Settings")}</summary>
<ul class="bg-base-200 rounded-t-none p-2 z-10 w-48">
<li>
<.link navigate="/settings">{gettext("Global Settings")}</.link>
</li>
<%= if can_access_page?(@current_user, ~p"/admin/roles") do %>
<li>
<.link navigate={~p"/admin/roles"}>{gettext("Roles")}</.link>
</li>
<% end %>
</ul>
</details>
</li>
<li><.link navigate="/users">{gettext("Users")}</.link></li>
<li>
<details>
<summary>{gettext("Contributions")}</summary>
<ul class="bg-base-200 rounded-t-none p-2 z-10">
<li>
<.link navigate="/membership_fee_types">{gettext("Membership Fee Types")}</.link>
</li>
<li>
<.link navigate="/membership_fee_settings">
{gettext("Membership Fee Settings")}
</.link>
</li>
</ul>
</details>
</li>
</ul>
</div>
<div class="flex gap-2">
<form method="post" action="/set_locale" class="mr-4">
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()} />
<label class="sr-only" for="locale-select">{gettext("Select language")}</label>
<select
id="locale-select"
name="locale"
onchange="this.form.submit()"
class="select select-sm"
aria-label={gettext("Select language")}
>
<option value="de" selected={Gettext.get_locale() == "de"}>Deutsch</option>
<option value="en" selected={Gettext.get_locale() == "en"}>English</option>
</select>
</form>
<!-- Daisy UI Theme Toggle for dark and light mode-->
<label class="flex cursor-pointer gap-2" aria-label={gettext("Toggle dark mode")}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="5" />
<path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" />
</svg>
<input
type="checkbox"
value="dark"
class="toggle theme-controller"
aria-label={gettext("Toggle dark mode")}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</label>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar avatar-placeholder">
<div class="bg-neutral text-neutral-content w-12 rounded-full">
<span>AA</span>
</div>
</div>
<ul
tabindex="0"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"
>
<li>
<.link navigate={~p"/users/#{@current_user.id}"}>
{gettext("Profil")}
</.link>
</li>
<li>
<.link navigate={~p"/settings"}>{gettext("Settings")}</.link>
</li>
<li>
<.link href={~p"/sign-out"}>{gettext("Logout")}</.link>
</li>
</ul>
</div>
</div>
</header>
"""
end
# Helper function to get club name from settings
# Falls back to "Mitgliederverwaltung" if settings can't be loaded
defp get_club_name do
case Membership.get_settings() do
{:ok, settings} -> settings.club_name
_ -> "Mitgliederverwaltung"
end
end
end

View file

@ -0,0 +1,323 @@
defmodule MvWeb.Layouts.Sidebar do
@moduledoc """
Sidebar navigation component used in the drawer layout
"""
use MvWeb, :html
attr :current_user, :map, default: nil, doc: "The current user"
attr :club_name, :string, required: true, doc: "The name of the club"
attr :mobile, :boolean, default: false, doc: "Whether this is mobile view"
def sidebar(assigns) do
~H"""
<label
for="mobile-drawer"
aria-label={gettext("Close sidebar")}
class="drawer-overlay lg:hidden focus:outline-none focus:ring-2 focus:ring-primary"
tabindex="-1"
>
</label>
<aside id="main-sidebar" class="sidebar" aria-label={gettext("Main navigation")}>
{sidebar_header(assigns)}
<%= if @current_user do %>
{sidebar_menu(assigns)}
<% end %>
<%= if @current_user do %>
<.sidebar_footer current_user={@current_user} />
<% end %>
</aside>
"""
end
defp sidebar_header(assigns) do
~H"""
<div class="flex items-center gap-3 p-4 border-b border-base-300">
<!-- Logo -->
<img src={~p"/images/mila.svg"} alt="Mila Logo" class="size-8 shrink-0" />
<!-- Club Name -->
<span class="menu-label text-lg font-bold truncate">
{@club_name}
</span>
<!-- Toggle Button (Desktop only) -->
<%= unless @mobile do %>
<button
type="button"
id="sidebar-toggle"
class="hidden lg:flex ml-auto btn btn-ghost btn-sm btn-square"
aria-label={gettext("Toggle sidebar")}
aria-controls="main-sidebar"
aria-expanded="true"
onclick="toggleSidebar()"
>
<!-- Expanded Icon (Chevron Left) -->
<.icon
name="hero-chevron-left"
class="size-5 sidebar-expanded-icon"
aria-hidden="true"
/>
<!-- Collapsed Icon (Chevron Right) -->
<.icon
name="hero-chevron-right"
class="size-5 sidebar-collapsed-icon"
aria-hidden="true"
/>
</button>
<% end %>
</div>
"""
end
defp sidebar_menu(assigns) do
~H"""
<ul class="menu flex-1 w-full p-2" role="menubar">
<.menu_item
href={~p"/members"}
icon="hero-users"
label={gettext("Members")}
/>
<.menu_item
href={~p"/users"}
icon="hero-user-circle"
label={gettext("Users")}
/>
<.menu_item
href={~p"/custom_field_values"}
icon="hero-rectangle-group"
label={gettext("Custom Fields")}
/>
<!-- Nested Menu: Contributions -->
<.menu_group
icon="hero-currency-dollar"
label={gettext("Contributions")}
>
<.menu_subitem href="/contribution_types" label={gettext("Contribution Types")} />
<.menu_subitem href="/membership_fee_settings" label={gettext("Settings")} />
</.menu_group>
<.menu_item
href={~p"/settings"}
icon="hero-cog-6-tooth"
label={gettext("Settings")}
/>
</ul>
"""
end
attr :href, :string, required: true, doc: "Navigation path"
attr :icon, :string, required: true, doc: "Heroicon name"
attr :label, :string, required: true, doc: "Menu item label"
defp menu_item(assigns) do
~H"""
<li role="none">
<.link
navigate={@href}
class="flex items-center gap-3 tooltip tooltip-right focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
data-tip={@label}
role="menuitem"
>
<.icon name={@icon} class="size-5 shrink-0" aria-hidden="true" />
<span class="menu-label">{@label}</span>
</.link>
</li>
"""
end
attr :icon, :string, required: true, doc: "Heroicon name for the menu group"
attr :label, :string, required: true, doc: "Menu group label"
slot :inner_block, required: true, doc: "Submenu items"
defp menu_group(assigns) do
~H"""
<li role="none" class="menu-group">
<!-- Expanded Mode: Details/Summary -->
<details class="expanded-menu-group">
<summary
class="flex items-center gap-3 cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
role="menuitem"
aria-haspopup="true"
>
<.icon name={@icon} class="size-5 shrink-0" aria-hidden="true" />
<span class="menu-label">{@label}</span>
</summary>
<ul role="menu" class="ml-4">
{render_slot(@inner_block)}
</ul>
</details>
<!-- Collapsed Mode: Dropdown -->
<div class="collapsed-menu-group dropdown dropdown-right">
<button
type="button"
tabindex="0"
class="flex items-center w-full p-2 rounded-lg hover:bg-base-300 tooltip tooltip-right focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
data-tip={@label}
aria-haspopup="menu"
aria-label={@label}
>
<.icon name={@icon} class="size-5" aria-hidden="true" />
</button>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box shadow-lg z-50 min-w-48 p-2 focus:outline-none"
role="menu"
>
<li class="menu-title">{@label}</li>
{render_slot(@inner_block)}
</ul>
</div>
</li>
"""
end
attr :href, :string, required: true, doc: "Navigation path for submenu item"
attr :label, :string, required: true, doc: "Submenu item label"
defp menu_subitem(assigns) do
~H"""
<li role="none">
<.link
navigate={@href}
role="menuitem"
class="focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
{@label}
</.link>
</li>
"""
end
attr :current_user, :map, default: nil, doc: "The current user"
defp sidebar_footer(assigns) do
~H"""
<div class="mt-auto p-4 border-t border-base-300 space-y-4">
<!-- Language Selector (nur expanded) -->
<form method="post" action={~p"/set_locale"} class="expanded-only">
<input type="hidden" name="_csrf_token" value={get_csrf_token()} />
<select
name="locale"
onchange="this.form.submit()"
class="select select-sm w-full focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
aria-label={gettext("Select language")}
>
<option value="de" selected={Gettext.get_locale() == "de"}>Deutsch</option>
<option value="en" selected={Gettext.get_locale() == "en"}>English</option>
</select>
</form>
<!-- Theme Toggle (immer sichtbar) -->
<.theme_toggle />
<!-- User Menu (nur wenn current_user existiert) -->
<%= if @current_user do %>
<.user_menu current_user={@current_user} />
<% end %>
</div>
"""
end
defp theme_toggle(assigns) do
~H"""
<label
class="flex items-center gap-2 cursor-pointer justify-center focus-within:outline-none focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2"
aria-label={gettext("Toggle dark mode")}
>
<.icon name="hero-sun" class="size-5" aria-hidden="true" />
<input
type="checkbox"
value="dark"
class="toggle toggle-sm theme-controller focus:outline-none"
aria-label={gettext("Toggle dark mode")}
/>
<.icon name="hero-moon" class="size-5" aria-hidden="true" />
</label>
"""
end
attr :current_user, :map, default: nil, doc: "The current user"
defp user_menu(assigns) do
# Defensive check: ensure current_user and email exist
# current_user might be a struct, so we need to handle both maps and structs
# email might be an Ash.CiString, so we use to_string/1 (not String.to_string/1)
email =
case assigns.current_user do
nil -> ""
%{email: email_val} when not is_nil(email_val) -> to_string(email_val)
_ -> ""
end
first_letter =
if email != "" do
String.first(email) |> String.upcase()
else
"?"
end
user_id =
case assigns.current_user do
nil -> nil
%{id: id_val} when not is_nil(id_val) -> id_val
_ -> nil
end
# Store computed values in assigns to avoid HEEx warnings
assigns = assign(assigns, :email, email)
assigns = assign(assigns, :first_letter, first_letter)
assigns = assign(assigns, :user_id, user_id)
~H"""
<div class="dropdown dropdown-top dropdown-end w-full">
<button
type="button"
tabindex="0"
class="user-menu-button flex items-center w-full justify-start gap-3 px-4 h-12 min-h-12 rounded-lg cursor-pointer focus:outline-none"
aria-label={gettext("User menu")}
aria-haspopup="menu"
>
<!-- Avatar: Placeholder with first letter -->
<div class="avatar placeholder shrink-0">
<div class="w-8 h-8 rounded-full bg-neutral text-neutral-content">
<span class="text-sm font-semibold">
{@first_letter}
</span>
</div>
</div>
<span class="menu-label truncate flex-1 text-left">{@email}</span>
</button>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box shadow-lg w-52 p-2 focus:outline-none absolute z-[100]"
role="menu"
>
<li role="none">
<%= if @user_id do %>
<.link
navigate={~p"/users/#{@user_id}"}
role="menuitem"
class="focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
{gettext("Profile")}
</.link>
<% else %>
<span class="opacity-50">{gettext("Profile")}</span>
<% end %>
</li>
<li role="none">
<.link
href={~p"/sign-out"}
role="menuitem"
class="focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
{gettext("Logout")}
</.link>
</li>
</ul>
</div>
"""
end
end

View file

@ -302,7 +302,7 @@ msgstr "Benutzer*in bearbeiten"
msgid "Enabled" msgid "Enabled"
msgstr "Aktiviert" msgstr "Aktiviert"
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Logout" msgid "Logout"
msgstr "Abmelden" msgstr "Abmelden"
@ -319,6 +319,7 @@ msgid "Member"
msgstr "Mitglied" msgstr "Mitglied"
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/contribution_type_live/index.ex
#: lib/mv_web/live/member_live/index.ex #: lib/mv_web/live/member_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
@ -362,11 +363,6 @@ msgstr "Hinweis"
msgid "Password Authentication" msgid "Password Authentication"
msgstr "Passwort-Authentifizierung" msgstr "Passwort-Authentifizierung"
#: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr "Profil"
#: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/form_component.ex
#: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex
#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/form_component.ex
@ -386,6 +382,7 @@ msgid "Select member"
msgstr "Mitglied auswählen" msgstr "Mitglied auswählen"
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
@ -555,11 +552,13 @@ msgid "Back to users list"
msgstr "Zurück zur Benutzer*innen-Liste" msgstr "Zurück zur Benutzer*innen-Liste"
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Select language" msgid "Select language"
msgstr "Sprache auswählen" msgstr "Sprache auswählen"
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Toggle dark mode" msgid "Toggle dark mode"
msgstr "Dunklen Modus umschalten" msgstr "Dunklen Modus umschalten"
@ -571,6 +570,7 @@ msgid "Search..."
msgstr "Suchen..." msgstr "Suchen..."
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Users" msgid "Users"
@ -636,6 +636,7 @@ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}"
msgid "Please select a custom field first" msgid "Please select a custom field first"
msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld" msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/form.ex
#: lib/mv_web/live/member_live/show.ex #: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -919,6 +920,7 @@ msgstr "Beitragsart ändern"
msgid "Contribution Start" msgid "Contribution Start"
msgstr "Beitragsbeginn" msgstr "Beitragsbeginn"
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/contribution_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Contribution Types" msgid "Contribution Types"
@ -930,6 +932,7 @@ msgid "Contribution type"
msgstr "Beitragsart" msgstr "Beitragsart"
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Contributions" msgid "Contributions"
msgstr "Beiträge" msgstr "Beiträge"
@ -1735,7 +1738,12 @@ msgstr "Wählen Sie eine Mitgliedsbeitragsart für dieses Mitglied. Mitglieder k
msgid "Select interval" msgid "Select interval"
msgstr "Intervall auswählen" msgstr "Intervall auswählen"
#: lib/mv_web/live/member_live/show.ex #: lib/mv_web/live/membership_fee_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Settings saved successfully."
msgstr "Einstellungen erfolgreich gespeichert"
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "This action cannot be undone." msgid "This action cannot be undone."
msgstr "Diese Aktion kann nicht rückgängig gemacht werden." msgstr "Diese Aktion kann nicht rückgängig gemacht werden."
@ -1756,6 +1764,7 @@ msgid "This membership fee type is automatically assigned to all new members. Ca
msgstr "Diese Mitgliedsbeitragsart wird automatisch allen neuen Mitgliedern zugewiesen. Kann individuell pro Mitglied geändert werden." msgstr "Diese Mitgliedsbeitragsart wird automatisch allen neuen Mitgliedern zugewiesen. Kann individuell pro Mitglied geändert werden."
#: lib/mv_web/live/member_live/show.ex #: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Type" msgid "Type"
msgstr "Art" msgstr "Art"
@ -1872,31 +1881,198 @@ msgstr "Neues Datenfeld"
msgid "Save Data Field" msgid "Save Data Field"
msgstr "Datenfeld speichern" msgstr "Datenfeld speichern"
#~ #: lib/mv_web/live/member_live/index.html.heex
#~ #, elixir-autogen, elixir-format
#~ msgid "Show current cycle"
#~ msgstr "Aktuellen Zyklus anzeigen"
#~ #: lib/mv_web/live/member_live/index.html.heex
#~ #, elixir-autogen, elixir-format
#~ msgid "Unpaid in last cycle"
#~ msgstr "Unbezahlt im letzten Zyklus"
#~ #: lib/mv_web/live/custom_field_live/index_component.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "New Custom field"
#~ msgstr "Benutzerdefiniertes Feld speichern"
#~ #: lib/mv_web/live/member_live/index.html.heex
#~ #, elixir-autogen, elixir-format
#~ msgid "Show Last/Current Cycle Payment Status"
#~ msgstr ""
#: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Back to roles list" msgid "Back to roles list"
msgstr "Zurück zur Rollen-Liste" msgstr "Zurück zur Rollen-Liste"
#: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Cannot delete system role"
msgstr ""
#: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy
msgid "Custom"
msgstr "Benutzerdefinierte Felder"
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Edit Role"
msgstr "Bearbeiten"
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Failed to delete role: %{error}"
msgstr "Konnte Feld nicht löschen: %{error}"
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy
msgid "Listing Roles"
msgstr "Benutzer*innen auflisten"
#: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Manage user roles and their permission sets."
msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Cannot delete role. %{count} user(s) are still assigned to this role. Please assign them to another role first."
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Close sidebar"
msgstr ""
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Delete Role"
msgstr "Zyklus löschen"
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Main navigation"
msgstr ""
#: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "New Role"
msgstr ""
#: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "No description"
msgstr "Beschreibung"
#: lib/mv_web/components/layouts.ex
#, elixir-autogen, elixir-format
msgid "Open navigation menu"
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Permission Set"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Profile"
msgstr "Profil"
#: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Role"
msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Role deleted successfully."
msgstr "Zyklen erfolgreich regeneriert"
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Role details and permissions."
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Role not found."
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format
msgid "Role saved successfully."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format
msgid "Roles"
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Save Role"
msgstr "Speichern"
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format
msgid "Select permission set"
msgstr ""
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Show Role"
msgstr "Anzeigen"
#: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "System"
msgstr ""
#: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "System Role"
msgstr ""
#: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "System roles cannot be deleted"
msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "System roles cannot be deleted."
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Toggle sidebar"
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Use this form to manage roles in your database."
msgstr "Verwenden Sie dieses Formular, um Benutzer*innen-Datensätze zu verwalten."
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "User menu"
msgstr "Benutzer*in"
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format
msgid "admin - Unrestricted access"
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format
msgid "normal_user - Create/Read/Update access"
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format
msgid "own_data - Access only to own data"
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format
msgid "read_only - Read access to all data"
msgstr ""
#~ #: lib/mv_web/live/custom_field_live/show.ex #~ #: lib/mv_web/live/custom_field_live/show.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Auto-generated identifier (immutable)" #~ msgid "Auto-generated identifier (immutable)"
@ -1929,17 +2105,6 @@ msgstr "Zurück zur Rollen-Liste"
#~ msgid "Default Contribution Type" #~ msgid "Default Contribution Type"
#~ msgstr "Standard-Beitragsart" #~ msgstr "Standard-Beitragsart"
#: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "No description"
msgstr "Beschreibung"
#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Failed to delete some cycles: %{errors}"
#~ msgstr "Konnte Feld nicht löschen: %{error}"
#~ #: lib/mv_web/live/custom_field_live/form_component.ex #~ #: lib/mv_web/live/custom_field_live/form_component.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Immutable" #~ msgid "Immutable"

View file

@ -303,7 +303,7 @@ msgstr ""
msgid "Enabled" msgid "Enabled"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Logout" msgid "Logout"
msgstr "" msgstr ""
@ -320,6 +320,7 @@ msgid "Member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/contribution_type_live/index.ex
#: lib/mv_web/live/member_live/index.ex #: lib/mv_web/live/member_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
@ -363,11 +364,6 @@ msgstr ""
msgid "Password Authentication" msgid "Password Authentication"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr ""
#: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/form_component.ex
#: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex
#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/form_component.ex
@ -387,6 +383,7 @@ msgid "Select member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
@ -556,11 +553,13 @@ msgid "Back to users list"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Select language" msgid "Select language"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Toggle dark mode" msgid "Toggle dark mode"
msgstr "" msgstr ""
@ -572,6 +571,7 @@ msgid "Search..."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Users" msgid "Users"
@ -637,6 +637,7 @@ msgstr ""
msgid "Please select a custom field first" msgid "Please select a custom field first"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/form.ex
#: lib/mv_web/live/member_live/show.ex #: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -920,6 +921,7 @@ msgstr ""
msgid "Contribution Start" msgid "Contribution Start"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/contribution_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Contribution Types" msgid "Contribution Types"
@ -931,6 +933,7 @@ msgid "Contribution type"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Contributions" msgid "Contributions"
msgstr "" msgstr ""
@ -1916,6 +1919,27 @@ msgstr ""
msgid "Manage user roles and their permission sets." msgid "Manage user roles and their permission sets."
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Cannot delete role. %{count} user(s) are still assigned to this role. Please assign them to another role first."
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Close sidebar"
msgstr ""
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Delete Role"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Main navigation"
msgstr ""
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "New Role" msgid "New Role"
@ -1927,6 +1951,11 @@ msgstr ""
msgid "No description" msgid "No description"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts.ex
#, elixir-autogen, elixir-format
msgid "Open navigation menu"
msgstr ""
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/role_live/show.ex
@ -1934,17 +1963,45 @@ msgstr ""
msgid "Permission Set" msgid "Permission Set"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Profile"
msgstr ""
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Role" msgid "Role"
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Role deleted successfully."
msgstr ""
#: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Role details and permissions." msgid "Role details and permissions."
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Role not found."
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format
msgid "Role saved successfully."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format
msgid "Roles"
msgstr ""
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save Role" msgid "Save Role"
@ -1976,11 +2033,27 @@ msgstr ""
msgid "System roles cannot be deleted" msgid "System roles cannot be deleted"
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "System roles cannot be deleted."
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Toggle sidebar"
msgstr ""
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Use this form to manage roles in your database." msgid "Use this form to manage roles in your database."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "User menu"
msgstr ""
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "admin - Unrestricted access" msgid "admin - Unrestricted access"
@ -2000,43 +2073,3 @@ msgstr ""
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "read_only - Read access to all data" msgid "read_only - Read access to all data"
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Delete Role"
msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Role deleted successfully."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format
msgid "Roles"
msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Cannot delete role. %{count} user(s) are still assigned to this role. Please assign them to another role first."
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Role not found."
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format
msgid "Role saved successfully."
msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "System roles cannot be deleted."
msgstr ""

View file

@ -303,7 +303,7 @@ msgstr ""
msgid "Enabled" msgid "Enabled"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Logout" msgid "Logout"
msgstr "" msgstr ""
@ -320,6 +320,7 @@ msgid "Member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/contribution_type_live/index.ex
#: lib/mv_web/live/member_live/index.ex #: lib/mv_web/live/member_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
@ -363,11 +364,6 @@ msgstr ""
msgid "Password Authentication" msgid "Password Authentication"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr ""
#: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/form_component.ex
#: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex
#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/form_component.ex
@ -387,6 +383,7 @@ msgid "Select member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
@ -556,11 +553,13 @@ msgid "Back to users list"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Select language" msgid "Select language"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Toggle dark mode" msgid "Toggle dark mode"
msgstr "" msgstr ""
@ -572,6 +571,7 @@ msgid "Search..."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Users" msgid "Users"
@ -637,6 +637,7 @@ msgstr ""
msgid "Please select a custom field first" msgid "Please select a custom field first"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/form.ex
#: lib/mv_web/live/member_live/show.ex #: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
@ -920,6 +921,7 @@ msgstr ""
msgid "Contribution Start" msgid "Contribution Start"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/contribution_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Contribution Types" msgid "Contribution Types"
@ -931,6 +933,7 @@ msgid "Contribution type"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Contributions" msgid "Contributions"
msgstr "" msgstr ""
@ -1916,6 +1919,27 @@ msgstr ""
msgid "Manage user roles and their permission sets." msgid "Manage user roles and their permission sets."
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Cannot delete role. %{count} user(s) are still assigned to this role. Please assign them to another role first."
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Close sidebar"
msgstr ""
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Delete Role"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Main navigation"
msgstr ""
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "New Role" msgid "New Role"
@ -1927,6 +1951,11 @@ msgstr ""
msgid "No description" msgid "No description"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts.ex
#, elixir-autogen, elixir-format
msgid "Open navigation menu"
msgstr ""
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/role_live/show.ex
@ -1934,17 +1963,45 @@ msgstr ""
msgid "Permission Set" msgid "Permission Set"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Profile"
msgstr ""
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Role" msgid "Role"
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Role deleted successfully."
msgstr ""
#: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Role details and permissions." msgid "Role details and permissions."
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Role not found."
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Role saved successfully."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Roles"
msgstr ""
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Save Role" msgid "Save Role"
@ -1976,11 +2033,27 @@ msgstr ""
msgid "System roles cannot be deleted" msgid "System roles cannot be deleted"
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "System roles cannot be deleted."
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Toggle sidebar"
msgstr ""
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Use this form to manage roles in your database." msgid "Use this form to manage roles in your database."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "User menu"
msgstr ""
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "admin - Unrestricted access" msgid "admin - Unrestricted access"
@ -2001,71 +2074,16 @@ msgstr ""
msgid "read_only - Read access to all data" msgid "read_only - Read access to all data"
msgstr "" msgstr ""
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Delete Role"
msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Role deleted successfully."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Roles"
msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Cannot delete role. %{count} user(s) are still assigned to this role. Please assign them to another role first."
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Role not found."
msgstr ""
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Role saved successfully."
msgstr ""
#: lib/mv_web/live/role_live/index.ex
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "System roles cannot be deleted."
msgstr ""
#~ #: lib/mv_web/live/components/payment_filter_component.ex #~ #: lib/mv_web/live/components/payment_filter_component.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "All payment statuses" #~ msgid "All payment statuses"
#~ msgstr "" #~ msgstr ""
#~ #: lib/mv_web/live/custom_field_live/show.ex
#~ #, elixir-autogen, elixir-format
#~ msgstr ""
#~ #: lib/mv_web/live/member_field_live/form_component.ex
#~ #: lib/mv_web/live/member_field_live/index_component.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "String"
#~ msgstr ""
#~ #: lib/mv_web/live/contribution_settings_live.ex #~ #: lib/mv_web/live/contribution_settings_live.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Configure global settings for membership contributions." #~ msgid "Configure global settings for membership contributions."
#~ msgstr "" #~ msgstr ""
#~ #: lib/mv_web/live/global_settings_live.ex
#~ #, elixir-autogen, elixir-format
#~ msgid "Failed to update member field visibility: %{error}"
#~ msgstr ""
#~ #: lib/mv_web/live/member_live/form.ex #~ #: lib/mv_web/live/member_live/form.ex
#~ #: lib/mv_web/live/member_live/show.ex #~ #: lib/mv_web/live/member_live/show.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
@ -2087,6 +2105,11 @@ msgstr ""
#~ msgid "Failed to save settings. Please check the errors below." #~ msgid "Failed to save settings. Please check the errors below."
#~ msgstr "" #~ msgstr ""
#~ #: lib/mv_web/live/global_settings_live.ex
#~ #, elixir-autogen, elixir-format
#~ msgid "Failed to update member field visibility: %{error}"
#~ msgstr ""
#~ #: lib/mv_web/live/user_live/index.html.heex #~ #: lib/mv_web/live/user_live/index.html.heex
#~ #: lib/mv_web/live/user_live/show.ex #~ #: lib/mv_web/live/user_live/show.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
@ -2112,11 +2135,6 @@ msgstr ""
#~ msgid "Pending" #~ msgid "Pending"
#~ msgstr "" #~ msgstr ""
#~ #: lib/mv_web/live/custom_field_live/form_component.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Save Custom Field"
#~ msgstr ""
#~ #: lib/mv_web/live/member_live/form.ex #~ #: lib/mv_web/live/member_live/form.ex
#~ #: lib/mv_web/live/member_live/show.ex #~ #: lib/mv_web/live/member_live/show.ex
#~ #: lib/mv_web/translations/member_fields.ex #~ #: lib/mv_web/translations/member_fields.ex
@ -2134,6 +2152,11 @@ msgstr ""
#~ msgid "Quarterly Interval - Joining Period Excluded" #~ msgid "Quarterly Interval - Joining Period Excluded"
#~ msgstr "" #~ msgstr ""
#~ #: lib/mv_web/live/custom_field_live/form_component.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Save Custom Field"
#~ msgstr ""
#~ #: lib/mv_web/live/member_live/index.html.heex #~ #: lib/mv_web/live/member_live/index.html.heex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Show Last/Current Cycle Payment Status" #~ msgid "Show Last/Current Cycle Payment Status"
@ -2149,6 +2172,12 @@ msgstr ""
#~ msgid "Show last completed cycle" #~ msgid "Show last completed cycle"
#~ msgstr "" #~ msgstr ""
#~ #: lib/mv_web/live/member_field_live/form_component.ex
#~ #: lib/mv_web/live/member_field_live/index_component.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "String"
#~ msgstr ""
#~ #: lib/mv_web/live/member_live/index.html.heex #~ #: lib/mv_web/live/member_live/index.html.heex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Switch to current cycle" #~ msgid "Switch to current cycle"

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<rect width="100" height="100" rx="12" fill="#4f46e5"/>
<text x="50" y="65" font-family="Arial, sans-serif" font-size="48" font-weight="bold" fill="white" text-anchor="middle">M</text>
</svg>

After

Width:  |  Height:  |  Size: 285 B

View file

@ -1,106 +0,0 @@
defmodule MvWeb.Layouts.NavbarTest do
use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
describe "navbar profile section" do
test "renders profile button with correct attributes", %{conn: _conn} do
# Setup: Create a user
user = create_test_user(%{email: "test@example.com"})
html =
render_component(&MvWeb.Layouts.Navbar.navbar/1, %{
current_user: user
})
# Test dropdown structure
assert html =~ "dropdown-content"
assert html =~ "dropdown-end"
assert html =~ ~s(role="button")
# Test profile link
assert html =~ ~s(href="/users/#{user.id}")
assert html =~ "Profil"
end
@tag :skip
# TODO: Implement user initials in navbar avatar - see issue #170
test "shows user initials in avatar", %{conn: _conn} do
# Setup: Create a user with specific email for testing initials
user = create_test_user(%{email: "test.user@example.com"})
html =
render_component(&MvWeb.Layouts.Navbar.navbar/1, %{
current_user: user
})
# Initials from test.user@example.com
assert html =~ "<span>TU</span>"
end
@tag :skip
# TODO: Implement user initials in navbar avatar - see issue #170
test "shows different initials for OIDC user", %{conn: _conn} do
# Setup: Create OIDC user
user_info = %{
"sub" => "oidc_123",
"preferred_username" => "oidc.user@example.com"
}
oauth_tokens = %{
"access_token" => "test_token",
"id_token" => "test_id_token"
}
user =
Mv.Accounts.User
|> Ash.Changeset.for_create(:register_with_rauthy, %{
user_info: user_info,
oauth_tokens: oauth_tokens
})
|> Ash.create!(domain: Mv.Accounts)
html =
render_component(&MvWeb.Layouts.Navbar.navbar/1, %{
current_user: user
})
# Initials from oidc.user@example.com
assert html =~ "<span>OU</span>"
end
test "includes all required navigation items", %{conn: _conn} do
user = create_test_user(%{email: "test@example.com"})
html =
render_component(&MvWeb.Layouts.Navbar.navbar/1, %{
current_user: user
})
# Check for all required menu items
assert html =~ "Profil"
assert html =~ "Settings"
assert html =~ "Logout"
# Check for correct logout path
assert html =~ ~s(href="/sign-out")
end
test "Settings link navigates to global settings page", %{conn: conn} do
user = create_test_user(%{email: "test@example.com"})
conn = conn_with_oidc_user(conn, user)
html =
render_component(&MvWeb.Layouts.Navbar.navbar/1, %{
current_user: user
})
# Check that Settings link exists and points to /settings
assert html =~ "Settings"
assert html =~ ~s(href="/settings") || html =~ ~s(navigate="/settings")
# Verify the link actually works by navigating to it
{:ok, _view, settings_html} = live(conn, ~p"/settings")
assert settings_html =~ "Club Settings"
end
end
end

View file

@ -0,0 +1,850 @@
defmodule MvWeb.Layouts.SidebarTest do
@moduledoc """
Unit tests for the Sidebar component.
Tests cover:
- Basic rendering and structure
- Props handling (current_user, club_name, mobile)
- Menu structure (flat and nested items)
- Footer/Profile section
- Accessibility attributes (ARIA labels, roles)
- CSS classes (DaisyUI conformance)
- Icon rendering
- Conditional visibility
"""
use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import MvWeb.Layouts.Sidebar
# =============================================================================
# Helper Functions
# =============================================================================
# Returns assigns for an authenticated user with all required attributes.
defp authenticated_assigns(mobile \\ false) do
%{
current_user: %{id: "user-123", email: "test@example.com"},
club_name: "Test Club",
mobile: mobile
}
end
# Returns assigns for a guest user (not authenticated).
defp guest_assigns(mobile \\ false) do
%{
current_user: nil,
club_name: "Test Club",
mobile: mobile
}
end
# Renders the sidebar component with the given assigns.
defp render_sidebar(assigns) do
render_component(&sidebar/1, assigns)
end
# Checks if the HTML contains a specific CSS class.
defp has_class?(html, class) do
html =~ class
end
# =============================================================================
# Group 1: Basic Rendering (T1.1-T1.3)
# =============================================================================
describe "basic rendering" do
test "T1.1: renders sidebar with main navigation structure" do
html = render_sidebar(authenticated_assigns())
# Check for navigation element with correct ID
assert html =~ ~s(id="main-sidebar")
assert html =~ ~s(aria-label="Main navigation")
# Check for sidebar class
assert has_class?(html, "sidebar")
end
test "T1.2: renders logo correctly" do
html = render_sidebar(authenticated_assigns())
# Check for logo image
assert html =~ ~s(src="/images/mila.svg")
assert html =~ ~s(alt="Mila Logo")
# Check logo has correct size class
assert has_class?(html, "size-8")
end
test "T1.3: renders toggle button with correct attributes (desktop only)" do
html = render_sidebar(authenticated_assigns(false))
# Check for toggle button
assert html =~ ~s(id="sidebar-toggle")
assert html =~ "onclick="
# Check for DaisyUI button classes
assert has_class?(html, "btn")
assert has_class?(html, "btn-ghost")
assert has_class?(html, "btn-sm")
assert has_class?(html, "btn-square")
# Check for both toggle icons (expanded and collapsed)
assert has_class?(html, "sidebar-expanded-icon")
assert has_class?(html, "sidebar-collapsed-icon")
end
test "T1.4: does not render toggle button on mobile" do
html = render_sidebar(authenticated_assigns(true))
# Toggle button should not be rendered on mobile
refute html =~ ~s(id="sidebar-toggle")
end
end
# =============================================================================
# Group 2: Props Handling (T2.1-T2.3)
# =============================================================================
describe "props handling" do
test "T2.1: displays club name when provided" do
assigns = %{
current_user: %{id: "user-1", email: "test@example.com"},
club_name: "My Awesome Club",
mobile: false
}
html = render_sidebar(assigns)
assert html =~ "My Awesome Club"
end
test "T2.2: does not render menu items when current_user is nil" do
html = render_sidebar(guest_assigns())
# Navigation links should not be rendered
refute html =~ ~s(href="/members")
refute html =~ ~s(href="/users")
refute html =~ ~s(href="/settings")
refute html =~ ~s(href="/contribution_types")
# Footer section should not be rendered
refute html =~ "locale-select"
refute html =~ "theme-controller"
end
test "T2.3: renders menu items when current_user is present" do
html = render_sidebar(authenticated_assigns())
# Check for Members link
assert html =~ ~s(href="/members")
# Check for Users link
assert html =~ ~s(href="/users")
# Check for Custom Fields link
assert html =~ ~s(href="/custom_field_values")
# Check for Contributions section
assert html =~ ~s(href="/contribution_types")
assert html =~ ~s(href="/membership_fee_settings")
# Check for Settings link (placeholder)
assert html =~ ~s(href="/settings")
end
test "T2.4: renders sidebar with main-sidebar ID" do
html = render_sidebar(authenticated_assigns(true))
assert html =~ ~s(id="main-sidebar")
end
test "T2.5: renders sidebar with main-sidebar ID on desktop" do
html = render_sidebar(authenticated_assigns(false))
assert html =~ ~s(id="main-sidebar")
end
end
# =============================================================================
# Group 3: Menu Structure (T3.1-T3.3)
# =============================================================================
describe "menu structure" do
test "T3.1: renders flat menu items with icons and labels" do
html = render_sidebar(authenticated_assigns())
# Check for Members link with icon
assert html =~ ~s(href="/members")
assert html =~ "hero-users"
# Check for Users link with icon
assert html =~ ~s(href="/users")
assert html =~ "hero-user-circle"
# Check for Custom Fields link with icon
assert html =~ ~s(href="/custom_field_values")
assert html =~ "hero-rectangle-group"
# Check for Settings link with icon
assert html =~ ~s(href="/settings")
assert html =~ "hero-cog-6-tooth"
# Check for tooltips (data-tip attribute)
assert html =~ "data-tip="
end
test "T3.2: renders nested menu with details element for expanded state" do
html = render_sidebar(authenticated_assigns())
# Check for Contributions section structure with details
assert html =~ "<details"
assert has_class?(html, "expanded-menu-group")
# Check for contribution links
assert html =~ ~s(href="/contribution_types")
assert html =~ ~s(href="/membership_fee_settings")
end
test "T3.3: renders nested menu with dropdown for collapsed state" do
html = render_sidebar(authenticated_assigns())
# Check for collapsed dropdown container
assert has_class?(html, "collapsed-menu-group")
assert has_class?(html, "dropdown")
assert has_class?(html, "dropdown-right")
# Check for dropdown-content
assert has_class?(html, "dropdown-content")
# Check for icon button
assert html =~ "hero-currency-dollar"
assert html =~ ~s(aria-haspopup="menu")
end
end
# =============================================================================
# Group 4: Footer/Profile Section (T4.1-T4.3)
# =============================================================================
describe "footer/profile section" do
test "T4.1: renders footer section when user is authenticated" do
html = render_sidebar(authenticated_assigns())
# Check for footer container with mt-auto
assert has_class?(html, "mt-auto")
# Check for language selector form
assert html =~ ~s(action="/set_locale")
# Check for theme toggle
assert has_class?(html, "theme-controller")
# Check for user menu/avatar
assert has_class?(html, "avatar")
end
test "T4.2: renders language selector with form and options" do
html = render_sidebar(authenticated_assigns())
# Check for form with correct action
assert html =~ ~s(action="/set_locale")
assert html =~ ~s(method="post")
# Check for CSRF token
assert html =~ "_csrf_token"
# Check for select element
assert html =~ ~s(name="locale")
# Check for language options
assert html =~ ~s(value="de")
assert html =~ "Deutsch"
assert html =~ ~s(value="en")
assert html =~ "English"
# Check expanded-only class
assert has_class?(html, "expanded-only")
end
test "T4.3: renders user dropdown with profile and logout links" do
assigns = %{
current_user: %{id: "user-456", email: "test@example.com"},
club_name: "Test Club",
mobile: false
}
html = render_sidebar(assigns)
# Check for dropdown container
assert has_class?(html, "dropdown")
assert has_class?(html, "dropdown-top")
# Check for avatar button
assert html =~ ~s(aria-haspopup="menu")
# Check for profile link (with user ID)
assert html =~ ~s(href="/users/user-456")
# Check for logout link
assert html =~ ~s(href="/sign-out")
# Check for DaisyUI dropdown classes
assert has_class?(html, "dropdown-content")
end
test "T4.4: renders user avatar with placeholder" do
assigns = %{
current_user: %{id: "user-789", email: "alice@example.com"},
club_name: "Test Club",
mobile: false
}
html = render_sidebar(assigns)
# Should have avatar placeholder classes
assert has_class?(html, "avatar")
assert has_class?(html, "placeholder")
assert has_class?(html, "bg-neutral")
assert has_class?(html, "text-neutral-content")
end
end
# =============================================================================
# Group 5: Accessibility Attributes (T5.1-T5.5)
# =============================================================================
describe "accessibility attributes" do
test "T5.1: navigation has correct ARIA label" do
html = render_sidebar(authenticated_assigns())
assert html =~ ~s(aria-label="Main navigation")
end
test "T5.2: toggle button has correct ARIA attributes" do
html = render_sidebar(authenticated_assigns(false))
# Toggle button
assert html =~ ~s(aria-label="Toggle sidebar")
assert html =~ ~s(aria-controls="main-sidebar")
assert html =~ ~s(aria-expanded="true")
end
test "T5.3: menu has correct roles" do
html = render_sidebar(authenticated_assigns())
# Main menu should have menubar role
assert html =~ ~s(role="menubar")
# List items should have role="none"
assert html =~ ~s(role="none")
# Links should have role="menuitem"
assert html =~ ~s(role="menuitem")
end
test "T5.4: nested menu has correct ARIA attributes" do
html = render_sidebar(authenticated_assigns())
# Details summary should have haspopup
assert html =~ ~s(aria-haspopup="true")
# Dropdown button should have haspopup
assert html =~ ~s(aria-haspopup="menu")
# Nested menus should have role="menu"
assert html =~ ~s(role="menu")
end
test "T5.5: icons are hidden from screen readers" do
html = render_sidebar(authenticated_assigns())
# Icons should have aria-hidden="true"
assert html =~ ~s(aria-hidden="true")
end
end
# =============================================================================
# Group 6: CSS Classes - DaisyUI Conformance (T6.1-T6.4)
# =============================================================================
describe "CSS classes - DaisyUI conformance" do
test "T6.1: uses correct DaisyUI menu classes" do
html = render_sidebar(authenticated_assigns())
# menu class on ul
assert has_class?(html, "menu")
end
test "T6.2: uses correct DaisyUI button classes" do
html = render_sidebar(authenticated_assigns(false))
# Button classes
assert has_class?(html, "btn")
assert has_class?(html, "btn-ghost")
assert has_class?(html, "btn-sm")
assert has_class?(html, "btn-square")
end
test "T6.3: uses correct tooltip classes" do
html = render_sidebar(authenticated_assigns())
# Tooltip classes for menu items
assert has_class?(html, "tooltip")
assert has_class?(html, "tooltip-right")
end
test "T6.4: uses correct dropdown classes" do
html = render_sidebar(authenticated_assigns())
# Dropdown classes
assert has_class?(html, "dropdown")
assert has_class?(html, "dropdown-right")
assert has_class?(html, "dropdown-top")
assert has_class?(html, "dropdown-content")
assert has_class?(html, "rounded-box")
end
end
# =============================================================================
# Group 7: Icon Rendering (T7.1-T7.2)
# =============================================================================
describe "icon rendering" do
test "T7.1: renders hero icons for menu items" do
html = render_sidebar(authenticated_assigns())
# Check for hero icons
assert html =~ "hero-users"
assert html =~ "hero-user-circle"
assert html =~ "hero-rectangle-group"
assert html =~ "hero-currency-dollar"
assert html =~ "hero-cog-6-tooth"
assert html =~ "hero-chevron-left"
assert html =~ "hero-chevron-right"
# Icons should have aria-hidden
assert html =~ ~s(aria-hidden="true")
end
test "T7.2: renders icons for theme toggle" do
html = render_sidebar(authenticated_assigns())
# Theme toggle icons (sun and moon)
assert html =~ "hero-sun"
assert html =~ "hero-moon"
end
end
# =============================================================================
# Group 8: State-dependent classes (T8.1-T8.3)
# =============================================================================
describe "state-dependent classes" do
test "T8.1: expanded-only elements have correct CSS class" do
html = render_sidebar(authenticated_assigns())
# Language form should be expanded-only
assert has_class?(html, "expanded-only")
end
test "T8.2: menu-label class is used for text that hides when collapsed" do
html = render_sidebar(authenticated_assigns())
# Menu labels that hide in collapsed state
assert has_class?(html, "menu-label")
end
test "T8.3: toggle button has state icons" do
html = render_sidebar(authenticated_assigns(false))
# Expanded icon
assert has_class?(html, "sidebar-expanded-icon")
# Collapsed icon
assert has_class?(html, "sidebar-collapsed-icon")
end
end
# =============================================================================
# Additional Edge Cases and Validation
# =============================================================================
describe "edge cases" do
test "handles user with minimal attributes" do
assigns = %{
current_user: %{id: "minimal-user", email: "min@test.com"},
club_name: "Minimal Club",
mobile: false
}
html = render_sidebar(assigns)
# Should render without error
assert html =~ "Minimal Club"
assert html =~ ~s(href="/users/minimal-user")
end
test "handles empty club name" do
assigns = %{
current_user: %{id: "user-1", email: "test@test.com"},
club_name: "",
mobile: false
}
html = render_sidebar(assigns)
# Should render without error
assert html =~ ~s(id="main-sidebar")
end
test "sidebar structure is complete with all sections" do
html = render_sidebar(authenticated_assigns())
# Header section
assert html =~ "Mila Logo"
# Navigation section
assert html =~ ~s(role="menubar")
# Footer section
assert html =~ "theme-controller"
# All expected links
expected_links = [
"/members",
"/users",
"/custom_field_values",
"/contribution_types",
"/membership_fee_settings",
"/sign-out"
]
for link <- expected_links do
assert html =~ ~s(href="#{link}"), "Missing link: #{link}"
end
end
end
# =============================================================================
# Component Tests - sidebar_header/1
# =============================================================================
describe "sidebar_header/1" do
test "renders logo with correct size" do
html = render_sidebar(authenticated_assigns())
# Logo is size-8 (32px)
assert html =~ ~s(src="/images/mila.svg")
assert html =~ ~s(alt="Mila Logo")
assert has_class?(html, "size-8")
# Logo is always visible (no conditional classes)
assert html =~ ~s(<img)
end
test "renders club name" do
assigns = %{
current_user: %{id: "user-1", email: "test@example.com"},
club_name: "My Test Club",
mobile: false
}
html = render_sidebar(assigns)
# Club name is present
assert html =~ "My Test Club"
assert has_class?(html, "menu-label")
end
test "renders toggle button for desktop" do
html = render_sidebar(authenticated_assigns(false))
# Toggle button has both icons
assert has_class?(html, "sidebar-expanded-icon")
assert has_class?(html, "sidebar-collapsed-icon")
# Toggle button is not mobile (hidden on mobile)
assert html =~ ~s(id="sidebar-toggle")
assert html =~ ~s(aria-label="Toggle sidebar")
assert html =~ ~s(aria-expanded="true")
end
test "does not render toggle button on mobile" do
html = render_sidebar(authenticated_assigns(true))
# Toggle button should not be rendered on mobile
refute html =~ ~s(id="sidebar-toggle")
end
end
# =============================================================================
# Component Tests - menu_item/1
# =============================================================================
describe "menu_item/1" do
test "renders icon and label" do
html = render_sidebar(authenticated_assigns())
# Icon is visible
assert html =~ "hero-users"
assert html =~ ~s(aria-hidden="true")
# Label is present
assert html =~ "Members"
assert has_class?(html, "menu-label")
end
test "has tooltip with correct text" do
html = render_sidebar(authenticated_assigns())
# data-tip attribute set
assert html =~ ~s(data-tip="Members")
assert has_class?(html, "tooltip")
assert has_class?(html, "tooltip-right")
end
test "has correct link" do
html = render_sidebar(authenticated_assigns())
# navigate attribute correct (rendered as href)
assert html =~ ~s(href="/members")
assert html =~ ~s(role="menuitem")
end
end
# =============================================================================
# Component Tests - menu_group/1
# =============================================================================
describe "menu_group/1" do
test "renders expanded menu group" do
html = render_sidebar(authenticated_assigns())
# details/summary present
assert html =~ "<details"
assert html =~ "<summary"
assert has_class?(html, "expanded-menu-group")
end
test "renders collapsed menu group" do
html = render_sidebar(authenticated_assigns())
# dropdown present
assert has_class?(html, "collapsed-menu-group")
assert has_class?(html, "dropdown")
assert has_class?(html, "dropdown-right")
end
test "renders submenu items" do
html = render_sidebar(authenticated_assigns())
# Inner_block items rendered
assert html =~ ~s(href="/contribution_types")
assert html =~ ~s(href="/membership_fee_settings")
assert html =~ ~s(role="menu")
end
end
# =============================================================================
# Component Tests - sidebar_footer/1
# =============================================================================
describe "sidebar_footer/1" do
test "renders at bottom of sidebar" do
html = render_sidebar(authenticated_assigns())
# mt-auto present
assert has_class?(html, "mt-auto")
end
test "renders theme toggle" do
html = render_sidebar(authenticated_assigns())
# Toggle is always visible
assert has_class?(html, "theme-controller")
assert html =~ "hero-sun"
assert html =~ "hero-moon"
end
test "renders language selector in expanded only" do
html = render_sidebar(authenticated_assigns())
# expanded-only class
assert has_class?(html, "expanded-only")
assert html =~ ~s(action="/set_locale")
end
test "renders user menu with avatar" do
assigns = %{
current_user: %{id: "user-123", email: "alice@example.com"},
club_name: "Test Club",
mobile: false
}
html = render_sidebar(assigns)
# Avatar present
assert has_class?(html, "avatar")
assert has_class?(html, "placeholder")
# First letter correct (A for alice@example.com)
assert html =~ "A"
end
end
# =============================================================================
# Integration Tests - Sidebar State Management
# =============================================================================
describe "sidebar state management" do
test "sidebar starts expanded by default" do
# The data-sidebar-expanded attribute is set in layouts.ex, not in sidebar.ex
# This test verifies the sidebar structure supports the expanded state
html = render_sidebar(authenticated_assigns())
# Sidebar has classes that support expanded state
assert has_class?(html, "menu-label")
assert has_class?(html, "expanded-only")
end
test "toggle button has correct onclick handler" do
html = render_sidebar(authenticated_assigns(false))
# Toggle button has onclick handler
assert html =~ ~r/onclick="toggleSidebar\(\)"/
end
test "no duplicate IDs in layout" do
html = render_sidebar(authenticated_assigns())
# Check that main-sidebar ID appears only once
id_count = html |> String.split(~r/id="main-sidebar"/) |> length() |> Kernel.-(1)
assert id_count == 1, "main-sidebar ID should appear exactly once"
# Check that sidebar-toggle ID appears only once (if present)
if html =~ ~r/id="sidebar-toggle"/ do
toggle_count = html |> String.split(~r/id="sidebar-toggle"/) |> length() |> Kernel.-(1)
assert toggle_count == 1, "sidebar-toggle ID should appear exactly once"
end
end
end
# =============================================================================
# Integration Tests - Mobile Drawer
# =============================================================================
describe "mobile drawer" do
test "mobile header renders on small screens" do
# Mobile header is in layouts.ex, not sidebar.ex
# This test verifies sidebar works correctly with mobile flag
html = render_sidebar(authenticated_assigns(true))
# Sidebar should render without toggle button on mobile
refute html =~ ~s(id="sidebar-toggle")
end
test "drawer overlay is present" do
html = render_sidebar(authenticated_assigns())
# Drawer overlay label for mobile
assert html =~ ~s(for="mobile-drawer")
assert has_class?(html, "drawer-overlay")
assert html =~ ~s(lg:hidden)
end
end
# =============================================================================
# Accessibility Tests - Extended
# =============================================================================
describe "accessibility - extended" do
test "sidebar has aria-label" do
html = render_sidebar(authenticated_assigns())
assert html =~ ~s(aria-label="Main navigation")
end
test "toggle button has aria-label and aria-expanded" do
html = render_sidebar(authenticated_assigns(false))
# aria-label present
assert html =~ ~s(aria-label="Toggle sidebar")
# aria-expanded="true" or "false"
assert html =~ ~s(aria-expanded="true") || html =~ ~s(aria-expanded="false")
end
test "menu items have role attributes" do
html = render_sidebar(authenticated_assigns())
# role="menubar", "menuitem", "menu"
assert html =~ ~s(role="menubar")
assert html =~ ~s(role="menuitem")
assert html =~ ~s(role="menu")
end
test "icons have aria-hidden" do
html = render_sidebar(authenticated_assigns())
# Decorative icons: aria-hidden="true"
# Count occurrences to ensure multiple icons have it
assert html =~ ~s(aria-hidden="true")
end
test "user menu has aria-haspopup" do
html = render_sidebar(authenticated_assigns())
# aria-haspopup="menu"
assert html =~ ~s(aria-haspopup="menu")
end
end
# =============================================================================
# Regression Tests
# =============================================================================
describe "regression tests" do
test "no duplicate profile links" do
html = render_sidebar(authenticated_assigns())
# Only one Profile link in DOM
profile_count = html |> String.split(~s[href="/users/"]) |> length() |> Kernel.-(1)
assert profile_count <= 1, "Should have at most one profile link"
end
test "nested menu has only one hover effect" do
html = render_sidebar(authenticated_assigns())
# Check that menu-group has proper structure
# Both expanded and collapsed versions should be present
assert has_class?(html, "expanded-menu-group")
assert has_class?(html, "collapsed-menu-group")
# Details element should not have duplicate hover classes
# (CSS handles this, but we verify structure)
assert html =~ "<details"
end
test "tooltips only visible when collapsed" do
html = render_sidebar(authenticated_assigns())
# Tooltip classes are present (CSS handles visibility)
assert has_class?(html, "tooltip")
assert has_class?(html, "tooltip-right")
assert html =~ "data-tip="
end
test "user menu dropdown has correct structure" do
html = render_sidebar(authenticated_assigns())
# Dropdown should have proper classes
assert has_class?(html, "dropdown")
assert has_class?(html, "dropdown-top")
assert has_class?(html, "dropdown-content")
# Should have both Profile and Logout links
assert html =~ "sign-out"
end
end
end

View file

@ -33,7 +33,7 @@ defmodule MvWeb.ProfileNavigationTest do
end end
end end
describe "navbar" do describe "sidebar" do
test "renders profile button with correct attributes", %{conn: conn} do test "renders profile button with correct attributes", %{conn: conn} do
# Setup: Create and login a user # Setup: Create and login a user
user = create_test_user(%{email: "test@example.com"}) user = create_test_user(%{email: "test@example.com"})
@ -167,8 +167,8 @@ defmodule MvWeb.ProfileNavigationTest do
test "layout shows user data on user profile page", %{conn: conn, user: user} do test "layout shows user data on user profile page", %{conn: conn, user: user} do
{:ok, _view, html} = live(conn, ~p"/users/#{user.id}") {:ok, _view, html} = live(conn, ~p"/users/#{user.id}")
# The navbar (which requires current_user) should be visible # The sidebar (which requires current_user) should be visible
assert html =~ "navbar" assert html =~ "sidebar"
# Profile button should be visible # Profile button should be visible
assert html =~ "Profil" assert html =~ "Profil"
# User ID should be in profile link # User ID should be in profile link