Incentive (E1): Bilder je Incentive, Teaser mit Call to Action oben, Praemie je Teampartner statt Punkte

Titelbild und Galerie liegen jetzt in public/img/incentive/<slug>/, der
Hauptordner ist nur noch Fallback fuers Titelbild. Damit erscheinen die
Montenegro-Impressionen nicht mehr unter der Challenge; die Fotos sind in
montenegro-incentive-2026/ verschoben.

Teaser: Call to Action "Bist du bereit fuer die Challenge?" mit Button ganz
oben. Bei Geldpraemie zeigen die Kacheln "Deine Praemie" (50 EUR je neuem
Teampartner) und "Top 3 Bonus" statt Incentive-Punkte, darunter der Abschnitt
"So funktioniert die Challenge". Rangliste: Button "Jetzt teilnehmen" wieder
in der Sekundaerfarbe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-09-22 12:37:58 +00:00
parent 3341c929eb
commit f340d0de3d
20 changed files with 278 additions and 135 deletions

View file

@ -166,29 +166,13 @@ class IncentiveController extends Controller
}
/**
* Sammelt alle verfuegbaren Galerie-Bilder aus public/img/incentive/
* (ohne das Hauptbild, das bereits als Hero verwendet wird).
* Galerie-Bilder des Incentives aus seinem eigenen Ordner ({@see Incentive::galleryImages()}).
*
* @return list<string>
*/
private function collectGalleryImages(Incentive $incentive): array
{
$dir = public_path('img/incentive');
if (! is_dir($dir)) {
return [];
}
$files = glob($dir.'/*.{jpg,jpeg,png,webp}', GLOB_BRACE) ?: [];
$images = [];
foreach ($files as $file) {
$basename = basename($file);
$images[] = 'img/incentive/'.$basename;
}
sort($images);
return $images;
return $incentive->galleryImages();
}
/**

View file

@ -208,6 +208,60 @@ class Incentive extends Model
->get();
}
/**
* Ordner der Bilder dieses Incentives (relativ zu public/): img/incentive/<slug>.
*/
public function imageDirectory(): string
{
return 'img/incentive/'.$this->slug;
}
/**
* Pfad des Titelbilds relativ zu public/, bevorzugt aus dem eigenen Ordner,
* sonst aus dem Hauptordner (Altbestand). NULL ohne Titelbild.
*/
public function heroImagePath(): ?string
{
if (! $this->image) {
return null;
}
$own = $this->imageDirectory().'/'.$this->image;
if (is_file(public_path($own))) {
return $own;
}
return 'img/incentive/'.$this->image;
}
/**
* Galerie-Bilder nur aus dem eigenen Ordner, ohne Titelbild, sortiert.
* Der Hauptordner zaehlt bewusst nicht mehr, damit Bilder eines anderen
* Incentives nicht unter diesem erscheinen.
*
* @return list<string>
*/
public function galleryImages(): array
{
$dir = public_path($this->imageDirectory());
if (! is_dir($dir)) {
return [];
}
$files = glob($dir.'/*.{jpg,jpeg,png,webp}', GLOB_BRACE) ?: [];
$images = [];
foreach ($files as $file) {
$basename = basename($file);
if ($basename === $this->image) {
continue;
}
$images[] = $this->imageDirectory().'/'.$basename;
}
sort($images);
return $images;
}
/**
* Darf dieser Berater das Incentive sehen (Teaser, Rangliste, Details, Teilnahme)?
*/

View file

