Shared Components
Layout- und Feature-Components, die uebergreifend in der gesamten Applikation eingebunden werden.
WithWorkspaceSelection (Trait)
Shared Trait fuer alle "Hinzufuegen"-Buttons in der App. Oeffnet IMMER einen Workspace-Auswahl-Dialog — auch bei nur einem Workspace. Inklusive Inline-Workspace-Erstellung und Session-basiertem "letzter Workspace"-Memory.
Location: app/Concerns/WithWorkspaceSelection.php
Modal: resources/views/components/workspace-selection-modal.blade.php
Tests: tests/Feature/Livewire/WorkspaceSelectionTest.php (15 Tests)
Public Properties
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
$showWorkspaceModal | bool | false | Modal sichtbar |
$selectedWorkspaceId | int|null | null | Gewaehlter Workspace (Radio) |
$pendingProfileId | int|null | null | Profil-ID fuer Single-Add |
$pendingBulkIds | array | [] | Profil-IDs fuer Bulk-Add |
$showCreateWorkspace | bool | false | Inline-Erstellungsformular |
$newWorkspaceName | string | '' | Name fuer neuen Workspace |
Computed Properties
| Property | Typ | Beschreibung |
|---|---|---|
selectableWorkspaces | Collection<Workspace> | Workspaces ohne Favorites + Admin, mit withCount('watchers') |
Actions
| Method | Beschreibung |
|---|---|
openWorkspaceSelection(int $profileId) | Modal fuer Single-Add oeffnen |
openBulkWorkspaceSelection(array $ids) | Modal fuer Bulk-Add oeffnen |
confirmWorkspaceSelection() | Auswahl bestaetigen → performAddToWorkspace() aufrufen |
createAndSelectWorkspace() | Workspace inline erstellen und auswaehlen |
resetWorkspaceSelection() | Modal-State zuruecksetzen |
performAddToWorkspace(int $profileId, int $workspaceId) | Default-Implementierung: Watcher + WatcherSource erstellen |
performBulkAddToWorkspace(array $ids, int $workspaceId) | Default: Ruft performAddToWorkspace() pro Profil auf |
Datenfluss
graph TD
A["Button: openWorkspaceSelection(profileId)"] --> B["Modal oeffnen"]
B --> C{"User waehlt Workspace"}
C -->|"Bestehender Workspace"| D["confirmWorkspaceSelection()"]
C -->|"Neuer Workspace"| E["createAndSelectWorkspace()"]
E --> D
D --> F{"Single oder Bulk?"}
F -->|"Single"| G["performAddToWorkspace()"]
F -->|"Bulk"| H["performBulkAddToWorkspace()"]
G --> I["Watcher + WatcherSource erstellen"]
H --> G
I --> J["Toast + Modal schliessen"]
Verwendende Components (12)
| Component | Override | Zusatz-Logik |
|---|---|---|
Explore\Browse | performAddToWorkspace | Matomo Tracking (Alias-Pattern) |
Explore\Index | — | Default-Implementierung |
Explore\ProfileGrid | — | Default-Implementierung |
Explore\TrendingVideos | — | Default-Implementierung |
Discover\Index | performAddToWorkspace, performBulkAddToWorkspace | Matomo + UI-Refresh |
TopsFlops\Index | performAddToWorkspace | Matomo + loadData() Refresh |
VideoTrends\Index | — | Default-Implementierung |
Dashboard\RecommendedProfiles | — | Default-Implementierung |
Dashboard\TrendingSlider | — | Default-Implementierung |
Watchers\RelatedChannels | — | Default-Implementierung |
Watchers\RelatedProfiles | — | Default-Implementierung |
Watchers\CrossPlatformRelated | — | Default-Implementierung |
Override-Pattern
Components mit Custom-Logik nutzen PHP Trait-Aliasing:
use WithWorkspaceSelection {
WithWorkspaceSelection::performAddToWorkspace as traitPerformAddToWorkspace;
}
public function performAddToWorkspace(int $profileId, int $workspaceId): void
{
$this->traitPerformAddToWorkspace($profileId, $workspaceId);
$this->trackEvent('explore', 'add_to_workspace', '', $profileId);
}
Blade-Integration
WICHTIG: @include() statt <x-...> verwenden — anonyme Blade Components haben isolierten Scope.
{{-- Am Ende des Component-Views: --}}
@include('components.workspace-selection-modal')
WorkspaceSwitcher
Workspace-Dropdown im Header fuer den Kontext-Wechsel.
View: resources/views/livewire/workspace-switcher.blade.php
Location: app/Livewire/WorkspaceSwitcher.php
Public Properties
| Property | Typ | Beschreibung |
|---|---|---|
$currentWorkspaceId | int|null | Aktiver Workspace |
$workspaces | Collection | Alle User-Workspaces |
Actions
| Method | Beschreibung |
|---|---|
switchWorkspace(int $id) | Workspace wechseln, Session aktualisieren |
Events
| Richtung | Event | Beschreibung |
|---|---|---|
| dispatch | workspace-switched | Nach Workspace-Wechsel (andere Components reagieren) |
| listen | workspaces-updated | Workspace-Liste neu laden (nach Rename/Create/Delete) |
Der Switcher filtert Favorites-Workspace und Admin-Workspace aus der Anzeige fuer Non-Admin-User. Admin-User sehen den Admin-Workspace als separaten Eintrag.
Sidebar-Sortierung: Favoriten zuerst, dann Admin-View, danach alphabetisch. Auf der Workspace-Verwaltungsseite (/workspaces) ist die Sortierung invertiert: System-Workspaces stehen ganz unten.
Das workspaces-updated Event wird von Workspaces\Index nach Create, Rename und Delete dispatcht — der Switcher laedt die Liste daraufhin sofort neu.
NotificationCenter
Globaler WebSocket Event Handler. In das Haupt-Layout eingebettet, empfaengt und verarbeitet alle Reverb-Broadcasts.
View: resources/views/livewire/notification-center.blade.php
Location: app/Livewire/NotificationCenter.php
Public Properties
| Property | Typ | Beschreibung |
|---|---|---|
$userId | int | Aktueller User (0 fuer Guests) |
$isAdmin | bool | Admin-Status |
$workspaceIds | array | IDs aller User-Workspaces |
$subscribedProfileIds | array | Dynamisch abonnierte Profil-Channels |
Event Handlers (#[On])
| Event (Listen) | Handler | Persistenz |
|---|---|---|
reverb-import-progress | onImportProgress() | Toast + Nachrichtenzentrale bei Finish |
reverb-youtube-sync-completed | onYouTubeSyncCompleted() | Toast + Nachrichtenzentrale (nur manuell) |
reverb-daily-scrape-completed | onDailyScrapeCompleted() | Toast + Nachrichtenzentrale |
reverb-user-registered | onUserRegistered() | Toast + Nachrichtenzentrale (Admin only) |
reverb-related-profiles-calculated | onRelatedProfilesCalculated() | Toast + Nachrichtenzentrale (nur manuell) |
reverb-profile-reactivated | onProfileReactivated() | Toast + Nachrichtenzentrale |
reverb-profile-sanitized | onProfileSanitized() | Toast + Nachrichtenzentrale |
reverb-profile-unsanitized | onProfileUnsanitized() | Toast + Nachrichtenzentrale |
reverb-server-alert-triggered | onServerAlertTriggered() | Nur Toast |
subscribe-profile | subscribeToProfile(int $id) | -- |
Dispatch Events
Jeder Handler dispatcht:
show-toast-- Sofortige Benachrichtigung- Spezifisches Event (z.B.
import-progress-updated,youtube-sync-completed) -- Fuer lauschende Child-Components notification-received-- Bell-Badge aktualisieren
JS-Integration
Channel-Subscriptions werden via JavaScript (subscribeToAllChannels() in Alpine) gehandhabt -- nicht ueber Livewires Echo-Integration. Das stellt sicher, dass die WebSocket-Verbindung zuerst aufgebaut wird, bevor Channels abonniert werden.
Rate-Limiting: safeServerDispatch() begrenzt Server-Calls auf max 40 pro Burst.
NotificationBell
Glocken-Icon mit Unread-Badge im Header.
View: resources/views/livewire/notification-bell.blade.php
Location: app/Livewire/NotificationBell.php
Public Properties
| Property | Typ | Beschreibung |
|---|---|---|
$unreadCount | int | Ungelesene Benachrichtigungen |
$loaded | bool | Ob Dropdown schon geoeffnet wurde (steuert DB-Query) |
$refreshKey | int | Wird bei jedem Open inkrementiert fuer DOM-Replacement |
Events
| Richtung | Event | Beschreibung |
|---|---|---|
| listen | notification-received | Unread-Count neu laden, $loaded zuruecksetzen |
Wichtig: @persist-Verhalten
Die Komponente ist mit @persist('notification-bell-sidebar') eingebunden und ueberlebt wire:navigate. Daher:
- Kein DB-Query bei initialem Render —
$loaded=falseverhindert stalen DOM im @persist-Cache - Alpine Loading-State — Spinner statt Stale-Content beim Oeffnen (
$wire.openDropdown().then()) refreshUnreadCount()setzt$loaded=false— WebSocket-Events invalidieren alten Content
Notifications/Index
Vollseiten-Ansicht aller Benachrichtigungen mit Mark-as-Read und Dismiss.
Route: /notifications
View: resources/views/livewire/notifications/index.blade.php
Location: app/Livewire/Notifications/Index.php
Actions
| Method | Beschreibung |
|---|---|
markAsRead(int $id) | Einzelne Benachrichtigung als gelesen markieren |
markAllAsRead() | Alle als gelesen markieren |
dismiss(int $id) | Benachrichtigung ausblenden (dismissed_at) |
dismissAll() | Alle ausblenden |
loadMore() | Weitere Eintraege laden (Infinite Scroll) |
Feedback/ReportIssue
Unified Feedback-Widget mit Gast-Support, Profil-Kontext, Screenshot-Capture und Contact-Modus. Ersetzt auch das alte ContactForm-Component.
View: resources/views/livewire/feedback/report-issue.blade.php
Location: app/Livewire/Feedback/ReportIssue.php
Modi
| Modus | Beschreibung | Verwendung |
|---|---|---|
feedback (Default) | Bug-Report / Feature-Request Modal | Watcher-Seiten, Explorer-Seiten, Sidebar |
contact | Kontaktformular auf CMS-Seiten | /kontakt-Seite (ersetzt das alte ContactForm) |
Gast-Support
Nicht-angemeldete Benutzer koennen Feedback geben. Zusaetzliche Felder fuer Gaeste:
- Name (Pflicht)
- E-Mail (Pflicht)
- Privacy-Checkbox (Pflicht)
- Honeypot-Feld (Spam-Schutz)
- Cloudflare Turnstile (Bot-Schutz, nur wenn aktiviert)
Gast-Einreichungen werden in contact_submissions mit source='feedback' archiviert.
Profil-Kontext
Auf Explorer-Profilseiten (/explorer/{slug}_{id}) werden Profil-Daten automatisch mitgeschickt:
| Property | Beschreibung |
|---|---|
$profileId | Social Profile ID |
$profileName | Profilname |
$profilePlatform | Plattform (youtube/instagram) |
$profileUrl | Explorer-Profil-URL |
Screenshot-Capture
One-Click Screenshot via html2canvas-pro (Vite lazy-loaded, ~200 kB Chunk). Erfasst den aktuellen Seiteninhalt als PNG und haengt ihn automatisch als Attachment an das Feedback an.
Der Screenshot-Button nutzt Alpine.js-State (captured, failed, capturing) fuer sofortiges visuelles Feedback:
- Capturing: "Wird aufgenommen..." mit deaktiviertem Button
- Captured: Gruener Text "Screenshot angehaengt" ersetzt den Button
- Failed: Roter Text "Screenshot fehlgeschlagen" wird angezeigt
Waehrend der Aufnahme werden dialog[open]-Elemente (Flux UI Modals) temporaer ausgeblendet, damit das Modal nicht im Screenshot erscheint.
Flux UI Custom Element Handling: Flux rendert UI-Komponenten als ui-* HTML-Tags (ui-sidebar, ui-button, ui-chart etc.). Nur ui-chart ist ein registrierter Custom Element (customElements.define), die anderen sind einfache HTML-Tags. html2canvas-pro kann diese im geklonten Dokument nicht korrekt rendern. Der onclone-Callback loest drei Probleme:
- Custom Elements → div-Replacement: Alle Tags mit Hyphen werden mit
<div>-Wrappern ersetzt. Children (Light DOM: Chart-SVGs, Button-Elemente, Sidebar-Inhalte) bleiben erhalten. Die computeddisplay-Values der Originalelemente werden als Inline-Style uebertragen. - SVG-Logo Inlining:
<img src="*.svg">-Tags werden zu Inline-<svg>konvertiert. html2canvas rendert externe SVG-Bilder manchmal als graue Bloecke. - Nur strukturelle Elemente ignoriert:
ignoreElementsfiltert nur DIALOG, TEMPLATE, SCRIPT und NOSCRIPT — kein pauschales Custom-Element-Skipping mehr.
Location: resources/js/screenshot-capture.js
Public Properties
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
$show | bool | false | Modal sichtbar |
$mode | string | 'feedback' | Modus: feedback oder contact |
$watcherId | int|null | null | Kontext: Watcher-ID |
$watcherName | string|null | null | Kontext: Watcher-Name |
$profileId | int|null | null | Kontext: Explorer-Profil-ID |
$profileName | string|null | null | Kontext: Explorer-Profilname |
$profilePlatform | string|null | null | Kontext: Plattform |
$profileUrl | string|null | null | Kontext: Explorer-Profil-URL |
$category | string | 'bug' | Feedback-Kategorie (aus config('feedback.categories')) |
$subject | string | '' | Betreff (max. 100 Zeichen) |
$message | string | '' | Nachricht (max. 5000 Zeichen) |
$attachments | array | [] | Datei-Uploads |
$guestName | string | '' | Gast: Name |
$guestEmail | string | '' | Gast: E-Mail |
$privacyAccepted | bool | false | Gast: Privacy-Checkbox |
Kategorien
Konfigurierbar via config('feedback.categories'):
| Key | Label |
|---|---|
bug | Fehler melden |
feature | Feature-Request |
question | Frage |
data-issue | Datenproblem |
block-request | Sperranfrage |
profile-suggestion | Profil vorschlagen |
other | Sonstiges |
Actions
| Method | Beschreibung |
|---|---|
open() | Modal oeffnen (bewahrt via mount() gesetzten Kontext) |
openWithWatcher(Watcher $watcher) | Modal mit Watcher-Kontext oeffnen |
openWithProfile(array $data) | Modal mit Explorer-Profil-Kontext oeffnen |
close() | Modal schliessen, Formular zuruecksetzen |
submit() | Feedback validieren und per E-Mail senden |
removeAttachment(int $index) | Datei-Anhang entfernen |
captureScreenshot() | html2canvas Screenshot als PNG-Attachment |
Rate-Limiting
Max. 30 Feedback-Mails pro Stunde pro User/IP (konfigurierbar via FEEDBACK_RATE_LIMIT env). Bei Ueberschreitung wird eine Wartemeldung angezeigt.
Datei-Upload Limits
Validierung via ValidatesAttachments Trait (app/Concerns/ValidatesAttachments.php):
| Limit | Wert |
|---|---|
| Max. pro Datei | 5 MB |
| Max. gesamt | 15 MB |
| Erlaubte Typen | JPEG, PNG, GIF, WebP, HEIC, MP4, MOV |
Dateien werden temporaer unter storage/app/feedback-attachments/ gespeichert und an die queued FeedbackReport Mailable angehaengt.
Explorer-Integration
Feedback-Button auf folgenden oeffentlichen Explorer-Seiten:
/explorer(Index)/explorer/category/{slug}(Category Landing)/explorer/tag/{slug}(Tag Landing)/explorer/{slug}_{id}(Profilseite — mit automatischem Profil-Kontext)
Settings/Notifications
Benachrichtigungs-Einstellungen pro Notification-Typ.
Route: /settings/notifications
View: resources/views/livewire/settings/notifications.blade.php
Location: app/Livewire/Settings/Notifications.php
Erlaubt Usern, pro Notification-Typ (Import, Sync, Scrape, etc.) festzulegen, ob sie Benachrichtigungen per Toast, Nachrichtenzentrale und/oder E-Mail erhalten moechten. Einstellungen werden in der notification_preferences-Tabelle gespeichert.
Settings/TwoFactor
Verwaltung der Zwei-Faktor-Authentifizierung (TOTP) in den User-Einstellungen.
Route: /settings/two-factor
View: resources/views/livewire/settings/two-factor.blade.php
Location: app/Livewire/Settings/TwoFactor.php
Public Properties
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
$showSetup | bool | false | QR-Code + Secret anzeigen |
$showRecoveryCodes | bool | false | Recovery Codes anzeigen |
$confirmationCode | string | '' | Eingegebener TOTP-Code |
Computed Properties
| Property | Typ | Beschreibung |
|---|---|---|
twoFactorEnabled | bool | Secret vorhanden |
twoFactorConfirmed | bool | two_factor_confirmed_at gesetzt |
qrCodeSvg | string|null | SVG-QR-Code fuer Authenticator |
setupKey | string|null | Entschluesselter Secret-Key |
recoveryCodes | array | 8 Backup-Codes |
Actions
| Method | Beschreibung |
|---|---|
enableTwoFactor() | 2FA aktivieren (generiert Secret + Codes) |
confirmTwoFactor() | 6-stelligen TOTP-Code validieren |
regenerateRecoveryCodes() | Neue Recovery Codes generieren |
disableTwoFactor() | 2FA deaktivieren |
Nutzt Fortify-Actions (EnableTwoFactorAuthentication, ConfirmTwoFactorAuthentication, GenerateNewRecoveryCodes, DisableTwoFactorAuthentication) direkt statt HTTP-Routes.
Detaillierter Ablauf: siehe Authentifizierung → 2FA.
Overview/Index
Landing-Seite nach Registrierung fuer neue User.
Route: /overview
View: resources/views/livewire/overview/index.blade.php
Location: app/Livewire/Overview/Index.php
Seitentitel: "Uebersicht". Fortify leitet nach Registration automatisch hierher weiter (konfiguriert in config/fortify.php). Zeigt Onboarding-Informationen fuer neue User.