Compare commits

...

2 Commits

Author SHA1 Message Date
7e382ffe1d Redesign nav search 2025-10-23 15:39:07 +02:00
6a25fd2700 Use meilisearch in nav search 2025-10-23 15:30:32 +02:00
8 changed files with 749 additions and 65 deletions

View File

@@ -57,3 +57,8 @@ VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
SCOUT_QUEUE=true
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=masterKey

View File

@@ -28,6 +28,10 @@ apt install php8.3-fpm
apt install mariadb-server
sudo mysql_secure_installation
# Install Meilisearch
echo "deb [trusted=yes] https://apt.fury.io/meilisearch/ /" | sudo tee /etc/apt/sources.list.d/fury.list
sudo apt update && sudo apt install meilisearch
# Clone Repo
cd /var/www
git clone https://gitea.hstream.moe/w33b/hstream.git

View File

@@ -3,7 +3,6 @@
namespace App\Livewire;
use App\Models\Episode;
use App\Models\Gallery;
use Livewire\Component;
use Illuminate\Support\Facades\Auth;
@@ -18,22 +17,15 @@ class NavLiveSearch extends Component
public function render()
{
$episodes = [];
$randomimage = null;
if ($this->navSearch != '') {
$episodes = Episode::with('gallery')->where('title', 'like', '%'.$this->navSearch.'%')
->orWhere('title_jpn', 'like', '%'.$this->navSearch.'%')
->when(Auth::guest(), fn ($query) => $query->withoutTags(['loli', 'shota']))
->take(10)
$episodes = Episode::search($this->navSearch)
->when(Auth::guest(), fn ($query) => $query->whereNotIn('tags', ['Loli', 'Shota']))
->take(7)
->get();
$randomimage = Gallery::all()
->random(1)
->first();
}
return view('livewire.nav-live-search', [
'episodes' => $episodes,
'randomimage' => $randomimage,
'query' => $this->navSearch,
'hide' => empty($this->navSearch),
]);

View File

@@ -9,6 +9,7 @@ use App\Models\PopularDaily;
use Conner\Tagging\Taggable;
use Laravelista\Comments\Commentable;
use Laravel\Scout\Searchable;
use Maize\Markable\Markable;
use Maize\Markable\Models\Like;
@@ -26,11 +27,40 @@ class Episode extends Model implements Sitemapable
{
use Commentable, Markable, Taggable;
use HasFactory;
use Searchable;
protected static $marks = [
Like::class
];
/**
* Get the name of the index associated with the model.
*/
public function searchableAs(): string
{
return 'episodes_index';
}
/**
* Get the indexable data array for the model.
*
* @return array<string, mixed>
*/
public function toSearchableArray()
{
return [
'title' => $this->title,
'title_search' => $this->title_search,
'title_jpn' => $this->title_jpn,
'slug' => $this->slug,
'description' => $this->description,
'view_count' => $this->view_count,
'tags' => $this->tagNames(),
'release_date' => $this->release_date,
'created_at' => $this->created_at,
];
}
/**
* Get the studio for the Hentai.
*/

View File

@@ -11,15 +11,18 @@
"php": "^8.2",
"guzzlehttp/guzzle": "^7.8.1",
"hisorange/browser-detect": "^5.0",
"http-interop/http-factory-guzzle": "^1.2",
"intervention/image": "^3.9",
"intervention/image-laravel": "^1.3",
"jakyeru/larascord": "^6.0",
"laravel/framework": "^11.0",
"laravel/sanctum": "^4.0",
"laravel/scout": "^10.20",
"laravel/tinker": "^2.10",
"laravelista/comments": "dev-l11-compatibility",
"livewire/livewire": "^3.6.4",
"maize-tech/laravel-markable": "^2.3.0",
"meilisearch/meilisearch-php": "^1.16",
"mews/captcha": "3.4.4",
"predis/predis": "^2.2",
"realrashid/sweet-alert": "^7.2",

380
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0a70102075d514ad96b36656bb447aa3",
"content-hash": "484d21a7c10b1609a22d642e71a71cc3",
"packages": [
{
"name": "brick/math",
@@ -1247,6 +1247,64 @@
},
"time": "2024-02-05T08:21:06+00:00"
},
{
"name": "http-interop/http-factory-guzzle",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/http-interop/http-factory-guzzle.git",
"reference": "8f06e92b95405216b237521cc64c804dd44c4a81"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/8f06e92b95405216b237521cc64c804dd44c4a81",
"reference": "8f06e92b95405216b237521cc64c804dd44c4a81",
"shasum": ""
},
"require": {
"guzzlehttp/psr7": "^1.7||^2.0",
"php": ">=7.3",
"psr/http-factory": "^1.0"
},
"provide": {
"psr/http-factory-implementation": "^1.0"
},
"require-dev": {
"http-interop/http-factory-tests": "^0.9",
"phpunit/phpunit": "^9.5"
},
"suggest": {
"guzzlehttp/psr7": "Includes an HTTP factory starting in version 2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Http\\Factory\\Guzzle\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "An HTTP Factory using Guzzle PSR7",
"keywords": [
"factory",
"http",
"psr-17",
"psr-7"
],
"support": {
"issues": "https://github.com/http-interop/http-factory-guzzle/issues",
"source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.0"
},
"time": "2021-07-21T13:50:14+00:00"
},
{
"name": "intervention/gif",
"version": "4.2.2",
@@ -1979,6 +2037,87 @@
},
"time": "2025-07-09T19:45:24+00:00"
},
{
"name": "laravel/scout",
"version": "v10.20.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/scout.git",
"reference": "a04d7a8eb27b66c8b7edb7e0c6a078e9e78c4f5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/scout/zipball/a04d7a8eb27b66c8b7edb7e0c6a078e9e78c4f5b",
"reference": "a04d7a8eb27b66c8b7edb7e0c6a078e9e78c4f5b",
"shasum": ""
},
"require": {
"illuminate/bus": "^9.0|^10.0|^11.0|^12.0",
"illuminate/contracts": "^9.0|^10.0|^11.0|^12.0",
"illuminate/database": "^9.0|^10.0|^11.0|^12.0",
"illuminate/http": "^9.0|^10.0|^11.0|^12.0",
"illuminate/pagination": "^9.0|^10.0|^11.0|^12.0",
"illuminate/queue": "^9.0|^10.0|^11.0|^12.0",
"illuminate/support": "^9.0|^10.0|^11.0|^12.0",
"php": "^8.0",
"symfony/console": "^6.0|^7.0"
},
"conflict": {
"algolia/algoliasearch-client-php": "<3.2.0|>=5.0.0"
},
"require-dev": {
"algolia/algoliasearch-client-php": "^3.2|^4.0",
"meilisearch/meilisearch-php": "^1.0",
"mockery/mockery": "^1.0",
"orchestra/testbench": "^7.31|^8.11|^9.0|^10.0",
"php-http/guzzle7-adapter": "^1.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.3|^10.4|^11.5",
"typesense/typesense-php": "^4.9.3"
},
"suggest": {
"algolia/algoliasearch-client-php": "Required to use the Algolia engine (^3.2).",
"meilisearch/meilisearch-php": "Required to use the Meilisearch engine (^1.0).",
"typesense/typesense-php": "Required to use the Typesense engine (^4.9)."
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Scout\\ScoutServiceProvider"
]
},
"branch-alias": {
"dev-master": "10.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\Scout\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Scout provides a driver based solution to searching your Eloquent models.",
"keywords": [
"algolia",
"laravel",
"search"
],
"support": {
"issues": "https://github.com/laravel/scout/issues",
"source": "https://github.com/laravel/scout"
},
"time": "2025-10-14T14:09:26+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.5",
@@ -3069,6 +3208,86 @@
},
"time": "2025-08-20T17:20:16+00:00"
},
{
"name": "meilisearch/meilisearch-php",
"version": "v1.16.1",
"source": {
"type": "git",
"url": "https://github.com/meilisearch/meilisearch-php.git",
"reference": "f9f63e0e7d12ffaae54f7317fa8f4f4dfa8ae7b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/meilisearch/meilisearch-php/zipball/f9f63e0e7d12ffaae54f7317fa8f4f4dfa8ae7b6",
"reference": "f9f63e0e7d12ffaae54f7317fa8f4f4dfa8ae7b6",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.4 || ^8.0",
"php-http/discovery": "^1.7",
"psr/http-client": "^1.0",
"symfony/polyfill-php81": "^1.33"
},
"require-dev": {
"http-interop/http-factory-guzzle": "^1.2.0",
"php-cs-fixer/shim": "^3.59.3",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.5 || ^10.5",
"symfony/http-client": "^5.4|^6.0|^7.0"
},
"suggest": {
"guzzlehttp/guzzle": "Use Guzzle ^7 as HTTP client",
"http-interop/http-factory-guzzle": "Factory for guzzlehttp/guzzle",
"symfony/http-client": "Use Symfony Http client"
},
"type": "library",
"autoload": {
"psr-4": {
"MeiliSearch\\": "src/",
"Meilisearch\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Clémentine Urquizar",
"email": "clementine@meilisearch.com"
},
{
"name": "Bruno Casali",
"email": "bruno@meilisearch.com"
},
{
"name": "Laurent Cazanove",
"email": "lau.cazanove@gmail.com"
},
{
"name": "Tomas Norkūnas",
"email": "norkunas.tom@gmail.com"
}
],
"description": "PHP wrapper for the Meilisearch API",
"keywords": [
"api",
"client",
"instant",
"meilisearch",
"php",
"search"
],
"support": {
"issues": "https://github.com/meilisearch/meilisearch-php/issues",
"source": "https://github.com/meilisearch/meilisearch-php/tree/v1.16.1"
},
"time": "2025-09-18T10:15:45+00:00"
},
{
"name": "mews/captcha",
"version": "3.4.4",
@@ -3819,6 +4038,85 @@
],
"time": "2025-05-08T08:14:37+00:00"
},
{
"name": "php-http/discovery",
"version": "1.20.0",
"source": {
"type": "git",
"url": "https://github.com/php-http/discovery.git",
"reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d",
"reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.0|^2.0",
"php": "^7.1 || ^8.0"
},
"conflict": {
"nyholm/psr7": "<1.0",
"zendframework/zend-diactoros": "*"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "*",
"psr/http-factory-implementation": "*",
"psr/http-message-implementation": "*"
},
"require-dev": {
"composer/composer": "^1.0.2|^2.0",
"graham-campbell/phpspec-skip-example-extension": "^5.0",
"php-http/httplug": "^1.0 || ^2.0",
"php-http/message-factory": "^1.0",
"phpspec/phpspec": "^5.1 || ^6.1 || ^7.3",
"sebastian/comparator": "^3.0.5 || ^4.0.8",
"symfony/phpunit-bridge": "^6.4.4 || ^7.0.1"
},
"type": "composer-plugin",
"extra": {
"class": "Http\\Discovery\\Composer\\Plugin",
"plugin-optional": true
},
"autoload": {
"psr-4": {
"Http\\Discovery\\": "src/"
},
"exclude-from-classmap": [
"src/Composer/Plugin.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations",
"homepage": "http://php-http.org",
"keywords": [
"adapter",
"client",
"discovery",
"factory",
"http",
"message",
"psr17",
"psr7"
],
"support": {
"issues": "https://github.com/php-http/discovery/issues",
"source": "https://github.com/php-http/discovery/tree/1.20.0"
},
"time": "2024-10-02T11:20:13+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.4",
@@ -6963,6 +7261,86 @@
],
"time": "2025-01-02T08:10:11+00:00"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.33.0",

224
config/scout.php Normal file
View File

@@ -0,0 +1,224 @@
<?php
use App\Models\Episode;
return [
/*
|--------------------------------------------------------------------------
| Default Search Engine
|--------------------------------------------------------------------------
|
| This option controls the default search connection that gets used while
| using Laravel Scout. This connection is used when syncing all models
| to the search service. You should adjust this based on your needs.
|
| Supported: "algolia", "meilisearch", "typesense",
| "database", "collection", "null"
|
*/
'driver' => env('SCOUT_DRIVER', 'collection'),
/*
|--------------------------------------------------------------------------
| Index Prefix
|--------------------------------------------------------------------------
|
| Here you may specify a prefix that will be applied to all search index
| names used by Scout. This prefix may be useful if you have multiple
| "tenants" or applications sharing the same search infrastructure.
|
*/
'prefix' => env('SCOUT_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Queue Data Syncing
|--------------------------------------------------------------------------
|
| This option allows you to control if the operations that sync your data
| with your search engines are queued. When this is set to "true" then
| all automatic data syncing will get queued for better performance.
|
*/
'queue' => env('SCOUT_QUEUE', false),
/*
|--------------------------------------------------------------------------
| Database Transactions
|--------------------------------------------------------------------------
|
| This configuration option determines if your data will only be synced
| with your search indexes after every open database transaction has
| been committed, thus preventing any discarded data from syncing.
|
*/
'after_commit' => false,
/*
|--------------------------------------------------------------------------
| Chunk Sizes
|--------------------------------------------------------------------------
|
| These options allow you to control the maximum chunk size when you are
| mass importing data into the search engine. This allows you to fine
| tune each of these chunk sizes based on the power of the servers.
|
*/
'chunk' => [
'searchable' => 500,
'unsearchable' => 500,
],
/*
|--------------------------------------------------------------------------
| Soft Deletes
|--------------------------------------------------------------------------
|
| This option allows to control whether to keep soft deleted records in
| the search indexes. Maintaining soft deleted records can be useful
| if your application still needs to search for the records later.
|
*/
'soft_delete' => false,
/*
|--------------------------------------------------------------------------
| Identify User
|--------------------------------------------------------------------------
|
| This option allows you to control whether to notify the search engine
| of the user performing the search. This is sometimes useful if the
| engine supports any analytics based on this application's users.
|
| Supported engines: "algolia"
|
*/
'identify' => env('SCOUT_IDENTIFY', false),
/*
|--------------------------------------------------------------------------
| Algolia Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your Algolia settings. Algolia is a cloud hosted
| search engine which works great with Scout out of the box. Just plug
| in your application ID and admin API key to get started searching.
|
*/
'algolia' => [
'id' => env('ALGOLIA_APP_ID', ''),
'secret' => env('ALGOLIA_SECRET', ''),
'index-settings' => [
// 'users' => [
// 'searchableAttributes' => ['id', 'name', 'email'],
// 'attributesForFaceting'=> ['filterOnly(email)'],
// ],
],
],
/*
|--------------------------------------------------------------------------
| Meilisearch Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your Meilisearch settings. Meilisearch is an open
| source search engine with minimal configuration. Below, you can state
| the host and key information for your own Meilisearch installation.
|
| See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options
|
*/
'meilisearch' => [
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
'key' => env('MEILISEARCH_KEY'),
'index-settings' => [
Episode::class => [
'filterableAttributes' => [
'title',
'title_search',
'title_jpn',
'slug',
'description',
'tags'
],
'sortableAttributes' => [
'created_at',
'release_date',
'view_count',
'title'
],
],
],
],
/*
|--------------------------------------------------------------------------
| Typesense Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your Typesense settings. Typesense is an open
| source search engine using minimal configuration. Below, you will
| state the host, key, and schema configuration for the instance.
|
*/
'typesense' => [
'client-settings' => [
'api_key' => env('TYPESENSE_API_KEY', 'xyz'),
'nodes' => [
[
'host' => env('TYPESENSE_HOST', 'localhost'),
'port' => env('TYPESENSE_PORT', '8108'),
'path' => env('TYPESENSE_PATH', ''),
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
],
],
'nearest_node' => [
'host' => env('TYPESENSE_HOST', 'localhost'),
'port' => env('TYPESENSE_PORT', '8108'),
'path' => env('TYPESENSE_PATH', ''),
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
],
'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2),
'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30),
'num_retries' => env('TYPESENSE_NUM_RETRIES', 3),
'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1),
],
// 'max_total_results' => env('TYPESENSE_MAX_TOTAL_RESULTS', 1000),
'model-settings' => [
// User::class => [
// 'collection-schema' => [
// 'fields' => [
// [
// 'name' => 'id',
// 'type' => 'string',
// ],
// [
// 'name' => 'name',
// 'type' => 'string',
// ],
// [
// 'name' => 'created_at',
// 'type' => 'int64',
// ],
// ],
// 'default_sorting_field' => 'created_at',
// ],
// 'search-parameters' => [
// 'query_by' => 'name'
// ],
// ],
],
],
];

View File

@@ -1,63 +1,111 @@
<div class="flex items-center">
<form method="POST" action="{{ route('hentai.searchredirect') }}">
<div class="flex items-center relative">
<form method="POST" action="{{ route('hentai.searchredirect') }}" class="w-full">
@csrf
<label for="live-search" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="absolute right-2 left-2 sm:relative sm:min-w-[200px] md:min-w-[300px] lg:min-w-[400px] xl:min-w-[500px] transition-all">
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
</svg>
</div>
<input wire:model.live.debounce.600ms="navSearch" type="search" id="live-search" name="live-search" class="block p-4 pl-10 w-full text-sm text-gray-900 rounded-lg border border-gray-300/50 bg-gray-50/20 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900/40 dark:border-neutral-600/50 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900" placeholder="@if(request()->path() !== 'search'){{ __('search.search-hentai') }}@endif" required @if(request()->path() == 'search') disabled @endif>
<button type="submit" class="absolute right-2.5 bottom-2.5 px-4 py-2 text-sm font-medium text-white bg-rose-700 rounded-lg hover:bg-rose-800 disabled:bg-gray-300 disabled:hover:bg-gray-300 disabled:dark:bg-gray-500 disabled:dark:hover:bg-gray-500 focus:ring-4 focus:outline-none focus:ring-rose-300 dark:bg-rose-600 dark:hover:bg-rose-700 dark:focus:ring-rose-800" @if(request()->path() == 'search') disabled @endif>{{ __('search.search') }}</button>
</div>
</form>
<div class="relative group">
<label for="live-search" class="sr-only">Search</label>
<div class="relative w-full sm:min-w-[200px] md:min-w-[300px] lg:min-w-[400px] xl:min-w-[500px]">
{{-- Search Icon --}}
<div class="pointer-events-none absolute inset-y-0 left-0 pl-3 flex items-center">
<svg class="w-4 h-4 text-gray-400 dark:text-gray-300" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
</svg>
</div>
@if((! $hide) && request()->path() != 'search' && request()->path() != 'download-search')
<!-- BG Blur and BG Size -->
<div class="absolute left-0 sm:top-[65px] w-[100%] h-[calc(100vh-60px)] z-40 text-gray-900 dark:text-white bg-neutral-100/80 dark:bg-neutral-900/80">
<div class="flex justify-center items-center">
<!-- Padding for Grid -->
<div class="flex justify-center w-5/6">
<div class="grid grid-cols-1 gap-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
@foreach($episodes as $episode)
<div class="relative p-1 mb-8 w-full transition duration-300 ease-in-out md:p-2 hover:-translate-y-1 hover:scale-110">
<a class="hover:text-blue-600" href="{{ route('hentai.index', ['title' => $episode->slug ]) }}">
<div class="absolute w-[95%] top-[38%] text-center z-10">
<svg aria-hidden="true" class="inline mr-2 w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-pink-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
{{-- Search Input --}}
<input
wire:model.live.debounce.600ms="navSearch"
type="search"
id="live-search"
name="live-search"
class="block w-full pl-10 pr-28 py-3 text-sm rounded-2xl border border-gray-200 bg-white/80 dark:bg-neutral-900/50 dark:border-neutral-700 placeholder-gray-400 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-rose-600 focus:border-rose-700 transition"
placeholder="@if(request()->path() !== 'search'){{ __('search.search-hentai') }}@endif"
required
@if(request()->path() == 'search') disabled @endif
aria-autocomplete="list"
aria-label="Search"
>
{{-- Submit button to redirect to advanced search --}}
<button
type="submit"
class="absolute right-1 top-1/2 -translate-y-1/2 px-4 py-2 text-sm font-medium rounded-xl bg-rose-700 text-white hover:bg-rose-800 focus:outline-none focus:ring-2 focus:ring-rose-300 disabled:bg-gray-300 disabled:hover:bg-gray-300 disabled:dark:bg-gray-500 disabled:dark:hover:bg-gray-500"
@if(request()->path() == 'search') disabled @endif
>{{ __('search.search') }}</button>
</div>
@if((! $hide) && request()->path() != 'search' && request()->path() != 'download-search')
<div
class="pointer-events-auto absolute left-0 right-0 mt-3 z-50 flex justify-center"
aria-live="polite"
>
<div
class="max-h-[70vh] overflow-auto rounded-2xl bg-white dark:bg-neutral-900 border border-gray-200 dark:border-neutral-700 shadow-lg transition-all transform hidden group-focus-within:block group-focus-within:translate-y-0">
<div class="flex items-center justify-between p-3 border-b border-gray-100 dark:border-neutral-800">
<div class="text-sm text-gray-700 dark:text-gray-200 font-medium">
@if($episodes->count())
{{ __('Search result for ') }} {{ $query ?: $navSearch }}
@else
{{ __('No results') }}
@endif
</div>
{{-- Loading indicator using Livewire --}}
<div class="flex items-center gap-2">
<div wire:loading.class.remove="hidden" class="hidden">
<svg class="w-5 h-5 animate-spin text-rose-600" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-opacity="0.2" stroke-width="4"></circle>
<path d="M22 12a10 10 0 0 1-10 10" stroke="currentColor" stroke-width="4" stroke-linecap="round"></path>
</svg>
</div>
<img
alt="{{ $episode->title }} - {{ $episode->episode }}"
loading="lazy"
width="500"
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
src="{{ $episode->gallery->first()->thumbnail_url }}">
</img>
<p class="absolute right-4 top-4 bg-white/80 dark:bg-neutral-700/80 !text-gray-900 dark:!text-white rounded-bl-lg rounded-lg p-1 pr-2 pl-2 font-semibold text-sm">{{ $episode->getResolution() }}</p>
<p class="absolute left-4 bottom-4 bg-white/80 dark:bg-neutral-700/80 !text-gray-900 dark:!text-white rounded-bl-lg rounded-lg p-1 pr-2 pl-2 font-semibold text-sm"><i class="fa-regular fa-eye"></i> {{ $episode->view_count }} <i class="fa-regular fa-heart"></i> {{ count($episode->likes) }}</p>
<p class="absolute w-[95%] text-center text-sm">{{ $episode->title }} - {{ $episode->episode }}</p>
</a>
</div>
</div>
@endforeach
<div class="relative p-1 mb-8 w-full transition duration-300 ease-in-out md:p-2 hover:-translate-y-1 hover:scale-110">
<a class="hover:text-blue-600" href="{{ route('hentai.search', ['s' => $query]) }}">
<img
alt="gallery"
loading="lazy"
width="500"
class="block object-cover object-center rounded-lg aspect-video"
src="{{ $randomimage->thumbnail_url }}">
</img>
<p class="absolute left-2 top-2 w-[95%] h-[91.5%] bg-white/10 dark:bg-neutral-700/10 !text-gray-900 dark:!text-white rounded-bl-lg rounded-lg font-semibold p-4 pr-8 pl-8 text-center"></p>
<p class="absolute left-[20%] top-[35%] bg-white/80 dark:bg-neutral-700/80 !text-gray-900 dark:!text-white rounded-bl-lg rounded-lg font-semibold p-4 pr-8 pl-8 text-center">Advanced Search...</p>
</a>
{{-- content area: responsive grid --}}
<div class="p-4">
@if($episodes->count())
<div class="grid grid-cols-1 gap-4 md:grid-cols-1 lg:grid-cols-2">
@foreach($episodes as $episode)
<a href="{{ route('hentai.index', ['title' => $episode->slug ]) }}" class="group block rounded-xl overflow-hidden bg-neutral-50 dark:bg-neutral-950 border border-transparent hover:border-gray-200 dark:hover:border-neutral-700 shadow-sm hover:shadow-md transition">
<div class="relative aspect-video">
<img
alt="{{ $episode->title }} - {{ $episode->episode }}"
loading="lazy"
class="object-cover w-full h-full"
src="{{ $episode->gallery->first()->thumbnail_url }}"
>
<span class="absolute right-0 top-0 bg-white/90 dark:bg-neutral-800/80 dark:text-white text-xs font-semibold rounded-tr rounded-bl-xl px-2 py-1">{{ $episode->getResolution() }}</span>
<div class="absolute left-0 bottom-0 bg-white/90 dark:bg-neutral-800/80 dark:text-white text-xs rounded-tr-xl px-2 py-1 font-medium">
<i class="fa-regular fa-eye mr-1"></i> {{ $episode->view_count }}
<i class="fa-regular fa-heart ml-2"></i> {{ count($episode->likes) }}
</div>
</div>
<div class="p-3">
<h3 class="text-sm font-semibold truncate text-gray-900 dark:text-white">{{ $episode->title }} - {{ $episode->episode }}</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate"> {{ \Illuminate\Support\Str::limit($episode->description ?? '', 80) }}</p>
</div>
</a>
@endforeach
{{-- Advanced Search card --}}
<a href="{{ route('hentai.search', ['search' => $query]) }}" class="flex items-center justify-center rounded-xl border border-dashed border-gray-200 dark:border-neutral-700 p-6 hover:bg-gray-50 dark:hover:bg-neutral-900 transition">
<div class="text-center">
<div class="text-2xl font-bold text-rose-600 mb-1">🔎</div>
<div class="font-semibold text-sm dark:text-white">Advanced Search</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">View more results</div>
</div>
</a>
</div>
@else
{{-- Empty state --}}
<div class="py-12 text-center text-sm text-gray-600 dark:text-gray-300">
<div class="mb-3">No results found for {{ $query ?: $navSearch }}</div>
<a href="{{ route('hentai.search', ['search' => $navSearch ?: $query]) }}" class="inline-block px-4 py-2 rounded-lg bg-rose-700 text-white text-sm hover:bg-rose-800">Try advanced search</a>
</div>
@endif
</div>
</div>
</div>
@endif
</div>
</div>
@endif
</form>
</div>