feat: gravatar integration

This commit is contained in:
Björn Fromme
2026-03-16 11:59:11 +01:00
parent 142db19627
commit 5d69e4a8aa
5 changed files with 94 additions and 20 deletions
+17
View File
@@ -0,0 +1,17 @@
import {Controller} from '@hotwired/stimulus'
export default class extends Controller {
static values = {url: String, alt: String}
static classes = ['image']
connect() {
const img = new Image()
img.onload = () => {
img.classList.add(...this.imageClasses)
this.element.innerHTML = ''
this.element.appendChild(img)
}
img.alt = this.altValue
img.src = this.urlValue
}
}
+8 -4
View File
@@ -25,7 +25,7 @@ class ParticipantCardDataService
/** /**
* Get card data for a single participant. * Get card data for a single participant.
* *
* @return array{name: string, roomName: string, price: string, isCanceled: bool} * @return array{name: string, email: string, roomName: string, price: string, isCanceled: bool}
*/ */
public function getCardData(BookingDto $bookingDto, int $index): array public function getCardData(BookingDto $bookingDto, int $index): array
{ {
@@ -38,6 +38,9 @@ class ParticipantCardDataService
// Extract participant name with fallback // Extract participant name with fallback
$name = $this->getParticipantName($participant, $index); $name = $this->getParticipantName($participant, $index);
// Extract email
$email = $participant->email ?? '';
// Extract room name // Extract room name
$roomName = $this->getRoomName($bookingDto, $participant); $roomName = $this->getRoomName($bookingDto, $participant);
@@ -49,6 +52,7 @@ class ParticipantCardDataService
return [ return [
'name' => $name, 'name' => $name,
'email' => $email,
'roomName' => $roomName, 'roomName' => $roomName,
'price' => $price, 'price' => $price,
'isCanceled' => $isCanceled, 'isCanceled' => $isCanceled,
@@ -58,7 +62,7 @@ class ParticipantCardDataService
/** /**
* Get card data for all participants. * Get card data for all participants.
* *
* @return array<int, array{name: string, roomName: string, price: string, isCanceled: bool}> * @return array<int, array{name: string, email: string, roomName: string, price: string, isCanceled: bool}>
*/ */
public function getAllCardsData(BookingDto $bookingDto): array public function getAllCardsData(BookingDto $bookingDto): array
{ {
@@ -132,7 +136,7 @@ class ParticipantCardDataService
/** /**
* Get card data for a single participant with validation state. * Get card data for a single participant with validation state.
* *
* @return array{name: string, roomName: string, price: string, isCanceled: bool, isValid: bool, errorMessages: array<string>} * @return array{name: string, email: string, roomName: string, price: string, isCanceled: bool, isValid: bool, errorMessages: array<string>}
*/ */
public function getCardDataWithValidation(BookingDto $bookingDto, int $index, array $validationGroups): array public function getCardDataWithValidation(BookingDto $bookingDto, int $index, array $validationGroups): array
{ {
@@ -171,7 +175,7 @@ class ParticipantCardDataService
/** /**
* Get card data for all participants with validation state. * Get card data for all participants with validation state.
* *
* @return array<int, array{name: string, roomName: string, price: string, isCanceled: bool, isValid: bool, errorMessages: array<string>}> * @return array<int, array{name: string, email: string, roomName: string, price: string, isCanceled: bool, isValid: bool, errorMessages: array<string>}>
*/ */
public function getAllCardsDataWithValidation(BookingDto $bookingDto, array $validationGroups): array public function getAllCardsDataWithValidation(BookingDto $bookingDto, array $validationGroups): array
{ {
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
final class GravatarExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('gravatar_url', [$this, 'getGravatarUrl']),
];
}
/**
* Generates a Gravatar URL for the given email address.
*/
public function getGravatarUrl(string $email, int $size = 80): string
{
$hash = md5(strtolower(trim($email)));
return sprintf('https://www.gravatar.com/avatar/%s?s=%d&d=404', $hash, $size);
}
}
+29 -15
View File
@@ -8,22 +8,36 @@
class="{{ html_classes('border rounded p-4', { 'border-gray-400 bg-gray-50': isCanceled, 'border-red-700': not isValid }) }}"> class="{{ html_classes('border rounded p-4', { 'border-gray-400 bg-gray-50': isCanceled, 'border-red-700': not isValid }) }}">
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-2"> <div class="flex items-start gap-3">
<h3 class="font-semibold {{ isCanceled ? 'text-gray-600' : '' }}" {{ qa_attribute('participant-name', index) }}> <div class="flex-shrink-0"
{{ cardData.name }} {{ stimulus_controller('gravatar', {
</h3> url: gravatar_url(cardData.email),
{% if isCanceled %} alt: cardData.name
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-700 text-white" {{ qa_attribute('participant-canceled', index) }}> }, {
storniert image: 'w-12 h-12 rounded-full'
</span> }) }}
{% endif %} {{ qa_attribute('participant-avatar', index) }}>
{% if not isValid %} {{ icon('user', 'w-12 h-12 text-gray-400') }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-700 text-white" {{ qa_attribute('participant-invalid', index) }}> </div>
unvollständige oder fehlerhafte Daten <div class="flex-1">
</span> <div class="flex items-center gap-2">
{% endif %} <h3 class="font-semibold {{ isCanceled ? 'text-gray-600' : '' }}" {{ qa_attribute('participant-name', index) }}>
{{ cardData.name }}
</h3>
{% if isCanceled %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-700 text-white" {{ qa_attribute('participant-canceled', index) }}>
storniert
</span>
{% endif %}
{% if not isValid %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-700 text-white" {{ qa_attribute('participant-invalid', index) }}>
unvollständige oder fehlerhafte Daten
</span>
{% endif %}
</div>
<p class="text-sm text-gray-600" {{ qa_attribute('participant-room-name', index) }}>{{ cardData.roomName }}</p>
</div>
</div> </div>
<p class="text-sm text-gray-600" {{ qa_attribute('participant-room-name', index) }}>{{ cardData.roomName }}</p>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<span class="font-medium {{ isCanceled ? 'text-gray-500' : '' }}" {{ qa_attribute('participant-price', index) }}>{{ cardData.price }}</span> <span class="font-medium {{ isCanceled ? 'text-gray-500' : '' }}" {{ qa_attribute('participant-price', index) }}>{{ cardData.price }}</span>
+12 -1
View File
@@ -45,7 +45,18 @@
{% block participant_form %} {% block participant_form %}
{% import _self as macros %} {% import _self as macros %}
<div id="participant-form-view"> <div id="participant-form-view">
<h2>{{ participantIndex == 0 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }}</h2> <div class="flex items-center gap-4 mb-6">
<div class="flex-shrink-0"
{{ stimulus_controller('gravatar', {
url: gravatar_url(form.vars.data.participant.email ?? ''),
alt: participantIndex == 0 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1)
}, {
image: 'w-16 h-16 rounded-full'
}) }}>
{{ icon('user', 'w-16 h-16 text-gray-400') }}
</div>
<h2 class="text-2xl">{{ participantIndex == 0 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }}</h2>
</div>
{{ form_start(form, { {{ form_start(form, {
'attr': { 'attr': {