integration vom Blog-plugin, hübschere webseite,

This commit is contained in:
Codex Agent
2025-09-26 17:41:17 +02:00
parent 492b9b9fd1
commit 6fc36ebaf4
31 changed files with 22823 additions and 96 deletions

View File

@@ -292,7 +292,7 @@ class EventPublicController extends BaseController
}
if ($since) {
$query->where('created_at', '>', $since);
$query->where('photos.created_at', '>', $since);
}
$locale = request()->query('locale', 'de');

View File

@@ -127,6 +127,34 @@ class MarketingController extends Controller
return view('marketing.success', ['provider' => 'Stripe']);
}
public function blogIndex(Request $request)
{
$locale = $request->get('locale', app()->getLocale());
$posts = \Stephenjude\FilamentBlog\Models\Post::query()
->where('is_published', true)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->whereJsonContains("translations->locale->title->{$locale}", true)
->orderBy('published_at', 'desc')
->paginate(8);
return view('marketing.blog', compact('posts'));
}
public function blogShow($slug)
{
$locale = app()->getLocale();
$post = \Stephenjude\FilamentBlog\Models\Post::query()
->where('slug', $slug)
->where('is_published', true)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->whereJsonContains("translations->locale->title->{$locale}", true)
->firstOrFail();
return view('marketing.blog-show', compact('post'));
}
public function paypalCheckout(Request $request, $package)
{
$packages = [

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$locale = $request->segment(1);
$supportedLocales = ['de', 'en'];
if (in_array($locale, $supportedLocales)) {
app()->setLocale($locale);
session()->put('locale', $locale);
} else {
$locale = session('locale', config('app.locale', 'de'));
app()->setLocale($locale);
}
return $next($request);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class SuperAdminAuth
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->is('super-admin/login') || $request->is('super-admin/register')) {
return $next($request);
}
if (!Auth::check()) {
abort(403, 'Nicht angemeldet.');
}
$user = Auth::user();
Log::info('SuperAdminAuth: User ID ' . $user->id . ', role: ' . $user->role);
if ($user->role !== 'super_admin') {
abort(403, 'Zugriff nur für SuperAdmin. User ID: ' . $user->id . ', Role: ' . $user->role);
}
return $next($request);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Translatable\HasTranslations;
use Stephenjude\FilamentBlog\Models\Category as BaseCategory;
class BlogCategory extends BaseCategory
{
use HasFactory, SoftDeletes, HasTranslations;
protected $translatable = [
'name',
'description',
];
protected $fillable = [
'slug',
'is_visible',
'translations',
];
}

32
app/Models/BlogPost.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Translatable\HasTranslations;
use Stephenjude\FilamentBlog\Models\Post as BasePost;
class BlogPost extends BasePost
{
use HasFactory, SoftDeletes, HasTranslations;
protected $translatable = [
'title',
'excerpt',
'content',
'meta_title',
'meta_description',
];
protected $fillable = [
'blog_author_id',
'blog_category_id',
'slug',
'banner',
'published_at',
'is_published',
'translations',
];
}

23
app/Models/BlogTag.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Translatable\HasTranslations;
use Stephenjude\FilamentBlog\Models\Tag as BaseTag;
class BlogTag extends BaseTag
{
use HasFactory, SoftDeletes, HasTranslations;
protected $translatable = [
'name',
];
protected $fillable = [
'slug',
'translations',
];
}

View File

@@ -35,5 +35,9 @@ class AppServiceProvider extends ServiceProvider
RateLimiter::for('oauth', function (Request $request) {
return Limit::perMinute(10)->by('oauth:' . ($request->ip() ?? 'unknown'));
});
if ($this->app->runningInConsole()) {
$this->app->register(\App\Providers\Filament\AdminPanelProvider::class);
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Stephenjude\FilamentBlog\Filament\Resources\CategoryResource;
use Stephenjude\FilamentBlog\Filament\Resources\PostResource;
use Stephenjude\FilamentBlog\Filament\Resources\TagResource;
use App\Models\BlogCategory;
use App\Models\BlogPost;
use App\Models\BlogTag;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->colors([
'primary' => Color::Pink,
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
])
->resources([
// Blog-Resources moved to SuperAdminPanel
])
// Remove blog models as they are global and handled in SuperAdmin
;
}
}

View File

@@ -3,21 +3,33 @@
namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\AuthenticateSession;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages\Dashboard;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets\AccountWidget;
use Filament\Widgets\FilamentInfoWidget;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use App\Filament\Resources\LegalPageResource;
use App\Filament\Resources\TenantResource;
use App\Filament\Pages\SuperAdminProfile;
use App\Models\User;
use App\Models\Tenant;
use App\Models\BlogPost;
use App\Models\BlogCategory;
use App\Models\BlogTag;
use App\Filament\Widgets\PlatformStatsWidget;
use App\Filament\Widgets\TopTenantsByUploads;
use Stephenjude\FilamentBlog\Filament\Resources\CategoryResource;
use Stephenjude\FilamentBlog\Filament\Resources\PostResource;
use Stephenjude\FilamentBlog\BlogPlugin;
class SuperAdminPanelProvider extends PanelProvider
{
@@ -25,26 +37,27 @@ class SuperAdminPanelProvider extends PanelProvider
{
return $panel
->default()
->id('super-admin')
->id('superadmin')
->path('super-admin')
->profile(\App\Filament\Pages\SuperAdminProfile::class, true)
->login()
->colors([
'primary' => Color::Amber,
'primary' => Color::Pink,
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
Dashboard::class,
Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets')
->plugin(
BlogPlugin::make()
)
->profile(SuperAdminProfile::class)
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
AccountWidget::class,
FilamentInfoWidget::class,
\App\Filament\Widgets\PlatformStatsWidget::class,
\App\Filament\Widgets\UploadsPerDayChart::class,
\App\Filament\Widgets\RecentPhotosTable::class,
\App\Filament\Widgets\TopTenantsByUploads::class,
\App\Filament\Widgets\EventsActiveToday::class,
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
PlatformStatsWidget::class,
TopTenantsByUploads::class,
])
->middleware([
EncryptCookies::class,
@@ -59,6 +72,18 @@ class SuperAdminPanelProvider extends PanelProvider
])
->authMiddleware([
Authenticate::class,
]);
])
->resources([
TenantResource::class,
LegalPageResource::class,
])
->authMiddleware([
Authenticate::class,
'superadmin.auth',
])
->authGuard('web')
// SuperAdmin-Zugriff durch custom Middleware, globale Sichtbarkeit ohne Tenant-Isolation
// Blog-Resources werden durch das Plugin-ServiceProvider automatisch registriert
;
}
}

