mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 08:51:11 -05:00
Add photo management features, update database configuration, and enhance park model seeding
This commit is contained in:
@@ -1,64 +1,151 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ $title ?? 'ThrillWiki' }}</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@livewireStyles
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Prevent flash of wrong theme -->
|
||||
<script>
|
||||
let theme = localStorage.getItem("theme");
|
||||
if (!theme) {
|
||||
theme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
localStorage.setItem("theme", theme);
|
||||
}
|
||||
if (theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- HTMX -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
|
||||
|
||||
<!-- Scripts and Styles (loaded via Vite) -->
|
||||
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
|
||||
<style>
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin-top: 0.5rem;
|
||||
width: 12rem;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
z-index: 50;
|
||||
overflow: hidden;
|
||||
}
|
||||
.htmx-indicator {
|
||||
display: none;
|
||||
}
|
||||
.htmx-request .htmx-indicator {
|
||||
display: block;
|
||||
}
|
||||
.htmx-request.htmx-indicator {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body class="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||
<header class="bg-white dark:bg-gray-800 shadow">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<body class="flex flex-col min-h-screen text-gray-900 bg-gradient-to-br from-white via-blue-50 to-indigo-50 dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950 dark:text-white">
|
||||
<!-- Header -->
|
||||
<header class="sticky top-0 z-40 border-b shadow-lg bg-white/90 dark:bg-gray-800/90 backdrop-blur-lg border-gray-200/50 dark:border-gray-700/50">
|
||||
<nav class="container mx-auto nav-container">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center">
|
||||
<a href="{{ route('home') }}" class="text-xl font-bold text-indigo-600 dark:text-indigo-400">
|
||||
<a href="{{ route('home') }}" class="font-bold text-transparent transition-transform site-logo bg-gradient-to-r from-primary to-secondary bg-clip-text hover:scale-105">
|
||||
ThrillWiki
|
||||
</a>
|
||||
<nav class="ml-10 space-x-4 hidden md:flex">
|
||||
<a href="{{ route('parks.index') }}" class="px-3 py-2 rounded-md text-sm font-medium {{ request()->routeIs('parks.*') ? 'bg-indigo-100 dark:bg-indigo-900 text-indigo-700 dark:text-indigo-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700' }}">
|
||||
Parks
|
||||
</a>
|
||||
<a href="{{ route('rides.index') }}" class="px-3 py-2 rounded-md text-sm font-medium {{ request()->routeIs('rides.*') ? 'bg-indigo-100 dark:bg-indigo-900 text-indigo-700 dark:text-indigo-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700' }}">
|
||||
Rides
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
@livewire('theme-toggle-component')
|
||||
|
||||
<!-- Navigation Links (Always Visible) -->
|
||||
<div class="flex items-center space-x-2 sm:space-x-4">
|
||||
<a href="{{ route('parks.index') }}" class="nav-link">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span>Parks</span>
|
||||
</a>
|
||||
<a href="{{ route('rides.index') }}" class="nav-link">
|
||||
<i class="fas fa-rocket"></i>
|
||||
<span>Rides</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="flex-1 hidden max-w-md mx-8 lg:flex">
|
||||
<form action="{{ route('search') }}" method="get" class="w-full">
|
||||
<div class="relative">
|
||||
<input type="text" name="q" placeholder="Search parks and rides..." class="form-input">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Right Side Menu -->
|
||||
<div class="flex items-center space-x-2 sm:space-x-6">
|
||||
<!-- Theme Toggle -->
|
||||
<livewire:theme-toggle-component />
|
||||
|
||||
<!-- User Menu -->
|
||||
@auth
|
||||
@livewire('user-menu-component')
|
||||
@if(auth()->user()->can('access-moderation'))
|
||||
<a href="{{ route('moderation.dashboard') }}" class="nav-link">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<span>Moderation</span>
|
||||
</a>
|
||||
@endif
|
||||
<livewire:user-menu-component />
|
||||
@else
|
||||
@livewire('auth-menu-component')
|
||||
<!-- Generic Profile Icon for Unauthenticated Users -->
|
||||
<livewire:auth-menu-component />
|
||||
@endauth
|
||||
@livewire('mobile-menu-component')
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<livewire:mobile-menu-component />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Flash Messages -->
|
||||
@if (session('status'))
|
||||
<div class="fixed top-0 right-0 z-50 p-4 space-y-4">
|
||||
<div class="alert alert-success">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container flex-grow px-6 py-8 mx-auto">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
|
||||
<footer class="bg-white dark:bg-gray-800 shadow mt-auto">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
© {{ date('Y') }} ThrillWiki. All rights reserved.
|
||||
<!-- Footer -->
|
||||
<footer class="mt-auto border-t bg-white/90 dark:bg-gray-800/90 backdrop-blur-lg border-gray-200/50 dark:border-gray-700/50">
|
||||
<div class="container px-6 py-6 mx-auto">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-gray-600 dark:text-gray-400">
|
||||
<p>© {{ date('Y') }} ThrillWiki. All rights reserved.</p>
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<a href="{{ route('terms') }}" class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">
|
||||
Terms
|
||||
</a>
|
||||
<a href="{{ route('privacy') }}" class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">
|
||||
Privacy
|
||||
</a>
|
||||
<div class="space-x-4">
|
||||
<a href="{{ route('terms') }}" class="text-gray-600 transition-colors hover:text-primary dark:text-gray-400 dark:hover:text-primary">Terms</a>
|
||||
<a href="{{ route('privacy') }}" class="text-gray-600 transition-colors hover:text-primary dark:text-gray-400 dark:hover:text-primary">Privacy</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@livewireScripts
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Featured Photo</h3>
|
||||
|
||||
@if ($isLoading)
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<svg class="animate-spin h-8 w-8 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
@elseif ($error)
|
||||
<div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded relative dark:bg-red-900 dark:border-red-800 dark:text-red-200" role="alert">
|
||||
<span class="block sm:inline">{{ $error }}</span>
|
||||
</div>
|
||||
@elseif (count($photos) === 0)
|
||||
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 mx-auto mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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" />
|
||||
</svg>
|
||||
<p>No photos available</p>
|
||||
<p class="text-sm mt-1">Upload photos to select a featured image.</p>
|
||||
</div>
|
||||
@else
|
||||
@if ($success)
|
||||
<div class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded relative mb-4 dark:bg-green-900 dark:border-green-800 dark:text-green-200" role="alert">
|
||||
<span class="block sm:inline">Featured photo updated successfully!</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
||||
@foreach ($photos as $photo)
|
||||
<div
|
||||
wire:key="featured-photo-{{ $photo->id }}"
|
||||
class="relative aspect-square overflow-hidden rounded-lg bg-gray-100 dark:bg-gray-700 {{ $featuredPhotoId === $photo->id ? 'ring-2 ring-yellow-500 ring-offset-2 dark:ring-offset-gray-800' : '' }}"
|
||||
>
|
||||
<img
|
||||
src="{{ $photo->url }}"
|
||||
alt="{{ $photo->alt_text ?? $photo->title ?? 'Park photo' }}"
|
||||
class="w-full h-full object-cover"
|
||||
>
|
||||
|
||||
@if ($featuredPhotoId === $photo->id)
|
||||
<div class="absolute top-2 left-2 bg-yellow-500 text-white text-xs px-2 py-1 rounded-full">
|
||||
Featured
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="absolute inset-0 bg-black bg-opacity-0 hover:bg-opacity-40 transition-all duration-300 flex items-center justify-center">
|
||||
@if ($featuredPhotoId !== $photo->id)
|
||||
<button
|
||||
wire:click="setFeatured({{ $photo->id }})"
|
||||
class="opacity-0 hover:opacity-100 p-2 bg-white rounded-full text-gray-800 hover:bg-yellow-500 hover:text-white transition-colors duration-200"
|
||||
title="Set as featured"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
299
resources/views/livewire/photo-gallery-component.blade.php
Normal file
299
resources/views/livewire/photo-gallery-component.blade.php
Normal file
@@ -0,0 +1,299 @@
|
||||
<div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Photo Gallery</h3>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
wire:click="toggleViewMode"
|
||||
class="inline-flex items-center px-3 py-1.5 bg-gray-100 border border-gray-300 rounded-md font-medium text-xs text-gray-700 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition ease-in-out duration-150 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||
>
|
||||
@if ($viewMode === 'grid')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7" />
|
||||
</svg>
|
||||
Carousel View
|
||||
@else
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||||
</svg>
|
||||
Grid View
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($isLoading)
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<svg class="animate-spin h-8 w-8 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
@elseif ($error)
|
||||
<div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded relative dark:bg-red-900 dark:border-red-800 dark:text-red-200" role="alert">
|
||||
<span class="block sm:inline">{{ $error }}</span>
|
||||
</div>
|
||||
@elseif (count($photos) === 0)
|
||||
<div class="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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" />
|
||||
</svg>
|
||||
<p class="text-lg font-medium">No photos yet</p>
|
||||
<p class="mt-1">Upload photos to showcase this park.</p>
|
||||
</div>
|
||||
@else
|
||||
@if ($viewMode === 'grid')
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
||||
@foreach ($photos as $photo)
|
||||
<div
|
||||
wire:key="photo-{{ $photo->id }}"
|
||||
class="relative group aspect-square overflow-hidden rounded-lg bg-gray-100 dark:bg-gray-700"
|
||||
>
|
||||
<img
|
||||
src="{{ $photo->url }}"
|
||||
alt="{{ $photo->alt_text ?? $photo->title ?? 'Park photo' }}"
|
||||
class="w-full h-full object-cover cursor-pointer"
|
||||
wire:click="selectPhoto({{ $photo->id }})"
|
||||
>
|
||||
|
||||
@if ($photo->is_featured)
|
||||
<div class="absolute top-2 left-2 bg-yellow-500 text-white text-xs px-2 py-1 rounded-full">
|
||||
Featured
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-40 transition-all duration-300 flex items-center justify-center opacity-0 group-hover:opacity-100">
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
wire:click="selectPhoto({{ $photo->id }})"
|
||||
class="p-1.5 bg-white rounded-full text-gray-800 hover:bg-blue-500 hover:text-white transition-colors duration-200"
|
||||
title="View photo"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<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" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@if (!$photo->is_featured)
|
||||
<button
|
||||
wire:click="setFeatured({{ $photo->id }})"
|
||||
class="p-1.5 bg-white rounded-full text-gray-800 hover:bg-yellow-500 hover:text-white transition-colors duration-200"
|
||||
title="Set as featured"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<button
|
||||
wire:click="deletePhoto({{ $photo->id }})"
|
||||
wire:confirm="Are you sure you want to delete this photo? This action cannot be undone."
|
||||
class="p-1.5 bg-white rounded-full text-gray-800 hover:bg-red-500 hover:text-white transition-colors duration-200"
|
||||
title="Delete photo"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div
|
||||
x-data="{
|
||||
activeSlide: 0,
|
||||
totalSlides: {{ count($photos) }},
|
||||
next() {
|
||||
this.activeSlide = (this.activeSlide + 1) % this.totalSlides;
|
||||
},
|
||||
prev() {
|
||||
this.activeSlide = (this.activeSlide - 1 + this.totalSlides) % this.totalSlides;
|
||||
}
|
||||
}"
|
||||
class="relative"
|
||||
>
|
||||
<div class="relative aspect-video overflow-hidden rounded-lg bg-gray-100 dark:bg-gray-700">
|
||||
@foreach ($photos as $index => $photo)
|
||||
<div
|
||||
x-show="activeSlide === {{ $index }}"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
class="absolute inset-0"
|
||||
>
|
||||
<img
|
||||
src="{{ $photo->url }}"
|
||||
alt="{{ $photo->alt_text ?? $photo->title ?? 'Park photo' }}"
|
||||
class="w-full h-full object-contain"
|
||||
>
|
||||
|
||||
@if ($photo->is_featured)
|
||||
<div class="absolute top-4 left-4 bg-yellow-500 text-white px-2 py-1 rounded-full text-sm">
|
||||
Featured
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-4 text-white">
|
||||
<h4 class="font-medium">{{ $photo->title ?? 'Untitled Photo' }}</h4>
|
||||
@if ($photo->description)
|
||||
<p class="text-sm text-gray-200 mt-1">{{ $photo->description }}</p>
|
||||
@endif
|
||||
@if ($photo->credit)
|
||||
<p class="text-xs text-gray-300 mt-2">Credit: {{ $photo->credit }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="prev"
|
||||
class="absolute left-2 top-1/2 -translate-y-1/2 p-1.5 bg-black/50 hover:bg-black/70 rounded-full text-white transition-colors duration-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="next"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 bg-black/50 hover:bg-black/70 rounded-full text-white transition-colors duration-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="flex justify-center mt-4 space-x-2">
|
||||
@foreach ($photos as $index => $photo)
|
||||
<button
|
||||
@click="activeSlide = {{ $index }}"
|
||||
:class="{'bg-blue-600': activeSlide === {{ $index }}, 'bg-gray-300 dark:bg-gray-600': activeSlide !== {{ $index }}}"
|
||||
class="w-2.5 h-2.5 rounded-full transition-colors duration-200"
|
||||
></button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Photo Detail Modal -->
|
||||
@if ($selectedPhoto)
|
||||
<div
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
x-data="{}"
|
||||
x-init="$nextTick(() => { document.body.style.overflow = 'hidden'; })"
|
||||
x-on:keydown.escape.window="$wire.closePhotoDetail(); document.body.style.overflow = '';"
|
||||
>
|
||||
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 bg-gray-900 bg-opacity-75 transition-opacity" wire:click="closePhotoDetail"></div>
|
||||
|
||||
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
|
||||
<div class="absolute top-0 right-0 pt-4 pr-4">
|
||||
<button
|
||||
wire:click="closePhotoDetail"
|
||||
class="bg-white dark:bg-gray-800 rounded-md text-gray-400 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<span class="sr-only">Close</span>
|
||||
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<div class="md:w-2/3">
|
||||
<div class="aspect-video bg-gray-100 dark:bg-gray-700 rounded-lg overflow-hidden">
|
||||
<img
|
||||
src="{{ $selectedPhoto->url }}"
|
||||
alt="{{ $selectedPhoto->alt_text ?? $selectedPhoto->title ?? 'Park photo' }}"
|
||||
class="w-full h-full object-contain"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:w-1/3">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{{ $selectedPhoto->title ?? 'Untitled Photo' }}
|
||||
</h3>
|
||||
|
||||
@if ($selectedPhoto->description)
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ $selectedPhoto->description }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<div class="mt-4 border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<dl class="space-y-3 text-sm">
|
||||
@if ($selectedPhoto->credit)
|
||||
<div>
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Credit:</dt>
|
||||
<dd class="mt-1 text-gray-900 dark:text-white">{{ $selectedPhoto->credit }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($selectedPhoto->source_url)
|
||||
<div>
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Source:</dt>
|
||||
<dd class="mt-1 text-blue-600 dark:text-blue-400">
|
||||
<a href="{{ $selectedPhoto->source_url }}" target="_blank" rel="noopener noreferrer" class="hover:underline">
|
||||
{{ $selectedPhoto->source_url }}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Dimensions:</dt>
|
||||
<dd class="mt-1 text-gray-900 dark:text-white">{{ $selectedPhoto->width }} × {{ $selectedPhoto->height }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">File Size:</dt>
|
||||
<dd class="mt-1 text-gray-900 dark:text-white">{{ number_format($selectedPhoto->file_size / 1024, 2) }} KB</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex space-x-3">
|
||||
@if (!$selectedPhoto->is_featured)
|
||||
<button
|
||||
wire:click="setFeatured({{ $selectedPhoto->id }})"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:bg-gray-600"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
Set as Featured
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<button
|
||||
wire:click="deletePhoto({{ $selectedPhoto->id }})"
|
||||
wire:confirm="Are you sure you want to delete this photo? This action cannot be undone."
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:bg-gray-600"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
Delete Photo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
127
resources/views/livewire/photo-manager-component.blade.php
Normal file
127
resources/views/livewire/photo-manager-component.blade.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Manage Photos</h3>
|
||||
|
||||
<div>
|
||||
@if (!$reordering)
|
||||
<button
|
||||
wire:click="startReordering"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:bg-gray-600"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
||||
</svg>
|
||||
Reorder Photos
|
||||
</button>
|
||||
@else
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
wire:click="saveOrder"
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Save Order
|
||||
</button>
|
||||
|
||||
<button
|
||||
wire:click="cancelReordering"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:bg-gray-600"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($isLoading)
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<svg class="animate-spin h-8 w-8 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
@elseif ($error)
|
||||
<div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded relative dark:bg-red-900 dark:border-red-800 dark:text-red-200" role="alert">
|
||||
<span class="block sm:inline">{{ $error }}</span>
|
||||
</div>
|
||||
@elseif (count($photos) === 0)
|
||||
<div class="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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" />
|
||||
</svg>
|
||||
<p class="text-lg font-medium">No photos yet</p>
|
||||
<p class="mt-1">Upload photos to showcase this park.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-4">
|
||||
@foreach ($photoOrder as $index => $photoId)
|
||||
@php
|
||||
$photo = collect($photos)->firstWhere('id', $photoId);
|
||||
@endphp
|
||||
@if ($photo)
|
||||
<div
|
||||
wire:key="photo-order-{{ $photo['id'] }}"
|
||||
class="flex items-center bg-gray-50 dark:bg-gray-700 rounded-lg overflow-hidden"
|
||||
>
|
||||
<div class="w-20 h-20 flex-shrink-0">
|
||||
<img
|
||||
src="{{ asset('storage/' . $photo['file_path']) }}"
|
||||
alt="{{ $photo['alt_text'] ?? $photo['title'] ?? 'Park photo' }}"
|
||||
class="w-full h-full object-cover"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0 px-4 py-2">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{{ $photo['title'] ?? 'Untitled Photo' }}
|
||||
</p>
|
||||
@if ($photo['description'])
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{{ $photo['description'] }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($photo['is_featured'])
|
||||
<div class="px-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100">
|
||||
Featured
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($reordering)
|
||||
<div class="flex items-center space-x-1 px-4">
|
||||
<button
|
||||
wire:click="moveUp({{ $index }})"
|
||||
class="p-1 text-gray-400 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200 {{ $index === 0 ? 'opacity-50 cursor-not-allowed' : '' }}"
|
||||
{{ $index === 0 ? 'disabled' : '' }}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
wire:click="moveDown({{ $index }})"
|
||||
class="p-1 text-gray-400 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200 {{ $index === count($photoOrder) - 1 ? 'opacity-50 cursor-not-allowed' : '' }}"
|
||||
{{ $index === count($photoOrder) - 1 ? 'disabled' : '' }}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
120
resources/views/livewire/photo-upload-component.blade.php
Normal file
120
resources/views/livewire/photo-upload-component.blade.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Upload New Photo</h3>
|
||||
|
||||
<form wire:submit.prevent="save" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label for="photo" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Photo <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div
|
||||
x-data="{
|
||||
isUploading: false,
|
||||
progress: 0,
|
||||
photoPreview: null,
|
||||
handleFileSelect(event) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.photoPreview = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(event.target.files[0]);
|
||||
}
|
||||
}"
|
||||
x-on:livewire-upload-start="isUploading = true"
|
||||
x-on:livewire-upload-finish="isUploading = false; progress = 0"
|
||||
x-on:livewire-upload-error="isUploading = false"
|
||||
x-on:livewire-upload-progress="progress = $event.detail.progress"
|
||||
class="space-y-2"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
id="photo"
|
||||
wire:model="photo"
|
||||
x-on:change="handleFileSelect"
|
||||
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 dark:file:bg-gray-700 dark:file:text-gray-200"
|
||||
/>
|
||||
|
||||
<div x-show="photoPreview" class="mt-2">
|
||||
<img x-bind:src="photoPreview" class="h-32 w-auto object-cover rounded-md" />
|
||||
</div>
|
||||
|
||||
<div x-show="isUploading" class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700 mt-2">
|
||||
<div class="bg-blue-600 h-2.5 rounded-full" x-bind:style="`width: ${progress}%`"></div>
|
||||
</div>
|
||||
|
||||
@error('photo')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Title</label>
|
||||
<input type="text" id="title" wire:model="title" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('title') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="alt_text" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Alt Text</label>
|
||||
<input type="text" id="alt_text" wire:model="alt_text" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('alt_text') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
|
||||
<textarea id="description" wire:model="description" rows="3" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"></textarea>
|
||||
@error('description') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="credit" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Photo Credit</label>
|
||||
<input type="text" id="credit" wire:model="credit" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('credit') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="source_url" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Source URL</label>
|
||||
<input type="url" id="source_url" wire:model="source_url" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('source_url') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="is_featured" wire:model="is_featured" class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="is_featured" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">Set as featured photo</label>
|
||||
@error('is_featured') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
@if ($uploadError)
|
||||
<div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded relative dark:bg-red-900 dark:border-red-800 dark:text-red-200" role="alert">
|
||||
<span class="block sm:inline">{{ $uploadError }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($uploadSuccess)
|
||||
<div class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded relative dark:bg-green-900 dark:border-green-800 dark:text-green-200" role="alert">
|
||||
<span class="block sm:inline">Photo uploaded successfully!</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition ease-in-out duration-150 disabled:opacity-50"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="save,photo"
|
||||
>
|
||||
<span wire:loading.remove wire:target="save">Upload Photo</span>
|
||||
<span wire:loading wire:target="save">
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Uploading...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
163
resources/views/parks/show.blade.php
Normal file
163
resources/views/parks/show.blade.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="title">{{ $park->name }}</x-slot>
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-wrap items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ $park->name }}</h1>
|
||||
<div class="flex items-center mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $park->status_classes }} mr-2">
|
||||
{{ $park->status->name }}
|
||||
</span>
|
||||
@if ($park->operator)
|
||||
<span>Operated by <a href="{{ route('operators.show', $park->operator) }}" class="text-blue-600 dark:text-blue-400 hover:underline">{{ $park->operator->name }}</a></span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 md:mt-0 flex space-x-2">
|
||||
<a href="{{ route('parks.edit', $park) }}" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:bg-gray-600">
|
||||
<svg class="-ml-1 mr-2 h-5 w-5 text-gray-500 dark:text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
Edit Park
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Main Content -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<!-- Featured Photo -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
||||
<div class="aspect-video bg-gray-100 dark:bg-gray-700">
|
||||
<img
|
||||
src="{{ $park->featured_photo_url }}"
|
||||
alt="{{ $park->name }}"
|
||||
class="w-full h-full object-cover"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">About {{ $park->name }}</h2>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
@if ($park->description)
|
||||
<p>{{ $park->description }}</p>
|
||||
@else
|
||||
<p class="text-gray-500 dark:text-gray-400">No description available.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Photo Gallery -->
|
||||
<livewire:photo-gallery-component :park="$park" />
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Park Info -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h2 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Park Information</h2>
|
||||
|
||||
<dl class="space-y-3 text-sm">
|
||||
@if ($park->opening_date)
|
||||
<div class="flex justify-between">
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Opened:</dt>
|
||||
<dd class="text-gray-900 dark:text-white">{{ $park->opening_date->format('F j, Y') }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($park->closing_date)
|
||||
<div class="flex justify-between">
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Closed:</dt>
|
||||
<dd class="text-gray-900 dark:text-white">{{ $park->closing_date->format('F j, Y') }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($park->size_acres)
|
||||
<div class="flex justify-between">
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Size:</dt>
|
||||
<dd class="text-gray-900 dark:text-white">{{ $park->size_display }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($park->operating_season)
|
||||
<div class="flex justify-between">
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Season:</dt>
|
||||
<dd class="text-gray-900 dark:text-white">{{ $park->operating_season }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($park->website)
|
||||
<div class="flex justify-between">
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Website:</dt>
|
||||
<dd class="text-gray-900 dark:text-white">
|
||||
<a href="{{ $park->website_url }}" target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline">
|
||||
Visit Website
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-between">
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Location:</dt>
|
||||
<dd class="text-gray-900 dark:text-white">{{ $park->formatted_location ?: 'Unknown' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h2 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Statistics</h2>
|
||||
|
||||
<dl class="space-y-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Total Rides:</dt>
|
||||
<dd class="text-gray-900 dark:text-white">{{ $park->total_rides ?: 0 }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Roller Coasters:</dt>
|
||||
<dd class="text-gray-900 dark:text-white">{{ $park->total_coasters ?: 0 }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Flat Rides:</dt>
|
||||
<dd class="text-gray-900 dark:text-white">{{ $park->total_flat_rides ?: 0 }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Water Rides:</dt>
|
||||
<dd class="text-gray-900 dark:text-white">{{ $park->total_water_rides ?: 0 }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<dt class="font-medium text-gray-500 dark:text-gray-400">Areas:</dt>
|
||||
<dd class="text-gray-900 dark:text-white">{{ $park->total_areas ?: 0 }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
@if ($park->location)
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
||||
<livewire:location.location-map-component :location="$park->location" :height="300" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Photo Upload -->
|
||||
<livewire:photo-upload-component :park="$park" />
|
||||
|
||||
<!-- Photo Management -->
|
||||
<livewire:photo-manager-component :park="$park" />
|
||||
|
||||
<!-- Featured Photo Selector -->
|
||||
<livewire:featured-photo-selector-component :park="$park" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
Reference in New Issue
Block a user