first commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,7 @@
|
||||
import './bootstrap';
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.start();
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
@@ -0,0 +1,27 @@
|
||||
<x-guest-layout>
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('password.confirm') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Password -->
|
||||
<div>
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
|
||||
<x-text-input id="password" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password"
|
||||
required autocomplete="current-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Confirm') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
@@ -0,0 +1,25 @@
|
||||
<x-guest-layout>
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
|
||||
</div>
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('password.email') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Email Password Reset Link') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
@@ -0,0 +1,47 @@
|
||||
<x-guest-layout>
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
|
||||
<x-text-input id="password" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password"
|
||||
required autocomplete="current-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="block mt-4">
|
||||
<label for="remember_me" class="inline-flex items-center">
|
||||
<input id="remember_me" type="checkbox" class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="remember">
|
||||
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Remember me') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
@if (Route::has('password.request'))
|
||||
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('password.request') }}">
|
||||
{{ __('Forgot your password?') }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<x-primary-button class="ms-3">
|
||||
{{ __('Log in') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
@@ -0,0 +1,52 @@
|
||||
<x-guest-layout>
|
||||
<form method="POST" action="{{ route('register') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<x-input-label for="name" :value="__('Name')" />
|
||||
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
|
||||
<x-input-error :messages="$errors->get('name')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Email Address -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
|
||||
<x-text-input id="password" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password"
|
||||
required autocomplete="new-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||
|
||||
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password_confirmation" required autocomplete="new-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('login') }}">
|
||||
{{ __('Already registered?') }}
|
||||
</a>
|
||||
|
||||
<x-primary-button class="ms-4">
|
||||
{{ __('Register') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
@@ -0,0 +1,39 @@
|
||||
<x-guest-layout>
|
||||
<form method="POST" action="{{ route('password.store') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Password Reset Token -->
|
||||
<input type="hidden" name="token" value="{{ $request->route('token') }}">
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||
|
||||
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password_confirmation" required autocomplete="new-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Reset Password') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
@@ -0,0 +1,31 @@
|
||||
<x-guest-layout>
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
|
||||
</div>
|
||||
|
||||
@if (session('status') == 'verification-link-sent')
|
||||
<div class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
|
||||
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<form method="POST" action="{{ route('verification.send') }}">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<x-primary-button>
|
||||
{{ __('Resend Verification Email') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<button type="submit" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
|
||||
{{ __('Log Out') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-guest-layout>
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,7 @@
|
||||
@props(['status'])
|
||||
|
||||
@if ($status)
|
||||
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600 dark:text-green-400']) }}>
|
||||
{{ $status }}
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,3 @@
|
||||
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
||||
@@ -0,0 +1 @@
|
||||
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
||||
@@ -0,0 +1,35 @@
|
||||
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-700'])
|
||||
|
||||
@php
|
||||
$alignmentClasses = match ($align) {
|
||||
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
|
||||
'top' => 'origin-top',
|
||||
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
|
||||
};
|
||||
|
||||
$width = match ($width) {
|
||||
'48' => 'w-48',
|
||||
default => $width,
|
||||
};
|
||||
@endphp
|
||||
|
||||
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
|
||||
<div @click="open = ! open">
|
||||
{{ $trigger }}
|
||||
</div>
|
||||
|
||||
<div x-show="open"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
|
||||
style="display: none;"
|
||||
@click="open = false">
|
||||
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
|
||||
{{ $content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
@props(['messages'])
|
||||
|
||||
@if ($messages)
|
||||
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 dark:text-red-400 space-y-1']) }}>
|
||||
@foreach ((array) $messages as $message)
|
||||
<li>{{ $message }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
@@ -0,0 +1,5 @@
|
||||
@props(['value'])
|
||||
|
||||
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700 dark:text-gray-300']) }}>
|
||||
{{ $value ?? $slot }}
|
||||
</label>
|
||||
@@ -0,0 +1,78 @@
|
||||
@props([
|
||||
'name',
|
||||
'show' => false,
|
||||
'maxWidth' => '2xl'
|
||||
])
|
||||
|
||||
@php
|
||||
$maxWidth = [
|
||||
'sm' => 'sm:max-w-sm',
|
||||
'md' => 'sm:max-w-md',
|
||||
'lg' => 'sm:max-w-lg',
|
||||
'xl' => 'sm:max-w-xl',
|
||||
'2xl' => 'sm:max-w-2xl',
|
||||
][$maxWidth];
|
||||
@endphp
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
show: @js($show),
|
||||
focusables() {
|
||||
// All focusable element types...
|
||||
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
|
||||
return [...$el.querySelectorAll(selector)]
|
||||
// All non-disabled elements...
|
||||
.filter(el => ! el.hasAttribute('disabled'))
|
||||
},
|
||||
firstFocusable() { return this.focusables()[0] },
|
||||
lastFocusable() { return this.focusables().slice(-1)[0] },
|
||||
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
|
||||
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
|
||||
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
|
||||
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
|
||||
}"
|
||||
x-init="$watch('show', value => {
|
||||
if (value) {
|
||||
document.body.classList.add('overflow-y-hidden');
|
||||
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
|
||||
} else {
|
||||
document.body.classList.remove('overflow-y-hidden');
|
||||
}
|
||||
})"
|
||||
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
|
||||
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
|
||||
x-on:close.stop="show = false"
|
||||
x-on:keydown.escape.window="show = false"
|
||||
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
|
||||
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
|
||||
x-show="show"
|
||||
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
|
||||
style="display: {{ $show ? 'block' : 'none' }};"
|
||||
>
|
||||
<div
|
||||
x-show="show"
|
||||
class="fixed inset-0 transform transition-all"
|
||||
x-on:click="show = false"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
x-show="show"
|
||||
class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
@props(['active'])
|
||||
|
||||
@php
|
||||
$classes = ($active ?? false)
|
||||
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
|
||||
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out';
|
||||
@endphp
|
||||
|
||||
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||
{{ $slot }}
|
||||
</a>
|
||||
@@ -0,0 +1,3 @@
|
||||
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
||||
@@ -0,0 +1,11 @@
|
||||
@props(['active'])
|
||||
|
||||
@php
|
||||
$classes = ($active ?? false)
|
||||
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out'
|
||||
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out';
|
||||
@endphp
|
||||
|
||||
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||
{{ $slot }}
|
||||
</a>
|
||||
@@ -0,0 +1,3 @@
|
||||
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
||||
@@ -0,0 +1,3 @@
|
||||
@props(['disabled' => false])
|
||||
|
||||
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm']) }}>
|
||||
@@ -0,0 +1,17 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Dashboard') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
{{ __("You're logged in!") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
@include('layouts.navigation')
|
||||
|
||||
<!-- Page Heading -->
|
||||
@isset($header)
|
||||
<header class="bg-white dark:bg-gray-800 shadow">
|
||||
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
{{ $header }}
|
||||
</div>
|
||||
</header>
|
||||
@endisset
|
||||
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="font-sans text-gray-900 antialiased">
|
||||
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
|
||||
<div>
|
||||
<a href="/">
|
||||
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,132 @@
|
||||
<nav x-data="{ open: false }" class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
|
||||
<!-- Primary Navigation Menu -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<!-- Logo -->
|
||||
<div class="shrink-0 flex items-center">
|
||||
<a href="{{ route('home') }}" class="flex items-center space-x-2">
|
||||
<span class="text-2xl">🍽️</span>
|
||||
<span class="text-xl font-bold text-gray-800 dark:text-gray-200">DailyDish</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<x-nav-link :href="route('home')" :active="request()->routeIs('home') || request()->routeIs('recipes.index')">
|
||||
{{ __('Recipes') }}
|
||||
</x-nav-link>
|
||||
@auth
|
||||
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||
{{ __('My Favorites') }}
|
||||
</x-nav-link>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
@auth
|
||||
<x-dropdown align="right" width="48">
|
||||
<x-slot name="trigger">
|
||||
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
|
||||
<div>{{ Auth::user()->name }}</div>
|
||||
|
||||
<div class="ms-1">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<x-dropdown-link :href="route('profile.edit')">
|
||||
{{ __('Profile') }}
|
||||
</x-dropdown-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<x-dropdown-link :href="route('logout')"
|
||||
onclick="event.preventDefault();
|
||||
this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-dropdown-link>
|
||||
</form>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
@else
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="{{ route('login') }}" class="text-sm text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100">Log in</a>
|
||||
<a href="{{ route('register') }}" class="text-sm text-white bg-orange-600 hover:bg-orange-700 px-4 py-2 rounded-md">Register</a>
|
||||
</div>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
<!-- Hamburger -->
|
||||
<div class="-me-2 flex items-center sm:hidden">
|
||||
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out">
|
||||
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Navigation Menu -->
|
||||
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
<x-responsive-nav-link :href="route('home')" :active="request()->routeIs('home') || request()->routeIs('recipes.index')">
|
||||
{{ __('Recipes') }}
|
||||
</x-responsive-nav-link>
|
||||
@auth
|
||||
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||
{{ __('My Favorites') }}
|
||||
</x-responsive-nav-link>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
@auth
|
||||
<!-- Responsive Settings Options -->
|
||||
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
|
||||
<div class="px-4">
|
||||
<div class="font-medium text-base text-gray-800 dark:text-gray-200">{{ Auth::user()->name }}</div>
|
||||
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<x-responsive-nav-link :href="route('profile.edit')">
|
||||
{{ __('Profile') }}
|
||||
</x-responsive-nav-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<x-responsive-nav-link :href="route('logout')"
|
||||
onclick="event.preventDefault();
|
||||
this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-responsive-nav-link>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<!-- Guest Links -->
|
||||
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
|
||||
<div class="mt-3 space-y-1">
|
||||
<x-responsive-nav-link :href="route('login')">
|
||||
{{ __('Log in') }}
|
||||
</x-responsive-nav-link>
|
||||
<x-responsive-nav-link :href="route('register')">
|
||||
{{ __('Register') }}
|
||||
</x-responsive-nav-link>
|
||||
</div>
|
||||
</div>
|
||||
@endauth
|
||||
</div>
|
||||
</nav>
|
||||
@@ -0,0 +1,29 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Profile') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.update-profile-information-form')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.update-password-form')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.delete-user-form')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,55 @@
|
||||
<section class="space-y-6">
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('Delete Account') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<x-danger-button
|
||||
x-data=""
|
||||
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')"
|
||||
>{{ __('Delete Account') }}</x-danger-button>
|
||||
|
||||
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
|
||||
<form method="post" action="{{ route('profile.destroy') }}" class="p-6">
|
||||
@csrf
|
||||
@method('delete')
|
||||
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('Are you sure you want to delete your account?') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
|
||||
</p>
|
||||
|
||||
<div class="mt-6">
|
||||
<x-input-label for="password" value="{{ __('Password') }}" class="sr-only" />
|
||||
|
||||
<x-text-input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
class="mt-1 block w-3/4"
|
||||
placeholder="{{ __('Password') }}"
|
||||
/>
|
||||
|
||||
<x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<x-secondary-button x-on:click="$dispatch('close')">
|
||||
{{ __('Cancel') }}
|
||||
</x-secondary-button>
|
||||
|
||||
<x-danger-button class="ms-3">
|
||||
{{ __('Delete Account') }}
|
||||
</x-danger-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-modal>
|
||||
</section>
|
||||
@@ -0,0 +1,48 @@
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('Update Password') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('Ensure your account is using a long, random password to stay secure.') }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form method="post" action="{{ route('password.update') }}" class="mt-6 space-y-6">
|
||||
@csrf
|
||||
@method('put')
|
||||
|
||||
<div>
|
||||
<x-input-label for="update_password_current_password" :value="__('Current Password')" />
|
||||
<x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
|
||||
<x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="update_password_password" :value="__('New Password')" />
|
||||
<x-text-input id="update_password_password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
|
||||
<x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" />
|
||||
<x-text-input id="update_password_password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
|
||||
<x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<x-primary-button>{{ __('Save') }}</x-primary-button>
|
||||
|
||||
@if (session('status') === 'password-updated')
|
||||
<p
|
||||
x-data="{ show: true }"
|
||||
x-show="show"
|
||||
x-transition
|
||||
x-init="setTimeout(() => show = false, 2000)"
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
>{{ __('Saved.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -0,0 +1,64 @@
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('Profile Information') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __("Update your account's profile information and email address.") }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form id="send-verification" method="post" action="{{ route('verification.send') }}">
|
||||
@csrf
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{ route('profile.update') }}" class="mt-6 space-y-6">
|
||||
@csrf
|
||||
@method('patch')
|
||||
|
||||
<div>
|
||||
<x-input-label for="name" :value="__('Name')" />
|
||||
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autofocus autocomplete="name" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('name')" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)" required autocomplete="username" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('email')" />
|
||||
|
||||
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
|
||||
<div>
|
||||
<p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
|
||||
{{ __('Your email address is unverified.') }}
|
||||
|
||||
<button form="send-verification" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@if (session('status') === 'verification-link-sent')
|
||||
<p class="mt-2 font-medium text-sm text-green-600 dark:text-green-400">
|
||||
{{ __('A new verification link has been sent to your email address.') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<x-primary-button>{{ __('Save') }}</x-primary-button>
|
||||
|
||||
@if (session('status') === 'profile-updated')
|
||||
<p
|
||||
x-data="{ show: true }"
|
||||
x-show="show"
|
||||
x-transition
|
||||
x-init="setTimeout(() => show = false, 2000)"
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
>{{ __('Saved.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -0,0 +1,227 @@
|
||||
<x-app-layout>
|
||||
<div class="min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-red-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
|
||||
<!-- Hero Section -->
|
||||
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm shadow-sm">
|
||||
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
||||
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Discover Delicious Recipes
|
||||
</h1>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300">
|
||||
Explore our collection of homemade recipes, find your next favorite dish
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="lg:grid lg:grid-cols-4 lg:gap-8">
|
||||
<!-- Filters Sidebar -->
|
||||
<div class="lg:col-span-1 mb-8 lg:mb-0">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 sticky top-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Filters</h2>
|
||||
|
||||
<form method="GET" action="{{ route('recipes.index') }}" class="space-y-6">
|
||||
<!-- Search -->
|
||||
<div>
|
||||
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Search
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
id="search"
|
||||
value="{{ request('search') }}"
|
||||
placeholder="Search recipes..."
|
||||
class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-orange-500 focus:ring-orange-500"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Category -->
|
||||
<div>
|
||||
<label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
name="category"
|
||||
id="category"
|
||||
class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-orange-500 focus:ring-orange-500"
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
@foreach($categories as $category)
|
||||
<option value="{{ $category }}" {{ request('category') == $category ? 'selected' : '' }}>
|
||||
{{ $category }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Cuisine -->
|
||||
<div>
|
||||
<label for="cuisine" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Cuisine
|
||||
</label>
|
||||
<select
|
||||
name="cuisine"
|
||||
id="cuisine"
|
||||
class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-orange-500 focus:ring-orange-500"
|
||||
>
|
||||
<option value="">All Cuisines</option>
|
||||
@foreach($cuisines as $cuisine)
|
||||
<option value="{{ $cuisine }}" {{ request('cuisine') == $cuisine ? 'selected' : '' }}>
|
||||
{{ $cuisine }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Difficulty -->
|
||||
<div>
|
||||
<label for="difficulty" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Difficulty
|
||||
</label>
|
||||
<select
|
||||
name="difficulty"
|
||||
id="difficulty"
|
||||
class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-orange-500 focus:ring-orange-500"
|
||||
>
|
||||
<option value="">All Levels</option>
|
||||
@foreach($difficulties as $difficulty)
|
||||
<option value="{{ $difficulty }}" {{ request('difficulty') == $difficulty ? 'selected' : '' }}>
|
||||
{{ $difficulty }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Sort -->
|
||||
<div>
|
||||
<label for="sort" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Sort By
|
||||
</label>
|
||||
<select
|
||||
name="sort"
|
||||
id="sort"
|
||||
class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-orange-500 focus:ring-orange-500"
|
||||
>
|
||||
<option value="latest" {{ request('sort') == 'latest' ? 'selected' : '' }}>Latest</option>
|
||||
<option value="popular" {{ request('sort') == 'popular' ? 'selected' : '' }}>Most Popular</option>
|
||||
<option value="rated" {{ request('sort') == 'rated' ? 'selected' : '' }}>Highest Rated</option>
|
||||
<option value="title" {{ request('sort') == 'title' ? 'selected' : '' }}>Alphabetical</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="flex-1 bg-orange-600 hover:bg-orange-700 text-white font-medium py-2 px-4 rounded-lg transition duration-150"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
<a
|
||||
href="{{ route('recipes.index') }}"
|
||||
class="flex-1 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 font-medium py-2 px-4 rounded-lg text-center transition duration-150"
|
||||
>
|
||||
Clear
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recipe Grid -->
|
||||
<div class="lg:col-span-3">
|
||||
@if($recipes->count() > 0)
|
||||
<div class="grid md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
@foreach($recipes as $recipe)
|
||||
<a href="{{ route('recipes.show', $recipe->slug) }}" class="group">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-md overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
|
||||
<!-- Image -->
|
||||
<div class="relative h-48 bg-gradient-to-br from-orange-200 to-red-200 dark:from-orange-900 dark:to-red-900 overflow-hidden">
|
||||
@if($recipe->getRecipeImage())
|
||||
<img
|
||||
src="{{ $recipe->getRecipeImage() }}"
|
||||
alt="{{ $recipe->title }}"
|
||||
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
||||
>
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-6xl">
|
||||
🍳
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Difficulty Badge -->
|
||||
@if($recipe->difficulty)
|
||||
<div class="absolute top-3 right-3 bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm px-3 py-1 rounded-full text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ $recipe->difficulty }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-5">
|
||||
<h3 class="font-bold text-lg text-gray-900 dark:text-white mb-2 line-clamp-2 group-hover:text-orange-600 dark:group-hover:text-orange-400 transition-colors">
|
||||
{{ $recipe->title }}
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
|
||||
{{ $recipe->description }}
|
||||
</p>
|
||||
|
||||
<!-- Meta Info -->
|
||||
<div class="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
|
||||
<div class="flex items-center space-x-4">
|
||||
@if($recipe->prep_time || $recipe->cook_time)
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
{{ ($recipe->prep_time ?? 0) + ($recipe->cook_time ?? 0) }}m
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if($recipe->rating_avg)
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
|
||||
</svg>
|
||||
{{ number_format($recipe->rating_avg, 1) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
@if($recipe->tags->count() > 0)
|
||||
<div class="mt-3 flex flex-wrap gap-1">
|
||||
@foreach($recipe->tags->take(3) as $tag)
|
||||
<span class="text-xs bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-300 px-2 py-1 rounded-full">
|
||||
{{ $tag->name }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-8">
|
||||
{{ $recipes->links() }}
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-md p-12 text-center">
|
||||
<div class="text-6xl mb-4">🔍</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">No recipes found</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6">Try adjusting your filters to find what you're looking for</p>
|
||||
<a href="{{ route('recipes.index') }}" class="inline-block bg-orange-600 hover:bg-orange-700 text-white font-medium py-2 px-6 rounded-lg transition duration-150">
|
||||
Clear Filters
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,390 @@
|
||||
<x-app-layout>
|
||||
<div class="min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-red-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Recipe Header -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl overflow-hidden mb-8">
|
||||
<div class="md:flex">
|
||||
<!-- Image -->
|
||||
<div class="md:w-1/2 relative">
|
||||
<div class="h-96 bg-gradient-to-br from-orange-200 to-red-200 dark:from-orange-900 dark:to-red-900">
|
||||
@if($recipe->getRecipeImage())
|
||||
<img src="{{ $recipe->getRecipeImage() }}" alt="{{ $recipe->title }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-9xl">🍳</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="md:w-1/2 p-8">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex-1">
|
||||
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-2">{{ $recipe->title }}</h1>
|
||||
|
||||
<!-- Rating & Views -->
|
||||
<div class="flex items-center space-x-4 text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
@if($recipe->rating_avg)
|
||||
<div class="flex items-center">
|
||||
@for($i = 1; $i <= 5; $i++)
|
||||
<svg class="w-5 h-5 {{ $i <= round($recipe->rating_avg) ? 'text-yellow-400' : 'text-gray-300 dark:text-gray-600' }}" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
|
||||
</svg>
|
||||
@endfor
|
||||
<span class="ml-2">{{ number_format($recipe->rating_avg, 1) }} ({{ $recipe->rating_count }} {{ Str::plural('rating', $recipe->rating_count) }})</span>
|
||||
</div>
|
||||
@endif
|
||||
<span>{{ $recipe->views_count }} views</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Favorite Button -->
|
||||
@auth
|
||||
<button
|
||||
onclick="toggleFavorite({{ $recipe->id }})"
|
||||
id="favorite-btn"
|
||||
class="ml-4 p-3 rounded-full {{ $recipe->isFavoritedBy(auth()->user()) ? 'bg-red-100 dark:bg-red-900/30' : 'bg-gray-100 dark:bg-gray-700' }} hover:scale-110 transition-all"
|
||||
>
|
||||
<svg class="w-6 h-6 {{ $recipe->isFavoritedBy(auth()->user()) ? 'text-red-600 dark:text-red-400' : 'text-gray-600 dark:text-gray-400' }} fill-current" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6 text-lg">{{ $recipe->description }}</p>
|
||||
|
||||
<!-- Quick Info -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
@if($recipe->prep_time)
|
||||
<div class="text-center p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
|
||||
<div class="text-2xl font-bold text-orange-600 dark:text-orange-400">{{ $recipe->prep_time }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Prep Time</div>
|
||||
</div>
|
||||
@endif
|
||||
@if($recipe->cook_time)
|
||||
<div class="text-center p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
|
||||
<div class="text-2xl font-bold text-orange-600 dark:text-orange-400">{{ $recipe->cook_time }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Cook Time</div>
|
||||
</div>
|
||||
@endif
|
||||
@if($recipe->servings)
|
||||
<div class="text-center p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
|
||||
<div class="text-2xl font-bold text-orange-600 dark:text-orange-400">{{ $recipe->servings }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Servings</div>
|
||||
</div>
|
||||
@endif
|
||||
@if($recipe->difficulty)
|
||||
<div class="text-center p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
|
||||
<div class="text-lg font-bold text-orange-600 dark:text-orange-400">{{ $recipe->difficulty }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Difficulty</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
@if($recipe->tags->count() > 0)
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($recipe->tags as $tag)
|
||||
<span class="px-3 py-1 bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-300 text-sm rounded-full">
|
||||
{{ $tag->name }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid lg:grid-cols-3 gap-8">
|
||||
<!-- Main Content -->
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
<!-- Ingredients -->
|
||||
@if($recipe->ingredients->count() > 0)
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6 flex items-center">
|
||||
<span class="text-3xl mr-3">🛒</span>
|
||||
Ingredients
|
||||
</h2>
|
||||
<ul class="space-y-3">
|
||||
@foreach($recipe->ingredients->sortBy('pivot.sort_order') as $ingredient)
|
||||
<li class="flex items-start">
|
||||
<svg class="w-5 h-5 text-orange-600 dark:text-orange-400 mr-3 mt-1 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
<strong>{{ $ingredient->pivot->quantity }} {{ $ingredient->pivot->unit }}</strong> {{ $ingredient->name }}
|
||||
@if($ingredient->pivot->notes)
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">({{ $ingredient->pivot->notes }})</span>
|
||||
@endif
|
||||
</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Instructions -->
|
||||
@if($recipe->steps->count() > 0)
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6 flex items-center">
|
||||
<span class="text-3xl mr-3">👨🍳</span>
|
||||
Instructions
|
||||
</h2>
|
||||
<ol class="space-y-6">
|
||||
@foreach($recipe->steps->sortBy('sort_order') as $step)
|
||||
<li class="flex">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-orange-600 dark:bg-orange-500 text-white rounded-full flex items-center justify-center font-bold mr-4">
|
||||
{{ $step->step_number }}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
@if($step->title)
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">{{ $step->title }}</h4>
|
||||
@endif
|
||||
<p class="text-gray-700 dark:text-gray-300">{{ $step->instruction }}</p>
|
||||
@if($step->time_estimate)
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">⏱️ ~{{ $step->time_estimate }} minutes</p>
|
||||
@endif
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Comments Section -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6 flex items-center">
|
||||
<span class="text-3xl mr-3">💬</span>
|
||||
Comments ({{ $recipe->comments->count() }})
|
||||
</h2>
|
||||
|
||||
<!-- Add Comment Form -->
|
||||
@auth
|
||||
<form onsubmit="submitComment(event, {{ $recipe->id }})" class="mb-8">
|
||||
<textarea
|
||||
id="comment-input"
|
||||
rows="3"
|
||||
placeholder="Share your thoughts about this recipe..."
|
||||
class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-orange-500 focus:ring-orange-500"
|
||||
required
|
||||
></textarea>
|
||||
<button
|
||||
type="submit"
|
||||
class="mt-2 bg-orange-600 hover:bg-orange-700 text-white font-medium py-2 px-6 rounded-lg transition duration-150"
|
||||
>
|
||||
Post Comment
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<div class="mb-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg text-center">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
<a href="{{ route('login') }}" class="text-orange-600 dark:text-orange-400 hover:underline">Log in</a> to leave a comment
|
||||
</p>
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
<!-- Comments List -->
|
||||
<div id="comments-list" class="space-y-4">
|
||||
@forelse($recipe->comments as $comment)
|
||||
<div class="border-b border-gray-200 dark:border-gray-700 pb-4 last:border-0">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 bg-orange-500 rounded-full flex items-center justify-center text-white font-bold mr-3">
|
||||
{{ strtoupper(substr($comment->user->name, 0, 1)) }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ $comment->user->name }}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">{{ $comment->created_at->diffForHumans() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-gray-300 ml-13">{{ $comment->comment }}</p>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-500 dark:text-gray-400 text-center py-8">No comments yet. Be the first to comment!</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="lg:col-span-1 space-y-6">
|
||||
<!-- Nutrition -->
|
||||
@if($recipe->nutrition)
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-4 flex items-center">
|
||||
<span class="text-2xl mr-2">📊</span>
|
||||
Nutrition Facts
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
@if($recipe->nutrition->calories)
|
||||
<div class="flex justify-between items-center border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
<span class="text-gray-600 dark:text-gray-400">Calories</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $recipe->nutrition->calories }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($recipe->nutrition->protein)
|
||||
<div class="flex justify-between items-center border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
<span class="text-gray-600 dark:text-gray-400">Protein</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $recipe->nutrition->protein }}g</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($recipe->nutrition->carbohydrates)
|
||||
<div class="flex justify-between items-center border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
<span class="text-gray-600 dark:text-gray-400">Carbs</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $recipe->nutrition->carbohydrates }}g</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($recipe->nutrition->fat)
|
||||
<div class="flex justify-between items-center border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
<span class="text-gray-600 dark:text-gray-400">Fat</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $recipe->nutrition->fat }}g</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Rate this Recipe -->
|
||||
@auth
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Rate this Recipe</h3>
|
||||
<div class="flex justify-center space-x-2" id="rating-stars">
|
||||
@for($i = 1; $i <= 5; $i++)
|
||||
<button
|
||||
onclick="rateRecipe({{ $recipe->id }}, {{ $i }})"
|
||||
class="text-4xl {{ ($recipe->userRating(auth()->user()) >= $i) ? 'text-yellow-400' : 'text-gray-300 dark:text-gray-600' }} hover:text-yellow-400 transition-colors"
|
||||
>
|
||||
★
|
||||
</button>
|
||||
@endfor
|
||||
</div>
|
||||
@if($recipe->userRating(auth()->user()))
|
||||
<p class="text-sm text-center text-gray-600 dark:text-gray-400 mt-2">
|
||||
You rated this {{ $recipe->userRating(auth()->user()) }} star{{ $recipe->userRating(auth()->user()) > 1 ? 's' : '' }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
<!-- Similar Recipes -->
|
||||
@if($similarRecipes->count() > 0)
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Similar Recipes</h3>
|
||||
<div class="space-y-4">
|
||||
@foreach($similarRecipes as $similar)
|
||||
<a href="{{ route('recipes.show', $similar->slug) }}" class="block group">
|
||||
<div class="flex space-x-3">
|
||||
<div class="w-20 h-20 bg-gradient-to-br from-orange-200 to-red-200 dark:from-orange-900 dark:to-red-900 rounded-lg overflow-hidden flex-shrink-0">
|
||||
@if($similar->getRecipeImage())
|
||||
<img src="{{ $similar->getRecipeImage() }}" alt="{{ $similar->title }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-2xl">🍳</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white group-hover:text-orange-600 dark:group-hover:text-orange-400 transition-colors line-clamp-2">
|
||||
{{ $similar->title }}
|
||||
</h4>
|
||||
@if($similar->rating_avg)
|
||||
<div class="flex items-center mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<svg class="w-4 h-4 text-yellow-400 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
|
||||
</svg>
|
||||
{{ number_format($similar->rating_avg, 1) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@auth
|
||||
<script>
|
||||
function toggleFavorite(recipeId) {
|
||||
fetch(`/recipes/${recipeId}/favorite`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const btn = document.getElementById('favorite-btn');
|
||||
const svg = btn.querySelector('svg');
|
||||
|
||||
if (data.is_favorited) {
|
||||
btn.classList.remove('bg-gray-100', 'dark:bg-gray-700');
|
||||
btn.classList.add('bg-red-100', 'dark:bg-red-900/30');
|
||||
svg.classList.remove('text-gray-600', 'dark:text-gray-400');
|
||||
svg.classList.add('text-red-600', 'dark:text-red-400');
|
||||
} else {
|
||||
btn.classList.remove('bg-red-100', 'dark:bg-red-900/30');
|
||||
btn.classList.add('bg-gray-100', 'dark:bg-gray-700');
|
||||
svg.classList.remove('text-red-600', 'dark:text-red-400');
|
||||
svg.classList.add('text-gray-600', 'dark:text-gray-400');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function rateRecipe(recipeId, rating) {
|
||||
fetch(`/recipes/${recipeId}/rate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ rating })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Update star display
|
||||
const stars = document.querySelectorAll('#rating-stars button');
|
||||
stars.forEach((star, index) => {
|
||||
if (index < rating) {
|
||||
star.classList.remove('text-gray-300', 'dark:text-gray-600');
|
||||
star.classList.add('text-yellow-400');
|
||||
} else {
|
||||
star.classList.remove('text-yellow-400');
|
||||
star.classList.add('text-gray-300', 'dark:text-gray-600');
|
||||
}
|
||||
});
|
||||
|
||||
// Show success message
|
||||
alert('Thank you for rating this recipe!');
|
||||
});
|
||||
}
|
||||
|
||||
function submitComment(event, recipeId) {
|
||||
event.preventDefault();
|
||||
|
||||
const commentInput = document.getElementById('comment-input');
|
||||
const comment = commentInput.value;
|
||||
|
||||
fetch(`/recipes/${recipeId}/comment`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ comment })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Reload the page to show new comment
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endauth
|
||||
</x-app-layout>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user