@ -93,7 +93,7 @@ So im Seeder hinterlegt. Die Punkte 2 (Bestätigung als Voraussetzung), 6 (Gleic
Hero-Bild liefert Kevin: Querformat, ca. 1600 × 600 px, JPG. Kein Strandmotiv, sonst sieht es aus wie Montenegro. Ablage unter `public/img/incentive/`, Dateiname im Admin ins Feld "Bild".
Achtung Galerie: Dashboard-Karte und Teaser zeigen heute alle Bilder aus diesem Ordner, also auch die acht Montenegro-Strandfotos unter der Challenge. Das wird vor dem Start umgebaut (Unterordner je Incentive, Abschnitt 10). Bis dahin keine weiteren Bilder in den Ordner legen.
Galerie (seit 22.09. getrennt): Jedes Incentive hat seinen eigenen Bildordner `public/img/incentive/<slug>/`. Die Challenge bekommt nur ein Titelbild, also `public/img/incentive/recruiting-challenge-2026-27/<dateiname>.jpg`, keine Impressionen. Die Montenegro-Fotos liegen jetzt unter `montenegro-incentive-2026/`. Der Teaser beginnt mit dem Call to Action "Bist du bereit für die Challenge?" und dem Button zur Teilnahme, darunter Titelbild, Text und die drei Kacheln Zeitraum, Prämie je Teampartner und Top 3 Bonus.
Das Badge auf der Dashboard-Karte heißt bei Geldprämie automatisch "Challenge" statt "Incentive VIP". Farbwelt bleibt die des Incentive-Moduls.
@ -131,7 +131,6 @@ Bonushöhe ist entschieden (500 / 300 / 200 €). Übrig bleiben die Regelfragen
| Punkt | Aufwand | Wann |
|---|---|---|
| Galerie je Incentive trennen (Unterordner `public/img/incentive/<slug>/`, Fallback auf Hauptordner für Montenegro) | 0,5 PT | vor dem 01.10. |
| Gleichstandsregel "zuerst erreicht" (Frage 1), steht bereits in den Bedingungen | 0,5 PT | vor dem 01.10. |
| Storno entfernt Neupartner-Wertung (Frage 2), falls Automatik gewünscht | 1 PT | vor der Auszahlung |
| Export nur bestätigte Teilnehmer (Frage 3) | 0,25 PT | vor der Auszahlung |
@ -147,14 +146,14 @@ php artisan db:seed --class=RecruitingChallenge2026Seeder
php artisan view:clear && php artisan route:clear
```
Danach im Admin: Bild eintragen, Texte prüfen, Status auf "Aktiv". Der Seeder setzt den Haken "Nur für VIP sichtbar": Sidenav, Dashboard, Teaser und Rangliste sehen dann nur VIP-Konten, so kann der Kunde die Challenge im Backoffice prüfen, ohne dass Berater sie sehen. Punkte laufen trotzdem für alle mit. Nach Freigabe den Haken im Admin entfernen. Lokal ist die Challenge angelegt, aktiv und auf VIP gestellt (ID 3).
Beim Deploy den Ordner `public/img/incentive/montenegro-incentive-2026/` mit hochladen; die alten Dateien direkt in `public/img/incentive/` können danach gelöscht werden. Danach im Admin: Bild eintragen, Texte prüfen, Status auf "Aktiv". Der Seeder setzt den Haken "Nur für VIP sichtbar": Sidenav, Dashboard, Teaser und Rangliste sehen dann nur VIP-Konten, so kann der Kunde die Challenge im Backoffice prüfen, ohne dass Berater sie sehen. Punkte laufen trotzdem für alle mit. Nach Freigabe den Haken im Admin entfernen. Lokal ist die Challenge angelegt, aktiv und auf VIP gestellt (ID 3).
## 12. Checkliste bis zum Start
- [ ] Fragen 1 bis 10 mit Dani/Alois entschieden
- [ ] Texte freigegeben (Name, Untertitel, Beschreibung, Bedingungen), ggf. Übersetzungen
- [ ] Hero-Bild geliefert und abgelegt
- [ ] Galerie-Trennung und Gleichstandsregel deployt
- [ ] Gleichstandsregel deployt
- [ ] Deploy E1 und Seeder auf Produktion
- [ ] Challenge im Admin geprüft, Teaser und Rangliste angesehen
- [ ] Ankündigungsmail raus

View file

@ -452,6 +452,7 @@ Das Incentive-Modul trägt jetzt optional eine Geldprämie; Montenegro bleibt un
- **Mehrere aktive Incentives:** Sidenav listet alle aktiven Incentives (bei mehreren mit Namen), Dashboard zeigt je Incentive eine Karte mit eigenen Modal-IDs; `HomeController` liefert `activeIncentives`/`incentiveParticipants` (die alten Einzelvariablen bleiben befüllt). Montenegro kann so bis zum Abschluss parallel stehen bleiben.
- **Berater-Ansichten:** Teaser und Rangliste zeigen bei Geldprämie ohne Mindestqualifikation eine Prämien-Kachel statt „Dein Ticket“, blenden die Abo-Blöcke aus, wenn Abos keine Rolle spielen, und ergänzen die Rangliste um die Spalte „Prämie“; Header/Details zeigen „Deine Prämie bisher“. Der Punkte-Boost-Text erscheint nur, wenn Umsatzpunkte zählen.
- **Nur für VIP sichtbar (22.09.):** Haken am Incentive (`incentives.vip_only`, Migration `2026_09_22_100000`). Aktives Incentive erscheint dann nur für VIP-Konten (Sidenav, Dashboard, Teaser, Rangliste, Details; andere bekommen 404), Tracking läuft für alle. Zur Abstimmung mit dem Kunden; danach Haken entfernen. Ranglisten-Seite umgebaut: Teilnahme-Block mit Prämien-Chips und Bedingungen steht oben, solange die Teilnahme nicht bestätigt ist, darunter Trennlinie und die Inhalte; Überschriften vergrößert. Inhalte/Texte: [2026-09-18-recruiting-challenge-inhalte.md](2026-09-18-recruiting-challenge-inhalte.md), Seeder `RecruitingChallenge2026Seeder`.
- **Bilder je Incentive (22.09.):** Titelbild und Galerie liegen in `public/img/incentive/<slug>/` (`Incentive::heroImagePath()`, `galleryImages()`); der Hauptordner dient nur noch als Fallback fürs Titelbild. Montenegro-Fotos nach `montenegro-incentive-2026/` verschoben (beim Deploy mit hochladen). Teaser: Call to Action ganz oben, bei Geldprämie Kacheln „Deine Prämie“ (50 € je Teampartner) und „Top 3 Bonus“ statt Punkte, Abschnitt „So funktioniert die Challenge“.
- **Tests:** [`IncentiveVipOnlyTest`](../../tests/Feature/Incentive/IncentiveVipOnlyTest.php), [`RecruitingChallengeSeederTest`](../../tests/Feature/Incentive/RecruitingChallengeSeederTest.php), [`IncentivePayoutTest`](../../tests/Feature/Incentive/IncentivePayoutTest.php), [`IncentiveSalesPointsToggleTest`](../../tests/Feature/Incentive/IncentiveSalesPointsToggleTest.php), [`IncentiveAdminPayoutTest`](../../tests/Feature/Incentive/IncentiveAdminPayoutTest.php). Incentive-Suite: 48 grün, 1 vorbestehender Fehlschlag (`IncentiveTeaserPageTest`, Hero-Bild in der Galerie – unabhängig von E1).
**Konfiguration der Challenge (Admin → Incentives → Neu):**

View file

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 226 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 228 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 205 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 199 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Before After
Before After

View file

@ -199,7 +199,7 @@ return [
'payout_incl_bonus' => 'inkl. :amount Rang-Bonus',
'payout_so_far' => 'Deine Prämie bisher',
'payout_tile_title' => 'Deine Prämie',
'payout_per_partner_label' => 'je direkt gesponsertem Neupartner mit Starterpaket',
'payout_per_partner_label' => 'je neuem Teampartner mit Starterpaket',
'payout_per_partner_text' => 'Prämie: :amount je direkt gesponsertem Neupartner mit Starterpaket.',
'payout_rank_bonus_label' => 'Zusatz-Bonus für Platz :n',
'payout_top_bonus_title' => 'Zusatz-Bonus für die Top :n',
@ -222,4 +222,6 @@ return [
'join_highlight_points' => ':points Punkte je neuem Partner',
'join_highlight_abo' => ':points Punkte je Kundenabo',
'section_info' => 'Alles zur Challenge',
'section_how_it_works' => 'So funktioniert die Challenge',
'payout_bonus_tile_title' => 'Top :n Bonus',
];

View file

@ -199,7 +199,7 @@ return [
'payout_incl_bonus' => 'incl. :amount rank bonus',
'payout_so_far' => 'Your reward so far',
'payout_tile_title' => 'Your reward',
'payout_per_partner_label' => 'per directly sponsored new partner with a starter kit',
'payout_per_partner_label' => 'per new team partner with a starter kit',
'payout_per_partner_text' => 'Reward: :amount per directly sponsored new partner with a starter kit.',
'payout_rank_bonus_label' => 'Extra bonus for rank :n',
'payout_top_bonus_title' => 'Extra bonus for the top :n',
@ -222,4 +222,6 @@ return [
'join_highlight_points' => ':points points per new partner',
'join_highlight_abo' => ':points points per customer subscription',
'section_info' => 'All about the challenge',
'section_how_it_works' => 'How the challenge works',
'payout_bonus_tile_title' => 'Top :n bonus',
];

View file

@ -199,7 +199,7 @@ return [
'payout_incl_bonus' => 'incl. :amount bono por puesto',
'payout_so_far' => 'Tu prima hasta ahora',
'payout_tile_title' => 'Tu prima',
'payout_per_partner_label' => 'por nuevo socio patrocinado directamente con paquete de inicio',
'payout_per_partner_label' => 'por nuevo socio de equipo con paquete de inicio',
'payout_per_partner_text' => 'Prima: :amount por nuevo socio patrocinado directamente con paquete de inicio.',
'payout_rank_bonus_label' => 'Bono adicional para el puesto :n',
'payout_top_bonus_title' => 'Bono adicional para el top :n',
@ -222,4 +222,6 @@ return [
'join_highlight_points' => ':points puntos por nuevo socio',
'join_highlight_abo' => ':points puntos por suscripción de cliente',
'section_info' => 'Todo sobre el reto',
'section_how_it_works' => 'Así funciona el reto',
'payout_bonus_tile_title' => 'Bono top :n',
];

View file

@ -174,7 +174,7 @@ return [
'payout_incl_bonus' => 'dont :amount de bonus de classement',
'payout_so_far' => 'Ta prime jusqu’ici',
'payout_tile_title' => 'Ta prime',
'payout_per_partner_label' => 'par nouveau partenaire parrainé directement avec kit de démarrage',
'payout_per_partner_label' => 'par nouveau partenaire d’équipe avec kit de démarrage',
'payout_per_partner_text' => 'Prime : :amount par nouveau partenaire parrainé directement avec kit de démarrage.',
'payout_rank_bonus_label' => 'Bonus supplémentaire pour la place :n',
'payout_top_bonus_title' => 'Bonus supplémentaire pour le top :n',
@ -197,4 +197,6 @@ return [
'join_highlight_points' => ':points points par nouveau partenaire',
'join_highlight_abo' => ':points points par abonnement client',
'section_info' => 'Tout sur le challenge',
'section_how_it_works' => 'Comment fonctionne le challenge',
'payout_bonus_tile_title' => 'Bonus top :n',
];

View file

@ -21,7 +21,7 @@
{{-- Hero-Bild mit Overlay --}}
@if ($activeIncentive->image)
<div class="inc-dash-hero">
<img src="{{ asset('img/incentive/' . $activeIncentive->image) }}" alt="{{ $activeIncentive->name }}">
<img src="{{ asset($activeIncentive->heroImagePath()) }}" alt="{{ $activeIncentive->name }}">
<div class="inc-dash-hero-overlay"></div>
<div class="inc-dash-hero-content">
<span class="inc-dash-badge">
@ -110,22 +110,7 @@
@endif
{{-- Bilder-Leiste --}}
@php
$dashGallery = [];
$galleryDir = public_path('img/incentive');
if (is_dir($galleryDir)) {
$galleryFiles = glob($galleryDir . '/*.{jpg,jpeg,png,webp}', GLOB_BRACE) ?: [];
$heroImg = $activeIncentive->image;
foreach ($galleryFiles as $gf) {
$bn = basename($gf);
if ($bn !== $heroImg) {
$dashGallery[] = 'img/incentive/' . $bn;
}
}
sort($dashGallery);
$dashGallery = array_slice($dashGallery, 0, 4);
}
@endphp
@php $dashGallery = array_slice($activeIncentive->galleryImages(), 0, 4); @endphp
@if (count($dashGallery) > 0)
<div class="inc-dash-gallery">
@foreach ($dashGallery as $gi)

View file

@ -169,8 +169,21 @@
}
.inc-join-form .btn-participate {
background: #d7d700;
color: #333;
border: none;
border-radius: 50px;
font-size: 1.05rem;
font-weight: 800;
padding: .7rem 1.5rem;
transition: transform .2s, box-shadow .2s;
}
.inc-join-form .btn-participate:hover {
background: #e8e800;
color: #333;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
.inc-join-hint {
@ -719,7 +732,7 @@
@if ($incentive->image)
<div class="inc-hero-img mb-4">
<img src="{{ asset('img/incentive/' . $incentive->image) }}" alt="{{ $incentive->name }}">
<img src="{{ asset($incentive->heroImagePath()) }}" alt="{{ $incentive->name }}">
<div class="hero-gradient"></div>
</div>
@endif
@ -998,7 +1011,7 @@
@if ($incentive->image)
<div class="inc-hero-img mb-4">
<img src="{{ asset('img/incentive/' . $incentive->image) }}" alt="{{ $incentive->name }}"
<img src="{{ asset($incentive->heroImagePath()) }}" alt="{{ $incentive->name }}"
style="max-height: 450px;">
<div class="hero-gradient"></div>
</div>

View file

@ -3,7 +3,7 @@
@section('styles')
<style>
.incentive-hero {
margin: -1.5rem -1.5rem 0 -1.5rem;
margin: 0 -1.5rem 0 -1.5rem;
position: relative;
overflow: hidden;
min-height: 520px;
@ -442,10 +442,51 @@
@section('content')
{{-- ===== CTA-BEREICH ===== --}}
<div class="cta-banner mb-4" id="incentive-teaser-cta">
@if ($participant && $participant->accepted_terms_at)
<div class="cta-status-icon cta-status-icon-success">
<i class="ion ion-md-checkmark-circle"></i>
</div>
<h3>{{ __('incentive.you_participate') }}</h3>
<p class="mb-4">{{ __('incentive.teaser_cta_already_in') }}</p>
<a href="{{ route('user_incentive_show', [$incentive->slug]) }}" class="btn btn-secondary btn-lg mr-2">
<i class="ion ion-md-list mr-1"></i>{{ __('incentive.teaser_cta_to_ranking') }}
</a>
<a href="{{ route('user_incentive_details', [$incentive->slug]) }}" class="btn btn-outline-secondary btn-lg">
<i class="ion ion-md-stats mr-1"></i>{{ __('incentive.my_details') }}
</a>
@elseif ($participant)
<div class="cta-status-icon cta-status-icon-warning">
<i class="ion ion-md-trophy"></i>
</div>
<h3>{{ __('incentive.teaser_pending_title') }}</h3>
<p class="mb-4">{{ __('incentive.teaser_pending_text') }}</p>
<a href="{{ route('user_incentive_show', [$incentive->slug]) }}" class="btn btn-secondary btn-lg">
<i class="ion ion-md-checkmark mr-1"></i>{{ __('incentive.teaser_cta_confirm') }}
</a>
@elseif($incentive->isActive())
<div class="cta-status-icon cta-status-icon-warning">
<i class="ion ion-md-trophy"></i>
</div>
<h3>{{ __('incentive.teaser_cta_ready') }}</h3>
<p class="mb-4">{{ __('incentive.teaser_cta_text', ['n' => $incentive->max_winners]) }}</p>
<a href="{{ route('user_incentive_show', [$incentive->slug]) }}" class="btn btn-secondary btn-lg">
<i class="ion ion-md-checkmark mr-1"></i>{{ __('incentive.teaser_cta_button') }}
</a>
@else
<div class="cta-status-icon cta-status-icon-muted">
<i class="ion ion-md-time"></i>
</div>
<h3>{{ __('incentive.teaser_cta_coming_soon') }}</h3>
<p class="mb-0">{{ __('incentive.not_active') }}</p>
@endif
</div>
{{-- ===== HERO ===== --}}
@if ($incentive->image)
<div class="incentive-hero mb-5">
<img src="{{ asset('img/incentive/' . $incentive->image) }}" alt="{{ $incentive->name }}">
<img src="{{ asset($incentive->heroImagePath()) }}" alt="{{ $incentive->name }}">
<div class="hero-overlay"></div>
<div class="hero-content">
<span class="hero-badge">
@ -530,7 +571,7 @@
<span class="tile-label">{{ __('incentive.payout_per_partner_label') }}</span>
</div>
@endif
@if ($incentive->rankBonuses() !== [])
@if ($incentive->rankBonuses() !== [] && !$incentive->ranksByPartnerCount())
<div>
<span class="tile-label d-block mb-1">{{ __('incentive.payout_top_bonus_title', ['n' => count($incentive->rankBonuses())]) }}</span>
@foreach ($incentive->rankBonuses() as $bonusRank => $bonusAmount)
@ -564,6 +605,27 @@
</div>
@endif
@if ($incentive->hasCashPayout() && $incentive->ranksByPartnerCount())
{{-- Top 3 Bonus (Recruiting-Challenge) --}}
<div class="col-md-4">
<div class="card info-tile h-100 shadow-sm" id="incentive-bonus-tile">
<div class="card-body text-center py-4">
<div class="tile-icon tile-icon-star">
<i class="ion ion-md-trophy"></i>
</div>
<h5 class="font-weight-bold mb-3">{{ __('incentive.payout_bonus_tile_title', ['n' => max(count($incentive->rankBonuses()), $incentive->max_winners)]) }}</h5>
@forelse ($incentive->rankBonuses() as $bonusRank => $bonusAmount)
<div class="mb-2">
<span class="tile-value" style="color: #6b7758;">{{ \App\Services\Incentive\IncentivePayoutService::formatAmount($bonusAmount) }}</span>
<span class="tile-label d-block">{{ __('incentive.payout_rank_label', ['n' => $bonusRank]) }}</span>
</div>
@empty
<span class="tile-label d-block">{{ __('incentive.ranking_winners_hint', ['n' => $incentive->max_winners]) }}</span>
@endforelse
</div>
</div>
</div>
@else
{{-- Punkte-System --}}
<div class="col-md-4">
<div class="card info-tile h-100 shadow-sm">
@ -593,57 +655,89 @@
</div>
</div>
@endif
{{-- ===== PUNKTE-DETAIL ===== --}}
<div class="points-section mb-5">
<h4 class="points-heading">
<i class="ion ion-md-star text-warning mr-1"></i>
{{ __('incentive.section_points') }}
</h4>
<div class="row">
<div class="{{ $incentive->usesAbos() ? 'col-md-6 mb-3 mb-md-0' : 'col-md-12' }}">
<div class="point-card point-card-partner">
<h6 class="font-weight-bold mb-2">
<i class="ion ion-md-person-add mr-1"></i>
{{ __('incentive.points_partners_title') }}
</h6>
<div class="point-value">
{{ number_format($incentive->points_partner_onetime, 0, ',', '.') }}
<span class="small font-weight-normal">{{ __('incentive.points_short') }}</span>
{{-- ===== SO FUNKTIONIERT DIE CHALLENGE (Geldpraemie) / PUNKTE-DETAIL ===== --}}
@if ($incentive->hasCashPayout() && $incentive->ranksByPartnerCount())
<div class="points-section mb-5" id="incentive-how-it-works">
<h4 class="points-heading">
<i class="ion ion-md-cash text-warning mr-1"></i>
{{ __('incentive.section_how_it_works') }}
</h4>
<div class="row">
<div class="col-md-12">
<div class="point-card point-card-partner">
<h6 class="font-weight-bold mb-2">
<i class="ion ion-md-person-add mr-1"></i>
{{ __('incentive.payout_tile_title') }}
</h6>
<div class="point-value">
{{ \App\Services\Incentive\IncentivePayoutService::formatAmount((float) $incentive->amount_per_partner) }}
<span class="small font-weight-normal">{{ __('incentive.payout_per_partner_label') }}</span>
</div>
<ul>
<li>{{ __('incentive.points_starter_package_label') }}</li>
@if ($incentive->rankBonuses() !== [])
<li>{{ __('incentive.payout_top_bonus_text', ['n' => count($incentive->rankBonuses()), 'list' => collect($incentive->rankBonuses())->map(fn ($amount, $rank) => __('incentive.payout_rank_label', ['n' => $rank]).' '.\App\Services\Incentive\IncentivePayoutService::formatAmount($amount))->implode(', ')]) }}</li>
@endif
<li>{{ __('incentive.payout_ranking_hint') }}</li>
</ul>
</div>
<ul>
<li>{{ __('incentive.teaser_partner_onetime_text') }}</li>
<li>{{ __('incentive.points_starter_package_label') }}</li>
@if ($incentive->includesSalesPoints())
<li>{{ __('incentive.points_partner_boost') }}</li>
@endif
@include('user.incentive._payout_bullets', ['incentive' => $incentive])
</ul>
</div>
</div>
@if ($incentive->usesAbos())
<div class="col-md-6">
<div class="point-card point-card-abo">
<h6 class="font-weight-bold mb-2">
<i class="ion ion-md-ribbon mr-1"></i>
{{ __('incentive.points_abos_title') }}
</h6>
<div class="point-value">
{{ number_format($incentive->points_abo_onetime, 0, ',', '.') }}
<span class="small font-weight-normal">{{ __('incentive.points_short') }}</span>
</div>
<ul>
<li>{{ __('incentive.teaser_abo_onetime_text') }}</li>
<li>{{ __('incentive.points_abo_direct') }}</li>
@if ($incentive->includesSalesPoints())
<li>{{ __('incentive.points_abo_boost') }}</li>
@endif
</ul>
</div>
</div>
@endif
</div>
</div>
@else
{{-- ===== PUNKTE-DETAIL ===== --}}
<div class="points-section mb-5">
<h4 class="points-heading">
<i class="ion ion-md-star text-warning mr-1"></i>
{{ __('incentive.section_points') }}
</h4>
<div class="row">
<div class="{{ $incentive->usesAbos() ? 'col-md-6 mb-3 mb-md-0' : 'col-md-12' }}">
<div class="point-card point-card-partner">
<h6 class="font-weight-bold mb-2">
<i class="ion ion-md-person-add mr-1"></i>
{{ __('incentive.points_partners_title') }}
</h6>
<div class="point-value">
{{ number_format($incentive->points_partner_onetime, 0, ',', '.') }}
<span class="small font-weight-normal">{{ __('incentive.points_short') }}</span>
</div>
<ul>
<li>{{ __('incentive.teaser_partner_onetime_text') }}</li>
<li>{{ __('incentive.points_starter_package_label') }}</li>
@if ($incentive->includesSalesPoints())
<li>{{ __('incentive.points_partner_boost') }}</li>
@endif
@include('user.incentive._payout_bullets', ['incentive' => $incentive])
</ul>
</div>
</div>
@if ($incentive->usesAbos())
<div class="col-md-6">
<div class="point-card point-card-abo">
<h6 class="font-weight-bold mb-2">
<i class="ion ion-md-ribbon mr-1"></i>
{{ __('incentive.points_abos_title') }}
</h6>
<div class="point-value">
{{ number_format($incentive->points_abo_onetime, 0, ',', '.') }}
<span class="small font-weight-normal">{{ __('incentive.points_short') }}</span>
</div>
<ul>
<li>{{ __('incentive.teaser_abo_onetime_text') }}</li>
<li>{{ __('incentive.points_abo_direct') }}</li>
@if ($incentive->includesSalesPoints())
<li>{{ __('incentive.points_abo_boost') }}</li>
@endif
</ul>
</div>
</div>
@endif
</div>
</div>
@endif
{{-- ===== BILDERGALERIE ===== --}}
@if (count($galleryImages) > 0)
@ -670,47 +764,6 @@
</div>
@endif
{{-- ===== CTA-BEREICH ===== --}}
<div class="cta-banner mb-5">
@if ($participant && $participant->accepted_terms_at)
<div class="cta-status-icon cta-status-icon-success">
<i class="ion ion-md-checkmark-circle"></i>
</div>
<h3>{{ __('incentive.you_participate') }}</h3>
<p class="mb-4">{{ __('incentive.teaser_cta_already_in') }}</p>
<a href="{{ route('user_incentive_show', [$incentive->slug]) }}" class="btn btn-secondary btn-lg mr-2">
<i class="ion ion-md-list mr-1"></i>{{ __('incentive.teaser_cta_to_ranking') }}
</a>
<a href="{{ route('user_incentive_details', [$incentive->slug]) }}" class="btn btn-outline-secondary btn-lg">
<i class="ion ion-md-stats mr-1"></i>{{ __('incentive.my_details') }}
</a>
@elseif ($participant)
<div class="cta-status-icon cta-status-icon-warning">
<i class="ion ion-md-trophy"></i>
</div>
<h3>{{ __('incentive.teaser_pending_title') }}</h3>
<p class="mb-4">{{ __('incentive.teaser_pending_text') }}</p>
<a href="{{ route('user_incentive_show', [$incentive->slug]) }}" class="btn btn-secondary btn-lg">
<i class="ion ion-md-checkmark mr-1"></i>{{ __('incentive.teaser_cta_confirm') }}
</a>
@elseif($incentive->isActive())
<div class="cta-status-icon cta-status-icon-warning">
<i class="ion ion-md-trophy"></i>
</div>
<h3>{{ __('incentive.teaser_cta_ready') }}</h3>
<p class="mb-4">{{ __('incentive.teaser_cta_text', ['n' => $incentive->max_winners]) }}</p>
<a href="{{ route('user_incentive_show', [$incentive->slug]) }}" class="btn btn-secondary btn-lg">
<i class="ion ion-md-checkmark mr-1"></i>{{ __('incentive.teaser_cta_button') }}
</a>
@else
<div class="cta-status-icon cta-status-icon-muted">
<i class="ion ion-md-time"></i>
</div>
<h3>{{ __('incentive.teaser_cta_coming_soon') }}</h3>
<p class="mb-0">{{ __('incentive.not_active') }}</p>
@endif
</div>
{{-- ===== TEILNAHMEBEDINGUNGEN (Collapse) ===== --}}
@if ($incentive->getLang('terms'))
<div class="card terms-toggle mb-4">

View file

@ -185,3 +185,49 @@ describe('Ranglisten-Seite: Teilnahme-Block oben', function () {
->assertSee(__('incentive.my_details'));
});
});
describe('Teaser-Seite der Challenge', function () {
it('zeigt den Call to Action ganz oben, die Praemie je Teampartner und keine Galerie', function () {
$incentive = vipOnlyChallenge(['vip_only' => false, 'image' => 'challenge.jpg', 'bonus_rank_2' => 300, 'bonus_rank_3' => 200]);
$this->actingAs(vipTestUser());
$response = $this->get(route('user_incentive_teaser', [$incentive->slug]))->assertOk();
$html = $response->getContent();
$cta = strpos($html, 'id="incentive-teaser-cta"');
$hero = strpos($html, 'class="incentive-hero mb-5"');
expect($cta)->not->toBeFalse()
->and($hero)->toBeGreaterThan($cta);
$response->assertSee(__('incentive.teaser_cta_ready'))
->assertSee(__('incentive.payout_per_partner_label'))
->assertSee('id="incentive-bonus-tile"', false)
->assertSee('id="incentive-how-it-works"', false)
->assertSee(__('incentive.section_how_it_works'))
->assertDontSee(__('incentive.section_points'))
->assertDontSee('class="gallery-section', false)
->assertDontSee(__('incentive.gallery_title'));
});
it('nimmt Galerie-Bilder nur aus dem eigenen Ordner des Incentives', function () {
$incentive = vipOnlyChallenge(['vip_only' => false, 'image' => 'hero.jpg']);
$other = vipOnlyChallenge(['vip_only' => false, 'image' => 'alt.jpg']);
$dir = public_path($incentive->imageDirectory());
@mkdir($dir, 0777, true);
file_put_contents($dir.'/hero.jpg', 'x');
file_put_contents($dir.'/b.jpg', 'x');
file_put_contents($dir.'/a.jpg', 'x');
try {
expect($incentive->galleryImages())->toBe([$incentive->imageDirectory().'/a.jpg', $incentive->imageDirectory().'/b.jpg'])
->and($incentive->heroImagePath())->toBe($incentive->imageDirectory().'/hero.jpg')
->and($other->galleryImages())->toBe([])
->and($other->heroImagePath())->toBe('img/incentive/'.$other->image);
} finally {
array_map('unlink', glob($dir.'/*') ?: []);
@rmdir($dir);
}
});
});