Add email templates, page layouts, and shop themes (template system)
Deploy to Production (witstore.wende.it) / deploy (push) Successful in 7s
Deploy to Production (witstore.wende.it) / deploy (push) Successful in 7s
- E-Mail-Templates: DB-gespeicherte HTML-Templates mit {{placeholder}}-System,
Admin-UI zum Bearbeiten von Betreff und Body, alle 4 Mailables aktualisiert
- Seiten-Layouts: 4 Layouts für CMS-Seiten (Standard, Sidebar, Volle Breite, Landing),
Layout-Auswahl im Admin-Formular
- Shop-Themes: 5 Farbthemen via CSS-Variablen, Picker in Einstellungen
- Deployment: EmailTemplateSeeder wird automatisch beim Deploy ausgeführt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ jobs:
|
|||||||
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan route:clear
|
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan route:clear
|
||||||
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan view:clear
|
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan view:clear
|
||||||
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan migrate --force
|
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan migrate --force
|
||||||
|
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan db:seed --class=EmailTemplateSeeder --force
|
||||||
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan config:cache
|
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan config:cache
|
||||||
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan route:cache
|
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan route:cache
|
||||||
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan view:cache
|
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan view:cache
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\EmailTemplate;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class EmailTemplateController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$templates = EmailTemplate::orderBy('label')->get();
|
||||||
|
return view('admin.email-templates.index', compact('templates'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(EmailTemplate $emailTemplate)
|
||||||
|
{
|
||||||
|
return view('admin.email-templates.edit', ['template' => $emailTemplate]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, EmailTemplate $emailTemplate)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'subject' => 'required|string|max:255',
|
||||||
|
'body' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$emailTemplate->update([
|
||||||
|
'subject' => $request->input('subject'),
|
||||||
|
'body' => $request->input('body'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'E-Mail-Template gespeichert.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,11 +59,13 @@ class PageController extends Controller
|
|||||||
'show_in_footer' => 'boolean',
|
'show_in_footer' => 'boolean',
|
||||||
'show_in_nav' => 'boolean',
|
'show_in_nav' => 'boolean',
|
||||||
'nav_sort' => 'integer|min:0',
|
'nav_sort' => 'integer|min:0',
|
||||||
|
'layout' => 'nullable|in:default,sidebar,full,landing',
|
||||||
]);
|
]);
|
||||||
$data['slug'] = $data['slug'] ? Str::slug($data['slug']) : Str::slug($data['title']);
|
$data['slug'] = $data['slug'] ? Str::slug($data['slug']) : Str::slug($data['title']);
|
||||||
$data['published'] = $request->boolean('published');
|
$data['published'] = $request->boolean('published');
|
||||||
$data['show_in_footer'] = $request->boolean('show_in_footer');
|
$data['show_in_footer'] = $request->boolean('show_in_footer');
|
||||||
$data['show_in_nav'] = $request->boolean('show_in_nav');
|
$data['show_in_nav'] = $request->boolean('show_in_nav');
|
||||||
|
$data['layout'] = $request->input('layout', 'default');
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class SettingsController extends Controller
|
|||||||
'bank_name','bank_iban','bank_bic','invoice_payment_days',
|
'bank_name','bank_iban','bank_bic','invoice_payment_days',
|
||||||
'smtp_host','smtp_port','smtp_user','smtp_password','smtp_from_name','smtp_from_email',
|
'smtp_host','smtp_port','smtp_user','smtp_password','smtp_from_name','smtp_from_email',
|
||||||
'cookie_consent','google_analytics','require_customer_approval',
|
'cookie_consent','google_analytics','require_customer_approval',
|
||||||
|
'theme_name',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Mail;
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\EmailTemplate;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
@@ -16,8 +17,71 @@ class OrderConfirmationMail extends Mailable
|
|||||||
|
|
||||||
public function build()
|
public function build()
|
||||||
{
|
{
|
||||||
|
$order = $this->order;
|
||||||
|
$template = EmailTemplate::getByKey('order_confirmation');
|
||||||
|
|
||||||
|
if (!$template) {
|
||||||
return $this
|
return $this
|
||||||
->subject('Bestellbestätigung ' . $this->order->order_number . ' – ' . Setting::get('shop_name'))
|
->subject('Bestellbestätigung ' . $order->order_number . ' – ' . Setting::get('shop_name'))
|
||||||
->view('emails.order_confirmation');
|
->view('emails.order_confirmation');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$itemRows = '';
|
||||||
|
foreach ($order->items as $item) {
|
||||||
|
$variant = $item->variant_label ? ' <small>(' . e($item->variant_label) . ')</small>' : '';
|
||||||
|
$itemRows .= '<tr>'
|
||||||
|
. '<td style="padding:8px;border-bottom:1px solid #eee">' . e($item->product_name) . $variant . '</td>'
|
||||||
|
. '<td style="padding:8px;border-bottom:1px solid #eee">' . $item->quantity . '</td>'
|
||||||
|
. '<td style="padding:8px;border-bottom:1px solid #eee">' . number_format($item->unit_price, 2, ',', '.') . ' €</td>'
|
||||||
|
. '<td style="padding:8px;border-bottom:1px solid #eee">' . number_format($item->line_total, 2, ',', '.') . ' €</td>'
|
||||||
|
. '</tr>';
|
||||||
|
}
|
||||||
|
$itemsTable = '<table style="width:100%;border-collapse:collapse">'
|
||||||
|
. '<thead><tr>'
|
||||||
|
. '<th style="padding:8px;background:#f8fafc;text-align:left">Artikel</th>'
|
||||||
|
. '<th style="padding:8px;background:#f8fafc;text-align:left">Menge</th>'
|
||||||
|
. '<th style="padding:8px;background:#f8fafc;text-align:left">Einzelpreis</th>'
|
||||||
|
. '<th style="padding:8px;background:#f8fafc;text-align:left">Gesamt</th>'
|
||||||
|
. '</tr></thead><tbody>' . $itemRows . '</tbody>'
|
||||||
|
. '<tfoot>'
|
||||||
|
. '<tr><td colspan="3" style="padding:8px">Zwischensumme</td><td style="padding:8px">' . number_format($order->subtotal, 2, ',', '.') . ' €</td></tr>'
|
||||||
|
. '<tr><td colspan="3" style="padding:8px">Versand</td><td style="padding:8px">' . number_format($order->shipping_cost, 2, ',', '.') . ' €</td></tr>'
|
||||||
|
. '<tr style="font-weight:bold"><td colspan="3" style="padding:8px">Gesamt (inkl. MwSt.)</td><td style="padding:8px">' . number_format($order->total, 2, ',', '.') . ' €</td></tr>'
|
||||||
|
. '</tfoot></table>';
|
||||||
|
|
||||||
|
if ($order->payment_method === 'prepayment') {
|
||||||
|
$paymentInfo = '<div style="background:#f8fafc;padding:16px;border-radius:8px;margin:16px 0">'
|
||||||
|
. '<strong>Bitte überweisen Sie den Betrag auf folgendes Konto:</strong><br>'
|
||||||
|
. 'Empfänger: ' . e(Setting::get('shop_owner')) . '<br>'
|
||||||
|
. 'Bank: ' . e(Setting::get('bank_name')) . '<br>'
|
||||||
|
. 'IBAN: ' . e(Setting::get('bank_iban')) . '<br>'
|
||||||
|
. 'BIC: ' . e(Setting::get('bank_bic')) . '<br>'
|
||||||
|
. 'Verwendungszweck: <strong>' . e($order->order_number) . '</strong><br>'
|
||||||
|
. 'Betrag: <strong>' . number_format($order->total, 2, ',', '.') . ' €</strong>'
|
||||||
|
. '</div>';
|
||||||
|
} else {
|
||||||
|
$days = Setting::get('invoice_payment_days', 14);
|
||||||
|
$paymentInfo = '<div style="background:#f8fafc;padding:16px;border-radius:8px;margin:16px 0">'
|
||||||
|
. 'Sie erhalten Ihre Rechnung separat per E-Mail.<br>'
|
||||||
|
. 'Zahlungsziel: ' . $days . ' Tage nach Rechnungserhalt.'
|
||||||
|
. '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$customerName = $order->billingAddress()?->full_name ?? $order->guest_email;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'customer_name' => $customerName,
|
||||||
|
'order_number' => $order->order_number,
|
||||||
|
'order_total' => number_format($order->total, 2, ',', '.'),
|
||||||
|
'payment_info' => $paymentInfo,
|
||||||
|
'order_items_table' => $itemsTable,
|
||||||
|
'shop_name' => Setting::get('shop_name'),
|
||||||
|
'shop_email' => Setting::get('shop_email'),
|
||||||
|
'shop_address' => Setting::get('shop_address'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this
|
||||||
|
->subject($template->renderSubject($data))
|
||||||
|
->html($template->renderBody($data));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Mail;
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\EmailTemplate;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
@@ -16,8 +17,32 @@ class OrderShippedMail extends Mailable
|
|||||||
|
|
||||||
public function build()
|
public function build()
|
||||||
{
|
{
|
||||||
|
$order = $this->order;
|
||||||
|
$template = EmailTemplate::getByKey('order_shipped');
|
||||||
|
|
||||||
|
if (!$template) {
|
||||||
return $this
|
return $this
|
||||||
->subject('Ihre Bestellung wurde versendet – ' . $this->order->order_number)
|
->subject('Ihre Bestellung wurde versendet – ' . $order->order_number)
|
||||||
->view('emails.order_shipped');
|
->view('emails.order_shipped');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$trackingInfo = $order->tracking_code
|
||||||
|
? '<div style="background:#f0fdf4;padding:16px;border-radius:8px;border:1px solid #86efac;margin:16px 0">'
|
||||||
|
. '<strong>Sendungsverfolgung:</strong><br>'
|
||||||
|
. 'Tracking-Nummer: <strong>' . e($order->tracking_code) . '</strong>'
|
||||||
|
. '</div>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'order_number' => $order->order_number,
|
||||||
|
'tracking_code' => $order->tracking_code ?? '',
|
||||||
|
'tracking_info' => $trackingInfo,
|
||||||
|
'shop_name' => Setting::get('shop_name'),
|
||||||
|
'shop_email' => Setting::get('shop_email'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this
|
||||||
|
->subject($template->renderSubject($data))
|
||||||
|
->html($template->renderBody($data));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Mail;
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\EmailTemplate;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
@@ -20,9 +21,29 @@ class PaymentReceivedMail extends Mailable
|
|||||||
|
|
||||||
public function build()
|
public function build()
|
||||||
{
|
{
|
||||||
|
$order = $this->order;
|
||||||
|
$template = EmailTemplate::getByKey('payment_received');
|
||||||
|
|
||||||
|
if (!$template) {
|
||||||
return $this
|
return $this
|
||||||
->subject('Ihre Rechnung – ' . $this->order->order_number . ' – ' . Setting::get('shop_name'))
|
->subject('Ihre Rechnung – ' . $order->order_number . ' – ' . Setting::get('shop_name'))
|
||||||
->view('emails.payment_received')
|
->view('emails.payment_received')
|
||||||
->attachData($this->pdfContent, $this->pdfFilename, ['mime' => 'application/pdf']);
|
->attachData($this->pdfContent, $this->pdfFilename, ['mime' => 'application/pdf']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$customerName = $order->billingAddress()?->full_name ?? $order->guest_email;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'customer_name' => $customerName,
|
||||||
|
'order_number' => $order->order_number,
|
||||||
|
'order_total' => number_format($order->total, 2, ',', '.'),
|
||||||
|
'shop_name' => Setting::get('shop_name'),
|
||||||
|
'shop_email' => Setting::get('shop_email'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this
|
||||||
|
->subject($template->renderSubject($data))
|
||||||
|
->html($template->renderBody($data))
|
||||||
|
->attachData($this->pdfContent, $this->pdfFilename, ['mime' => 'application/pdf']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Mail;
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\EmailTemplate;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Mail\Mailable;
|
use Illuminate\Mail\Mailable;
|
||||||
@@ -18,10 +19,32 @@ class WiderrufsformularMail extends Mailable
|
|||||||
|
|
||||||
public function build()
|
public function build()
|
||||||
{
|
{
|
||||||
$subject = $this->isConfirmation
|
if (!$this->isConfirmation) {
|
||||||
? 'Bestätigung Ihres Widerrufs – ' . Setting::get('shop_name')
|
$subject = 'Neues Widerrufsformular eingegangen';
|
||||||
: 'Neues Widerrufsformular eingegangen';
|
|
||||||
|
|
||||||
return $this->subject($subject)->view('emails.widerruf');
|
return $this->subject($subject)->view('emails.widerruf');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$template = EmailTemplate::getByKey('widerruf_confirmation');
|
||||||
|
|
||||||
|
if (!$template) {
|
||||||
|
return $this
|
||||||
|
->subject('Bestätigung Ihres Widerrufs – ' . Setting::get('shop_name'))
|
||||||
|
->view('emails.widerruf');
|
||||||
|
}
|
||||||
|
|
||||||
|
$vars = [
|
||||||
|
'customer_name' => $this->data['name'],
|
||||||
|
'widerruf_name' => $this->data['name'],
|
||||||
|
'widerruf_email' => $this->data['email'],
|
||||||
|
'widerruf_order_number' => $this->data['order_number'] ?? '—',
|
||||||
|
'widerruf_product' => $this->data['product'],
|
||||||
|
'widerruf_date' => $this->data['date'],
|
||||||
|
'widerruf_reason' => $this->data['reason'] ?? '—',
|
||||||
|
'shop_name' => Setting::get('shop_name'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this
|
||||||
|
->subject($template->renderSubject($vars))
|
||||||
|
->html($template->renderBody($vars));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class EmailTemplate extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['key', 'label', 'subject', 'body', 'variables'];
|
||||||
|
protected $casts = ['variables' => 'array'];
|
||||||
|
|
||||||
|
public static function getByKey(string $key): ?self
|
||||||
|
{
|
||||||
|
return static::where('key', $key)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderBody(array $data): string
|
||||||
|
{
|
||||||
|
$body = $this->body;
|
||||||
|
foreach ($data as $k => $v) {
|
||||||
|
$body = str_replace('{{' . $k . '}}', (string) $v, $body);
|
||||||
|
}
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderSubject(array $data): string
|
||||||
|
{
|
||||||
|
$subject = $this->subject;
|
||||||
|
foreach ($data as $k => $v) {
|
||||||
|
$subject = str_replace('{{' . $k . '}}', (string) $v, $subject);
|
||||||
|
}
|
||||||
|
return $subject;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
|
|
||||||
class Page extends Model
|
class Page extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = ['slug','title','content','type','image','meta_title','meta_description','published','published_at','show_in_footer','show_in_nav','nav_sort'];
|
protected $fillable = ['slug','title','content','type','image','meta_title','meta_description','published','published_at','show_in_footer','show_in_nav','nav_sort','layout'];
|
||||||
|
|
||||||
protected $casts = ['published' => 'boolean', 'published_at' => 'datetime', 'show_in_footer' => 'boolean', 'show_in_nav' => 'boolean'];
|
protected $casts = ['published' => 'boolean', 'published_at' => 'datetime', 'show_in_footer' => 'boolean', 'show_in_nav' => 'boolean'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'default' => [
|
||||||
|
'name' => 'Blau (Standard)',
|
||||||
|
'primary' => '#1d4ed8',
|
||||||
|
'dark' => '#1e40af',
|
||||||
|
'light' => '#eff6ff',
|
||||||
|
'text' => '#1e40af',
|
||||||
|
],
|
||||||
|
'green' => [
|
||||||
|
'name' => 'Grün',
|
||||||
|
'primary' => '#16a34a',
|
||||||
|
'dark' => '#15803d',
|
||||||
|
'light' => '#f0fdf4',
|
||||||
|
'text' => '#15803d',
|
||||||
|
],
|
||||||
|
'red' => [
|
||||||
|
'name' => 'Rot',
|
||||||
|
'primary' => '#dc2626',
|
||||||
|
'dark' => '#b91c1c',
|
||||||
|
'light' => '#fef2f2',
|
||||||
|
'text' => '#b91c1c',
|
||||||
|
],
|
||||||
|
'purple' => [
|
||||||
|
'name' => 'Lila',
|
||||||
|
'primary' => '#7c3aed',
|
||||||
|
'dark' => '#6d28d9',
|
||||||
|
'light' => '#f5f3ff',
|
||||||
|
'text' => '#6d28d9',
|
||||||
|
],
|
||||||
|
'orange' => [
|
||||||
|
'name' => 'Orange',
|
||||||
|
'primary' => '#ea580c',
|
||||||
|
'dark' => '#c2410c',
|
||||||
|
'light' => '#fff7ed',
|
||||||
|
'text' => '#c2410c',
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('email_templates', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('key')->unique();
|
||||||
|
$table->string('label');
|
||||||
|
$table->string('subject');
|
||||||
|
$table->longText('body');
|
||||||
|
$table->json('variables')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('email_templates');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('pages', function (Blueprint $table) {
|
||||||
|
$table->string('layout')->default('default')->after('show_in_footer');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('pages', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('layout');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -11,6 +11,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
$this->call([
|
$this->call([
|
||||||
SettingsSeeder::class,
|
SettingsSeeder::class,
|
||||||
AdminUserSeeder::class,
|
AdminUserSeeder::class,
|
||||||
|
EmailTemplateSeeder::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\EmailTemplate;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class EmailTemplateSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$templates = [
|
||||||
|
[
|
||||||
|
'key' => 'order_confirmation',
|
||||||
|
'label' => 'Bestellbestätigung',
|
||||||
|
'subject' => 'Bestellbestätigung {{order_number}} – {{shop_name}}',
|
||||||
|
'variables' => ['customer_name','order_number','order_total','payment_info','order_items_table','shop_name','shop_email','shop_address','shop_owner','bank_name','bank_iban','bank_bic','invoice_payment_days'],
|
||||||
|
'body' => <<<'HTML'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head><meta charset="UTF-8"><style>
|
||||||
|
body { font-family: Arial, sans-serif; color: #333; max-width: 600px; margin: 0 auto; }
|
||||||
|
h1 { color: #1d4ed8; }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
th, td { padding: 8px; border-bottom: 1px solid #eee; text-align: left; }
|
||||||
|
th { background: #f8fafc; }
|
||||||
|
.total { font-weight: bold; font-size: 1.1em; }
|
||||||
|
.box { background: #f8fafc; padding: 16px; border-radius: 8px; margin: 16px 0; }
|
||||||
|
</style></head>
|
||||||
|
<body>
|
||||||
|
<h1>Vielen Dank für Ihre Bestellung!</h1>
|
||||||
|
<p>Guten Tag {{customer_name}},</p>
|
||||||
|
<p>wir haben Ihre Bestellung <strong>{{order_number}}</strong> erhalten und bestätigen sie hiermit.</p>
|
||||||
|
|
||||||
|
{{payment_info}}
|
||||||
|
|
||||||
|
<h2>Bestellübersicht</h2>
|
||||||
|
{{order_items_table}}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<p style="font-size:0.85em;color:#666;">
|
||||||
|
{{shop_name}} | {{shop_address}}<br>
|
||||||
|
Bei Fragen: <a href="mailto:{{shop_email}}">{{shop_email}}</a>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'order_shipped',
|
||||||
|
'label' => 'Versandbestätigung',
|
||||||
|
'subject' => 'Ihre Bestellung {{order_number}} ist unterwegs',
|
||||||
|
'variables' => ['order_number','tracking_code','shop_name','shop_email'],
|
||||||
|
'body' => <<<'HTML'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head><meta charset="UTF-8"><style>
|
||||||
|
body { font-family: Arial, sans-serif; color: #333; max-width: 600px; margin: 0 auto; }
|
||||||
|
h1 { color: #1d4ed8; }
|
||||||
|
.box { background: #f0fdf4; padding: 16px; border-radius: 8px; border: 1px solid #86efac; }
|
||||||
|
</style></head>
|
||||||
|
<body>
|
||||||
|
<h1>Ihre Bestellung ist unterwegs!</h1>
|
||||||
|
<p>Ihre Bestellung <strong>{{order_number}}</strong> wurde versendet.</p>
|
||||||
|
{{tracking_info}}
|
||||||
|
<p>Bei Fragen zu Ihrer Lieferung können Sie uns jederzeit kontaktieren.</p>
|
||||||
|
<hr>
|
||||||
|
<p style="font-size:0.85em;color:#666;">{{shop_name}} | {{shop_email}}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'payment_received',
|
||||||
|
'label' => 'Zahlungsbestätigung',
|
||||||
|
'subject' => 'Zahlung eingegangen – Bestellung {{order_number}}',
|
||||||
|
'variables' => ['customer_name','order_number','order_total','shop_name','shop_email'],
|
||||||
|
'body' => <<<'HTML'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head><meta charset="UTF-8"><style>
|
||||||
|
body { font-family: Arial, sans-serif; color: #333; max-width: 600px; margin: 0 auto; }
|
||||||
|
h1 { color: #16a34a; }
|
||||||
|
</style></head>
|
||||||
|
<body>
|
||||||
|
<h1>Zahlung eingegangen</h1>
|
||||||
|
<p>Guten Tag {{customer_name}},</p>
|
||||||
|
<p>Ihre Zahlung für Bestellung <strong>{{order_number}}</strong> ({{order_total}} €) ist eingegangen.</p>
|
||||||
|
<p>Im Anhang finden Sie Ihre Rechnung als PDF.</p>
|
||||||
|
<p>Wir werden Ihre Bestellung schnellstmöglich bearbeiten und versenden.</p>
|
||||||
|
<hr>
|
||||||
|
<p style="font-size:0.85em;color:#666;">{{shop_name}} | {{shop_email}}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'widerruf_confirmation',
|
||||||
|
'label' => 'Widerrufsbestätigung',
|
||||||
|
'subject' => 'Bestätigung Ihres Widerrufs',
|
||||||
|
'variables' => ['customer_name','widerruf_name','widerruf_email','widerruf_order_number','widerruf_product','widerruf_date','widerruf_reason','shop_name'],
|
||||||
|
'body' => <<<'HTML'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head><meta charset="UTF-8"><style>
|
||||||
|
body { font-family: Arial, sans-serif; color: #333; max-width: 600px; margin: 0 auto; }
|
||||||
|
</style></head>
|
||||||
|
<body>
|
||||||
|
<h2>Bestätigung Ihres Widerrufs</h2>
|
||||||
|
<p>Guten Tag {{customer_name}},</p>
|
||||||
|
<p>Ihr Widerruf ist bei uns eingegangen. Wir werden ihn schnellstmöglich bearbeiten.</p>
|
||||||
|
|
||||||
|
<table style="border-collapse:collapse;width:100%">
|
||||||
|
<tr><td style="padding:6px;font-weight:bold">Name</td><td style="padding:6px">{{widerruf_name}}</td></tr>
|
||||||
|
<tr><td style="padding:6px;font-weight:bold">E-Mail</td><td style="padding:6px">{{widerruf_email}}</td></tr>
|
||||||
|
<tr><td style="padding:6px;font-weight:bold">Bestellnummer</td><td style="padding:6px">{{widerruf_order_number}}</td></tr>
|
||||||
|
<tr><td style="padding:6px;font-weight:bold">Produkt</td><td style="padding:6px">{{widerruf_product}}</td></tr>
|
||||||
|
<tr><td style="padding:6px;font-weight:bold">Datum</td><td style="padding:6px">{{widerruf_date}}</td></tr>
|
||||||
|
<tr><td style="padding:6px;font-weight:bold">Begründung</td><td style="padding:6px">{{widerruf_reason}}</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="font-size:0.85em;color:#666;">{{shop_name}}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($templates as $tpl) {
|
||||||
|
EmailTemplate::updateOrCreate(['key' => $tpl['key']], $tpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title', 'E-Mail-Template: ' . $template->label)
|
||||||
|
@section('content')
|
||||||
|
<div class="flex items-center gap-3 mb-6">
|
||||||
|
<a href="{{ route('admin.email-templates.index') }}" class="text-gray-400 hover:text-gray-600">← Zurück</a>
|
||||||
|
<h2 class="text-xl font-bold">{{ $template->label }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="bg-green-100 border border-green-300 text-green-800 px-4 py-3 rounded mb-4">{{ session('success') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('admin.email-templates.update', $template) }}" class="space-y-5">
|
||||||
|
@csrf @method('PUT')
|
||||||
|
|
||||||
|
<div class="bg-white rounded shadow p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Betreff</label>
|
||||||
|
<input type="text" name="subject" value="{{ old('subject', $template->subject) }}"
|
||||||
|
class="w-full border rounded px-3 py-2 text-sm @error('subject') border-red-500 @enderror">
|
||||||
|
@error('subject')<p class="text-red-600 text-xs mt-1">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">HTML-Inhalt</label>
|
||||||
|
<textarea name="body" rows="24"
|
||||||
|
class="w-full border rounded px-3 py-2 text-sm font-mono @error('body') border-red-500 @enderror">{{ old('body', $template->body) }}</textarea>
|
||||||
|
@error('body')<p class="text-red-600 text-xs mt-1">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($template->variables)
|
||||||
|
<div class="bg-blue-50 border border-blue-200 rounded p-4 text-sm">
|
||||||
|
<p class="font-medium text-blue-800 mb-2">Verfügbare Platzhalter:</p>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@foreach($template->variables as $var)
|
||||||
|
<code class="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs">{{"{{"}}{{ $var }}{{"}}"}}</code>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button type="submit" class="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700">Speichern</button>
|
||||||
|
<a href="{{ route('admin.email-templates.index') }}" class="border px-6 py-2 rounded hover:bg-gray-50 text-sm">Abbrechen</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title', 'E-Mail-Templates')
|
||||||
|
@section('content')
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h2 class="text-xl font-bold">E-Mail-Templates</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="bg-green-100 border border-green-300 text-green-800 px-4 py-3 rounded mb-4">{{ session('success') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($templates->isEmpty())
|
||||||
|
<div class="bg-yellow-50 border border-yellow-200 text-yellow-800 px-4 py-3 rounded">
|
||||||
|
Keine Templates gefunden. Bitte führen Sie <code>php artisan db:seed --class=EmailTemplateSeeder</code> aus.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="bg-white rounded shadow overflow-hidden">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-gray-50 text-gray-600 uppercase text-xs">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left">Template</th>
|
||||||
|
<th class="px-4 py-3 text-left">Betreff</th>
|
||||||
|
<th class="px-4 py-3 text-left">Zuletzt geändert</th>
|
||||||
|
<th class="px-4 py-3"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100">
|
||||||
|
@foreach($templates as $tpl)
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-3 font-medium">{{ $tpl->label }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-600 max-w-xs truncate">{{ $tpl->subject }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-500">{{ $tpl->updated_at->format('d.m.Y H:i') }}</td>
|
||||||
|
<td class="px-4 py-3 text-right">
|
||||||
|
<a href="{{ route('admin.email-templates.edit', $tpl) }}"
|
||||||
|
class="text-blue-600 hover:underline">Bearbeiten</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endsection
|
||||||
@@ -152,6 +152,14 @@
|
|||||||
class="w-full border rounded px-3 py-2 text-sm" min="0">
|
class="w-full border rounded px-3 py-2 text-sm" min="0">
|
||||||
<p class="text-xs text-gray-400 mt-1">Niedrigere Zahl = weiter links</p>
|
<p class="text-xs text-gray-400 mt-1">Niedrigere Zahl = weiter links</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Seiten-Layout</label>
|
||||||
|
<select name="layout" class="w-full border rounded px-3 py-2 text-sm">
|
||||||
|
@foreach(['default'=>'Standard (zentriert)','sidebar'=>'Mit Sidebar','full'=>'Volle Breite','landing'=>'Landing Page (ohne Header/Footer)'] as $val => $label)
|
||||||
|
<option value="{{ $val }}" {{ old('layout', $page->layout ?? 'default') === $val ? 'selected' : '' }}>{{ $label }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
<a href="{{ route('admin.faqs.index') }}" class="block px-3 py-2 rounded hover:bg-gray-700 {{ request()->routeIs('admin.faqs.*') ? 'bg-gray-700' : '' }}">FAQ</a>
|
<a href="{{ route('admin.faqs.index') }}" class="block px-3 py-2 rounded hover:bg-gray-700 {{ request()->routeIs('admin.faqs.*') ? 'bg-gray-700' : '' }}">FAQ</a>
|
||||||
<div class="pt-2 pb-1 text-xs text-gray-400 uppercase tracking-wide">System</div>
|
<div class="pt-2 pb-1 text-xs text-gray-400 uppercase tracking-wide">System</div>
|
||||||
<a href="{{ route('admin.admins.index') }}" class="block px-3 py-2 rounded hover:bg-gray-700 {{ request()->routeIs('admin.admins.*') ? 'bg-gray-700' : '' }}">Admin-Benutzer</a>
|
<a href="{{ route('admin.admins.index') }}" class="block px-3 py-2 rounded hover:bg-gray-700 {{ request()->routeIs('admin.admins.*') ? 'bg-gray-700' : '' }}">Admin-Benutzer</a>
|
||||||
|
<a href="{{ route('admin.email-templates.index') }}" class="block px-3 py-2 rounded hover:bg-gray-700 {{ request()->routeIs('admin.email-templates.*') ? 'bg-gray-700' : '' }}">E-Mail-Templates</a>
|
||||||
<a href="{{ route('admin.settings.index') }}" class="block px-3 py-2 rounded hover:bg-gray-700 {{ request()->routeIs('admin.settings.*') ? 'bg-gray-700' : '' }}">Einstellungen</a>
|
<a href="{{ route('admin.settings.index') }}" class="block px-3 py-2 rounded hover:bg-gray-700 {{ request()->routeIs('admin.settings.*') ? 'bg-gray-700' : '' }}">Einstellungen</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="p-4 border-t border-gray-700 text-xs text-gray-400">
|
<div class="p-4 border-t border-gray-700 text-xs text-gray-400">
|
||||||
|
|||||||
@@ -6,6 +6,33 @@
|
|||||||
<title>@yield('title', \App\Models\Setting::get('shop_name', 'Shop'))</title>
|
<title>@yield('title', \App\Models\Setting::get('shop_name', 'Shop'))</title>
|
||||||
<meta name="description" content="@yield('meta_description', '')">
|
<meta name="description" content="@yield('meta_description', '')">
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
@php
|
||||||
|
$themes = config('themes');
|
||||||
|
$themeName = \App\Models\Setting::get('theme_name', 'default');
|
||||||
|
$theme = $themes[$themeName] ?? $themes['default'];
|
||||||
|
@endphp
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: {{ $theme['primary'] }};
|
||||||
|
--primary-dark: {{ $theme['dark'] }};
|
||||||
|
--primary-light: {{ $theme['light'] }};
|
||||||
|
--primary-text: {{ $theme['text'] }};
|
||||||
|
}
|
||||||
|
.btn-primary { background-color: var(--primary); color: #fff; }
|
||||||
|
.btn-primary:hover { background-color: var(--primary-dark); }
|
||||||
|
.text-primary { color: var(--primary); }
|
||||||
|
.bg-primary { background-color: var(--primary); }
|
||||||
|
.bg-primary-dark { background-color: var(--primary-dark); }
|
||||||
|
.bg-primary-light { background-color: var(--primary-light); }
|
||||||
|
.border-primary { border-color: var(--primary); }
|
||||||
|
.hover-primary:hover { background-color: var(--primary-light); color: var(--primary-text); }
|
||||||
|
.cat-bar { background-color: var(--primary); }
|
||||||
|
.cat-bar a { color: rgba(255,255,255,0.85); }
|
||||||
|
.cat-bar a:hover { background-color: var(--primary-dark); color: #fff; }
|
||||||
|
.cat-bar a.active { background-color: var(--primary-dark); color: #fff; }
|
||||||
|
.cart-badge { color: var(--primary); }
|
||||||
|
.shop-name-text { color: var(--primary); }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50 text-gray-800 flex flex-col min-h-screen">
|
<body class="bg-gray-50 text-gray-800 flex flex-col min-h-screen">
|
||||||
|
|
||||||
@@ -15,7 +42,7 @@
|
|||||||
<p class="text-sm">Wir verwenden Cookies, um Ihnen die bestmögliche Nutzererfahrung zu bieten. Mit dem Klick auf „Akzeptieren" stimmen Sie der Verwendung aller Cookies zu. <a href="{{ route('page', 'datenschutz') }}" class="underline">Mehr erfahren</a></p>
|
<p class="text-sm">Wir verwenden Cookies, um Ihnen die bestmögliche Nutzererfahrung zu bieten. Mit dem Klick auf „Akzeptieren" stimmen Sie der Verwendung aller Cookies zu. <a href="{{ route('page', 'datenschutz') }}" class="underline">Mehr erfahren</a></p>
|
||||||
<div class="flex gap-2 shrink-0">
|
<div class="flex gap-2 shrink-0">
|
||||||
<button onclick="setCookieConsent(false)" class="px-4 py-2 text-sm border border-gray-500 rounded hover:bg-gray-700">Ablehnen</button>
|
<button onclick="setCookieConsent(false)" class="px-4 py-2 text-sm border border-gray-500 rounded hover:bg-gray-700">Ablehnen</button>
|
||||||
<button onclick="setCookieConsent(true)" class="px-4 py-2 text-sm bg-blue-500 rounded hover:bg-blue-600">Akzeptieren</button>
|
<button onclick="setCookieConsent(true)" class="px-4 py-2 text-sm btn-primary rounded">Akzeptieren</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@@ -44,56 +71,56 @@
|
|||||||
<header class="bg-white border-b shadow-sm">
|
<header class="bg-white border-b shadow-sm">
|
||||||
<div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between gap-4">
|
<div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between gap-4">
|
||||||
|
|
||||||
{{-- Logo: Bild (wenn hinterlegt) oder Shop-Name als Text --}}
|
{{-- Logo --}}
|
||||||
<a href="{{ url('/') }}" class="shrink-0 flex items-center">
|
<a href="{{ url('/') }}" class="shrink-0 flex items-center">
|
||||||
@php $logo = \App\Models\Setting::get('shop_logo'); @endphp
|
@php $logo = \App\Models\Setting::get('shop_logo'); @endphp
|
||||||
@if($logo)
|
@if($logo)
|
||||||
<img src="{{ asset('storage/'.$logo) }}" alt="{{ \App\Models\Setting::get('shop_name', 'Shop') }}" class="h-12 w-auto object-contain">
|
<img src="{{ asset('storage/'.$logo) }}" alt="{{ \App\Models\Setting::get('shop_name', 'Shop') }}" class="h-12 w-auto object-contain">
|
||||||
@else
|
@else
|
||||||
<span class="text-2xl font-bold text-blue-700 tracking-tight">
|
<span class="text-2xl font-bold tracking-tight shop-name-text">
|
||||||
{{ \App\Models\Setting::get('shop_name', 'Shop') }}
|
{{ \App\Models\Setting::get('shop_name', 'Shop') }}
|
||||||
</span>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{{-- Haupt-Navigation rechts --}}
|
{{-- Haupt-Navigation --}}
|
||||||
<nav class="flex items-center gap-1">
|
<nav class="flex items-center gap-1">
|
||||||
<a href="{{ route('shop.products.index') }}"
|
<a href="{{ route('shop.products.index') }}"
|
||||||
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
|
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover-primary transition">
|
||||||
Alle Produkte
|
Alle Produkte
|
||||||
</a>
|
</a>
|
||||||
@foreach($navCategories as $navCat)
|
@foreach($navCategories as $navCat)
|
||||||
<a href="{{ route('shop.category', $navCat->slug) }}"
|
<a href="{{ route('shop.category', $navCat->slug) }}"
|
||||||
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition
|
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover-primary transition
|
||||||
{{ request()->route('slug') === $navCat->slug ? 'bg-blue-50 text-blue-700' : '' }}">
|
{{ request()->route('slug') === $navCat->slug ? 'bg-primary-light text-primary-text' : '' }}">
|
||||||
{{ $navCat->name }}
|
{{ $navCat->name }}
|
||||||
</a>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
@foreach($navPages as $navPage)
|
@foreach($navPages as $navPage)
|
||||||
<a href="{{ route('page', $navPage->slug) }}"
|
<a href="{{ route('page', $navPage->slug) }}"
|
||||||
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition
|
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover-primary transition
|
||||||
{{ request()->route('slug') === $navPage->slug ? 'bg-blue-50 text-blue-700' : '' }}">
|
{{ request()->route('slug') === $navPage->slug ? 'bg-primary-light text-primary-text' : '' }}">
|
||||||
{{ $navPage->title }}
|
{{ $navPage->title }}
|
||||||
</a>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
@auth
|
@auth
|
||||||
<a href="{{ route('account.index') }}"
|
<a href="{{ route('account.index') }}"
|
||||||
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
|
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover-primary transition">
|
||||||
Mein Konto
|
Mein Konto
|
||||||
</a>
|
</a>
|
||||||
@if(auth()->user()->isAdmin())
|
@if(auth()->user()->isAdmin())
|
||||||
<a href="{{ route('admin.dashboard') }}"
|
<a href="{{ route('admin.dashboard') }}"
|
||||||
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
|
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover-primary transition">
|
||||||
Admin
|
Admin
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
<a href="{{ route('login') }}"
|
<a href="{{ route('login') }}"
|
||||||
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
|
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover-primary transition">
|
||||||
Anmelden
|
Anmelden
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('register') }}"
|
<a href="{{ route('register') }}"
|
||||||
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
|
class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover-primary transition">
|
||||||
Registrieren
|
Registrieren
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
@endauth
|
||||||
@@ -108,14 +135,14 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="{{ route('cart.index') }}"
|
<a href="{{ route('cart.index') }}"
|
||||||
class="relative ml-2 px-3 py-2 rounded-lg text-sm font-medium bg-blue-700 text-white hover:bg-blue-800 transition flex items-center gap-1.5">
|
class="relative ml-2 px-3 py-2 rounded-lg text-sm font-medium btn-primary transition flex items-center gap-1.5">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Warenkorb
|
Warenkorb
|
||||||
@php $cartCount = array_sum(array_column(session('cart', []), 'qty')); @endphp
|
@php $cartCount = array_sum(array_column(session('cart', []), 'qty')); @endphp
|
||||||
@if($cartCount > 0)
|
@if($cartCount > 0)
|
||||||
<span class="bg-white text-blue-700 text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center leading-none">{{ $cartCount }}</span>
|
<span class="bg-white cart-badge text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center leading-none">{{ $cartCount }}</span>
|
||||||
@endif
|
@endif
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -124,12 +151,12 @@
|
|||||||
{{-- Kategorien-Navigation --}}
|
{{-- Kategorien-Navigation --}}
|
||||||
@php $navCats = \App\Models\Category::whereNull('parent_id')->where('active', true)->orderBy('sort_order')->get(); @endphp
|
@php $navCats = \App\Models\Category::whereNull('parent_id')->where('active', true)->orderBy('sort_order')->get(); @endphp
|
||||||
@if($navCats->count())
|
@if($navCats->count())
|
||||||
<div class="bg-blue-700">
|
<div class="cat-bar">
|
||||||
<div class="max-w-6xl mx-auto px-4 flex gap-1 overflow-x-auto">
|
<div class="max-w-6xl mx-auto px-4 flex gap-1 overflow-x-auto">
|
||||||
@foreach($navCats as $cat)
|
@foreach($navCats as $cat)
|
||||||
<a href="{{ route('shop.category', $cat->slug) }}"
|
<a href="{{ route('shop.category', $cat->slug) }}"
|
||||||
class="whitespace-nowrap px-4 py-2.5 text-sm font-medium text-blue-100 hover:text-white hover:bg-blue-600 transition
|
class="whitespace-nowrap px-4 py-2.5 text-sm font-medium transition
|
||||||
{{ request()->route('slug') === $cat->slug ? 'bg-blue-800 text-white' : '' }}">
|
{{ request()->route('slug') === $cat->slug ? 'active' : '' }}">
|
||||||
{{ $cat->name }}
|
{{ $cat->name }}
|
||||||
</a>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|||||||
@@ -1,11 +1,59 @@
|
|||||||
|
@php $layout = $page->layout ?? 'default'; @endphp
|
||||||
|
|
||||||
|
@if($layout === 'landing')
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ $page->meta_title ?: $page->title }}</title>
|
||||||
|
<meta name="description" content="{{ $page->meta_description }}">
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 text-gray-800">
|
||||||
|
<div class="prose max-w-none text-gray-700 p-8">
|
||||||
|
{!! $page->content !!}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
@else
|
||||||
@extends('layouts.shop')
|
@extends('layouts.shop')
|
||||||
@section('title', $page->meta_title ?: $page->title)
|
@section('title', $page->meta_title ?: $page->title)
|
||||||
@section('meta_description', $page->meta_description)
|
@section('meta_description', $page->meta_description)
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|
||||||
|
@switch($layout)
|
||||||
|
|
||||||
|
@case('full')
|
||||||
|
<h1 class="text-3xl font-bold mb-6">{{ $page->title }}</h1>
|
||||||
|
<div class="prose max-w-none text-gray-700">
|
||||||
|
{!! $page->content !!}
|
||||||
|
</div>
|
||||||
|
@break
|
||||||
|
|
||||||
|
@case('sidebar')
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<h1 class="text-3xl font-bold mb-6">{{ $page->title }}</h1>
|
||||||
|
<div class="prose max-w-none text-gray-700">
|
||||||
|
{!! $page->content !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<aside class="space-y-6">
|
||||||
|
@include('shop.partials._sidebar')
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
@break
|
||||||
|
|
||||||
|
@default
|
||||||
<div class="max-w-3xl mx-auto">
|
<div class="max-w-3xl mx-auto">
|
||||||
<h1 class="text-3xl font-bold mb-6">{{ $page->title }}</h1>
|
<h1 class="text-3xl font-bold mb-6">{{ $page->title }}</h1>
|
||||||
<div class="prose max-w-none text-gray-700">
|
<div class="prose max-w-none text-gray-700">
|
||||||
{!! $page->content !!}
|
{!! $page->content !!}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@endswitch
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
@endif
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{{-- Kategorien --}}
|
||||||
|
@php
|
||||||
|
$sidebarCats = \App\Models\Category::whereNull('parent_id')->where('active', true)->orderBy('sort_order')->get();
|
||||||
|
$sidebarBlogPages = \App\Models\Page::where('type','blog')->where('published',true)->orderByDesc('published_at')->limit(5)->get();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if($sidebarCats->count())
|
||||||
|
<div class="bg-white rounded shadow p-4">
|
||||||
|
<h3 class="font-semibold mb-3 text-gray-800">Kategorien</h3>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
@foreach($sidebarCats as $cat)
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('shop.category', $cat->slug) }}"
|
||||||
|
class="block px-2 py-1.5 rounded text-sm text-gray-700 hover-primary transition">
|
||||||
|
{{ $cat->name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($sidebarBlogPages->count())
|
||||||
|
<div class="bg-white rounded shadow p-4">
|
||||||
|
<h3 class="font-semibold mb-3 text-gray-800">Aktuelle Beiträge</h3>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
@foreach($sidebarBlogPages as $bp)
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('page', $bp->slug) }}" class="text-sm text-primary hover:underline">
|
||||||
|
{{ $bp->title }}
|
||||||
|
</a>
|
||||||
|
@if($bp->published_at)
|
||||||
|
<p class="text-xs text-gray-400">{{ $bp->published_at->format('d.m.Y') }}</p>
|
||||||
|
@endif
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@@ -105,6 +105,11 @@ Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(fun
|
|||||||
Route::put('admins/{admin}', [Admin\AdminUserController::class, 'update'])->name('admins.update');
|
Route::put('admins/{admin}', [Admin\AdminUserController::class, 'update'])->name('admins.update');
|
||||||
Route::delete('admins/{admin}', [Admin\AdminUserController::class, 'destroy'])->name('admins.destroy');
|
Route::delete('admins/{admin}', [Admin\AdminUserController::class, 'destroy'])->name('admins.destroy');
|
||||||
|
|
||||||
|
// E-Mail-Templates
|
||||||
|
Route::get('email-templates', [Admin\EmailTemplateController::class, 'index'])->name('email-templates.index');
|
||||||
|
Route::get('email-templates/{emailTemplate}/edit', [Admin\EmailTemplateController::class, 'edit'])->name('email-templates.edit');
|
||||||
|
Route::put('email-templates/{emailTemplate}', [Admin\EmailTemplateController::class, 'update'])->name('email-templates.update');
|
||||||
|
|
||||||
// Einstellungen
|
// Einstellungen
|
||||||
Route::get('settings', [Admin\SettingsController::class, 'index'])->name('settings.index');
|
Route::get('settings', [Admin\SettingsController::class, 'index'])->name('settings.index');
|
||||||
Route::put('settings', [Admin\SettingsController::class, 'update'])->name('settings.update');
|
Route::put('settings', [Admin\SettingsController::class, 'update'])->name('settings.update');
|
||||||
|
|||||||
Reference in New Issue
Block a user