View File

@@ -23,11 +23,14 @@ return Application::configure(basePath: dirname(__DIR__))
'tenant.token' => TenantTokenGuard::class,
'tenant.isolation' => TenantIsolation::class,
'credit.check' => CreditCheckMiddleware::class,
'locale' => \App\Http\Middleware\SetLocale::class,
'superadmin.auth' => \App\Http\Middleware\SuperAdminAuth::class,
]);
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
$middleware->web(append: [
\App\Http\Middleware\SetLocale::class,
SetLocaleFromUser::class,
HandleAppearance::class,
HandleInertiaRequests::class,

View File

@@ -2,5 +2,6 @@
return [
App\Providers\AppServiceProvider::class,
Stephenjude\FilamentBlog\FilamentBlogServiceProvider::class,
App\Providers\Filament\SuperAdminPanelProvider::class,
];

View File

@@ -16,6 +16,8 @@
"laravel/wayfinder": "^0.1.9",
"paypal/rest-api-sdk-php": "^1.6",
"simplesoftwareio/simple-qrcode": "^4.2",
"spatie/laravel-translatable": "^6.11",
"stephenjude/filament-blog": "*",
"stripe/stripe-php": "^17.6"
},
"require-dev": {

339
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b0eebcaa1d38fb0a00e546fad7bdc991",
"content-hash": "a79b02d59d8ee7716beea2fc8442a905",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -1386,6 +1386,43 @@
},
"time": "2025-09-04T14:12:50+00:00"
},
{
"name": "filament/spatie-laravel-tags-plugin",
"version": "v3.3.30",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/spatie-laravel-tags-plugin.git",
"reference": "7763c2bab92c619cdd9294c69004f89e136c0afc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/spatie-laravel-tags-plugin/zipball/7763c2bab92c619cdd9294c69004f89e136c0afc",
"reference": "7763c2bab92c619cdd9294c69004f89e136c0afc",
"shasum": ""
},
"require": {
"illuminate/database": "^10.45|^11.0|^12.0",
"php": "^8.1",
"spatie/laravel-tags": "^4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Filament\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Filament support for `spatie/laravel-tags`.",
"homepage": "https://github.com/filamentphp/filament",
"support": {
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-05-19T07:27:08+00:00"
},
{
"name": "filament/support",
"version": "v4.0.7",
@@ -5566,6 +5603,80 @@
},
"time": "2021-02-08T20:43:55+00:00"
},
{
"name": "spatie/eloquent-sortable",
"version": "4.5.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/eloquent-sortable.git",
"reference": "c1c4f3a66cd41eb7458783c8a4c8e5d7924a9f20"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/eloquent-sortable/zipball/c1c4f3a66cd41eb7458783c8a4c8e5d7924a9f20",
"reference": "c1c4f3a66cd41eb7458783c8a4c8e5d7924a9f20",
"shasum": ""
},
"require": {
"illuminate/database": "^9.31|^10.0|^11.0|^12.0",
"illuminate/support": "^9.31|^10.0|^11.0|^12.0",
"nesbot/carbon": "^2.63|^3.0",
"php": "^8.1",
"spatie/laravel-package-tools": "^1.9"
},
"require-dev": {
"orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
"phpunit/phpunit": "^9.5|^10.0|^11.5.3"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\EloquentSortable\\EloquentSortableServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Spatie\\EloquentSortable\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be"
}
],
"description": "Sortable behaviour for eloquent models",
"homepage": "https://github.com/spatie/eloquent-sortable",
"keywords": [
"behaviour",
"eloquent",
"laravel",
"model",
"sort",
"sortable"
],
"support": {
"issues": "https://github.com/spatie/eloquent-sortable/issues",
"source": "https://github.com/spatie/eloquent-sortable/tree/4.5.2"
},
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
},
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-08-25T11:46:57+00:00"
},
{
"name": "spatie/invade",
"version": "2.1.0",
@@ -5686,6 +5797,159 @@
],
"time": "2025-07-17T15:46:43+00:00"
},
{
"name": "spatie/laravel-tags",
"version": "4.10.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-tags.git",
"reference": "9fc59a9328e892bbb5b01c948b0d703e22d543ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-tags/zipball/9fc59a9328e892bbb5b01c948b0d703e22d543ec",
"reference": "9fc59a9328e892bbb5b01c948b0d703e22d543ec",
"shasum": ""
},
"require": {
"laravel/framework": "^10.0|^11.0|^12.0",
"nesbot/carbon": "^2.63|^3.0",
"php": "^8.1",
"spatie/eloquent-sortable": "^4.0",
"spatie/laravel-package-tools": "^1.4",
"spatie/laravel-translatable": "^6.0"
},
"require-dev": {
"orchestra/testbench": "^8.0|^9.0|^10.0",
"pestphp/pest": "^1.22|^2.0",
"phpunit/phpunit": "^9.5.2"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\Tags\\TagsServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Spatie\\Tags\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Add tags and taggable behaviour to your Laravel app",
"homepage": "https://github.com/spatie/laravel-tags",
"keywords": [
"laravel-tags",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/laravel-tags/issues",
"source": "https://github.com/spatie/laravel-tags/tree/4.10.0"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-03-08T07:49:06+00:00"
},
{
"name": "spatie/laravel-translatable",
"version": "6.11.4",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-translatable.git",
"reference": "032d85b28de315310dab2048b857016f1194f68b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-translatable/zipball/032d85b28de315310dab2048b857016f1194f68b",
"reference": "032d85b28de315310dab2048b857016f1194f68b",
"shasum": ""
},
"require": {
"illuminate/database": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0|^12.0",
"php": "^8.0",
"spatie/laravel-package-tools": "^1.11"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.64",
"mockery/mockery": "^1.4",
"orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
"pestphp/pest": "^1.20|^2.0|^3.0"
},
"type": "library",
"extra": {
"aliases": {
"Translatable": "Spatie\\Translatable\\Facades\\Translatable"
},
"laravel": {
"providers": [
"Spatie\\Translatable\\TranslatableServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Spatie\\Translatable\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
},
{
"name": "Sebastian De Deyne",
"email": "sebastian@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "A trait to make an Eloquent model hold translations",
"homepage": "https://github.com/spatie/laravel-translatable",
"keywords": [
"eloquent",
"i8n",
"laravel-translatable",
"model",
"multilingual",
"spatie",
"translate"
],
"support": {
"issues": "https://github.com/spatie/laravel-translatable/issues",
"source": "https://github.com/spatie/laravel-translatable/tree/6.11.4"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-02-20T15:51:22+00:00"
},
{
"name": "spatie/shiki-php",
"version": "2.3.2",
@@ -5751,6 +6015,79 @@
],
"time": "2025-02-21T14:16:57+00:00"
},
{
"name": "stephenjude/filament-blog",
"version": "4.2.1",
"source": {
"type": "git",
"url": "https://github.com/stephenjude/filament-blog.git",
"reference": "040414004f876e880889e8d1646de219d85365bc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/stephenjude/filament-blog/zipball/040414004f876e880889e8d1646de219d85365bc",
"reference": "040414004f876e880889e8d1646de219d85365bc",
"shasum": ""
},
"require": {
"filament/filament": "^4.0",
"filament/spatie-laravel-tags-plugin": "^v3.0",
"php": "^8.3",
"spatie/laravel-package-tools": "^1.16.1"
},
"require-dev": {
"laravel/pint": "^1.13.1",
"nunomaduro/collision": "^7.8.1|^8.0",
"orchestra/testbench": "^9.0|^10.0",
"pestphp/pest": "^2.18.2|^3.7",
"pestphp/pest-plugin-arch": "^2.3.3|^3.0",
"pestphp/pest-plugin-laravel": "^2.2|^3.1",
"pestphp/pest-plugin-livewire": "^2.1|^3.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Stephenjude\\FilamentBlog\\BlogServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Stephenjude\\FilamentBlog\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "stephenjude",
"email": "stephenjudesuccess@gmail.com",
"role": "Developer"
}
],
"description": "Filament Blog Builder",
"homepage": "https://github.com/stephenjude/filament-blog",
"keywords": [
"blog",
"filament-blog",
"laravel",
"stephenjude"
],
"support": {
"issues": "https://github.com/stephenjude/filament-blog/issues",
"source": "https://github.com/stephenjude/filament-blog/tree/4.2.1"
},
"funding": [
{
"url": "https://github.com/stephenjude",
"type": "github"
}
],
"time": "2025-09-08T07:49:43+00:00"
},
{
"name": "stripe/stripe-php",
"version": "v17.6.0",

View File

@@ -78,7 +78,7 @@ return [
|
*/
'locale' => env('APP_LOCALE', 'en'),
'locale' => env('APP_LOCALE', 'de'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),

81
config/filament-blog.php Normal file
View File

@@ -0,0 +1,81 @@
<?php
return [
'post_model' => \App\Models\BlogPost::class,
'category_model' => \App\Models\BlogCategory::class,
'tag_model' => \App\Models\BlogTag::class,
'panels' => [
'superadmin' => [
'resources' => [
\Stephenjude\FilamentBlog\Filament\Resources\PostResource::class,
\Stephenjude\FilamentBlog\Filament\Resources\CategoryResource::class,
\Stephenjude\FilamentBlog\Filament\Resources\TagResource::class,
],
],
],
/**
* Supported content editors: richtext & markdown:
* \Filament\Forms\Components\RichEditor::class
* \Filament\Forms\Components\MarkdownEditor::class
*/
'editor' => \Filament\Forms\Components\RichEditor::class,
/**
* Configs for Posts banner file that give you option to change
* Disk,Directory name , maximum upload size and the
* Crop aspect ratio of the Posts banner image.
*/
'banner' => [
'disk' => 'public',
'directory' => 'blog',
'maxSize' => 5120,
'cropAspectRatio' => '16:9',
'visibility' => 'public',
],
/**
* Configs for Posts that give you option to change
* the sort column and direction of the Posts.
*/
'sort' => [
'column' => 'published_at',
'direction' => 'desc',
],
/**
* Configs for Author Avatar file that give you option to change
* Disk,Directory name , maximum upload size and the
* Of the Author avatar image.
*/
'avatar' => [
'disk' => 'public',
'directory' => 'blog',
'maxSize' => 5120,
'visibility' => 'public',
],
/**
* Buttons for text editor toolbar.
*/
'toolbar_buttons' => [
'attachFiles',
'blockquote',
'bold',
'bulletList',
'codeBlock',
'h1',
'h2',
'h3',
'hr',
'image',
'italic',
'link',
'orderedList',
'redo',
'strike',
'table',
'underline',
'undo',
],
];

View File

@@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('blog_categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->longText('description')->nullable();
$table->boolean('is_visible')->default(false);
$table->timestamps();
});
Schema::create('blog_authors', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('photo')->nullable();
$table->longText('bio')->nullable();
$table->string('github_handle')->nullable();
$table->string('twitter_handle')->nullable();
$table->timestamps();
});
Schema::create('blog_posts', function (Blueprint $table) {
$table->id();
$table->foreignId('blog_author_id')->nullable()->constrained()->cascadeOnDelete();
$table->foreignId('blog_category_id')->nullable()->constrained()->nullOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('excerpt')->nullable();
$table->string('banner')->nullable();
$table->longText('content');
$table->date('published_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('blog_posts');
Schema::dropIfExists('blog_categories');
Schema::dropIfExists('blog_authors');
}
};

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('blog_posts', function (Blueprint $table) {
$table->json('translations')->nullable();
});
Schema::table('blog_categories', function (Blueprint $table) {
$table->json('translations')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('blog_posts', function (Blueprint $table) {
$table->dropColumn('translations');
});
Schema::table('blog_categories', function (Blueprint $table) {
$table->dropColumn('translations');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('tenants', function (Blueprint $table) {
$table->string('email')->nullable()->after('slug');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('tenants', function (Blueprint $table) {
$table->dropColumn('email');
});
}
};

View File

@@ -0,0 +1,36 @@
<?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('tags', function (Blueprint $table) {
$table->id();
$table->json('name');
$table->json('slug');
$table->string('type')->nullable();
$table->integer('order_column')->nullable();
$table->timestamps();
});
Schema::create('taggables', function (Blueprint $table) {
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->morphs('taggable');
$table->unique(['tag_id', 'taggable_id', 'taggable_type']);
});
}
public function down(): void
{
Schema::dropIfExists('taggables');
Schema::dropIfExists('tags');
}
};

11146
public/vendor/livewire/livewire.esm.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

10234
public/vendor/livewire/livewire.js vendored Normal file

File diff suppressed because it is too large Load Diff

103
public/vendor/livewire/livewire.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/vendor/livewire/manifest.json vendored Normal file
View File

@@ -0,0 +1,2 @@
{"/livewire.js":"df3a17f2"}

View File

@@ -3,111 +3,158 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fotospiel - Event-Fotos einfach und sicher</title>
<meta name="description" content="Erstellen Sie unvergessliche Event-Fotos mit unserer PWA-Plattform. Für Hochzeiten, Firmenevents und mehr. Kostenloser Einstieg.">
<title>Fotospiel - Event-Fotos einfach und sicher mit QR-Codes</title>
<meta name="description" content="Sammle Gastfotos für Events mit QR-Codes und unserer PWA-Plattform. Für Hochzeiten, Firmenevents und mehr. Kostenloser Einstieg.">
<link rel="icon" href="{{ asset('logo.svg') }}" type="image/svg+xml">
@vite(['resources/css/app.css'])
<style>
@keyframes aurora {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.bg-aurora {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: aurora 15s ease infinite;
}
</style>
</head>
<body class="bg-gray-50 text-gray-900">
<!-- Hero Section -->
<section class="bg-gradient-to-r from-[#FFB6C1] via-[#FFD700] to-[#87CEEB] text-white py-20 px-4">
<div class="container mx-auto text-center">
<!-- Header -->
<header class="bg-white shadow-md sticky top-0 z-50">
<div class="container mx-auto px-4 py-4 flex items-center justify-between">
<div class="flex items-center space-x-2">
<a href="/" class="text-2xl font-bold text-gray-900">Fotospiel</a>
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
</div>
<nav class="hidden md:flex space-x-6 items-center">
<a href="#how-it-works" class="text-gray-600 hover:text-gray-900">How it works</a>
<a href="#features" class="text-gray-600 hover:text-gray-900">Features</a>
<div class="relative group">
<button class="text-gray-600 hover:text-gray-900">Occasions</button>
<div class="absolute top-full left-0 mt-2 bg-white border rounded shadow-lg hidden group-hover:block">
<a href="/occasions/weddings" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Weddings</a>
<a href="/occasions/birthdays" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Birthdays</a>
<a href="/occasions/corporate-events" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Corporate Events</a>
<a href="/occasions/family-celebrations" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Family Celebrations</a>
</div>
</div>
<a href="/blog" class="text-gray-600 hover:text-gray-900">Blog</a>
<a href="#pricing" class="text-gray-600 hover:text-gray-900">Pricing</a>
<a href="#contact" class="text-gray-600 hover:text-gray-900">Contact</a>
<a href="/buy-credits/basic" class="bg-[#FFB6C1] text-white px-6 py-2 rounded-full font-semibold hover:bg-[#FF69B4] transition">Jetzt starten</a>
</nav>
<!-- Mobile Menu Placeholder (Hamburger) -->
<button class="md:hidden text-gray-600"></button>
</div>
</header>
<!-- Hero Section id="hero" -->
<section id="hero" class="bg-aurora text-white py-20 px-4">
<div class="container mx-auto flex flex-col md:flex-row items-center gap-8 max-w-6xl">
<div class="md:w-1/2 text-center md:text-left">
<h1 class="text-4xl md:text-6xl font-bold mb-4">Fotospiel</h1>
<p class="text-xl md:text-2xl mb-8 max-w-3xl mx-auto">Erleben Sie Events durch professionelle Fotos. Unsere sichere PWA-Plattform für Gäste und Organisatoren. Einfach, mobil und datenschutzkonform.</p>
<p class="text-xl md:text-2xl mb-8">Sammle Gastfotos für Events mit QR-Codes. Unsere sichere PWA-Plattform für Gäste und Organisatoren einfach, mobil und datenschutzkonform. Besser als Konkurrenz, geliebt von Tausenden.</p>
<a href="/buy-credits/basic" class="bg-white text-[#FFB6C1] px-8 py-4 rounded-full font-semibold text-lg hover:bg-gray-100 transition">Jetzt starten Kostenlos</a>
<img src="https://images.unsplash.com/photo-1511285560929-80b456fea0bc?w=800&h=400&fit=crop" alt="Event-Fotos" class="mt-8 mx-auto rounded-lg shadow-lg max-w-4xl">
</div>
<div class="md:w-1/2">
<img src="https://images.unsplash.com/photo-1511285560929-80b456fea0bc?w=600&h=400&fit=crop" alt="Event-Fotos mit QR" class="rounded-lg shadow-lg w-full" style="filter: drop-shadow(0 10px 8px rgba(0,0,0,0.1));">
</div>
</div>
</section>
<!-- Features Section -->
<section class="py-20 px-4 bg-white">
<!-- How it works Section id="how-it-works" -->
<section id="how-it-works" class="py-20 px-4 bg-gray-50">
<div class="container mx-auto">
<h2 class="text-3xl font-bold text-center mb-12">Warum Fotospiel?</h2>
<h2 class="text-3xl font-bold text-center mb-12">So funktioniert es in 4 einfachen Schritten mit QR-Codes</h2>
<div class="grid md:grid-cols-4 gap-8">
<div class="text-center">
<div class="w-12 h-12 bg-[#FFB6C1] rounded-full flex items-center justify-center mx-auto mb-4 text-white font-bold">1</div>
<h3 class="font-semibold mb-2">Event erstellen & QR generieren</h3>
<p>Als Organisator: Registrieren, Event anlegen, QR-Code erstellen und drucken/teilen.</p>
</div>
<div class="text-center">
<div class="w-12 h-12 bg-[#FFD700] rounded-full flex items-center justify-center mx-auto mb-4 text-white font-bold">2</div>
<h3 class="font-semibold mb-2">Fotos hochladen via QR</h3>
<p>Gäste: QR scannen, PWA öffnen, Fotos via Kamera oder Galerie teilen.</p>
</div>
<div class="text-center">
<div class="w-12 h-12 bg-[#87CEEB] rounded-full flex items-center justify-center mx-auto mb-4 text-white font-bold">3</div>
<h3 class="font-semibold mb-2">Freigaben & Likes</h3>
<p>Emotions auswählen, Fotos liken, Galerie browsen alles anonym.</p>
</div>
<div class="text-center">
<div class="w-12 h-12 bg-[#FFB6C1] rounded-full flex items-center justify-center mx-auto mb-4 text-white font-bold">4</div>
<h3 class="font-semibold mb-2">Download & Teilen</h3>
<p>Freigegebene Fotos herunterladen, Event abschließen und QR archivieren.</p>
</div>
</div>
</div>
</section>
<!-- Features Section id="features" -->
<section id="features" class="py-20 px-4 bg-white">
<div class="container mx-auto">
<h2 class="text-3xl font-bold text-center mb-12">Warum Fotospiel mit QR?</h2>
<div class="grid md:grid-cols-3 gap-8">
<div class="text-center p-6">
<div class="w-16 h-16 bg-[#FFB6C1] rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
</div>
<h3 class="text-xl font-semibold mb-2">Sichere Uploads</h3>
<p>GDPR-konform, anonyme Sessions, keine PII-Speicherung.</p>
<h3 class="text-xl font-semibold mb-2">Sichere QR-Uploads</h3>
<p>GDPR-konform, anonyme Sessions, QR-basierte Zugriffe ohne PII-Speicherung.</p>
</div>
<div class="text-center p-6">
<div class="w-16 h-16 bg-[#FFD700] rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
</div>
<h3 class="text-xl font-semibold mb-2">Mobile PWA</h3>
<p>Offline-fähig, App-ähnlich, für iOS und Android.</p>
<h3 class="text-xl font-semibold mb-2">Mobile PWA & QR</h3>
<p>Offline-fähig, App-ähnlich für iOS/Android, QR-Scan für schnellen Einstieg.</p>
</div>
<div class="text-center p-6">
<div class="w-16 h-16 bg-[#87CEEB] rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
</div>
<h3 class="text-xl font-semibold mb-2">Schnell & Einfach</h3>
<p>Automatische Thumbnails, Echtzeit-Updates, einfache Bedienung.</p>
<h3 class="text-xl font-semibold mb-2">Schnell & Einfach mit QR</h3>
<p>Automatische Thumbnails, Echtzeit-Updates, QR-Sharing für Gäste.</p>
</div>
</div>
</div>
</section>
<!-- 4-Schritte Section -->
<section class="py-20 px-4 bg-gray-50">
<!-- Pricing Section id="pricing" -->
<section id="pricing" class="py-20 px-4 bg-gray-50">
<div class="container mx-auto">
<h2 class="text-3xl font-bold text-center mb-12">So funktioniert es in 4 einfachen Schritten</h2>
<div class="grid md:grid-cols-4 gap-8">
<div class="text-center">
<div class="w-12 h-12 bg-[#FFB6C1] rounded-full flex items-center justify-center mx-auto mb-4 text-white font-bold">1</div>
<h3 class="font-semibold mb-2">Event erstellen</h3>
<p>Als Organisator: Registrieren, Event anlegen, Gäste einladen.</p>
</div>
<div class="text-center">
<div class="w-12 h-12 bg-[#FFD700] rounded-full flex items-center justify-center mx-auto mb-4 text-white font-bold">2</div>
<h3 class="font-semibold mb-2">Fotos hochladen</h3>
<p>Gäste: PWA öffnen, Fotos via Kamera oder Galerie teilen.</p>
</div>
<div class="text-center">
<div class="w-12 h-12 bg-[#87CEEB] rounded-full flex items-center justify-center mx-auto mb-4 text-white font-bold">3</div>
<h3 class="font-semibold mb-2">Freigaben & Likes</h3>
<p>Emotions auswählen, Fotos liken, Galerie browsen.</p>
</div>
<div class="text-center">
<div class="w-12 h-12 bg-[#FFB6C1] rounded-full flex items-center justify-center mx-auto mb-4 text-white font-bold">4</div>
<h3 class="font-semibold mb-2">Download & Teilen</h3>
<p>Freigegebene Fotos herunterladen, Event abschließen.</p>
</div>
</div>
</div>
</section>
<!-- Preise Section -->
<section class="py-20 px-4 bg-white">
<div class="container mx-auto">
<h2 class="text-3xl font-bold text-center mb-12">Tarife</h2>
<h2 class="text-3xl font-bold text-center mb-12">Tarife für QR-Events</h2>
<div class="grid md:grid-cols-3 gap-8 max-w-4xl mx-auto">
<div class="bg-gray-50 p-8 rounded-lg text-center border-2 border-gray-200">
<div class="bg-white p-8 rounded-lg text-center border-2 border-gray-200">
<h3 class="text-2xl font-bold mb-4">Basic</h3>
<p class="text-4xl font-bold text-[#FFB6C1] mb-4">0 </p>
<ul class="mb-6 space-y-2">
<li>1 Event</li>
<li>1 Event mit QR</li>
<li>100 Fotos</li>
<li>Grundfunktionen</li>
</ul>
<a href="/buy-credits/basic" class="bg-[#FFB6C1] text-white px-6 py-3 rounded-full font-semibold">Kostenlos starten</a>
</div>
<div class="bg-gray-50 p-8 rounded-lg text-center border-2 border-[#FFD700]">
<div class="bg-white p-8 rounded-lg text-center border-2 border-[#FFD700]">
<h3 class="text-2xl font-bold mb-4">Standard</h3>
<p class="text-4xl font-bold text-[#FFD700] mb-4">99 </p>
<ul class="mb-6 space-y-2">
<li>10 Events</li>
<li>10 Events mit QR</li>
<li>Unbegrenzt Fotos</li>
<li>Erweiterte Features</li>
</ul>
<a href="/buy-credits/standard" class="bg-[#FFD700] text-white px-6 py-3 rounded-full font-semibold">Kaufen</a>
</div>
<div class="bg-gray-50 p-8 rounded-lg text-center border-2 border-gray-200">
<div class="bg-white p-8 rounded-lg text-center border-2 border-gray-200">
<h3 class="text-2xl font-bold mb-4">Premium</h3>
<p class="text-4xl font-bold text-[#87CEEB] mb-4">199 </p>
<ul class="mb-6 space-y-2">
<li>50 Events</li>
<li>50 Events mit QR</li>
<li>Support & Custom</li>
<li>Alle Features</li>
</ul>
@@ -117,8 +164,8 @@
</div>
</section>
<!-- Kontakt Section -->
<section class="py-20 px-4 bg-gray-50">
<!-- Contact Section id="contact" -->
<section id="contact" class="py-20 px-4 bg-white">
<div class="container mx-auto max-w-2xl">
<h2 class="text-3xl font-bold text-center mb-12">Kontakt</h2>
<form method="POST" action="/kontakt" class="space-y-4">
@@ -153,16 +200,16 @@
</section>
<!-- Testimonials Section -->
<section class="py-20 px-4 bg-white">
<section class="py-20 px-4 bg-gray-50">
<div class="container mx-auto">
<h2 class="text-3xl font-bold text-center mb-12">Was unsere Kunden sagen</h2>
<div class="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
<div class="bg-gray-50 p-6 rounded-lg">
<p class="mb-4">"Perfekt für unsere Hochzeit! Einfach und sicher."</p>
<div class="bg-white p-6 rounded-lg">
<p class="mb-4">"Perfekt für unsere Hochzeit! QR-Sharing war super einfach."</p>
<p class="font-semibold">- Anna & Max</p>
</div>
<div class="bg-gray-50 p-6 rounded-lg">
<p class="mb-4">"Großes Firmenevent alle Fotos zentral und mobil."</p>
<div class="bg-white p-6 rounded-lg">
<p class="mb-4">"Großes Firmenevent alle Fotos zentral via QR."</p>
<p class="font-semibold">- Team XYZ GmbH</p>
</div>
</div>
@@ -170,21 +217,21 @@
</section>
<!-- FAQ Section -->
<section class="py-20 px-4 bg-gray-50">
<section class="py-20 px-4 bg-white">
<div class="container mx-auto max-w-3xl">
<h2 class="text-3xl font-bold text-center mb-12">Häufige Fragen</h2>
<div class="space-y-4">
<div class="bg-white p-4 rounded-lg">
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="font-semibold">Ist es kostenlos?</h3>
<p>Ja, der Basic-Tarif ist kostenlos für 1 Event. Upgrades ab 99.</p>
<p>Ja, der Basic-Tarif ist kostenlos für 1 Event mit QR. Upgrades ab 99.</p>
</div>
<div class="bg-white p-4 rounded-lg">
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="font-semibold">Datenschutz?</h3>
<p>100% GDPR-konform. Keine personenbezogenen Daten gespeichert. Siehe <a href="/datenschutz" class="text-[#FFB6C1]">Datenschutzerklärung</a>.</p>
<p>100% GDPR-konform. Keine personenbezogenen Daten gespeichert. QR-Zugriffe anonym. Siehe <a href="/datenschutz" class="text-[#FFB6C1]">Datenschutzerklärung</a>.</p>
</div>
<div class="bg-white p-4 rounded-lg">
<h3 class="font-semibold">Wie lade ich Fotos hoch?</h3>
<p>Über die PWA-App: Kamera oder Galerie, Emotions zuweisen, teilen.</p>
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="font-semibold">Wie funktioniert QR-Sharing?</h3>
<p>Generiere QR im Dashboard, teile es Gäste scannen, laden Fotos hoch in der PWA.</p>
</div>
</div>
</div>
@@ -197,7 +244,7 @@
<div class="mt-4 space-x-4">
<a href="/impressum" class="hover:text-[#FFB6C1]">Impressum</a>
<a href="/datenschutz" class="hover:text-[#FFB6C1]">Datenschutz</a>
<a href="/kontakt" class="hover:text-[#FFB6C1]">Kontakt</a>
<a href="#contact" class="hover:text-[#FFB6C1]">Kontakt</a>
</div>
</div>
</footer>

View File

@@ -0,0 +1,89 @@
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $post->meta_title ?? $post->title }} - Fotospiel</title>
<meta name="description" content="{{ $post->meta_description ?? $post->excerpt }}">
<meta property="og:title" content="{{ $post->meta_title ?? $post->title }}">
<meta property="og:description" content="{{ $post->meta_description ?? $post->excerpt }}">
<meta property="og:image" content="{{ $post->featured_image }}">
<meta property="og:url" content="{{ route('blog.show', $post) }}">
<link rel="icon" href="{{ asset('logo.svg') }}" type="image/svg+xml">
@vite(['resources/css/app.css'])
</head>
<body class="bg-gray-50 text-gray-900">
<!-- Shared Header -->
<header class="bg-white shadow-md sticky top-0 z-50">
<div class="container mx-auto px-4 py-4 flex items-center justify-between">
<div class="flex items-center space-x-2">
<a href="/marketing" class="text-2xl font-bold text-gray-900">Fotospiel</a>
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
</div>
<nav class="hidden md:flex space-x-6">
<a href="/marketing#how-it-works" class="text-gray-600 hover:text-gray-900">How it works</a>
<a href="/marketing#features" class="text-gray-600 hover:text-gray-900">Features</a>
<div class="relative">
<button class="text-gray-600 hover:text-gray-900">Occasions</button>
<div class="absolute top-full left-0 mt-2 bg-white border rounded shadow-lg">
<a href="/occasions/weddings" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Weddings</a>
<a href="/occasions/birthdays" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Birthdays</a>
<a href="/occasions/corporate-events" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Corporate Events</a>
<a href="/occasions/family-celebrations" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Family Celebrations</a>
</div>
</div>
<a href="/blog" class="text-gray-900 font-semibold">Blog</a>
<a href="/marketing#pricing" class="text-gray-600 hover:text-gray-900">Pricing</a>
<a href="/marketing#contact" class="text-gray-600 hover:text-gray-900">Contact</a>
</nav>
<a href="/buy-credits/basic" class="bg-[#FFB6C1] text-white px-6 py-2 rounded-full font-semibold">Jetzt starten</a>
</div>
</header>
<!-- Blog Post Hero -->
<section class="py-20 px-4 bg-gradient-to-r from-[#FFB6C1] via-[#FFD700] to-[#87CEEB] text-white">
<div class="container mx-auto max-w-4xl">
@if ($post->featured_image)
<img src="{{ $post->featured_image }}" alt="{{ $post->title }}" class="w-full h-64 object-cover rounded mb-8">
@endif
<h1 class="text-4xl md:text-5xl font-bold mb-4">{{ $post->title }}</h1>
<p class="text-xl mb-8">{{ $post->excerpt }}</p>
<p class="text-sm text-gray-200">Veröffentlicht am {{ $post->published_at->format('d.m.Y') }}</p>
</div>
</section>
<!-- Blog Post Content -->
<section class="py-20 px-4">
<div class="container mx-auto max-w-4xl">
<div class="prose max-w-none">
{!! $post->content !!}
</div>
<div class="mt-8 p-4 bg-gray-100 rounded">
<h3 class="font-semibold mb-2">Kategorien:</h3>
@foreach ($post->categories as $category)
<span class="inline-block bg-[#FFB6C1] text-white px-2 py-1 rounded text-sm mr-2 mb-2">{{ $category->name }}</span>
@endforeach
<h3 class="font-semibold mt-4 mb-2">Tags:</h3>
@foreach ($post->tags as $tag)
<span class="inline-block bg-gray-200 text-gray-800 px-2 py-1 rounded text-sm mr-2 mb-2">#{{ $tag->name }}</span>
@endforeach
</div>
<div class="mt-8 text-center">
<a href="/blog" class="bg-[#FFB6C1] text-white px-6 py-2 rounded-full font-semibold">Zurück zum Blog</a>
</div>
</div>
</section>
<!-- Footer -->
<footer class="bg-gray-800 text-white py-8 px-4 mt-20">
<div class="container mx-auto text-center">
<p>&copy; 2025 Fotospiel GmbH. Alle Rechte vorbehalten.</p>
<div class="mt-4 space-x-4">
<a href="/impressum" class="hover:text-[#FFB6C1]">Impressum</a>
<a href="/datenschutz" class="hover:text-[#FFB6C1]">Datenschutz</a>
<a href="/marketing#contact" class="hover:text-[#FFB6C1]">Kontakt</a>
</div>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,89 @@
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fotospiel - Blog</title>
<meta name="description" content="Tipps, News und Anleitungen zu Event-Fotos mit QR-Codes und PWA.">
<link rel="icon" href="{{ asset('logo.svg') }}" type="image/svg+xml">
@vite(['resources/css/app.css'])
</head>
<body class="bg-gray-50 text-gray-900">
<!-- Shared Header (wie in occasions.blade.php) -->
<header class="bg-white shadow-md sticky top-0 z-50">
<div class="container mx-auto px-4 py-4 flex items-center justify-between">
<div class="flex items-center space-x-2">
<a href="/marketing" class="text-2xl font-bold text-gray-900">Fotospiel</a>
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
</div>
<nav class="hidden md:flex space-x-6">
<a href="/marketing#how-it-works" class="text-gray-600 hover:text-gray-900">How it works</a>
<a href="/marketing#features" class="text-gray-600 hover:text-gray-900">Features</a>
<div class="relative">
<button class="text-gray-600 hover:text-gray-900">Occasions</button>
<div class="absolute top-full left-0 mt-2 bg-white border rounded shadow-lg">
<a href="/occasions/weddings" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Weddings</a>
<a href="/occasions/birthdays" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Birthdays</a>
<a href="/occasions/corporate-events" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Corporate Events</a>
<a href="/occasions/family-celebrations" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Family Celebrations</a>
</div>
</div>
<a href="/blog" class="text-gray-900 font-semibold">Blog</a>
<a href="/marketing#pricing" class="text-gray-600 hover:text-gray-900">Pricing</a>
<a href="/marketing#contact" class="text-gray-600 hover:text-gray-900">Contact</a>
</nav>
<a href="/buy-credits/basic" class="bg-[#FFB6C1] text-white px-6 py-2 rounded-full font-semibold">Jetzt starten</a>
</div>
</header>
<!-- Hero for Blog -->
<section class="bg-gradient-to-r from-[#FFB6C1] via-[#FFD700] to-[#87CEEB] text-white py-20 px-4">
<div class="container mx-auto text-center">
<h1 class="text-4xl md:text-6xl font-bold mb-4">Fotospiel Blog</h1>
<p class="text-xl md:text-2xl mb-8 max-w-3xl mx-auto">Tipps, News und Anleitungen zu perfekten Event-Fotos mit QR-Codes, PWA und mehr. Bleib informiert!</p>
<a href="/marketing#how-it-works" class="bg-white text-[#FFB6C1] px-8 py-4 rounded-full font-semibold text-lg hover:bg-gray-100 transition">Mehr über Fotospiel</a>
</div>
</section>
<!-- Blog Posts Section -->
<section class="py-20 px-4 bg-white">
<div class="container mx-auto max-w-4xl">
<h2 class="text-3xl font-bold text-center mb-12">Aktuelle Blog-Posts</h2>
@if ($posts->count() > 0)
<div class="grid md:grid-cols-2 gap-8">
@foreach ($posts as $post)
<div class="bg-gray-50 p-6 rounded-lg">
@if ($post->featured_image)
<img src="{{ $post->featured_image }}" alt="{{ $post->title }}" class="w-full h-48 object-cover rounded mb-4">
@endif
<h3 class="text-xl font-semibold mb-2"><a href="{{ route('blog.show', $post->slug) }}" class="hover:text-[#FFB6C1]">{{ $post->title }}</a></h3>
<p class="mb-4">{{ Str::limit($post->excerpt, 150) }}</p>
<p class="text-sm text-gray-500 mb-4">Veröffentlicht am {{ $post->published_at->format('d.m.Y') }}</p>
<a href="{{ route('blog.show', $post->slug) }}" class="text-[#FFB6C1] font-semibold">Lesen</a>
</div>
@endforeach
</div>
@if ($posts->hasPages())
<div class="mt-12 text-center">
{{ $posts->links() }}
</div>
@endif
@else
<p class="text-center text-gray-600">Noch keine Posts verfügbar. Bleib dran!</p>
@endif
</div>
</section>
<!-- Footer (wie in occasions.blade.php) -->
<footer class="bg-gray-800 text-white py-8 px-4 mt-20">
<div class="container mx-auto text-center">
<p>&copy; 2025 Fotospiel GmbH. Alle Rechte vorbehalten.</p>
<div class="mt-4 space-x-4">
<a href="/impressum" class="hover:text-[#FFB6C1]">Impressum</a>
<a href="/datenschutz" class="hover:text-[#FFB6C1]">Datenschutz</a>
<a href="/marketing#contact" class="hover:text-[#FFB6C1]">Kontakt</a>
</div>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,137 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fotospiel - {{ ucfirst($type) }} Occasion</title>
<meta name="description" content="Fotospiel für {{ ucfirst($type) }}: Sammle Gastfotos mit QR-Codes.">
<link rel="icon" href="{{ asset('logo.svg') }}" type="image/svg+xml">
@vite(['resources/css/app.css'])
</head>
<body class="bg-gray-50 text-gray-900">
<!-- Shared Header (kopiert aus marketing, vereinfacht) -->
<header class="bg-white shadow-md sticky top-0 z-50">
<div class="container mx-auto px-4 py-4 flex items-center justify-between">
<div class="flex items-center space-x-2">
<a href="/marketing" class="text-2xl font-bold text-gray-900">Fotospiel</a>
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
</div>
<nav class="hidden md:flex space-x-6">
<a href="/marketing#how-it-works" class="text-gray-600 hover:text-gray-900">How it works</a>
<a href="/marketing#features" class="text-gray-600 hover:text-gray-900">Features</a>
<div class="relative">
<button class="text-gray-600 hover:text-gray-900">Occasions</button>
<div class="absolute top-full left-0 mt-2 bg-white border rounded shadow-lg">
<a href="/occasions/weddings" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Weddings</a>
<a href="/occasions/birthdays" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Birthdays</a>
<a href="/occasions/corporate-events" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Corporate Events</a>
<a href="/occasions/family-celebrations" class="block px-4 py-2 text-gray-600 hover:text-gray-900">Family Celebrations</a>
</div>
</div>
<a href="/blog" class="text-gray-600 hover:text-gray-900">Blog</a>
<a href="/marketing#pricing" class="text-gray-600 hover:text-gray-900">Pricing</a>
<a href="/marketing#contact" class="text-gray-600 hover:text-gray-900">Contact</a>
</nav>
<a href="/buy-credits/basic" class="bg-[#FFB6C1] text-white px-6 py-2 rounded-full font-semibold">Jetzt starten</a>
</div>
</header>
<!-- Hero for Occasion -->
<section class="bg-gradient-to-r from-[#FFB6C1] via-[#FFD700] to-[#87CEEB] text-white py-20 px-4">
<div class="container mx-auto text-center">
<h1 class="text-4xl md:text-6xl font-bold mb-4">Fotospiel für {{ ucfirst($type) }}</h1>
<p class="text-xl md:text-2xl mb-8 max-w-3xl mx-auto">Sammle unvergessliche Fotos von deinen Gästen mit QR-Codes. Perfekt für {{ ucfirst($type) }} einfach, mobil und datenschutzkonform.</p>
<a href="/buy-credits/basic" class="bg-white text-[#FFB6C1] px-8 py-4 rounded-full font-semibold text-lg hover:bg-gray-100 transition">Event starten</a>
</div>
</section>
<!-- Occasion Specific Content -->
<section class="py-20 px-4 bg-white">
<div class="container mx-auto max-w-4xl">
@if($type === 'weddings')
<h2 class="text-3xl font-bold text-center mb-12">Hochzeiten mit Fotospiel</h2>
<p class="text-lg mb-8 text-center">Erfange romantische Momente: Gäste teilen Fotos via QR, wähle Emotions wie 'Romantisch' oder 'Fröhlich'. Besser als traditionelle Fotoboxen.</p>
<div class="grid md:grid-cols-2 gap-8">
<div>
<img src="https://images.unsplash.com/photo-1515934751635-c81c6bc9a2d8?w=600&h=400&fit=crop" alt="Hochzeitsfotos" class="rounded-lg shadow-lg">
</div>
<div class="space-y-4">
<h3 class="text-2xl font-semibold">Vorteile für Hochzeiten</h3>
<ul class="space-y-2">
<li> QR-Code für Gäste: Einfaches Teilen ohne App-Download.</li>
<li> Emotion-Filter: Kategorisiere Fotos (z.B. 'Tanz', 'Kuss').</li>
<li> Private Galerie: Nur freigegebene Fotos sichtbar.</li>
<li> Download: Hochauflösend für Album.</li>
</ul>
</div>
</div>
@elseif($type === 'birthdays')
<h2 class="text-3xl font-bold text-center mb-12">Geburtstage feiern</h2>
<p class="text-lg mb-8 text-center">Lass Freunde und Familie spontane Fotos teilen. QR auf der Torte Spaß garantiert!</p>
<div class="grid md:grid-cols-2 gap-8">
<div>
<img src="https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=600&h=400&fit=crop" alt="Geburtstagsfotos" class="rounded-lg shadow-lg">
</div>
<div class="space-y-4">
<h3 class="text-2xl font-semibold">Vorteile für Geburtstage</h3>
<ul class="space-y-2">
<li> Schnelle Uploads: Kamera oder Galerie.</li>
<li> Likes & Shares: Beliebte Momente hervorheben.</li>
<li> Offline-fähig: PWA funktioniert ohne Internet.</li>
<li> Anonym: Keine Registrierung nötig.</li>
</ul>
</div>
</div>
@elseif($type === 'corporate-events')
<h2 class="text-3xl font-bold text-center mb-12">Firmenevents professionell</h2>
<p class="text-lg mb-8 text-center">Netzwerken und Team-Building: Sammle Fotos zentral, teile Highlights intern.</p>
<div class="grid md:grid-cols-2 gap-8">
<div>
<img src="https://images.unsplash.com/photo-1521737604893-d14cc237f11d?w=600&h=400&fit=crop" alt="Firmenevent-Fotos" class="rounded-lg shadow-lg">
</div>
<div class="space-y-4">
<h3 class="text-2xl font-semibold">Vorteile für Firmenevents</h3>
<ul class="space-y-2">
<li> QR an Ständen: Gäste fotografieren sich selbst.</li>
<li> Kategorien: 'Team', 'Netzwerk', 'Präsentation'.</li>
<li> Export: Für Social Media oder Intranet.</li>
<li> GDPR-sicher: Keine PII gespeichert.</li>
</ul>
</div>
</div>
@elseif($type === 'family-celebrations')
<h2 class="text-3xl font-bold text-center mb-12">Familienfeiern</h2>
<p class="text-lg mb-8 text-center">Von Taufen bis Jubiläen: Sammle Erinnerungen von allen Verwandten.</p>
<div class="grid md:grid-cols-2 gap-8">
<div>
<img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=600&h=400&fit=crop" alt="Familienfotos" class="rounded-lg shadow-lg">
</div>
<div class="space-y-4">
<h3 class="text-2xl font-semibold">Vorteile für Familienfeiern</h3>
<ul class="space-y-2">
<li> Einfach für alle Altersgruppen: Große Buchstaben, Touch-freundlich.</li>
<li> Emotionen: 'Familie', 'Glück', 'Zusammenhalt'.</li>
<li> Teilen: Per Link oder QR für Nachfeier.</li>
<li> Unbegrenzt: Im Premium-Tarif.</li>
</ul>
</div>
</div>
@else
<p class="text-center">Occasion nicht gefunden. <a href="/marketing">Zurück zur Startseite</a>.</p>
@endif
</div>
</section>
<!-- Footer (kopiert aus marketing) -->
<footer class="bg-gray-800 text-white py-8 px-4 mt-20">
<div class="container mx-auto text-center">
<p>&copy; 2025 Fotospiel GmbH. Alle Rechte vorbehalten.</p>
<div class="mt-4 space-x-4">
<a href="/impressum" class="hover:text-[#FFB6C1]">Impressum</a>
<a href="/datenschutz" class="hover:text-[#FFB6C1]">Datenschutz</a>
<a href="/marketing#contact" class="hover:text-[#FFB6C1]">Kontakt</a>
</div>
</div>
</footer>
</body>
</html>

View File

@@ -3,8 +3,15 @@
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
// Marketing-Seite unter Root
Route::view('/', 'marketing')->name('marketing');
// Marketing-Seite mit Locale-Prefix
Route::prefix('{locale?}')->where(['locale' => 'de|en'])->middleware('locale')->group(function () {
Route::view('/', 'marketing')->name('marketing');
Route::get('/occasions/{type}', function ($type) {
return view('marketing.occasions', ['type' => $type]);
})->name('occasions.type');
Route::get('/blog', [\App\Http\Controllers\MarketingController::class, 'blogIndex'])->name('blog');
Route::get('/blog/{post}', [\App\Http\Controllers\MarketingController::class, 'blogShow'])->name('blog.show');
});
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard', function () {