Restaurare curata Streamzy - Full Code
18
.editorconfig
Normal file
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
65
.env.example
Normal file
@@ -0,0 +1,65 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
11
.gitattributes
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/framework/cache/data/*
|
||||
/storage/framework/sessions/*
|
||||
/storage/framework/views/*.php
|
||||
/storage/logs/*
|
||||
.env
|
||||
.phpunit.result.cache
|
||||
61
README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||
|
||||
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
|
||||
|
||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
|
||||
## Laravel Sponsors
|
||||
|
||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||
|
||||
### Premium Partners
|
||||
|
||||
- **[Vehikl](https://vehikl.com)**
|
||||
- **[Tighten Co.](https://tighten.co)**
|
||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||
- **[64 Robots](https://64robots.com)**
|
||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
||||
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||
- **[Redberry](https://redberry.international/laravel-development)**
|
||||
- **[Active Logic](https://activelogic.com)**
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
154
app/Http/Controllers/AnalyticsProxyController.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AnalyticsProxyController extends Controller
|
||||
{
|
||||
private string $origin = 'https://deadlocked.fun';
|
||||
|
||||
// GET /stats.js – proxy matomo.js with proper content-type and caching
|
||||
public function script(Request $request)
|
||||
{
|
||||
$url = rtrim($this->origin, '/').'/matomo.js';
|
||||
$body = $this->httpGet($url, [
|
||||
'Accept' => 'application/javascript,*/*;q=0.1',
|
||||
'User-Agent' => $request->header('User-Agent', 'Mozilla/5.0'),
|
||||
'Accept-Language' => $request->header('Accept-Language', 'en-US,en;q=0.9'),
|
||||
'X-Forwarded-For' => $request->ip(),
|
||||
'X-Real-IP' => $request->ip(),
|
||||
]);
|
||||
if ($body === null) {
|
||||
$body = '// analytics disabled';
|
||||
}
|
||||
|
||||
return response($body, 200, [
|
||||
'Content-Type' => 'application/javascript; charset=utf-8',
|
||||
'Cache-Control' => 'public, max-age=3600',
|
||||
]);
|
||||
}
|
||||
|
||||
// GET/POST /stats – proxy matomo.php tracker requests with query/body
|
||||
public function collect(Request $request)
|
||||
{
|
||||
$base = rtrim($this->origin, '/').'/matomo.php';
|
||||
$headers = [
|
||||
'Accept' => 'image/gif,*/*;q=0.1',
|
||||
'User-Agent' => $request->header('User-Agent', 'Mozilla/5.0'),
|
||||
'Accept-Language' => $request->header('Accept-Language', 'en-US,en;q=0.9'),
|
||||
'X-Forwarded-For' => $request->ip(),
|
||||
'X-Real-IP' => $request->ip(),
|
||||
'Referer' => $request->headers->get('referer', url('/')),
|
||||
];
|
||||
|
||||
if ($request->isMethod('get')) {
|
||||
$url = $base;
|
||||
$query = $request->query();
|
||||
if (!empty($query)) {
|
||||
$url .= (str_contains($url, '?') ? '&' : '?') . http_build_query($query);
|
||||
}
|
||||
$resp = $this->httpGet($url, $headers);
|
||||
} else {
|
||||
$resp = $this->httpPostForm($base, $request->all(), $headers);
|
||||
}
|
||||
|
||||
if ($resp === null) {
|
||||
// return empty gif on failure
|
||||
$status = 200;
|
||||
$type = 'image/gif';
|
||||
$body = base64_decode('R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==');
|
||||
} else {
|
||||
$status = 200; // upstream typically returns 200
|
||||
$type = 'image/gif'; // Matomo tracker returns a 1x1 gif
|
||||
$body = $resp;
|
||||
}
|
||||
|
||||
$respHeaders = [
|
||||
'Content-Type' => $type,
|
||||
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
|
||||
'Pragma' => 'no-cache',
|
||||
];
|
||||
return new Response($body, $status, $respHeaders);
|
||||
}
|
||||
|
||||
/** Simple GET via cURL or streams; returns body or null */
|
||||
private function httpGet(string $url, array $headers = [], int $timeout = 5): ?string
|
||||
{
|
||||
$headerLines = [];
|
||||
foreach ($headers as $k => $v) { $headerLines[] = $k . ': ' . $v; }
|
||||
|
||||
if (function_exists('curl_init')) {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => $timeout,
|
||||
CURLOPT_TIMEOUT => $timeout,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_HTTPHEADER => $headerLines,
|
||||
]);
|
||||
$data = curl_exec($ch);
|
||||
$err = curl_errno($ch);
|
||||
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($err || $status < 200 || $status >= 400) { return null; }
|
||||
return is_string($data) ? $data : null;
|
||||
}
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => implode("\r\n", $headerLines),
|
||||
'timeout' => $timeout,
|
||||
],
|
||||
'ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false ],
|
||||
]);
|
||||
$data = @file_get_contents($url, false, $context);
|
||||
return $data === false ? null : $data;
|
||||
}
|
||||
|
||||
/** Simple POST form via cURL or streams; returns body or null */
|
||||
private function httpPostForm(string $url, array $params = [], array $headers = [], int $timeout = 5): ?string
|
||||
{
|
||||
$headers['Content-Type'] = $headers['Content-Type'] ?? 'application/x-www-form-urlencoded';
|
||||
$headerLines = [];
|
||||
foreach ($headers as $k => $v) { $headerLines[] = $k . ': ' . $v; }
|
||||
$postFields = http_build_query($params);
|
||||
|
||||
if (function_exists('curl_init')) {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => $timeout,
|
||||
CURLOPT_TIMEOUT => $timeout,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_HTTPHEADER => $headerLines,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $postFields,
|
||||
]);
|
||||
$data = curl_exec($ch);
|
||||
$err = curl_errno($ch);
|
||||
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($err || $status < 200 || $status >= 400) { return null; }
|
||||
return is_string($data) ? $data : null;
|
||||
}
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => implode("\r\n", $headerLines),
|
||||
'content' => $postFields,
|
||||
'timeout' => $timeout,
|
||||
],
|
||||
'ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false ],
|
||||
]);
|
||||
$data = @file_get_contents($url, false, $context);
|
||||
return $data === false ? null : $data;
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
16
app/Http/Controllers/HistoryController.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HistoryController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the localStorage-based watch history page.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('history.index');
|
||||
}
|
||||
}
|
||||
252
app/Http/Controllers/HomeController.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\VidSrcService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
private VidSrcService $vidSrcService;
|
||||
|
||||
public function __construct(VidSrcService $vidSrcService)
|
||||
{
|
||||
$this->vidSrcService = $vidSrcService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the homepage with trending content
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
// Get latest content for homepage
|
||||
$latestMovies = $this->vidSrcService->getLatestMovies(1);
|
||||
$latestTvShows = $this->vidSrcService->getLatestTvShows(1);
|
||||
$latestEpisodes = $this->vidSrcService->getLatestEpisodes(1);
|
||||
$genres = $this->vidSrcService->getGenres();
|
||||
|
||||
// Prepare hero content: interleave top movies and TV shows with backdrops
|
||||
$heroContent = [];
|
||||
$mv = $latestMovies['movies'] ?? [];
|
||||
$tv = $latestTvShows['shows'] ?? [];
|
||||
$max = max(count($mv), count($tv));
|
||||
for ($i = 0; $i < $max && count($heroContent) < 8; $i++) {
|
||||
if (isset($mv[$i]) && !empty($mv[$i]['backdrop'])) {
|
||||
$heroContent[] = $mv[$i];
|
||||
}
|
||||
if (isset($tv[$i]) && !empty($tv[$i]['backdrop']) && count($heroContent) < 8) {
|
||||
$heroContent[] = $tv[$i];
|
||||
}
|
||||
}
|
||||
|
||||
// Build optional hero origin details for the first item (used for initial render only)
|
||||
$hero = $heroContent[0] ?? null;
|
||||
$image = $hero['backdrop'] ?? ($hero['poster'] ?? null);
|
||||
$heroOrigin = null;
|
||||
if ($hero) {
|
||||
try {
|
||||
if (!empty($hero['id'])) {
|
||||
$details = ($hero['type'] ?? 'movie') === 'tv'
|
||||
? $this->vidSrcService->getTvShowDetails((string)$hero['id'])
|
||||
: $this->vidSrcService->getMovieDetails((string)$hero['id']);
|
||||
if ($details) {
|
||||
$countries = $details['production_countries'] ?? ($details['origin_country'] ?? []);
|
||||
$companies = array_slice($details['production_companies'] ?? [], 0, 6);
|
||||
$heroOrigin = [
|
||||
'title' => $details['title'] ?? ($details['name'] ?? ($hero['title'] ?? 'Unknown')),
|
||||
'year' => $details['year'] ?? ($hero['year'] ?? null),
|
||||
'countries' => $countries,
|
||||
'companies' => $companies,
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
// Homepage SEO should stay branded, not per-hero item
|
||||
$title = 'Streamzy — Watch Movies & TV Shows Online';
|
||||
$description = 'Streamzy lets you watch trending movies and TV shows instantly in HD. No signup required.';
|
||||
|
||||
return view('home.index', [
|
||||
'heroContent' => $heroContent,
|
||||
'latestMovies' => array_slice($latestMovies['movies'] ?? [], 0, 12),
|
||||
'latestTvShows' => array_slice($latestTvShows['shows'] ?? [], 0, 12),
|
||||
'latestEpisodes' => array_slice($latestEpisodes['episodes'] ?? [], 0, 8),
|
||||
'genres' => $genres,
|
||||
'heroOrigin' => $heroOrigin,
|
||||
'meta' => [
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'image' => $image,
|
||||
'type' => 'website',
|
||||
'keywords' => 'movies, tv shows, streaming, watch online, free movies, latest movies'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON endpoint for hero item details (companies & countries)
|
||||
*/
|
||||
public function heroDetails(Request $request): JsonResponse
|
||||
{
|
||||
$type = $request->query('type'); // 'movie' or 'tv'
|
||||
$id = (string)$request->query('id');
|
||||
if (!$type || !$id) {
|
||||
return response()->json(['error' => 'Missing type or id'], 422);
|
||||
}
|
||||
try {
|
||||
$details = $type === 'tv'
|
||||
? $this->vidSrcService->getTvShowDetails($id)
|
||||
: $this->vidSrcService->getMovieDetails($id);
|
||||
if (!$details) {
|
||||
return response()->json(['error' => 'Not found'], 404);
|
||||
}
|
||||
return response()->json([
|
||||
'title' => $details['title'] ?? ($details['name'] ?? 'Unknown'),
|
||||
'year' => $details['year'] ?? null,
|
||||
'companies' => array_slice($details['production_companies'] ?? [], 0, 6),
|
||||
'countries' => $details['production_countries'] ?? ($details['origin_country'] ?? []),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json(['error' => 'Server error'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the movies listing page
|
||||
*/
|
||||
public function movies(Request $request): View
|
||||
{
|
||||
$page = (int)$request->get('page', 1);
|
||||
// Normalize genres from query (accept scalar ?genre=28 or array ?genre[]=28)
|
||||
$genresRaw = $request->get('genre', []);
|
||||
if (!is_array($genresRaw) && $genresRaw !== null && $genresRaw !== '') {
|
||||
$genresRaw = [$genresRaw];
|
||||
}
|
||||
$selectedGenres = is_array($genresRaw) ? array_values($genresRaw) : [];
|
||||
$filters = [
|
||||
'year_from' => $request->get('year_from'),
|
||||
'year_to' => $request->get('year_to'),
|
||||
'min_rating' => $request->get('rating') ? (float)$request->get('rating') : null,
|
||||
'sort' => $request->get('sort', 'popularity'),
|
||||
'genres' => $selectedGenres,
|
||||
];
|
||||
|
||||
// Use discoverWithFilters to support server-side filtering
|
||||
$result = $this->vidSrcService->discoverWithFilters('movie', $filters, $page);
|
||||
$genresList = $this->vidSrcService->getGenres();
|
||||
|
||||
return view('movies.index', [
|
||||
'movies' => $result['results'] ?? [],
|
||||
'pagination' => $result['pagination'] ?? ['current_page' => $page, 'total_pages' => 1, 'has_prev' => false, 'has_next' => false],
|
||||
'genresList' => $genresList,
|
||||
'selectedGenres' => $selectedGenres,
|
||||
'meta' => [
|
||||
'title' => 'Latest Movies - Streamzy',
|
||||
'description' => 'Browse and watch the latest movies in HD quality. New movies added daily.',
|
||||
'keywords' => 'latest movies, new movies, watch movies online, HD movies'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the TV shows listing page
|
||||
*/
|
||||
public function tvShows(Request $request): View
|
||||
{
|
||||
$page = (int)$request->get('page', 1);
|
||||
// Normalize genres from query (accept scalar ?genre=18 or array ?genre[]=18)
|
||||
$genresRaw = $request->get('genre', []);
|
||||
if (!is_array($genresRaw) && $genresRaw !== null && $genresRaw !== '') {
|
||||
$genresRaw = [$genresRaw];
|
||||
}
|
||||
$selectedGenres = is_array($genresRaw) ? array_values($genresRaw) : [];
|
||||
$filters = [
|
||||
'year_from' => $request->get('year_from'),
|
||||
'year_to' => $request->get('year_to'),
|
||||
'min_rating' => $request->get('rating') ? (float)$request->get('rating') : null,
|
||||
'sort' => $request->get('sort', 'popularity'),
|
||||
'genres' => $selectedGenres,
|
||||
];
|
||||
|
||||
// Use discoverWithFilters to support server-side filtering
|
||||
$result = $this->vidSrcService->discoverWithFilters('tv', $filters, $page);
|
||||
$genresList = $this->vidSrcService->getGenres();
|
||||
|
||||
return view('tv-shows.index', [
|
||||
'tvShows' => $result['results'] ?? [],
|
||||
'pagination' => $result['pagination'] ?? ['current_page' => $page, 'total_pages' => 1, 'has_prev' => false, 'has_next' => false],
|
||||
'genresList' => $genresList,
|
||||
'selectedGenres' => $selectedGenres,
|
||||
'meta' => [
|
||||
'title' => 'Latest TV Shows - Streamzy',
|
||||
'description' => 'Watch the latest TV shows and series online. Stream episodes in HD quality.',
|
||||
'keywords' => 'tv shows, latest series, watch tv online, streaming shows'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get random content based on genre (I'm Feeling Lucky)
|
||||
*/
|
||||
public function lucky(Request $request): JsonResponse
|
||||
{
|
||||
$type = $request->get('type', 'movie'); // 'movie' or 'tv'
|
||||
$genre = $request->get('genre', null);
|
||||
|
||||
$content = $this->vidSrcService->getRandomContentByGenre($type, $genre);
|
||||
|
||||
if (!$content) {
|
||||
return response()->json([
|
||||
'error' => 'No content found for the selected criteria'
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'content' => $content,
|
||||
'redirect_url' => $type === 'movie'
|
||||
? route('movie.show', ['slug' => $content['slug']])
|
||||
: route('tv-show.show', ['slug' => $content['slug']])
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available genres
|
||||
*/
|
||||
public function genres(): JsonResponse
|
||||
{
|
||||
$genres = $this->vidSrcService->getGenres();
|
||||
|
||||
return response()->json([
|
||||
'genres' => $genres
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to a random movie
|
||||
*/
|
||||
public function randomMovie(): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$content = $this->vidSrcService->getRandomContentByGenre('movie');
|
||||
if (!$content) {
|
||||
return redirect()->route('movies.index');
|
||||
}
|
||||
return redirect()->route('movie.show', ['slug' => $content['slug']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to a random TV show
|
||||
*/
|
||||
public function randomTvShow(): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$content = $this->vidSrcService->getRandomContentByGenre('tv');
|
||||
if (!$content) {
|
||||
return redirect()->route('tv-shows.index');
|
||||
}
|
||||
return redirect()->route('tv-show.show', ['slug' => $content['slug']]);
|
||||
}
|
||||
}
|
||||
146
app/Http/Controllers/MovieController.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\VidSrcService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class MovieController extends Controller
|
||||
{
|
||||
private VidSrcService $vidSrcService;
|
||||
|
||||
public function __construct(VidSrcService $vidSrcService)
|
||||
{
|
||||
$this->vidSrcService = $vidSrcService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a specific movie by slug
|
||||
*/
|
||||
public function show(string $slug, Request $request): View|Response
|
||||
{
|
||||
// Extract ID from slug (assuming format: movie-title-{id})
|
||||
$id = $this->extractIdFromSlug($slug);
|
||||
|
||||
if (!$id) {
|
||||
abort(404, 'Movie not found');
|
||||
}
|
||||
|
||||
$movie = $this->vidSrcService->getMovieDetails($id);
|
||||
|
||||
if (!$movie) {
|
||||
abort(404, 'Movie not found');
|
||||
}
|
||||
|
||||
// Blacklist guard
|
||||
if ($this->vidSrcService->isBlacklistedItem([
|
||||
'id' => $movie['id'] ?? null,
|
||||
'title' => $movie['title'] ?? null,
|
||||
'slug' => $movie['slug'] ?? null,
|
||||
'type' => 'movie'
|
||||
])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Verify slug matches (redirect if different)
|
||||
$expectedSlug = $movie['slug'];
|
||||
if ($slug !== $expectedSlug) {
|
||||
return redirect()->route('movie.show', ['slug' => $expectedSlug], 301);
|
||||
}
|
||||
|
||||
return view('movies.show', [
|
||||
'movie' => $movie,
|
||||
'relatedMovies' => $this->getRelatedMovies($movie['id']),
|
||||
'meta' => [
|
||||
'title' => $movie['title'] . ' (' . ($movie['year'] ?? 'Unknown') . ') - Watch Online | Streamzy',
|
||||
'description' => $movie['overview'] ?: 'Watch ' . $movie['title'] . ' online in HD quality on Streamzy.',
|
||||
'keywords' => 'watch ' . $movie['title'] . ', ' . $movie['title'] . ' online, movie streaming',
|
||||
'image' => $movie['poster'] ?? $movie['backdrop'],
|
||||
'type' => 'video.movie'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display movie by ID (for direct links)
|
||||
*/
|
||||
public function showById(string $id): Response
|
||||
{
|
||||
$movie = $this->vidSrcService->getMovieDetails($id);
|
||||
|
||||
if (!$movie) {
|
||||
abort(404, 'Movie not found');
|
||||
}
|
||||
|
||||
// Blacklist guard
|
||||
if ($this->vidSrcService->isBlacklistedItem([
|
||||
'id' => $movie['id'] ?? null,
|
||||
'title' => $movie['title'] ?? null,
|
||||
'slug' => $movie['slug'] ?? null,
|
||||
'type' => 'movie'
|
||||
])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Redirect to SEO-friendly URL
|
||||
return redirect()->route('movie.show', ['slug' => $movie['slug']], 301);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the embed iframe for a movie
|
||||
*/
|
||||
public function embed(string $id): View
|
||||
{
|
||||
$movie = $this->vidSrcService->getMovieDetails($id);
|
||||
|
||||
if (!$movie) {
|
||||
abort(404, 'Movie not found');
|
||||
}
|
||||
|
||||
return view('movies.embed', [
|
||||
'movie' => $movie,
|
||||
'embedUrl' => $movie['embed_url']
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ID from slug
|
||||
* Assumes slug format: movie-title-{id} or just {id}
|
||||
*/
|
||||
private function extractIdFromSlug(string $slug): ?string
|
||||
{
|
||||
// Check if slug is just an ID
|
||||
if (preg_match('/^(tt\d+|\d+)$/', $slug)) {
|
||||
return $slug;
|
||||
}
|
||||
|
||||
// Extract ID from end of slug
|
||||
if (preg_match('/-(tt\d+|\d+)$/', $slug, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// Try to extract ID from anywhere in the slug
|
||||
if (preg_match('/(tt\d+|\d+)/', $slug, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get related movies (placeholder implementation)
|
||||
*/
|
||||
private function getRelatedMovies(string $currentId): array
|
||||
{
|
||||
// Get some latest movies as related content
|
||||
$latestMovies = $this->vidSrcService->getLatestMovies(1);
|
||||
$movies = $latestMovies['movies'] ?? [];
|
||||
|
||||
// Filter out current movie and return first 6
|
||||
$related = array_filter($movies, fn($movie) => $movie['id'] !== $currentId);
|
||||
|
||||
return array_slice($related, 0, 6);
|
||||
}
|
||||
}
|
||||
242
app/Http/Controllers/ProxyController.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProxyController extends Controller
|
||||
{
|
||||
private const VIDSRC_BASE = 'https://vidsrc.xyz';
|
||||
private const ALLOWED_ORIGINS = [
|
||||
'streamzy.ch',
|
||||
'www.streamzy.ch',
|
||||
'localhost'
|
||||
];
|
||||
|
||||
/**
|
||||
* Proxy movie embed with popup blocking
|
||||
*/
|
||||
public function movieEmbed(string $id, Request $request): Response
|
||||
{
|
||||
return $this->proxyEmbed("/embed/movie/{$id}", $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy TV show embed with popup blocking
|
||||
*/
|
||||
public function tvEmbed(string $id, Request $request): Response
|
||||
{
|
||||
return $this->proxyEmbed("/embed/tv/{$id}", $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy TV episode embed with popup blocking
|
||||
*/
|
||||
public function tvEpisodeEmbed(string $id, int $season, int $episode, Request $request): Response
|
||||
{
|
||||
return $this->proxyEmbed("/embed/tv/{$id}/{$season}-{$episode}", $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main proxy method that handles the embed with popup blocking
|
||||
*/
|
||||
private function proxyEmbed(string $path, Request $request): Response
|
||||
{
|
||||
try {
|
||||
$url = self::VIDSRC_BASE . $path;
|
||||
|
||||
// Get the original HTML content
|
||||
$response = Http::withHeaders([
|
||||
'User-Agent' => $request->header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'),
|
||||
'Referer' => $request->header('Referer', ''),
|
||||
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language' => $request->header('Accept-Language', 'en-US,en;q=0.5'),
|
||||
])->get($url);
|
||||
|
||||
if (!$response->successful()) {
|
||||
return response('Failed to load video player', 503);
|
||||
}
|
||||
|
||||
$html = $response->body();
|
||||
|
||||
// Inject popup blocking and security scripts
|
||||
$html = $this->injectPopupBlocker($html);
|
||||
|
||||
// Create response with proper headers
|
||||
return response($html)
|
||||
->header('Content-Type', 'text/html; charset=utf-8')
|
||||
->header('X-Frame-Options', 'SAMEORIGIN')
|
||||
->header('Content-Security-Policy', $this->getCSP())
|
||||
->header('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Proxy embed error: " . $e->getMessage());
|
||||
return response('Video player temporarily unavailable', 503);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject popup blocking and security scripts into the HTML
|
||||
*/
|
||||
private function injectPopupBlocker(string $html): string
|
||||
{
|
||||
$popupBlockerScript = "
|
||||
<script>
|
||||
// Advanced popup blocker
|
||||
(function() {
|
||||
// Block window.open
|
||||
const originalOpen = window.open;
|
||||
window.open = function(url, name, specs) {
|
||||
console.log('🛡️ Streamzy: Blocked popup attempt:', url);
|
||||
return null;
|
||||
};
|
||||
|
||||
// Block new window creation
|
||||
Object.defineProperty(window, 'open', {
|
||||
value: function() {
|
||||
console.log('🛡️ Streamzy: Blocked window.open attempt');
|
||||
return null;
|
||||
},
|
||||
writable: false,
|
||||
configurable: false
|
||||
});
|
||||
|
||||
// Block target='_blank' links
|
||||
document.addEventListener('click', function(e) {
|
||||
const target = e.target.closest('a');
|
||||
if (target && (target.target === '_blank' || target.hasAttribute('target'))) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('🛡️ Streamzy: Blocked _blank link:', target.href);
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Block new tab/window keyboard shortcuts
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && (e.key === 't' || e.key === 'n')) {
|
||||
e.preventDefault();
|
||||
console.log('🛡️ Streamzy: Blocked keyboard shortcut');
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Block context menu to prevent 'open in new tab'
|
||||
document.addEventListener('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}, true);
|
||||
|
||||
// Block focus events that might trigger popups
|
||||
window.addEventListener('blur', function(e) {
|
||||
e.stopPropagation();
|
||||
}, true);
|
||||
|
||||
// Override history methods that might be used for navigation
|
||||
const originalPushState = history.pushState;
|
||||
const originalReplaceState = history.replaceState;
|
||||
|
||||
history.pushState = function() {
|
||||
console.log('🛡️ Streamzy: Blocked history.pushState');
|
||||
return false;
|
||||
};
|
||||
|
||||
history.replaceState = function() {
|
||||
console.log('🛡️ Streamzy: Blocked history.replaceState');
|
||||
return false;
|
||||
};
|
||||
|
||||
// Block beforeunload events that might trigger popups
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}, true);
|
||||
|
||||
// Block alert/confirm/prompt dialogs
|
||||
window.alert = function(msg) {
|
||||
console.log('🛡️ Streamzy: Blocked alert:', msg);
|
||||
return false;
|
||||
};
|
||||
|
||||
window.confirm = function(msg) {
|
||||
console.log('🛡️ Streamzy: Blocked confirm:', msg);
|
||||
return false;
|
||||
};
|
||||
|
||||
window.prompt = function(msg) {
|
||||
console.log('🛡️ Streamzy: Blocked prompt:', msg);
|
||||
return null;
|
||||
};
|
||||
|
||||
// Block document.write that might inject ads
|
||||
document.write = function() {
|
||||
console.log('🛡️ Streamzy: Blocked document.write');
|
||||
return false;
|
||||
};
|
||||
|
||||
document.writeln = function() {
|
||||
console.log('🛡️ Streamzy: Blocked document.writeln');
|
||||
return false;
|
||||
};
|
||||
|
||||
console.log('🛡️ Streamzy popup blocker loaded - All popups and redirects blocked');
|
||||
})();
|
||||
</script>";
|
||||
|
||||
// Also inject CSS to hide common ad elements
|
||||
$adBlockerCSS = "
|
||||
<style>
|
||||
/* Hide common ad containers */
|
||||
[id*=\"ad\"], [class*=\"ad\"], [id*=\"popup\"], [class*=\"popup\"],
|
||||
[id*=\"banner\"], [class*=\"banner\"], [id*=\"sponsor\"], [class*=\"sponsor\"],
|
||||
iframe[src*=\"ads\"], iframe[src*=\"doubleclick\"], iframe[src*=\"googlesyndication\"] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
opacity: 0 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
position: absolute !important;
|
||||
top: -9999px !important;
|
||||
left: -9999px !important;
|
||||
}
|
||||
|
||||
/* Ensure video player stays visible */
|
||||
iframe[src*=\"/embed/\"], video, .video-player, [class*=\"player\"] {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
</style>";
|
||||
|
||||
// Inject both script and CSS before closing head tag or at the beginning of body
|
||||
$injection = $adBlockerCSS . $popupBlockerScript;
|
||||
|
||||
if (strpos($html, '</head>') !== false) {
|
||||
$html = str_replace('</head>', $injection . '</head>', $html);
|
||||
} else {
|
||||
$html = str_replace('<body', $injection . '<body', $html);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Content Security Policy headers
|
||||
*/
|
||||
private function getCSP(): string
|
||||
{
|
||||
return "sandbox allow-scripts allow-same-origin allow-forms allow-presentation; " .
|
||||
"default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: *; " .
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' *; " .
|
||||
"style-src 'self' 'unsafe-inline' *; " .
|
||||
"img-src 'self' data: blob: *; " .
|
||||
"media-src 'self' blob: *; " .
|
||||
"frame-src 'self' *; " .
|
||||
"connect-src 'self' *; " .
|
||||
"object-src 'none'; " .
|
||||
"base-uri 'self';";
|
||||
}
|
||||
}
|
||||
265
app/Http/Controllers/SearchController.php
Normal file
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\VidSrcService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
private VidSrcService $vidSrcService;
|
||||
|
||||
public function __construct(VidSrcService $vidSrcService)
|
||||
{
|
||||
$this->vidSrcService = $vidSrcService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display search page
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
// Validate input parameters
|
||||
$request->validate([
|
||||
'q' => 'nullable|string|max:100',
|
||||
'type' => 'nullable|string|in:all,movie,tv',
|
||||
// Accept both legacy scalar (e.g., ?genre=12) and array (e.g., ?genre[]=12)
|
||||
'genre' => 'nullable',
|
||||
'year_from' => 'nullable|integer|min:1900|max:' . (date('Y') + 1),
|
||||
'year_to' => 'nullable|integer|min:1900|max:' . (date('Y') + 1),
|
||||
'rating' => 'nullable|string|in:5,6,7,8,9',
|
||||
'sort' => 'nullable|string|in:relevance,popularity,rating,year_desc,year_asc,title',
|
||||
'page' => 'nullable|integer|min:1'
|
||||
]);
|
||||
|
||||
// Use null coalescing and casting to avoid nulls from ConvertEmptyStringsToNull
|
||||
$query = trim((string) ($request->get('q') ?? ''));
|
||||
$type = $request->get('type') ?? 'all';
|
||||
$genres = $request->get('genre') ?? [];
|
||||
$yearFrom = $request->get('year_from');
|
||||
$yearTo = $request->get('year_to');
|
||||
$minRating = $request->get('rating');
|
||||
$sort = $request->get('sort') ?? 'relevance';
|
||||
$page = (int) ($request->get('page') ?? 1);
|
||||
$results = [];
|
||||
$genresList = $this->vidSrcService->getGenres();
|
||||
|
||||
// Prepare search filters
|
||||
$filters = [
|
||||
'year_from' => $yearFrom,
|
||||
'year_to' => $yearTo,
|
||||
'genres' => $genres,
|
||||
'min_rating' => $minRating,
|
||||
'sort' => $sort
|
||||
];
|
||||
|
||||
// Legacy single genre support
|
||||
$legacyGenre = $request->get('genre');
|
||||
if ($legacyGenre && !is_array($legacyGenre)) {
|
||||
$filters['genres'] = [$legacyGenre];
|
||||
}
|
||||
|
||||
if ($query || !empty($filters['genres']) || $yearFrom || $yearTo || $minRating) {
|
||||
if ($query) {
|
||||
$results = $this->vidSrcService->searchWithFilters($query, $type, $filters, $page);
|
||||
} else {
|
||||
// If no query but filters are present, use discovery
|
||||
$results = $this->vidSrcService->discoverWithFilters($type, $filters, $page);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize selected genres to an array of IDs/values
|
||||
$selectedGenres = $filters['genres'] ?? [];
|
||||
if (!is_array($selectedGenres)) {
|
||||
$selectedGenres = [$selectedGenres];
|
||||
}
|
||||
|
||||
return view('search.index', [
|
||||
'query' => $query,
|
||||
'type' => $type,
|
||||
'genre' => $legacyGenre, // For backward compatibility
|
||||
'genresList' => $genresList,
|
||||
'selectedGenres' => $selectedGenres,
|
||||
'results' => $results['results'] ?? [],
|
||||
'total' => $results['total'] ?? 0,
|
||||
'pagination' => $results['pagination'] ?? ['current_page' => $page, 'total_pages' => 1, 'has_prev' => false, 'has_next' => false],
|
||||
'meta' => [
|
||||
'title' => $this->generatePageTitle($query, $filters),
|
||||
'description' => $this->generatePageDescription($query, $filters),
|
||||
'keywords' => 'search movies, search tv shows, find movies online, movie search, advanced search'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate dynamic page title based on search parameters
|
||||
*/
|
||||
private function generatePageTitle(?string $query, array $filters): string
|
||||
{
|
||||
if (!empty($query)) {
|
||||
return "Search results for \"{$query}\" - Streamzy";
|
||||
}
|
||||
|
||||
if (!empty($filters['genres'])) {
|
||||
return "Browse by Genre - Streamzy";
|
||||
}
|
||||
|
||||
if ($filters['year_from'] || $filters['year_to']) {
|
||||
$yearRange = ($filters['year_from'] ?? '1900') . '-' . ($filters['year_to'] ?? date('Y'));
|
||||
return "Movies & TV Shows from {$yearRange} - Streamzy";
|
||||
}
|
||||
|
||||
return "Search Movies & TV Shows - Streamzy";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate dynamic page description based on search parameters
|
||||
*/
|
||||
private function generatePageDescription(?string $query, array $filters): string
|
||||
{
|
||||
if (!empty($query)) {
|
||||
return "Search results for \"{$query}\" on Streamzy. Find movies and TV shows to watch online.";
|
||||
}
|
||||
|
||||
$description = "Discover movies and TV shows on Streamzy";
|
||||
|
||||
if (!empty($filters['genres'])) {
|
||||
$description .= " by genre";
|
||||
}
|
||||
|
||||
if ($filters['year_from'] || $filters['year_to']) {
|
||||
$yearRange = ($filters['year_from'] ?? '1900') . '-' . ($filters['year_to'] ?? date('Y'));
|
||||
$description .= " from {$yearRange}";
|
||||
}
|
||||
|
||||
if ($filters['min_rating']) {
|
||||
$description .= " with rating {$filters['min_rating']}.0+";
|
||||
}
|
||||
|
||||
return $description . ". Find your favorite content to stream online.";
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX search endpoint
|
||||
*/
|
||||
public function ajax(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'q' => 'required|string|min:1|max:100',
|
||||
'type' => 'sometimes|string|in:all,movie,tv',
|
||||
'page' => 'sometimes|integer|min:1'
|
||||
]);
|
||||
|
||||
$query = $request->get('q');
|
||||
$type = $request->get('type', 'all');
|
||||
$page = (int) $request->get('page', 1);
|
||||
|
||||
$results = $this->vidSrcService->search($query, $type, $page);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'query' => $query,
|
||||
'type' => $type,
|
||||
'page' => $page,
|
||||
'results' => $results['results'] ?? [],
|
||||
'total' => $results['total'] ?? 0,
|
||||
'pagination' => $results['pagination'] ?? ['current_page' => $page, 'total_pages' => 1, 'has_prev' => false, 'has_next' => false]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search suggestions endpoint
|
||||
*/
|
||||
public function suggestions(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'q' => 'required|string|min:1|max:50'
|
||||
]);
|
||||
|
||||
$query = $request->get('q');
|
||||
|
||||
// Use VidSrcService to get real results
|
||||
try {
|
||||
$results = $this->vidSrcService->search($query, 'all', 1);
|
||||
|
||||
// Map to a simplified format
|
||||
$suggestions = array_map(function($item) {
|
||||
$type = $item['type'] ?? ($item['media_type'] ?? 'movie');
|
||||
$slug = $item['slug'] ?? null;
|
||||
$url = $slug ? ($type === 'movie' ? route('movie.show', ['slug' => $slug]) : route('tv-show.show', ['slug' => $slug])) : '#';
|
||||
|
||||
return [
|
||||
'title' => $item['title'] ?? $item['name'] ?? 'Untitled',
|
||||
'year' => $item['year'] ?? null,
|
||||
'type' => $type,
|
||||
'poster' => $item['poster'] ?? $item['poster_path'] ?? null,
|
||||
'url' => $url
|
||||
];
|
||||
}, array_slice($results['results'] ?? [], 0, 5));
|
||||
|
||||
return response()->json($suggestions);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate search suggestions (placeholder implementation)
|
||||
*/
|
||||
private function generateSuggestions(string $query): array
|
||||
{
|
||||
// This is a basic implementation
|
||||
// In a real app, you'd want to:
|
||||
// 1. Use a proper search API (like TMDB)
|
||||
// 2. Cache popular searches
|
||||
// 3. Use elasticsearch or similar for fuzzy matching
|
||||
|
||||
$popularMovies = [
|
||||
'Avatar',
|
||||
'Avengers',
|
||||
'Spider-Man',
|
||||
'Batman',
|
||||
'Superman',
|
||||
'Wonder Woman',
|
||||
'Iron Man',
|
||||
'Captain America',
|
||||
'Thor',
|
||||
'Black Panther'
|
||||
];
|
||||
|
||||
$popularShows = [
|
||||
'Game of Thrones',
|
||||
'Breaking Bad',
|
||||
'Stranger Things',
|
||||
'The Office',
|
||||
'Friends',
|
||||
'House of Cards',
|
||||
'Narcos',
|
||||
'The Crown',
|
||||
'Westworld',
|
||||
'Better Call Saul'
|
||||
];
|
||||
|
||||
$allSuggestions = array_merge($popularMovies, $popularShows);
|
||||
|
||||
// Filter suggestions that start with or contain the query
|
||||
$filtered = array_filter($allSuggestions, function($suggestion) use ($query) {
|
||||
return stripos($suggestion, $query) !== false;
|
||||
});
|
||||
|
||||
// Sort by relevance (starts with query first)
|
||||
usort($filtered, function($a, $b) use ($query) {
|
||||
$aStartsWith = stripos($a, $query) === 0;
|
||||
$bStartsWith = stripos($b, $query) === 0;
|
||||
|
||||
if ($aStartsWith && !$bStartsWith) return -1;
|
||||
if (!$aStartsWith && $bStartsWith) return 1;
|
||||
|
||||
return strcasecmp($a, $b);
|
||||
});
|
||||
|
||||
return array_slice(array_values($filtered), 0, 5);
|
||||
}
|
||||
}
|
||||
52
app/Http/Controllers/SitemapController.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\VidSrcService;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class SitemapController extends Controller
|
||||
{
|
||||
private VidSrcService $vidSrcService;
|
||||
|
||||
public function __construct(VidSrcService $vidSrcService)
|
||||
{
|
||||
$this->vidSrcService = $vidSrcService;
|
||||
}
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
// Build a sitemap index referencing first few pages per content type
|
||||
$moviePages = range(1, 5); // Adjust higher later if needed
|
||||
$showPages = range(1, 5);
|
||||
$episodePages = range(1, 3);
|
||||
|
||||
return response()
|
||||
->view('sitemap.index', compact('moviePages', 'showPages', 'episodePages'))
|
||||
->header('Content-Type', 'text/xml');
|
||||
}
|
||||
|
||||
public function movies(int $page): Response
|
||||
{
|
||||
$data = $this->vidSrcService->getLatestMovies($page);
|
||||
$items = $data['movies'] ?? [];
|
||||
$type = 'movie';
|
||||
return response()->view('sitemap.urls', compact('items', 'type'))->header('Content-Type', 'text/xml');
|
||||
}
|
||||
|
||||
public function shows(int $page): Response
|
||||
{
|
||||
$data = $this->vidSrcService->getLatestTvShows($page);
|
||||
$items = $data['shows'] ?? [];
|
||||
$type = 'show';
|
||||
return response()->view('sitemap.urls', compact('items', 'type'))->header('Content-Type', 'text/xml');
|
||||
}
|
||||
|
||||
public function episodes(int $page): Response
|
||||
{
|
||||
$data = $this->vidSrcService->getLatestEpisodes($page);
|
||||
$items = $data['episodes'] ?? [];
|
||||
$type = 'episode';
|
||||
return response()->view('sitemap.urls', compact('items', 'type'))->header('Content-Type', 'text/xml');
|
||||
}
|
||||
}
|
||||
174
app/Http/Controllers/StatsController.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
// use Illuminate\Support\Str;
|
||||
|
||||
class StatsController extends Controller
|
||||
{
|
||||
public function counts(Request $request)
|
||||
{
|
||||
$fallbackMovies = 85123;
|
||||
$fallbackTv = 15456;
|
||||
|
||||
$apiKey = config('services.tmdb.api_key', env('TMDB_API_KEY'));
|
||||
if (!$apiKey) {
|
||||
return response()->json([
|
||||
'movies' => $fallbackMovies,
|
||||
'tv' => $fallbackTv,
|
||||
'source' => 'fallback',
|
||||
]);
|
||||
}
|
||||
|
||||
// Use a cached estimate that scales TMDB totals by an availability ratio measured from VidSrc
|
||||
$data = Cache::remember('stats.vidsrc.counts.v2', now()->addHours(12), function () use ($apiKey, $fallbackMovies, $fallbackTv) {
|
||||
try {
|
||||
// 1) Get TMDB total_results for a broad baseline
|
||||
$movieRes = Http::timeout(6)->get('https://api.themoviedb.org/3/discover/movie', [
|
||||
'api_key' => $apiKey,
|
||||
'include_adult' => false,
|
||||
])->json();
|
||||
$tvRes = Http::timeout(6)->get('https://api.themoviedb.org/3/discover/tv', [
|
||||
'api_key' => $apiKey,
|
||||
'include_adult' => false,
|
||||
])->json();
|
||||
|
||||
$tmdbMoviesTotal = (int) data_get($movieRes, 'total_results', 0);
|
||||
$tmdbTvTotal = (int) data_get($tvRes, 'total_results', 0);
|
||||
|
||||
// 2) Sample several pages and test VidSrc embed availability
|
||||
// Keep reasonable to avoid load; results are cached.
|
||||
$pagesToSampleMovies = 3;
|
||||
$pagesToSampleTv = 5;
|
||||
$sampleSizeMovies = 0; $availableMovies = 0;
|
||||
$sampleSizeTv = 0; $availableTv = 0;
|
||||
$episodeSamples = [];
|
||||
|
||||
// Movies: sequential pages for popularity distribution
|
||||
for ($page = 1; $page <= $pagesToSampleMovies; $page++) {
|
||||
// Movies
|
||||
$m = Http::timeout(6)->get('https://api.themoviedb.org/3/movie/popular', [
|
||||
'api_key' => $apiKey,
|
||||
'page' => $page,
|
||||
])->json();
|
||||
foreach ((array) data_get($m, 'results', []) as $it) {
|
||||
$id = (int) data_get($it, 'id');
|
||||
if (!$id) continue;
|
||||
$sampleSizeMovies++;
|
||||
if ($this->checkVidSrc('movie', $id)) $availableMovies++;
|
||||
}
|
||||
}
|
||||
|
||||
// TV: sample a mix of popular and top-rated pages (randomized small set)
|
||||
$tvPages = [];
|
||||
$totalTvPages = max(1, (int) data_get($tvRes, 'total_pages', 1));
|
||||
for ($i = 0; $i < $pagesToSampleTv; $i++) {
|
||||
$tvPages[] = max(1, random_int(1, min(500, $totalTvPages))); // cap at 500 to be safe
|
||||
}
|
||||
$detailsSampleLimit = (int) env('STATS_EPISODES_SAMPLE_LIMIT', 20); // configurable cap
|
||||
foreach ($tvPages as $page) {
|
||||
$t = Http::timeout(6)->get('https://api.themoviedb.org/3/tv/popular', [
|
||||
'api_key' => $apiKey,
|
||||
'page' => $page,
|
||||
])->json();
|
||||
foreach ((array) data_get($t, 'results', []) as $it) {
|
||||
$id = (int) data_get($it, 'id');
|
||||
if (!$id) continue;
|
||||
$sampleSizeTv++;
|
||||
if ($this->checkVidSrc('tv', $id)) {
|
||||
$availableTv++;
|
||||
if (count($episodeSamples) < $detailsSampleLimit) {
|
||||
$count = $this->getTvEpisodeCount($id, $apiKey);
|
||||
if (is_int($count) && $count > 0) {
|
||||
$episodeSamples[] = min($count, 1000); // clamp outliers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Compute availability ratios (fallback to 1 if no sample)
|
||||
$movieRatio = $sampleSizeMovies ? ($availableMovies / $sampleSizeMovies) : 1.0;
|
||||
$tvRatio = $sampleSizeTv ? ($availableTv / $sampleSizeTv) : 1.0;
|
||||
|
||||
// 4) Estimate available totals by scaling TMDB counts
|
||||
$movies = (int) floor(max($fallbackMovies, $tmdbMoviesTotal * $movieRatio));
|
||||
$tv = (int) floor(max($fallbackTv, $tmdbTvTotal * $tvRatio));
|
||||
|
||||
// 5) Estimate total episodes based on avg episodes per VidSrc-available show sample
|
||||
$fallbackAvgEpisodesPerShow = (int) env('STATS_EPISODES_FALLBACK_AVG', 12); // configurable fallback
|
||||
$avgEpisodes = count($episodeSamples) ? array_sum($episodeSamples) / count($episodeSamples) : $fallbackAvgEpisodesPerShow;
|
||||
$avgEpisodes = max(1, min(1000, (int) round($avgEpisodes)));
|
||||
$episodes = (int) ($tv * $avgEpisodes);
|
||||
|
||||
return [
|
||||
'movies' => $movies,
|
||||
'tv' => $tv,
|
||||
'episodes' => $episodes,
|
||||
'source' => 'vidsrc-estimate',
|
||||
'sample' => [
|
||||
'movies' => [$availableMovies, $sampleSizeMovies],
|
||||
'tv' => [$availableTv, $sampleSizeTv],
|
||||
'avg_ep' => $avgEpisodes,
|
||||
],
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return ['movies' => $fallbackMovies, 'tv' => $fallbackTv, 'source' => 'fallback'];
|
||||
}
|
||||
});
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given TMDB id appears available on VidSrc by probing embed URL.
|
||||
* Uses a per-id cache to avoid repeated remote calls.
|
||||
*/
|
||||
private function checkVidSrc(string $type, int $tmdbId): bool
|
||||
{
|
||||
$type = $type === 'tv' ? 'tv' : 'movie';
|
||||
$cacheKey = "vidsrc.availability.$type.$tmdbId";
|
||||
return Cache::remember($cacheKey, now()->addDays(7), function () use ($type, $tmdbId) {
|
||||
try {
|
||||
$url = "https://vidsrc.xyz/embed/{$type}/{$tmdbId}";
|
||||
$res = Http::timeout(4)->withHeaders([
|
||||
'Accept' => 'text/html',
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer' => url('/'),
|
||||
])->get($url);
|
||||
return $res->ok();
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return number_of_episodes for a TV show id (cached).
|
||||
*/
|
||||
private function getTvEpisodeCount(int $tmdbId, string $apiKey): ?int
|
||||
{
|
||||
$cacheKey = "tv.episodes.count.$tmdbId";
|
||||
return Cache::remember($cacheKey, now()->addDays(7), function () use ($tmdbId, $apiKey) {
|
||||
try {
|
||||
$res = Http::timeout(6)->get("https://api.themoviedb.org/3/tv/{$tmdbId}", [
|
||||
'api_key' => $apiKey,
|
||||
])->json();
|
||||
$count = (int) data_get($res, 'number_of_episodes');
|
||||
if ($count > 0) return $count;
|
||||
// fallback: sum season episode counts
|
||||
$seasons = (array) data_get($res, 'seasons', []);
|
||||
$sum = 0;
|
||||
foreach ($seasons as $s) {
|
||||
$sum += (int) ($s['episode_count'] ?? 0);
|
||||
}
|
||||
return $sum > 0 ? $sum : null;
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
334
app/Http/Controllers/TvShowController.php
Normal file
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\VidSrcService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class TvShowController extends Controller
|
||||
{
|
||||
private VidSrcService $vidSrcService;
|
||||
|
||||
public function __construct(VidSrcService $vidSrcService)
|
||||
{
|
||||
$this->vidSrcService = $vidSrcService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a specific TV show by slug
|
||||
*/
|
||||
public function show(string $slug): View|Response
|
||||
{
|
||||
// Extract ID from slug
|
||||
$id = $this->extractIdFromSlug($slug);
|
||||
|
||||
if (!$id) {
|
||||
abort(404, 'TV show not found');
|
||||
}
|
||||
|
||||
$show = $this->vidSrcService->getTvShowDetails($id);
|
||||
|
||||
if (!$show) {
|
||||
abort(404, 'TV show not found');
|
||||
}
|
||||
|
||||
// Blacklist guard
|
||||
if ($this->vidSrcService->isBlacklistedItem([
|
||||
'id' => $show['id'] ?? null,
|
||||
'title' => $show['title'] ?? null,
|
||||
'slug' => $show['slug'] ?? null,
|
||||
'type' => 'tv'
|
||||
])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Verify slug matches (redirect if different)
|
||||
$expectedSlug = $show['slug'];
|
||||
if ($slug !== $expectedSlug) {
|
||||
return redirect()->route('tv-show.show', ['slug' => $expectedSlug], 301);
|
||||
}
|
||||
|
||||
// Get season 1 episodes by default
|
||||
$currentSeason = 1;
|
||||
$seasonData = $this->vidSrcService->getSeasonDetails($show['id'], $currentSeason);
|
||||
|
||||
return view('tv-shows.show', [
|
||||
'show' => $show,
|
||||
'currentSeason' => $currentSeason,
|
||||
'seasonData' => $seasonData,
|
||||
'relatedShows' => $this->getRelatedShows($show['id']),
|
||||
'meta' => [
|
||||
'title' => $show['title'] . ' (' . ($show['year'] ?? 'Unknown') . ') - Watch Online | Streamzy',
|
||||
'description' => $show['overview'] ?: 'Watch ' . $show['title'] . ' episodes online in HD quality on Streamzy.',
|
||||
'keywords' => 'watch ' . $show['title'] . ', ' . $show['title'] . ' episodes, tv show streaming',
|
||||
'image' => $show['poster'] ?? $show['backdrop'],
|
||||
'type' => 'video.tv_show'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get season data via AJAX
|
||||
*/
|
||||
public function getSeason(string $showId, int $season): JsonResponse
|
||||
{
|
||||
$seasonData = $this->vidSrcService->getSeasonDetails($showId, $season);
|
||||
|
||||
if (!$seasonData) {
|
||||
return response()->json(['error' => 'Season not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json($seasonData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a specific episode
|
||||
*/
|
||||
public function episode(string $slug, int $season, int $episode): View|Response
|
||||
{
|
||||
// Extract show ID from slug
|
||||
$showId = $this->extractIdFromSlug($slug);
|
||||
|
||||
if (!$showId) {
|
||||
abort(404, 'TV show not found');
|
||||
}
|
||||
|
||||
$show = $this->vidSrcService->getTvShowDetails($showId);
|
||||
$episodeData = $this->vidSrcService->getEpisodeDetails($showId, $season, $episode);
|
||||
|
||||
if (!$show || !$episodeData) {
|
||||
abort(404, 'Episode not found');
|
||||
}
|
||||
|
||||
// Blacklist guard for show
|
||||
if ($this->vidSrcService->isBlacklistedItem([
|
||||
'id' => $show['id'] ?? null,
|
||||
'title' => $show['title'] ?? null,
|
||||
'slug' => $show['slug'] ?? null,
|
||||
'type' => 'tv'
|
||||
])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Verify slug matches
|
||||
$expectedSlug = $show['slug'];
|
||||
if ($slug !== $expectedSlug) {
|
||||
return redirect()->route('tv-show.episode', [
|
||||
'slug' => $expectedSlug,
|
||||
'season' => $season,
|
||||
'episode' => $episode
|
||||
], 301);
|
||||
}
|
||||
|
||||
return view('tv-shows.episode', [
|
||||
'show' => $show,
|
||||
'episode' => $episodeData,
|
||||
'nextEpisode' => $this->getNextEpisode($showId, $season, $episode),
|
||||
'previousEpisode' => $this->getPreviousEpisode($showId, $season, $episode),
|
||||
'meta' => [
|
||||
'title' => $show['title'] . ' S' . $season . 'E' . $episode . ' - Watch Online | Streamzy',
|
||||
'description' => 'Watch ' . $show['title'] . ' Season ' . $season . ' Episode ' . $episode . ' online in HD quality.',
|
||||
'keywords' => 'watch ' . $show['title'] . ', season ' . $season . ' episode ' . $episode . ', tv streaming',
|
||||
'image' => $show['poster'] ?? $show['backdrop'],
|
||||
'type' => 'video.episode'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display TV show by ID (for direct links)
|
||||
*/
|
||||
public function showById(string $id): Response
|
||||
{
|
||||
$show = $this->vidSrcService->getTvShowDetails($id);
|
||||
|
||||
if (!$show) {
|
||||
abort(404, 'TV show not found');
|
||||
}
|
||||
|
||||
// Blacklist guard
|
||||
if ($this->vidSrcService->isBlacklistedItem([
|
||||
'id' => $show['id'] ?? null,
|
||||
'title' => $show['title'] ?? null,
|
||||
'slug' => $show['slug'] ?? null,
|
||||
'type' => 'tv'
|
||||
])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Redirect to SEO-friendly URL
|
||||
return redirect()->route('tv-show.show', ['slug' => $show['slug']], 301);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the embed iframe for a TV show
|
||||
*/
|
||||
public function embed(string $id): View
|
||||
{
|
||||
$show = $this->vidSrcService->getTvShowDetails($id);
|
||||
|
||||
if (!$show) {
|
||||
abort(404, 'TV show not found');
|
||||
}
|
||||
|
||||
return view('tv-shows.embed', [
|
||||
'show' => $show,
|
||||
'embedUrl' => $show['embed_url']
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the embed iframe for a specific episode
|
||||
*/
|
||||
public function embedEpisode(string $id, int $season, int $episode): View
|
||||
{
|
||||
$episodeData = $this->vidSrcService->getEpisodeDetails($id, $season, $episode);
|
||||
|
||||
if (!$episodeData) {
|
||||
abort(404, 'Episode not found');
|
||||
}
|
||||
|
||||
// Blacklist guard for parent show by id
|
||||
$show = $this->vidSrcService->getTvShowDetails($id);
|
||||
if ($show && $this->vidSrcService->isBlacklistedItem([
|
||||
'id' => $show['id'] ?? null,
|
||||
'title' => $show['title'] ?? null,
|
||||
'slug' => $show['slug'] ?? null,
|
||||
'type' => 'tv'
|
||||
])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return view('tv-shows.embed-episode', [
|
||||
'episode' => $episodeData,
|
||||
'embedUrl' => $episodeData['embed_url']
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ID from slug
|
||||
*/
|
||||
private function extractIdFromSlug(string $slug): ?string
|
||||
{
|
||||
// Check if slug is just an ID
|
||||
if (preg_match('/^(tt\d+|\d+)$/', $slug)) {
|
||||
return $slug;
|
||||
}
|
||||
|
||||
// Extract ID from end of slug
|
||||
if (preg_match('/-(tt\d+|\d+)$/', $slug, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// Try to extract ID from anywhere in the slug
|
||||
if (preg_match('/(tt\d+|\d+)/', $slug, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get related TV shows (placeholder implementation)
|
||||
*/
|
||||
private function getRelatedShows(string $currentId): array
|
||||
{
|
||||
// Get some latest shows as related content
|
||||
$latestShows = $this->vidSrcService->getLatestTvShows(1);
|
||||
$shows = $latestShows['shows'] ?? [];
|
||||
|
||||
// Filter out current show and return first 6
|
||||
$related = array_filter($shows, fn($show) => $show['id'] !== $currentId);
|
||||
|
||||
return array_slice($related, 0, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next episode (placeholder implementation)
|
||||
*/
|
||||
private function getNextEpisode(string $showId, int $season, int $episode): ?array
|
||||
{
|
||||
// Get current season details
|
||||
$seasonData = $this->vidSrcService->getSeasonDetails($showId, $season);
|
||||
if (!$seasonData || empty($seasonData['episodes'])) {
|
||||
return null;
|
||||
}
|
||||
$episodes = $seasonData['episodes'];
|
||||
$episodeNumbers = array_column($episodes, 'episode_number');
|
||||
$maxEpisode = max($episodeNumbers);
|
||||
|
||||
if ($episode < $maxEpisode) {
|
||||
// Next episode in same season
|
||||
return [
|
||||
'season_number' => $season,
|
||||
'episode_number' => $episode + 1,
|
||||
'available' => true
|
||||
];
|
||||
}
|
||||
|
||||
// If last episode, try next season
|
||||
$show = $this->vidSrcService->getTvShowDetails($showId);
|
||||
if (!$show || empty($show['seasons'])) {
|
||||
return null;
|
||||
}
|
||||
// Find next season number
|
||||
$seasonNumbers = array_map(fn($s) => $s['season_number'], $show['seasons']);
|
||||
sort($seasonNumbers);
|
||||
$currentIndex = array_search($season, $seasonNumbers);
|
||||
if ($currentIndex === false || $currentIndex + 1 >= count($seasonNumbers)) {
|
||||
return null; // No next season
|
||||
}
|
||||
$nextSeason = $seasonNumbers[$currentIndex + 1];
|
||||
$nextSeasonData = $this->vidSrcService->getSeasonDetails($showId, $nextSeason);
|
||||
if (!$nextSeasonData || empty($nextSeasonData['episodes'])) {
|
||||
return null;
|
||||
}
|
||||
$firstEpisode = min(array_column($nextSeasonData['episodes'], 'episode_number'));
|
||||
return [
|
||||
'season_number' => $nextSeason,
|
||||
'episode_number' => $firstEpisode,
|
||||
'available' => true
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get previous episode (placeholder implementation)
|
||||
*/
|
||||
private function getPreviousEpisode(string $showId, int $season, int $episode): ?array
|
||||
{
|
||||
if ($episode > 1) {
|
||||
return [
|
||||
'season_number' => $season,
|
||||
'episode_number' => $episode - 1,
|
||||
'available' => true
|
||||
];
|
||||
}
|
||||
|
||||
if ($season > 1) {
|
||||
// Try to get last episode of previous season
|
||||
$prevSeason = $season - 1;
|
||||
$prevSeasonData = $this->vidSrcService->getSeasonDetails($showId, $prevSeason);
|
||||
|
||||
if ($prevSeasonData && !empty($prevSeasonData['episodes'])) {
|
||||
$lastEpisode = max(array_column($prevSeasonData['episodes'], 'episode_number'));
|
||||
return [
|
||||
'season_number' => $prevSeason,
|
||||
'episode_number' => $lastEpisode,
|
||||
'available' => true
|
||||
];
|
||||
}
|
||||
|
||||
// Fallback if we can't fetch details
|
||||
return [
|
||||
'season_number' => $season - 1,
|
||||
'episode_number' => 1,
|
||||
'available' => true
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
48
app/Models/User.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
1541
app/Services/VidSrcService.php
Normal file
18
artisan
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
11
assets/images/logo-desktop.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg height="22" viewBox="0 0 686 69" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- no defs needed -->
|
||||
<path d="M1.64799 65.092V46.084C4.07999 47.108 6.63999 47.972 9.32799 48.676C12.016 49.316 14.736 49.86 17.488 50.308C20.304 50.692 23.056 50.98 25.744 51.172C28.496 51.3 31.12 51.364 33.616 51.364C36.112 51.364 38.096 51.3 39.568 51.172C41.104 51.044 42.256 50.884 43.024 50.692C43.792 50.436 44.24 50.212 44.368 50.02C44.56 49.764 44.56 49.54 44.368 49.348C44.112 49.156 42.832 49.06 40.528 49.06C38.288 48.996 35.504 48.868 32.176 48.676C28.912 48.484 25.392 48.132 21.616 47.62C17.904 47.044 14.416 46.18 11.152 45.028C7.95199 43.876 5.29599 42.308 3.18399 40.324C1.07199 38.276 0.0159912 35.684 0.0159912 32.548C0.0159912 29.924 0.687991 27.684 2.03199 25.828C3.43999 23.908 5.23199 22.308 7.40799 21.028C9.64799 19.748 12.176 18.724 14.992 17.956C17.872 17.188 20.752 16.612 23.632 16.228C26.576 15.78 29.424 15.492 32.176 15.364C34.928 15.236 37.36 15.172 39.472 15.172C44.336 15.172 49.296 15.396 54.352 15.844C59.408 16.228 64.56 16.932 69.808 17.956V37.156C64.24 35.108 58.896 33.668 53.776 32.836C48.72 32.004 43.76 31.588 38.896 31.588C35.312 31.588 32.816 31.684 31.408 31.876C30 32.068 29.136 32.42 28.816 32.932C28.432 33.572 29.264 34.052 31.312 34.372C33.36 34.628 36.016 34.884 39.28 35.14C42.608 35.396 46.256 35.78 50.224 36.292C54.192 36.74 57.904 37.54 61.36 38.692C64.816 39.78 67.696 41.316 70 43.3C72.304 45.22 73.456 47.78 73.456 50.98C73.456 54.308 72.368 57.092 70.192 59.332C68.016 61.572 65.072 63.396 61.36 64.804C57.712 66.212 53.488 67.204 48.688 67.78C43.888 68.42 38.864 68.74 33.616 68.74C28.304 68.74 22.928 68.484 17.488 67.972C12.112 67.524 6.83199 66.564 1.64799 65.092Z" fill="white"/>
|
||||
<path d="M78.4561 35.62V16.036H88.2481V0.00402832H113.208V16.036H143.64V35.62H113.208V47.524C113.208 48.1 113.464 48.612 113.976 49.06C114.488 49.444 115.352 49.764 116.568 50.02C117.848 50.212 119.544 50.372 121.656 50.5C123.768 50.564 126.456 50.596 129.72 50.596H146.232V66.532C143.096 67.236 139.448 67.78 135.288 68.164C131.192 68.548 127.096 68.74 123 68.74C116.664 68.74 111.288 68.388 106.872 67.684C102.456 67.044 98.8721 66.052 96.1201 64.708C93.3681 63.364 91.3521 61.636 90.0721 59.524C88.8561 57.412 88.2481 54.916 88.2481 52.036V35.62H78.4561Z" fill="white"/>
|
||||
<path d="M151.232 16.996H176.192V22.468C181.184 20.74 186.208 19.396 191.264 18.436C196.32 17.412 201.312 16.9 206.24 16.9V35.716C202.976 35.716 199.968 35.716 197.216 35.716C194.464 35.652 191.872 35.684 189.44 35.812C187.072 35.876 184.8 36.068 182.624 36.388C180.448 36.644 178.304 37.124 176.192 37.828V68.74H151.232V16.996Z" fill="white"/>
|
||||
<path d="M235.912 48.772C236.424 49.412 237.352 49.9561 238.696 50.4041C240.04 50.8521 241.64 51.2041 243.496 51.4601C245.352 51.7161 247.432 51.908 249.736 52.036C252.04 52.1 254.376 52.1 256.744 52.036C261.864 51.972 266.856 51.7801 271.72 51.4601C276.584 51.0761 281.576 50.6601 286.696 50.2121V65.3801C281.832 66.7881 276.808 67.6841 271.624 68.0681C266.44 68.5161 261.352 68.7401 256.36 68.7401C253.544 68.7401 250.44 68.58 247.048 68.26C243.72 68.004 240.392 67.5241 237.064 66.8201C233.736 66.0521 230.504 65.06 227.368 63.844C224.296 62.564 221.544 60.9321 219.112 58.9481C216.744 56.9001 214.824 54.5 213.352 51.748C211.944 48.932 211.24 45.6361 211.24 41.8601C211.24 38.2121 211.848 35.0441 213.064 32.356C214.28 29.604 215.912 27.236 217.96 25.252C220.072 23.268 222.472 21.636 225.16 20.356C227.912 19.0121 230.792 17.9561 233.8 17.188C236.808 16.42 239.848 15.876 242.92 15.556C246.056 15.236 249.032 15.076 251.848 15.076C257.544 15.076 262.696 15.78 267.304 17.188C271.976 18.5321 275.944 20.3881 279.208 22.7561C282.472 25.1241 284.968 27.9081 286.696 31.1081C288.488 34.3081 289.384 37.7 289.384 41.284V47.428L235.912 48.772ZM268.84 36.4841C268.392 35.9721 267.56 35.4601 266.344 34.9481C265.192 34.4361 263.88 33.9881 262.408 33.604C260.936 33.156 259.368 32.8041 257.704 32.548C256.04 32.2921 254.472 32.1641 253 32.1641C249.416 32.1641 246.152 32.5481 243.208 33.3161C240.328 34.0841 238.056 35.1401 236.392 36.4841H268.84Z" fill="white"/>
|
||||
<path d="M300.048 33.604V17.38C305.296 16.228 310.8 15.556 316.56 15.3641C322.384 15.1721 328.176 15.076 333.936 15.076C336.432 15.076 339.184 15.1721 342.192 15.3641C345.264 15.4921 348.336 15.812 351.408 16.3241C354.544 16.8361 357.584 17.5721 360.528 18.5321C363.536 19.4921 366.192 20.772 368.496 22.372C370.8 23.908 372.656 25.8281 374.064 28.132C375.472 30.3721 376.176 33.0921 376.176 36.292V67.6841H350.256V64.036C344.496 65.7 339.344 66.9161 334.8 67.6841C330.256 68.3881 325.84 68.7401 321.552 68.7401C318.928 68.7401 316.016 68.4841 312.816 67.9721C309.68 67.4601 306.736 66.5961 303.984 65.3801C301.296 64.1001 299.024 62.4041 297.168 60.2921C295.312 58.1161 294.384 55.396 294.384 52.132C294.384 49.636 294.928 47.4921 296.016 45.7001C297.168 43.8441 298.672 42.276 300.528 40.996C302.448 39.716 304.592 38.692 306.96 37.924C309.328 37.092 311.76 36.484 314.256 36.1C316.816 35.652 319.312 35.364 321.744 35.236C324.24 35.0441 326.48 34.9481 328.464 34.9481H349.296C349.296 34.6281 348.624 34.3081 347.28 33.9881C345.936 33.6681 344.304 33.412 342.384 33.22C340.464 32.964 338.48 32.7721 336.432 32.6441C334.384 32.452 332.656 32.356 331.248 32.356C325.616 32.356 320.24 32.356 315.12 32.356C310 32.356 304.976 32.772 300.048 33.604ZM350.256 48.58H330.864C326.448 48.58 323.344 48.7721 321.552 49.1561C319.824 49.4761 318.96 49.9881 318.96 50.6921C318.96 51.2681 319.536 51.748 320.688 52.132C321.84 52.452 323.152 52.74 324.624 52.996C326.096 53.188 327.504 53.3161 328.848 53.3801C330.192 53.4441 331.056 53.4761 331.44 53.4761C335.472 53.4761 339.152 53.2201 342.48 52.7081C345.808 52.1321 348.4 51.3321 350.256 50.3081V48.58Z" fill="white"/>
|
||||
<path d="M381.176 16.9959H406.136V21.8919C411.256 20.2279 415.992 18.8199 420.344 17.6679C424.76 16.5159 428.792 15.9399 432.44 15.9399C441.336 15.9399 447.576 18.5959 451.16 23.9079C456.92 21.8599 462.264 20.0359 467.192 18.4359C472.184 16.7719 476.696 15.9399 480.728 15.9399C487.704 15.9399 493.112 17.6039 496.952 20.9319C500.792 24.2599 502.712 29.2519 502.712 35.9079V68.7399H477.752V39.6519C477.752 37.5399 477.08 36.0679 475.736 35.2359C474.456 34.4039 472.728 34.0199 470.552 34.0839C468.44 34.1479 465.976 34.5639 463.16 35.3319C460.344 36.0999 457.432 36.9959 454.424 38.0199V68.7399H429.464V39.6519C429.464 37.5399 428.792 36.0359 427.448 35.1399C426.168 34.2439 424.44 33.7959 422.264 33.7959C420.152 33.7319 417.688 34.0519 414.872 34.7559C412.056 35.3959 409.144 36.1959 406.136 37.1559V68.7399H381.176V16.9959Z" fill="white"/>
|
||||
<path d="M529.79 0.0579043L594.39 0.00112611C597.516 -0.0693572 599.305 3.18267 597.889 5.86495L575.788 34.4831C574.581 36.3451 575.584 38.5947 577.729 38.9765L620.828 38.9706C622.408 39.1057 623.73 40.4194 623.125 42.0523L604.27 66.279C602.717 67.9784 601.649 68.5892 599.289 68.74H532.674C529.406 68.693 527.505 65.6446 528.979 62.6804L551.158 34.1405C552.769 32.2492 552.003 29.6492 549.531 29.1773L510.154 29.1832C507.09 28.4098 507.235 26.9669 508.804 24.6997C514.25 16.8349 521.338 9.52225 526.905 1.67511C527.73 0.854758 528.654 0.320259 529.79 0.0559465V0.0579043Z" fill="#dc2626"/>
|
||||
<path d="M685.452 16.0094V53.3168C685.452 55.6789 684.965 57.8094 683.993 59.7084C683.02 61.6536 681.631 63.2747 679.824 64.5715C678.018 65.9147 675.818 66.9336 673.224 67.6284C670.631 68.3694 667.69 68.7399 664.401 68.7399H635.639V56.5126H658.426C661.808 56.5126 664.146 56.2115 665.443 55.6094C666.74 55.0073 667.412 53.9189 667.458 52.3441V49.0789C662.873 50.561 658.635 51.7884 654.744 52.761C650.854 53.6873 647.334 54.1505 644.184 54.1505C639.136 54.1041 635.222 52.8768 632.443 50.4684C629.664 48.0599 628.275 44.4473 628.275 39.6305V15.9399H646.338V36.921C646.338 38.5884 646.894 39.7694 648.005 40.4641C649.117 41.1589 650.576 41.5063 652.382 41.5063C654.466 41.5063 656.829 41.1589 659.469 40.4641C662.109 39.7694 664.772 38.9589 667.458 38.0326V16.0094H685.452Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
3
assets/images/logo-mobile.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg height="32" viewBox="0 0 117 69" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.7904 0.0579043L87.3902 0.00112611C90.5164 -0.0693572 92.3047 3.18267 90.8885 5.86495L68.7879 34.4831C67.5813 36.3451 68.5842 38.5947 70.729 38.9765L113.828 38.9706C115.408 39.1057 116.73 40.4194 116.125 42.0523L97.2702 66.279C95.7169 67.9784 94.6493 68.5892 92.289 68.74H25.6737C22.4065 68.693 20.5045 65.6446 21.9795 62.6804L44.1585 34.1405C45.7686 32.2492 45.0027 29.6492 42.5308 29.1773L3.15388 29.1832C0.0903822 28.4098 0.23533 26.9669 1.80429 24.6997C7.24964 16.8349 14.3384 9.52225 19.9052 1.67511C20.7298 0.854758 21.6543 0.320259 22.7904 0.0559465V0.0579043Z" fill="#dc2626"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 691 B |
18
bootstrap/app.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
2
bootstrap/cache/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
5
bootstrap/providers.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
];
|
||||
75
composer.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^11.5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
8393
composer.lock
generated
Normal file
126
config/app.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
115
config/auth.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
24
config/blacklist.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// TMDB IDs of movies to hide everywhere (lists, search, sitemap, etc.)
|
||||
'movie_ids' => [
|
||||
// e.g., 12345,
|
||||
],
|
||||
|
||||
// TMDB IDs of TV shows to hide everywhere
|
||||
'tv_ids' => [
|
||||
// e.g., 67890,
|
||||
],
|
||||
|
||||
// Optional: hide by slug fragments (case-insensitive contains)
|
||||
// Applies to both movies and TV shows
|
||||
'slug_keywords' => [
|
||||
// e.g., 'some-movie-name'
|
||||
],
|
||||
|
||||
// Optional: hide by title keyword fragments (case-insensitive contains)
|
||||
'title_keywords' => [
|
||||
// e.g., 'test cut', 'sample'
|
||||
],
|
||||
];
|
||||
108
config/cache.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
];
|
||||
183
config/database.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
80
config/filesystems.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
132
config/logging.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
118
config/mail.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
],
|
||||
|
||||
];
|
||||
112
config/queue.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
38
config/services.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'token' => env('POSTMARK_TOKEN'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
217
config/session.php
Normal file
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "memcached",
|
||||
| "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug(env('APP_NAME', 'laravel')).'-session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain and all subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
];
|
||||
1
database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
44
database/factories/UserFactory.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
23
database/seeders/DatabaseSeeder.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
2772
package-lock.json
generated
Normal file
27
package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"postbuild": "php artisan optimize:clear || php artisan cache:clear && php artisan view:clear && php artisan route:clear",
|
||||
"deploy": "npm run build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"axios": "^1.11.0",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
||||
34
phpunit.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
6
postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
25
public/.htaccess
Normal file
@@ -0,0 +1,25 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
@@ -0,0 +1 @@
|
||||
546c91beb949b3d04db88e5c312186ea3753b4877456acedb72e08b6c32a43b4
|
||||
11
public/assets/fonts/material-symbols-rounded.woff2
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang=en>
|
||||
<meta charset=utf-8>
|
||||
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
|
||||
<title>Error 404 (Not Found)!!1</title>
|
||||
<style>
|
||||
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
|
||||
</style>
|
||||
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
|
||||
<p><b>404.</b> <ins>That’s an error.</ins>
|
||||
<p>The requested URL <code>/s/materialsymbolsrounded/v187/sykg-zNym6YjUruM-QrEh7-nyTnjDwKNJ_190FjpZIvDmUSVOK7BDB_Qb9vUSzq3wzLK-P0J-V_Zs-QtQth3-jOcbTCVpeRL2w5rwZu2rIekIA.woff2</code> was not found on this server. <ins>That’s all we know.</ins>
|
||||
11
public/assets/fonts/open-sans-500.woff2
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang=en>
|
||||
<meta charset=utf-8>
|
||||
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
|
||||
<title>Error 404 (Not Found)!!1</title>
|
||||
<style>
|
||||
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
|
||||
</style>
|
||||
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
|
||||
<p><b>404.</b> <ins>That’s an error.</ins>
|
||||
<p>The requested URL <code>/s/opensans/v40/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsg-1x4gaVc.woff2</code> was not found on this server. <ins>That’s all we know.</ins>
|
||||
11
public/assets/fonts/open-sans-600.woff2
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang=en>
|
||||
<meta charset=utf-8>
|
||||
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
|
||||
<title>Error 404 (Not Found)!!1</title>
|
||||
<style>
|
||||
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
|
||||
</style>
|
||||
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
|
||||
<p><b>404.</b> <ins>That’s an error.</ins>
|
||||
<p>The requested URL <code>/s/opensans/v40/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsiH1x4gaVc.woff2</code> was not found on this server. <ins>That’s all we know.</ins>
|
||||
11
public/assets/fonts/open-sans-700.woff2
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang=en>
|
||||
<meta charset=utf-8>
|
||||
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
|
||||
<title>Error 404 (Not Found)!!1</title>
|
||||
<style>
|
||||
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
|
||||
</style>
|
||||
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
|
||||
<p><b>404.</b> <ins>That’s an error.</ins>
|
||||
<p>The requested URL <code>/s/opensans/v40/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsg-1x4gaVc.woff2</code> was not found on this server. <ins>That’s all we know.</ins>
|
||||
11
public/assets/fonts/open-sans-regular.woff2
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang=en>
|
||||
<meta charset=utf-8>
|
||||
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
|
||||
<title>Error 404 (Not Found)!!1</title>
|
||||
<style>
|
||||
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
|
||||
</style>
|
||||
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
|
||||
<p><b>404.</b> <ins>That’s an error.</ins>
|
||||
<p>The requested URL <code>/s/opensans/v40/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVc.woff2</code> was not found on this server. <ins>That’s all we know.</ins>
|
||||
10
public/assets/images/logo-desktop.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg height="22" viewBox="0 0 686 69" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.64799 65.092V46.084C4.07999 47.108 6.63999 47.972 9.32799 48.676C12.016 49.316 14.736 49.86 17.488 50.308C20.304 50.692 23.056 50.98 25.744 51.172C28.496 51.3 31.12 51.364 33.616 51.364C36.112 51.364 38.096 51.3 39.568 51.172C41.104 51.044 42.256 50.884 43.024 50.692C43.792 50.436 44.24 50.212 44.368 50.02C44.56 49.764 44.56 49.54 44.368 49.348C44.112 49.156 42.832 49.06 40.528 49.06C38.288 48.996 35.504 48.868 32.176 48.676C28.912 48.484 25.392 48.132 21.616 47.62C17.904 47.044 14.416 46.18 11.152 45.028C7.95199 43.876 5.29599 42.308 3.18399 40.324C1.07199 38.276 0.0159912 35.684 0.0159912 32.548C0.0159912 29.924 0.687991 27.684 2.03199 25.828C3.43999 23.908 5.23199 22.308 7.40799 21.028C9.64799 19.748 12.176 18.724 14.992 17.956C17.872 17.188 20.752 16.612 23.632 16.228C26.576 15.78 29.424 15.492 32.176 15.364C34.928 15.236 37.36 15.172 39.472 15.172C44.336 15.172 49.296 15.396 54.352 15.844C59.408 16.228 64.56 16.932 69.808 17.956V37.156C64.24 35.108 58.896 33.668 53.776 32.836C48.72 32.004 43.76 31.588 38.896 31.588C35.312 31.588 32.816 31.684 31.408 31.876C30 32.068 29.136 32.42 28.816 32.932C28.432 33.572 29.264 34.052 31.312 34.372C33.36 34.628 36.016 34.884 39.28 35.14C42.608 35.396 46.256 35.78 50.224 36.292C54.192 36.74 57.904 37.54 61.36 38.692C64.816 39.78 67.696 41.316 70 43.3C72.304 45.22 73.456 47.78 73.456 50.98C73.456 54.308 72.368 57.092 70.192 59.332C68.016 61.572 65.072 63.396 61.36 64.804C57.712 66.212 53.488 67.204 48.688 67.78C43.888 68.42 38.864 68.74 33.616 68.74C28.304 68.74 22.928 68.484 17.488 67.972C12.112 67.524 6.83199 66.564 1.64799 65.092Z" fill="white"/>
|
||||
<path d="M78.4561 35.62V16.036H88.2481V0.00402832H113.208V16.036H143.64V35.62H113.208V47.524C113.208 48.1 113.464 48.612 113.976 49.06C114.488 49.444 115.352 49.764 116.568 50.02C117.848 50.212 119.544 50.372 121.656 50.5C123.768 50.564 126.456 50.596 129.72 50.596H146.232V66.532C143.096 67.236 139.448 67.78 135.288 68.164C131.192 68.548 127.096 68.74 123 68.74C116.664 68.74 111.288 68.388 106.872 67.684C102.456 67.044 98.8721 66.052 96.1201 64.708C93.3681 63.364 91.3521 61.636 90.0721 59.524C88.8561 57.412 88.2481 54.916 88.2481 52.036V35.62H78.4561Z" fill="white"/>
|
||||
<path d="M151.232 16.996H176.192V22.468C181.184 20.74 186.208 19.396 191.264 18.436C196.32 17.412 201.312 16.9 206.24 16.9V35.716C202.976 35.716 199.968 35.716 197.216 35.716C194.464 35.652 191.872 35.684 189.44 35.812C187.072 35.876 184.8 36.068 182.624 36.388C180.448 36.644 178.304 37.124 176.192 37.828V68.74H151.232V16.996Z" fill="white"/>
|
||||
<path d="M235.912 48.772C236.424 49.412 237.352 49.9561 238.696 50.4041C240.04 50.8521 241.64 51.2041 243.496 51.4601C245.352 51.7161 247.432 51.908 249.736 52.036C252.04 52.1 254.376 52.1 256.744 52.036C261.864 51.972 266.856 51.7801 271.72 51.4601C276.584 51.0761 281.576 50.6601 286.696 50.2121V65.3801C281.832 66.7881 276.808 67.6841 271.624 68.0681C266.44 68.5161 261.352 68.7401 256.36 68.7401C253.544 68.7401 250.44 68.58 247.048 68.26C243.72 68.004 240.392 67.5241 237.064 66.8201C233.736 66.0521 230.504 65.06 227.368 63.844C224.296 62.564 221.544 60.9321 219.112 58.9481C216.744 56.9001 214.824 54.5 213.352 51.748C211.944 48.932 211.24 45.6361 211.24 41.8601C211.24 38.2121 211.848 35.0441 213.064 32.356C214.28 29.604 215.912 27.236 217.96 25.252C220.072 23.268 222.472 21.636 225.16 20.356C227.912 19.0121 230.792 17.9561 233.8 17.188C236.808 16.42 239.848 15.876 242.92 15.556C246.056 15.236 249.032 15.076 251.848 15.076C257.544 15.076 262.696 15.78 267.304 17.188C271.976 18.5321 275.944 20.3881 279.208 22.7561C282.472 25.1241 284.968 27.9081 286.696 31.1081C288.488 34.3081 289.384 37.7 289.384 41.284V47.428L235.912 48.772ZM268.84 36.4841C268.392 35.9721 267.56 35.4601 266.344 34.9481C265.192 34.4361 263.88 33.9881 262.408 33.604C260.936 33.156 259.368 32.8041 257.704 32.548C256.04 32.2921 254.472 32.1641 253 32.1641C249.416 32.1641 246.152 32.5481 243.208 33.3161C240.328 34.0841 238.056 35.1401 236.392 36.4841H268.84Z" fill="white"/>
|
||||
<path d="M300.048 33.604V17.38C305.296 16.228 310.8 15.556 316.56 15.3641C322.384 15.1721 328.176 15.076 333.936 15.076C336.432 15.076 339.184 15.1721 342.192 15.3641C345.264 15.4921 348.336 15.812 351.408 16.3241C354.544 16.8361 357.584 17.5721 360.528 18.5321C363.536 19.4921 366.192 20.772 368.496 22.372C370.8 23.908 372.656 25.8281 374.064 28.132C375.472 30.3721 376.176 33.0921 376.176 36.292V67.6841H350.256V64.036C344.496 65.7 339.344 66.9161 334.8 67.6841C330.256 68.3881 325.84 68.7401 321.552 68.7401C318.928 68.7401 316.016 68.4841 312.816 67.9721C309.68 67.4601 306.736 66.5961 303.984 65.3801C301.296 64.1001 299.024 62.4041 297.168 60.2921C295.312 58.1161 294.384 55.396 294.384 52.132C294.384 49.636 294.928 47.4921 296.016 45.7001C297.168 43.8441 298.672 42.276 300.528 40.996C302.448 39.716 304.592 38.692 306.96 37.924C309.328 37.092 311.76 36.484 314.256 36.1C316.816 35.652 319.312 35.364 321.744 35.236C324.24 35.0441 326.48 34.9481 328.464 34.9481H349.296C349.296 34.6281 348.624 34.3081 347.28 33.9881C345.936 33.6681 344.304 33.412 342.384 33.22C340.464 32.964 338.48 32.7721 336.432 32.6441C334.384 32.452 332.656 32.356 331.248 32.356C325.616 32.356 320.24 32.356 315.12 32.356C310 32.356 304.976 32.772 300.048 33.604ZM350.256 48.58H330.864C326.448 48.58 323.344 48.7721 321.552 49.1561C319.824 49.4761 318.96 49.9881 318.96 50.6921C318.96 51.2681 319.536 51.748 320.688 52.132C321.84 52.452 323.152 52.74 324.624 52.996C326.096 53.188 327.504 53.3161 328.848 53.3801C330.192 53.4441 331.056 53.4761 331.44 53.4761C335.472 53.4761 339.152 53.2201 342.48 52.7081C345.808 52.1321 348.4 51.3321 350.256 50.3081V48.58Z" fill="white"/>
|
||||
<path d="M381.176 16.9959H406.136V21.8919C411.256 20.2279 415.992 18.8199 420.344 17.6679C424.76 16.5159 428.792 15.9399 432.44 15.9399C441.336 15.9399 447.576 18.5959 451.16 23.9079C456.92 21.8599 462.264 20.0359 467.192 18.4359C472.184 16.7719 476.696 15.9399 480.728 15.9399C487.704 15.9399 493.112 17.6039 496.952 20.9319C500.792 24.2599 502.712 29.2519 502.712 35.9079V68.7399H477.752V39.6519C477.752 37.5399 477.08 36.0679 475.736 35.2359C474.456 34.4039 472.728 34.0199 470.552 34.0839C468.44 34.1479 465.976 34.5639 463.16 35.3319C460.344 36.0999 457.432 36.9959 454.424 38.0199V68.7399H429.464V39.6519C429.464 37.5399 428.792 36.0359 427.448 35.1399C426.168 34.2439 424.44 33.7959 422.264 33.7959C420.152 33.7319 417.688 34.0519 414.872 34.7559C412.056 35.3959 409.144 36.1959 406.136 37.1559V68.7399H381.176V16.9959Z" fill="white"/>
|
||||
<path d="M529.79 0.0579043L594.39 0.00112611C597.516 -0.0693572 599.305 3.18267 597.889 5.86495L575.788 34.4831C574.581 36.3451 575.584 38.5947 577.729 38.9765L620.828 38.9706C622.408 39.1057 623.73 40.4194 623.125 42.0523L604.27 66.279C602.717 67.9784 601.649 68.5892 599.289 68.74H532.674C529.406 68.693 527.505 65.6446 528.979 62.6804L551.158 34.1405C552.769 32.2492 552.003 29.6492 549.531 29.1773L510.154 29.1832C507.09 28.4098 507.235 26.9669 508.804 24.6997C514.25 16.8349 521.338 9.52225 526.905 1.67511C527.73 0.854758 528.654 0.320259 529.79 0.0559465V0.0579043Z" fill="#dc2626"/>
|
||||
<path d="M685.452 16.0094V53.3168C685.452 55.6789 684.965 57.8094 683.993 59.7084C683.02 61.6536 681.631 63.2747 679.824 64.5715C678.018 65.9147 675.818 66.9336 673.224 67.6284C670.631 68.3694 667.69 68.7399 664.401 68.7399H635.639V56.5126H658.426C661.808 56.5126 664.146 56.2115 665.443 55.6094C666.74 55.0073 667.412 53.9189 667.458 52.3441V49.0789C662.873 50.561 658.635 51.7884 654.744 52.761C650.854 53.6873 647.334 54.1505 644.184 54.1505C639.136 54.1041 635.222 52.8768 632.443 50.4684C629.664 48.0599 628.275 44.4473 628.275 39.6305V15.9399H646.338V36.921C646.338 38.5884 646.894 39.7694 648.005 40.4641C649.117 41.1589 650.576 41.5063 652.382 41.5063C654.466 41.5063 656.829 41.1589 659.469 40.4641C662.109 39.7694 664.772 38.9589 667.458 38.0326V16.0094H685.452Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
3
public/assets/images/logo-mobile.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg height="32" viewBox="0 0 117 69" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.7904 0.0579043L87.3902 0.00112611C90.5164 -0.0693572 92.3047 3.18267 90.8885 5.86495L68.7879 34.4831C67.5813 36.3451 68.5842 38.5947 70.729 38.9765L113.828 38.9706C115.408 39.1057 116.73 40.4194 116.125 42.0523L97.2702 66.279C95.7169 67.9784 94.6493 68.5892 92.289 68.74H25.6737C22.4065 68.693 20.5045 65.6446 21.9795 62.6804L44.1585 34.1405C45.7686 32.2492 45.0027 29.6492 42.5308 29.1773L3.15388 29.1832C0.0903822 28.4098 0.23533 26.9669 1.80429 24.6997C7.24964 16.8349 14.3384 9.52225 19.9052 1.67511C20.7298 0.854758 21.6543 0.320259 22.7904 0.0559465V0.0579043Z" fill="#dc2626"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 691 B |
BIN
public/assets/videos/hero-loop.mp4
Normal file
0
public/favicon.ico
Normal file
3
public/favicon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg height="32" viewBox="0 0 117 69" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.7904 0.0579043L87.3902 0.00112611C90.5164 -0.0693572 92.3047 3.18267 90.8885 5.86495L68.7879 34.4831C67.5813 36.3451 68.5842 38.5947 70.729 38.9765L113.828 38.9706C115.408 39.1057 116.73 40.4194 116.125 42.0523L97.2702 66.279C95.7169 67.9784 94.6493 68.5892 92.289 68.74H25.6737C22.4065 68.693 20.5045 65.6446 21.9795 62.6804L44.1585 34.1405C45.7686 32.2492 45.0027 29.6492 42.5308 29.1773L3.15388 29.1832C0.0903822 28.4098 0.23533 26.9669 1.80429 24.6997C7.24964 16.8349 14.3384 9.52225 19.9052 1.67511C20.7298 0.854758 21.6543 0.320259 22.7904 0.0559465V0.0579043Z" fill="#dc2626"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 691 B |
20
public/index.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
10
public/robots.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Block internal or thin pages
|
||||
Disallow: /search
|
||||
Disallow: /api/
|
||||
Disallow: /embed/
|
||||
|
||||
# Sitemap location
|
||||
Sitemap: https://streamzy.ch/sitemap.xml
|
||||
809
resources/css/app.css
Normal file
@@ -0,0 +1,809 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Plus Jakarta Sans', 'BR-Shape', 'BRShape', 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
|
||||
--color-red-50: #fef2f2;
|
||||
--color-red-100: #fee2e2;
|
||||
--color-red-200: #fecaca;
|
||||
--color-red-300: #fca5a5;
|
||||
--color-red-400: #f87171;
|
||||
--color-red-500: #ef4444;
|
||||
--color-red-600: #dc2626;
|
||||
--color-red-700: #b91c1c;
|
||||
--color-red-800: #991b1b;
|
||||
--color-red-900: #7f1d1d;
|
||||
--color-red-950: #450a0a;
|
||||
|
||||
--color-yellow-50: #fefce8;
|
||||
--color-yellow-100: #fef3c7;
|
||||
--color-yellow-200: #fde68a;
|
||||
--color-yellow-300: #fcd34d;
|
||||
--color-yellow-400: #fbbf24;
|
||||
--color-yellow-500: #f59e0b;
|
||||
--color-yellow-600: #d97706;
|
||||
--color-yellow-700: #b45309;
|
||||
--color-yellow-800: #92400e;
|
||||
--color-yellow-900: #78350f;
|
||||
--color-yellow-950: #451a03;
|
||||
|
||||
--color-zinc-50: #fafafa;
|
||||
--color-zinc-100: #f4f4f5;
|
||||
--color-zinc-200: #e4e4e7;
|
||||
--color-zinc-300: #d4d4d8;
|
||||
--color-zinc-400: #a1a1aa;
|
||||
--color-zinc-500: #71717a;
|
||||
--color-zinc-600: #52525b;
|
||||
--color-zinc-700: #3f3f46;
|
||||
--color-zinc-800: #27272a;
|
||||
--color-zinc-900: #18181b;
|
||||
--color-zinc-950: #09090b;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
html, body {
|
||||
background-color: var(--color-zinc-950);
|
||||
color: var(--color-zinc-100);
|
||||
font-family: var(--font-sans);
|
||||
/* Modern subtle texture */
|
||||
background-image:
|
||||
radial-gradient(1200px 500px at 10% -10%, rgba(24, 24, 27, 0.18), transparent 60%),
|
||||
radial-gradient(1000px 400px at 110% 0%, rgba(24, 24, 27, 0.15), transparent 60%);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
/* Simple fixed padding utilities */
|
||||
.pad-hero { padding-block: 4rem; }
|
||||
.pad-section { padding-block: 2rem; }
|
||||
|
||||
/* Minimal full-bleed helper */
|
||||
.hero-fullbleed { position: relative; overflow: hidden; }
|
||||
|
||||
/* Less containered: global wrap utility for wider pages */
|
||||
.wrap {
|
||||
max-width: 90rem; /* 1440px */
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1.5rem; /* px-6 */
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
.wrap { max-width: 100rem; } /* 1600px */
|
||||
}
|
||||
|
||||
@media (min-width: 1920px) {
|
||||
.wrap { max-width: 112rem; } /* 1792px */
|
||||
}
|
||||
|
||||
/* Ensure form controls also inherit the font */
|
||||
input, select, textarea, button {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
/* Scrollbar hiding utility */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Custom Component Classes */
|
||||
.movie-poster {
|
||||
aspect-ratio: 2/3;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.movie-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.movie-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.movie-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--color-red-600);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--color-red-700);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--color-zinc-800);
|
||||
color: var(--color-zinc-100);
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--color-zinc-700);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #0f0f10;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 12px 24px -12px rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.nomarginbc li {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.movie-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 0.75rem;
|
||||
background-color: #0f0f10;
|
||||
transition: transform .25s ease, border-color .25s ease;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.movie-card:hover { transform: translateY(-2px); border-color: rgba(255, 255, 255, 0.12); }
|
||||
|
||||
.movie-card .movie-poster {
|
||||
aspect-ratio: 2/3 !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
object-fit: cover !important;
|
||||
border-radius: 0.75rem 0.75rem 0 0 !important;
|
||||
background-color: #1a1a1e !important;
|
||||
display: block !important;
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.group:hover .movie-poster {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Title hover color inside cards */
|
||||
.movie-card a:hover h3,
|
||||
.enhanced-movie-card a:hover h3 {
|
||||
color: var(--color-red-300);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
background-color: rgba(39, 39, 42, 0.8);
|
||||
border: 1px solid rgba(82, 82, 91, 0.7);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
color: var(--color-zinc-100);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--color-zinc-400);
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--color-red-600);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* Hero section styles */
|
||||
.hero-backdrop {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-backdrop::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0.4) 0%,
|
||||
rgba(0, 0, 0, 0.6) 50%,
|
||||
rgba(0, 0, 0, 0.8) 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Video player modal styles */
|
||||
.video-modal {
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.video-container {
|
||||
aspect-ratio: 16/9;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.video-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Search container */
|
||||
.search-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Utility styles */
|
||||
.text-shadow {
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
|
||||
.gradient-overlay {
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
|
||||
}
|
||||
|
||||
/* Additional utility classes */
|
||||
.line-clamp-1 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Simple aspect-square helper (in case utility not present) */
|
||||
.aspect-square {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
/* Logo chip images (networks/production) */
|
||||
.logo-chip-img {
|
||||
height: 16px;
|
||||
max-height: 16px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Ensure Tailwind's hidden works on Material Symbols */
|
||||
.material-symbols-rounded.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Responsive text */
|
||||
@media (max-width: 640px) {
|
||||
.hero-title {
|
||||
font-size: 2rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced Movie Banner Hover Effects */
|
||||
.movie-banner {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 0.75rem;
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
background: linear-gradient(135deg, var(--color-zinc-900), var(--color-zinc-800));
|
||||
border: 1px solid var(--color-zinc-700);
|
||||
}
|
||||
|
||||
.movie-banner:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(220, 38, 38, 0.3),
|
||||
0 0 20px rgba(220, 38, 38, 0.1);
|
||||
border-color: var(--color-red-600);
|
||||
}
|
||||
|
||||
.movie-banner-image {
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.movie-banner:hover .movie-banner-image {
|
||||
transform: scale(1.1);
|
||||
filter: brightness(1.1) contrast(1.1);
|
||||
}
|
||||
|
||||
.movie-banner-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.1) 20%,
|
||||
rgba(0, 0, 0, 0.6) 70%,
|
||||
rgba(0, 0, 0, 0.9) 100%
|
||||
);
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.movie-banner:hover .movie-banner-overlay {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(220, 38, 38, 0.1) 0%,
|
||||
rgba(0, 0, 0, 0.2) 20%,
|
||||
rgba(0, 0, 0, 0.7) 70%,
|
||||
rgba(0, 0, 0, 0.95) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.movie-banner-content {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1.5rem;
|
||||
transform: translateY(10px);
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.movie-banner:hover .movie-banner-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.movie-banner-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin-bottom: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.movie-banner:hover .movie-banner-title {
|
||||
color: var(--color-red-100);
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.movie-banner-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: var(--color-zinc-300);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.75rem;
|
||||
opacity: 0.8;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.movie-banner:hover .movie-banner-meta {
|
||||
opacity: 1;
|
||||
color: var(--color-zinc-200);
|
||||
}
|
||||
|
||||
.movie-banner-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.movie-banner-play {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.8);
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
background: rgba(220, 38, 38, 0.9);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
backdrop-filter: blur(4px);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.movie-banner:hover .movie-banner-play {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
.movie-banner-play:hover {
|
||||
background: var(--color-red-600);
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
box-shadow: 0 0 20px rgba(220, 38, 38, 0.4);
|
||||
}
|
||||
|
||||
.movie-banner-genre {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: rgba(220, 38, 38, 0.9);
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.movie-banner:hover .movie-banner-genre {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Enhanced card hover for list views */
|
||||
.enhanced-movie-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 0.75rem;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
background: var(--color-zinc-900);
|
||||
border: 1px solid var(--color-zinc-800);
|
||||
}
|
||||
|
||||
.enhanced-movie-card:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow:
|
||||
0 20px 40px -12px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(220, 38, 38, 0.2);
|
||||
border-color: var(--color-red-600);
|
||||
}
|
||||
|
||||
.enhanced-movie-card .movie-poster {
|
||||
transition: all 0.3s ease;
|
||||
aspect-ratio: 2/3 !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
object-fit: cover !important;
|
||||
border-radius: 0.75rem 0.75rem 0 0 !important;
|
||||
background-color: var(--color-zinc-800) !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.enhanced-movie-card:hover .movie-poster {
|
||||
transform: scale(1.05);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
/* Floating action buttons */
|
||||
.floating-actions {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.enhanced-movie-card:hover .floating-actions {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.floating-action-btn {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 1px solid var(--color-zinc-600);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.floating-action-btn:hover {
|
||||
background: var(--color-red-600);
|
||||
border-color: var(--color-red-500);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* I'm Feeling Lucky Enhanced Styles */
|
||||
.content-type-card {
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + .content-type-card {
|
||||
background: linear-gradient(135deg, var(--color-red-600), var(--color-red-700));
|
||||
border-color: var(--color-red-500);
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 10px 25px -3px rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + .content-type-card .text-zinc-400 {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* Ensure hero overlays layer correctly */
|
||||
.hero-backdrop { position: absolute; inset: 0; z-index: 0; }
|
||||
.hero-gradient { position: absolute; inset: 0; z-index: 10; pointer-events: none; }
|
||||
.hero-content { position: relative; z-index: 20; }
|
||||
|
||||
/* Header and Navigation Styles */
|
||||
.header-backdrop {
|
||||
backdrop-filter: blur(20px);
|
||||
background: rgba(9, 9, 11, 0.8);
|
||||
border-bottom: 1px solid rgba(244, 244, 245, 0.06);
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
backdrop-filter: blur(20px);
|
||||
background: rgba(9, 9, 11, 0.95);
|
||||
border: 1px solid rgba(244, 244, 245, 0.08);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.mobile-menu-item {
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-menu-item:hover {
|
||||
background: rgba(244, 244, 245, 0.1);
|
||||
color: var(--color-red-400);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--color-red-400);
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scaleX(0);
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--color-red-500);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover::after {
|
||||
transform: translateX(-50%) scaleX(1);
|
||||
}
|
||||
|
||||
/* Footer Styles */
|
||||
.footer-gradient {
|
||||
background: linear-gradient(to bottom, transparent 0%, rgba(9, 9, 11, 0.25) 20%, rgba(9, 9, 11, 0.85) 60%, rgba(9, 9, 11, 1) 100%);
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
transition: all 0.2s ease;
|
||||
color: var(--color-zinc-400);
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: var(--color-red-400);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.footer-section-title {
|
||||
color: var(--color-zinc-100);
|
||||
font-weight: 600;
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.footer-section-title::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
width: 2rem;
|
||||
height: 2px;
|
||||
background: var(--color-red-500);
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
transition: all 0.2s ease;
|
||||
color: var(--color-zinc-400);
|
||||
}
|
||||
|
||||
.social-icon:hover {
|
||||
color: var(--color-red-400);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Material Symbols Styling */
|
||||
.material-symbols-rounded {
|
||||
font-variation-settings:
|
||||
'FILL' 0,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 24;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.material-symbols-rounded.filled {
|
||||
font-variation-settings:
|
||||
'FILL' 1,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 24;
|
||||
}
|
||||
.word .material-symbols-rounded {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
.material-symbols-rounded.large {
|
||||
font-size: 32px;
|
||||
font-variation-settings:
|
||||
'FILL' 0,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 32;
|
||||
}
|
||||
|
||||
/* Mobile Menu Animation */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-menu.show {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for header */
|
||||
@media (max-width: 768px) {
|
||||
.header-backdrop {
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
margin-top: 0.5rem;
|
||||
right: 1rem;
|
||||
left: 1rem;
|
||||
}
|
||||
}
|
||||
/* Optimized 3D Dice Animation */
|
||||
.dice-container {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
perspective: 200px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dice {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
animation: dice-spin 4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes dice-spin {
|
||||
0% { transform: rotateX(0deg) rotateY(0deg); }
|
||||
20% { transform: rotateX(72deg) rotateY(90deg); }
|
||||
40% { transform: rotateX(144deg) rotateY(180deg); }
|
||||
60% { transform: rotateX(216deg) rotateY(270deg); }
|
||||
80% { transform: rotateX(288deg) rotateY(360deg); }
|
||||
100% { transform: rotateX(360deg) rotateY(450deg); }
|
||||
}
|
||||
.dice-face {
|
||||
position: absolute;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
background: linear-gradient(135deg, #fbbf24, #f59e0b);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
box-shadow: 0 3px 6px rgba(251, 191, 36, 0.25);
|
||||
border: 1px solid rgba(251, 191, 36, 0.5);
|
||||
}
|
||||
.dice-face-1 { transform: rotateY(0deg) translateZ(17.5px); }
|
||||
.dice-face-2 { transform: rotateY(90deg) translateZ(17.5px); }
|
||||
.dice-face-3 { transform: rotateY(180deg) translateZ(17.5px); }
|
||||
.dice-face-4 { transform: rotateY(-90deg) translateZ(17.5px); }
|
||||
.dice-face-5 { transform: rotateX(90deg) translateZ(17.5px); }
|
||||
.dice-face-6 { transform: rotateX(-90deg) translateZ(17.5px); }
|
||||
|
||||
/* Pause animation on hover for better UX */
|
||||
.dice-container:hover .dice { transform: scale(0.98); }
|
||||
|
||||
/* Performance optimizations */
|
||||
.dice, .dice-face { will-change: transform; backface-visibility: hidden; }
|
||||
|
||||
/* Hero Rotating Icons */
|
||||
.hero-rotator {
|
||||
position: relative;
|
||||
display: inline-grid; /* layer items and center */
|
||||
place-items: center;
|
||||
height: 1em; /* lock line height to avoid layout shift */
|
||||
vertical-align: middle;
|
||||
line-height: 1em; /* sync with text to avoid vertical clipping */
|
||||
}
|
||||
.hero-rotator .word {
|
||||
grid-area: 1 / 1; /* stack in one cell */
|
||||
opacity: 0;
|
||||
transform: translateY(10%) scale(.9);
|
||||
transform-origin: center;
|
||||
transition: opacity .45s ease, transform .45s ease;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1em;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.hero-rotator .word.active {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
animation: rotator-pop .25s ease;
|
||||
}
|
||||
@keyframes rotator-pop {
|
||||
0% { transform: translateY(0) scale(.9); }
|
||||
100% { transform: translateY(0) scale(1); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-rotator .word { transition: none; transform: translateY(0) scale(1); }
|
||||
.hero-rotator .word.active { animation: none; }
|
||||
}
|
||||
1
resources/js/app.js
Normal file
@@ -0,0 +1 @@
|
||||
import './bootstrap';
|
||||
4
resources/js/bootstrap.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
72
resources/views/about/index.blade.php
Normal file
@@ -0,0 +1,72 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<!-- Hero Section -->
|
||||
<section class="relative py-20 overflow-hidden bg-zinc-950">
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[50%] left-[20%] w-[100%] h-[100%] bg-red-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute -bottom-[50%] right-[20%] w-[100%] h-[100%] bg-purple-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 brightness-100 contrast-150"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-zinc-950/50 via-zinc-950/80 to-zinc-950"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-6">
|
||||
About <span class="text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-purple-500">Streamzy</span>
|
||||
</h1>
|
||||
<p class="text-xl text-zinc-400 max-w-2xl mx-auto">
|
||||
The next generation of streaming. Unlimited entertainment, zero compromise.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Content Section -->
|
||||
<section class="relative py-12 bg-zinc-950 min-h-[50vh]">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-zinc-900/50 border border-white/5 rounded-2xl p-8 md:p-12 backdrop-blur-sm">
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">Our Mission</h3>
|
||||
<p class="text-zinc-400 mb-8">
|
||||
Streamzy was built with a single goal in mind: to provide a seamless, high-quality streaming experience for everyone. We believe that entertainment should be accessible, fast, and beautiful.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 my-12">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-red-500/10 flex items-center justify-center text-red-500">
|
||||
<span class="material-symbols-rounded text-2xl">speed</span>
|
||||
</div>
|
||||
<h4 class="text-lg font-medium text-white">Lightning Fast</h4>
|
||||
<p class="text-zinc-400 text-sm">Optimized for speed and performance on any device.</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-purple-500/10 flex items-center justify-center text-purple-500">
|
||||
<span class="material-symbols-rounded text-2xl">hd</span>
|
||||
</div>
|
||||
<h4 class="text-lg font-medium text-white">High Quality</h4>
|
||||
<p class="text-zinc-400 text-sm">Crystal clear HD streaming for the best viewing experience.</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-blue-500/10 flex items-center justify-center text-blue-500">
|
||||
<span class="material-symbols-rounded text-2xl">devices</span>
|
||||
</div>
|
||||
<h4 class="text-lg font-medium text-white">Cross Platform</h4>
|
||||
<p class="text-zinc-400 text-sm">Watch on your phone, tablet, or desktop computer.</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-green-500/10 flex items-center justify-center text-green-500">
|
||||
<span class="material-symbols-rounded text-2xl">update</span>
|
||||
</div>
|
||||
<h4 class="text-lg font-medium text-white">Daily Updates</h4>
|
||||
<p class="text-zinc-400 text-sm">New content added every single day.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">Contact Us</h3>
|
||||
<p class="text-zinc-400">
|
||||
Have questions or suggestions? We'd love to hear from you. Reach out to our support team at <a href="mailto:support@streamzy.ch" class="text-red-400 hover:text-red-300 transition-colors">support@streamzy.ch</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
3
resources/views/components/application-icon.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 117 69" fill="none" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||
<path d="M22.7904 0.0579043L87.3902 0.00112611C90.5164 -0.0693572 92.3047 3.18267 90.8885 5.86495L68.7879 34.4831C67.5813 36.3451 68.5842 38.5947 70.729 38.9765L113.828 38.9706C115.408 39.1057 116.73 40.4194 116.125 42.0523L97.2702 66.279C95.7169 67.9784 94.6493 68.5892 92.289 68.74H25.6737C22.4065 68.693 20.5045 65.6446 21.9795 62.6804L44.1585 34.1405C45.7686 32.2492 45.0027 29.6492 42.5308 29.1773L3.15388 29.1832C0.0903822 28.4098 0.23533 26.9669 1.80429 24.6997C7.24964 16.8349 14.3384 9.52225 19.9052 1.67511C20.7298 0.854758 21.6543 0.320259 22.7904 0.0559465V0.0579043Z" fill="#dc2626"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 700 B |
10
resources/views/components/application-logo-full.blade.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg viewBox="0 0 686 69" fill="none" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||
<path d="M1.64799 65.092V46.084C4.07999 47.108 6.63999 47.972 9.32799 48.676C12.016 49.316 14.736 49.86 17.488 50.308C20.304 50.692 23.056 50.98 25.744 51.172C28.496 51.3 31.12 51.364 33.616 51.364C36.112 51.364 38.096 51.3 39.568 51.172C41.104 51.044 42.256 50.884 43.024 50.692C43.792 50.436 44.24 50.212 44.368 50.02C44.56 49.764 44.56 49.54 44.368 49.348C44.112 49.156 42.832 49.06 40.528 49.06C38.288 48.996 35.504 48.868 32.176 48.676C28.912 48.484 25.392 48.132 21.616 47.62C17.904 47.044 14.416 46.18 11.152 45.028C7.95199 43.876 5.29599 42.308 3.18399 40.324C1.07199 38.276 0.0159912 35.684 0.0159912 32.548C0.0159912 29.924 0.687991 27.684 2.03199 25.828C3.43999 23.908 5.23199 22.308 7.40799 21.028C9.64799 19.748 12.176 18.724 14.992 17.956C17.872 17.188 20.752 16.612 23.632 16.228C26.576 15.78 29.424 15.492 32.176 15.364C34.928 15.236 37.36 15.172 39.472 15.172C44.336 15.172 49.296 15.396 54.352 15.844C59.408 16.228 64.56 16.932 69.808 17.956V37.156C64.24 35.108 58.896 33.668 53.776 32.836C48.72 32.004 43.76 31.588 38.896 31.588C35.312 31.588 32.816 31.684 31.408 31.876C30 32.068 29.136 32.42 28.816 32.932C28.432 33.572 29.264 34.052 31.312 34.372C33.36 34.628 36.016 34.884 39.28 35.14C42.608 35.396 46.256 35.78 50.224 36.292C54.192 36.74 57.904 37.54 61.36 38.692C64.816 39.78 67.696 41.316 70 43.3C72.304 45.22 73.456 47.78 73.456 50.98C73.456 54.308 72.368 57.092 70.192 59.332C68.016 61.572 65.072 63.396 61.36 64.804C57.712 66.212 53.488 67.204 48.688 67.78C43.888 68.42 38.864 68.74 33.616 68.74C28.304 68.74 22.928 68.484 17.488 67.972C12.112 67.524 6.83199 66.564 1.64799 65.092Z" fill="currentColor"/>
|
||||
<path d="M78.4561 35.62V16.036H88.2481V0.00402832H113.208V16.036H143.64V35.62H113.208V47.524C113.208 48.1 113.464 48.612 113.976 49.06C114.488 49.444 115.352 49.764 116.568 50.02C117.848 50.212 119.544 50.372 121.656 50.5C123.768 50.564 126.456 50.596 129.72 50.596H146.232V66.532C143.096 67.236 139.448 67.78 135.288 68.164C131.192 68.548 127.096 68.74 123 68.74C116.664 68.74 111.288 68.388 106.872 67.684C102.456 67.044 98.8721 66.052 96.1201 64.708C93.3681 63.364 91.3521 61.636 90.0721 59.524C88.8561 57.412 88.2481 54.916 88.2481 52.036V35.62H78.4561Z" fill="currentColor"/>
|
||||
<path d="M151.232 16.996H176.192V22.468C181.184 20.74 186.208 19.396 191.264 18.436C196.32 17.412 201.312 16.9 206.24 16.9V35.716C202.976 35.716 199.968 35.716 197.216 35.716C194.464 35.652 191.872 35.684 189.44 35.812C187.072 35.876 184.8 36.068 182.624 36.388C180.448 36.644 178.304 37.124 176.192 37.828V68.74H151.232V16.996Z" fill="currentColor"/>
|
||||
<path d="M235.912 48.772C236.424 49.412 237.352 49.9561 238.696 50.4041C240.04 50.8521 241.64 51.2041 243.496 51.4601C245.352 51.7161 247.432 51.908 249.736 52.036C252.04 52.1 254.376 52.1 256.744 52.036C261.864 51.972 266.856 51.7801 271.72 51.4601C276.584 51.0761 281.576 50.6601 286.696 50.2121V65.3801C281.832 66.7881 276.808 67.6841 271.624 68.0681C266.44 68.5161 261.352 68.7401 256.36 68.7401C253.544 68.7401 250.44 68.58 247.048 68.26C243.72 68.004 240.392 67.5241 237.064 66.8201C233.736 66.0521 230.504 65.06 227.368 63.844C224.296 62.564 221.544 60.9321 219.112 58.9481C216.744 56.9001 214.824 54.5 213.352 51.748C211.944 48.932 211.24 45.6361 211.24 41.8601C211.24 38.2121 211.848 35.0441 213.064 32.356C214.28 29.604 215.912 27.236 217.96 25.252C220.072 23.268 222.472 21.636 225.16 20.356C227.912 19.0121 230.792 17.9561 233.8 17.188C236.808 16.42 239.848 15.876 242.92 15.556C246.056 15.236 249.032 15.076 251.848 15.076C257.544 15.076 262.696 15.78 267.304 17.188C271.976 18.5321 275.944 20.3881 279.208 22.7561C282.472 25.1241 284.968 27.9081 286.696 31.1081C288.488 34.3081 289.384 37.7 289.384 41.284V47.428L235.912 48.772ZM268.84 36.4841C268.392 35.9721 267.56 35.4601 266.344 34.9481C265.192 34.4361 263.88 33.9881 262.408 33.604C260.936 33.156 259.368 32.8041 257.704 32.548C256.04 32.2921 254.472 32.1641 253 32.1641C249.416 32.1641 246.152 32.5481 243.208 33.3161C240.328 34.0841 238.056 35.1401 236.392 36.4841H268.84Z" fill="currentColor"/>
|
||||
<path d="M300.048 33.604V17.38C305.296 16.228 310.8 15.556 316.56 15.3641C322.384 15.1721 328.176 15.076 333.936 15.076C336.432 15.076 339.184 15.1721 342.192 15.3641C345.264 15.4921 348.336 15.812 351.408 16.3241C354.544 16.8361 357.584 17.5721 360.528 18.5321C363.536 19.4921 366.192 20.772 368.496 22.372C370.8 23.908 372.656 25.8281 374.064 28.132C375.472 30.3721 376.176 33.0921 376.176 36.292V67.6841H350.256V64.036C344.496 65.7 339.344 66.9161 334.8 67.6841C330.256 68.3881 325.84 68.7401 321.552 68.7401C318.928 68.7401 316.016 68.4841 312.816 67.9721C309.68 67.4601 306.736 66.5961 303.984 65.3801C301.296 64.1001 299.024 62.4041 297.168 60.2921C295.312 58.1161 294.384 55.396 294.384 52.132C294.384 49.636 294.928 47.4921 296.016 45.7001C297.168 43.8441 298.672 42.276 300.528 40.996C302.448 39.716 304.592 38.692 306.96 37.924C309.328 37.092 311.76 36.484 314.256 36.1C316.816 35.652 319.312 35.364 321.744 35.236C324.24 35.0441 326.48 34.9481 328.464 34.9481H349.296C349.296 34.6281 348.624 34.3081 347.28 33.9881C345.936 33.6681 344.304 33.412 342.384 33.22C340.464 32.964 338.48 32.7721 336.432 32.6441C334.384 32.452 332.656 32.356 331.248 32.356C325.616 32.356 320.24 32.356 315.12 32.356C310 32.356 304.976 32.772 300.048 33.604ZM350.256 48.58H330.864C326.448 48.58 323.344 48.7721 321.552 49.1561C319.824 49.4761 318.96 49.9881 318.96 50.6921C318.96 51.2681 319.536 51.748 320.688 52.132C321.84 52.452 323.152 52.74 324.624 52.996C326.096 53.188 327.504 53.3161 328.848 53.3801C330.192 53.4441 331.056 53.4761 331.44 53.4761C335.472 53.4761 339.152 53.2201 342.48 52.7081C345.808 52.1321 348.4 51.3321 350.256 50.3081V48.58Z" fill="currentColor"/>
|
||||
<path d="M381.176 16.9959H406.136V21.8919C411.256 20.2279 415.992 18.8199 420.344 17.6679C424.76 16.5159 428.792 15.9399 432.44 15.9399C441.336 15.9399 447.576 18.5959 451.16 23.9079C456.92 21.8599 462.264 20.0359 467.192 18.4359C472.184 16.7719 476.696 15.9399 480.728 15.9399C487.704 15.9399 493.112 17.6039 496.952 20.9319C500.792 24.2599 502.712 29.2519 502.712 35.9079V68.7399H477.752V39.6519C477.752 37.5399 477.08 36.0679 475.736 35.2359C474.456 34.4039 472.728 34.0199 470.552 34.0839C468.44 34.1479 465.976 34.5639 463.16 35.3319C460.344 36.0999 457.432 36.9959 454.424 38.0199V68.7399H429.464V39.6519C429.464 37.5399 428.792 36.0359 427.448 35.1399C426.168 34.2439 424.44 33.7959 422.264 33.7959C420.152 33.7319 417.688 34.0519 414.872 34.7559C412.056 35.3959 409.144 36.1959 406.136 37.1559V68.7399H381.176V16.9959Z" fill="currentColor"/>
|
||||
<path d="M529.79 0.0579043L594.39 0.00112611C597.516 -0.0693572 599.305 3.18267 597.889 5.86495L575.788 34.4831C574.581 36.3451 575.584 38.5947 577.729 38.9765L620.828 38.9706C622.408 39.1057 623.73 40.4194 623.125 42.0523L604.27 66.279C602.717 67.9784 601.649 68.5892 599.289 68.74H532.674C529.406 68.693 527.505 65.6446 528.979 62.6804L551.158 34.1405C552.769 32.2492 552.003 29.6492 549.531 29.1773L510.154 29.1832C507.09 28.4098 507.235 26.9669 508.804 24.6997C514.25 16.8349 521.338 9.52225 526.905 1.67511C527.73 0.854758 528.654 0.320259 529.79 0.0559465V0.0579043Z" fill="#dc2626"/>
|
||||
<path d="M685.452 16.0094V53.3168C685.452 55.6789 684.965 57.8094 683.993 59.7084C683.02 61.6536 681.631 63.2747 679.824 64.5715C678.018 65.9147 675.818 66.9336 673.224 67.6284C670.631 68.3694 667.69 68.7399 664.401 68.7399H635.639V56.5126H658.426C661.808 56.5126 664.146 56.2115 665.443 55.6094C666.74 55.0073 667.412 53.9189 667.458 52.3441V49.0789C662.873 50.561 658.635 51.7884 654.744 52.761C650.854 53.6873 647.334 54.1505 644.184 54.1505C639.136 54.1041 635.222 52.8768 632.443 50.4684C629.664 48.0599 628.275 44.4473 628.275 39.6305V15.9399H646.338V36.921C646.338 38.5884 646.894 39.7694 648.005 40.4641C649.117 41.1589 650.576 41.5063 652.382 41.5063C654.466 41.5063 656.829 41.1589 659.469 40.4641C662.109 39.7694 664.772 38.9589 667.458 38.0326V16.0094H685.452Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
3
resources/views/components/application-logo.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="507 0 117 69" fill="none" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||
<path d="M529.79 0.0579043L594.39 0.00112611C597.516 -0.0693572 599.305 3.18267 597.889 5.86495L575.788 34.4831C574.581 36.3451 575.584 38.5947 577.729 38.9765L620.828 38.9706C622.408 39.1057 623.73 40.4194 623.125 42.0523L604.27 66.279C602.717 67.9784 601.649 68.5892 599.289 68.74H532.674C529.406 68.693 527.505 65.6446 528.979 62.6804L551.158 34.1405C552.769 32.2492 552.003 29.6492 549.531 29.1773L510.154 29.1832C507.09 28.4098 507.235 26.9669 508.804 24.6997C514.25 16.8349 521.338 9.52225 526.905 1.67511C527.73 0.854758 528.654 0.320259 529.79 0.0559465V0.0579043Z" fill="currentColor"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 707 B |
26
resources/views/errors/404.blade.php
Normal file
@@ -0,0 +1,26 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Page Not Found - Streamzy')
|
||||
|
||||
@section('content')
|
||||
<section class="relative min-h-[70vh] flex items-center justify-center overflow-hidden bg-zinc-950">
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[50%] left-[20%] w-[100%] h-[100%] bg-red-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute -bottom-[50%] right-[20%] w-[100%] h-[100%] bg-purple-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 brightness-100 contrast-150"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-zinc-950/50 via-zinc-950/80 to-zinc-950"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 text-center px-4">
|
||||
<h1 class="text-9xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-purple-600 mb-4">404</h1>
|
||||
<h2 class="text-3xl font-bold text-white mb-6">Page Not Found</h2>
|
||||
<p class="text-xl text-zinc-400 max-w-lg mx-auto mb-10">
|
||||
The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.
|
||||
</p>
|
||||
<a href="{{ route('home') }}" class="inline-flex items-center gap-2 px-8 py-4 bg-white text-black rounded-xl font-bold text-lg hover:bg-zinc-200 transition-colors">
|
||||
<span class="material-symbols-rounded">home</span>
|
||||
Back to Home
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
26
resources/views/errors/500.blade.php
Normal file
@@ -0,0 +1,26 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Server Error - Streamzy')
|
||||
|
||||
@section('content')
|
||||
<section class="relative min-h-[70vh] flex items-center justify-center overflow-hidden bg-zinc-950">
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[50%] left-[20%] w-[100%] h-[100%] bg-red-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute -bottom-[50%] right-[20%] w-[100%] h-[100%] bg-purple-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 brightness-100 contrast-150"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-zinc-950/50 via-zinc-950/80 to-zinc-950"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 text-center px-4">
|
||||
<h1 class="text-9xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-purple-600 mb-4">500</h1>
|
||||
<h2 class="text-3xl font-bold text-white mb-6">Server Error</h2>
|
||||
<p class="text-xl text-zinc-400 max-w-lg mx-auto mb-10">
|
||||
Something went wrong on our servers. Please try again later.
|
||||
</p>
|
||||
<a href="{{ route('home') }}" class="inline-flex items-center gap-2 px-8 py-4 bg-white text-black rounded-xl font-bold text-lg hover:bg-zinc-200 transition-colors">
|
||||
<span class="material-symbols-rounded">home</span>
|
||||
Back to Home
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
124
resources/views/help/adblocker.blade.php
Normal file
@@ -0,0 +1,124 @@
|
||||
@php($meta = [
|
||||
'title' => 'Enable Ad‑Blocker for Streamzy (Recommended)',
|
||||
'description' => 'Shield up! For the best experience on Streamzy, use an ad‑blocker to remove pop‑ups and trackers from third‑party embed providers. Step‑by‑step guides for Chrome, Brave, Edge, Firefox, iOS, and Android.',
|
||||
'type' => 'article'
|
||||
])
|
||||
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<!-- Hero Section -->
|
||||
<section class="relative py-20 overflow-hidden bg-zinc-950">
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[50%] left-[20%] w-[100%] h-[100%] bg-red-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute -bottom-[50%] right-[20%] w-[100%] h-[100%] bg-purple-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 brightness-100 contrast-150"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-zinc-950/50 via-zinc-950/80 to-zinc-950"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-green-500/10 text-green-500 mb-6 ring-1 ring-green-500/20">
|
||||
<span class="material-symbols-rounded text-4xl">shield</span>
|
||||
</div>
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-6">
|
||||
Enable <span class="text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-emerald-600">Ad-Blocker</span>
|
||||
</h1>
|
||||
<p class="text-xl text-zinc-400 max-w-2xl mx-auto">
|
||||
Shield up! For the best experience on Streamzy, we strongly recommend using an ad blocker to remove pop‑ups and trackers from third‑party embed providers.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Content Section -->
|
||||
<section class="relative py-12 bg-zinc-950 min-h-[50vh]">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Cards Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Chrome/Brave/Edge -->
|
||||
<div class="bg-zinc-900/50 border border-white/5 rounded-2xl p-8 backdrop-blur-sm hover:border-white/10 transition-colors">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="w-10 h-10 rounded-lg bg-red-500/10 flex items-center justify-center text-red-500">
|
||||
<span class="material-symbols-rounded text-2xl">web</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white">Desktop (Chrome / Brave / Edge)</h2>
|
||||
</div>
|
||||
<ol class="list-decimal list-inside text-zinc-400 space-y-4">
|
||||
<li>Open your browser’s extension store:
|
||||
<ul class="list-disc list-inside mt-2 ml-4 text-sm text-zinc-500 space-y-2">
|
||||
<li><a class="text-red-400 hover:text-red-300 transition-colors" href="https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm" target="_blank" rel="noopener">Chrome Web Store – uBlock Origin</a></li>
|
||||
<li><a class="text-red-400 hover:text-red-300 transition-colors" href="https://microsoftedge.microsoft.com/addons/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm" target="_blank" rel="noopener">Edge Add‑ons – uBlock Origin</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Search for <span class="text-white font-medium">uBlock Origin</span></li>
|
||||
<li>Click <span class="text-white font-medium">Add to Browser</span> and enable it</li>
|
||||
</ol>
|
||||
<p class="text-zinc-500 text-sm mt-6 pt-6 border-t border-white/5">Tip: Brave has built‑in blocking, but adding uBlock Origin gives extra filter control.</p>
|
||||
</div>
|
||||
|
||||
<!-- Firefox -->
|
||||
<div class="bg-zinc-900/50 border border-white/5 rounded-2xl p-8 backdrop-blur-sm hover:border-white/10 transition-colors">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="w-10 h-10 rounded-lg bg-orange-500/10 flex items-center justify-center text-orange-500">
|
||||
<span class="material-symbols-rounded text-2xl">local_fire_department</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white">Firefox (Desktop & Android)</h2>
|
||||
</div>
|
||||
<ol class="list-decimal list-inside text-zinc-400 space-y-4">
|
||||
<li>Open Add‑ons Manager (<span class="text-white font-medium">about:addons</span> in address bar)</li>
|
||||
<li>Search for <span class="text-white font-medium">uBlock Origin</span></li>
|
||||
<li>Install from <a class="text-red-400 hover:text-red-300 transition-colors" href="https://addons.mozilla.org/firefox/addon/ublock-origin/" target="_blank" rel="noopener">Firefox Add‑ons – uBlock Origin</a></li>
|
||||
<li><span class="text-white font-medium">Android:</span> Install <a class="text-red-400 hover:text-red-300 transition-colors" href="https://play.google.com/store/apps/details?id=org.mozilla.firefox" target="_blank" rel="noopener">Firefox from Google Play</a>, then add uBlock Origin.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- iOS Safari -->
|
||||
<div class="bg-zinc-900/50 border border-white/5 rounded-2xl p-8 backdrop-blur-sm hover:border-white/10 transition-colors">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center text-blue-500">
|
||||
<span class="material-symbols-rounded text-2xl">phone_iphone</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white">iOS (Safari)</h2>
|
||||
</div>
|
||||
<ol class="list-decimal list-inside text-zinc-400 space-y-4">
|
||||
<li>Open the <a class="text-red-400 hover:text-red-300 transition-colors" href="https://apps.apple.com/" target="_blank" rel="noopener">App Store</a></li>
|
||||
<li>Search for and install:
|
||||
<ul class="list-disc list-inside mt-2 ml-4 text-sm text-zinc-500 space-y-2">
|
||||
<li><a class="text-red-400 hover:text-red-300 transition-colors" href="https://apps.apple.com/app/adguard-for-safari/id1440147259" target="_blank" rel="noopener">AdGuard for Safari</a></li>
|
||||
<li><a class="text-red-400 hover:text-red-300 transition-colors" href="https://apps.apple.com/app/1blocker/id1365531024" target="_blank" rel="noopener">1Blocker for Safari</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Enable in <span class="text-white font-medium">Settings → Safari → Extensions</span></li>
|
||||
</ol>
|
||||
<p class="text-zinc-500 text-sm mt-6 pt-6 border-t border-white/5">You can enable multiple content blockers in Safari for stronger protection.</p>
|
||||
</div>
|
||||
|
||||
<!-- Android -->
|
||||
<div class="bg-zinc-900/50 border border-white/5 rounded-2xl p-8 backdrop-blur-sm hover:border-white/10 transition-colors">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center text-green-500">
|
||||
<span class="material-symbols-rounded text-2xl">android</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white">Android</h2>
|
||||
</div>
|
||||
<ul class="list-disc list-inside text-zinc-400 space-y-4">
|
||||
<li>Use <a class="text-red-400 hover:text-red-300 transition-colors" href="https://play.google.com/store/apps/details?id=com.brave.browser" target="_blank" rel="noopener">Brave Browser</a> (built‑in ad‑blocking)</li>
|
||||
<li>Or, install <a class="text-red-400 hover:text-red-300 transition-colors" href="https://play.google.com/store/apps/details?id=org.mozilla.firefox" target="_blank" rel="noopener">Firefox</a> and add <a class="text-red-400 hover:text-red-300 transition-colors" href="https://addons.mozilla.org/android/addon/ublock-origin/" target="_blank" rel="noopener">uBlock Origin</a></li>
|
||||
<li class="text-zinc-500 text-sm">Avoid installing random “adblock” apps from Play Store – many are ineffective.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Extra Tips -->
|
||||
<div class="mt-8 bg-zinc-900/50 border border-white/5 rounded-2xl p-8 backdrop-blur-sm">
|
||||
<h3 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<span class="material-symbols-rounded text-yellow-500">tips_and_updates</span>
|
||||
Helpful Tips
|
||||
</h3>
|
||||
<ul class="list-disc list-inside text-zinc-400 space-y-2">
|
||||
<li>After installing, <span class="text-white font-medium">refresh Streamzy</span> or reopen the player.</li>
|
||||
<li>When a player opens in a modal, <span class="text-white font-medium">let the page load fully</span> before clicking.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
0
resources/views/history/index.blade.php
Normal file
311
resources/views/home/index.blade.php
Normal file
@@ -0,0 +1,311 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', $meta['title'] ?? 'Streamzy — Watch Movies & TV Shows Online')
|
||||
@section('description', $meta['description'] ?? 'Streamzy lets you watch trending movies and TV shows instantly in HD. No signup required.')
|
||||
|
||||
@section('content')
|
||||
<!-- Hero Section -->
|
||||
<section class="relative min-h-[60vh] md:min-h-[80vh] flex items-center justify-center overflow-hidden bg-zinc-950">
|
||||
<!-- Background Effects -->
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<!-- Video Background -->
|
||||
<div class="relative w-full h-full">
|
||||
<video src="/assets/videos/hero-loop.mp4" autoplay loop muted playsinline class="absolute top-1/2 left-1/2 w-[250%] max-w-none h-full object-cover -translate-x-1/2 -translate-y-1/2 md:top-0 md:left-0 md:w-full md:h-full md:object-cover md:translate-x-0 md:translate-y-0"></video>
|
||||
<!-- Dark Overlay -->
|
||||
<div class="absolute inset-0 bg-black/40"></div>
|
||||
</div>
|
||||
|
||||
<!-- Noise Texture (Darker) -->
|
||||
<div class="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-10 mix-blend-soft-light"></div>
|
||||
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-zinc-950/30 via-zinc-950/60 to-zinc-950"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center py-20">
|
||||
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10 text-sm text-zinc-300 mb-8 backdrop-blur-sm animate-fade-in-up">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
||||
</span>
|
||||
No subscription required
|
||||
</div>
|
||||
|
||||
<h1 class="text-5xl sm:text-6xl lg:text-8xl font-bold tracking-tight text-white mb-6 animate-fade-in-up delay-100">
|
||||
Unlimited <span class="text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-purple-600">Entertainment</span>
|
||||
</h1>
|
||||
|
||||
<p class="text-lg sm:text-xl text-zinc-400 max-w-2xl mx-auto mb-10 leading-relaxed animate-fade-in-up delay-200">
|
||||
Stream thousands of movies and TV shows in HD quality. <br class="hidden sm:block">
|
||||
Fast, free, and designed for you.
|
||||
</p>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="max-w-2xl mx-auto mb-12 animate-fade-in-up delay-300">
|
||||
<form action="{{ route('search') }}" method="get" class="relative group">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-red-600 to-purple-600 rounded-2xl opacity-20 group-hover:opacity-40 blur transition duration-500"></div>
|
||||
<div class="relative flex items-center bg-zinc-900/90 backdrop-blur-xl border border-white/10 rounded-xl p-2 shadow-2xl">
|
||||
<span class="material-symbols-rounded text-zinc-400 ml-4 text-xl">search</span>
|
||||
<input type="text" name="q" placeholder="Search for movies, TV shows..."
|
||||
class="w-full bg-transparent border-none text-white placeholder-zinc-500 focus:ring-0 px-4 py-3 text-lg"
|
||||
autocomplete="off">
|
||||
<button type="submit" class="bg-white text-zinc-950 hover:bg-zinc-200 font-semibold px-6 py-3 rounded-lg transition-colors">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="flex flex-wrap items-center justify-center gap-4 animate-fade-in-up delay-400">
|
||||
<a href="{{ route('movies.index') }}" class="group flex items-center gap-3 px-6 py-3 rounded-xl bg-zinc-900/50 border border-white/10 hover:bg-white/10 hover:border-white/20 transition-all">
|
||||
<div class="w-10 h-10 rounded-lg bg-red-500/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<span class="material-symbols-rounded text-red-500">movie</span>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-medium text-white">Movies</div>
|
||||
<div class="text-xs text-zinc-500">Latest releases</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="{{ route('tv-shows.index') }}" class="group flex items-center gap-3 px-6 py-3 rounded-xl bg-zinc-900/50 border border-white/10 hover:bg-white/10 hover:border-white/20 transition-all">
|
||||
<div class="w-10 h-10 rounded-lg bg-purple-500/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<span class="material-symbols-rounded text-purple-500">tv</span>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-medium text-white">TV Shows</div>
|
||||
<div class="text-xs text-zinc-500">Binge-worthy series</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Content Sections -->
|
||||
<div class="bg-zinc-950 pb-20 space-y-20">
|
||||
|
||||
<!-- Latest Movies -->
|
||||
@if(!empty($latestMovies))
|
||||
<section class="relative z-10">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-end justify-between mb-8">
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-white mb-2">Latest Movies</h2>
|
||||
<p class="text-zinc-400 text-sm">Fresh from the cinema</p>
|
||||
</div>
|
||||
<a href="{{ route('movies.index') }}" class="group flex items-center gap-1 text-sm font-medium text-red-400 hover:text-red-300 transition-colors">
|
||||
View all
|
||||
<span class="material-symbols-rounded text-lg group-hover:translate-x-1 transition-transform">arrow_forward</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="relative group/slider">
|
||||
<div id="latest-movies-scroller" class="flex gap-6 overflow-x-auto scrollbar-hide pb-8 snap-x snap-mandatory">
|
||||
@foreach($latestMovies as $movie)
|
||||
<div class="flex-none w-[160px] sm:w-[200px] snap-start">
|
||||
<a href="{{ route('movie.show', ['slug' => $movie['slug']]) }}" class="group block">
|
||||
<div class="relative aspect-[2/3] rounded-xl overflow-hidden bg-zinc-900 mb-4 shadow-lg ring-1 ring-white/5 group-hover:ring-red-500/50 transition-all duration-300">
|
||||
@if($movie['poster'])
|
||||
<img src="{{ $movie['poster'] }}"
|
||||
alt="{{ $movie['title'] }}"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-zinc-700">
|
||||
<span class="material-symbols-rounded text-4xl">movie</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-end p-4">
|
||||
<div class="transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
|
||||
<button class="w-full py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-red-900/20">
|
||||
<span class="material-symbols-rounded text-lg">play_arrow</span>
|
||||
Watch Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($movie['rating'])
|
||||
<div class="absolute top-3 left-3 px-2 py-1 rounded-md bg-black/60 backdrop-blur-md border border-white/10 text-xs font-bold text-white flex items-center gap-1">
|
||||
<span class="material-symbols-rounded text-yellow-400 text-xs">star</span>
|
||||
{{ number_format($movie['rating'], 1) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h3 class="text-white font-medium truncate group-hover:text-red-400 transition-colors">{{ $movie['title'] }}</h3>
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-500 mt-1">
|
||||
<span>{{ $movie['year'] ?? 'N/A' }}</span>
|
||||
<span class="w-1 h-1 rounded-full bg-zinc-700"></span>
|
||||
<span>Movie</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<button class="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-4 w-12 h-12 rounded-full bg-zinc-900/90 border border-white/10 text-white flex items-center justify-center opacity-0 group-hover/slider:opacity-100 hover:bg-white hover:text-black transition-all shadow-xl z-20" onclick="document.getElementById('latest-movies-scroller').scrollBy({left: -300, behavior: 'smooth'})">
|
||||
<span class="material-symbols-rounded">chevron_left</span>
|
||||
</button>
|
||||
<button class="absolute right-0 top-1/2 -translate-y-1/2 translate-x-4 w-12 h-12 rounded-full bg-zinc-900/90 border border-white/10 text-white flex items-center justify-center opacity-0 group-hover/slider:opacity-100 hover:bg-white hover:text-black transition-all shadow-xl z-20" onclick="document.getElementById('latest-movies-scroller').scrollBy({left: 300, behavior: 'smooth'})">
|
||||
<span class="material-symbols-rounded">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<!-- Latest TV Shows -->
|
||||
@if(!empty($latestTvShows))
|
||||
<section class="relative z-10">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-end justify-between mb-8">
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-white mb-2">Latest TV Shows</h2>
|
||||
<p class="text-zinc-400 text-sm">Trending series</p>
|
||||
</div>
|
||||
<a href="{{ route('tv-shows.index') }}" class="group flex items-center gap-1 text-sm font-medium text-purple-400 hover:text-purple-300 transition-colors">
|
||||
View all
|
||||
<span class="material-symbols-rounded text-lg group-hover:translate-x-1 transition-transform">arrow_forward</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="relative group/slider">
|
||||
<div id="latest-tv-scroller" class="flex gap-6 overflow-x-auto scrollbar-hide pb-8 snap-x snap-mandatory">
|
||||
@foreach($latestTvShows as $show)
|
||||
<div class="flex-none w-[160px] sm:w-[200px] snap-start">
|
||||
<a href="{{ route('tv-show.show', ['slug' => $show['slug']]) }}" class="group block">
|
||||
<div class="relative aspect-[2/3] rounded-xl overflow-hidden bg-zinc-900 mb-4 shadow-lg ring-1 ring-white/5 group-hover:ring-purple-500/50 transition-all duration-300">
|
||||
@if($show['poster'])
|
||||
<img src="{{ $show['poster'] }}"
|
||||
alt="{{ $show['title'] }}"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-zinc-700">
|
||||
<span class="material-symbols-rounded text-4xl">tv</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-end p-4">
|
||||
<div class="transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
|
||||
<button class="w-full py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-purple-900/20">
|
||||
<span class="material-symbols-rounded text-lg">play_arrow</span>
|
||||
Watch Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($show['rating'])
|
||||
<div class="absolute top-3 left-3 px-2 py-1 rounded-md bg-black/60 backdrop-blur-md border border-white/10 text-xs font-bold text-white flex items-center gap-1">
|
||||
<span class="material-symbols-rounded text-yellow-400 text-xs">star</span>
|
||||
{{ number_format($show['rating'], 1) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h3 class="text-white font-medium truncate group-hover:text-purple-400 transition-colors">{{ $show['title'] }}</h3>
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-500 mt-1">
|
||||
<span>{{ $show['year'] ?? 'N/A' }}</span>
|
||||
<span class="w-1 h-1 rounded-full bg-zinc-700"></span>
|
||||
<span>TV Show</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<button class="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-4 w-12 h-12 rounded-full bg-zinc-900/90 border border-white/10 text-white flex items-center justify-center opacity-0 group-hover/slider:opacity-100 hover:bg-white hover:text-black transition-all shadow-xl z-20" onclick="document.getElementById('latest-tv-scroller').scrollBy({left: -300, behavior: 'smooth'})">
|
||||
<span class="material-symbols-rounded">chevron_left</span>
|
||||
</button>
|
||||
<button class="absolute right-0 top-1/2 -translate-y-1/2 translate-x-4 w-12 h-12 rounded-full bg-zinc-900/90 border border-white/10 text-white flex items-center justify-center opacity-0 group-hover/slider:opacity-100 hover:bg-white hover:text-black transition-all shadow-xl z-20" onclick="document.getElementById('latest-tv-scroller').scrollBy({left: 300, behavior: 'smooth'})">
|
||||
<span class="material-symbols-rounded">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<!-- Latest Episodes -->
|
||||
@if(!empty($latestEpisodes))
|
||||
<section class="relative z-10">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-end justify-between mb-8">
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-white mb-2">New Episodes</h2>
|
||||
<p class="text-zinc-400 text-sm">Just aired</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative group/slider">
|
||||
<div id="latest-episodes-scroller" class="flex gap-6 overflow-x-auto scrollbar-hide pb-8 snap-x snap-mandatory">
|
||||
@foreach($latestEpisodes as $episode)
|
||||
<div class="flex-none w-[280px] snap-start">
|
||||
@php
|
||||
$epSlug = $episode['show_slug'] ?? ($episode['slug'] ?? null);
|
||||
$epSeason = $episode['season_number'] ?? ($episode['season'] ?? null);
|
||||
$epNumber = $episode['episode_number'] ?? ($episode['episode'] ?? null);
|
||||
$epName = $episode['name'] ?? $episode['title'] ?? 'Episode';
|
||||
@endphp
|
||||
<a href="{{ ($epSlug && $epSeason && $epNumber) ? route('tv-show.episode', ['slug' => $epSlug, 'season' => $epSeason, 'episode' => $epNumber]) : '#' }}" class="group block">
|
||||
<div class="relative aspect-video rounded-xl overflow-hidden bg-zinc-900 mb-4 shadow-lg ring-1 ring-white/5 group-hover:ring-blue-500/50 transition-all duration-300">
|
||||
@if(!empty($episode['still']))
|
||||
<img src="{{ $episode['still'] }}"
|
||||
alt="{{ $epName }}"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-zinc-700">
|
||||
<span class="material-symbols-rounded text-4xl">tv</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="absolute inset-0 bg-black/50 group-hover:bg-black/30 transition-colors flex items-center justify-center">
|
||||
<div class="w-12 h-12 rounded-full bg-white/10 backdrop-blur-sm flex items-center justify-center group-hover:scale-110 group-hover:bg-blue-600 transition-all duration-300">
|
||||
<span class="material-symbols-rounded text-white text-2xl">play_arrow</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-3 left-3 px-2 py-1 rounded-md bg-black/60 backdrop-blur-md border border-white/10 text-xs font-bold text-white">
|
||||
S{{ $epSeason }} E{{ $epNumber }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-white font-medium truncate group-hover:text-blue-400 transition-colors">{{ $epName }}</h3>
|
||||
<p class="text-sm text-zinc-500 truncate">{{ $episode['show_title'] ?? '' }}</p>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<button class="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-4 w-12 h-12 rounded-full bg-zinc-900/90 border border-white/10 text-white flex items-center justify-center opacity-0 group-hover/slider:opacity-100 hover:bg-white hover:text-black transition-all shadow-xl z-20" onclick="document.getElementById('latest-episodes-scroller').scrollBy({left: -300, behavior: 'smooth'})">
|
||||
<span class="material-symbols-rounded">chevron_left</span>
|
||||
</button>
|
||||
<button class="absolute right-0 top-1/2 -translate-y-1/2 translate-x-4 w-12 h-12 rounded-full bg-zinc-900/90 border border-white/10 text-white flex items-center justify-center opacity-0 group-hover/slider:opacity-100 hover:bg-white hover:text-black transition-all shadow-xl z-20" onclick="document.getElementById('latest-episodes-scroller').scrollBy({left: 300, behavior: 'smooth'})">
|
||||
<span class="material-symbols-rounded">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<!-- Genres Grid -->
|
||||
@if(!empty($genres))
|
||||
<section class="relative z-10">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 class="text-3xl font-bold text-white mb-8">Browse by Genre</h2>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
@foreach($genres as $genre)
|
||||
<a href="{{ route('search', ['genre' => $genre['movie_id'] ?? ($genre['tv_id'] ?? null)]) }}"
|
||||
class="px-6 py-3 rounded-full bg-zinc-900 border border-white/10 text-zinc-400 font-medium hover:bg-white hover:text-black hover:border-white transition-all duration-300">
|
||||
{{ $genre['name'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
282
resources/views/layouts/app.blade.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<script defer src="https://analytics.xodo.ro/script.js" data-website-id="b2b92aa1-5730-40d4-8328-659a3441e1d9"></script>
|
||||
@php
|
||||
$meta = $meta ?? [];
|
||||
$pageTitle = $meta['title'] ?? trim($__env->yieldContent('title')) ?: 'Streamzy';
|
||||
$description = $meta['description'] ?? null;
|
||||
$keywords = $meta['keywords'] ?? null;
|
||||
$canonical = $meta['canonical'] ?? url()->current();
|
||||
$robots = $meta['robots'] ?? 'index,follow';
|
||||
$image = $meta['image'] ?? null;
|
||||
$type = $meta['type'] ?? 'website';
|
||||
@endphp
|
||||
<title>{{ $pageTitle }}</title>
|
||||
@if($description)
|
||||
<meta name="description" content="{{ $description }}">
|
||||
@endif
|
||||
@if($keywords)
|
||||
<meta name="keywords" content="{{ is_array($keywords) ? implode(', ', $keywords) : $keywords }}">
|
||||
@endif
|
||||
<meta name="robots" content="{{ $robots }}">
|
||||
<link rel="canonical" href="{{ $canonical }}">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:title" content="{{ $pageTitle }}">
|
||||
@if($description)
|
||||
<meta property="og:description" content="{{ $description }}">
|
||||
@endif
|
||||
<meta property="og:type" content="{{ $type }}">
|
||||
<meta property="og:url" content="{{ $canonical }}">
|
||||
@if($image)
|
||||
<meta property="og:image" content="{{ $image }}">
|
||||
@endif
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ $pageTitle }}">
|
||||
@if($description)
|
||||
<meta name="twitter:description" content="{{ $description }}">
|
||||
@endif
|
||||
@if($image)
|
||||
<meta name="twitter:image" content="{{ $image }}">
|
||||
@endif
|
||||
|
||||
@stack('head')
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,400,0..1,0..200" rel="stylesheet">
|
||||
<link rel="icon" href="{{ asset('favicon.svg') }}" type="image/svg+xml">
|
||||
<style>
|
||||
:root {
|
||||
--font-sans: 'Rubik', system-ui, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
.material-symbols-rounded {
|
||||
font-variation-settings:
|
||||
'FILL' 1,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 24;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- CSS -->
|
||||
@vite(['resources/css/app.css'])
|
||||
|
||||
<!-- Critical inline styles for posters and tmdb images -->
|
||||
<style>
|
||||
.movie-poster,
|
||||
img.movie-poster {
|
||||
aspect-ratio: 2/3 !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
object-fit: cover !important;
|
||||
transition: transform 0.3s ease !important;
|
||||
border-radius: 0.5rem !important;
|
||||
background-color: rgb(39,39,42) !important;
|
||||
display: block !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.movie-card .movie-poster,
|
||||
.movie-card img.movie-poster { border-radius: 0.5rem !important; }
|
||||
.group:hover .movie-poster { transform: scale(1.05) !important; }
|
||||
.movie-card { background-color: rgb(24,24,27) !important; border-radius: 0.75rem !important; overflow: hidden !important; transition: all .3s ease !important; border: 1px solid rgb(63,63,70) !important; }
|
||||
.movie-card:hover { transform: translateY(-4px) !important; box-shadow: 0 20px 25px -5px rgba(0,0,0,.3), 0 10px 10px -5px rgba(0,0,0,.2) !important; border-color: rgb(220,38,38) !important; }
|
||||
img[src*="tmdb.org"] { background-color: rgb(39,39,42) !important; }
|
||||
.relative img { position: relative !important; z-index: 1 !important; }
|
||||
/* Scrollbar styling (WebKit) */
|
||||
*::-webkit-scrollbar { height: 10px; width: 10px; }
|
||||
*::-webkit-scrollbar-track { background: rgba(39,39,42,0.6); border-radius: 9999px; }
|
||||
*::-webkit-scrollbar-thumb { background: rgba(82,82,91,0.9); border-radius: 9999px; border: 2px solid rgba(24,24,27,0.9); }
|
||||
*::-webkit-scrollbar-thumb:hover { background: rgba(113,113,122,0.95); }
|
||||
/* Firefox */
|
||||
* { scrollbar-width: thin; scrollbar-color: rgba(82,82,91,0.9) rgba(39,39,42,0.6); }
|
||||
/* Horizontal scrollers */
|
||||
.row-scroller { scrollbar-gutter: stable both-edges; }
|
||||
.row-scroller::-webkit-scrollbar { height: 10px; }
|
||||
.row-scroller::-webkit-scrollbar-thumb { background: rgba(113,113,122,0.9); }
|
||||
.row-scroller:hover::-webkit-scrollbar-thumb { background: rgba(161,161,170,1); }
|
||||
</style>
|
||||
</head>
|
||||
<!-- Matomo (proxied first-party to avoid ad blockers) -->
|
||||
<script>
|
||||
var _paq = window._paq = window._paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u = '/';
|
||||
_paq.push(['setTrackerUrl', u + 'stats']);
|
||||
_paq.push(['setSiteId', '6']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true; g.src = u + 'm'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
// Silent if blocked
|
||||
window.addEventListener('error', function(e){ if((e && e.filename || '').includes('stats.js')){/* noop */} }, true);
|
||||
</script>
|
||||
<!-- End Matomo Code -->
|
||||
|
||||
<body class="bg-zinc-950 text-zinc-100 antialiased page-base">
|
||||
<noscript>
|
||||
<p><img referrerpolicy="no-referrer-when-downgrade" src="/stats?idsite=6&rec=1" style="border:0;" alt="" /></p>
|
||||
</noscript>
|
||||
<!-- Header -->
|
||||
<header class="sticky top-0 left-0 right-0 z-50 bg-zinc-950/80 backdrop-blur-xl border-b border-white/5">
|
||||
<nav class="wrap py-4">
|
||||
<div class="relative flex items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<a href="{{ route('home') }}" class="flex items-center gap-2 group relative z-10">
|
||||
<x-application-logo class="h-8 w-auto text-white group-hover:text-red-500 transition-colors" />
|
||||
</a>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="hidden md:flex absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 items-center gap-1 bg-white/5 p-1 rounded-full border border-white/5 z-10">
|
||||
<a href="{{ route('home') }}" class="px-4 py-1.5 rounded-full text-sm font-medium transition-all {{ request()->routeIs('home') ? 'bg-zinc-800 text-white shadow-sm' : 'text-zinc-400 hover:text-white hover:bg-white/5' }}">
|
||||
Home
|
||||
</a>
|
||||
<a href="{{ route('movies.index') }}" class="px-4 py-1.5 rounded-full text-sm font-medium transition-all {{ request()->routeIs('movies.*') ? 'bg-red-600/20 text-red-200 shadow-sm' : 'text-zinc-400 hover:text-red-400 hover:bg-white/5' }}">
|
||||
Movies
|
||||
</a>
|
||||
<a href="{{ route('tv-shows.index') }}" class="px-4 py-1.5 rounded-full text-sm font-medium transition-all {{ request()->routeIs('tv-shows.*') || request()->routeIs('tv-show.*') ? 'bg-purple-600/20 text-purple-200 shadow-sm' : 'text-zinc-400 hover:text-purple-400 hover:bg-white/5' }}">
|
||||
TV Shows
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Right Actions -->
|
||||
<div class="flex items-center gap-3 relative z-10">
|
||||
<a href="{{ route('search') }}" class="p-2 aspect-square flex items-center justify-center text-zinc-400 hover:text-white transition-colors rounded-full hover:bg-white/10" aria-label="Search">
|
||||
<span class="material-symbols-rounded text-xl">search</span>
|
||||
</a>
|
||||
<div class="hidden sm:flex items-center gap-2 border-l border-white/10 pl-3">
|
||||
<a href="{{ route('random.movie') }}" class="p-2 aspect-square flex items-center justify-center text-zinc-400 hover:text-red-400 transition-colors rounded-full hover:bg-white/10" title="Random Movie">
|
||||
<span class="material-symbols-rounded text-xl">shuffle</span>
|
||||
</a>
|
||||
</div>
|
||||
<button id="mobile-menu-btn" class="md:hidden p-2 aspect-square flex items-center justify-center text-zinc-400 hover:text-white transition-colors rounded-lg hover:bg-white/10">
|
||||
<span class="material-symbols-rounded text-xl">menu</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
<div id="mobile-menu" class="hidden md:hidden mt-4 pt-4 border-t border-white/10">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<a href="{{ route('home') }}" class="flex items-center gap-3 text-zinc-400 hover:text-white transition-colors p-3 rounded-xl hover:bg-white/5 {{ request()->routeIs('home') ? 'bg-white/5 text-white' : '' }}">
|
||||
<span class="material-symbols-rounded">home</span>
|
||||
<span class="font-medium">Home</span>
|
||||
</a>
|
||||
<a href="{{ route('movies.index') }}" class="flex items-center gap-3 text-zinc-400 hover:text-red-400 transition-colors p-3 rounded-xl hover:bg-white/5 {{ request()->routeIs('movies.*') ? 'bg-red-500/10 text-red-400' : '' }}">
|
||||
<span class="material-symbols-rounded">movie</span>
|
||||
<span class="font-medium">Movies</span>
|
||||
</a>
|
||||
<a href="{{ route('tv-shows.index') }}" class="flex items-center gap-3 text-zinc-400 hover:text-purple-400 transition-colors p-3 rounded-xl hover:bg-white/5 {{ request()->routeIs('tv-shows.*') || request()->routeIs('tv-show.*') ? 'bg-purple-500/10 text-purple-400' : '' }}">
|
||||
<span class="material-symbols-rounded">tv</span>
|
||||
<span class="font-medium">TV Shows</span>
|
||||
</a>
|
||||
<div class="grid grid-cols-2 gap-2 pt-2">
|
||||
<a href="{{ route('random.movie') }}" class="flex items-center justify-center gap-2 p-3 rounded-xl bg-zinc-900 border border-zinc-800 text-zinc-400 hover:text-white hover:border-zinc-700 transition-all">
|
||||
<span class="material-symbols-rounded text-sm">shuffle</span>
|
||||
<span class="text-sm font-medium">Random Movie</span>
|
||||
</a>
|
||||
<a href="{{ route('random.tv-show') }}" class="flex items-center justify-center gap-2 p-3 rounded-xl bg-zinc-900 border border-zinc-800 text-zinc-400 hover:text-white hover:border-zinc-700 transition-all">
|
||||
<span class="material-symbols-rounded text-sm">shuffle</span>
|
||||
<span class="text-sm font-medium">Random TV</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
@yield('content')
|
||||
</main>
|
||||
|
||||
<!-- Footer (revamped) -->
|
||||
<footer class="bg-zinc-950 border-t border-white/5 pt-16 pb-8">
|
||||
<div class="wrap">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-12">
|
||||
<!-- Brand -->
|
||||
<div class="space-y-4">
|
||||
<a href="{{ route('home') }}" class="block group">
|
||||
<x-application-logo-full class="h-8 w-auto text-white group-hover:text-red-500 transition-colors" />
|
||||
</a>
|
||||
<p class="text-zinc-400 text-sm leading-relaxed">
|
||||
Your premium destination for streaming movies and TV shows in HD. No subscription, no sign-up, just entertainment.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Browse -->
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-4">Browse</h3>
|
||||
<ul class="space-y-2 text-sm text-zinc-400">
|
||||
<li><a href="{{ route('movies.index') }}" class="hover:text-red-400 transition-colors">Movies</a></li>
|
||||
<li><a href="{{ route('tv-shows.index') }}" class="hover:text-purple-400 transition-colors">TV Shows</a></li>
|
||||
<li><a href="{{ route('search') }}" class="hover:text-white transition-colors">Search</a></li>
|
||||
<li><a href="{{ route('random.movie') }}" class="hover:text-white transition-colors">Random Movie</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Support -->
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-4">Support</h3>
|
||||
<ul class="space-y-2 text-sm text-zinc-400">
|
||||
<li><a href="{{ route('help.adblocker') }}" class="hover:text-white transition-colors">Ad-blocker Guide</a></li>
|
||||
<li><a href="{{ route('about') }}" class="hover:text-white transition-colors">About Us</a></li>
|
||||
<li><a href="{{ route('sitemap') }}" class="hover:text-white transition-colors">Sitemap</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Legal -->
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-sm text-zinc-400">
|
||||
<li><a href="{{ route('legal.dmca') }}" class="hover:text-white transition-colors">DMCA</a></li>
|
||||
<li><a href="{{ route('legal.terms') }}" class="hover:text-white transition-colors">Terms of Service</a></li>
|
||||
<li><a href="{{ route('legal.privacy') }}" class="hover:text-white transition-colors">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-8 border-t border-white/5 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<p class="text-zinc-500 text-sm">© {{ date('Y') }} Streamzy. All rights reserved.</p>
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ route('help.adblocker') }}" class="flex items-center gap-2 px-3 py-1.5 rounded-full bg-zinc-900 border border-zinc-800 text-xs text-zinc-400 hover:text-white hover:border-zinc-700 transition-all">
|
||||
<span class="material-symbols-rounded text-sm">shield</span>
|
||||
<span>Ad-blocker recommended</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- JavaScript -->
|
||||
@vite(['resources/js/app.js'])
|
||||
<script>
|
||||
// Mobile menu toggle
|
||||
document.getElementById('mobile-menu-btn')?.addEventListener('click', function () {
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
menu?.classList.toggle('hidden');
|
||||
});
|
||||
// Close mobile menu when clicking outside
|
||||
document.addEventListener('click', function (e) {
|
||||
const btn = document.getElementById('mobile-menu-btn');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
if (!btn?.contains(e.target) && !menu?.contains(e.target)) {
|
||||
menu?.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
62
resources/views/legal/dmca.blade.php
Normal file
@@ -0,0 +1,62 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<!-- Hero Section -->
|
||||
<section class="relative py-20 overflow-hidden bg-zinc-950">
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[50%] left-[20%] w-[100%] h-[100%] bg-red-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute -bottom-[50%] right-[20%] w-[100%] h-[100%] bg-purple-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 brightness-100 contrast-150"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-zinc-950/50 via-zinc-950/80 to-zinc-950"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-6">
|
||||
DMCA <span class="text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-purple-500">Policy</span>
|
||||
</h1>
|
||||
<p class="text-xl text-zinc-400 max-w-2xl mx-auto">
|
||||
Digital Millennium Copyright Act Notice
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Content Section -->
|
||||
<section class="relative py-12 bg-zinc-950 min-h-[50vh]">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-zinc-900/50 border border-white/5 rounded-2xl p-8 md:p-12 backdrop-blur-sm">
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">Copyright Infringement Notification</h3>
|
||||
<p class="text-zinc-400 mb-8">
|
||||
Streamzy respects the intellectual property rights of others and expects its users to do the same. In accordance with the Digital Millennium Copyright Act of 1998, the text of which may be found on the U.S. Copyright Office website at <a href="http://www.copyright.gov/legislation/dmca.pdf" target="_blank" class="text-red-400 hover:text-red-300">http://www.copyright.gov/legislation/dmca.pdf</a>, we will respond expeditiously to claims of copyright infringement committed using the Streamzy service and/or the Streamzy website if such claims are reported to our Designated Copyright Agent identified in the sample notice below.
|
||||
</p>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">Takedown Notice</h3>
|
||||
<p class="text-zinc-400 mb-4">
|
||||
If you are a copyright owner, authorized to act on behalf of one, or authorized to act under any exclusive right under copyright, please report alleged copyright infringements taking place on or through the Site by completing the following DMCA Notice of Alleged Infringement and delivering it to our Designated Copyright Agent. Upon receipt of Notice as described below, Streamzy will take whatever action, in its sole discretion, it deems appropriate, including removal of the challenged content from the Site.
|
||||
</p>
|
||||
|
||||
<div class="bg-zinc-950/50 p-6 rounded-xl border border-white/5 my-8">
|
||||
<h4 class="text-lg font-medium text-white mb-4">DMCA Notice of Alleged Infringement ("Notice")</h4>
|
||||
<ol class="list-decimal list-inside text-zinc-400 space-y-2">
|
||||
<li>Identify the copyrighted work that you claim has been infringed, or - if multiple copyrighted works are covered by this Notice - you may provide a representative list of the copyrighted works that you claim have been infringed.</li>
|
||||
<li>Identify the material or link you claim is infringing (or the subject of infringing activity) and that access to which is to be disabled, including at a minimum, if applicable, the URL of the link shown on the Site where such material may be found.</li>
|
||||
<li>Provide your mailing address, telephone number, and, if available, email address.</li>
|
||||
<li>Include both of the following statements in the body of the Notice:
|
||||
<ul class="list-disc list-inside ml-4 mt-2 space-y-1">
|
||||
<li>"I hereby state that I have a good faith belief that the disputed use of the copyrighted material is not authorized by the copyright owner, its agent, or the law (e.g., as a fair use)."</li>
|
||||
<li>"I hereby state that the information in this Notice is accurate and, under penalty of perjury, that I am the owner, or authorized to act on behalf of the owner, of the copyright or of an exclusive right under the copyright that is allegedly infringed."</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Provide your full legal name and your electronic or physical signature.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">Contact</h3>
|
||||
<p class="text-zinc-400">
|
||||
Deliver this Notice, with all items completed, to our Designated Copyright Agent at: <a href="mailto:dmca@streamzy.ch" class="text-red-400 hover:text-red-300 transition-colors">dmca@streamzy.ch</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
60
resources/views/legal/privacy.blade.php
Normal file
@@ -0,0 +1,60 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<!-- Hero Section -->
|
||||
<section class="relative py-20 overflow-hidden bg-zinc-950">
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[50%] left-[20%] w-[100%] h-[100%] bg-red-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute -bottom-[50%] right-[20%] w-[100%] h-[100%] bg-purple-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 brightness-100 contrast-150"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-zinc-950/50 via-zinc-950/80 to-zinc-950"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-6">
|
||||
Privacy <span class="text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-purple-500">Policy</span>
|
||||
</h1>
|
||||
<p class="text-xl text-zinc-400 max-w-2xl mx-auto">
|
||||
Your privacy is important to us. It is Streamzy's policy to respect your privacy regarding any information we may collect.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Content Section -->
|
||||
<section class="relative py-12 bg-zinc-950 min-h-[50vh]">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-zinc-900/50 border border-white/5 rounded-2xl p-8 md:p-12 backdrop-blur-sm">
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">1. Information We Collect</h3>
|
||||
<p class="text-zinc-400 mb-8">
|
||||
We only ask for personal information when we truly need it to provide a service to you. We collect it by fair and lawful means, with your knowledge and consent. We also let you know why we’re collecting it and how it will be used.
|
||||
</p>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">2. Data Retention</h3>
|
||||
<p class="text-zinc-400 mb-8">
|
||||
We only retain collected information for as long as necessary to provide you with your requested service. What data we store, we’ll protect within commercially acceptable means to prevent loss and theft, as well as unauthorized access, disclosure, copying, use or modification.
|
||||
</p>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">3. Sharing of Information</h3>
|
||||
<p class="text-zinc-400 mb-8">
|
||||
We don’t share any personally identifying information publicly or with third-parties, except when required to by law.
|
||||
</p>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">4. External Links</h3>
|
||||
<p class="text-zinc-400 mb-8">
|
||||
Our website may link to external sites that are not operated by us. Please be aware that we have no control over the content and practices of these sites, and cannot accept responsibility or liability for their respective privacy policies.
|
||||
</p>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">5. Cookies</h3>
|
||||
<p class="text-zinc-400 mb-8">
|
||||
We use cookies to help us identify and track visitors, their usage of our website, and their website access preferences. Visitors who do not wish to have cookies placed on their computers should set their browsers to refuse cookies before using Streamzy's websites.
|
||||
</p>
|
||||
|
||||
<p class="text-zinc-500 text-sm mt-12 pt-8 border-t border-white/5">
|
||||
Last updated: {{ date('F j, Y') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
60
resources/views/legal/terms.blade.php
Normal file
@@ -0,0 +1,60 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<!-- Hero Section -->
|
||||
<section class="relative py-20 overflow-hidden bg-zinc-950">
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[50%] left-[20%] w-[100%] h-[100%] bg-red-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute -bottom-[50%] right-[20%] w-[100%] h-[100%] bg-purple-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 brightness-100 contrast-150"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-zinc-950/50 via-zinc-950/80 to-zinc-950"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-6">
|
||||
Terms of <span class="text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-purple-500">Service</span>
|
||||
</h1>
|
||||
<p class="text-xl text-zinc-400 max-w-2xl mx-auto">
|
||||
Please read these terms carefully before using our service.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Content Section -->
|
||||
<section class="relative py-12 bg-zinc-950 min-h-[50vh]">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-zinc-900/50 border border-white/5 rounded-2xl p-8 md:p-12 backdrop-blur-sm">
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">1. Acceptance of Terms</h3>
|
||||
<p class="text-zinc-400 mb-8">
|
||||
By accessing and using Streamzy, you accept and agree to be bound by the terms and provision of this agreement. In addition, when using these particular services, you shall be subject to any posted guidelines or rules applicable to such services.
|
||||
</p>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">2. Service Description</h3>
|
||||
<p class="text-zinc-400 mb-8">
|
||||
Streamzy provides a platform for streaming entertainment content. We do not host any files on our servers. All content is provided by non-affiliated third parties.
|
||||
</p>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">3. User Conduct</h3>
|
||||
<p class="text-zinc-400 mb-8">
|
||||
You agree to use the service only for lawful purposes. You are prohibited from posting or transmitting any unlawful, threatening, libelous, defamatory, obscene, scandalous, inflammatory, pornographic, or profane material.
|
||||
</p>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">4. Disclaimer</h3>
|
||||
<p class="text-zinc-400 mb-8">
|
||||
The materials on Streamzy's website are provided on an 'as is' basis. Streamzy makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties including, without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.
|
||||
</p>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-white mb-4">5. Limitations</h3>
|
||||
<p class="text-zinc-400 mb-8">
|
||||
In no event shall Streamzy or its suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on Streamzy's website.
|
||||
</p>
|
||||
|
||||
<p class="text-zinc-500 text-sm mt-12 pt-8 border-t border-white/5">
|
||||
Last updated: {{ date('F j, Y') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
20
resources/views/movies/embed.blade.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Player</title>
|
||||
<style>html,body,iframe{margin:0;padding:0;height:100%;width:100%;background:#000}</style>
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</head>
|
||||
<body>
|
||||
<iframe
|
||||
src="{{ $embedUrl }}"
|
||||
allow="autoplay; encrypted-media; fullscreen *; picture-in-picture; clipboard-write; accelerometer"
|
||||
allowfullscreen
|
||||
referrerpolicy="no-referrer"
|
||||
style="border:0;width:100%;height:100%"
|
||||
></iframe></body>
|
||||
</html>
|
||||
284
resources/views/movies/index.blade.php
Normal file
@@ -0,0 +1,284 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('head')
|
||||
<style>
|
||||
details .details-content{max-height:0;overflow:hidden;transition:max-height .25s ease}
|
||||
details[open] .details-content{max-height:1200px}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<!-- Hero Section -->
|
||||
<section class="relative py-20 overflow-hidden bg-zinc-950">
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[50%] -left-[20%] w-[100%] h-[100%] bg-red-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 brightness-100 contrast-150"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-zinc-950/50 via-zinc-950/80 to-zinc-950"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-4xl md:text-6xl font-bold text-white mb-4 animate-fade-in-up">
|
||||
Explore <span class="text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-orange-500">Movies</span>
|
||||
</h1>
|
||||
<p class="text-zinc-400 text-lg max-w-2xl mx-auto animate-fade-in-up delay-100">
|
||||
Discover the latest blockbusters, timeless classics, and hidden gems.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Movies Grid Section -->
|
||||
<section class="bg-zinc-950 pb-20 min-h-screen">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Filters (collapsible) -->
|
||||
<form action="{{ route('movies.index') }}" method="GET" class="mb-10 animate-fade-in-up delay-200">
|
||||
<details class="group rounded-2xl bg-zinc-900/50 border border-white/5 backdrop-blur-sm overflow-hidden transition-all duration-300">
|
||||
@php
|
||||
$selectedGenresLocal = request('genre', $selectedGenres ?? []);
|
||||
if (!is_array($selectedGenresLocal)) { $selectedGenresLocal = [$selectedGenresLocal]; }
|
||||
$selectedGenresLocal = array_map('strval', $selectedGenresLocal);
|
||||
$activeChips = [];
|
||||
if (request('year_from') || request('year_to')) {
|
||||
$yf = request('year_from');
|
||||
$yt = request('year_to');
|
||||
$activeChips[] = 'Year: ' . ($yf ?: 'Any') . '–' . ($yt ?: 'Any');
|
||||
}
|
||||
if (request('rating')) { $activeChips[] = 'Rating: ' . request('rating') . '.0+'; }
|
||||
$sortMap = ['popularity'=>'Popularity','rating'=>'Rating','year_desc'=>'Year ↓','year_asc'=>'Year ↑','title'=>'Title A–Z'];
|
||||
$currentSort = $sortMap[request('sort','popularity')] ?? 'Popularity';
|
||||
if (request('sort') && request('sort') !== 'popularity') { $activeChips[] = 'Sort: ' . $currentSort; }
|
||||
|
||||
// Build map of selected genre id => name for per-chip removal
|
||||
$selectedGenreMap = [];
|
||||
foreach(($genresList ?? []) as $g){
|
||||
$gid = $g['movie_id'] ?? null;
|
||||
if ($gid && in_array((string)$gid, $selectedGenresLocal, true)) {
|
||||
$selectedGenreMap[(string)$gid] = $g['name'];
|
||||
}
|
||||
}
|
||||
$activeCount = count($activeChips);
|
||||
$activeCount += count($selectedGenreMap);
|
||||
$qsBase = request()->query();
|
||||
unset($qsBase['page']);
|
||||
@endphp
|
||||
<summary class="cursor-pointer p-4 md:p-6 flex items-center justify-between gap-3 hover:bg-white/5 transition-colors select-none">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-red-500/10 flex items-center justify-center text-red-500">
|
||||
<span class="material-symbols-rounded">tune</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-semibold text-white block">Filters & Sort</span>
|
||||
@if($activeCount > 0)
|
||||
<span class="text-xs text-red-400">{{$activeCount}} active filters</span>
|
||||
@else
|
||||
<span class="text-xs text-zinc-500">Refine your search</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-rounded text-zinc-400 transition-transform duration-300 group-open:rotate-180">expand_more</span>
|
||||
</summary>
|
||||
|
||||
<div class="details-content border-t border-white/5 bg-black/20">
|
||||
<div class="p-4 md:p-6 space-y-6">
|
||||
<!-- Active Chips -->
|
||||
@if($activeCount > 0)
|
||||
<div class="flex flex-wrap gap-2 pb-4 border-b border-white/5">
|
||||
<span class="text-xs font-medium text-zinc-500 uppercase tracking-wider py-1 mr-2">Active:</span>
|
||||
<!-- Genre chips with removal -->
|
||||
@foreach($selectedGenreMap as $gid => $gname)
|
||||
@php
|
||||
$qs = $qsBase;
|
||||
$gq = $qs['genre'] ?? [];
|
||||
if (!is_array($gq)) { $gq = [$gq]; }
|
||||
$gq = array_values(array_filter($gq, fn($v) => (string)$v !== (string)$gid));
|
||||
if (!empty($gq)) { $qs['genre'] = $gq; } else { unset($qs['genre']); }
|
||||
$url = route('movies.index') . (empty($qs) ? '' : ('?' . http_build_query($qs)));
|
||||
@endphp
|
||||
<a href="{{ $url }}" class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium bg-red-500/10 text-red-400 border border-red-500/20 hover:bg-red-500/20 transition-colors">
|
||||
{{ $gname }}
|
||||
<span class="material-symbols-rounded text-[14px]">close</span>
|
||||
</a>
|
||||
@endforeach
|
||||
<!-- Other chips -->
|
||||
@foreach($activeChips as $chip)
|
||||
<span class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium bg-zinc-800 text-zinc-300 border border-zinc-700">
|
||||
{{ $chip }}
|
||||
</span>
|
||||
@endforeach
|
||||
<a href="{{ route('movies.index') }}" class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium text-zinc-400 hover:text-white transition-colors ml-auto">
|
||||
Clear all
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<!-- Sort -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Sort By</label>
|
||||
<div class="relative">
|
||||
<select name="sort" class="w-full appearance-none bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block p-2.5 pr-8" onchange="this.form.submit()">
|
||||
@foreach($sortMap as $k => $v)
|
||||
<option value="{{ $k }}" {{ request('sort','popularity') === $k ? 'selected' : '' }}>{{ $v }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-zinc-400">
|
||||
<span class="material-symbols-rounded text-sm">expand_more</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Min Rating</label>
|
||||
<div class="relative">
|
||||
<select name="rating" class="w-full appearance-none bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block p-2.5 pr-8">
|
||||
<option value="">Any Rating</option>
|
||||
@for($i=9; $i>=1; $i--)
|
||||
<option value="{{ $i }}" {{ request('rating') == $i ? 'selected' : '' }}>{{ $i }}+ Stars</option>
|
||||
@endfor
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-zinc-400">
|
||||
<span class="material-symbols-rounded text-sm">star</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Year Range -->
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Release Year</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" name="year_from" placeholder="From" value="{{ request('year_from') }}" min="1900" max="{{ date('Y') }}"
|
||||
class="w-full bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block p-2.5">
|
||||
<span class="text-zinc-500">–</span>
|
||||
<input type="number" name="year_to" placeholder="To" value="{{ request('year_to') }}" min="1900" max="{{ date('Y') }}"
|
||||
class="w-full bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block p-2.5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Genres -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Genres</label>
|
||||
<div class="flex flex-wrap gap-2 max-h-40 overflow-y-auto pr-2 custom-scrollbar">
|
||||
@foreach(($genresList ?? []) as $genre)
|
||||
@php
|
||||
$gid = (string)($genre['movie_id'] ?? '');
|
||||
$isSelected = in_array($gid, $selectedGenresLocal, true);
|
||||
@endphp
|
||||
<label class="cursor-pointer select-none">
|
||||
<input type="checkbox" name="genre[]" value="{{ $gid }}" class="peer sr-only" {{ $isSelected ? 'checked' : '' }}>
|
||||
<span class="inline-block px-3 py-1.5 rounded-lg text-xs font-medium border transition-all
|
||||
peer-checked:bg-red-600 peer-checked:text-white peer-checked:border-red-600
|
||||
bg-zinc-800/50 text-zinc-400 border-zinc-700 hover:border-zinc-500 hover:text-zinc-200">
|
||||
{{ $genre['name'] }}
|
||||
</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-4 border-t border-white/5">
|
||||
<button type="submit" class="bg-white text-black hover:bg-zinc-200 font-medium px-6 py-2.5 rounded-lg transition-colors flex items-center gap-2">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</form>
|
||||
|
||||
<!-- Grid -->
|
||||
@if(count($movies) > 0)
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-x-4 gap-y-8">
|
||||
@foreach($movies as $movie)
|
||||
<div class="group animate-fade-in-up" style="animation-delay: {{ $loop->index * 50 }}ms">
|
||||
<a href="{{ route('movie.show', ['slug' => $movie['slug']]) }}" class="block">
|
||||
<div class="relative aspect-[2/3] rounded-xl overflow-hidden bg-zinc-900 mb-3 shadow-lg ring-1 ring-white/5 group-hover:ring-red-500/50 transition-all duration-300">
|
||||
@if($movie['poster'])
|
||||
<img src="{{ $movie['poster'] }}"
|
||||
alt="{{ $movie['title'] }}"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-zinc-700">
|
||||
<span class="material-symbols-rounded text-4xl">movie</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-end p-4">
|
||||
<div class="transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
|
||||
<button class="w-full py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-red-900/20">
|
||||
<span class="material-symbols-rounded text-lg">play_arrow</span>
|
||||
Watch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($movie['rating'])
|
||||
<div class="absolute top-2 left-2 px-1.5 py-0.5 rounded bg-black/60 backdrop-blur-md border border-white/10 text-[10px] font-bold text-white flex items-center gap-1">
|
||||
<span class="material-symbols-rounded text-yellow-400 text-[12px]">star</span>
|
||||
{{ number_format($movie['rating'], 1) }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($movie['year'])
|
||||
<div class="absolute top-2 right-2 px-1.5 py-0.5 rounded bg-red-600/80 backdrop-blur-md text-[10px] font-bold text-white">
|
||||
{{ $movie['year'] }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h3 class="text-white font-medium text-sm truncate group-hover:text-red-400 transition-colors">{{ $movie['title'] }}</h3>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if(!empty($pagination) && ($pagination['total_pages'] ?? 1) > 1)
|
||||
@php
|
||||
$currentPage = (int)($pagination['current_page'] ?? 1);
|
||||
$totalPages = (int)($pagination['total_pages'] ?? 1);
|
||||
$window = 2;
|
||||
@endphp
|
||||
<div class="mt-12 flex items-center justify-center gap-2">
|
||||
@if ($currentPage > 1)
|
||||
<a href="{{ request()->fullUrlWithQuery(['page' => $currentPage - 1]) }}" class="w-10 h-10 flex items-center justify-center rounded-lg bg-zinc-900 text-zinc-400 hover:bg-zinc-800 hover:text-white transition-colors">
|
||||
<span class="material-symbols-rounded">chevron_left</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@for ($i = 1; $i <= $totalPages; $i++)
|
||||
@if ($i == 1 || $i == $totalPages || ($i >= $currentPage - $window && $i <= $currentPage + $window))
|
||||
<a href="{{ request()->fullUrlWithQuery(['page' => $i]) }}" class="w-10 h-10 flex items-center justify-center rounded-lg transition-colors {{ $i == $currentPage ? 'bg-white text-black font-bold' : 'bg-zinc-900 text-zinc-400 hover:bg-zinc-800 hover:text-white' }}">
|
||||
{{ $i }}
|
||||
</a>
|
||||
@elseif ($i == $currentPage - $window - 1 || $i == $currentPage + $window + 1)
|
||||
<span class="w-10 h-10 flex items-center justify-center text-zinc-600">...</span>
|
||||
@endif
|
||||
@endfor
|
||||
|
||||
@if ($currentPage < $totalPages)
|
||||
<a href="{{ request()->fullUrlWithQuery(['page' => $currentPage + 1]) }}" class="w-10 h-10 flex items-center justify-center rounded-lg bg-zinc-900 text-zinc-400 hover:bg-zinc-800 hover:text-white transition-colors">
|
||||
<span class="material-symbols-rounded">chevron_right</span>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="text-center py-20">
|
||||
<div class="w-20 h-20 bg-zinc-900 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<span class="material-symbols-rounded text-zinc-600 text-4xl">movie_filter</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-white mb-2">No movies found</h3>
|
||||
<p class="text-zinc-400 max-w-md mx-auto mb-8">
|
||||
We couldn't find any movies matching your filters. Try adjusting your search criteria.
|
||||
</p>
|
||||
<a href="{{ route('movies.index') }}" class="inline-flex items-center gap-2 bg-white text-black hover:bg-zinc-200 font-medium px-6 py-3 rounded-lg transition-colors">
|
||||
Clear Filters
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
431
resources/views/movies/show-original.blade.php
Normal file
@@ -0,0 +1,431 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('head')
|
||||
@php
|
||||
// Build directors array safely and uniquely
|
||||
$directorsRaw = array_filter($movie['crew'] ?? [], function ($c) {
|
||||
return ($c['job'] ?? '') === 'Director' && !empty($c['name']);
|
||||
});
|
||||
$seenNames = [];
|
||||
$directors = [];
|
||||
foreach ($directorsRaw as $c) {
|
||||
$name = $c['name'];
|
||||
if (!isset($seenNames[$name])) {
|
||||
$directors[] = ['@type' => 'Person', 'name' => $name];
|
||||
$seenNames[$name] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$ld = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Movie',
|
||||
'name' => $movie['title'],
|
||||
'datePublished' => $movie['release_date'] ?? ($movie['year'] ?? null),
|
||||
'image' => array_values(array_filter([$movie['poster'] ?? null, $movie['backdrop'] ?? null]))[0] ?? null,
|
||||
'description' => $movie['overview'] ?? null,
|
||||
'aggregateRating' => isset($movie['rating']) ? [
|
||||
'@type' => 'AggregateRating',
|
||||
'ratingValue' => round($movie['rating'], 1),
|
||||
'ratingCount' => $movie['vote_count'] ?? 0
|
||||
] : null,
|
||||
'genre' => $movie['genres'] ?? null,
|
||||
'actor' => array_values(array_map(fn($c) => ['@type' => 'Person', 'name' => $c['name']], $movie['cast'] ?? [])),
|
||||
'director' => !empty($directors) ? $directors : null,
|
||||
'trailer' => $movie['trailer'] ? [
|
||||
'@type' => 'VideoObject',
|
||||
'name' => $movie['title'] . ' Trailer',
|
||||
'embedUrl' => $movie['trailer']['url'] ?? null
|
||||
] : null,
|
||||
];
|
||||
// Clean nulls
|
||||
$ld = array_filter($ld, fn($v) => !is_null($v));
|
||||
@endphp
|
||||
<script type="application/ld+json">{!! json_encode($ld, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) !!}</script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<!-- Movie Hero Section -->
|
||||
<section class="relative min-h-screen flex items-center">
|
||||
@if($movie['backdrop'])
|
||||
<div class="absolute inset-0 z-0 opacity-20 md:opacity-[0.1] grayscale">
|
||||
<img src="{{ $movie['backdrop'] }}" alt="{{ $movie['title'] }}" class="w-full h-full object-cover">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-black/80 via-black/60 to-black/90 md:from-black/60 md:via-black/40 md:to-black/80"></div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="relative z-20 w-full">
|
||||
<div class="wrap px-4 py-8 md:p-12 lg:p-24">
|
||||
<div class="grid lg:grid-cols-5 gap-8 md:gap-12 lg:gap-16 items-center">
|
||||
<!-- Movie Poster -->
|
||||
<div class="lg:col-span-2 flex justify-center lg:justify-start">
|
||||
@if($movie['poster'])
|
||||
<img src="{{ $movie['poster'] }}" alt="{{ $movie['title'] }}"
|
||||
class="w-64 md:w-80 lg:w-96 rounded-xl md:rounded-2xl shadow-2xl transform hover:scale-105 transition-transform duration-500">
|
||||
@else
|
||||
<div class="w-64 md:w-80 lg:w-96 aspect-[2/3] bg-zinc-800/50 backdrop-blur-sm rounded-xl md:rounded-2xl shadow-2xl flex items-center justify-center">
|
||||
<span class="material-symbols-rounded text-zinc-500 text-4xl md:text-6xl lg:text-[80px]">image_not_supported</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Movie Details -->
|
||||
<div class="lg:col-span-3 space-y-4 md:space-y-6 lg:space-y-8 text-center lg:text-left">
|
||||
<div class="space-y-3 md:space-y-4 lg:space-y-6">
|
||||
<h1 class="text-2xl md:text-4xl lg:text-6xl xl:text-7xl font-bold text-white leading-tight">
|
||||
{{ $movie['title'] }}
|
||||
</h1>
|
||||
@if($movie['year'])
|
||||
<p class="text-lg md:text-xl lg:text-2xl xl:text-3xl text-zinc-300 font-light">{{ $movie['year'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Movie Meta -->
|
||||
<div class="flex flex-wrap items-center justify-center lg:justify-start gap-3 md:gap-4 lg:gap-6 text-sm md:text-base lg:text-lg text-zinc-300">
|
||||
@if($movie['rating'])
|
||||
<div class="flex items-center bg-black/30 backdrop-blur-sm px-3 py-2 md:px-4 md:py-2 rounded-full">
|
||||
<span class="material-symbols-rounded text-yellow-400 mr-2 text-lg md:text-xl">star</span>
|
||||
<span class="font-semibold text-white">{{ number_format($movie['rating'], 1) }}</span>
|
||||
<span class="text-zinc-400 ml-1">/10</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($movie['runtime'])
|
||||
<div class="flex items-center">
|
||||
<span class="material-symbols-rounded text-zinc-400 mr-2 text-lg md:text-xl">schedule</span>
|
||||
<span>{{ $movie['runtime'] }} min</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($movie['release_date'])
|
||||
<div class="flex items-center">
|
||||
<span class="material-symbols-rounded text-zinc-400 mr-2 text-lg md:text-xl">calendar_today</span>
|
||||
<span class="hidden sm:inline">{{ date('M j, Y', strtotime($movie['release_date'])) }}</span>
|
||||
<span class="sm:hidden">{{ date('Y', strtotime($movie['release_date'])) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Genres -->
|
||||
@if(!empty($movie['genres']) && is_array($movie['genres']))
|
||||
<div class="flex flex-wrap gap-2 md:gap-3 justify-center lg:justify-start">
|
||||
@foreach($movie['genres'] as $genre)
|
||||
@php
|
||||
$genreLabel = is_array($genre) ? ($genre['name'] ?? ($genre['title'] ?? null)) : $genre;
|
||||
@endphp
|
||||
@if(!empty($genreLabel))
|
||||
<span class="bg-red-600/20 border border-red-500/30 text-red-400 px-3 py-1.5 md:px-4 md:py-2 rounded-full text-xs md:text-sm font-medium backdrop-blur-sm">
|
||||
{{ $genreLabel }}
|
||||
</span>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Tagline -->
|
||||
@if($movie['tagline'])
|
||||
<p class="text-base md:text-lg lg:text-xl text-red-400 italic font-light">{{ $movie['tagline'] }}</p>
|
||||
@endif
|
||||
|
||||
<!-- Overview -->
|
||||
@if($movie['overview'])
|
||||
<p class="text-sm md:text-base lg:text-lg text-zinc-200 leading-relaxed max-w-3xl">{{ $movie['overview'] }}</p>
|
||||
@endif
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row flex-wrap gap-3 md:gap-4 pt-2 md:pt-4 justify-center lg:justify-start">
|
||||
<button onclick="openPlayer()"
|
||||
class="bg-red-600 hover:bg-red-700 text-white px-6 py-3 md:px-8 md:py-4 rounded-lg md:rounded-xl font-semibold text-base md:text-lg transition-all duration-300 flex items-center justify-center transform hover:scale-105 shadow-lg">
|
||||
<span class="material-symbols-rounded mr-2 md:mr-3 text-lg md:text-2xl">play_circle</span>
|
||||
Watch Now
|
||||
</button>
|
||||
@if($movie['trailer'])
|
||||
<button onclick="openTrailer()"
|
||||
class="bg-black/30 backdrop-blur-sm border border-white/20 hover:bg-white/10 text-white px-6 py-3 md:px-8 md:py-4 rounded-lg md:rounded-xl font-semibold text-base md:text-lg transition-all duration-300 flex items-center justify-center transform hover:scale-105">
|
||||
<span class="material-symbols-rounded mr-2 md:mr-3 text-lg md:text-2xl">smart_display</span>
|
||||
Watch Trailer
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Movie Details Section -->
|
||||
<section class="py-24 bg-zinc-950">
|
||||
<div class="wrap p-12 lg:p-24">
|
||||
<div class="grid lg:grid-cols-3 gap-16">
|
||||
<!-- Main Content -->
|
||||
<div class="lg:col-span-2 space-y-16">
|
||||
<!-- Cast Section -->
|
||||
@if(!empty($movie['cast']))
|
||||
<div class="space-y-8">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white">Cast</h2>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-6">
|
||||
@foreach(array_slice($movie['cast'], 0, 10) as $actor)
|
||||
<div class="group">
|
||||
<div class="relative overflow-hidden rounded-2xl mb-4">
|
||||
@if($actor['profile_path'])
|
||||
<img src="{{ $actor['profile_path'] }}" alt="{{ $actor['name'] }}"
|
||||
class="w-full aspect-square object-cover group-hover:scale-110 transition-transform duration-500">
|
||||
@else
|
||||
<div class="w-full aspect-square bg-zinc-800 flex items-center justify-center">
|
||||
<span class="material-symbols-rounded text-zinc-600 text-[48px]">person</span>
|
||||
</div>
|
||||
@endif
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</div>
|
||||
<h3 class="text-white font-semibold text-sm leading-tight mb-1">{{ $actor['name'] }}</h3>
|
||||
@if($actor['character'])
|
||||
<p class="text-zinc-400 text-xs">{{ $actor['character'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Crew Section -->
|
||||
@if(!empty($movie['crew']))
|
||||
<div class="space-y-8">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white">Key Crew</h2>
|
||||
<div class="grid sm:grid-cols-2 gap-6">
|
||||
@foreach(array_slice($movie['crew'], 0, 8) as $crewMember)
|
||||
<div class="group bg-zinc-900/50 backdrop-blur-sm rounded-2xl p-6 hover:bg-zinc-800/50 transition-colors duration-300">
|
||||
<div class="flex items-center space-x-4">
|
||||
@if($crewMember['profile_path'])
|
||||
<img src="{{ $crewMember['profile_path'] }}" alt="{{ $crewMember['name'] }}"
|
||||
class="w-16 h-16 object-cover rounded-full">
|
||||
@else
|
||||
<div class="w-16 h-16 bg-zinc-700 rounded-full flex items-center justify-center">
|
||||
<span class="material-symbols-rounded text-zinc-500 text-[24px]">person</span>
|
||||
</div>
|
||||
@endif
|
||||
<div>
|
||||
<h3 class="text-white font-semibold">{{ $crewMember['name'] }}</h3>
|
||||
<p class="text-zinc-400 text-sm">{{ $crewMember['job'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="lg:col-span-1 space-y-8">
|
||||
<div class="bg-zinc-900/30 backdrop-blur-sm rounded-2xl p-8 space-y-6">
|
||||
<h3 class="text-xl font-bold text-white mb-6">Movie Information</h3>
|
||||
|
||||
@if($movie['status'])
|
||||
<div>
|
||||
<p class="text-zinc-400 text-sm uppercase tracking-wide mb-2">Status</p>
|
||||
<p class="text-white font-medium">{{ $movie['status'] }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($movie['budget'] ?? false)
|
||||
<div>
|
||||
<p class="text-zinc-400 text-sm uppercase tracking-wide mb-2">Budget</p>
|
||||
<p class="text-white font-medium">${{ number_format($movie['budget']) }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($movie['revenue'] ?? false)
|
||||
<div>
|
||||
<p class="text-zinc-400 text-sm uppercase tracking-wide mb-2">Revenue</p>
|
||||
<p class="text-white font-medium">${{ number_format($movie['revenue']) }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($movie['production_companies'] ?? false)
|
||||
<div>
|
||||
<p class="text-zinc-400 text-sm uppercase tracking-wide mb-2">Production Companies</p>
|
||||
<div class="space-y-2">
|
||||
@foreach(array_slice($movie['production_companies'], 0, 3) as $company)
|
||||
<p class="text-white text-sm">{{ $company['name'] ?? $company }}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Trailer Modal -->
|
||||
<div id="trailer-modal" class="fixed inset-0 bg-black bg-opacity-90 z-50 hidden flex items-center justify-center p-4">
|
||||
<div class="relative w-full max-w-6xl">
|
||||
<button onclick="closeTrailer()" class="absolute top-2 right-2 md:-top-12 md:right-0 text-white hover:text-red-500 transition-colors duration-200">
|
||||
<span class="material-symbols-rounded text-[32px]">close</span>
|
||||
</button>
|
||||
<div class="aspect-video bg-black rounded-lg overflow-hidden">
|
||||
<iframe id="trailer-iframe" src="" class="w-full h-full" frameborder="0" allow="autoplay; encrypted-media; fullscreen; picture-in-picture" allowfullscreen referrerpolicy="no-referrer"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Video Player Modal -->
|
||||
<div id="player-modal" class="fixed inset-0 bg-black bg-opacity-90 z-50 hidden flex items-center justify-center p-4">
|
||||
<div class="relative w-full max-w-6xl">
|
||||
<button onclick="closePlayer()" class="absolute top-2 right-2 md:-top-12 md:right-0 text-white hover:text-red-500 transition-colors duration-200">
|
||||
<span class="material-symbols-rounded text-[32px]">close</span>
|
||||
</button>
|
||||
<div class="aspect-video bg-black rounded-lg overflow-hidden relative" id="player-container">
|
||||
<iframe id="player-iframe" src="" class="w-full h-full" frameborder="0" allow="autoplay; encrypted-media; fullscreen; picture-in-picture" allowfullscreen referrerpolicy="no-referrer"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Related Movies Section -->
|
||||
@if(!empty($relatedMovies))
|
||||
<section class="py-24 bg-black">
|
||||
<div class="wrap p-12 lg:p-24">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">You Might Also Like</h2>
|
||||
<p class="text-zinc-400 text-lg">Discover more movies similar to {{ $movie['title'] }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||
@foreach($relatedMovies as $relatedMovie)
|
||||
<div class="group">
|
||||
<a href="{{ route('movie.show', ['slug' => $relatedMovie['slug']]) }}" class="block">
|
||||
<div class="relative overflow-hidden rounded-2xl mb-4">
|
||||
@if($relatedMovie['poster'])
|
||||
<img src="{{ $relatedMovie['poster'] }}"
|
||||
alt="{{ $relatedMovie['title'] }}"
|
||||
loading="lazy"
|
||||
class="w-full aspect-[2/3] object-cover group-hover:scale-110 transition-transform duration-500">
|
||||
@else
|
||||
<div class="w-full aspect-[2/3] bg-zinc-800 flex items-center justify-center">
|
||||
<span class="material-symbols-rounded text-zinc-600 text-[48px]">image_not_supported</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Rating Badge -->
|
||||
@if(!empty($relatedMovie['rating']))
|
||||
<div class="absolute top-3 left-3 bg-black/80 backdrop-blur-sm text-white px-3 py-1 rounded-full text-xs font-semibold flex items-center">
|
||||
<span class="material-symbols-rounded text-yellow-400 text-[14px] mr-1">star</span>
|
||||
{{ number_format($relatedMovie['rating'], 1) }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Hover Overlay -->
|
||||
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/60 transition-all duration-300 flex items-center justify-center">
|
||||
<span class="material-symbols-rounded text-white text-[48px] opacity-0 group-hover:opacity-100 transition-opacity duration-300 transform group-hover:scale-110">play_circle</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h3 class="font-semibold text-white text-sm leading-tight line-clamp-2 group-hover:text-red-400 transition-colors duration-300">
|
||||
{{ $relatedMovie['title'] }}
|
||||
</h3>
|
||||
@if($relatedMovie['year'])
|
||||
<p class="text-zinc-400 text-xs">{{ $relatedMovie['year'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const playerModal = document.getElementById('player-modal');
|
||||
const playerIframe = document.getElementById('player-iframe');
|
||||
const trailerModal = document.getElementById('trailer-modal');
|
||||
const trailerIframe = document.getElementById('trailer-iframe');
|
||||
const embedUrl = @json($movie['embed_url']);
|
||||
const trailerData = @json($movie['trailer'] ?? null);
|
||||
|
||||
function openPlayer() {
|
||||
playerIframe.src = embedUrl;
|
||||
playerModal.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closePlayer() {
|
||||
playerIframe.src = '';
|
||||
playerModal.classList.add('hidden');
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
function openTrailer() {
|
||||
if (trailerData && trailerData.key) {
|
||||
trailerIframe.src = `https://www.youtube.com/embed/${trailerData.key}?autoplay=1`;
|
||||
trailerModal.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
alert('Trailer not available for this movie.');
|
||||
}
|
||||
}
|
||||
|
||||
function closeTrailer() {
|
||||
trailerIframe.src = '';
|
||||
trailerModal.classList.add('hidden');
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
function addToWatchlist() {
|
||||
// This would typically send a request to add to user's watchlist
|
||||
alert('Feature coming soon! Movie will be added to your watchlist.');
|
||||
}
|
||||
|
||||
// Close modals when clicking outside
|
||||
playerModal.addEventListener('click', function(e) {
|
||||
if (e.target === playerModal) {
|
||||
closePlayer();
|
||||
}
|
||||
});
|
||||
|
||||
// No click shield; iframe is interactive immediately
|
||||
|
||||
trailerModal.addEventListener('click', function(e) {
|
||||
if (e.target === trailerModal) {
|
||||
closeTrailer();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modals with escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
if (!playerModal.classList.contains('hidden')) {
|
||||
closePlayer();
|
||||
}
|
||||
if (!trailerModal.classList.contains('hidden')) {
|
||||
closeTrailer();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add line-clamp utility for older browsers
|
||||
const addLineClamp = () => {
|
||||
const twoLine = document.querySelectorAll('.line-clamp-2');
|
||||
twoLine.forEach(el => {
|
||||
el.style.display = '-webkit-box';
|
||||
el.style.webkitLineClamp = '2';
|
||||
el.style.webkitBoxOrient = 'vertical';
|
||||
el.style.overflow = 'hidden';
|
||||
});
|
||||
const oneLine = document.querySelectorAll('.line-clamp-1');
|
||||
oneLine.forEach(el => {
|
||||
el.style.display = '-webkit-box';
|
||||
el.style.webkitLineClamp = '1';
|
||||
el.style.webkitBoxOrient = 'vertical';
|
||||
el.style.overflow = 'hidden';
|
||||
});
|
||||
};
|
||||
|
||||
// Run on page load
|
||||
document.addEventListener('DOMContentLoaded', addLineClamp);
|
||||
</script>
|
||||
@endpush
|
||||
333
resources/views/movies/show.blade.php
Normal file
@@ -0,0 +1,333 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('head')
|
||||
@php
|
||||
$ld = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Movie',
|
||||
'name' => $movie['title'],
|
||||
'datePublished' => $movie['release_date'] ?? ($movie['year'] ?? null),
|
||||
'image' => $movie['poster'] ?? null,
|
||||
'description' => $movie['overview'] ?? null,
|
||||
'aggregateRating' => isset($movie['rating']) ? [
|
||||
'@type' => 'AggregateRating',
|
||||
'ratingValue' => round($movie['rating'], 1),
|
||||
'ratingCount' => $movie['vote_count'] ?? 0
|
||||
] : null,
|
||||
'genre' => $movie['genres'] ?? null,
|
||||
];
|
||||
$ld = array_filter($ld, fn($v) => !is_null($v));
|
||||
@endphp
|
||||
<script type="application/ld+json">{!! json_encode($ld, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) !!}</script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<section class="bg-zinc-950 min-h-screen pb-20">
|
||||
<!-- Player Container -->
|
||||
<div class="w-full bg-black aspect-video max-h-[80vh] relative group">
|
||||
<iframe
|
||||
src="{{ route('movie.embed', ['id' => $movie['id']]) }}"
|
||||
class="w-full h-full border-0"
|
||||
allow="autoplay; encrypted-media; fullscreen *; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
<!-- Back Button Overlay -->
|
||||
<div class="absolute top-4 left-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<a href="{{ route('movies.index') }}" class="flex items-center gap-2 px-4 py-2 bg-black/50 hover:bg-black/80 backdrop-blur-sm text-white rounded-lg transition-colors">
|
||||
<span class="material-symbols-rounded">arrow_back</span>
|
||||
Back to Movies
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Movie Info -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="flex flex-col md:flex-row gap-8 items-start justify-between">
|
||||
<div class="flex-1 space-y-6">
|
||||
<!-- Title & Tagline -->
|
||||
<div>
|
||||
<h1 class="text-2xl md:text-4xl font-bold text-white mb-2">
|
||||
{{ $movie['title'] }}
|
||||
</h1>
|
||||
@if(!empty($movie['tagline']))
|
||||
<p class="text-xl text-red-400 font-medium italic">{{ $movie['tagline'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Metadata -->
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm md:text-base text-zinc-300">
|
||||
@if($movie['year'])
|
||||
<span class="px-2.5 py-1 rounded-md bg-zinc-900 border border-zinc-800 font-medium text-white">
|
||||
{{ $movie['year'] }}
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if($movie['rating'])
|
||||
<div class="flex items-center gap-1.5 text-yellow-400">
|
||||
<span class="material-symbols-rounded fill-current">star</span>
|
||||
<span class="font-bold text-white">{{ number_format($movie['rating'], 1) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($movie['runtime'])
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="material-symbols-rounded text-zinc-500">schedule</span>
|
||||
<span>{{ $movie['runtime'] }} min</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Overview -->
|
||||
@if($movie['overview'])
|
||||
<p class="text-zinc-300 leading-relaxed max-w-3xl text-lg">
|
||||
{{ $movie['overview'] }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<!-- Genres -->
|
||||
@if(!empty($movie['genres']))
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($movie['genres'] as $genre)
|
||||
@php $genreLabel = is_array($genre) ? ($genre['name'] ?? '') : $genre; @endphp
|
||||
@if($genreLabel)
|
||||
<a href="{{ route('movies.index', ['genre[]' => is_array($genre) ? $genre['id'] : '']) }}" class="px-3 py-1 rounded-full text-sm font-medium bg-zinc-900/80 border border-zinc-700 text-zinc-300 hover:text-white hover:border-red-500/50 transition-colors">
|
||||
{{ $genreLabel }}
|
||||
</a>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Actions (Trailer) -->
|
||||
@if($movie['trailer'])
|
||||
<div class="pt-4">
|
||||
<button onclick="openTrailer()" class="px-6 py-3 bg-zinc-900 hover:bg-zinc-800 text-white rounded-xl font-bold border border-zinc-800 transition-all flex items-center gap-2">
|
||||
<span class="material-symbols-rounded">smart_display</span>
|
||||
Watch Trailer
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Poster -->
|
||||
<div class="hidden md:block w-64 flex-shrink-0 rounded-xl overflow-hidden shadow-2xl ring-1 ring-white/10">
|
||||
@if($movie['poster'])
|
||||
<img src="{{ $movie['poster'] }}" alt="{{ $movie['title'] }}" class="w-full h-auto object-cover">
|
||||
@else
|
||||
<div class="w-full aspect-[2/3] bg-zinc-900 flex items-center justify-center">
|
||||
<span class="material-symbols-rounded text-6xl text-zinc-700">movie</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cast Section -->
|
||||
@if(!empty($movie['cast']))
|
||||
<section class="py-16 bg-zinc-950 border-t border-white/5">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 class="text-2xl font-bold text-white mb-8 flex items-center gap-3">
|
||||
<span class="w-1 h-8 bg-red-500 rounded-full"></span>
|
||||
Top Cast
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||
@foreach(array_slice($movie['cast'], 0, 12) as $actor)
|
||||
<div class="group">
|
||||
<div class="relative aspect-[2/3] rounded-xl overflow-hidden bg-zinc-900 mb-3 ring-1 ring-white/5 group-hover:ring-red-500/50 transition-all">
|
||||
@if($actor['profile_path'])
|
||||
<img src="{{ $actor['profile_path'] }}" alt="{{ $actor['name'] }}" loading="lazy" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-zinc-700">
|
||||
<span class="material-symbols-rounded text-4xl">person</span>
|
||||
</div>
|
||||
@endif
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</div>
|
||||
<h3 class="text-white font-medium truncate group-hover:text-red-400 transition-colors">{{ $actor['name'] }}</h3>
|
||||
@if($actor['character'])
|
||||
<p class="text-zinc-500 text-sm truncate">{{ $actor['character'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<!-- Production Info -->
|
||||
<section class="py-16 bg-zinc-900/30 border-t border-white/5">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid md:grid-cols-2 gap-12">
|
||||
@if(!empty($movie['production_companies']))
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||
<span class="material-symbols-rounded text-zinc-500">apartment</span>
|
||||
Production Companies
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
@foreach($movie['production_companies'] as $co)
|
||||
<div class="flex items-center gap-3 px-4 py-3 rounded-xl bg-zinc-900 border border-white/5 hover:border-white/10 transition-colors">
|
||||
@if(!empty($co['logo']))
|
||||
<img src="{{ $co['logo'] }}" alt="{{ $co['name'] }}" class="h-8 w-auto object-contain brightness-0 invert opacity-70">
|
||||
@else
|
||||
<span class="material-symbols-rounded text-zinc-600">business</span>
|
||||
@endif
|
||||
<span class="text-sm font-medium text-zinc-300">{{ $co['name'] }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(!empty($movie['production_countries']))
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||
<span class="material-symbols-rounded text-zinc-500">public</span>
|
||||
Production Countries
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($movie['production_countries'] as $cty)
|
||||
<span class="px-3 py-1.5 rounded-lg bg-zinc-900 border border-white/5 text-sm text-zinc-400">
|
||||
{{ $cty }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Trailer Modal -->
|
||||
@if($movie['trailer'])
|
||||
<div id="trailer-modal" class="fixed inset-0 z-50 hidden" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div class="absolute inset-0 bg-black/95 backdrop-blur-sm transition-opacity opacity-0" id="trailer-backdrop"></div>
|
||||
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div class="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<div class="relative transform overflow-hidden rounded-2xl bg-zinc-900 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-5xl opacity-0 scale-95" id="trailer-panel">
|
||||
<div class="flex items-center justify-between px-4 py-3 bg-zinc-800 border-b border-white/5">
|
||||
<h3 class="text-lg font-medium leading-6 text-white">Official Trailer</h3>
|
||||
<button type="button" onclick="closeTrailer()" class="rounded-lg p-1 text-zinc-400 hover:bg-white/10 hover:text-white transition-colors">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative aspect-video bg-black">
|
||||
<iframe id="trailer-player" src="" data-src="{{ $movie['trailer']['url'] ?? '' }}" class="absolute inset-0 w-full h-full border-0" allow="autoplay; encrypted-media; fullscreen; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Animation helpers
|
||||
function animateModal(modalId, show) {
|
||||
const modal = document.getElementById(modalId);
|
||||
const backdrop = modal.querySelector('[id$="-backdrop"]');
|
||||
const panel = modal.querySelector('[id$="-panel"]');
|
||||
|
||||
if (show) {
|
||||
modal.classList.remove('hidden');
|
||||
// Trigger reflow
|
||||
void modal.offsetWidth;
|
||||
backdrop.classList.remove('opacity-0');
|
||||
panel.classList.remove('opacity-0', 'scale-95');
|
||||
panel.classList.add('opacity-100', 'scale-100');
|
||||
} else {
|
||||
backdrop.classList.add('opacity-0');
|
||||
panel.classList.remove('opacity-100', 'scale-100');
|
||||
panel.classList.add('opacity-0', 'scale-95');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@if($movie['trailer'])
|
||||
function openTrailer() {
|
||||
const iframe = document.getElementById('trailer-player');
|
||||
iframe.src = iframe.dataset.src;
|
||||
animateModal('trailer-modal', true);
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeTrailer() {
|
||||
const iframe = document.getElementById('trailer-player');
|
||||
iframe.src = '';
|
||||
animateModal('trailer-modal', false);
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Close on Escape
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeTrailer();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on backdrop click
|
||||
document.getElementById('trailer-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this || e.target.id === 'trailer-backdrop') closeTrailer();
|
||||
});
|
||||
@endif
|
||||
|
||||
// Keyboard shortcuts for video player
|
||||
document.addEventListener('keydown', function(e) {
|
||||
const iframe = document.querySelector('.w-full.h-full.border-0');
|
||||
if (!iframe) return;
|
||||
|
||||
// Ignore if user is typing in an input field
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
|
||||
// F key for fullscreen
|
||||
if (e.key === 'f' || e.key === 'F') {
|
||||
e.preventDefault();
|
||||
if (iframe.requestFullscreen) {
|
||||
iframe.requestFullscreen();
|
||||
} else if (iframe.webkitRequestFullscreen) {
|
||||
iframe.webkitRequestFullscreen();
|
||||
} else if (iframe.mozRequestFullScreen) {
|
||||
iframe.mozRequestFullScreen();
|
||||
} else if (iframe.msRequestFullscreen) {
|
||||
iframe.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
// ESC to exit fullscreen
|
||||
if (e.key === 'Escape') {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Show keyboard shortcuts hint on first visit
|
||||
if (!sessionStorage.getItem('keyboardHintShown')) {
|
||||
setTimeout(() => {
|
||||
const hint = document.createElement('div');
|
||||
hint.className = 'fixed bottom-4 right-4 bg-zinc-900 border border-zinc-700 text-white px-4 py-3 rounded-lg shadow-xl text-sm z-50 animate-fade-in';
|
||||
hint.innerHTML = `
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="material-symbols-rounded text-red-400">keyboard</span>
|
||||
<div>
|
||||
<p class="font-semibold mb-1">Keyboard Shortcuts</p>
|
||||
<p class="text-zinc-400">Press <kbd class="px-1.5 py-0.5 bg-zinc-800 rounded text-xs">F</kbd> for fullscreen</p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.parentElement.remove()" class="text-zinc-400 hover:text-white">
|
||||
<span class="material-symbols-rounded text-sm">close</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(hint);
|
||||
sessionStorage.setItem('keyboardHintShown', 'true');
|
||||
setTimeout(() => hint.remove(), 5000);
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
14
resources/views/robots.txt.blade.php
Normal file
@@ -0,0 +1,14 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Disallow internal or thin pages
|
||||
Disallow: /api/
|
||||
Disallow: /embed/
|
||||
|
||||
# Disallow sensitive areas
|
||||
Disallow: /admin
|
||||
Disallow: /.env
|
||||
Disallow: /storage
|
||||
|
||||
# Sitemap
|
||||
Sitemap: {{ route('sitemap') }}
|
||||
234
resources/views/search/index-original.blade.php
Normal file
@@ -0,0 +1,234 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="wrap py-12 sm:py-16">
|
||||
<!-- Search Header -->
|
||||
<div class="max-w-5xl mx-auto mb-16 sm:mb-20">
|
||||
<h1 class="text-5xl md:text-6xl font-bold text-white mb-8 sm:mb-12 text-center text-shadow">Search Movies & TV Shows</h1>
|
||||
|
||||
<!-- Search Form -->
|
||||
<form method="GET" action="{{ route('search') }}" class="mb-12">
|
||||
<div class="flex flex-col md:flex-row gap-6 sm:gap-6">
|
||||
<div class="flex-1">
|
||||
<input type="text"
|
||||
name="q"
|
||||
value="{{ $query }}"
|
||||
placeholder="Search for movies, TV shows, actors..."
|
||||
class="w-full bg-zinc-900/80 border border-zinc-700 text-white rounded-2xl px-6 py-4 text-xl focus:ring-2 focus:ring-red-600 focus:border-red-600 transition-all duration-300 backdrop-blur-xl"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<select name="type" class="form-select bg-zinc-900/80 border border-zinc-700 text-white rounded-2xl px-6 py-4 text-lg focus:ring-2 focus:ring-red-600 focus:border-red-600 w-auto backdrop-blur-xl">
|
||||
<option value="all" {{ $type === 'all' ? 'selected' : '' }}>All</option>
|
||||
<option value="movie" {{ $type === 'movie' ? 'selected' : '' }}>Movies</option>
|
||||
<option value="tv" {{ $type === 'tv' ? 'selected' : '' }}>TV Shows</option>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="bg-gradient-to-r from-red-600 to-red-700 hover:from-red-500 hover:to-red-600 text-white font-bold px-8 py-4 rounded-2xl transition-all duration-300 transform hover:scale-105 shadow-2xl">
|
||||
<span class="material-symbols-rounded align-middle text-2xl">search</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Quick Categories (always visible) -->
|
||||
<div class="mb-8">
|
||||
<div class="flex flex-wrap gap-3 justify-center mb-12">
|
||||
<a href="{{ route('movies.index') }}" class="px-6 py-3 rounded-2xl border border-red-600 text-red-400 hover:bg-red-600 hover:text-white transition-all duration-300 text-base font-medium backdrop-blur-sm bg-zinc-900/40">Movies</a>
|
||||
<a href="{{ route('tv-shows.index') }}" class="px-6 py-3 rounded-2xl border border-red-600 text-red-400 hover:bg-red-600 hover:text-white transition-all duration-300 text-base font-medium backdrop-blur-sm bg-zinc-900/40">TV Shows</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 justify-center">
|
||||
<!-- TMDB common genre ids: Action 28, Comedy 35, Drama 18, Thriller 53, Sci-Fi 878, Documentary 99 -->
|
||||
<a href="{{ route('search', ['type' => 'movie', 'genre' => 28]) }}" class="px-4 py-2 rounded-2xl bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors text-base">Action</a>
|
||||
<a href="{{ route('search', ['type' => 'movie', 'genre' => 35]) }}" class="px-4 py-2 rounded-2xl bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors text-base">Comedy</a>
|
||||
<a href="{{ route('search', ['type' => 'movie', 'genre' => 18]) }}" class="px-4 py-2 rounded-2xl bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors text-base">Drama</a>
|
||||
<a href="{{ route('search', ['type' => 'movie', 'genre' => 53]) }}" class="px-4 py-2 rounded-2xl bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors text-base">Thriller</a>
|
||||
<a href="{{ route('search', ['type' => 'tv', 'genre' => 10765]) }}" class="px-4 py-2 rounded-2xl bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors text-base">Sci-Fi</a>
|
||||
<a href="{{ route('search', ['type' => 'tv', 'genre' => 99]) }}" class="px-4 py-2 rounded-2xl bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors text-base">Documentary</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search/Filter Results Info -->
|
||||
@if($query || ($genre ?? false))
|
||||
<div class="text-center text-zinc-400 text-lg">
|
||||
@if($total > 0)
|
||||
Found {{ $total }} result{{ $total !== 1 ? 's' : '' }}
|
||||
@if($query)
|
||||
for "{{ $query }}"
|
||||
@endif
|
||||
@else
|
||||
@if($query)
|
||||
No results found for "{{ $query }}"
|
||||
@else
|
||||
No results found for this genre
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Search/Filter Results -->
|
||||
@if(($query || ($genre ?? false)) && !empty($results))
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6 md:gap-8 mb-16 sm:mb-20">
|
||||
@foreach($results as $result)
|
||||
<div class="group">
|
||||
<a href="{{ $result['type'] === 'movie' ? route('movie.show', ['slug' => $result['slug']]) : route('tv-show.show', ['slug' => $result['slug']]) }}" class="block" aria-label="{{ $result['title'] }} details">
|
||||
<div class="relative overflow-hidden rounded-2xl mb-4 shadow-lg hover:shadow-2xl transition-shadow duration-300">
|
||||
@if($result['poster'])
|
||||
<img src="{{ $result['poster'] }}"
|
||||
alt="{{ $result['title'] }}"
|
||||
loading="lazy"
|
||||
class="w-full aspect-[2/3] object-cover group-hover:scale-110 transition-transform duration-500">
|
||||
@else
|
||||
<div class="w-full aspect-[2/3] bg-zinc-800 flex items-center justify-center">
|
||||
<span class="material-symbols-rounded text-zinc-600 text-[48px]">image_not_supported</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Hover Overlay -->
|
||||
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/60 transition-all duration-300 flex items-center justify-center">
|
||||
<div class="opacity-0 group-hover:opacity-100 transition-all duration-300 transform scale-75 group-hover:scale-100">
|
||||
<div class="w-16 h-16 {{ $result['type'] === 'movie' ? 'bg-red-600' : 'bg-purple-600' }} rounded-full flex items-center justify-center shadow-xl">
|
||||
<span class="material-symbols-rounded text-white text-[24px] ml-0.5">play_arrow</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating Badge -->
|
||||
@if($result['rating'])
|
||||
<div class="absolute top-3 left-3 bg-black/80 backdrop-blur-sm text-white px-2.5 py-1.5 rounded-full text-xs font-bold flex items-center">
|
||||
<span class="material-symbols-rounded text-yellow-400 text-[14px] mr-1">star</span>
|
||||
{{ number_format($result['rating'], 1) }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Year/Type Badge -->
|
||||
<div class="absolute top-3 right-3 {{ $result['type'] === 'movie' ? 'bg-red-600/20 border border-red-500/30 text-red-400' : 'bg-purple-600/20 border border-purple-500/30 text-purple-400' }} backdrop-blur-sm px-2.5 py-1.5 rounded-full text-xs font-bold">
|
||||
{{ $result['type'] === 'movie' ? ($result['year'] ?? 'Movie') : 'TV' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h3 class="font-bold text-white text-base md:text-lg leading-tight truncate group-hover:text-{{ $result['type'] === 'movie' ? 'red' : 'purple' }}-400 transition-colors duration-300">
|
||||
{{ $result['title'] }}
|
||||
</h3>
|
||||
<div class="flex items-center justify-between">
|
||||
@if($result['year'])
|
||||
<span class="text-zinc-400 text-sm">{{ $result['year'] }}</span>
|
||||
@endif
|
||||
@if($result['rating'])
|
||||
<div class="flex items-center">
|
||||
<span class="material-symbols-rounded text-yellow-400 text-[14px] mr-1">star</span>
|
||||
<span class="text-zinc-300 text-sm font-medium">{{ number_format($result['rating'], 1) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@if(!empty($pagination))
|
||||
<div class="flex items-center justify-center gap-4 sm:gap-6">
|
||||
@php
|
||||
$params = array_filter([
|
||||
'q' => $query ?? null,
|
||||
'type' => $type ?? null,
|
||||
'genre' => $genre ?? null,
|
||||
], function($v){ return $v !== null && $v !== ''; });
|
||||
@endphp
|
||||
@if(($pagination['has_prev'] ?? false))
|
||||
<a href="{{ route('search', array_merge($params, ['page' => max(1, ($pagination['current_page'] ?? 2) - 1)])) }}" class="px-4 py-2 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800">
|
||||
<span class="material-symbols-rounded align-middle mr-1">chevron_left</span>
|
||||
Prev
|
||||
</a>
|
||||
@else
|
||||
<span class="px-4 py-2 rounded border border-zinc-800 text-zinc-600 cursor-not-allowed">Prev</span>
|
||||
@endif
|
||||
<span class="text-zinc-400 text-sm">Page {{ $pagination['current_page'] ?? 1 }} @if(($pagination['total_pages'] ?? 1) > 1) of {{ $pagination['total_pages'] }} @endif</span>
|
||||
@if(($pagination['has_next'] ?? false))
|
||||
<a href="{{ route('search', array_merge($params, ['page' => ($pagination['current_page'] ?? 1) + 1])) }}" class="px-4 py-2 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800">
|
||||
Next
|
||||
<span class="material-symbols-rounded align-middle ml-1">chevron_right</span>
|
||||
</a>
|
||||
@else
|
||||
<span class="px-4 py-2 rounded border border-zinc-800 text-zinc-600 cursor-not-allowed">Next</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@elseif(($query || ($genre ?? false)) && empty($results))
|
||||
<div class="text-center py-16">
|
||||
<span class="material-symbols-rounded text-zinc-600 text-[96px] mx-auto mb-4 block">search</span>
|
||||
<h3 class="text-xl font-semibold text-white mb-2">No Results Found</h3>
|
||||
<p class="text-zinc-400 mb-4">Try different keywords or choose another genre</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="{{ route('movies.index') }}" class="btn-primary px-6 py-3 rounded-lg">Browse Movies</a>
|
||||
<a href="{{ route('tv-shows.index') }}" class="btn-secondary px-6 py-3 rounded-lg">Browse TV Shows</a>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<!-- Popular Categories -->
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-2xl font-bold text-white mb-6 text-center">Popular Categories</h2>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12">
|
||||
<a href="{{ route('movies.index') }}" class="card p-6 text-center hover:bg-zinc-800 transition-colors duration-200">
|
||||
<span class="material-symbols-rounded text-red-600 text-[48px] mx-auto mb-3 block">movie</span>
|
||||
<h3 class="font-semibold text-white">Movies</h3>
|
||||
<p class="text-zinc-400 text-sm mt-1">Latest Movies</p>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('tv-shows.index') }}" class="card p-6 text-center hover:bg-zinc-800 transition-colors duration-200">
|
||||
<span class="material-symbols-rounded text-red-600 text-[48px] mx-auto mb-3 block">tv</span>
|
||||
<h3 class="font-semibold text-white">TV Shows</h3>
|
||||
<p class="text-zinc-400 text-sm mt-1">Popular Series</p>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('search', ['type' => 'movie', 'genre' => 28]) }}" class="card p-6 text-center hover:bg-zinc-800 transition-colors duration-200">
|
||||
<span class="material-symbols-rounded text-red-600 text-[48px] mx-auto mb-3 block">local_fire_department</span>
|
||||
<h3 class="font-semibold text-white">Action</h3>
|
||||
<p class="text-zinc-400 text-sm mt-1">Action Movies</p>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('search', ['type' => 'tv', 'genre' => 18]) }}" class="card p-6 text-center hover:bg-zinc-800 transition-colors duration-200">
|
||||
<span class="material-symbols-rounded text-red-600 text-[48px] mx-auto mb-3 block">theater_comedy</span>
|
||||
<h3 class="font-semibold text-white">Drama</h3>
|
||||
<p class="text-zinc-400 text-sm mt-1">Drama Series</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="text-zinc-400 mb-6">Start typing in the search box above to find your favorite movies and TV shows.</p>
|
||||
<p class="text-sm text-zinc-500">Powered by The Movie Database (TMDB) for accurate and up-to-date content information.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Add line-clamp utility for older browsers
|
||||
const addLineClamp = () => {
|
||||
const twoLine = document.querySelectorAll('.line-clamp-2');
|
||||
twoLine.forEach(el => {
|
||||
el.style.display = '-webkit-box';
|
||||
el.style.webkitLineClamp = '2';
|
||||
el.style.webkitBoxOrient = 'vertical';
|
||||
el.style.overflow = 'hidden';
|
||||
});
|
||||
const oneLine = document.querySelectorAll('.line-clamp-1');
|
||||
oneLine.forEach(el => {
|
||||
el.style.display = '-webkit-box';
|
||||
el.style.webkitLineClamp = '1';
|
||||
el.style.webkitBoxOrient = 'vertical';
|
||||
el.style.overflow = 'hidden';
|
||||
});
|
||||
};
|
||||
|
||||
// Run on page load
|
||||
document.addEventListener('DOMContentLoaded', addLineClamp);
|
||||
</script>
|
||||
@endpush
|
||||
394
resources/views/search/index.blade.php
Normal file
@@ -0,0 +1,394 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('head')
|
||||
@if(!empty($meta))
|
||||
<title>{{ $meta['title'] ?? 'Search - Streamzy' }}</title>
|
||||
<meta name="description" content="{{ $meta['description'] ?? '' }}" />
|
||||
<meta name="keywords" content="{{ $meta['keywords'] ?? '' }}" />
|
||||
@endif
|
||||
<style>
|
||||
details .details-content{max-height:0;overflow:hidden;transition:max-height .25s ease}
|
||||
details[open] .details-content{max-height:1200px}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<!-- Hero Section -->
|
||||
<section class="relative py-20 bg-zinc-950">
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[50%] left-[20%] w-[100%] h-[100%] bg-red-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute -bottom-[50%] right-[20%] w-[100%] h-[100%] bg-purple-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 brightness-100 contrast-150"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-zinc-950/50 via-zinc-950/80 to-zinc-950"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-8 animate-fade-in-up">
|
||||
Search <span class="text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-purple-500">Streamzy</span>
|
||||
</h1>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<form action="{{ route('search') }}" method="GET" class="relative max-w-2xl mx-auto animate-fade-in-up delay-100">
|
||||
<div class="relative group">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-red-500 to-purple-500 rounded-2xl opacity-20 group-hover:opacity-40 blur transition duration-500"></div>
|
||||
<div class="relative flex items-center bg-zinc-900/90 backdrop-blur-xl rounded-2xl border border-white/10 shadow-2xl">
|
||||
<div class="pl-6 text-zinc-400">
|
||||
<span class="material-symbols-rounded text-2xl">search</span>
|
||||
</div>
|
||||
<input type="text"
|
||||
name="q"
|
||||
value="{{ request('q', $query ?? '') }}"
|
||||
placeholder="Search movies, TV shows, people..."
|
||||
autocomplete="off"
|
||||
class="w-full bg-transparent border-none text-white placeholder-zinc-500 focus:ring-0 py-4 px-4 text-lg font-medium" />
|
||||
<button type="submit" class="mr-2 px-6 py-2 bg-white text-black rounded-xl font-semibold hover:bg-zinc-200 transition-colors">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Suggestions Box -->
|
||||
<div id="search-suggestions" class="absolute top-full left-0 right-0 bg-zinc-900/95 backdrop-blur-xl border border-white/10 rounded-2xl mt-4 shadow-2xl hidden overflow-y-auto max-h-[60vh] z-50"></div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Results Section -->
|
||||
<section class="bg-zinc-950 pb-20 min-h-screen">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Filters (collapsible) -->
|
||||
<form action="{{ route('search') }}" method="GET" id="filter-form" class="mb-10 animate-fade-in-up delay-200">
|
||||
@if(request('q'))
|
||||
<input type="hidden" name="q" value="{{ request('q') }}">
|
||||
@endif
|
||||
|
||||
<details class="group rounded-2xl bg-zinc-900/50 border border-white/5 backdrop-blur-sm overflow-hidden transition-all duration-300" open>
|
||||
@php
|
||||
$selectedGenresLocal = request('genre', $selectedGenres ?? []);
|
||||
if (!is_array($selectedGenresLocal)) { $selectedGenresLocal = [$selectedGenresLocal]; }
|
||||
$selectedGenresLocal = array_map('strval', $selectedGenresLocal);
|
||||
$activeChips = [];
|
||||
if (request('type') && request('type') !== 'all') { $activeChips[] = ucfirst(request('type')); }
|
||||
if (request('year_from') || request('year_to')) {
|
||||
$yf = request('year_from');
|
||||
$yt = request('year_to');
|
||||
$activeChips[] = 'Year: ' . ($yf ?: 'Any') . '–' . ($yt ?: 'Any');
|
||||
}
|
||||
if (request('rating')) { $activeChips[] = 'Rating: ' . request('rating') . '.0+'; }
|
||||
$sortMap = ['relevance'=>'Relevance','popularity'=>'Popularity','rating'=>'Rating','year_desc'=>'Year ↓','year_asc'=>'Year ↑','title'=>'Title A–Z'];
|
||||
$currentSort = $sortMap[request('sort','relevance')] ?? 'Relevance';
|
||||
if (request('sort') && request('sort') !== 'relevance') { $activeChips[] = 'Sort: ' . $currentSort; }
|
||||
|
||||
$genreNames = [];
|
||||
foreach(($genresList ?? []) as $g){
|
||||
$gid = null;
|
||||
$currentType = request('type', $type ?? 'all');
|
||||
if ($currentType === 'tv') { $gid = $g['tv_id'] ?? null; }
|
||||
elseif ($currentType === 'movie') { $gid = $g['movie_id'] ?? null; }
|
||||
else { $gid = $g['movie_id'] ?? ($g['tv_id'] ?? null); }
|
||||
if ($gid && in_array((string)$gid, $selectedGenresLocal, true)) { $genreNames[] = $g['name']; }
|
||||
}
|
||||
if (!empty($genreNames)) {
|
||||
$display = implode(', ', array_slice($genreNames, 0, 3));
|
||||
if (count($genreNames) > 3) { $display .= ' +' . (count($genreNames)-3); }
|
||||
$activeChips[] = 'Genres: ' . $display;
|
||||
}
|
||||
$activeCount = count($activeChips);
|
||||
@endphp
|
||||
<summary class="cursor-pointer p-4 md:p-6 flex items-center justify-between gap-3 hover:bg-white/5 transition-colors select-none">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-zinc-800 flex items-center justify-center text-zinc-400">
|
||||
<span class="material-symbols-rounded">tune</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-semibold text-white block">Filters & Sort</span>
|
||||
@if($activeCount > 0)
|
||||
<span class="text-xs text-red-400">{{$activeCount}} active filters</span>
|
||||
@else
|
||||
<span class="text-xs text-zinc-500">Refine your search</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-rounded text-zinc-400 transition-transform duration-300 group-open:rotate-180">expand_more</span>
|
||||
</summary>
|
||||
|
||||
<div class="details-content border-t border-white/5 bg-black/20">
|
||||
<div class="p-4 md:p-6 space-y-6">
|
||||
<!-- Active Chips -->
|
||||
@if($activeCount > 0)
|
||||
<div class="flex flex-wrap gap-2 pb-4 border-b border-white/5">
|
||||
<span class="text-xs font-medium text-zinc-500 uppercase tracking-wider py-1 mr-2">Active:</span>
|
||||
@foreach($activeChips as $chip)
|
||||
<span class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium bg-zinc-800 text-zinc-300 border border-zinc-700">
|
||||
{{ $chip }}
|
||||
</span>
|
||||
@endforeach
|
||||
<a href="{{ route('search', ['q' => request('q')]) }}" class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium text-zinc-400 hover:text-white transition-colors ml-auto">
|
||||
Clear all
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<!-- Type -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Type</label>
|
||||
<div class="relative">
|
||||
<select name="type" class="w-full appearance-none bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block p-2.5 pr-8" onchange="this.form.submit()">
|
||||
<option value="all" {{ ($type ?? 'all')==='all'?'selected':'' }}>All</option>
|
||||
<option value="movie" {{ ($type ?? 'all')==='movie'?'selected':'' }}>Movies</option>
|
||||
<option value="tv" {{ ($type ?? 'all')==='tv'?'selected':'' }}>TV Shows</option>
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-zinc-400">
|
||||
<span class="material-symbols-rounded text-sm">expand_more</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sort -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Sort By</label>
|
||||
<div class="relative">
|
||||
<select name="sort" class="w-full appearance-none bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block p-2.5 pr-8" onchange="this.form.submit()">
|
||||
@foreach($sortMap as $k => $v)
|
||||
<option value="{{ $k }}" {{ request('sort','relevance') === $k ? 'selected' : '' }}>{{ $v }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-zinc-400">
|
||||
<span class="material-symbols-rounded text-sm">expand_more</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Min Rating</label>
|
||||
<div class="relative">
|
||||
<select name="rating" class="w-full appearance-none bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block p-2.5 pr-8">
|
||||
<option value="">Any Rating</option>
|
||||
@for($i=9; $i>=1; $i--)
|
||||
<option value="{{ $i }}" {{ request('rating') == $i ? 'selected' : '' }}>{{ $i }}+ Stars</option>
|
||||
@endfor
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-zinc-400">
|
||||
<span class="material-symbols-rounded text-sm">star</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Year Range -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Release Year</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" name="year_from" placeholder="From" value="{{ request('year_from') }}" min="1900" max="{{ date('Y') }}"
|
||||
class="w-full bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block p-2.5">
|
||||
<span class="text-zinc-500">–</span>
|
||||
<input type="number" name="year_to" placeholder="To" value="{{ request('year_to') }}" min="1900" max="{{ date('Y') }}"
|
||||
class="w-full bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block p-2.5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Genres -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Genres</label>
|
||||
<div class="flex flex-wrap gap-2 max-h-40 overflow-y-auto pr-2 custom-scrollbar">
|
||||
@php
|
||||
$currentType = request('type', $type ?? 'all');
|
||||
@endphp
|
||||
@foreach(($genresList ?? []) as $g)
|
||||
@php
|
||||
$gid = null;
|
||||
if ($currentType === 'tv') { $gid = $g['tv_id'] ?? null; }
|
||||
elseif ($currentType === 'movie') { $gid = $g['movie_id'] ?? null; }
|
||||
else { $gid = $g['movie_id'] ?? ($g['tv_id'] ?? null); }
|
||||
$isSelected = $gid && in_array((string)$gid, $selectedGenresLocal, true);
|
||||
@endphp
|
||||
@if($gid)
|
||||
<label class="cursor-pointer select-none">
|
||||
<input type="checkbox" name="genre[]" value="{{ $gid }}" class="peer sr-only" {{ $isSelected ? 'checked' : '' }}>
|
||||
<span class="inline-block px-3 py-1.5 rounded-lg text-xs font-medium border transition-all
|
||||
peer-checked:bg-white peer-checked:text-black peer-checked:border-white
|
||||
bg-zinc-800/50 text-zinc-400 border-zinc-700 hover:border-zinc-500 hover:text-zinc-200">
|
||||
{{ $g['name'] }}
|
||||
</span>
|
||||
</label>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-4 border-t border-white/5">
|
||||
<button type="submit" class="bg-white text-black hover:bg-zinc-200 font-medium px-6 py-2.5 rounded-lg transition-colors flex items-center gap-2">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</form>
|
||||
|
||||
@if(!empty($results))
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold text-white">
|
||||
Results <span class="text-zinc-500 text-sm font-normal ml-2">({{ $total }} found)</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-x-4 gap-y-8">
|
||||
@foreach($results as $item)
|
||||
@php
|
||||
$typeResolved = $item['type'] ?? ($item['media_type'] ?? 'movie');
|
||||
$isMovie = $typeResolved === 'movie';
|
||||
$title = $item['title'] ?? $item['name'] ?? 'Untitled';
|
||||
$poster = $item['poster'] ?? $item['poster_path'] ?? null;
|
||||
$slug = $item['slug'] ?? null;
|
||||
$href = $slug ? ($isMovie ? route('movie.show', ['slug' => $slug]) : route('tv-show.show', ['slug' => $slug])) : '#';
|
||||
$accentColor = $isMovie ? 'red' : 'purple';
|
||||
@endphp
|
||||
<div class="group animate-fade-in-up" style="animation-delay: {{ $loop->index * 50 }}ms">
|
||||
<a href="{{ $href }}" class="block">
|
||||
<div class="relative aspect-[2/3] rounded-xl overflow-hidden bg-zinc-900 mb-3 shadow-lg ring-1 ring-white/5 group-hover:ring-{{ $accentColor }}-500/50 transition-all duration-300">
|
||||
@if($poster)
|
||||
<img src="{{ $poster }}"
|
||||
alt="{{ $title }}"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-zinc-700">
|
||||
<span class="material-symbols-rounded text-4xl">{{ $isMovie ? 'movie' : 'tv' }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-end p-4">
|
||||
<div class="transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
|
||||
<button class="w-full py-2 bg-{{ $accentColor }}-600 hover:bg-{{ $accentColor }}-700 text-white rounded-lg font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-{{ $accentColor }}-900/20">
|
||||
<span class="material-symbols-rounded text-lg">play_arrow</span>
|
||||
Watch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($item['rating']))
|
||||
<div class="absolute top-2 left-2 px-1.5 py-0.5 rounded bg-black/60 backdrop-blur-md border border-white/10 text-[10px] font-bold text-white flex items-center gap-1">
|
||||
<span class="material-symbols-rounded text-yellow-400 text-[12px]">star</span>
|
||||
{{ number_format($item['rating'], 1) }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="absolute top-2 right-2 px-1.5 py-0.5 rounded bg-{{ $accentColor }}-600/80 backdrop-blur-md text-[10px] font-bold text-white uppercase">
|
||||
{{ $isMovie ? 'Movie' : 'TV' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-white font-medium text-sm truncate group-hover:text-{{ $accentColor }}-400 transition-colors">{{ $title }}</h3>
|
||||
@if(!empty($item['year']))
|
||||
<p class="text-zinc-500 text-xs mt-1">{{ $item['year'] }}</p>
|
||||
@endif
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if(!empty($pagination) && ($pagination['total_pages'] ?? 1) > 1)
|
||||
@php
|
||||
$currentPage = (int)($pagination['current_page'] ?? 1);
|
||||
$totalPages = (int)($pagination['total_pages'] ?? 1);
|
||||
$window = 2;
|
||||
@endphp
|
||||
<div class="mt-12 flex items-center justify-center gap-2">
|
||||
@if ($currentPage > 1)
|
||||
<a href="{{ request()->fullUrlWithQuery(['page' => $currentPage - 1]) }}" class="w-10 h-10 flex items-center justify-center rounded-lg bg-zinc-900 text-zinc-400 hover:bg-zinc-800 hover:text-white transition-colors">
|
||||
<span class="material-symbols-rounded">chevron_left</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@for ($i = 1; $i <= $totalPages; $i++)
|
||||
@if ($i == 1 || $i == $totalPages || ($i >= $currentPage - $window && $i <= $currentPage + $window))
|
||||
<a href="{{ request()->fullUrlWithQuery(['page' => $i]) }}" class="w-10 h-10 flex items-center justify-center rounded-lg transition-colors {{ $i == $currentPage ? 'bg-white text-black font-bold' : 'bg-zinc-900 text-zinc-400 hover:bg-zinc-800 hover:text-white' }}">
|
||||
{{ $i }}
|
||||
</a>
|
||||
@elseif ($i == $currentPage - $window - 1 || $i == $currentPage + $window + 1)
|
||||
<span class="w-10 h-10 flex items-center justify-center text-zinc-600">...</span>
|
||||
@endif
|
||||
@endfor
|
||||
|
||||
@if ($currentPage < $totalPages)
|
||||
<a href="{{ request()->fullUrlWithQuery(['page' => $currentPage + 1]) }}" class="w-10 h-10 flex items-center justify-center rounded-lg bg-zinc-900 text-zinc-400 hover:bg-zinc-800 hover:text-white transition-colors">
|
||||
<span class="material-symbols-rounded">chevron_right</span>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="text-center py-20">
|
||||
<div class="w-20 h-20 bg-zinc-900 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<span class="material-symbols-rounded text-zinc-600 text-4xl">search_off</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-white mb-2">No results found</h3>
|
||||
<p class="text-zinc-400 max-w-md mx-auto mb-8">
|
||||
We couldn't find anything matching your search. Try different keywords or filters.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const searchInput = document.querySelector('input[name="q"]');
|
||||
const suggestionsBox = document.getElementById('search-suggestions');
|
||||
let debounceTimer;
|
||||
|
||||
if (searchInput && suggestionsBox) {
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
clearTimeout(debounceTimer);
|
||||
const query = e.target.value.trim();
|
||||
|
||||
if (query.length < 2) {
|
||||
suggestionsBox.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
debounceTimer = setTimeout(() => {
|
||||
fetch(`{{ route('search.suggestions') }}?q=${encodeURIComponent(query)}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
suggestionsBox.innerHTML = data.map(item => {
|
||||
const isMovie = item.type === 'movie';
|
||||
const accentColor = isMovie ? 'text-red-500' : 'text-purple-500';
|
||||
return `
|
||||
<a href="${item.url}" class="flex items-center gap-4 p-4 hover:bg-white/5 transition-colors border-b border-white/5 last:border-0">
|
||||
${item.poster ? `<img src="${item.poster}" class="w-12 h-16 object-cover rounded-lg bg-zinc-800 shadow-lg">` : '<div class="w-12 h-16 bg-zinc-800 rounded-lg flex items-center justify-center"><span class="material-symbols-rounded text-zinc-600">movie</span></div>'}
|
||||
<div>
|
||||
<div class="text-white font-medium text-base">${item.title}</div>
|
||||
<div class="flex items-center gap-2 text-xs mt-1">
|
||||
<span class="${accentColor} font-medium uppercase tracking-wider">${isMovie ? 'Movie' : 'TV Show'}</span>
|
||||
<span class="text-zinc-500">•</span>
|
||||
<span class="text-zinc-400">${item.year || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`}).join('');
|
||||
suggestionsBox.classList.remove('hidden');
|
||||
} else {
|
||||
suggestionsBox.classList.add('hidden');
|
||||
}
|
||||
})
|
||||
.catch(() => suggestionsBox.classList.add('hidden'));
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Close suggestions on click outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!searchInput.contains(e.target) && !suggestionsBox.contains(e.target)) {
|
||||
suggestionsBox.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
26
resources/views/sitemap.xml.blade.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>{{ url('/') }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>{{ route('movies.index') }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>{{ route('tv-shows.index') }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>{{ route('search') }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
20
resources/views/sitemap/index.blade.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
@foreach($moviePages as $p)
|
||||
<sitemap>
|
||||
<loc>{{ url("/sitemap-movies-{$p}.xml") }}</loc>
|
||||
<lastmod>{{ date('c') }}</lastmod>
|
||||
</sitemap>
|
||||
@endforeach
|
||||
@foreach($showPages as $p)
|
||||
<sitemap>
|
||||
<loc>{{ url("/sitemap-shows-{$p}.xml") }}</loc>
|
||||
<lastmod>{{ date('c') }}</lastmod>
|
||||
</sitemap>
|
||||
@endforeach
|
||||
@foreach($episodePages as $p)
|
||||
<sitemap>
|
||||
<loc>{{ url("/sitemap-episodes-{$p}.xml") }}</loc>
|
||||
<lastmod>{{ date('c') }}</lastmod>
|
||||
</sitemap>
|
||||
@endforeach
|
||||
</sitemapindex>
|
||||
30
resources/views/sitemap/urls.blade.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
@if($type === 'movie')
|
||||
@foreach($items as $item)
|
||||
<url>
|
||||
<loc>{{ route('movie.show', ['slug' => $item['slug']]) }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
@endforeach
|
||||
@elseif($type === 'show')
|
||||
@foreach($items as $item)
|
||||
<url>
|
||||
<loc>{{ route('tv-show.show', ['slug' => $item['slug']]) }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
@endforeach
|
||||
@elseif($type === 'episode')
|
||||
@foreach($items as $ep)
|
||||
<url>
|
||||
<loc>{{ route('tv-show.episode', ['slug' => $ep['show_slug'], 'season' => $ep['season'], 'episode' => $ep['episode']]) }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
@endforeach
|
||||
@endif
|
||||
</urlset>
|
||||
59
resources/views/sitemap/xml.blade.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>{{ url('/') }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>{{ route('movies.index') }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>{{ route('tv-shows.index') }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>{{ route('search') }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
|
||||
@isset($movies)
|
||||
@foreach($movies as $movie)
|
||||
<url>
|
||||
<loc>{{ route('movie.show', ['slug' => $movie['slug']]) }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
@endforeach
|
||||
@endisset
|
||||
|
||||
@isset($shows)
|
||||
@foreach($shows as $show)
|
||||
<url>
|
||||
<loc>{{ route('tv-show.show', ['slug' => $show['slug']]) }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
@endforeach
|
||||
@endisset
|
||||
|
||||
@isset($episodes)
|
||||
@foreach($episodes as $ep)
|
||||
<url>
|
||||
<loc>{{ route('tv-show.episode', ['slug' => $ep['show_slug'], 'season' => $ep['season'], 'episode' => $ep['episode']]) }}</loc>
|
||||
<lastmod>{{ date('Y-m-d') }}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
@endforeach
|
||||
@endisset
|
||||
</urlset>
|
||||
21
resources/views/tv-shows/embed-episode.blade.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Player</title>
|
||||
<style>html,body,iframe{margin:0;padding:0;height:100%;width:100%;background:#000}</style>
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</head>
|
||||
<body>
|
||||
<iframe
|
||||
src="{{ $embedUrl }}"
|
||||
allow="autoplay; encrypted-media; fullscreen *; picture-in-picture; clipboard-write; accelerometer"
|
||||
allowfullscreen
|
||||
referrerpolicy="no-referrer"
|
||||
style="border:0;width:100%;height:100%"
|
||||
></iframe>
|
||||
</body>
|
||||
</html>
|
||||
51
resources/views/tv-shows/embed.blade.php
Normal file
@@ -0,0 +1,51 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Watch ' . ($show['title'] ?? 'TV Show') . (isset($show['year']) ? ' (' . $show['year'] . ')' : '') . ' | Streamzy')
|
||||
|
||||
@section('content')
|
||||
<section class="wrap px-4 py-6 md:py-10">
|
||||
<div id="player-topbar" class="flex items-center gap-3 mb-4 md:mb-6">
|
||||
<a href="{{ url()->previous() }}" class="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-zinc-700 text-zinc-200 hover:text-white hover:border-zinc-500">
|
||||
<span class="material-symbols-rounded">arrow_back</span>
|
||||
<span class="hidden sm:inline">Back</span>
|
||||
</a>
|
||||
<h1 class="text-lg sm:text-xl md:text-2xl font-semibold text-white truncate">
|
||||
{{ $show['title'] ?? 'TV Show' }}@if(!empty($show['year'])) <span class="text-zinc-400 font-normal">({{ $show['year'] }})</span>@endif
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="relative -mx-4 md:-mx-6">
|
||||
<div id="player-box" class="w-screen max-w-none bg-black overflow-hidden border-y border-zinc-800">
|
||||
<iframe
|
||||
id="player-iframe"
|
||||
src="{{ $embedUrl }}"
|
||||
allow="autoplay; encrypted-media; fullscreen *; picture-in-picture; clipboard-write; accelerometer"
|
||||
allowfullscreen
|
||||
referrerpolicy="no-referrer"
|
||||
style="border:0;width:100%;height:100%"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
(function(){
|
||||
const box = document.getElementById('player-box');
|
||||
function resize(){
|
||||
const header = document.querySelector('header');
|
||||
const topbar = document.getElementById('player-topbar');
|
||||
const headerH = header ? header.offsetHeight : 0;
|
||||
const topH = topbar ? topbar.offsetHeight : 0;
|
||||
const extra = 16;
|
||||
const h = Math.max(240, window.innerHeight - headerH - topH - extra);
|
||||
box.style.height = h + 'px';
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
window.addEventListener('orientationchange', resize);
|
||||
document.addEventListener('DOMContentLoaded', resize);
|
||||
resize();
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
158
resources/views/tv-shows/episode.blade.php
Normal file
@@ -0,0 +1,158 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('head')
|
||||
@php
|
||||
$ld = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'TVEpisode',
|
||||
'name' => $episode['name'],
|
||||
'partOfSeries' => [
|
||||
'@type' => 'TVSeries',
|
||||
'name' => $show['title']
|
||||
],
|
||||
'partOfSeason' => [
|
||||
'@type' => 'TVSeason',
|
||||
'seasonNumber' => $episode['season_number']
|
||||
],
|
||||
'episodeNumber' => $episode['episode_number'],
|
||||
'description' => $episode['overview'] ?? null,
|
||||
'image' => $episode['still_path'] ?? $show['poster'] ?? null,
|
||||
'datePublished' => $episode['air_date'] ?? null,
|
||||
];
|
||||
$ld = array_filter($ld, fn($v) => !is_null($v));
|
||||
@endphp
|
||||
<script type="application/ld+json">{!! json_encode($ld, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) !!}</script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<section class="bg-zinc-950 min-h-screen pb-20">
|
||||
<!-- Player Container -->
|
||||
<div class="w-full bg-black aspect-video max-h-[80vh] relative group">
|
||||
<iframe
|
||||
src="{{ route('tv-show.embed-episode', ['id' => $show['id'], 'season' => $episode['season_number'], 'episode' => $episode['episode_number']]) }}"
|
||||
class="w-full h-full border-0"
|
||||
allow="autoplay; encrypted-media; fullscreen *; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
<!-- Back Button Overlay (visible on hover/pause) -->
|
||||
<div class="absolute top-4 left-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none">
|
||||
<a href="{{ route('tv-show.show', ['slug' => $show['slug']]) }}" class="flex items-center gap-2 px-4 py-2 bg-black/50 hover:bg-black/80 backdrop-blur-sm text-white rounded-lg transition-colors pointer-events-auto">
|
||||
<span class="material-symbols-rounded">arrow_back</span>
|
||||
Back to Show
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Episode Info -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="flex flex-col md:flex-row gap-8 items-start justify-between">
|
||||
<div class="flex-1 space-y-4">
|
||||
<div class="flex items-center gap-3 text-sm text-zinc-400">
|
||||
<span class="px-2 py-1 rounded bg-zinc-900 border border-zinc-800 text-white font-medium">
|
||||
S{{ $episode['season_number'] }} E{{ $episode['episode_number'] }}
|
||||
</span>
|
||||
@if(!empty($episode['air_date']))
|
||||
<span>{{ $episode['air_date'] }}</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h1 class="text-2xl md:text-4xl font-bold text-white">
|
||||
{{ $episode['name'] }}
|
||||
</h1>
|
||||
|
||||
@if(!empty($episode['overview']))
|
||||
<p class="text-zinc-300 leading-relaxed max-w-3xl">
|
||||
{{ $episode['overview'] }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="flex items-center gap-4 w-full md:w-auto">
|
||||
@if($previousEpisode)
|
||||
<a href="{{ route('tv-show.episode', ['slug' => $show['slug'], 'season' => $previousEpisode['season_number'], 'episode' => $previousEpisode['episode_number']]) }}" class="flex-1 md:flex-none flex items-center justify-center gap-2 px-6 py-3 bg-zinc-900 hover:bg-zinc-800 text-white rounded-xl border border-zinc-800 transition-colors">
|
||||
<span class="material-symbols-rounded">skip_previous</span>
|
||||
Previous
|
||||
</a>
|
||||
@else
|
||||
<button disabled class="flex-1 md:flex-none flex items-center justify-center gap-2 px-6 py-3 bg-zinc-900/50 text-zinc-600 rounded-xl border border-zinc-800/50 cursor-not-allowed">
|
||||
<span class="material-symbols-rounded">skip_previous</span>
|
||||
Previous
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if($nextEpisode)
|
||||
<a href="{{ route('tv-show.episode', ['slug' => $show['slug'], 'season' => $nextEpisode['season_number'], 'episode' => $nextEpisode['episode_number']]) }}" class="flex-1 md:flex-none flex items-center justify-center gap-2 px-6 py-3 bg-red-600 hover:bg-red-700 text-white rounded-xl font-bold shadow-lg shadow-red-900/20 transition-colors">
|
||||
Next
|
||||
<span class="material-symbols-rounded">skip_next</span>
|
||||
</a>
|
||||
@else
|
||||
<button disabled class="flex-1 md:flex-none flex items-center justify-center gap-2 px-6 py-3 bg-zinc-900/50 text-zinc-600 rounded-xl border border-zinc-800/50 cursor-not-allowed">
|
||||
Next
|
||||
<span class="material-symbols-rounded">skip_next</span>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Keyboard shortcuts for video player
|
||||
document.addEventListener('keydown', function(e) {
|
||||
const iframe = document.querySelector('iframe');
|
||||
if (!iframe) return;
|
||||
|
||||
// Ignore if user is typing in an input field
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
|
||||
// F key for fullscreen
|
||||
if (e.key === 'f' || e.key === 'F') {
|
||||
e.preventDefault();
|
||||
if (iframe.requestFullscreen) {
|
||||
iframe.requestFullscreen();
|
||||
} else if (iframe.webkitRequestFullscreen) {
|
||||
iframe.webkitRequestFullscreen();
|
||||
} else if (iframe.mozRequestFullScreen) {
|
||||
iframe.mozRequestFullScreen();
|
||||
} else if (iframe.msRequestFullscreen) {
|
||||
iframe.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
// ESC to exit fullscreen
|
||||
if (e.key === 'Escape') {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Show keyboard shortcuts hint on first visit
|
||||
if (!sessionStorage.getItem('keyboardHintShown')) {
|
||||
setTimeout(() => {
|
||||
const hint = document.createElement('div');
|
||||
hint.className = 'fixed bottom-4 right-4 bg-zinc-900 border border-zinc-700 text-white px-4 py-3 rounded-lg shadow-xl text-sm z-50 animate-fade-in';
|
||||
hint.innerHTML = `
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="material-symbols-rounded text-red-400">keyboard</span>
|
||||
<div>
|
||||
<p class="font-semibold mb-1">Keyboard Shortcuts</p>
|
||||
<p class="text-zinc-400">Press <kbd class="px-1.5 py-0.5 bg-zinc-800 rounded text-xs">F</kbd> for fullscreen</p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.parentElement.remove()" class="text-zinc-400 hover:text-white">
|
||||
<span class="material-symbols-rounded text-sm">close</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(hint);
|
||||
sessionStorage.setItem('keyboardHintShown', 'true');
|
||||
setTimeout(() => hint.remove(), 5000);
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@endsection
|
||||
284
resources/views/tv-shows/index.blade.php
Normal file
@@ -0,0 +1,284 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('head')
|
||||
<style>
|
||||
details .details-content{max-height:0;overflow:hidden;transition:max-height .25s ease}
|
||||
details[open] .details-content{max-height:1200px}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<!-- Hero Section -->
|
||||
<section class="relative py-20 overflow-hidden bg-zinc-950">
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[50%] -left-[20%] w-[100%] h-[100%] bg-purple-600/5 blur-[120px] rounded-full mix-blend-screen"></div>
|
||||
<div class="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 brightness-100 contrast-150"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-zinc-950/50 via-zinc-950/80 to-zinc-950"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-4xl md:text-6xl font-bold text-white mb-4 animate-fade-in-up">
|
||||
Discover <span class="text-transparent bg-clip-text bg-gradient-to-r from-purple-500 to-pink-500">TV Shows</span>
|
||||
</h1>
|
||||
<p class="text-zinc-400 text-lg max-w-2xl mx-auto animate-fade-in-up delay-100">
|
||||
Binge-watch your favorite series, from trending hits to cult classics.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TV Shows Grid Section -->
|
||||
<section class="bg-zinc-950 pb-20 min-h-screen">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Filters (collapsible) -->
|
||||
<form action="{{ route('tv-shows.index') }}" method="GET" class="mb-10 animate-fade-in-up delay-200">
|
||||
<details class="group rounded-2xl bg-zinc-900/50 border border-white/5 backdrop-blur-sm overflow-hidden transition-all duration-300">
|
||||
@php
|
||||
$selectedGenresLocal = request('genre', $selectedGenres ?? []);
|
||||
if (!is_array($selectedGenresLocal)) { $selectedGenresLocal = [$selectedGenresLocal]; }
|
||||
$selectedGenresLocal = array_map('strval', $selectedGenresLocal);
|
||||
$activeChips = [];
|
||||
if (request('year_from') || request('year_to')) {
|
||||
$yf = request('year_from');
|
||||
$yt = request('year_to');
|
||||
$activeChips[] = 'Year: ' . ($yf ?: 'Any') . '–' . ($yt ?: 'Any');
|
||||
}
|
||||
if (request('rating')) { $activeChips[] = 'Rating: ' . request('rating') . '.0+'; }
|
||||
$sortMap = ['popularity'=>'Popularity','rating'=>'Rating','year_desc'=>'Year ↓','year_asc'=>'Year ↑','title'=>'Title A–Z'];
|
||||
$currentSort = $sortMap[request('sort','popularity')] ?? 'Popularity';
|
||||
if (request('sort') && request('sort') !== 'popularity') { $activeChips[] = 'Sort: ' . $currentSort; }
|
||||
|
||||
// Build map of selected genre id => name for per-chip removal
|
||||
$selectedGenreMap = [];
|
||||
foreach(($genresList ?? []) as $g){
|
||||
$gid = $g['tv_id'] ?? null;
|
||||
if ($gid && in_array((string)$gid, $selectedGenresLocal, true)) {
|
||||
$selectedGenreMap[(string)$gid] = $g['name'];
|
||||
}
|
||||
}
|
||||
$activeCount = count($activeChips);
|
||||
$activeCount += count($selectedGenreMap);
|
||||
$qsBase = request()->query();
|
||||
unset($qsBase['page']);
|
||||
@endphp
|
||||
<summary class="cursor-pointer p-4 md:p-6 flex items-center justify-between gap-3 hover:bg-white/5 transition-colors select-none">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-500">
|
||||
<span class="material-symbols-rounded">tune</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-semibold text-white block">Filters & Sort</span>
|
||||
@if($activeCount > 0)
|
||||
<span class="text-xs text-purple-400">{{$activeCount}} active filters</span>
|
||||
@else
|
||||
<span class="text-xs text-zinc-500">Refine your search</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-rounded text-zinc-400 transition-transform duration-300 group-open:rotate-180">expand_more</span>
|
||||
</summary>
|
||||
|
||||
<div class="details-content border-t border-white/5 bg-black/20">
|
||||
<div class="p-4 md:p-6 space-y-6">
|
||||
<!-- Active Chips -->
|
||||
@if($activeCount > 0)
|
||||
<div class="flex flex-wrap gap-2 pb-4 border-b border-white/5">
|
||||
<span class="text-xs font-medium text-zinc-500 uppercase tracking-wider py-1 mr-2">Active:</span>
|
||||
<!-- Genre chips with removal -->
|
||||
@foreach($selectedGenreMap as $gid => $gname)
|
||||
@php
|
||||
$qs = $qsBase;
|
||||
$gq = $qs['genre'] ?? [];
|
||||
if (!is_array($gq)) { $gq = [$gq]; }
|
||||
$gq = array_values(array_filter($gq, fn($v) => (string)$v !== (string)$gid));
|
||||
if (!empty($gq)) { $qs['genre'] = $gq; } else { unset($qs['genre']); }
|
||||
$url = route('tv-shows.index') . (empty($qs) ? '' : ('?' . http_build_query($qs)));
|
||||
@endphp
|
||||
<a href="{{ $url }}" class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium bg-purple-500/10 text-purple-400 border border-purple-500/20 hover:bg-purple-500/20 transition-colors">
|
||||
{{ $gname }}
|
||||
<span class="material-symbols-rounded text-[14px]">close</span>
|
||||
</a>
|
||||
@endforeach
|
||||
<!-- Other chips -->
|
||||
@foreach($activeChips as $chip)
|
||||
<span class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium bg-zinc-800 text-zinc-300 border border-zinc-700">
|
||||
{{ $chip }}
|
||||
</span>
|
||||
@endforeach
|
||||
<a href="{{ route('tv-shows.index') }}" class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium text-zinc-400 hover:text-white transition-colors ml-auto">
|
||||
Clear all
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<!-- Sort -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Sort By</label>
|
||||
<div class="relative">
|
||||
<select name="sort" class="w-full appearance-none bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-purple-500 focus:border-purple-500 block p-2.5 pr-8" onchange="this.form.submit()">
|
||||
@foreach($sortMap as $k => $v)
|
||||
<option value="{{ $k }}" {{ request('sort','popularity') === $k ? 'selected' : '' }}>{{ $v }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-zinc-400">
|
||||
<span class="material-symbols-rounded text-sm">expand_more</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Min Rating</label>
|
||||
<div class="relative">
|
||||
<select name="rating" class="w-full appearance-none bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-purple-500 focus:border-purple-500 block p-2.5 pr-8">
|
||||
<option value="">Any Rating</option>
|
||||
@for($i=9; $i>=1; $i--)
|
||||
<option value="{{ $i }}" {{ request('rating') == $i ? 'selected' : '' }}>{{ $i }}+ Stars</option>
|
||||
@endfor
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-zinc-400">
|
||||
<span class="material-symbols-rounded text-sm">star</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Year Range -->
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Release Year</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" name="year_from" placeholder="From" value="{{ request('year_from') }}" min="1900" max="{{ date('Y') }}"
|
||||
class="w-full bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-purple-500 focus:border-purple-500 block p-2.5">
|
||||
<span class="text-zinc-500">–</span>
|
||||
<input type="number" name="year_to" placeholder="To" value="{{ request('year_to') }}" min="1900" max="{{ date('Y') }}"
|
||||
class="w-full bg-zinc-900 border border-zinc-700 text-white text-sm rounded-lg focus:ring-purple-500 focus:border-purple-500 block p-2.5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Genres -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Genres</label>
|
||||
<div class="flex flex-wrap gap-2 max-h-40 overflow-y-auto pr-2 custom-scrollbar">
|
||||
@foreach(($genresList ?? []) as $genre)
|
||||
@php
|
||||
$gid = (string)($genre['tv_id'] ?? '');
|
||||
$isSelected = in_array($gid, $selectedGenresLocal, true);
|
||||
@endphp
|
||||
<label class="cursor-pointer select-none">
|
||||
<input type="checkbox" name="genre[]" value="{{ $gid }}" class="peer sr-only" {{ $isSelected ? 'checked' : '' }}>
|
||||
<span class="inline-block px-3 py-1.5 rounded-lg text-xs font-medium border transition-all
|
||||
peer-checked:bg-purple-600 peer-checked:text-white peer-checked:border-purple-600
|
||||
bg-zinc-800/50 text-zinc-400 border-zinc-700 hover:border-zinc-500 hover:text-zinc-200">
|
||||
{{ $genre['name'] }}
|
||||
</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-4 border-t border-white/5">
|
||||
<button type="submit" class="bg-white text-black hover:bg-zinc-200 font-medium px-6 py-2.5 rounded-lg transition-colors flex items-center gap-2">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</form>
|
||||
|
||||
<!-- Grid -->
|
||||
@if(count($tvShows) > 0)
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-x-4 gap-y-8">
|
||||
@foreach($tvShows as $show)
|
||||
<div class="group animate-fade-in-up" style="animation-delay: {{ $loop->index * 50 }}ms">
|
||||
<a href="{{ route('tv-show.show', ['slug' => $show['slug']]) }}" class="block">
|
||||
<div class="relative aspect-[2/3] rounded-xl overflow-hidden bg-zinc-900 mb-3 shadow-lg ring-1 ring-white/5 group-hover:ring-purple-500/50 transition-all duration-300">
|
||||
@if($show['poster'])
|
||||
<img src="{{ $show['poster'] }}"
|
||||
alt="{{ $show['title'] }}"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-zinc-700">
|
||||
<span class="material-symbols-rounded text-4xl">tv</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-end p-4">
|
||||
<div class="transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
|
||||
<button class="w-full py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-medium text-sm flex items-center justify-center gap-2 shadow-lg shadow-purple-900/20">
|
||||
<span class="material-symbols-rounded text-lg">play_arrow</span>
|
||||
Watch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($show['rating'])
|
||||
<div class="absolute top-2 left-2 px-1.5 py-0.5 rounded bg-black/60 backdrop-blur-md border border-white/10 text-[10px] font-bold text-white flex items-center gap-1">
|
||||
<span class="material-symbols-rounded text-yellow-400 text-[12px]">star</span>
|
||||
{{ number_format($show['rating'], 1) }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($show['year'])
|
||||
<div class="absolute top-2 right-2 px-1.5 py-0.5 rounded bg-purple-600/80 backdrop-blur-md text-[10px] font-bold text-white">
|
||||
{{ $show['year'] }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h3 class="text-white font-medium text-sm truncate group-hover:text-purple-400 transition-colors">{{ $show['title'] }}</h3>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if(!empty($pagination) && ($pagination['total_pages'] ?? 1) > 1)
|
||||
@php
|
||||
$currentPage = (int)($pagination['current_page'] ?? 1);
|
||||
$totalPages = (int)($pagination['total_pages'] ?? 1);
|
||||
$window = 2;
|
||||
@endphp
|
||||
<div class="mt-12 flex items-center justify-center gap-2">
|
||||
@if ($currentPage > 1)
|
||||
<a href="{{ request()->fullUrlWithQuery(['page' => $currentPage - 1]) }}" class="w-10 h-10 flex items-center justify-center rounded-lg bg-zinc-900 text-zinc-400 hover:bg-zinc-800 hover:text-white transition-colors">
|
||||
<span class="material-symbols-rounded">chevron_left</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@for ($i = 1; $i <= $totalPages; $i++)
|
||||
@if ($i == 1 || $i == $totalPages || ($i >= $currentPage - $window && $i <= $currentPage + $window))
|
||||
<a href="{{ request()->fullUrlWithQuery(['page' => $i]) }}" class="w-10 h-10 flex items-center justify-center rounded-lg transition-colors {{ $i == $currentPage ? 'bg-white text-black font-bold' : 'bg-zinc-900 text-zinc-400 hover:bg-zinc-800 hover:text-white' }}">
|
||||
{{ $i }}
|
||||
</a>
|
||||
@elseif ($i == $currentPage - $window - 1 || $i == $currentPage + $window + 1)
|
||||
<span class="w-10 h-10 flex items-center justify-center text-zinc-600">...</span>
|
||||
@endif
|
||||
@endfor
|
||||
|
||||
@if ($currentPage < $totalPages)
|
||||
<a href="{{ request()->fullUrlWithQuery(['page' => $currentPage + 1]) }}" class="w-10 h-10 flex items-center justify-center rounded-lg bg-zinc-900 text-zinc-400 hover:bg-zinc-800 hover:text-white transition-colors">
|
||||
<span class="material-symbols-rounded">chevron_right</span>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="text-center py-20">
|
||||
<div class="w-20 h-20 bg-zinc-900 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<span class="material-symbols-rounded text-zinc-600 text-4xl">tv_off</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-white mb-2">No TV shows found</h3>
|
||||
<p class="text-zinc-400 max-w-md mx-auto mb-8">
|
||||
We couldn't find any TV shows matching your filters. Try adjusting your search criteria.
|
||||
</p>
|
||||
<a href="{{ route('tv-shows.index') }}" class="inline-flex items-center gap-2 bg-white text-black hover:bg-zinc-200 font-medium px-6 py-3 rounded-lg transition-colors">
|
||||
Clear Filters
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
305
resources/views/tv-shows/show.blade.php
Normal file
@@ -0,0 +1,305 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('head')
|
||||
@php
|
||||
$ld = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'TVSeries',
|
||||
'name' => $show['title'],
|
||||
'startDate' => $show['first_air_date'] ?? ($show['year'] ?? null),
|
||||
'image' => $show['poster'] ?? null,
|
||||
'description' => $show['overview'] ?? null,
|
||||
'aggregateRating' => isset($show['rating']) ? [
|
||||
'@type' => 'AggregateRating',
|
||||
'ratingValue' => round($show['rating'], 1),
|
||||
'ratingCount' => $show['vote_count'] ?? 0
|
||||
] : null,
|
||||
'genre' => $show['genres'] ?? null,
|
||||
'numberOfSeasons' => $show['number_of_seasons'] ?? null,
|
||||
];
|
||||
$ld = array_filter($ld, fn($v) => !is_null($v));
|
||||
@endphp
|
||||
<script type="application/ld+json">{!! json_encode($ld, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) !!}</script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$heroImg = $show['backdrop'] ?? $show['poster'] ?? null;
|
||||
@endphp
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="relative min-h-[70vh] flex items-end pb-20 overflow-hidden bg-zinc-950">
|
||||
<!-- Background Image -->
|
||||
@if($heroImg)
|
||||
<div class="absolute inset-0">
|
||||
<img src="{{ $heroImg }}" alt="" class="w-full h-full object-cover opacity-40">
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-zinc-950 via-zinc-950/60 to-transparent"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-zinc-950 via-zinc-950/40 to-transparent"></div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex flex-col md:flex-row gap-8 md:gap-12 items-start">
|
||||
<!-- Poster (Desktop) -->
|
||||
<div class="hidden md:block w-64 flex-shrink-0 rounded-xl overflow-hidden shadow-2xl ring-1 ring-white/10 rotate-1 hover:rotate-0 transition-transform duration-500">
|
||||
@if($show['poster'])
|
||||
<img src="{{ $show['poster'] }}" alt="{{ $show['title'] }}" class="w-full h-auto object-cover">
|
||||
@else
|
||||
<div class="w-full aspect-[2/3] bg-zinc-900 flex items-center justify-center">
|
||||
<span class="material-symbols-rounded text-6xl text-zinc-700">tv</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 space-y-6">
|
||||
<div class="space-y-2">
|
||||
<a href="{{ route('tv-shows.index') }}" class="inline-flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors mb-4">
|
||||
<span class="material-symbols-rounded text-lg">arrow_back</span>
|
||||
Back to TV Shows
|
||||
</a>
|
||||
|
||||
<h1 class="text-4xl md:text-6xl font-bold text-white leading-tight">
|
||||
{{ $show['title'] }}
|
||||
</h1>
|
||||
|
||||
@if(!empty($show['tagline']))
|
||||
<p class="text-xl text-red-400 font-medium italic">{{ $show['tagline'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm md:text-base text-zinc-300">
|
||||
@if($show['year'])
|
||||
<span class="px-2.5 py-1 rounded-md bg-white/10 border border-white/10 font-medium text-white">
|
||||
{{ $show['year'] }}
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if($show['rating'])
|
||||
<div class="flex items-center gap-1.5 text-yellow-400">
|
||||
<span class="material-symbols-rounded fill-current">star</span>
|
||||
<span class="font-bold text-white">{{ number_format($show['rating'], 1) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(!empty($show['number_of_seasons']))
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="material-symbols-rounded text-zinc-500">layers</span>
|
||||
<span>{{ $show['number_of_seasons'] }} Seasons</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Genres -->
|
||||
@if(!empty($show['genres']))
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($show['genres'] as $genre)
|
||||
@php $genreLabel = is_array($genre) ? ($genre['name'] ?? '') : $genre; @endphp
|
||||
@if($genreLabel)
|
||||
<a href="{{ route('tv-shows.index', ['genre[]' => is_array($genre) ? $genre['id'] : '']) }}" class="px-3 py-1 rounded-full text-sm font-medium bg-zinc-900/80 border border-zinc-700 text-zinc-300 hover:text-white hover:border-red-500/50 transition-colors">
|
||||
{{ $genreLabel }}
|
||||
</a>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Overview -->
|
||||
@if($show['overview'])
|
||||
<p class="text-lg text-zinc-300 leading-relaxed max-w-3xl">
|
||||
{{ $show['overview'] }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex flex-wrap gap-4 pt-4">
|
||||
<a href="#episodes" class="group relative px-8 py-4 bg-red-600 hover:bg-red-700 text-white rounded-xl font-bold text-lg shadow-lg shadow-red-900/20 transition-all hover:scale-105 flex items-center gap-3">
|
||||
<span class="material-symbols-rounded text-3xl">play_circle</span>
|
||||
Watch Episodes
|
||||
<div class="absolute inset-0 rounded-xl ring-2 ring-white/20 group-hover:ring-white/40 transition-all"></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Episodes Section -->
|
||||
<section id="episodes" class="py-16 bg-zinc-950 border-t border-white/5">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-6 mb-8">
|
||||
<h2 class="text-2xl font-bold text-white flex items-center gap-3">
|
||||
<span class="w-1 h-8 bg-red-500 rounded-full"></span>
|
||||
Episodes
|
||||
</h2>
|
||||
|
||||
<!-- Season Selector -->
|
||||
@if(($show['number_of_seasons'] ?? 0) > 1)
|
||||
<div class="relative">
|
||||
<select id="season-select" onchange="loadSeason(this.value)" class="appearance-none bg-zinc-900 border border-zinc-700 text-white py-2 pl-4 pr-10 rounded-lg focus:ring-red-500 focus:border-red-500 cursor-pointer">
|
||||
@for($i = 1; $i <= ($show['number_of_seasons'] ?? 1); $i++)
|
||||
<option value="{{ $i }}" {{ $i == $currentSeason ? 'selected' : '' }}>Season {{ $i }}</option>
|
||||
@endfor
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-zinc-400">
|
||||
<span class="material-symbols-rounded">expand_more</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Episodes Grid -->
|
||||
<div id="episodes-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
@foreach($seasonData['episodes'] ?? [] as $ep)
|
||||
<a href="{{ route('tv-show.episode', ['slug' => $show['slug'], 'season' => $currentSeason, 'episode' => $ep['episode_number']]) }}" class="group block bg-zinc-900 rounded-xl overflow-hidden border border-white/5 hover:border-red-500/50 transition-all hover:-translate-y-1">
|
||||
<div class="relative aspect-video bg-zinc-800">
|
||||
@if($ep['still_path'])
|
||||
<img src="{{ $ep['still_path'] }}" alt="{{ $ep['name'] }}" loading="lazy" class="w-full h-full object-cover opacity-80 group-hover:opacity-100 transition-opacity">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-zinc-700"><span class="material-symbols-rounded text-4xl">image</span></div>
|
||||
@endif
|
||||
<div class="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span class="material-symbols-rounded text-4xl text-white drop-shadow-lg">play_circle</span>
|
||||
</div>
|
||||
<div class="absolute bottom-2 right-2 px-2 py-1 rounded bg-black/80 text-xs font-bold text-white">
|
||||
S{{ $currentSeason }} E{{ $ep['episode_number'] }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<h3 class="font-medium text-white truncate group-hover:text-red-400 transition-colors">{{ $ep['name'] }}</h3>
|
||||
<p class="text-sm text-zinc-500 mt-1 line-clamp-2">{{ $ep['overview'] ?? 'No overview available.' }}</p>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="episodes-loading" class="hidden py-20 text-center">
|
||||
<div class="inline-block w-10 h-10 border-4 border-red-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cast Section -->
|
||||
@if(!empty($show['cast']))
|
||||
<section class="py-16 bg-zinc-900/30 border-t border-white/5">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 class="text-2xl font-bold text-white mb-8 flex items-center gap-3">
|
||||
<span class="w-1 h-8 bg-red-500 rounded-full"></span>
|
||||
Top Cast
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||
@foreach(array_slice($show['cast'], 0, 12) as $actor)
|
||||
<div class="group">
|
||||
<div class="relative aspect-[2/3] rounded-xl overflow-hidden bg-zinc-900 mb-3 ring-1 ring-white/5 group-hover:ring-red-500/50 transition-all">
|
||||
@if($actor['profile_path'])
|
||||
<img src="{{ $actor['profile_path'] }}" alt="{{ $actor['name'] }}" loading="lazy" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-zinc-700">
|
||||
<span class="material-symbols-rounded text-4xl">person</span>
|
||||
</div>
|
||||
@endif
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</div>
|
||||
<h3 class="text-white font-medium truncate group-hover:text-red-400 transition-colors">{{ $actor['name'] }}</h3>
|
||||
@if($actor['character'])
|
||||
<p class="text-zinc-500 text-sm truncate">{{ $actor['character'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<!-- Production Info -->
|
||||
<section class="py-16 bg-zinc-950 border-t border-white/5">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid md:grid-cols-2 gap-12">
|
||||
@if(!empty($show['production_companies']))
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||
<span class="material-symbols-rounded text-zinc-500">apartment</span>
|
||||
Production Companies
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
@foreach($show['production_companies'] as $co)
|
||||
<div class="flex items-center gap-3 px-4 py-3 rounded-xl bg-zinc-900 border border-white/5 hover:border-white/10 transition-colors">
|
||||
@if(!empty($co['logo']))
|
||||
<img src="{{ $co['logo'] }}" alt="{{ $co['name'] }}" class="h-8 w-auto object-contain brightness-0 invert opacity-70">
|
||||
@else
|
||||
<span class="material-symbols-rounded text-zinc-600">business</span>
|
||||
@endif
|
||||
<span class="text-sm font-medium text-zinc-300">{{ $co['name'] }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(!empty($show['production_countries']))
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||
<span class="material-symbols-rounded text-zinc-500">public</span>
|
||||
Production Countries
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($show['production_countries'] as $cty)
|
||||
<span class="px-3 py-1.5 rounded-lg bg-zinc-900 border border-white/5 text-sm text-zinc-400">
|
||||
{{ $cty }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
async function loadSeason(season) {
|
||||
const grid = document.getElementById('episodes-grid');
|
||||
const loading = document.getElementById('episodes-loading');
|
||||
|
||||
grid.classList.add('hidden');
|
||||
loading.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/show/{{ $show['id'] }}/season/${season}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.episodes) {
|
||||
grid.innerHTML = data.episodes.map(ep => `
|
||||
<a href="/show/{{ $show['slug'] }}/s${season}e${ep.episode_number}" class="group block bg-zinc-900 rounded-xl overflow-hidden border border-white/5 hover:border-red-500/50 transition-all hover:-translate-y-1">
|
||||
<div class="relative aspect-video bg-zinc-800">
|
||||
${ep.still_path ?
|
||||
`<img src="${ep.still_path}" alt="${ep.name}" loading="lazy" class="w-full h-full object-cover opacity-80 group-hover:opacity-100 transition-opacity">` :
|
||||
`<div class="w-full h-full flex items-center justify-center text-zinc-700"><span class="material-symbols-rounded text-4xl">image</span></div>`
|
||||
}
|
||||
<div class="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span class="material-symbols-rounded text-4xl text-white drop-shadow-lg">play_circle</span>
|
||||
</div>
|
||||
<div class="absolute bottom-2 right-2 px-2 py-1 rounded bg-black/80 text-xs font-bold text-white">
|
||||
S${season} E${ep.episode_number}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<h3 class="font-medium text-white truncate group-hover:text-red-400 transition-colors">${ep.name}</h3>
|
||||
<p class="text-sm text-zinc-500 mt-1 line-clamp-2">${ep.overview || 'No overview available.'}</p>
|
||||
</div>
|
||||
</a>
|
||||
`).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading season:', error);
|
||||
} finally {
|
||||
loading.classList.add('hidden');
|
||||
grid.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
510
resources/views/welcome.blade.php
Normal file
@@ -0,0 +1,510 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Streamzy - Watch Movies & TV Shows Online | Free Streaming</title>
|
||||
<meta name="description" content="Welcome to Streamzy - your ultimate destination for streaming the latest movies and TV shows in HD quality. Watch thousands of titles for free with no subscription required.">
|
||||
<meta name="keywords" content="streaming, movies, tv shows, watch online, free movies, latest movies, HD streaming">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=inter:300,400,500,600,700&family=poppins:400,500,600,700" rel="stylesheet" />
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
||||
|
||||
<!-- Styles / Scripts -->
|
||||
@if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@else
|
||||
<style>
|
||||
/* Modern CSS Variables & Base Styles */
|
||||
:root {
|
||||
--primary-red: #ef4444;
|
||||
--primary-red-hover: #dc2626;
|
||||
--bg-dark: #0f0f0f;
|
||||
--bg-card: #1a1a1a;
|
||||
--bg-card-hover: #262626;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #a1a1aa;
|
||||
--text-muted: #71717a;
|
||||
--border-color: #27272a;
|
||||
--border-hover: #3f3f46;
|
||||
--glass-bg: rgba(26, 26, 26, 0.8);
|
||||
--gradient-primary: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
--shadow-lg: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
--shadow-xl: 0 35px 60px -12px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--gradient-primary);
|
||||
color: white;
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 0.75rem;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
padding: 0.75rem 2rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.75rem;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--primary-red);
|
||||
transform: translateY(-2px);
|
||||
color: var(--primary-red);
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
border-color: var(--border-hover);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: var(--gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
animation: fadeInUp 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in-delay {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
animation: fadeInUp 0.8s ease-out 0.2s forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in-delay-2 {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
animation: fadeInUp 0.8s ease-out 0.4s forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-bg {
|
||||
background: radial-gradient(ellipse at center, rgba(239, 68, 68, 0.1) 0%, transparent 70%),
|
||||
linear-gradient(180deg, var(--bg-dark) 0%, rgba(15, 15, 15, 0.8) 100%);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
background: var(--bg-card-hover);
|
||||
border-color: var(--border-hover);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 2rem;
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-red);
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem !important;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.1rem !important;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
padding: 0.625rem 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-dark);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-hover);
|
||||
}
|
||||
</style>
|
||||
@endif
|
||||
</head>
|
||||
<body class="bg-[#FDFDFC] dark:bg-[#0a0a0a] text-[#1b1b18] flex p-6 lg:p-8 items-center lg:justify-center min-h-screen flex-col">
|
||||
<header class="w-full lg:max-w-4xl max-w-[335px] text-sm mb-6 not-has-[nav]:hidden">
|
||||
@if (Route::has('login'))
|
||||
<nav class="flex items-center justify-end gap-4">
|
||||
@auth
|
||||
<a
|
||||
href="{{ url('/dashboard') }}"
|
||||
class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] border-[#19140035] hover:border-[#1915014a] border text-[#1b1b18] dark:border-[#3E3E3A] dark:hover:border-[#62605b] rounded-sm text-sm leading-normal"
|
||||
>
|
||||
Dashboard
|
||||
</a>
|
||||
@else
|
||||
<a
|
||||
href="{{ route('login') }}"
|
||||
class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] text-[#1b1b18] border border-transparent hover:border-[#19140035] dark:hover:border-[#3E3E3A] rounded-sm text-sm leading-normal"
|
||||
>
|
||||
Log in
|
||||
</a>
|
||||
|
||||
@if (Route::has('register'))
|
||||
<a
|
||||
href="{{ route('register') }}"
|
||||
class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] border-[#19140035] hover:border-[#1915014a] border text-[#1b1b18] dark:border-[#3E3E3A] dark:hover:border-[#62605b] rounded-sm text-sm leading-normal">
|
||||
Register
|
||||
</a>
|
||||
@endif
|
||||
@endauth
|
||||
</nav>
|
||||
@endif
|
||||
</header>
|
||||
<div class="flex items-center justify-center w-full transition-opacity opacity-100 duration-750 lg:grow starting:opacity-0">
|
||||
<main class="flex max-w-[335px] w-full flex-col-reverse lg:max-w-4xl lg:flex-row">
|
||||
<div class="text-[13px] leading-[20px] flex-1 p-6 pb-12 lg:p-20 bg-white dark:bg-[#161615] dark:text-[#EDEDEC] shadow-[inset_0px_0px_0px_1px_rgba(26,26,0,0.16)] dark:shadow-[inset_0px_0px_0px_1px_#fffaed2d] rounded-bl-lg rounded-br-lg lg:rounded-tl-lg lg:rounded-br-none">
|
||||
<h1 class="mb-1 font-medium">Let's get started</h1>
|
||||
<p class="mb-2 text-[#706f6c] dark:text-[#A1A09A]">Laravel has an incredibly rich ecosystem. <br>We suggest starting with the following.</p>
|
||||
<ul class="flex flex-col mb-4 lg:mb-6">
|
||||
<li class="flex items-center gap-4 py-2 relative before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A] before:top-1/2 before:bottom-0 before:left-[0.4rem] before:absolute">
|
||||
<span class="relative py-1 bg-white dark:bg-[#161615]">
|
||||
<span class="flex items-center justify-center rounded-full bg-[#FDFDFC] dark:bg-[#161615] shadow-[0px_0px_1px_0px_rgba(0,0,0,0.03),0px_1px_2px_0px_rgba(0,0,0,0.06)] w-3.5 h-3.5 border dark:border-[#3E3E3A] border-[#e3e3e0]">
|
||||
<span class="rounded-full bg-[#dbdbd7] dark:bg-[#3E3E3A] w-1.5 h-1.5"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
Read the
|
||||
<a href="https://laravel.com/docs" target="_blank" class="inline-flex items-center space-x-1 font-medium underline underline-offset-4 text-[#f53003] dark:text-[#FF4433] ml-1">
|
||||
<span>Documentation</span>
|
||||
<svg
|
||||
width="10"
|
||||
height="11"
|
||||
viewBox="0 0 10 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-2.5 h-2.5"
|
||||
>
|
||||
<path
|
||||
d="M7.70833 6.95834V2.79167H3.54167M2.5 8L7.5 3.00001"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-4 py-2 relative before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A] before:bottom-1/2 before:top-0 before:left-[0.4rem] before:absolute">
|
||||
<span class="relative py-1 bg-white dark:bg-[#161615]">
|
||||
<span class="flex items-center justify-center rounded-full bg-[#FDFDFC] dark:bg-[#161615] shadow-[0px_0px_1px_0px_rgba(0,0,0,0.03),0px_1px_2px_0px_rgba(0,0,0,0.06)] w-3.5 h-3.5 border dark:border-[#3E3E3A] border-[#e3e3e0]">
|
||||
<span class="rounded-full bg-[#dbdbd7] dark:bg-[#3E3E3A] w-1.5 h-1.5"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
Watch video tutorials at
|
||||
<a href="https://laracasts.com" target="_blank" class="inline-flex items-center space-x-1 font-medium underline underline-offset-4 text-[#f53003] dark:text-[#FF4433] ml-1">
|
||||
<span>Laracasts</span>
|
||||
<svg
|
||||
width="10"
|
||||
height="11"
|
||||
viewBox="0 0 10 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-2.5 h-2.5"
|
||||
>
|
||||
<path
|
||||
d="M7.70833 6.95834V2.79167H3.54167M2.5 8L7.5 3.00001"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="flex gap-3 text-sm leading-normal">
|
||||
<li>
|
||||
<a href="https://cloud.laravel.com" target="_blank" class="inline-block dark:bg-[#eeeeec] dark:border-[#eeeeec] dark:text-[#1C1C1A] dark:hover:bg-white dark:hover:border-white hover:bg-black hover:border-black px-5 py-1.5 bg-[#1b1b18] rounded-sm border border-black text-white text-sm leading-normal">
|
||||
Deploy now
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bg-[#fff2f2] dark:bg-[#1D0002] relative lg:-ml-px -mb-px lg:mb-0 rounded-t-lg lg:rounded-t-none lg:rounded-r-lg aspect-[335/376] lg:aspect-auto w-full lg:w-[438px] shrink-0 overflow-hidden">
|
||||
{{-- Laravel Logo --}}
|
||||
<svg class="w-full text-[#F53003] dark:text-[#F61500] transition-all translate-y-0 opacity-100 max-w-none duration-750 starting:opacity-0 starting:translate-y-6" viewBox="0 0 438 104" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.2036 -3H0V102.197H49.5189V86.7187H17.2036V-3Z" fill="currentColor" />
|
||||
<path d="M110.256 41.6337C108.061 38.1275 104.945 35.3731 100.905 33.3681C96.8667 31.3647 92.8016 30.3618 88.7131 30.3618C83.4247 30.3618 78.5885 31.3389 74.201 33.2923C69.8111 35.2456 66.0474 37.928 62.9059 41.3333C59.7643 44.7401 57.3198 48.6726 55.5754 53.1293C53.8287 57.589 52.9572 62.274 52.9572 67.1813C52.9572 72.1925 53.8287 76.8995 55.5754 81.3069C57.3191 85.7173 59.7636 89.6241 62.9059 93.0293C66.0474 96.4361 69.8119 99.1155 74.201 101.069C78.5885 103.022 83.4247 103.999 88.7131 103.999C92.8016 103.999 96.8667 102.997 100.905 100.994C104.945 98.9911 108.061 96.2359 110.256 92.7282V102.195H126.563V32.1642H110.256V41.6337ZM108.76 75.7472C107.762 78.4531 106.366 80.8078 104.572 82.8112C102.776 84.8161 100.606 86.4183 98.0637 87.6206C95.5202 88.823 92.7004 89.4238 89.6103 89.4238C86.5178 89.4238 83.7252 88.823 81.2324 87.6206C78.7388 86.4183 76.5949 84.8161 74.7998 82.8112C73.004 80.8078 71.6319 78.4531 70.6856 75.7472C69.7356 73.0421 69.2644 70.1868 69.2644 67.1821C69.2644 64.1758 69.7356 61.3205 70.6856 58.6154C71.6319 55.9102 73.004 53.5571 74.7998 51.5522C76.5949 49.5495 78.738 47.9451 81.2324 46.7427C83.7252 45.5404 86.5178 44.9396 89.6103 44.9396C92.7012 44.9396 95.5202 45.5404 98.0637 46.7427C100.606 47.9451 102.776 49.5487 104.572 51.5522C106.367 53.5571 107.762 55.9102 108.76 58.6154C109.756 61.3205 110.256 64.1758 110.256 67.1821C110.256 70.1868 109.756 73.0421 108.76 75.7472Z" fill="currentColor" />
|
||||
<path d="M242.805 41.6337C240.611 38.1275 237.494 35.3731 233.455 33.3681C229.416 31.3647 225.351 30.3618 221.262 30.3618C215.974 30.3618 211.138 31.3389 206.75 33.2923C202.36 35.2456 198.597 37.928 195.455 41.3333C192.314 44.7401 189.869 48.6726 188.125 53.1293C186.378 57.589 185.507 62.274 185.507 67.1813C185.507 72.1925 186.378 76.8995 188.125 81.3069C189.868 85.7173 192.313 89.6241 195.455 93.0293C198.597 96.4361 202.361 99.1155 206.75 101.069C211.138 103.022 215.974 103.999 221.262 103.999C225.351 103.999 229.416 102.997 233.455 100.994C237.494 98.9911 240.611 96.2359 242.805 92.7282V102.195H259.112V32.1642H242.805V41.6337ZM241.31 75.7472C240.312 78.4531 238.916 80.8078 237.122 82.8112C235.326 84.8161 233.156 86.4183 230.614 87.6206C228.07 88.823 225.251 89.4238 222.16 89.4238C219.068 89.4238 216.275 88.823 213.782 87.6206C211.289 86.4183 209.145 84.8161 207.35 82.8112C205.554 80.8078 204.182 78.4531 203.236 75.7472C202.286 73.0421 201.814 70.1868 201.814 67.1821C201.814 64.1758 202.286 61.3205 203.236 58.6154C204.182 55.9102 205.554 53.5571 207.35 51.5522C209.145 49.5495 211.288 47.9451 213.782 46.7427C216.275 45.5404 219.068 44.9396 222.16 44.9396C225.251 44.9396 228.07 45.5404 230.614 46.7427C233.156 47.9451 235.326 49.5487 237.122 51.5522C238.917 53.5571 240.312 55.9102 241.31 58.6154C242.306 61.3205 242.806 64.1758 242.806 67.1821C242.805 70.1868 242.305 73.0421 241.31 75.7472Z" fill="currentColor" />
|
||||
<path d="M438 -3H421.694V102.197H438V-3Z" fill="currentColor" />
|
||||
<path d="M139.43 102.197H155.735V48.2834H183.712V32.1665H139.43V102.197Z" fill="currentColor" />
|
||||
<path d="M324.49 32.1665L303.995 85.794L283.498 32.1665H266.983L293.748 102.197H314.242L341.006 32.1665H324.49Z" fill="currentColor" />
|
||||
<path d="M376.571 30.3656C356.603 30.3656 340.797 46.8497 340.797 67.1828C340.797 89.6597 356.094 104 378.661 104C391.29 104 399.354 99.1488 409.206 88.5848L398.189 80.0226C398.183 80.031 389.874 90.9895 377.468 90.9895C363.048 90.9895 356.977 79.3111 356.977 73.269H411.075C413.917 50.1328 398.775 30.3656 376.571 30.3656ZM357.02 61.0967C357.145 59.7487 359.023 43.3761 376.442 43.3761C393.861 43.3761 395.978 59.7464 396.099 61.0967H357.02Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
||||
{{-- Light Mode 12 SVG --}}
|
||||
<svg class="w-[448px] max-w-none relative -mt-[4.9rem] -ml-8 lg:ml-0 lg:-mt-[6.6rem] dark:hidden" viewBox="0 0 440 376" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="transition-all delay-300 translate-y-0 opacity-100 duration-750 starting:opacity-0 starting:translate-y-4">
|
||||
<path d="M188.263 355.73L188.595 355.73C195.441 348.845 205.766 339.761 219.569 328.477C232.93 317.193 242.978 308.205 249.714 301.511C256.34 294.626 260.867 287.358 263.296 279.708C265.725 272.058 264.565 264.121 259.816 255.896C254.516 246.716 247.062 239.352 237.454 233.805C227.957 228.067 217.908 225.198 207.307 225.198C196.927 225.197 190.136 227.97 186.934 233.516C183.621 238.872 184.726 246.331 190.247 255.894L125.647 255.891C116.371 239.825 112.395 225.481 113.72 212.858C115.265 200.235 121.559 190.481 132.602 183.596C143.754 176.52 158.607 172.982 177.159 172.983C196.594 172.984 215.863 176.523 234.968 183.6C253.961 190.486 271.299 200.241 286.98 212.864C302.661 225.488 315.14 239.833 324.416 255.899C333.03 270.817 336.841 283.918 335.847 295.203C335.075 306.487 331.376 316.336 324.75 324.751C318.346 333.167 308.408 343.494 294.936 355.734L377.094 355.737L405.917 405.656L217.087 405.649L188.263 355.73Z" fill="black" />
|
||||
<path d="M9.11884 226.339L-13.7396 226.338L-42.7286 176.132L43.0733 176.135L175.595 405.649L112.651 405.647L9.11884 226.339Z" fill="black" />
|
||||
<path d="M188.263 355.73L188.595 355.73C195.441 348.845 205.766 339.761 219.569 328.477C232.93 317.193 242.978 308.205 249.714 301.511C256.34 294.626 260.867 287.358 263.296 279.708C265.725 272.058 264.565 264.121 259.816 255.896C254.516 246.716 247.062 239.352 237.454 233.805C227.957 228.067 217.908 225.198 207.307 225.198C196.927 225.197 190.136 227.97 186.934 233.516C183.621 238.872 184.726 246.331 190.247 255.894L125.647 255.891C116.371 239.825 112.395 225.481 113.72 212.858C115.265 200.235 121.559 190.481 132.602 183.596C143.754 176.52 158.607 172.982 177.159 172.983C196.594 172.984 215.863 176.523 234.968 183.6C253.961 190.486 271.299 200.241 286.98 212.864C302.661 225.488 315.14 239.833 324.416 255.899C333.03 270.817 336.841 283.918 335.847 295.203C335.075 306.487 331.376 316.336 324.75 324.751C318.346 333.167 308.408 343.494 294.936 355.734L377.094 355.737L405.917 405.656L217.087 405.649L188.263 355.73Z" stroke="#1B1B18" stroke-width="1" />
|
||||
<path d="M9.11884 226.339L-13.7396 226.338L-42.7286 176.132L43.0733 176.135L175.595 405.649L112.651 405.647L9.11884 226.339Z" stroke="#1B1B18" stroke-width="1" />
|
||||
<path d="M204.592 327.449L204.923 327.449C211.769 320.564 222.094 311.479 235.897 300.196C249.258 288.912 259.306 279.923 266.042 273.23C272.668 266.345 277.195 259.077 279.624 251.427C282.053 243.777 280.893 235.839 276.145 227.615C270.844 218.435 263.39 211.071 253.782 205.524C244.285 199.786 234.236 196.917 223.635 196.916C213.255 196.916 206.464 199.689 203.262 205.235C199.949 210.59 201.054 218.049 206.575 227.612L141.975 227.61C132.699 211.544 128.723 197.2 130.048 184.577C131.593 171.954 137.887 162.2 148.93 155.315C160.083 148.239 174.935 144.701 193.487 144.702C212.922 144.703 232.192 148.242 251.296 155.319C270.289 162.205 287.627 171.96 303.308 184.583C318.989 197.207 331.468 211.552 340.745 227.618C349.358 242.536 353.169 255.637 352.175 266.921C351.403 278.205 347.704 288.055 341.078 296.47C334.674 304.885 324.736 315.213 311.264 327.453L393.422 327.456L422.246 377.375L233.415 377.368L204.592 327.449Z" fill="#F8B803" />
|
||||
<path d="M25.447 198.058L2.58852 198.057L-26.4005 147.851L59.4015 147.854L191.923 377.368L128.979 377.365L25.447 198.058Z" fill="#F8B803" />
|
||||
<path d="M204.592 327.449L204.923 327.449C211.769 320.564 222.094 311.479 235.897 300.196C249.258 288.912 259.306 279.923 266.042 273.23C272.668 266.345 277.195 259.077 279.624 251.427C282.053 243.777 280.893 235.839 276.145 227.615C270.844 218.435 263.39 211.071 253.782 205.524C244.285 199.786 234.236 196.917 223.635 196.916C213.255 196.916 206.464 199.689 203.262 205.235C199.949 210.59 201.054 218.049 206.575 227.612L141.975 227.61C132.699 211.544 128.723 197.2 130.048 184.577C131.593 171.954 137.887 162.2 148.93 155.315C160.083 148.239 174.935 144.701 193.487 144.702C212.922 144.703 232.192 148.242 251.296 155.319C270.289 162.205 287.627 171.96 303.308 184.583C318.989 197.207 331.468 211.552 340.745 227.618C349.358 242.536 353.169 255.637 352.175 266.921C351.403 278.205 347.704 288.055 341.078 296.47C334.674 304.885 324.736 315.213 311.264 327.453L393.422 327.456L422.246 377.375L233.415 377.368L204.592 327.449Z" stroke="#1B1B18" stroke-width="1" />
|
||||
<path d="M25.447 198.058L2.58852 198.057L-26.4005 147.851L59.4015 147.854L191.923 377.368L128.979 377.365L25.447 198.058Z" stroke="#1B1B18" stroke-width="1" />
|
||||
</g>
|
||||
<g style="mix-blend-mode: hard-light" class="transition-all delay-300 translate-y-0 opacity-100 duration-750 starting:opacity-0 starting:translate-y-4">
|
||||
<path d="M217.342 305.363L217.673 305.363C224.519 298.478 234.844 289.393 248.647 278.11C262.008 266.826 272.056 257.837 278.792 251.144C285.418 244.259 289.945 236.991 292.374 229.341C294.803 221.691 293.643 213.753 288.895 205.529C283.594 196.349 276.14 188.985 266.532 183.438C257.035 177.7 246.986 174.831 236.385 174.83C226.005 174.83 219.214 177.603 216.012 183.149C212.699 188.504 213.804 195.963 219.325 205.527L154.725 205.524C145.449 189.458 141.473 175.114 142.798 162.491C144.343 149.868 150.637 140.114 161.68 133.229C172.833 126.153 187.685 122.615 206.237 122.616C225.672 122.617 244.942 126.156 264.046 133.233C283.039 140.119 300.377 149.874 316.058 162.497C331.739 175.121 344.218 189.466 353.495 205.532C362.108 220.45 365.919 233.551 364.925 244.835C364.153 256.12 360.454 265.969 353.828 274.384C347.424 282.799 337.486 293.127 324.014 305.367L406.172 305.37L434.996 355.289L246.165 355.282L217.342 305.363Z" fill="#F0ACB8" />
|
||||
<path d="M38.197 175.972L15.3385 175.971L-13.6505 125.765L72.1515 125.768L204.673 355.282L141.729 355.279L38.197 175.972Z" fill="#F0ACB8" />
|
||||
<path d="M217.342 305.363L217.673 305.363C224.519 298.478 234.844 289.393 248.647 278.11C262.008 266.826 272.056 257.837 278.792 251.144C285.418 244.259 289.945 236.991 292.374 229.341C294.803 221.691 293.643 213.753 288.895 205.529C283.594 196.349 276.14 188.985 266.532 183.438C257.035 177.7 246.986 174.831 236.385 174.83C226.005 174.83 219.214 177.603 216.012 183.149C212.699 188.504 213.804 195.963 219.325 205.527L154.725 205.524C145.449 189.458 141.473 175.114 142.798 162.491C144.343 149.868 150.637 140.114 161.68 133.229C172.833 126.153 187.685 122.615 206.237 122.616C225.672 122.617 244.942 126.156 264.046 133.233C283.039 140.119 300.377 149.874 316.058 162.497C331.739 175.121 344.218 189.466 353.495 205.532C362.108 220.45 365.919 233.551 364.925 244.835C364.153 256.12 360.454 265.969 353.828 274.384C347.424 282.799 337.486 293.127 324.014 305.367L406.172 305.37L434.996 355.289L246.165 355.282L217.342 305.363Z" stroke="#1B1B18" stroke-width="1" />
|
||||
<path d="M38.197 175.972L15.3385 175.971L-13.6505 125.765L72.1515 125.768L204.673 355.282L141.729 355.279L38.197 175.972Z" stroke="#1B1B18" stroke-width="1" />
|
||||
</g>
|
||||
<g style="mix-blend-mode: plus-darker" class="transition-all delay-300 translate-y-0 opacity-100 duration-750 starting:opacity-0 starting:translate-y-4">
|
||||
<path d="M230.951 281.792L231.282 281.793C238.128 274.907 248.453 265.823 262.256 254.539C275.617 243.256 285.666 234.267 292.402 227.573C299.027 220.688 303.554 213.421 305.983 205.771C308.412 198.12 307.253 190.183 302.504 181.959C297.203 172.778 289.749 165.415 280.142 159.868C270.645 154.13 260.596 151.26 249.995 151.26C239.615 151.26 232.823 154.033 229.621 159.579C226.309 164.934 227.413 172.393 232.935 181.956L168.335 181.954C159.058 165.888 155.082 151.543 156.407 138.92C157.953 126.298 164.247 116.544 175.289 109.659C186.442 102.583 201.294 99.045 219.846 99.0457C239.281 99.0464 258.551 102.585 277.655 109.663C296.649 116.549 313.986 126.303 329.667 138.927C345.349 151.551 357.827 165.895 367.104 181.961C375.718 196.88 379.528 209.981 378.535 221.265C377.762 232.549 374.063 242.399 367.438 250.814C361.033 259.229 351.095 269.557 337.624 281.796L419.782 281.8L448.605 331.719L259.774 331.712L230.951 281.792Z" fill="#F3BEC7" />
|
||||
<path d="M51.8063 152.402L28.9479 152.401L-0.0411453 102.195L85.7608 102.198L218.282 331.711L155.339 331.709L51.8063 152.402Z" fill="#F3BEC7" />
|
||||
<path d="M230.951 281.792L231.282 281.793C238.128 274.907 248.453 265.823 262.256 254.539C275.617 243.256 285.666 234.267 292.402 227.573C299.027 220.688 303.554 213.421 305.983 205.771C308.412 198.12 307.253 190.183 302.504 181.959C297.203 172.778 289.749 165.415 280.142 159.868C270.645 154.13 260.596 151.26 249.995 151.26C239.615 151.26 232.823 154.033 229.621 159.579C226.309 164.934 227.413 172.393 232.935 181.956L168.335 181.954C159.058 165.888 155.082 151.543 156.407 138.92C157.953 126.298 164.247 116.544 175.289 109.659C186.442 102.583 201.294 99.045 219.846 99.0457C239.281 99.0464 258.551 102.585 277.655 109.663C296.649 116.549 313.986 126.303 329.667 138.927C345.349 151.551 357.827 165.895 367.104 181.961C375.718 196.88 379.528 209.981 378.535 221.265C377.762 232.549 374.063 242.399 367.438 250.814C361.033 259.229 351.095 269.557 337.624 281.796L419.782 281.8L448.605 331.719L259.774 331.712L230.951 281.792Z" stroke="#1B1B18" stroke-width="1" />
|
||||
<path d="M51.8063 152.402L28.9479 152.401L-0.0411453 102.195L85.7608 102.198L218.282 331.711L155.339 331.709L51.8063 152.402Z" stroke="#1B1B18" stroke-width="1" />
|
||||
</g>
|
||||
<g class="transition-all delay-300 translate-y-0 opacity-100 duration-750 starting:opacity-0 starting:translate-y-4">
|
||||
<path d="M188.467 355.363L188.798 355.363C195.644 348.478 205.969 339.393 219.772 328.11C233.133 316.826 243.181 307.837 249.917 301.144C253.696 297.217 256.792 293.166 259.205 288.991C261.024 285.845 262.455 282.628 263.499 279.341C265.928 271.691 264.768 263.753 260.02 255.529C254.719 246.349 247.265 238.985 237.657 233.438C228.16 227.7 218.111 224.831 207.51 224.83C197.13 224.83 190.339 227.603 187.137 233.149C183.824 238.504 184.929 245.963 190.45 255.527L125.851 255.524C116.574 239.458 112.598 225.114 113.923 212.491C114.615 206.836 116.261 201.756 118.859 197.253C122.061 191.704 126.709 187.03 132.805 183.229C143.958 176.153 158.81 172.615 177.362 172.616C196.797 172.617 216.067 176.156 235.171 183.233C254.164 190.119 271.502 199.874 287.183 212.497C302.864 225.121 315.343 239.466 324.62 255.532C333.233 270.45 337.044 283.551 336.05 294.835C335.46 303.459 333.16 311.245 329.151 318.194C327.915 320.337 326.515 322.4 324.953 324.384C318.549 332.799 308.611 343.127 295.139 355.367L377.297 355.37L406.121 405.289L217.29 405.282L188.467 355.363Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M9.32197 225.972L-13.5365 225.971L-42.5255 175.765L43.2765 175.768L175.798 405.282L112.854 405.279L9.32197 225.972Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M345.247 111.915C329.566 99.2919 312.229 89.5371 293.235 82.6512L235.167 183.228C254.161 190.114 271.498 199.869 287.179 212.492L345.247 111.915Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M382.686 154.964C373.41 138.898 360.931 124.553 345.25 111.93L287.182 212.506C302.863 225.13 315.342 239.475 324.618 255.541L382.686 154.964Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M293.243 82.6472C274.139 75.57 254.869 72.031 235.434 72.0303L177.366 172.607C196.801 172.608 216.071 176.147 235.175 183.224L293.243 82.6472Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M394.118 194.257C395.112 182.973 391.301 169.872 382.688 154.953L324.619 255.53C333.233 270.448 337.044 283.55 336.05 294.834L394.118 194.257Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M235.432 72.0311C216.88 72.0304 202.027 75.5681 190.875 82.6442L132.806 183.221C143.959 176.145 158.812 172.607 177.363 172.608L235.432 72.0311Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M265.59 124.25C276.191 124.251 286.24 127.12 295.737 132.858L237.669 233.435C228.172 227.697 218.123 224.828 207.522 224.827L265.59 124.25Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M295.719 132.859C305.326 138.406 312.78 145.77 318.081 154.95L260.013 255.527C254.712 246.347 247.258 238.983 237.651 233.436L295.719 132.859Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M387.218 217.608C391.227 210.66 393.527 202.874 394.117 194.25L336.049 294.827C335.459 303.451 333.159 311.237 329.15 318.185L387.218 217.608Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M245.211 132.577C248.413 127.03 255.204 124.257 265.584 124.258L207.516 224.835C197.136 224.834 190.345 227.607 187.143 233.154L245.211 132.577Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M318.094 154.945C322.842 163.17 324.002 171.107 321.573 178.757L263.505 279.334C265.934 271.684 264.774 263.746 260.026 255.522L318.094 154.945Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M176.925 96.6737C180.127 91.1249 184.776 86.4503 190.871 82.6499L132.803 183.227C126.708 187.027 122.059 191.702 118.857 197.25L176.925 96.6737Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M387.226 217.606C385.989 219.749 384.59 221.813 383.028 223.797L324.96 324.373C326.522 322.39 327.921 320.326 329.157 318.183L387.226 217.606Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M317.269 188.408C319.087 185.262 320.519 182.045 321.562 178.758L263.494 279.335C262.451 282.622 261.019 285.839 259.201 288.985L317.269 188.408Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M245.208 132.573C241.895 137.928 243 145.387 248.522 154.95L190.454 255.527C184.932 245.964 183.827 238.505 187.14 233.15L245.208 132.573Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M176.93 96.6719C174.331 101.175 172.686 106.255 171.993 111.91L113.925 212.487C114.618 206.831 116.263 201.752 118.862 197.249L176.93 96.6719Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M317.266 188.413C314.853 192.589 311.757 196.64 307.978 200.566L249.91 301.143C253.689 297.216 256.785 293.166 259.198 288.99L317.266 188.413Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M464.198 304.708L435.375 254.789L377.307 355.366L406.13 405.285L464.198 304.708Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M353.209 254.787C366.68 242.548 376.618 232.22 383.023 223.805L324.955 324.382C318.55 332.797 308.612 343.124 295.141 355.364L353.209 254.787Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M435.37 254.787L353.212 254.784L295.144 355.361L377.302 355.364L435.37 254.787Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M183.921 154.947L248.521 154.95L190.453 255.527L125.853 255.524L183.921 154.947Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M171.992 111.914C170.668 124.537 174.643 138.881 183.92 154.947L125.852 255.524C116.575 239.458 112.599 225.114 113.924 212.491L171.992 111.914Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M307.987 200.562C301.251 207.256 291.203 216.244 277.842 227.528L219.774 328.105C233.135 316.821 243.183 307.832 249.919 301.139L307.987 200.562Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M15.5469 75.1797L44.5359 125.386L-13.5321 225.963L-42.5212 175.756L15.5469 75.1797Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M277.836 227.536C264.033 238.82 253.708 247.904 246.862 254.789L188.794 355.366C195.64 348.481 205.965 339.397 219.768 328.113L277.836 227.536Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M275.358 304.706L464.189 304.713L406.12 405.29L217.29 405.283L275.358 304.706Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M44.5279 125.39L67.3864 125.39L9.31834 225.967L-13.5401 225.966L44.5279 125.39Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M101.341 75.1911L233.863 304.705L175.795 405.282L43.2733 175.768L101.341 75.1911ZM15.5431 75.19L-42.525 175.767L43.277 175.77L101.345 75.1932L15.5431 75.19Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M246.866 254.784L246.534 254.784L188.466 355.361L188.798 355.361L246.866 254.784Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M246.539 254.781L275.362 304.701L217.294 405.277L188.471 355.358L246.539 254.781Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M67.3906 125.391L170.923 304.698L112.855 405.275L9.32257 225.967L67.3906 125.391Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
<path d="M170.921 304.699L233.865 304.701L175.797 405.278L112.853 405.276L170.921 304.699Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="bevel" />
|
||||
</g>
|
||||
<g style="mix-blend-mode: hard-light" class="transition-all delay-300 translate-y-0 opacity-100 duration-750 starting:opacity-0 starting:translate-y-4">
|
||||
<path d="M246.544 254.79L246.875 254.79C253.722 247.905 264.046 238.82 277.849 227.537C291.21 216.253 301.259 207.264 307.995 200.57C314.62 193.685 319.147 186.418 321.577 178.768C324.006 171.117 322.846 163.18 318.097 154.956C312.796 145.775 305.342 138.412 295.735 132.865C286.238 127.127 276.189 124.258 265.588 124.257C255.208 124.257 248.416 127.03 245.214 132.576C241.902 137.931 243.006 145.39 248.528 154.953L183.928 154.951C174.652 138.885 170.676 124.541 172 111.918C173.546 99.2946 179.84 89.5408 190.882 82.6559C202.035 75.5798 216.887 72.0421 235.439 72.0428C254.874 72.0435 274.144 75.5825 293.248 82.6598C312.242 89.5457 329.579 99.3005 345.261 111.924C360.942 124.548 373.421 138.892 382.697 154.958C391.311 169.877 395.121 182.978 394.128 194.262C393.355 205.546 389.656 215.396 383.031 223.811C376.627 232.226 366.688 242.554 353.217 254.794L435.375 254.797L464.198 304.716L275.367 304.709L246.544 254.79Z" fill="#F0ACB8" />
|
||||
<path d="M246.544 254.79L246.875 254.79C253.722 247.905 264.046 238.82 277.849 227.537C291.21 216.253 301.259 207.264 307.995 200.57C314.62 193.685 319.147 186.418 321.577 178.768C324.006 171.117 322.846 163.18 318.097 154.956C312.796 145.775 305.342 138.412 295.735 132.865C286.238 127.127 276.189 124.258 265.588 124.257C255.208 124.257 248.416 127.03 245.214 132.576C241.902 137.931 243.006 145.39 248.528 154.953L183.928 154.951C174.652 138.885 170.676 124.541 172 111.918C173.546 99.2946 179.84 89.5408 190.882 82.6559C202.035 75.5798 216.887 72.0421 235.439 72.0428C254.874 72.0435 274.144 75.5825 293.248 82.6598C312.242 89.5457 329.579 99.3005 345.261 111.924C360.942 124.548 373.421 138.892 382.697 154.958C391.311 169.877 395.121 182.978 394.128 194.262C393.355 205.546 389.656 215.396 383.031 223.811C376.627 232.226 366.688 242.554 353.217 254.794L435.375 254.797L464.198 304.716L275.367 304.709L246.544 254.79Z" stroke="#1B1B18" stroke-width="1" stroke-linejoin="round" />
|
||||
</g>
|
||||
<g style="mix-blend-mode: hard-light" class="transition-all delay-300 translate-y-0 opacity-100 duration-750 starting:opacity-0 starting:translate-y-4">
|
||||
<path d="M67.41 125.402L44.5515 125.401L15.5625 75.1953L101.364 75.1985L233.886 304.712L170.942 304.71L67.41 125.402Z" fill="#F0ACB8" />
|
||||
<path d="M67.41 125.402L44.5515 125.401L15.5625 75.1953L101.364 75.1985L233.886 304.712L170.942 304.71L67.41 125.402Z" stroke="#1B1B18" stroke-width="1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
{{-- Dark Mode 12 SVG --}}
|
||||
<svg class="w-[448px] max-w-none relative -mt-[4.9rem] -ml-8 lg:ml-0 lg:-mt-[6.6rem] hidden dark:block" viewBox="0 0 440 376" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="transition-all delay-300 translate-y-0 opacity-100 duration-750 starting:opacity-0 starting:translate-y-4">
|
||||
<path d="M188.263 355.73L188.595 355.73C195.441 348.845 205.766 339.761 219.569 328.477C232.93 317.193 242.978 308.205 249.714 301.511C256.34 294.626 260.867 287.358 263.296 279.708C265.725 272.058 264.565 264.121 259.816 255.896C254.516 246.716 247.062 239.352 237.454 233.805C227.957 228.067 217.908 225.198 207.307 225.198C196.927 225.197 190.136 227.97 186.934 233.516C183.621 238.872 184.726 246.331 190.247 255.894L125.647 255.891C116.371 239.825 112.395 225.481 113.72 212.858C115.265 200.235 121.559 190.481 132.602 183.596C143.754 176.52 158.607 172.982 177.159 172.983C196.594 172.984 215.863 176.523 234.968 183.6C253.961 190.486 271.299 200.241 286.98 212.864C302.661 225.488 315.14 239.833 324.416 255.899C333.03 270.817 336.841 283.918 335.847 295.203C335.075 306.487 331.376 316.336 324.75 324.751C318.346 333.167 308.408 343.494 294.936 355.734L377.094 355.737L405.917 405.656L217.087 405.649L188.263 355.73Z" fill="black"/>
|
||||
<path d="M9.11884 226.339L-13.7396 226.338L-42.7286 176.132L43.0733 176.135L175.595 405.649L112.651 405.647L9.11884 226.339Z" fill="black"/>
|
||||
<path d="M188.263 355.73L188.595 355.73C195.441 348.845 205.766 339.761 219.569 328.477C232.93 317.193 242.978 308.205 249.714 301.511C256.34 294.626 260.867 287.358 263.296 279.708C265.725 272.058 264.565 264.121 259.816 255.896C254.516 246.716 247.062 239.352 237.454 233.805C227.957 228.067 217.908 225.198 207.307 225.198C196.927 225.197 190.136 227.97 186.934 233.516C183.621 238.872 184.726 246.331 190.247 255.894L125.647 255.891C116.371 239.825 112.395 225.481 113.72 212.858C115.265 200.235 121.559 190.481 132.602 183.596C143.754 176.52 158.607 172.982 177.159 172.983C196.594 172.984 215.863 176.523 234.968 183.6C253.961 190.486 271.299 200.241 286.98 212.864C302.661 225.488 315.14 239.833 324.416 255.899C333.03 270.817 336.841 283.918 335.847 295.203C335.075 306.487 331.376 316.336 324.75 324.751C318.346 333.167 308.408 343.494 294.936 355.734L377.094 355.737L405.917 405.656L217.087 405.649L188.263 355.73Z" stroke="#FF750F" stroke-width="1"/>
|
||||
<path d="M9.11884 226.339L-13.7396 226.338L-42.7286 176.132L43.0733 176.135L175.595 405.649L112.651 405.647L9.11884 226.339Z" stroke="#FF750F" stroke-width="1"/>
|
||||
<path d="M204.592 327.449L204.923 327.449C211.769 320.564 222.094 311.479 235.897 300.196C249.258 288.912 259.306 279.923 266.042 273.23C272.668 266.345 277.195 259.077 279.624 251.427C282.053 243.777 280.893 235.839 276.145 227.615C270.844 218.435 263.39 211.071 253.782 205.524C244.285 199.786 234.236 196.917 223.635 196.916C213.255 196.916 206.464 199.689 203.262 205.235C199.949 210.59 201.054 218.049 206.575 227.612L141.975 227.61C132.699 211.544 128.723 197.2 130.048 184.577C131.593 171.954 137.887 162.2 148.93 155.315C160.083 148.239 174.935 144.701 193.487 144.702C212.922 144.703 232.192 148.242 251.296 155.319C270.289 162.205 287.627 171.96 303.308 184.583C318.989 197.207 331.468 211.552 340.745 227.618C349.358 242.536 353.169 255.637 352.175 266.921C351.403 278.205 347.704 288.055 341.078 296.47C334.674 304.885 324.736 315.213 311.264 327.453L393.422 327.456L422.246 377.375L233.415 377.368L204.592 327.449Z" fill="#391800"/>
|
||||
<path d="M25.447 198.058L2.58852 198.057L-26.4005 147.851L59.4015 147.854L191.923 377.368L128.979 377.365L25.447 198.058Z" fill="#391800"/>
|
||||
<path d="M204.592 327.449L204.923 327.449C211.769 320.564 222.094 311.479 235.897 300.196C249.258 288.912 259.306 279.923 266.042 273.23C272.668 266.345 277.195 259.077 279.624 251.427C282.053 243.777 280.893 235.839 276.145 227.615C270.844 218.435 263.39 211.071 253.782 205.524C244.285 199.786 234.236 196.917 223.635 196.916C213.255 196.916 206.464 199.689 203.262 205.235C199.949 210.59 201.054 218.049 206.575 227.612L141.975 227.61C132.699 211.544 128.723 197.2 130.048 184.577C131.593 171.954 137.887 162.2 148.93 155.315C160.083 148.239 174.935 144.701 193.487 144.702C212.922 144.703 232.192 148.242 251.296 155.319C270.289 162.205 287.627 171.96 303.308 184.583C318.989 197.207 331.468 211.552 340.745 227.618C349.358 242.536 353.169 255.637 352.175 266.921C351.403 278.205 347.704 288.055 341.078 296.47C334.674 304.885 324.736 315.213 311.264 327.453L393.422 327.456L422.246 377.375L233.415 377.368L204.592 327.449Z" stroke="#FF750F" stroke-width="1"/>
|
||||
<path d="M25.447 198.058L2.58852 198.057L-26.4005 147.851L59.4015 147.854L191.923 377.368L128.979 377.365L25.447 198.058Z" stroke="#FF750F" stroke-width="1"/>
|
||||
</g>
|
||||
<g class="transition-all delay-300 translate-y-0 opacity-100 duration-750 starting:opacity-0 starting:translate-y-4" style="mix-blend-mode:hard-light">
|
||||
<path d="M217.342 305.363L217.673 305.363C224.519 298.478 234.844 289.393 248.647 278.11C262.008 266.826 272.056 257.837 278.792 251.144C285.418 244.259 289.945 236.991 292.374 229.341C294.803 221.691 293.643 213.753 288.895 205.529C283.594 196.349 276.14 188.985 266.532 183.438C257.035 177.7 246.986 174.831 236.385 174.83C226.005 174.83 219.214 177.603 216.012 183.149C212.699 188.504 213.804 195.963 219.325 205.527L154.725 205.524C145.449 189.458 141.473 175.114 142.798 162.491C144.343 149.868 150.637 140.114 161.68 133.229C172.833 126.153 187.685 122.615 206.237 122.616C225.672 122.617 244.942 126.156 264.046 133.233C283.039 140.119 300.377 149.874 316.058 162.497C331.739 175.121 344.218 189.466 353.495 205.532C362.108 220.45 365.919 233.551 364.925 244.835C364.153 256.12 360.454 265.969 353.828 274.384C347.424 282.799 337.486 293.127 324.014 305.367L406.172 305.37L434.996 355.289L246.165 355.282L217.342 305.363Z" fill="#733000"/>
|
||||
<path d="M38.197 175.972L15.3385 175.971L-13.6505 125.765L72.1515 125.768L204.673 355.282L141.729 355.279L38.197 175.972Z" fill="#733000"/>
|
||||
<path d="M217.342 305.363L217.673 305.363C224.519 298.478 234.844 289.393 248.647 278.11C262.008 266.826 272.056 257.837 278.792 251.144C285.418 244.259 289.945 236.991 292.374 229.341C294.803 221.691 293.643 213.753 288.895 205.529C283.594 196.349 276.14 188.985 266.532 183.438C257.035 177.7 246.986 174.831 236.385 174.83C226.005 174.83 219.214 177.603 216.012 183.149C212.699 188.504 213.804 195.963 219.325 205.527L154.725 205.524C145.449 189.458 141.473 175.114 142.798 162.491C144.343 149.868 150.637 140.114 161.68 133.229C172.833 126.153 187.685 122.615 206.237 122.616C225.672 122.617 244.942 126.156 264.046 133.233C283.039 140.119 300.377 149.874 316.058 162.497C331.739 175.121 344.218 189.466 353.495 205.532C362.108 220.45 365.919 233.551 364.925 244.835C364.153 256.12 360.454 265.969 353.828 274.384C347.424 282.799 337.486 293.127 324.014 305.367L406.172 305.37L434.996 355.289L246.165 355.282L217.342 305.363Z" stroke="#FF750F" stroke-width="1"/>
|
||||
<path d="M38.197 175.972L15.3385 175.971L-13.6505 125.765L72.1515 125.768L204.673 355.282L141.729 355.279L38.197 175.972Z" stroke="#FF750F" stroke-width="1"/>
|
||||
</g>
|
||||
<g class="transition-all delay-300 translate-y-0 opacity-100 duration-750 starting:opacity-0 starting:translate-y-4">
|
||||
<path d="M217.342 305.363L217.673 305.363C224.519 298.478 234.844 289.393 248.647 278.11C262.008 266.826 272.056 257.837 278.792 251.144C285.418 244.259 289.945 236.991 292.374 229.341C294.803 221.691 293.643 213.753 288.895 205.529C283.594 196.349 276.14 188.985 266.532 183.438C257.035 177.7 246.986 174.831 236.385 174.83C226.005 174.83 219.214 177.603 216.012 183.149C212.699 188.504 213.804 195.963 219.325 205.527L154.726 205.524C145.449 189.458 141.473 175.114 142.798 162.491C144.343 149.868 150.637 140.114 161.68 133.229C172.833 126.153 187.685 122.615 206.237 122.616C225.672 122.617 244.942 126.156 264.046 133.233C283.039 140.119 300.377 149.874 316.058 162.497C331.739 175.121 344.218 189.466 353.495 205.532C362.108 220.45 365.919 233.551 364.925 244.835C364.153 256.12 360.454 265.969 353.828 274.384C347.424 282.799 337.486 293.127 324.014 305.367L406.172 305.37L434.996 355.289L246.165 355.282L217.342 305.363Z" stroke="#FF750F" stroke-width="1"/>
|
||||
<path d="M38.197 175.972L15.3385 175.971L-13.6505 125.765L72.1515 125.768L204.673 355.282L141.729 355.279L38.197 175.972Z" stroke="#FF750F" stroke-width="1"/>
|
||||
</g>
|
||||
<g class="transition-all delay-300 translate-y-0 opacity-100 duration-750 starting:opacity-0 starting:translate-y-4">
|
||||
<path d="M188.467 355.363L188.798 355.363C195.644 348.478 205.969 339.393 219.772 328.11C233.133 316.826 243.181 307.837 249.917 301.144C253.696 297.217 256.792 293.166 259.205 288.991C261.024 285.845 262.455 282.628 263.499 279.341C265.928 271.691 264.768 263.753 260.02 255.529C254.719 246.349 247.265 238.985 237.657 233.438C228.16 227.7 218.111 224.831 207.51 224.83C197.13 224.83 190.339 227.603 187.137 233.149C183.824 238.504 184.929 245.963 190.45 255.527L125.851 255.524C116.574 239.458 112.598 225.114 113.923 212.491C114.615 206.836 116.261 201.756 118.859 197.253C122.061 191.704 126.709 187.03 132.805 183.229C143.958 176.153 158.81 172.615 177.362 172.616C196.797 172.617 216.067 176.156 235.171 183.233C254.164 190.119 271.502 199.874 287.183 212.497C302.864 225.121 315.343 239.466 324.62 255.532C333.233 270.45 337.044 283.551 336.05 294.835C335.46 303.459 333.16 311.245 329.151 318.194C327.915 320.337 326.515 322.4 324.953 324.384C318.549 332.799 308.611 343.127 295.139 355.367L377.297 355.37L406.121 405.289L217.29 405.282L188.467 355.363Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M9.32197 225.972L-13.5365 225.971L-42.5255 175.765L43.2765 175.768L175.798 405.282L112.854 405.279L9.32197 225.972Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M345.247 111.915C329.566 99.2919 312.229 89.5371 293.235 82.6512L235.167 183.228C254.161 190.114 271.498 199.869 287.179 212.492L345.247 111.915Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M382.686 154.964C373.41 138.898 360.931 124.553 345.25 111.93L287.182 212.506C302.863 225.13 315.342 239.475 324.618 255.541L382.686 154.964Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M293.243 82.6472C274.139 75.57 254.869 72.031 235.434 72.0303L177.366 172.607C196.801 172.608 216.071 176.147 235.175 183.224L293.243 82.6472Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M394.118 194.257C395.112 182.973 391.301 169.872 382.688 154.953L324.619 255.53C333.233 270.448 337.044 283.55 336.05 294.834L394.118 194.257Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M235.432 72.0311C216.88 72.0304 202.027 75.5681 190.875 82.6442L132.806 183.221C143.959 176.145 158.812 172.607 177.363 172.608L235.432 72.0311Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M265.59 124.25C276.191 124.251 286.24 127.12 295.737 132.858L237.669 233.435C228.172 227.697 218.123 224.828 207.522 224.827L265.59 124.25Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M295.719 132.859C305.326 138.406 312.78 145.77 318.081 154.95L260.013 255.527C254.712 246.347 247.258 238.983 237.651 233.436L295.719 132.859Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M387.218 217.608C391.227 210.66 393.527 202.874 394.117 194.25L336.049 294.827C335.459 303.451 333.159 311.237 329.15 318.185L387.218 217.608Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M245.211 132.577C248.413 127.03 255.204 124.257 265.584 124.258L207.516 224.835C197.136 224.834 190.345 227.607 187.143 233.154L245.211 132.577Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M318.094 154.945C322.842 163.17 324.002 171.107 321.573 178.757L263.505 279.334C265.934 271.684 264.774 263.746 260.026 255.522L318.094 154.945Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M176.925 96.6737C180.127 91.1249 184.776 86.4503 190.871 82.6499L132.803 183.227C126.708 187.027 122.059 191.702 118.857 197.25L176.925 96.6737Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M387.226 217.606C385.989 219.749 384.59 221.813 383.028 223.797L324.96 324.373C326.522 322.39 327.921 320.326 329.157 318.183L387.226 217.606Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M317.269 188.408C319.087 185.262 320.519 182.045 321.562 178.758L263.494 279.335C262.451 282.622 261.019 285.839 259.201 288.985L317.269 188.408Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M245.208 132.573C241.895 137.928 243 145.387 248.522 154.95L190.454 255.527C184.932 245.964 183.827 238.505 187.14 233.15L245.208 132.573Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M176.93 96.6719C174.331 101.175 172.686 106.255 171.993 111.91L113.925 212.487C114.618 206.831 116.263 201.752 118.862 197.249L176.93 96.6719Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M317.266 188.413C314.853 192.589 311.757 196.64 307.978 200.566L249.91 301.143C253.689 297.216 256.785 293.166 259.198 288.99L317.266 188.413Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M464.198 304.708L435.375 254.789L377.307 355.366L406.13 405.285L464.198 304.708Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M353.209 254.787C366.68 242.548 376.618 232.22 383.023 223.805L324.955 324.382C318.55 332.797 308.612 343.124 295.141 355.364L353.209 254.787Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M435.37 254.787L353.212 254.784L295.144 355.361L377.302 355.364L435.37 254.787Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M183.921 154.947L248.521 154.95L190.453 255.527L125.853 255.524L183.921 154.947Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M171.992 111.914C170.668 124.537 174.643 138.881 183.92 154.947L125.852 255.524C116.575 239.458 112.599 225.114 113.924 212.491L171.992 111.914Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M307.987 200.562C301.251 207.256 291.203 216.244 277.842 227.528L219.774 328.105C233.135 316.821 243.183 307.832 249.919 301.139L307.987 200.562Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M15.5469 75.1797L44.5359 125.386L-13.5321 225.963L-42.5212 175.756L15.5469 75.1797Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M277.836 227.536C264.033 238.82 253.708 247.904 246.862 254.789L188.794 355.366C195.64 348.481 205.965 339.397 219.768 328.113L277.836 227.536Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M275.358 304.706L464.189 304.713L406.12 405.29L217.29 405.283L275.358 304.706Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M44.5279 125.39L67.3864 125.39L9.31834 225.967L-13.5401 225.966L44.5279 125.39Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M101.341 75.1911L233.863 304.705L175.795 405.282L43.2733 175.768L101.341 75.1911ZM15.5431 75.19L-42.525 175.767L43.277 175.77L101.345 75.1932L15.5431 75.19Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M246.866 254.784L246.534 254.784L188.466 355.361L188.798 355.361L246.866 254.784Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M246.539 254.781L275.362 304.701L217.294 405.277L188.471 355.358L246.539 254.781Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M67.3906 125.391L170.923 304.698L112.855 405.275L9.32257 225.967L67.3906 125.391Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
<path d="M170.921 304.699L233.865 304.701L175.797 405.278L112.853 405.276L170.921 304.699Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="bevel"/>
|
||||
</g>
|
||||
<g class="transition-all delay-300 translate-y-0 opacity-100 duration-750 starting:opacity-0 starting:translate-y-4" style="mix-blend-mode:hard-light">
|
||||
<path d="M246.544 254.79L246.875 254.79C253.722 247.905 264.046 238.82 277.849 227.537C291.21 216.253 301.259 207.264 307.995 200.57C314.62 193.685 319.147 186.418 321.577 178.768C324.006 171.117 322.846 163.18 318.097 154.956C312.796 145.775 305.342 138.412 295.735 132.865C286.238 127.127 276.189 124.258 265.588 124.257C255.208 124.257 248.416 127.03 245.214 132.576C241.902 137.931 243.006 145.39 248.528 154.953L183.928 154.951C174.652 138.885 170.676 124.541 172 111.918C173.546 99.2946 179.84 89.5408 190.882 82.6559C202.035 75.5798 216.887 72.0421 235.439 72.0428C254.874 72.0435 274.144 75.5825 293.248 82.6598C312.242 89.5457 329.579 99.3005 345.261 111.924C360.942 124.548 373.421 138.892 382.697 154.958C391.311 169.877 395.121 182.978 394.128 194.262C393.355 205.546 389.656 215.396 383.031 223.811C376.627 232.226 366.688 242.554 353.217 254.794L435.375 254.797L464.198 304.716L275.367 304.709L246.544 254.79Z" fill="#4B0600"/>
|
||||
<path d="M246.544 254.79L246.875 254.79C253.722 247.905 264.046 238.82 277.849 227.537C291.21 216.253 301.259 207.264 307.995 200.57C314.62 193.685 319.147 186.418 321.577 178.768C324.006 171.117 322.846 163.18 318.097 154.956C312.796 145.775 305.342 138.412 295.735 132.865C286.238 127.127 276.189 124.258 265.588 124.257C255.208 124.257 248.416 127.03 245.214 132.576C241.902 137.931 243.006 145.39 248.528 154.953L183.928 154.951C174.652 138.885 170.676 124.541 172 111.918C173.546 99.2946 179.84 89.5408 190.882 82.6559C202.035 75.5798 216.887 72.0421 235.439 72.0428C254.874 72.0435 274.144 75.5825 293.248 82.6598C312.242 89.5457 329.579 99.3005 345.261 111.924C360.942 124.548 373.421 138.892 382.697 154.958C391.311 169.877 395.121 182.978 394.128 194.262C393.355 205.546 389.656 215.396 383.031 223.811C376.627 232.226 366.688 242.554 353.217 254.794L435.375 254.797L464.198 304.716L275.367 304.709L246.544 254.79Z" stroke="#FF750F" stroke-width="1" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<g class="transition-all delay-300 translate-y-0 opacity-100 duration-750 starting:opacity-0 starting:translate-y-4" style="mix-blend-mode:hard-light">
|
||||
<path d="M67.41 125.402L44.5515 125.401L15.5625 75.1953L101.364 75.1985L233.886 304.712L170.942 304.71L67.41 125.402Z" fill="#4B0600"/>
|
||||
<path d="M67.41 125.402L44.5515 125.401L15.5625 75.1953L101.364 75.1985L233.886 304.712L170.942 304.71L67.41 125.402Z" stroke="#FF750F" stroke-width="1"/>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="absolute inset-0 rounded-t-lg lg:rounded-t-none lg:rounded-r-lg shadow-[inset_0px_0px_0px_1px_rgba(26,26,0,0.16)] dark:shadow-[inset_0px_0px_0px_1px_#fffaed2d]"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@if (Route::has('login'))
|
||||
<div class="h-14.5 hidden lg:block"></div>
|
||||
@endif
|
||||
</body>
|
||||
</html>
|
||||
8
routes/console.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
85
routes/web.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\HomeController;
|
||||
use App\Http\Controllers\MovieController;
|
||||
use App\Http\Controllers\TvShowController;
|
||||
use App\Http\Controllers\SearchController;
|
||||
use App\Http\Controllers\SitemapController;
|
||||
use App\Http\Controllers\ProxyController;
|
||||
use App\Http\Controllers\StatsController;
|
||||
use App\Http\Controllers\AnalyticsProxyController;
|
||||
|
||||
// Homepage routes
|
||||
Route::get('/', [HomeController::class, 'index'])->name('home');
|
||||
Route::get('/movies', [HomeController::class, 'movies'])->name('movies.index');
|
||||
Route::get('/tv-shows', [HomeController::class, 'tvShows'])->name('tv-shows.index');
|
||||
// Hero details API (production companies & countries per item)
|
||||
Route::get('/api/hero/details', [HomeController::class, 'heroDetails'])->name('api.hero.details');
|
||||
// Simple random pick routes for UI buttons
|
||||
Route::get('/random/movie', [HomeController::class, 'randomMovie'])->name('random.movie');
|
||||
Route::get('/random/tv-show', [HomeController::class, 'randomTvShow'])->name('random.tv-show');
|
||||
|
||||
// Movie routes
|
||||
Route::get('/movie/{slug}', [MovieController::class, 'show'])->name('movie.show');
|
||||
Route::get('/movie-id/{id}', [MovieController::class, 'showById'])->name('movie.show-by-id');
|
||||
Route::get('/embed/movie/{id}', [MovieController::class, 'embed'])->name('movie.embed');
|
||||
|
||||
// TV Show routes
|
||||
Route::get('/show/{slug}', [TvShowController::class, 'show'])->name('tv-show.show');
|
||||
Route::get('/show/{slug}/s{season}e{episode}', [TvShowController::class, 'episode'])
|
||||
->name('tv-show.episode')
|
||||
->where(['season' => '[0-9]+', 'episode' => '[0-9]+']);
|
||||
Route::get('/show-id/{id}', [TvShowController::class, 'showById'])->name('tv-show.show-by-id');
|
||||
Route::get('/api/show/{showId}/season/{season}', [TvShowController::class, 'getSeason'])
|
||||
->name('tv-show.get-season')
|
||||
->where(['showId' => '[0-9]+', 'season' => '[0-9]+']);
|
||||
Route::get('/embed/tv/{id}', [TvShowController::class, 'embed'])->name('tv-show.embed');
|
||||
Route::get('/embed/tv/{id}/{season}-{episode}', [TvShowController::class, 'embedEpisode'])
|
||||
->name('tv-show.embed-episode')
|
||||
->where(['season' => '[0-9]+', 'episode' => '[0-9]+']);
|
||||
|
||||
// Search routes
|
||||
Route::get('/search', [SearchController::class, 'index'])->name('search');
|
||||
Route::get('/api/search', [SearchController::class, 'ajax'])->name('search.ajax');
|
||||
Route::get('/api/suggestions', [SearchController::class, 'suggestions'])->name('search.suggestions');
|
||||
|
||||
// Stats API
|
||||
Route::get('/api/stats', [StatsController::class, 'counts'])->name('api.stats');
|
||||
|
||||
// Matomo Proxy
|
||||
Route::get('/m', [AnalyticsProxyController::class, 'script'])->name('matomo.js');
|
||||
Route::any('/stats', [AnalyticsProxyController::class, 'collect'])->name('matomo.php');
|
||||
|
||||
// I'm Feeling Lucky routes
|
||||
Route::get('/api/lucky', [HomeController::class, 'lucky'])->name('lucky');
|
||||
Route::get('/api/genres', [HomeController::class, 'genres'])->name('genres');
|
||||
|
||||
// Proxy routes for popup blocking
|
||||
Route::get('/proxy/movie/{id}', [ProxyController::class, 'movieEmbed'])->name('proxy.movie');
|
||||
Route::get('/proxy/tv/{id}', [ProxyController::class, 'tvEmbed'])->name('proxy.tv');
|
||||
Route::get('/proxy/tv/{id}/{season}-{episode}', [ProxyController::class, 'tvEpisodeEmbed'])
|
||||
->name('proxy.tv.episode')
|
||||
->where(['id' => '[0-9]+', 'season' => '[0-9]+', 'episode' => '[0-9]+']);
|
||||
|
||||
// Additional utility routes
|
||||
Route::get('/sitemap.xml', [SitemapController::class, 'index'])->name('sitemap');
|
||||
|
||||
Route::get('/robots.txt', function () {
|
||||
return response()->view('robots.txt')->header('Content-Type', 'text/plain');
|
||||
})->name('robots');
|
||||
|
||||
// Legal & Help pages
|
||||
Route::view('/legal/dmca', 'legal.dmca')->name('legal.dmca');
|
||||
Route::view('/legal/terms', 'legal.terms')->name('legal.terms');
|
||||
Route::view('/legal/privacy', 'legal.privacy')->name('legal.privacy');
|
||||
Route::view('/help/adblocker', 'help.adblocker')->name('help.adblocker');
|
||||
Route::view('/about', 'about.index')->name('about');
|
||||
|
||||
// Sitemap segmented routes (index + paginated)
|
||||
Route::get('/sitemap-movies-{page}.xml', [SitemapController::class, 'movies'])
|
||||
->where(['page' => '[0-9]+']);
|
||||
Route::get('/sitemap-shows-{page}.xml', [SitemapController::class, 'shows'])
|
||||
->where(['page' => '[0-9]+']);
|
||||
Route::get('/sitemap-episodes-{page}.xml', [SitemapController::class, 'episodes'])
|
||||
->where(['page' => '[0-9]+']);
|
||||
101
tailwind.config.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./resources/**/*.blade.php",
|
||||
"./resources/**/*.js",
|
||||
"./resources/**/*.vue",
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
'xs': '475px',
|
||||
'3xl': '1600px',
|
||||
'4xl': '1920px',
|
||||
},
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
950: '#450a0a',
|
||||
},
|
||||
gray: {
|
||||
50: '#fafafa',
|
||||
100: '#f4f4f5',
|
||||
200: '#e4e4e7',
|
||||
300: '#d4d4d8',
|
||||
400: '#a1a1aa',
|
||||
500: '#71717a',
|
||||
600: '#52525b',
|
||||
700: '#3f3f46',
|
||||
800: '#27272a',
|
||||
900: '#18181b',
|
||||
950: '#09090b',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Plus Jakarta Sans', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
spacing: {
|
||||
'18': '4.5rem',
|
||||
'88': '22rem',
|
||||
'128': '32rem',
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.5s ease-in-out',
|
||||
'slide-up': 'slideUp 0.3s ease-out',
|
||||
'slide-down': 'slideDown 0.3s ease-out',
|
||||
'pulse-slow': 'pulse 3s ease-in-out infinite',
|
||||
'bounce-slow': 'bounce 2s infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
slideDown: {
|
||||
'0%': { transform: 'translateY(-10px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
}
|
||||
},
|
||||
backdropBlur: {
|
||||
xs: '2px',
|
||||
},
|
||||
backgroundImage: {
|
||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||
},
|
||||
aspectRatio: {
|
||||
'2/3': '2 / 3',
|
||||
'3/4': '3 / 4',
|
||||
'9/16': '9 / 16',
|
||||
'16/9': '16 / 9',
|
||||
},
|
||||
lineClamp: {
|
||||
7: '7',
|
||||
8: '8',
|
||||
9: '9',
|
||||
10: '10',
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms')({ strategy: 'class' }),
|
||||
require('@tailwindcss/typography'),
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
require('@tailwindcss/line-clamp'),
|
||||
require('@tailwindcss/container-queries'),
|
||||
],
|
||||
}
|
||||
19
tests/Feature/ExampleTest.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
10
tests/TestCase.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
16
tests/Unit/ExampleTest.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||