reviews photos clickable and contact form functionality

This commit is contained in:
elvira
2026-09-24 21:03:59 +03:00
parent e28faea546
commit 540da1af24
12 changed files with 223 additions and 14 deletions
+1
View File
@@ -59,6 +59,7 @@ MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
CONTACT_EMAIL=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
+62 -1
View File
@@ -2,10 +2,71 @@
namespace App\Http\Controllers;
use App\Http\Requests\ContactRequest;
use App\Mail\ContactConfirmationMail;
use App\Mail\ContactMessageMail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\View\View;
use Throwable;
/**
* Contact form. Nothing is stored: the message is emailed to the store
* (CONTACT_EMAIL) and the sender gets a generic confirmation. Both are sent
* synchronously so a failed store email can be reported back on the form.
*
* Limited per IP in here rather than with throttle middleware, so the limit
* shows as a message on the form instead of a bare 429 page.
*/
class ContactController extends Controller
{
public function index(string $locale)
private const SEND_LIMIT = 3;
private const SEND_DECAY_SECONDS = 600;
public function index(string $locale): View
{
return view('contact');
}
public function send(string $locale, ContactRequest $request): RedirectResponse
{
$key = 'contact:'.$request->ip();
if (RateLimiter::tooManyAttempts($key, self::SEND_LIMIT)) {
return back()->withInput()->with('contact_error', __('storefront.contact.too_many'));
}
RateLimiter::hit($key, self::SEND_DECAY_SECONDS);
$data = $request->validated();
$to = config('services.contact.email');
try {
if (blank($to)) {
throw new \RuntimeException('CONTACT_EMAIL is not set.');
}
Mail::to($to)->send(new ContactMessageMail(
$data['name'],
$data['email'],
$data['message'],
$locale,
));
} catch (Throwable $e) {
Log::error('Contact form: store email failed', ['exception' => $e]);
return back()->withInput()->with('contact_error', __('storefront.contact.send_failed'));
}
// The store already has the message, so a failed confirmation is only logged.
try {
Mail::to($data['email'])->locale($locale)->send(new ContactConfirmationMail);
} catch (Throwable $e) {
Log::warning('Contact form: confirmation email failed', ['exception' => $e]);
}
return redirect()->route('contact')->with('status', __('storefront.contact.sent'));
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ContactRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', 'max:255'],
'message' => ['required', 'string', 'max:5000'],
];
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Mail;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
/**
* Sent to whoever submitted the contact form. Deliberately generic: it never
* repeats what they wrote, so the form can't be used to deliver arbitrary text
* to someone else's inbox.
*/
class ContactConfirmationMail extends Mailable
{
public function envelope(): Envelope
{
return new Envelope(subject: __('storefront.contact.confirmation_subject'));
}
public function content(): Content
{
return new Content(view: 'emails.contact-confirmation');
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Mail;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
/**
* A contact-form message, sent to the store's CONTACT_EMAIL. Reply-To is the
* sender, so replying from the inbox goes straight to them.
*/
class ContactMessageMail extends Mailable
{
public function __construct(
public readonly string $senderName,
public readonly string $senderEmail,
public readonly string $body,
public readonly string $senderLocale,
) {}
public function envelope(): Envelope
{
return new Envelope(
replyTo: [new Address($this->senderEmail, $this->senderName)],
subject: "Νέο μήνυμα επικοινωνίας από {$this->senderName}",
);
}
public function content(): Content
{
return new Content(view: 'emails.contact-message');
}
}
+5
View File
@@ -35,6 +35,11 @@
],
],
// Where contact-form messages are sent (ContactController).
'contact' => [
'email' => env('CONTACT_EMAIL'),
],
'stoic' => [
'sso_secret' => env('STOIC_SSO_SECRET'),
'host' => env('STOIC_HOST'),
@@ -70,8 +70,15 @@
</div>
<div class="flex min-w-0 flex-1 flex-col gap-1">
{{-- Linked only while the product still exists and is published;
otherwise the name stays plain text (the order keeps it). --}}
@php($lineProduct = $line->purchasable?->product)
<p class="font-bold">
{{ $line->description }}
@if ($lineProduct?->status === 'published')
<a href="{{ route('product.show', ['id' => $lineProduct->id]) }}" class="underline hover:no-underline">{{ $line->description }}</a>
@else
{{ $line->description }}
@endif
<span class="font-normal">&times; {{ $line->quantity }}</span>
</p>
@@ -15,11 +15,13 @@
@if (!empty($review['image']))
<div class="mt-1">
<img
src="{{ $review['image'] }}"
alt="Φωτογραφία από τον αγοραστή"
class="w-24 h-24 object-cover border border-black"
>
<a href="{{ $review['image'] }}" target="_blank" rel="noopener noreferrer">
<img
src="{{ $review['image'] }}"
alt="Φωτογραφία από τον αγοραστή"
class="w-24 h-24 object-cover border border-black"
>
</a>
</div>
@endif
+19 -7
View File
@@ -24,23 +24,35 @@
Έχεις απορία, ιδέα ή θες να μας πεις κάτι; Γράψε μας το μήνυμά σου και θα σου απαντήσουμε το συντομότερο δυνατό.
</p>
<form method="POST" action="#" class="flex flex-col gap-8 max-w-xl">
<x-ui.status class="max-w-xl" />
<form method="POST" action="{{ route('contact.send') }}" class="flex flex-col gap-8 max-w-xl">
@csrf
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2">
<x-ui.field label="Το όνομά σου" for="contact-name">
<x-ui.input id="contact-name" name="name" autocomplete="name" :required="true" />
<x-ui.field label="Το όνομά σου" for="contact-name" :error="$errors->first('name')">
<x-ui.input id="contact-name" name="name" autocomplete="name" maxlength="100" :value="old('name')" :required="true"
:aria-invalid="$errors->has('name') ? 'true' : null"
:aria-describedby="$errors->has('name') ? 'contact-name-error' : null" />
</x-ui.field>
<x-ui.field label="Το email σου" for="contact-email">
<x-ui.input id="contact-email" name="email" type="email" autocomplete="email" :required="true" />
<x-ui.field label="Το email σου" for="contact-email" :error="$errors->first('email')">
<x-ui.input id="contact-email" name="email" type="email" autocomplete="email" maxlength="255" :value="old('email')" :required="true"
:aria-invalid="$errors->has('email') ? 'true' : null"
:aria-describedby="$errors->has('email') ? 'contact-email-error' : null" />
</x-ui.field>
</div>
<x-ui.field label="Το μήνυμά σου" for="contact-message">
<x-ui.textarea id="contact-message" name="message" rows="5" :required="true" />
<x-ui.field label="Το μήνυμά σου" for="contact-message" :error="$errors->first('message')">
<x-ui.textarea id="contact-message" name="message" rows="5" maxlength="5000" :required="true"
:aria-invalid="$errors->has('message') ? 'true' : null"
:aria-describedby="$errors->has('message') ? 'contact-message-error' : null">{{ old('message') }}</x-ui.textarea>
</x-ui.field>
@if (session('contact_error'))
<p role="alert" class="text-sm text-red-600">{{ session('contact_error') }}</p>
@endif
<div>
<x-ui.button type="submit" size="md">Υποβολή</x-ui.button>
</div>
@@ -0,0 +1,17 @@
@extends('emails.layout')
@section('title', __('storefront.contact.confirmation_subject'))
@section('preheader', __('storefront.contact.confirmation_preheader'))
@section('content')
<h1 style="margin:0 0 20px;font-family:'Manrope',Arial,Helvetica,sans-serif;font-size:22px;line-height:1.3;font-weight:700;color:#000000;">
{{ __('storefront.contact.confirmation_heading') }}
</h1>
<p style="margin:0;">{{ __('storefront.contact.confirmation_body') }}</p>
@endsection
@section('footer')
<p style="margin:0;">{{ __('storefront.contact.confirmation_footer') }}</p>
@endsection
@@ -0,0 +1,21 @@
@extends('emails.layout')
@section('title', 'Νέο μήνυμα επικοινωνίας')
@section('preheader', "Από {$senderName} ({$senderEmail})")
@section('content')
<h1 style="margin:0 0 20px;font-family:'Manrope',Arial,Helvetica,sans-serif;font-size:22px;line-height:1.3;font-weight:700;color:#000000;">
Νέο μήνυμα επικοινωνίας
</h1>
<p style="margin:0 0 4px;"><strong>Όνομα:</strong> {{ $senderName }}</p>
<p style="margin:0 0 4px;"><strong>Email:</strong> <a href="mailto:{{ $senderEmail }}" style="color:#000000;">{{ $senderEmail }}</a></p>
<p style="margin:0 0 24px;"><strong>Γλώσσα:</strong> {{ strtoupper($senderLocale) }}</p>
<p style="margin:0;white-space:pre-line;">{{ $body }}</p>
@endsection
@section('footer')
<p style="margin:0;">Στάλθηκε από τη φόρμα επικοινωνίας του 3dealer. Απάντησε σε αυτό το email για να απαντήσεις στον αποστολέα.</p>
@endsection
+1
View File
@@ -57,6 +57,7 @@
Route::get('/search', [SearchController::class, 'show'])->name('search');
Route::get('/contact', [ContactController::class, 'index'])->name('contact');
Route::post('/contact', [ContactController::class, 'send'])->name('contact.send');
Route::get('/terms-and-conditions', [LegalPageController::class, 'terms'])->name(
'legal.terms',