Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7553b9f895 | |||
| 5af1c3c447 | |||
| 2b1a967065 |
@@ -3,11 +3,13 @@
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Models\Comment;
|
||||
use App\Models\Downloads;
|
||||
use App\Models\Episode;
|
||||
use App\Models\Hentai;
|
||||
use App\Models\PopularDaily;
|
||||
use App\Models\PopularMonthly;
|
||||
use App\Models\PopularWeekly;
|
||||
use App\Models\User;
|
||||
use Conner\Tagging\Model\Tag;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -61,6 +63,100 @@ class CacheHelper
|
||||
});
|
||||
}
|
||||
|
||||
public static function getTotalUserCount()
|
||||
{
|
||||
return Cache::remember('total_user_count', now()->addMinutes(60), function () {
|
||||
return User::count();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getTotalCommentCount()
|
||||
{
|
||||
return Cache::remember('total_comment_count', now()->addMinutes(60), function () {
|
||||
return Comment::count();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getTotalLikeCount()
|
||||
{
|
||||
return Cache::remember('total_like_count', now()->addMinutes(60), function () {
|
||||
return DB::table('markable_likes')->count();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getTotalDownloadCount()
|
||||
{
|
||||
return Cache::remember('total_download_count', now()->addMinutes(60), function () {
|
||||
return Downloads::sum('count');
|
||||
});
|
||||
}
|
||||
|
||||
public static function get4kEpisodeCount()
|
||||
{
|
||||
return Cache::remember('episodes_4k_count', now()->addMinutes(60), function () {
|
||||
return Episode::where('interpolated', true)->count();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getUHD48FpsEpisodeCount()
|
||||
{
|
||||
return Cache::remember('episodes_uhd48_count', now()->addMinutes(60), function () {
|
||||
return Episode::where('interpolated_uhd', true)->count();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getTodayViewCount()
|
||||
{
|
||||
return Cache::remember('today_view_count', now()->addMinutes(30), function () {
|
||||
return PopularDaily::whereDate('created_at', today())->count();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getWeeklyViewCount()
|
||||
{
|
||||
return Cache::remember('weekly_view_count', now()->addMinutes(60), function () {
|
||||
return PopularWeekly::whereDate('created_at', '>=', today()->subDays(7))->count();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getPreviousWeeklyViewCount()
|
||||
{
|
||||
return Cache::remember('prev_weekly_view_count', now()->addMinutes(60), function () {
|
||||
return PopularWeekly::whereDate('created_at', '>=', today()->subDays(14))
|
||||
->whereDate('created_at', '<', today()->subDays(7))
|
||||
->count();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getAverageViewsPerEpisode()
|
||||
{
|
||||
return Cache::remember('avg_views_per_episode', now()->addMinutes(60), function () {
|
||||
$totalEpisodes = Episode::count();
|
||||
if ($totalEpisodes === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return round(Episode::sum('view_count') / $totalEpisodes);
|
||||
});
|
||||
}
|
||||
|
||||
public static function getNewEpisodesThisWeek()
|
||||
{
|
||||
return Cache::remember('new_episodes_this_week', now()->addMinutes(60), function () {
|
||||
return Episode::whereDate('created_at', '>=', today()->subDays(7))->count();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getTopTags()
|
||||
{
|
||||
return Cache::remember('top_tags_stats', now()->addMinutes(120), function () {
|
||||
return Tag::where('count', '>', 0)
|
||||
->orderBy('count', 'desc')
|
||||
->limit(10)
|
||||
->get();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getPopularAllTime(bool $guest)
|
||||
{
|
||||
$guestString = $guest ? 'guest' : 'authed';
|
||||
|
||||
@@ -103,6 +103,18 @@ class HomeController extends Controller
|
||||
'viewCount' => CacheHelper::getTotalViewCount(),
|
||||
'episodeCount' => CacheHelper::getTotalEpisodeCount(),
|
||||
'hentaiCount' => CacheHelper::getTotalHentaiCount(),
|
||||
'userCount' => CacheHelper::getTotalUserCount(),
|
||||
'commentCount' => CacheHelper::getTotalCommentCount(),
|
||||
'likeCount' => CacheHelper::getTotalLikeCount(),
|
||||
'downloadCount' => CacheHelper::getTotalDownloadCount(),
|
||||
'episodes4k' => CacheHelper::get4kEpisodeCount(),
|
||||
'episodesUHD48' => CacheHelper::getUHD48FpsEpisodeCount(),
|
||||
'todayViews' => CacheHelper::getTodayViewCount(),
|
||||
'weeklyViews' => CacheHelper::getWeeklyViewCount(),
|
||||
'prevWeeklyViews' => CacheHelper::getPreviousWeeklyViewCount(),
|
||||
'avgViewsPerEpisode' => CacheHelper::getAverageViewsPerEpisode(),
|
||||
'newEpisodesThisWeek' => CacheHelper::getNewEpisodesThisWeek(),
|
||||
'topTags' => CacheHelper::getTopTags(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,17 @@ class NotificationController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Notifcation
|
||||
* Delete a notification or clear all.
|
||||
*/
|
||||
public function delete(Request $request): RedirectResponse
|
||||
{
|
||||
// Clear all notifications if no specific ID is provided
|
||||
if (! $request->has('id')) {
|
||||
$request->user()->notifications()->delete();
|
||||
|
||||
return redirect()->back()->with('status', 'notifications-cleared');
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'id' => 'required|exists:notifications,id',
|
||||
]);
|
||||
|
||||
@@ -20,6 +20,7 @@ class NavLiveSearch extends Component
|
||||
if ($this->navSearch != '') {
|
||||
$episodes = Episode::search($this->navSearch)
|
||||
->when(Auth::guest(), fn ($query) => $query->whereNotIn('tags', ['Loli', 'Shota']))
|
||||
->query(fn ($query) => $query->with(['gallery', 'studio']))
|
||||
->take(7)
|
||||
->get();
|
||||
}
|
||||
|
||||
@@ -146,3 +146,35 @@ input:checked~.dot {
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
/* Stats Page - Shimmer Skeleton Loader */
|
||||
.shimmer-overlay {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.4) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dark .shimmer-overlay {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.05) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
+274
-34
@@ -1,73 +1,313 @@
|
||||
import Chart from 'chart.js/auto';
|
||||
|
||||
// Theming
|
||||
if (localStorage.theme !== 'light') {
|
||||
Chart.defaults.color = "#ADBABD";
|
||||
Chart.defaults.borderColor = "rgba(255,255,255,0.1)";
|
||||
Chart.defaults.backgroundColor = "rgba(255,255,0,0.1)";
|
||||
Chart.defaults.elements.line.borderColor = "rgba(255,255,0,0.4)";
|
||||
/**
|
||||
* Theme-aware chart defaults
|
||||
*/
|
||||
function getChartColors() {
|
||||
const isDark = localStorage.theme !== 'light' &&
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
if (isDark) {
|
||||
return {
|
||||
textColor: '#ADBABD',
|
||||
gridColor: 'rgba(255, 255, 255, 0.06)',
|
||||
fillStart: 'rgba(190, 18, 60, 0.2)',
|
||||
fillEnd: 'rgba(190, 18, 60, 0.0)',
|
||||
borderColor: 'rgba(190, 18, 60, 0.9)',
|
||||
pointColor: 'rgba(190, 18, 60, 1)',
|
||||
pointHoverColor: '#ffffff',
|
||||
skeletonBase: '#262626',
|
||||
skeletonShimmer: '#333333',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
textColor: '#6B7280',
|
||||
gridColor: 'rgba(0, 0, 0, 0.06)',
|
||||
fillStart: 'rgba(190, 18, 60, 0.15)',
|
||||
fillEnd: 'rgba(190, 18, 60, 0.0)',
|
||||
borderColor: 'rgba(190, 18, 60, 1.0)',
|
||||
pointColor: 'rgba(190, 18, 60, 1)',
|
||||
pointHoverColor: '#ffffff',
|
||||
skeletonBase: '#E5E7EB',
|
||||
skeletonShimmer: '#F3F4F6',
|
||||
};
|
||||
}
|
||||
|
||||
// Get Tags from API
|
||||
window.axios.get('/v1/monthly-views').then(function (response) {
|
||||
if (response.status != 200) {
|
||||
return;
|
||||
/**
|
||||
* Show the skeleton loader
|
||||
*/
|
||||
function showSkeleton() {
|
||||
const skeleton = document.getElementById('chart-skeleton');
|
||||
const canvas = document.getElementById('monthlyChart');
|
||||
const error = document.getElementById('chart-error');
|
||||
|
||||
if (skeleton) skeleton.style.display = '';
|
||||
if (canvas) canvas.style.opacity = '0';
|
||||
if (error) error.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the skeleton and show the chart canvas
|
||||
*/
|
||||
function hideSkeleton() {
|
||||
const skeleton = document.getElementById('chart-skeleton');
|
||||
const canvas = document.getElementById('monthlyChart');
|
||||
|
||||
if (skeleton) {
|
||||
// Fade out skeleton
|
||||
skeleton.style.transition = 'opacity 0.4s ease-out';
|
||||
skeleton.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
if (skeleton) skeleton.style.display = 'none';
|
||||
}, 400);
|
||||
}
|
||||
|
||||
const data = {
|
||||
labels: response.data.map((entry) => { return entry.date }),
|
||||
if (canvas) {
|
||||
setTimeout(() => {
|
||||
canvas.style.opacity = '1';
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error state
|
||||
*/
|
||||
function showError() {
|
||||
const skeleton = document.getElementById('chart-skeleton');
|
||||
const error = document.getElementById('chart-error');
|
||||
|
||||
if (skeleton) skeleton.style.display = 'none';
|
||||
if (error) {
|
||||
error.classList.remove('hidden');
|
||||
error.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide error state
|
||||
*/
|
||||
function hideError() {
|
||||
const error = document.getElementById('chart-error');
|
||||
if (error) {
|
||||
error.classList.add('hidden');
|
||||
error.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create gradient fill for the chart
|
||||
*/
|
||||
function createGradient(ctx, colors) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, ctx.canvas.clientHeight);
|
||||
gradient.addColorStop(0, colors.fillStart);
|
||||
gradient.addColorStop(1, colors.fillEnd);
|
||||
return gradient;
|
||||
}
|
||||
|
||||
let monthlyViewChart = null;
|
||||
|
||||
/**
|
||||
* Render the chart with data
|
||||
*/
|
||||
function renderChart(data) {
|
||||
const colors = getChartColors();
|
||||
const canvas = document.getElementById('monthlyChart');
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Destroy previous chart instance if it exists
|
||||
if (monthlyViewChart) {
|
||||
monthlyViewChart.destroy();
|
||||
monthlyViewChart = null;
|
||||
}
|
||||
|
||||
const gradient = createGradient(ctx, colors);
|
||||
|
||||
const chartData = {
|
||||
labels: data.map((entry) => entry.date),
|
||||
datasets: [{
|
||||
label: 'Views',
|
||||
fill: false,
|
||||
backgroundColor: 'rgba(190, 18, 60, 0.3)',
|
||||
borderColor: 'rgba(190, 18, 60, 1.0)',
|
||||
fill: true,
|
||||
backgroundColor: gradient,
|
||||
borderColor: colors.borderColor,
|
||||
borderWidth: 2.5,
|
||||
pointBackgroundColor: colors.pointColor,
|
||||
pointBorderColor: colors.pointColor,
|
||||
pointHoverBackgroundColor: colors.pointHoverColor,
|
||||
pointHoverBorderColor: colors.borderColor,
|
||||
pointHoverBorderWidth: 2,
|
||||
pointHoverRadius: 6,
|
||||
pointRadius: 2.5,
|
||||
pointHitRadius: 20,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
data: response.data.map((entry) => { return entry.count }),
|
||||
tension: 0.4,
|
||||
data: data.map((entry) => entry.count),
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const config = {
|
||||
type: 'line',
|
||||
data: data,
|
||||
data: chartData,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 1200,
|
||||
easing: 'easeOutQuart',
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Views the last 28 days',
|
||||
font: {
|
||||
size: 18
|
||||
display: false,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(17, 17, 17, 0.95)',
|
||||
titleColor: '#ffffff',
|
||||
bodyColor: '#D1D5DB',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
cornerRadius: 10,
|
||||
displayColors: false,
|
||||
bodyFont: {
|
||||
size: 13,
|
||||
},
|
||||
titleFont: {
|
||||
size: 12,
|
||||
weight: '600',
|
||||
},
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return 'Views: ' + new Intl.NumberFormat().format(context.parsed.y);
|
||||
},
|
||||
title: function(context) {
|
||||
return context[0].label;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
grid: {
|
||||
color: colors.gridColor,
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
color: colors.textColor,
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
maxTicksLimit: 14,
|
||||
maxRotation: 0,
|
||||
},
|
||||
title: {
|
||||
display: true
|
||||
display: false,
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Views'
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: colors.gridColor,
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
color: colors.textColor,
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
callback: function(value) {
|
||||
if (value >= 1000000) return (value / 1000000).toFixed(1) + 'M';
|
||||
if (value >= 1000) return (value / 1000).toFixed(1) + 'K';
|
||||
return value;
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
},
|
||||
suggestedMin: 0,
|
||||
suggestedMax: 40000
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const monthlyViewChart = new Chart(
|
||||
document.getElementById('monthlyChart'),
|
||||
config
|
||||
);
|
||||
}).catch(function (error) {
|
||||
console.log(error);
|
||||
hideError();
|
||||
monthlyViewChart = new Chart(canvas, config);
|
||||
hideSkeleton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch chart data from the API
|
||||
*/
|
||||
async function fetchChartData() {
|
||||
showSkeleton();
|
||||
hideError();
|
||||
|
||||
try {
|
||||
const response = await window.axios.get('/v1/monthly-views');
|
||||
|
||||
if (response.status !== 200 || !response.data || response.data.length === 0) {
|
||||
throw new Error('Invalid or empty response');
|
||||
}
|
||||
|
||||
renderChart(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load chart data:', error);
|
||||
showError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry loading the chart (called from error state button)
|
||||
*/
|
||||
window.retryChart = function() {
|
||||
if (monthlyViewChart) {
|
||||
monthlyViewChart.destroy();
|
||||
monthlyViewChart = null;
|
||||
}
|
||||
fetchChartData();
|
||||
};
|
||||
|
||||
// Listen for theme changes to re-render chart
|
||||
const themeObserver = new MutationObserver(() => {
|
||||
if (monthlyViewChart) {
|
||||
const data = monthlyViewChart.data.datasets[0].data.map((value, index) => ({
|
||||
date: monthlyViewChart.data.labels[index],
|
||||
count: value,
|
||||
}));
|
||||
renderChart(data);
|
||||
}
|
||||
});
|
||||
|
||||
// Observe theme class changes on html element
|
||||
const htmlElement = document.documentElement;
|
||||
if (htmlElement) {
|
||||
themeObserver.observe(htmlElement, { attributes: true, attributeFilter: ['class'] });
|
||||
}
|
||||
|
||||
// Start the fetch when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Small delay to ensure the page has rendered and skeleton is visible
|
||||
setTimeout(() => {
|
||||
fetchChartData();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Handle window resize for theme changes (system preference)
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (!('theme' in localStorage) && monthlyViewChart) {
|
||||
const data = monthlyViewChart.data.datasets[0].data.map((value, index) => ({
|
||||
date: monthlyViewChart.data.labels[index],
|
||||
count: value,
|
||||
}));
|
||||
renderChart(data);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
<x-app-layout>
|
||||
@include('partials.background')
|
||||
<div class="relative max-w-[120rem] mx-auto px-4 sm:px-6 lg:px-8 pt-10 pb-16">
|
||||
<div class="flex flex-col md:flex-row gap-6 md:gap-8">
|
||||
|
||||
{{-- Sidebar --}}
|
||||
<div class="w-full md:w-64 xl:w-72 shrink-0">
|
||||
@include('profile.partials.sidebar')
|
||||
</div>
|
||||
|
||||
{{-- Content --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -1,78 +1,365 @@
|
||||
<x-app-layout>
|
||||
<div class="container mx-auto px-4 py-12 md:py-24">
|
||||
<section class="text-center mb-16">
|
||||
<!-- Logo -->
|
||||
<div class="flex justify-center mb-8">
|
||||
<div class="container mx-auto px-4 py-8 md:py-16 max-w-7xl">
|
||||
{{-- Header Section --}}
|
||||
<section class="text-center mb-10 md:mb-14">
|
||||
<div class="flex justify-center mb-6">
|
||||
<img
|
||||
src="/images/cropped-HS-1-270x270.webp"
|
||||
alt="hstream.moe Logo"
|
||||
class="max-w-[150px] w-full h-auto rounded-lg"
|
||||
class="max-w-[120px] w-full h-auto rounded-xl shadow-lg hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8">
|
||||
<!-- View Count Card -->
|
||||
<div class="bg-sky-300/50 dark:bg-sky-950/50 rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow duration-300">
|
||||
<div class="flex justify-center mb-4">
|
||||
<i class="fa-solid fa-eye text-4xl text-sky-600 dark:text-sky-400 p-3"></i>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{{ number_format($viewCount) }}
|
||||
</div>
|
||||
<h5 class="text-lg font-medium text-gray-700 dark:text-neutral-300">
|
||||
total views
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<!-- Episode Count Card -->
|
||||
<div class="bg-sky-300/50 dark:bg-sky-950/50 rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow duration-300">
|
||||
<div class="flex justify-center mb-4">
|
||||
<i class="fa-solid fa-video text-4xl text-sky-600 dark:text-sky-400 p-3"></i>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{{ $episodeCount }}
|
||||
</div>
|
||||
<h5 class="text-lg font-medium text-gray-700 dark:text-neutral-300">
|
||||
episodes on this site
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<!-- Hentai Count Card -->
|
||||
<div class="bg-rose-300/50 dark:bg-rose-950/50 rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow duration-300">
|
||||
<div class="flex justify-center mb-4">
|
||||
<i class="fa-solid fa-list text-4xl text-rose-600 dark:text-rose-400 p-3"></i>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{{ $hentaiCount }}
|
||||
</div>
|
||||
<h5 class="text-lg font-medium text-gray-700 dark:text-neutral-300">
|
||||
hentais on this site
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<!-- Watch Time Card -->
|
||||
<div class="bg-rose-300/50 dark:bg-rose-950/50 rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow duration-300">
|
||||
<div class="flex justify-center mb-4">
|
||||
<i class="fa-solid fa-clock text-4xl text-rose-600 dark:text-rose-400 p-3"></i>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{{ number_format($viewCount * 6) }}
|
||||
</div>
|
||||
<h5 class="text-lg font-medium text-gray-700 dark:text-neutral-300">
|
||||
estimated minutes of watch time
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Container -->
|
||||
<div class="mt-12 mx-auto max-w-4xl">
|
||||
<div class="bg-gray-50 dark:bg-neutral-950 rounded-xl p-4 md:p-6 shadow-inner hidden sm:block">
|
||||
<canvas id="monthlyChart" class="w-full h-64 md:h-80"></canvas>
|
||||
<h1 class="text-3xl md:text-4xl font-extrabold text-gray-900 dark:text-white mb-2 tracking-tight">
|
||||
Site Statistics
|
||||
</h1>
|
||||
<p class="text-gray-500 dark:text-neutral-400 text-sm md:text-base max-w-lg mx-auto">
|
||||
A comprehensive overview of hstream.moe's content and community activity
|
||||
</p>
|
||||
<div class="mt-5 flex justify-center">
|
||||
<div class="inline-flex items-center gap-2 px-4 py-1.5 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded-full text-xs font-semibold">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||||
</span>
|
||||
Live data · Updated hourly
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- Primary Stats Grid --}}
|
||||
<section class="mb-8">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-5">
|
||||
{{-- Total Views --}}
|
||||
<div class="relative overflow-hidden bg-white dark:bg-neutral-900 rounded-2xl p-4 md:p-6 shadow-sm border border-gray-100 dark:border-neutral-800 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 group">
|
||||
<div class="absolute top-0 right-0 w-24 h-24 bg-sky-400/10 dark:bg-sky-500/10 rounded-bl-[80px] -mr-4 -mt-4"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-9 h-9 rounded-xl bg-sky-100 dark:bg-sky-900/50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-eye text-sky-600 dark:text-sky-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1 stat-counter" data-target="{{ $viewCount }}">
|
||||
{{ number_format($viewCount) }}
|
||||
</div>
|
||||
<p class="text-xs md:text-sm text-gray-500 dark:text-neutral-400 font-medium">Total Views</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Total Episodes --}}
|
||||
<div class="relative overflow-hidden bg-white dark:bg-neutral-900 rounded-2xl p-4 md:p-6 shadow-sm border border-gray-100 dark:border-neutral-800 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 group">
|
||||
<div class="absolute top-0 right-0 w-24 h-24 bg-violet-400/10 dark:bg-violet-500/10 rounded-bl-[80px] -mr-4 -mt-4"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-9 h-9 rounded-xl bg-violet-100 dark:bg-violet-900/50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-video text-violet-600 dark:text-violet-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1 stat-counter" data-target="{{ $episodeCount }}">
|
||||
{{ number_format($episodeCount) }}
|
||||
</div>
|
||||
<p class="text-xs md:text-sm text-gray-500 dark:text-neutral-400 font-medium">Episodes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Total Series --}}
|
||||
<div class="relative overflow-hidden bg-white dark:bg-neutral-900 rounded-2xl p-4 md:p-6 shadow-sm border border-gray-100 dark:border-neutral-800 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 group">
|
||||
<div class="absolute top-0 right-0 w-24 h-24 bg-rose-400/10 dark:bg-rose-500/10 rounded-bl-[80px] -mr-4 -mt-4"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-9 h-9 rounded-xl bg-rose-100 dark:bg-rose-900/50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-list text-rose-600 dark:text-rose-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1 stat-counter" data-target="{{ $hentaiCount }}">
|
||||
{{ number_format($hentaiCount) }}
|
||||
</div>
|
||||
<p class="text-xs md:text-sm text-gray-500 dark:text-neutral-400 font-medium">Series</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Total Users --}}
|
||||
<div class="relative overflow-hidden bg-white dark:bg-neutral-900 rounded-2xl p-4 md:p-6 shadow-sm border border-gray-100 dark:border-neutral-800 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 group">
|
||||
<div class="absolute top-0 right-0 w-24 h-24 bg-emerald-400/10 dark:bg-emerald-500/10 rounded-bl-[80px] -mr-4 -mt-4"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-9 h-9 rounded-xl bg-emerald-100 dark:bg-emerald-900/50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-users text-emerald-600 dark:text-emerald-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1 stat-counter" data-target="{{ $userCount }}">
|
||||
{{ number_format($userCount) }}
|
||||
</div>
|
||||
<p class="text-xs md:text-sm text-gray-500 dark:text-neutral-400 font-medium">Registered Users</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- Secondary Stats Grid --}}
|
||||
<section class="mb-8">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-5">
|
||||
{{-- Total Likes --}}
|
||||
<div class="relative overflow-hidden bg-white dark:bg-neutral-900 rounded-2xl p-4 md:p-6 shadow-sm border border-gray-100 dark:border-neutral-800 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 group">
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-9 h-9 rounded-xl bg-red-100 dark:bg-red-900/50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-heart text-red-500 dark:text-red-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1 stat-counter" data-target="{{ $likeCount }}">
|
||||
{{ number_format($likeCount) }}
|
||||
</div>
|
||||
<p class="text-xs md:text-sm text-gray-500 dark:text-neutral-400 font-medium">Total Likes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Total Comments --}}
|
||||
<div class="relative overflow-hidden bg-white dark:bg-neutral-900 rounded-2xl p-4 md:p-6 shadow-sm border border-gray-100 dark:border-neutral-800 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 group">
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-9 h-9 rounded-xl bg-amber-100 dark:bg-amber-900/50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-comments text-amber-600 dark:text-amber-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1 stat-counter" data-target="{{ $commentCount }}">
|
||||
{{ number_format($commentCount) }}
|
||||
</div>
|
||||
<p class="text-xs md:text-sm text-gray-500 dark:text-neutral-400 font-medium">Comments</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Total Downloads --}}
|
||||
<div class="relative overflow-hidden bg-white dark:bg-neutral-900 rounded-2xl p-4 md:p-6 shadow-sm border border-gray-100 dark:border-neutral-800 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 group">
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-9 h-9 rounded-xl bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-download text-blue-600 dark:text-blue-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1 stat-counter" data-target="{{ $downloadCount }}">
|
||||
{{ number_format($downloadCount) }}
|
||||
</div>
|
||||
<p class="text-xs md:text-sm text-gray-500 dark:text-neutral-400 font-medium">Downloads</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Avg Views Per Episode --}}
|
||||
<div class="relative overflow-hidden bg-white dark:bg-neutral-900 rounded-2xl p-4 md:p-6 shadow-sm border border-gray-100 dark:border-neutral-800 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 group">
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-9 h-9 rounded-xl bg-orange-100 dark:bg-orange-900/50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-chart-simple text-orange-600 dark:text-orange-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1 stat-counter" data-target="{{ $avgViewsPerEpisode }}">
|
||||
{{ number_format($avgViewsPerEpisode) }}
|
||||
</div>
|
||||
<p class="text-xs md:text-sm text-gray-500 dark:text-neutral-400 font-medium">Avg Views / Episode</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- Tertiary Stats Row: Today, This Week, New Episodes, 4K Content --}}
|
||||
<section class="mb-10">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-5">
|
||||
{{-- Today's Views --}}
|
||||
<div class="relative overflow-hidden bg-gradient-to-br from-sky-50 to-sky-100/50 dark:from-sky-950/40 dark:to-sky-900/20 rounded-2xl p-4 md:p-6 shadow-sm border border-sky-200/50 dark:border-sky-800/50 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300">
|
||||
<div class="relative z-10">
|
||||
<p class="text-xs text-sky-600 dark:text-sky-400 font-semibold uppercase tracking-wider mb-2">Today</p>
|
||||
<div class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1 stat-counter" data-target="{{ $todayViews }}">
|
||||
{{ number_format($todayViews) }}
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-neutral-400 font-medium">views so far</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- This Week's Views with Trend --}}
|
||||
<div class="relative overflow-hidden bg-gradient-to-br from-violet-50 to-violet-100/50 dark:from-violet-950/40 dark:to-violet-900/20 rounded-2xl p-4 md:p-6 shadow-sm border border-violet-200/50 dark:border-violet-800/50 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300">
|
||||
<div class="relative z-10">
|
||||
<p class="text-xs text-violet-600 dark:text-violet-400 font-semibold uppercase tracking-wider mb-2">Views This Week</p>
|
||||
<div class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1 stat-counter" data-target="{{ $weeklyViews }}">
|
||||
{{ number_format($weeklyViews) }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
@php
|
||||
$trend = $prevWeeklyViews > 0 ? round((($weeklyViews - $prevWeeklyViews) / $prevWeeklyViews) * 100) : 0;
|
||||
@endphp
|
||||
@if($trend > 0)
|
||||
<i class="fa-solid fa-arrow-trend-up text-green-500 text-xs"></i>
|
||||
<span class="text-xs text-green-600 dark:text-green-400 font-semibold">{{ $trend }}% vs last week</span>
|
||||
@elseif($trend < 0)
|
||||
<i class="fa-solid fa-arrow-trend-down text-red-500 text-xs"></i>
|
||||
<span class="text-xs text-red-600 dark:text-red-400 font-semibold">{{ abs($trend) }}% vs last week</span>
|
||||
@else
|
||||
<span class="text-xs text-gray-500 dark:text-neutral-400 font-medium">same as last week</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- New Episodes This Week --}}
|
||||
<div class="relative overflow-hidden bg-gradient-to-br from-emerald-50 to-emerald-100/50 dark:from-emerald-950/40 dark:to-emerald-900/20 rounded-2xl p-4 md:p-6 shadow-sm border border-emerald-200/50 dark:border-emerald-800/50 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300">
|
||||
<div class="relative z-10">
|
||||
<p class="text-xs text-emerald-600 dark:text-emerald-400 font-semibold uppercase tracking-wider mb-2">New This Week</p>
|
||||
<div class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1 stat-counter" data-target="{{ $newEpisodesThisWeek }}">
|
||||
{{ number_format($newEpisodesThisWeek) }}
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-neutral-400 font-medium">episodes added</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 4K / 48fps Content --}}
|
||||
<div class="relative overflow-hidden bg-gradient-to-br from-rose-50 to-rose-100/50 dark:from-rose-950/40 dark:to-rose-900/20 rounded-2xl p-4 md:p-6 shadow-sm border border-rose-200/50 dark:border-rose-800/50 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300">
|
||||
<div class="relative z-10">
|
||||
<p class="text-xs text-rose-600 dark:text-rose-400 font-semibold uppercase tracking-wider mb-2">High Quality</p>
|
||||
<div class="flex items-baseline gap-2 mb-1">
|
||||
<span class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white stat-counter" data-target="{{ $episodes4k }}">{{ number_format($episodes4k) }}</span>
|
||||
<span class="text-sm text-gray-500 dark:text-neutral-400">4K</span>
|
||||
<span class="text-gray-300 dark:text-neutral-700">/</span>
|
||||
<span class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white stat-counter" data-target="{{ $episodesUHD48 }}">{{ number_format($episodesUHD48) }}</span>
|
||||
<span class="text-sm text-gray-500 dark:text-neutral-400">48fps</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-neutral-400 font-medium">4K & 48fps content</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- Views Chart Section --}}
|
||||
<section class="mb-10">
|
||||
<div class="bg-white dark:bg-neutral-900 rounded-2xl shadow-sm border border-gray-100 dark:border-neutral-800 p-4 md:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-gray-900 dark:text-white">Views Over Time</h2>
|
||||
<p class="text-xs text-gray-500 dark:text-neutral-400">Daily views for the past 28 days</p>
|
||||
</div>
|
||||
<div class="hidden sm:flex items-center gap-3 text-xs text-gray-500 dark:text-neutral-400">
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-rose-500"></span> Views
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Chart wrapper with skeleton loader --}}
|
||||
<div class="relative min-h-[300px] md:min-h-[350px]">
|
||||
{{-- Skeleton Loader --}}
|
||||
<div id="chart-skeleton" class="absolute inset-0 flex flex-col justify-end px-1 pb-6 z-10">
|
||||
<div class="flex items-end justify-between gap-1 md:gap-2 h-full">
|
||||
@for ($i = 0; $i < 28; $i++)
|
||||
@php
|
||||
$heights = [55, 40, 65, 35, 60, 45, 70, 50, 75, 38, 58, 42, 80, 48, 68, 33, 62, 44, 72, 52, 55, 40, 65, 58, 78, 46, 63, 50];
|
||||
$h = $heights[$i % count($heights)];
|
||||
@endphp
|
||||
<div class="flex-1 flex flex-col items-center justify-end gap-1">
|
||||
<div class="w-full rounded-md bg-gray-200 dark:bg-neutral-800 animate-pulse" style="height: {{ $h }}%; min-height: 8px;"></div>
|
||||
</div>
|
||||
@endfor
|
||||
</div>
|
||||
{{-- Shimmer overlay --}}
|
||||
<div class="absolute inset-0 shimmer-overlay rounded-lg"></div>
|
||||
</div>
|
||||
|
||||
{{-- Actual Chart Canvas --}}
|
||||
<canvas id="monthlyChart" class="w-full h-[300px] md:h-[350px] relative z-0 opacity-0 transition-opacity duration-500"></canvas>
|
||||
|
||||
{{-- Error State --}}
|
||||
<div id="chart-error" class="absolute inset-0 hidden flex-col items-center justify-center bg-white/90 dark:bg-neutral-900/90 rounded-lg z-20">
|
||||
<div class="w-14 h-14 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center mb-3">
|
||||
<i class="fa-solid fa-triangle-exclamation text-red-500 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-neutral-300 font-medium">Unable to load chart data</p>
|
||||
<button onclick="retryChart()" class="mt-3 px-4 py-2 text-xs font-semibold text-white bg-rose-600 hover:bg-rose-700 rounded-lg transition-colors">
|
||||
<i class="fa-solid fa-rotate mr-1"></i> Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Mobile legend --}}
|
||||
<div class="flex sm:hidden items-center justify-center gap-3 mt-3 text-xs text-gray-500 dark:text-neutral-400">
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-rose-500"></span> Views
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- Bottom Section: Top Tags --}}
|
||||
@if($topTags->isNotEmpty())
|
||||
<section class="mb-8">
|
||||
<div class="bg-white dark:bg-neutral-900 rounded-2xl shadow-sm border border-gray-100 dark:border-neutral-800 p-4 md:p-6">
|
||||
<h2 class="text-lg font-bold text-gray-900 dark:text-white mb-4">Most Popular Tags</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($topTags as $tag)
|
||||
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold
|
||||
bg-gray-100 dark:bg-neutral-800 text-gray-700 dark:text-neutral-300
|
||||
hover:bg-rose-100 dark:hover:bg-rose-900/40 hover:text-rose-700 dark:hover:text-rose-400
|
||||
transition-colors duration-200 cursor-default">
|
||||
#{{ $tag->name }}
|
||||
<span class="text-[10px] text-gray-400 dark:text-neutral-500">{{ number_format($tag->count) }}</span>
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
{{-- Footer Note --}}
|
||||
<div class="text-center text-xs text-gray-400 dark:text-neutral-600">
|
||||
Statistics are cached and update periodically. Data shown may not reflect real-time changes.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Animated Counter Script --}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const animateCounters = () => {
|
||||
document.querySelectorAll('.stat-counter').forEach(counter => {
|
||||
const target = parseInt(counter.dataset.target, 10);
|
||||
if (isNaN(target)) return;
|
||||
|
||||
const duration = 1500;
|
||||
const start = performance.now();
|
||||
const startVal = 0;
|
||||
|
||||
const step = (currentTime) => {
|
||||
const elapsed = currentTime - start;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
// Ease out cubic
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
const current = Math.round(startVal + (target - startVal) * eased);
|
||||
|
||||
const formatted = new Intl.NumberFormat().format(current);
|
||||
counter.textContent = formatted;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(step);
|
||||
});
|
||||
};
|
||||
|
||||
// Use IntersectionObserver to trigger counters when visible
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
animateCounters();
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.3 });
|
||||
|
||||
const firstCounter = document.querySelector('.stat-counter');
|
||||
if (firstCounter) {
|
||||
observer.observe(firstCounter);
|
||||
} else {
|
||||
// Fallback
|
||||
animateCounters();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@vite(['resources/js/stats.js'])
|
||||
</x-app-layout>
|
||||
@@ -39,11 +39,12 @@
|
||||
aria-live="polite"
|
||||
>
|
||||
<div
|
||||
class="max-h-[70vh] overflow-auto rounded-2xl bg-white dark:bg-neutral-900 border border-gray-200 dark:border-neutral-700 shadow-lg transition-all transform hidden group-focus-within:block group-focus-within:translate-y-0">
|
||||
<div class="flex items-center justify-between p-3 border-b border-gray-100 dark:border-neutral-800">
|
||||
class="w-full max-h-[70vh] overflow-auto rounded-2xl bg-white dark:bg-neutral-900 border border-gray-200 dark:border-neutral-700 shadow-xl transition-all transform hidden group-focus-within:block group-focus-within:translate-y-0">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-100 dark:border-neutral-800">
|
||||
<div class="text-sm text-gray-700 dark:text-gray-200 font-medium">
|
||||
@if($episodes->count())
|
||||
{{ __('Search result for ') }} “{{ $query ?: $navSearch }}”
|
||||
{{ __('Search result for ') }}<span class="text-rose-600 dark:text-rose-400 font-semibold">"{{ $query ?: $navSearch }}"</span>
|
||||
@else
|
||||
{{ __('No results') }}
|
||||
@endif
|
||||
@@ -60,46 +61,105 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- content area: responsive grid --}}
|
||||
<div class="p-4">
|
||||
{{-- Results List --}}
|
||||
<div>
|
||||
@if($episodes->count())
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-1 lg:grid-cols-2">
|
||||
<ul class="divide-y divide-gray-100 dark:divide-neutral-800" role="listbox">
|
||||
@foreach($episodes as $episode)
|
||||
<a href="{{ route('hentai.index', ['title' => $episode->slug ]) }}" class="group block rounded-xl overflow-hidden bg-neutral-50 dark:bg-neutral-950 border border-transparent hover:border-gray-200 dark:hover:border-neutral-700 shadow-sm hover:shadow-md transition">
|
||||
<div class="relative aspect-video">
|
||||
<li role="option" aria-selected="false">
|
||||
<a href="{{ route('hentai.index', ['title' => $episode->slug ]) }}"
|
||||
class="flex items-center gap-4 px-5 py-2 group/row hover:bg-rose-50/60 dark:hover:bg-rose-950/30 transition-colors duration-150"
|
||||
>
|
||||
{{-- Left: Cover Image --}}
|
||||
<div class="flex-shrink-0 relative">
|
||||
<div class="w-16 h-[6rem] rounded-lg overflow-hidden ring-1 ring-gray-200/80 dark:ring-neutral-700/80 shadow-sm group-hover/row:ring-rose-300 dark:group-hover/row:ring-rose-700 group-hover/row:shadow-md transition-all duration-200">
|
||||
<img
|
||||
alt="{{ $episode->title }} - {{ $episode->episode }}"
|
||||
loading="lazy"
|
||||
class="object-cover w-full h-full"
|
||||
src="{{ $episode->gallery->first()->thumbnail_url }}"
|
||||
class="object-cover w-full h-full group-hover/row:scale-105 transition-transform duration-300"
|
||||
src="{{ $episode->cover_url }}"
|
||||
>
|
||||
<span class="absolute right-0 top-0 bg-white/90 dark:bg-neutral-800/80 dark:text-white text-xs font-semibold rounded-tr rounded-bl-xl px-2 py-1">{{ $episode->getResolution() }}</span>
|
||||
<div class="absolute left-0 bottom-0 bg-white/90 dark:bg-neutral-800/80 dark:text-white text-xs rounded-tr-xl px-2 py-1 font-medium">
|
||||
<i class="fa-regular fa-eye mr-1"></i> {{ $episode->viewCountFormatted() }}
|
||||
<i class="fa-regular fa-heart ml-2"></i> {{ $episode->likeCount() }}
|
||||
</div>
|
||||
<span class="absolute -top-1.5 -right-1.5 bg-rose-600 text-white text-[0.6rem] font-bold leading-none px-1.5 py-0.5 rounded-md shadow-sm">
|
||||
E{{ $episode->episode }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- Center: Title, Publisher, Tags --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
{{-- Row 1: Title --}}
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white truncate group-hover/row:text-rose-700 dark:group-hover/row:text-rose-400 transition-colors duration-150">
|
||||
{{ $episode->title }}
|
||||
</h3>
|
||||
|
||||
{{-- Row 2: Publisher --}}
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">
|
||||
<i class="fa-regular fa-building mr-1 text-[0.65rem] opacity-70"></i>
|
||||
{{ $episode->studio?->name ?? __('Unknown') }}
|
||||
</p>
|
||||
|
||||
{{-- Row 3: Tags --}}
|
||||
<div class="mt-1.5 flex items-center gap-1 flex-wrap">
|
||||
@php
|
||||
$tags = $episode->tagNames();
|
||||
$visibleTags = array_slice($tags, 0, 3);
|
||||
$remainingCount = count($tags) - 3;
|
||||
@endphp
|
||||
@foreach($visibleTags as $tag)
|
||||
<span class="inline-flex items-center px-1.5 py-0 text-[0.6rem] font-medium leading-tight rounded-md bg-rose-100/80 text-rose-700 dark:bg-rose-900/50 dark:text-rose-300 ring-1 ring-inset ring-rose-200/60 dark:ring-rose-800/60">
|
||||
{{ $tag }}
|
||||
</span>
|
||||
@endforeach
|
||||
@if($remainingCount > 0)
|
||||
<span class="inline-flex items-center px-1.5 py-0 text-[0.6rem] font-medium leading-tight rounded-md bg-gray-100 text-gray-500 dark:bg-neutral-800 dark:text-gray-400">
|
||||
+{{ $remainingCount }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<h3 class="text-sm font-semibold truncate text-gray-900 dark:text-white">{{ $episode->title }} - {{ $episode->episode }}</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate"> {{ \Illuminate\Support\Str::limit($episode->description ?? '', 80) }}</p>
|
||||
|
||||
{{-- Right: View Count, Like Count --}}
|
||||
<div class="flex-shrink-0 flex flex-col items-end gap-1.5">
|
||||
<div class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400 tabular-nums" title="{{ __('Views') }}">
|
||||
<i class="fa-regular fa-eye text-[0.65rem] opacity-70"></i>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{ $episode->viewCountFormatted() }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400 tabular-nums" title="{{ __('Likes') }}">
|
||||
<i class="fa-regular fa-heart text-[0.65rem] opacity-70 text-rose-500 dark:text-rose-400"></i>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{ number_format($episode->likeCount()) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
|
||||
{{-- Advanced Search card --}}
|
||||
<a href="{{ route('hentai.search', ['search' => $query]) }}" class="flex items-center justify-center rounded-xl border border-dashed border-gray-200 dark:border-neutral-700 p-6 hover:bg-gray-50 dark:hover:bg-neutral-900 transition">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-rose-600 mb-1">🔎</div>
|
||||
<div class="font-semibold text-sm dark:text-white">Advanced Search</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">View more results</div>
|
||||
{{-- Advanced Search footer --}}
|
||||
<li>
|
||||
<a href="{{ route('hentai.search', ['search' => $query]) }}"
|
||||
class="block px-5 py-3.5 text-center group/advanced hover:bg-rose-50/60 dark:hover:bg-rose-950/30 transition-colors duration-150"
|
||||
>
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<span class="text-sm font-semibold text-gray-700 dark:text-gray-200 group-hover/advanced:text-rose-700 dark:group-hover/advanced:text-rose-400 transition-colors">
|
||||
{{ __('Advanced Search') }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">{{ __('View all results') }}</span>
|
||||
<svg class="w-4 h-4 text-gray-400 group-hover/advanced:text-rose-600 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@else
|
||||
{{-- Empty state --}}
|
||||
<div class="py-12 text-center text-sm text-gray-600 dark:text-gray-300">
|
||||
<div class="mb-3">No results found for “{{ $query ?: $navSearch }}”</div>
|
||||
<a href="{{ route('hentai.search', ['search' => $navSearch ?: $query]) }}" class="inline-block px-4 py-2 rounded-lg bg-rose-700 text-white text-sm hover:bg-rose-800">Try advanced search</a>
|
||||
<div class="mb-3">{{ __('No results found for') }} <span class="font-semibold text-rose-600">"{{ $query ?: $navSearch }}"</span></div>
|
||||
<a href="{{ route('hentai.search', ['search' => $navSearch ?: $query]) }}" class="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-rose-700 text-white text-sm font-medium hover:bg-rose-800 transition shadow-sm hover:shadow-md">
|
||||
<span>{{ __('Try advanced search') }}</span>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
<div>
|
||||
<div class="mx-auto max-w-5xl px-4 space-y-6">
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="p-4 bg-white/40 dark:bg-neutral-950/40 backdrop-blur rounded-xl shadow flex flex-col sm:flex-row gap-3">
|
||||
|
||||
<!-- Search -->
|
||||
<div class="space-y-5">
|
||||
{{-- Filters --}}
|
||||
<div class="rounded-2xl border border-neutral-200/70 bg-white/80 p-4 shadow-sm backdrop-blur-xl dark:border-neutral-800/70 dark:bg-neutral-950/70">
|
||||
<div class="flex flex-col sm:flex-row gap-3">
|
||||
{{-- Search --}}
|
||||
<div class="relative flex-1">
|
||||
<input
|
||||
wire:model.live.debounce.500ms="commentSearch"
|
||||
type="search"
|
||||
placeholder="Search comments..."
|
||||
class="w-full pl-10 pr-4 py-3 rounded-lg border-neutral-300 dark:text-neutral-300 bg-white/80 dark:bg-neutral-900/50 dark:border-neutral-700 focus:outline-none focus:ring-2 focus:ring-rose-600 focus:border-rose-700 transition"
|
||||
>
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 pl-3 flex items-center">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<svg class="w-4 h-4 text-gray-400 dark:text-gray-300" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
wire:model.live.debounce.500ms="commentSearch"
|
||||
type="search"
|
||||
placeholder="Search comments..."
|
||||
class="w-full pl-10 pr-4 py-2.5 rounded-xl border-neutral-300 dark:text-neutral-300 bg-white dark:bg-neutral-900 dark:border-neutral-700 focus:outline-none focus:ring-2 focus:ring-rose-600 focus:border-rose-700 transition"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Order -->
|
||||
{{-- Order --}}
|
||||
<div class="relative">
|
||||
<i class="fa-solid fa-sort pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400"></i>
|
||||
<select
|
||||
wire:model.live="order"
|
||||
class="px-4 py-3 rounded-lg border-neutral-300 dark:text-gray-300 bg-white/80 dark:bg-neutral-900/50 dark:border-neutral-700 min-w-[128px]"
|
||||
class="appearance-none pl-10 pr-8 py-2.5 rounded-xl border-neutral-300 dark:text-gray-300 bg-white dark:bg-neutral-900 dark:border-neutral-700 min-w-[128px]"
|
||||
>
|
||||
<option value="created_at_desc">Newest</option>
|
||||
<option value="created_at_asc">Oldest</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<div class="space-y-4">
|
||||
{{-- Comments --}}
|
||||
<div class="space-y-3">
|
||||
@forelse ($comments as $comment)
|
||||
|
||||
@php
|
||||
@@ -46,35 +48,40 @@
|
||||
wire:key="comment-{{ $comment->id }}"
|
||||
class="block group">
|
||||
|
||||
<div class="bg-white/40 dark:bg-neutral-950/40 backdrop-blur rounded-xl shadow hover:shadow-lg transition overflow-hidden">
|
||||
<div class="rounded-xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 hover:shadow-md hover:ring-rose-300/50 dark:hover:ring-rose-700/30 transition-all duration-200 overflow-hidden">
|
||||
|
||||
<div class="flex flex-col sm:flex-row">
|
||||
|
||||
<!-- Thumbnail -->
|
||||
<div class="sm:w-48 shrink-0">
|
||||
{{-- Thumbnail --}}
|
||||
<div class="sm:w-44 shrink-0">
|
||||
<img
|
||||
src="{{ $episode->gallery->first()->thumbnail_url }}"
|
||||
alt=""
|
||||
class="w-full h-40 sm:h-full object-cover"
|
||||
alt="{{ $episode->title ?? '' }}"
|
||||
class="w-full h-36 sm:h-full object-cover"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 p-4 flex flex-col justify-between">
|
||||
{{-- Content --}}
|
||||
<div class="flex-1 p-4 flex flex-col justify-between min-w-0">
|
||||
|
||||
<!-- Comment -->
|
||||
<div class="text-gray-800 dark:text-gray-200 text-sm line-clamp-3">
|
||||
{{-- Episode Title --}}
|
||||
<p class="text-xs font-medium text-rose-600 dark:text-rose-400 truncate mb-1">
|
||||
{{ $episode->title ?? 'Episode' }}
|
||||
</p>
|
||||
|
||||
{{-- Comment --}}
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300 line-clamp-2 leading-relaxed">
|
||||
{!! $comment->presenter()->markdownBody() !!}
|
||||
</div>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="flex items-center justify-between mt-3 text-xs text-gray-700 dark:text-gray-400">
|
||||
|
||||
<span>
|
||||
{{-- Meta --}}
|
||||
<div class="flex items-center justify-between mt-2.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span class="flex items-center gap-1">
|
||||
<i class="fa-solid fa-clock text-[10px]"></i>
|
||||
{{ $comment->presenter()->relativeCreatedAt() }}
|
||||
</span>
|
||||
|
||||
<span class="text-rose-600 font-medium group-hover:underline">
|
||||
<span class="text-rose-600 dark:text-rose-400 font-medium group-hover:underline">
|
||||
View comment
|
||||
</span>
|
||||
</div>
|
||||
@@ -83,19 +90,19 @@
|
||||
</div>
|
||||
</a>
|
||||
@empty
|
||||
<div class="flex bg-white/40 dark:bg-neutral-950/40 backdrop-blur rounded-xl shadow hover:shadow-lg transition overflow-hidden">
|
||||
<div class="text-gray-800 dark:text-gray-200 text-center w-full p-4">
|
||||
<p class="text-lg">No results</p>
|
||||
<p class="text-sm opacity-70">(╥﹏╥)</p>
|
||||
<div class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 p-12 text-center">
|
||||
<div class="inline-flex h-20 w-20 items-center justify-center rounded-full bg-gray-100 dark:bg-neutral-800 mb-4">
|
||||
<i class="fa-solid fa-comment-slash text-3xl text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">No comments found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">You haven't commented on anything yet.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{{-- Pagination --}}
|
||||
<div>
|
||||
{{ $comments->links('pagination::tailwind') }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,139 @@
|
||||
<div>
|
||||
<div class="md:ml-8 my-8 md:my-0 space-y-6 max-w-[100%] xl:max-w-[95%] 2xl:max-w-[95%]">
|
||||
@include('livewire.partials.search-filter')
|
||||
<div class="space-y-5">
|
||||
{{-- Slim Profile Filter --}}
|
||||
<div class="rounded-2xl border border-neutral-200/70 bg-white/80 p-4 shadow-sm backdrop-blur-xl dark:border-neutral-800/70 dark:bg-neutral-950/70">
|
||||
<div class="flex flex-col sm:flex-row gap-3">
|
||||
{{-- Search --}}
|
||||
<div class="relative flex-1">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-4">
|
||||
<svg class="h-4 w-4 text-neutral-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
wire:model.live.debounce.400ms="search"
|
||||
type="search"
|
||||
placeholder="Search liked episodes..."
|
||||
class="w-full rounded-xl border border-neutral-300 bg-white py-2.5 pl-11 pr-4 text-sm text-neutral-900 shadow-sm transition focus:border-rose-500 focus:outline-none focus:ring-2 focus:ring-rose-500/20 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white dark:placeholder-neutral-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{-- Order --}}
|
||||
<div class="relative">
|
||||
<i class="fa-solid fa-sort pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400"></i>
|
||||
<select
|
||||
wire:model.live="order"
|
||||
class="w-full appearance-none rounded-xl border border-neutral-300 bg-white py-2.5 pl-10 pr-10 text-sm text-neutral-900 shadow-sm transition focus:border-rose-500 focus:outline-none focus:ring-2 focus:ring-rose-500/20 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white"
|
||||
>
|
||||
<option value="az">A-Z</option>
|
||||
<option value="za">Z-A</option>
|
||||
<option value="recently-uploaded">{{ __('home.recently-uploaded') }}</option>
|
||||
<option value="recently-released">{{ __('home.recently-released') }}</option>
|
||||
<option value="oldest-uploads">{{ __('search.oldest-uploads') }}</option>
|
||||
<option value="oldest-releases">{{ __('search.oldest-releases') }}</option>
|
||||
<option value="view-count">{{ __('search.view-count') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- View --}}
|
||||
<div class="relative">
|
||||
<i class="fa-solid fa-list pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400"></i>
|
||||
<select
|
||||
wire:model.live="view"
|
||||
class="w-full appearance-none rounded-xl border border-neutral-300 bg-white py-2.5 pl-10 pr-10 text-sm text-neutral-900 shadow-sm transition focus:border-rose-500 focus:outline-none focus:ring-2 focus:ring-rose-500/20 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white"
|
||||
>
|
||||
<option value="thumbnail">Thumbnail</option>
|
||||
<option value="poster">Poster</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Filter Buttons --}}
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
data-te-toggle="modal"
|
||||
data-te-target="#modalGenres"
|
||||
class="inline-flex items-center gap-1.5 rounded-xl border border-neutral-300 bg-white px-3.5 py-2.5 text-xs font-medium text-neutral-600 shadow-sm transition hover:border-rose-400 hover:bg-rose-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||
>
|
||||
<i class="fa-solid fa-sliders text-[10px]"></i>
|
||||
Genres
|
||||
@if($tagcount > 0)
|
||||
<span class="inline-flex h-4 min-w-[16px] items-center justify-center rounded-full bg-rose-600 px-1 text-[9px] font-bold text-white">{{ $tagcount }}</span>
|
||||
@endif
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-te-toggle="modal"
|
||||
data-te-target="#modalBlacklist"
|
||||
class="inline-flex items-center gap-1.5 rounded-xl border border-neutral-300 bg-white px-3.5 py-2.5 text-xs font-medium text-neutral-600 shadow-sm transition hover:border-rose-400 hover:bg-rose-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||
>
|
||||
<i class="fa-solid fa-shield text-[10px]"></i>
|
||||
Blacklist
|
||||
@if($blacklistcount > 0)
|
||||
<span class="inline-flex h-4 min-w-[16px] items-center justify-center rounded-full bg-rose-600 px-1 text-[9px] font-bold text-white">{{ $blacklistcount }}</span>
|
||||
@endif
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-te-toggle="modal"
|
||||
data-te-target="#modalStudios"
|
||||
class="inline-flex items-center gap-1.5 rounded-xl border border-neutral-300 bg-white px-3.5 py-2.5 text-xs font-medium text-neutral-600 shadow-sm transition hover:border-rose-400 hover:bg-rose-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||
>
|
||||
<i class="fa-solid fa-microphone-lines text-[10px]"></i>
|
||||
Studios
|
||||
@if($studiocount > 0)
|
||||
<span class="inline-flex h-4 min-w-[16px] items-center justify-center rounded-full bg-rose-600 px-1 text-[9px] font-bold text-white">{{ $studiocount }}</span>
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Hide Watched --}}
|
||||
@auth
|
||||
<label class="flex cursor-pointer items-center gap-2 rounded-xl border border-neutral-300 bg-white px-3.5 py-2.5 text-xs font-medium text-neutral-600 shadow-sm transition hover:border-rose-400 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 whitespace-nowrap">
|
||||
<input
|
||||
id="checkBoxHideWatched"
|
||||
type="checkbox"
|
||||
wire:model.live="hideWatched"
|
||||
class="h-4 w-4 rounded border-neutral-300 text-rose-600 focus:ring-rose-500 dark:border-neutral-700 dark:bg-neutral-800"
|
||||
/>
|
||||
<span>Hide watched</span>
|
||||
</label>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Modals --}}
|
||||
@include('modals.filter-genres')
|
||||
@include('modals.filter-studios')
|
||||
@include('modals.filter-blacklist')
|
||||
|
||||
<input type="hidden" id="ts_reference" value="{{ Carbon\Carbon::now()->timestamp }}" />
|
||||
<div class="relative md:ml-8 pt-5 mx-auto space-y-6 text-gray-900 dark:text-white xl:max-w-[95%] 2xl:max-w-[95%]" wire:keydown.right.window="nextPage" wire:keydown.left.window="previousPage">
|
||||
|
||||
{{-- Results --}}
|
||||
<div wire:keydown.right.window="nextPage" wire:keydown.left.window="previousPage">
|
||||
{{ $episodes->appends(['tags' => $selectedtags])->links('pagination::tailwind') }}
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="flex justify-center">
|
||||
<div class="mt-4">
|
||||
@if ($view == 'thumbnail')
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-4 gap-2">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
@else
|
||||
<div class="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-8 gap-2">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
|
||||
@endif
|
||||
@forelse($episodes as $episode)
|
||||
@include('livewire.partials.search-result')
|
||||
@empty
|
||||
<div class="col-span-full">
|
||||
<p class="text-2xl w-52 pt-6">No results (╥﹏╥)</p>
|
||||
<div class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 p-12 text-center">
|
||||
<div class="inline-flex h-20 w-20 items-center justify-center rounded-full bg-gray-100 dark:bg-neutral-800 mb-4">
|
||||
<i class="fa-solid fa-heart-crack text-3xl text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">No liked episodes</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Start exploring and like what you enjoy!</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ $episodes->appends(['tags' => $selectedtags])->links('pagination::tailwind') }}
|
||||
</div>
|
||||
</div
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,28 +1,51 @@
|
||||
<div>
|
||||
<div class="relative mx-auto sm:px-6 lg:px-8 text-gray-900 dark:text-white xl:max-w-[95%] 2xl:max-w-[90%]"
|
||||
wire:keydown.right.window="nextPage" wire:keydown.left.window="previousPage">
|
||||
<ol class="border-l border-neutral-300 dark:border-neutral-500">
|
||||
<div wire:keydown.right.window="nextPage" wire:keydown.left.window="previousPage" class="text-gray-900 dark:text-white">
|
||||
<div class="relative">
|
||||
{{-- Timeline --}}
|
||||
<ol class="relative border-l-2 border-neutral-300/70 dark:border-neutral-600/70 ml-3 sm:ml-4">
|
||||
@foreach ($watchedGrouped as $day => $episodes)
|
||||
<li>
|
||||
<div class="flex items-center pt-3 flex-start">
|
||||
<div class="-ml-[5px] mr-3 h-[9px] w-[9px] rounded-full bg-neutral-300 dark:bg-neutral-500">
|
||||
</div>
|
||||
<p class="text-sm text-neutral-500 dark:text-neutral-300">
|
||||
<li class="mb-10 ml-6 sm:ml-8 last:mb-0">
|
||||
{{-- Timeline Dot --}}
|
||||
<span class="absolute flex items-center justify-center w-6 h-6 rounded-full -left-3 ring-2 ring-white dark:ring-neutral-950 bg-rose-600 dark:bg-rose-500">
|
||||
<i class="fa-solid fa-circle text-[6px] text-white"></i>
|
||||
</span>
|
||||
|
||||
{{-- Date Header --}}
|
||||
<div class="mb-4">
|
||||
<time class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold text-gray-600 dark:text-gray-300 bg-gray-100/70 dark:bg-neutral-800/70">
|
||||
<i class="fa-solid fa-calendar text-[10px] text-rose-500"></i>
|
||||
{{ $episodes->first()->created_at->diffForHumans(['parts' => 1]) }}
|
||||
</p>
|
||||
</time>
|
||||
<span class="ml-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ $episodes->count() }} {{ Str::plural('episode', $episodes->count()) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-4">
|
||||
|
||||
{{-- Episodes Grid --}}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
@foreach ($episodes as $episode)
|
||||
<div class="mt-2 mb-6 ml-4">
|
||||
<x-episode-cover :episode="$episode->episode" view="thumbnail" />
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
|
||||
{{-- Pagination --}}
|
||||
@if($watched->hasPages())
|
||||
<div class="mt-8">
|
||||
{{ $watched->links('pagination::tailwind') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Empty State --}}
|
||||
@if($watched->isEmpty())
|
||||
<div class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 p-12 text-center">
|
||||
<div class="inline-flex h-20 w-20 items-center justify-center rounded-full bg-gray-100 dark:bg-neutral-800 mb-4">
|
||||
<i class="fa-solid fa-eye-slash text-3xl text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">No watch history</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Start watching and your history will appear here.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +1,12 @@
|
||||
<x-app-layout>
|
||||
@include('partials.background')
|
||||
<div class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-10 flex flex-row justify-center md:justify-normal">
|
||||
<div class="grid md:grid-flow-col gap-4 xl:w-5/6 flex-row">
|
||||
@include('profile.partials.sidebar')
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-profile-layout>
|
||||
<div class="space-y-5">
|
||||
{{-- Header --}}
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<i class="fa-solid fa-comment text-rose-500"></i>
|
||||
{{ __('nav.comments') }}
|
||||
</h2>
|
||||
|
||||
{{-- Content from Livewire --}}
|
||||
<livewire:user-comments :model="$user"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
</x-profile-layout>
|
||||
@@ -1,9 +1,123 @@
|
||||
<x-app-layout>
|
||||
@include('partials.background')
|
||||
<div class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-10 flex flex-row justify-center md:justify-normal">
|
||||
<div class="grid md:grid-flow-col gap-4 w-5/6 flex-row">
|
||||
@include('profile.partials.sidebar')
|
||||
@include('profile.partials.info')
|
||||
<x-profile-layout>
|
||||
<div class="space-y-6">
|
||||
{{-- Welcome Banner --}}
|
||||
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-br from-rose-600 via-rose-700 to-pink-700 p-6 sm:p-8 text-white shadow-xl shadow-rose-600/20">
|
||||
<div class="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGw9IiNmZmZmZmYiIGZpbGwtb3BhY2l0eT0iMC4wNSI+PGNpcmNsZSBjeD0iMzAiIGN5PSIzMCIgcj0iMiIvPjwvZz48L2c+PC9zdmc+')] opacity-50"></div>
|
||||
<div class="relative z-10">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">Welcome back, {{ $user->name }}!</h1>
|
||||
<p class="mt-2 text-rose-100 text-sm sm:text-base max-w-4xl">
|
||||
Here's an overview of your activity on hstream.moe. Dive back into your favorites or discover something new.
|
||||
</p>
|
||||
</div>
|
||||
{{-- Decorative circles --}}
|
||||
<div class="absolute -top-10 -right-10 h-40 w-40 rounded-full bg-white/5 blur-2xl"></div>
|
||||
<div class="absolute -bottom-10 -left-10 h-32 w-32 rounded-full bg-white/5 blur-2xl"></div>
|
||||
</div>
|
||||
|
||||
{{-- Quick Links Grid --}}
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-3 gap-3 sm:gap-4">
|
||||
<a href="{{ route('profile.likes') }}"
|
||||
class="group rounded-xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 p-4 sm:p-5 hover:shadow-md hover:ring-rose-300/50 dark:hover:ring-rose-700/30 transition-all duration-200">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-rose-100 dark:bg-rose-900/40 text-rose-600 dark:text-rose-400 group-hover:scale-110 transition-transform duration-200">
|
||||
<i class="fa-solid fa-heart text-lg"></i>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-700 dark:text-gray-200">Liked Episodes</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ number_format($user->likes()) }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Episodes you've liked</p>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('user.watched') }}"
|
||||
class="group rounded-xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 p-4 sm:p-5 hover:shadow-md hover:ring-rose-300/50 dark:hover:ring-rose-700/30 transition-all duration-200">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-sky-100 dark:bg-sky-900/40 text-sky-600 dark:text-sky-400 group-hover:scale-110 transition-transform duration-200">
|
||||
<i class="fa-solid fa-eye text-lg"></i>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-700 dark:text-gray-200">Watch History</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ number_format($user->watched->count()) }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Episodes watched</p>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('profile.playlists') }}"
|
||||
class="group rounded-xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 p-4 sm:p-5 hover:shadow-md hover:ring-rose-300/50 dark:hover:ring-rose-700/30 transition-all duration-200">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-violet-100 dark:bg-violet-900/40 text-violet-600 dark:text-violet-400 group-hover:scale-110 transition-transform duration-200">
|
||||
<i class="fa-solid fa-rectangle-list text-lg"></i>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-700 dark:text-gray-200">Your Playlists</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ number_format($user->playlists->count()) }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Custom collections</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Recent Activity Section --}}
|
||||
<div class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 p-5 sm:p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-5 flex items-center gap-2">
|
||||
<i class="fa-solid fa-clock-rotate-left text-rose-500"></i>
|
||||
Recent Activity
|
||||
</h3>
|
||||
|
||||
@php
|
||||
$recentWatched = $user->watched()->with('episode')->latest()->take(4)->get();
|
||||
$recentComments = $user->comments()->latest()->take(2)->get();
|
||||
@endphp
|
||||
|
||||
@if($recentWatched->isEmpty() && $recentComments->isEmpty())
|
||||
<div class="text-center py-10">
|
||||
<div class="inline-flex h-16 w-16 items-center justify-center rounded-full bg-gray-100 dark:bg-neutral-800 mb-4">
|
||||
<i class="fa-solid fa-ghost text-2xl text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm">No activity yet. Start watching something!</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-4">
|
||||
{{-- Recently Watched --}}
|
||||
@if($recentWatched->isNotEmpty())
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-gray-600 dark:text-gray-500 mb-3">Recently Watched</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
@foreach($recentWatched as $watched)
|
||||
<a href="{{ route('hentai.index', ['title' => $watched->episode->slug]) }}"
|
||||
class="group relative overflow-hidden rounded-lg bg-gray-100 dark:bg-neutral-800 aspect-video block">
|
||||
<img src="{{ $watched->episode->gallery->first()->thumbnail_url ?? '/images/default-avatar.webp' }}"
|
||||
alt="{{ $watched->episode->title }}"
|
||||
class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
loading="lazy">
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<p class="absolute bottom-2 left-2 right-2 text-xs text-white font-medium truncate">
|
||||
{{ $watched->episode->title }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@endif
|
||||
|
||||
{{-- Recent Comments --}}
|
||||
@if($recentComments->isNotEmpty())
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-gray-600 dark:text-gray-500 mb-3">Recent Comments</p>
|
||||
<div class="space-y-2">
|
||||
@foreach($recentComments as $comment)
|
||||
<a href="{{ route('hentai.index', ['title' => $comment->commentable->slug ?? '#']) }}#comment-{{ $comment->id }}"
|
||||
class="block rounded-lg bg-gray-50/70 dark:bg-neutral-900/50 p-3 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors">
|
||||
<div class="text-sm text-gray-700 dark:text-gray-200 line-clamp-2">
|
||||
{!! $comment->presenter()->markdownBody() !!}
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-400/80 mt-1.5">
|
||||
{{ $comment->presenter()->relativeCreatedAt() }}
|
||||
</p>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-profile-layout>
|
||||
@@ -1,9 +1,14 @@
|
||||
<x-app-layout>
|
||||
@include('partials.background')
|
||||
<div class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-10 flex flex-row justify-center md:justify-normal">
|
||||
<div class="flex flex-col md:flex-row">
|
||||
@include('profile.partials.sidebar')
|
||||
<x-profile-layout>
|
||||
<div class="space-y-5">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<i class="fa-solid fa-heart text-rose-500"></i>
|
||||
{{ __('nav.likes') }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{{-- Content from Livewire --}}
|
||||
@livewire('user-likes')
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
</x-profile-layout>
|
||||
@@ -1,55 +1,84 @@
|
||||
<x-app-layout>
|
||||
@include('partials.background')
|
||||
<div
|
||||
class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-10 flex flex-row justify-center md:justify-normal">
|
||||
<div class="grid md:grid-flow-col gap-4 xl:w-5/6 flex-row">
|
||||
@include('profile.partials.sidebar')
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-profile-layout>
|
||||
<div class="space-y-5">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<i class="fa-solid fa-bell text-rose-500"></i>
|
||||
Notifications
|
||||
</h2>
|
||||
@if($notifications->isNotEmpty())
|
||||
<form method="POST" action="{{ route('profile.notifications.delete') }}" class="hidden sm:block">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<button type="submit"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 dark:border-red-800/50 px-3 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30 transition-colors">
|
||||
<i class="fa-solid fa-trash-can text-[10px]"></i>
|
||||
Clear All
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@forelse($notifications as $notification)
|
||||
<div
|
||||
class="bg-white/40 dark:bg-neutral-950/40 backdrop-blur border border-gray-200 dark:border-neutral-700 rounded-xl shadow-sm p-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-x-4 transition hover:shadow-md">
|
||||
<div class="group relative rounded-xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 hover:shadow-md transition-all duration-200 overflow-hidden">
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex flex-col gap-2 w-full h-full mt-2">
|
||||
<div class="flex items-center justify-between flex-none h-2">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-sky-600 dark:text-rose-500">
|
||||
{{-- Unread indicator --}}
|
||||
@if(is_null($notification->read_at))
|
||||
<div class="absolute left-0 top-0 bottom-0 w-1 bg-rose-500"></div>
|
||||
@endif
|
||||
|
||||
<div class="p-4 sm:p-5 {{ is_null($notification->read_at) ? 'pl-5 sm:pl-6' : '' }}">
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
|
||||
|
||||
{{-- Content --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-rose-100 dark:bg-rose-900/30 px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-rose-700 dark:text-rose-400">
|
||||
<i class="fa-solid fa-tag text-[9px]"></i>
|
||||
{{ $notification->data['type'] ?? 'Notification' }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ $notification->created_at->diffForHumans(['parts' => 1]) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="text-sm sm:text-base text-gray-700 dark:text-gray-300 leading-relaxed h-full">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
{{ $notification->data['message'] ?? '' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2 sm:gap-2 shrink-0">
|
||||
{{-- Actions --}}
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
@if(isset($notification->data['url']))
|
||||
<a href="{{ $notification->data['url'] }}"
|
||||
class="text-center rounded-lg bg-sky-600 px-3 py-2 text-xs sm:text-sm font-medium text-white hover:bg-sky-700 transition">
|
||||
class="inline-flex items-center gap-1 rounded-lg bg-rose-600 px-3.5 py-2 text-xs font-semibold text-white hover:bg-rose-700 shadow-sm shadow-rose-600/20 transition-all duration-150 hover:shadow-md hover:shadow-rose-600/25">
|
||||
Open
|
||||
<i class="fa-solid fa-arrow-right text-[10px]"></i>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('profile.notifications.delete') }}">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<input type="hidden" value="{{ $notification->id }}" name="id">
|
||||
|
||||
<button type="submit"
|
||||
class="w-full rounded-lg bg-rose-600 px-3 py-2 text-xs sm:text-sm font-medium text-white hover:bg-rose-700 transition">
|
||||
Delete
|
||||
class="inline-flex items-center rounded-lg p-2 text-gray-400 hover:bg-red-50 dark:hover:bg-red-950/30 hover:text-red-500 dark:hover:text-red-400 transition-colors"
|
||||
title="Delete">
|
||||
<i class="fa-solid fa-xmark text-sm"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-16 text-gray-500 dark:text-gray-400">
|
||||
<p class="text-lg">No notifications</p>
|
||||
<p class="text-sm opacity-70">(╥﹏╥)</p>
|
||||
<div class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 p-12 text-center">
|
||||
<div class="inline-flex h-20 w-20 items-center justify-center rounded-full bg-gray-100 dark:bg-neutral-800 mb-4">
|
||||
<i class="fa-solid fa-bell-slash text-3xl text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">No notifications</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">You're all caught up! Nothing new here.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
</x-profile-layout>
|
||||
@@ -1,18 +1,26 @@
|
||||
@auth
|
||||
<div
|
||||
class="overflow-hidden mt-5 relative max-w-sm min-w-80 mx-auto bg-white/40 shadow-lg ring-1 ring-black/5 rounded-xl items-center gap-6 dark:bg-neutral-950/40 backdrop-blur dark:highlight-white/5">
|
||||
<div class="flex flex-col p-2">
|
||||
<a class="block w-full px-4 py-2 rounded-lg text-left text-lg leading-5 text-gray-700 dark:text-gray-300 @if(request()->routeIs('profile.settings')) bg-rose-900/40 @endif hover:bg-neutral-100/60 dark:hover:bg-neutral-900/60 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out"
|
||||
href="{{ route('profile.settings') }}"><i class="fa-solid fa-gear pr-4"></i>
|
||||
Settings</a>
|
||||
<div class="mt-5 overflow-hidden rounded-xl bg-white/40 shadow-lg ring-1 ring-black/5 dark:bg-neutral-950/40 backdrop-blur dark:ring-white/10">
|
||||
<div class="flex flex-col p-1.5">
|
||||
|
||||
<a href="{{ route('profile.settings') }}"
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-150
|
||||
@if(request()->routeIs('profile.settings'))
|
||||
bg-rose-600/10 text-rose-600 dark:bg-rose-500/15 dark:text-rose-400 border-l-[3px] border-rose-600 dark:border-rose-400 ml-[-3px]
|
||||
@else
|
||||
text-gray-700 dark:text-gray-300 hover:bg-gray-100/60 dark:hover:bg-neutral-800/60 border-l-[3px] border-transparent
|
||||
@endif">
|
||||
<i class="fa-solid fa-gear w-5 text-center text-base
|
||||
@if(request()->routeIs('profile.settings')) text-rose-600 dark:text-rose-400 @else text-gray-400 dark:text-gray-500 @endif"></i>
|
||||
Settings
|
||||
</a>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<button type="submit"
|
||||
class="block w-full px-4 py-2 rounded-lg text-left text-lg leading-5 text-gray-700 dark:text-gray-300 hover:bg-neutral-100/60 dark:hover:bg-neutral-900/60 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out"><i
|
||||
class="fa-solid fa-right-from-bracket pr-4"></i>
|
||||
Logout</button>
|
||||
class="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-red-50/60 dark:hover:bg-red-950/40 hover:text-red-600 dark:hover:text-red-400 border-l-[3px] border-transparent transition-all duration-150">
|
||||
<i class="fa-solid fa-right-from-bracket w-5 text-center text-base text-gray-400 dark:text-gray-500"></i>
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<div class="md:hidden mt-5 -mx-1">
|
||||
<nav class="flex gap-1 overflow-x-auto pb-2 scrollbar-hide">
|
||||
<a href="{{ route('profile.show') }}"
|
||||
class="shrink-0 inline-flex items-center gap-1.5 rounded-full px-4 py-2 text-xs font-medium transition-all duration-150
|
||||
@if(request()->routeIs('profile.show'))
|
||||
bg-rose-600 text-white shadow-md shadow-rose-600/25
|
||||
@else
|
||||
bg-white/60 text-gray-600 dark:bg-neutral-900/60 dark:text-gray-400 hover:bg-white/90 dark:hover:bg-neutral-800/90
|
||||
@endif">
|
||||
<i class="fa-solid fa-user text-[10px]"></i>
|
||||
{{ __('nav.profile') }}
|
||||
</a>
|
||||
|
||||
<a href="{{ route('profile.notifications') }}"
|
||||
class="shrink-0 inline-flex items-center gap-1.5 rounded-full px-4 py-2 text-xs font-medium transition-all duration-150
|
||||
@if(request()->routeIs('profile.notifications'))
|
||||
bg-rose-600 text-white shadow-md shadow-rose-600/25
|
||||
@else
|
||||
bg-white/60 text-gray-600 dark:bg-neutral-900/60 dark:text-gray-400 hover:bg-white/90 dark:hover:bg-neutral-800/90
|
||||
@endif">
|
||||
<i class="fa-solid fa-bell text-[10px]"></i>
|
||||
Notifications
|
||||
@php $unreadCount = auth()->user()->unreadNotifications()->count(); @endphp
|
||||
@if($unreadCount > 0)
|
||||
<span class="inline-flex items-center justify-center h-4 min-w-[16px] rounded-full bg-rose-500 px-1 text-[9px] font-bold text-white">
|
||||
{{ $unreadCount > 99 ? '99+' : $unreadCount }}
|
||||
</span>
|
||||
@endif
|
||||
</a>
|
||||
|
||||
<a href="{{ route('profile.likes') }}"
|
||||
class="shrink-0 inline-flex items-center gap-1.5 rounded-full px-4 py-2 text-xs font-medium transition-all duration-150
|
||||
@if(request()->routeIs('profile.likes'))
|
||||
bg-rose-600 text-white shadow-md shadow-rose-600/25
|
||||
@else
|
||||
bg-white/60 text-gray-600 dark:bg-neutral-900/60 dark:text-gray-400 hover:bg-white/90 dark:hover:bg-neutral-800/90
|
||||
@endif">
|
||||
<i class="fa-solid fa-heart text-[10px]"></i>
|
||||
{{ __('nav.likes') }}
|
||||
</a>
|
||||
|
||||
<a href="{{ route('user.watched') }}"
|
||||
class="shrink-0 inline-flex items-center gap-1.5 rounded-full px-4 py-2 text-xs font-medium transition-all duration-150
|
||||
@if(request()->routeIs('user.watched'))
|
||||
bg-rose-600 text-white shadow-md shadow-rose-600/25
|
||||
@else
|
||||
bg-white/60 text-gray-600 dark:bg-neutral-900/60 dark:text-gray-400 hover:bg-white/90 dark:hover:bg-neutral-800/90
|
||||
@endif">
|
||||
<i class="fa-solid fa-eye text-[10px]"></i>
|
||||
{{ __('nav.watched') }}
|
||||
</a>
|
||||
|
||||
<a href="{{ route('profile.comments') }}"
|
||||
class="shrink-0 inline-flex items-center gap-1.5 rounded-full px-4 py-2 text-xs font-medium transition-all duration-150
|
||||
@if(request()->routeIs('profile.comments'))
|
||||
bg-rose-600 text-white shadow-md shadow-rose-600/25
|
||||
@else
|
||||
bg-white/60 text-gray-600 dark:bg-neutral-900/60 dark:text-gray-400 hover:bg-white/90 dark:hover:bg-neutral-800/90
|
||||
@endif">
|
||||
<i class="fa-solid fa-comment text-[10px]"></i>
|
||||
{{ __('nav.comments') }}
|
||||
</a>
|
||||
|
||||
<a href="{{ route('profile.playlists') }}"
|
||||
class="shrink-0 inline-flex items-center gap-1.5 rounded-full px-4 py-2 text-xs font-medium transition-all duration-150
|
||||
@if(request()->routeIs('profile.playlists'))
|
||||
bg-rose-600 text-white shadow-md shadow-rose-600/25
|
||||
@else
|
||||
bg-white/60 text-gray-600 dark:bg-neutral-900/60 dark:text-gray-400 hover:bg-white/90 dark:hover:bg-neutral-800/90
|
||||
@endif">
|
||||
<i class="fa-solid fa-rectangle-list text-[10px]"></i>
|
||||
{{ __('nav.playlists') }}
|
||||
</a>
|
||||
|
||||
<a href="{{ route('profile.settings') }}"
|
||||
class="shrink-0 inline-flex items-center gap-1.5 rounded-full px-4 py-2 text-xs font-medium transition-all duration-150
|
||||
@if(request()->routeIs('profile.settings'))
|
||||
bg-rose-600 text-white shadow-md shadow-rose-600/25
|
||||
@else
|
||||
bg-white/60 text-gray-600 dark:bg-neutral-900/60 dark:text-gray-400 hover:bg-white/90 dark:hover:bg-neutral-800/90
|
||||
@endif">
|
||||
<i class="fa-solid fa-gear text-[10px]"></i>
|
||||
Settings
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -1,15 +1,68 @@
|
||||
<div
|
||||
class="overflow-hidden relative max-w-sm min-w-80 mx-auto bg-white/40 shadow-lg ring-1 ring-black/5 rounded-xl flex items-center gap-6 dark:bg-neutral-950/40 backdrop-blur dark:highlight-white/5">
|
||||
<img class="absolute -left-6 w-24 h-24 rounded-full shadow-lg" src="{{ $user->getAvatar() }}">
|
||||
<div class="flex flex-col py-5 pl-24">
|
||||
<strong class="text-slate-900 text-xl font-bold dark:text-slate-200">
|
||||
{{ $user->name }}
|
||||
@if ($user->hasRole(\App\Enums\UserRole::SUPPORTER))
|
||||
<a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i
|
||||
class="fa-solid fa-hand-holding-heart text-rose-600 animate-pulse"></i></a>
|
||||
<div class="overflow-hidden rounded-xl bg-white/40 shadow-lg ring-1 ring-black/5 dark:bg-neutral-950/40 backdrop-blur dark:ring-white/10">
|
||||
<div class="relative">
|
||||
{{-- Profile Banner Gradient --}}
|
||||
<div class="h-20 bg-gradient-to-br from-rose-500 via-rose-600 to-pink-600 dark:from-rose-700 dark:via-rose-800 dark:to-pink-800"></div>
|
||||
|
||||
{{-- Avatar overlapping the banner --}}
|
||||
<div class="flex justify-center -mt-10">
|
||||
<div class="relative">
|
||||
<img class="h-20 w-20 rounded-full border-4 border-white dark:border-neutral-900 shadow-lg object-cover bg-white dark:bg-neutral-800"
|
||||
src="{{ auth()->user()->getAvatar() }}"
|
||||
alt="{{ auth()->user()->name }}">
|
||||
@if(auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER))
|
||||
<span class="absolute -bottom-1 -right-1 flex h-7 w-7 items-center justify-center rounded-full bg-rose-600 text-white shadow-md ring-2 ring-white dark:ring-neutral-900"
|
||||
data-te-toggle="tooltip"
|
||||
title="Badge of appreciation for the horny people supporting us! :3">
|
||||
<i class="fa-solid fa-heart text-[11px]"></i>
|
||||
</span>
|
||||
@endif
|
||||
</strong>
|
||||
<span class="text-slate-500 text-sm font-medium dark:text-slate-400">Joined
|
||||
{{ $user->created_at->format('Y-m') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- User Info --}}
|
||||
<div class="px-4 pb-4 pt-2 text-center">
|
||||
<h2 class="text-base font-bold text-gray-900 dark:text-gray-100 truncate">
|
||||
{{ auth()->user()->name }}
|
||||
</h2>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
Joined {{ auth()->user()->created_at->format('F Y') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{-- Quick Stats Row --}}
|
||||
<div class="grid grid-cols-4 border-t border-gray-200/60 dark:border-neutral-800/60">
|
||||
<div class="py-3 text-center hover:bg-gray-50/50 dark:hover:bg-neutral-900/30 transition-colors cursor-default">
|
||||
<div class="text-sm font-bold text-gray-800 dark:text-gray-200">
|
||||
{{ number_format(auth()->user()->watched->count()) }}
|
||||
</div>
|
||||
<div class="text-[10px] font-medium uppercase tracking-wider text-gray-600 dark:text-gray-500">
|
||||
Views
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-3 text-center hover:bg-gray-50/50 dark:hover:bg-neutral-900/30 transition-colors cursor-default">
|
||||
<div class="text-sm font-bold text-gray-800 dark:text-gray-200">
|
||||
{{ number_format(auth()->user()->commentCount()) }}
|
||||
</div>
|
||||
<div class="text-[10px] font-medium uppercase tracking-wider text-gray-600 dark:text-gray-500">
|
||||
Cmts
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-3 text-center hover:bg-gray-50/50 dark:hover:bg-neutral-900/30 transition-colors cursor-default">
|
||||
<div class="text-sm font-bold text-gray-800 dark:text-gray-200">
|
||||
{{ number_format(auth()->user()->likes()) }}
|
||||
</div>
|
||||
<div class="text-[10px] font-medium uppercase tracking-wider text-gray-600 dark:text-gray-500">
|
||||
Likes
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-3 text-center hover:bg-gray-50/50 dark:hover:bg-neutral-900/30 transition-colors cursor-default">
|
||||
<div class="text-sm font-bold text-gray-800 dark:text-gray-200">
|
||||
{{ number_format(auth()->user()->playlists->count()) }}
|
||||
</div>
|
||||
<div class="text-[10px] font-medium uppercase tracking-wider text-gray-600 dark:text-gray-500">
|
||||
Lists
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,37 +1,96 @@
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
{{-- Profile Card --}}
|
||||
@include('profile.partials.profile')
|
||||
|
||||
<div
|
||||
class="overflow-hidden mt-5 relative max-w-sm min-w-80 mx-auto bg-white/40 shadow-lg ring-1 ring-black/5 rounded-xl items-center gap-6 dark:bg-neutral-950/40 backdrop-blur dark:highlight-white/5">
|
||||
<div class="flex flex-col p-2">
|
||||
{{-- Desktop Navigation (hidden on mobile) --}}
|
||||
<div class="hidden md:block">
|
||||
<nav
|
||||
class="mt-5 overflow-hidden rounded-xl bg-white/40 shadow-lg ring-1 ring-black/5 dark:bg-neutral-950/40 backdrop-blur dark:ring-white/10">
|
||||
<div class="flex flex-col p-1.5">
|
||||
<a href="{{ route('profile.show') }}"
|
||||
class="block cursor-pointer w-full px-4 py-2 rounded-lg text-left text-lg leading-5 text-gray-700 dark:text-gray-300 @if (request()->routeIs('profile.show')) bg-rose-900/40 @endif hover:bg-neutral-100/60 dark:hover:bg-neutral-900/60 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out"><i
|
||||
class="fa-solid fa-user pr-4"></i> {{ __('nav.profile') }}</a>
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-150
|
||||
@if(request()->routeIs('profile.show'))
|
||||
bg-rose-600/10 text-rose-600 dark:bg-rose-500/15 dark:text-rose-400 border-l-[3px] border-rose-600 dark:border-rose-400 ml-[-3px]
|
||||
@else
|
||||
text-gray-700 dark:text-gray-300 hover:bg-gray-100/60 dark:hover:bg-neutral-800/60 border-l-[3px] border-transparent
|
||||
@endif">
|
||||
<i class="fa-solid fa-user w-5 text-center text-base
|
||||
@if(request()->routeIs('profile.show')) text-rose-600 dark:text-rose-400 @else text-gray-400 dark:text-gray-500 @endif"></i>
|
||||
<span>{{ __('nav.profile') }}</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('profile.notifications') }}"
|
||||
class="block cursor-pointer w-full px-4 py-2 rounded-lg text-left text-lg leading-5 text-gray-700 dark:text-gray-300 @if (request()->routeIs('profile.notifications')) bg-rose-900/40 @endif hover:bg-neutral-100/60 dark:hover:bg-neutral-900/60 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out"><i
|
||||
class="fa-solid fa-bell pr-4"></i> Notifications</a>
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-150
|
||||
@if(request()->routeIs('profile.notifications'))
|
||||
bg-rose-600/10 text-rose-600 dark:bg-rose-500/15 dark:text-rose-400 border-l-[3px] border-rose-600 dark:border-rose-400 ml-[-3px]
|
||||
@else
|
||||
text-gray-700 dark:text-gray-300 hover:bg-gray-100/60 dark:hover:bg-neutral-800/60 border-l-[3px] border-transparent
|
||||
@endif">
|
||||
<i class="fa-solid fa-bell w-5 text-center text-base
|
||||
@if(request()->routeIs('profile.notifications')) text-rose-600 dark:text-rose-400 @else text-gray-400 dark:text-gray-500 @endif"></i>
|
||||
<span>Notifications</span>
|
||||
@php $unreadCount = auth()->user()->unreadNotifications()->count(); @endphp
|
||||
@if($unreadCount > 0)
|
||||
<span class="ml-auto inline-flex items-center justify-center h-5 min-w-[20px] rounded-full bg-rose-600 px-1.5 text-[10px] font-bold text-white">
|
||||
{{ $unreadCount > 99 ? '99+' : $unreadCount }}
|
||||
</span>
|
||||
@endif
|
||||
</a>
|
||||
|
||||
<a href="{{ route('profile.likes') }}"
|
||||
class="block cursor-pointer w-full px-4 py-2 rounded-lg text-left text-lg leading-5 text-gray-700 dark:text-gray-300 @if (request()->routeIs('profile.likes')) bg-rose-900/40 @endif hover:bg-neutral-100/60 dark:hover:bg-neutral-900/60 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out"><i
|
||||
class="fa-solid fa-heart pr-4"></i> {{ __('nav.likes') }}</a>
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-150
|
||||
@if(request()->routeIs('profile.likes'))
|
||||
bg-rose-600/10 text-rose-600 dark:bg-rose-500/15 dark:text-rose-400 border-l-[3px] border-rose-600 dark:border-rose-400 ml-[-3px]
|
||||
@else
|
||||
text-gray-700 dark:text-gray-300 hover:bg-gray-100/60 dark:hover:bg-neutral-800/60 border-l-[3px] border-transparent
|
||||
@endif">
|
||||
<i class="fa-solid fa-heart w-5 text-center text-base
|
||||
@if(request()->routeIs('profile.likes')) text-rose-600 dark:text-rose-400 @else text-gray-400 dark:text-gray-500 @endif"></i>
|
||||
<span>{{ __('nav.likes') }}</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('user.watched') }}"
|
||||
class="block cursor-pointer w-full px-4 py-2 rounded-lg text-left text-lg leading-5 text-gray-700 dark:text-gray-300 @if (request()->routeIs('user.watched')) bg-rose-900/40 @endif hover:bg-neutral-100/60 dark:hover:bg-neutral-900/60 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out"><i
|
||||
class="fa-solid fa-eye pr-4"></i>
|
||||
{{ __('nav.watched') }}</a>
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-150
|
||||
@if(request()->routeIs('user.watched'))
|
||||
bg-rose-600/10 text-rose-600 dark:bg-rose-500/15 dark:text-rose-400 border-l-[3px] border-rose-600 dark:border-rose-400 ml-[-3px]
|
||||
@else
|
||||
text-gray-700 dark:text-gray-300 hover:bg-gray-100/60 dark:hover:bg-neutral-800/60 border-l-[3px] border-transparent
|
||||
@endif">
|
||||
<i class="fa-solid fa-eye w-5 text-center text-base
|
||||
@if(request()->routeIs('user.watched')) text-rose-600 dark:text-rose-400 @else text-gray-400 dark:text-gray-500 @endif"></i>
|
||||
<span>{{ __('nav.watched') }}</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('profile.comments') }}"
|
||||
class="block cursor-pointer w-full px-4 py-2 rounded-lg text-left text-lg leading-5 text-gray-700 dark:text-gray-300 @if (request()->routeIs('profile.comments')) bg-rose-900/40 @endif hover:bg-neutral-100/60 dark:hover:bg-neutral-900/60 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out"><i
|
||||
class="fa-solid fa-comment pr-4"></i>
|
||||
{{ __('nav.comments') }}</a>
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-150
|
||||
@if(request()->routeIs('profile.comments'))
|
||||
bg-rose-600/10 text-rose-600 dark:bg-rose-500/15 dark:text-rose-400 border-l-[3px] border-rose-600 dark:border-rose-400 ml-[-3px]
|
||||
@else
|
||||
text-gray-700 dark:text-gray-300 hover:bg-gray-100/60 dark:hover:bg-neutral-800/60 border-l-[3px] border-transparent
|
||||
@endif">
|
||||
<i class="fa-solid fa-comment w-5 text-center text-base
|
||||
@if(request()->routeIs('profile.comments')) text-rose-600 dark:text-rose-400 @else text-gray-400 dark:text-gray-500 @endif"></i>
|
||||
<span>{{ __('nav.comments') }}</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('profile.playlists') }}"
|
||||
class="block cursor-pointer w-full px-4 py-2 rounded-lg text-left text-lg leading-5 text-gray-700 dark:text-gray-300 @if (request()->routeIs('profile.playlists')) bg-rose-900/40 @endif hover:bg-neutral-100/60 dark:hover:bg-neutral-900/60 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out"><i
|
||||
class="fa-solid fa-rectangle-list pr-4"></i>
|
||||
{{ __('nav.playlists') }}</a>
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-150
|
||||
@if(request()->routeIs('profile.playlists'))
|
||||
bg-rose-600/10 text-rose-600 dark:bg-rose-500/15 dark:text-rose-400 border-l-[3px] border-rose-600 dark:border-rose-400 ml-[-3px]
|
||||
@else
|
||||
text-gray-700 dark:text-gray-300 hover:bg-gray-100/60 dark:hover:bg-neutral-800/60 border-l-[3px] border-transparent
|
||||
@endif">
|
||||
<i class="fa-solid fa-rectangle-list w-5 text-center text-base
|
||||
@if(request()->routeIs('profile.playlists')) text-rose-600 dark:text-rose-400 @else text-gray-400 dark:text-gray-500 @endif"></i>
|
||||
<span>{{ __('nav.playlists') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{{-- Actions --}}
|
||||
@include('profile.partials.actions')
|
||||
</div>
|
||||
|
||||
@include('profile.partials.actions')
|
||||
{{-- Mobile Navigation (horizontal scroll tabs, visible only on mobile) --}}
|
||||
@include('profile.partials.mobile-nav')
|
||||
</div>
|
||||
@@ -1,34 +1,43 @@
|
||||
<div>
|
||||
<div class="grid-cols-1 sm:grid md:grid-cols-3 ">
|
||||
@if(count($playlists) > 0)
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
@foreach($playlists as $playlist)
|
||||
@php
|
||||
$count = $playlist->episodes->count();
|
||||
@endphp
|
||||
<div class="mx-3 mt-6 flex flex-col rounded-lg bg-white/60 shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)] dark:bg-neutral-950/60 sm:shrink-0 sm:grow sm:basis-0">
|
||||
@if($count > 0)
|
||||
<a href="{{ route('profile.playlist.show', $playlist->id) }}">
|
||||
@else
|
||||
<a href="#!">
|
||||
@endif
|
||||
<div class="group rounded-xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 overflow-hidden hover:shadow-md hover:ring-rose-300/50 dark:hover:ring-rose-700/30 transition-all duration-200 flex flex-col">
|
||||
{{-- Thumbnail --}}
|
||||
@if($count > 0)
|
||||
<a href="{{ route('profile.playlist.show', $playlist->id) }}" class="block overflow-hidden">
|
||||
@php
|
||||
$pe = \App\Models\PlaylistEpisode::where('playlist_id', $playlist->id)->orderBy('position', 'desc')->first();
|
||||
@endphp
|
||||
<img class="rounded-t-lg aspect-video" src="{{ $pe->episode->gallery->first()->thumbnail_url }}" alt="Hollywood Sign on The Hill" />
|
||||
@else
|
||||
<img src="/images/hentai/sukebe-elf-tanbouki/gallery-ep-1-0.webp" class="rounded-t-lg opacity-50 dark:opacity-20" alt="..." />
|
||||
@endif
|
||||
<img class="w-full aspect-video object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
src="{{ $pe->episode->gallery->first()->thumbnail_url }}"
|
||||
alt="{{ $playlist->name }}"
|
||||
loading="lazy" />
|
||||
</a>
|
||||
<div class="p-6" x-data="{ editing: false, name: '{{ $playlist->name }}', isPrivate: {{ $playlist->is_private ? 'true' : 'false' }} }">
|
||||
<!-- Edit mode -->
|
||||
@else
|
||||
<div class="w-full aspect-video bg-gray-200 dark:bg-neutral-800 flex items-center justify-center">
|
||||
<img src="/images/hentai/sukebe-elf-tanbouki/gallery-ep-1-0.webp"
|
||||
class="w-full aspect-video object-cover opacity-30 dark:opacity-15"
|
||||
alt="Empty playlist" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Content --}}
|
||||
<div class="p-4 flex-1 flex flex-col"
|
||||
x-data="{ editing: false, name: '{{ $playlist->name }}', isPrivate: {{ $playlist->is_private ? 'true' : 'false' }} }">
|
||||
|
||||
{{-- Edit mode --}}
|
||||
<template x-if="editing">
|
||||
<div>
|
||||
<form method="POST" action="{{ route('profile.playlist.update', $playlist->id) }}" class="space-y-3">
|
||||
<div class="flex-1 flex flex-col">
|
||||
<form method="POST" action="{{ route('profile.playlist.update', $playlist->id) }}" class="flex-1 flex flex-col">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<div class="space-y-3 flex-1">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-neutral-600 dark:text-neutral-400">Name:</label>
|
||||
<label class="mb-1 block text-xs font-medium text-neutral-500 dark:text-neutral-400">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
@@ -39,7 +48,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-neutral-600 dark:text-neutral-400">Visibility:</label>
|
||||
<label class="mb-1 block text-xs font-medium text-neutral-500 dark:text-neutral-400">Visibility</label>
|
||||
<select
|
||||
name="is_private"
|
||||
x-model="isPrivate"
|
||||
@@ -49,11 +58,12 @@
|
||||
<option value="1">Private</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="cursor-pointer rounded-lg bg-rose-600 px-4 py-2 text-xs font-semibold text-white transition hover:bg-rose-700">
|
||||
</div>
|
||||
<div class="flex gap-2 mt-3 pt-3 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<button type="submit" class="cursor-pointer rounded-lg bg-rose-600 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-rose-700 flex-1">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" @click="editing = false; name = '{{ $playlist->name }}'; isPrivate = {{ $playlist->is_private ? 'true' : 'false' }}" class="cursor-pointer rounded-lg border border-neutral-300 px-4 py-2 text-xs font-medium text-neutral-700 transition hover:bg-neutral-100 dark:border-neutral-600 dark:text-neutral-200 dark:hover:bg-neutral-800">
|
||||
<button type="button" @click="editing = false; name = '{{ $playlist->name }}'; isPrivate = {{ $playlist->is_private ? 'true' : 'false' }}" class="cursor-pointer rounded-lg border border-neutral-300 px-3 py-1.5 text-xs font-medium text-neutral-600 transition hover:bg-neutral-100 dark:border-neutral-600 dark:text-neutral-200 dark:hover:bg-neutral-800 flex-1">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
@@ -61,52 +71,81 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Display mode -->
|
||||
{{-- Display mode --}}
|
||||
<template x-if="!editing">
|
||||
<div>
|
||||
<h5 class="mb-2 text-xl font-medium leading-tight text-neutral-800 dark:text-neutral-50">
|
||||
<div class="flex-1 flex flex-col">
|
||||
<div class="flex-1">
|
||||
<h5 class="text-base font-semibold text-neutral-800 dark:text-neutral-50 truncate">
|
||||
{{ $playlist->name }}
|
||||
</h5>
|
||||
<p class="mb-2 text-sm leading-tight text-neutral-800 dark:text-neutral-50">
|
||||
{{ $count }} Episodes - {{ $playlist->is_private == 1 ? 'Private' : 'Public' }}
|
||||
@if($count > 0)
|
||||
<a href="{{ route('hentai.index', ['title' => $playlist->episodes->first()->episode->slug, 'playlist' => $playlist->id]) }}" class="cursor-pointer float-right text-white bg-rose-700 hover:bg-rose-800 focus:ring-4 focus:outline-none focus:ring-rose-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-rose-600 dark:hover:bg-rose-700 dark:focus:ring-rose-800">{{ __('playlist.play') }}</a>
|
||||
@endif
|
||||
</p>
|
||||
<div class="flex items-center gap-2 mt-1.5">
|
||||
<span class="inline-flex items-center gap-1 pl-2 pr-2 pt-1 pb-1 rounded-full bg-neutral-100 dark:bg-neutral-700 px-2 py-0.5 text-[11px] font-medium text-neutral-600 dark:text-neutral-300">
|
||||
<i class="fa-solid fa-film text-[9px]"></i>
|
||||
{{ $count }} {{ Str::plural('ep', $count) }}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1 pl-2 pr-2 pt-1 pb-1 rounded-full text-[11px] font-medium
|
||||
{{ $playlist->is_private ? 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400' : 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' }}">
|
||||
<i class="fa-solid fa-{{ $playlist->is_private ? 'lock' : 'globe' }} text-[9px]"></i>
|
||||
{{ $playlist->is_private ? 'Private' : 'Public' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-3 pt-3 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ route('profile.playlist.delete', $playlist->id) }}" class="inline-flex items-center cursor-pointer text-xs text-red-600" data-confirm-delete="true">Delete</a>
|
||||
<button @click="editing = true" class="inline-flex items-center cursor-pointer text-xs text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">Edit</button>
|
||||
<a href="{{ route('profile.playlist.delete', $playlist->id) }}"
|
||||
class="inline-flex items-center gap-1 text-[11px] text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||
data-confirm-delete="true">
|
||||
<i class="fa-solid fa-trash-can text-[10px]"></i>
|
||||
Delete
|
||||
</a>
|
||||
<button @click="editing = true"
|
||||
class="inline-flex items-center gap-1 text-[11px] text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">
|
||||
<i class="fa-solid fa-pen-to-square text-[10px]"></i>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
@if($count > 0)
|
||||
<a href="{{ route('hentai.index', ['title' => $playlist->episodes->first()->episode->slug, 'playlist' => $playlist->id]) }}"
|
||||
class="inline-flex items-center gap-1 rounded-lg bg-rose-600 px-3 py-1.5 text-[11px] font-semibold text-white hover:bg-rose-700 transition-colors shadow-sm shadow-rose-600/20">
|
||||
<i class="fa-solid fa-play text-[9px]"></i>
|
||||
Play
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<!-- Add Another Playlist -->
|
||||
<div class="mx-3 mt-6 flex flex-col rounded-lg bg-white/60 shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)] dark:bg-neutral-950/60 sm:shrink-0 sm:grow sm:basis-0">
|
||||
<img src="/images/hentai/sukebe-elf-tanbouki/gallery-ep-1-0.webp" class="rounded-t-lg opacity-50 dark:opacity-40" alt="..." />
|
||||
<div class="p-6">
|
||||
<p class="text-black dark:text-white">
|
||||
Create another Playlist
|
||||
</p>
|
||||
<a data-te-toggle="modal" data-te-target="#modalCreatePlaylist" data-te-ripple-init data-te-ripple-color="light" class="inline-flex items-center cursor-pointer px-4 py-2 mt-2 bg-rose-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-rose-700 active:bg-rose-900 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150">
|
||||
Create
|
||||
</a>
|
||||
|
||||
{{-- Add New Playlist Card --}}
|
||||
<button
|
||||
data-te-toggle="modal"
|
||||
data-te-target="#modalCreatePlaylist"
|
||||
class="group rounded-xl border-2 border-dashed border-neutral-300 dark:border-neutral-700 bg-white/20 dark:bg-neutral-950/20 backdrop-blur hover:border-rose-400 dark:hover:border-rose-600 hover:bg-rose-50/50 dark:hover:bg-rose-950/20 transition-all duration-200 flex flex-col items-center justify-center p-8 min-h-[200px]">
|
||||
<div class="flex h-14 w-14 items-center justify-center rounded-full bg-rose-100 dark:bg-rose-900/30 text-rose-600 dark:text-rose-400 group-hover:scale-110 transition-transform duration-200 mb-3">
|
||||
<i class="fa-solid fa-plus text-xl"></i>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-neutral-600 dark:text-neutral-300">Create Playlist</p>
|
||||
<p class="text-xs text-neutral-400 dark:text-neutral-500 mt-1">Organize your favorites</p>
|
||||
</button>
|
||||
</div>
|
||||
@else
|
||||
<!-- No Playlist Found -->
|
||||
<div class="mx-3 mt-6 flex flex-col rounded-lg bg-white shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)] dark:bg-neutral-700 sm:shrink-0 sm:grow sm:basis-0">
|
||||
<img src="/images/hentai/sukebe-elf-tanbouki/gallery-ep-1-0.webp" class="rounded-t-lg opacity-50 dark:opacity-20" alt="..." />
|
||||
<div class="p-6">
|
||||
<p class="text-black dark:text-white">
|
||||
No Playlist found!
|
||||
</p>
|
||||
<a data-te-toggle="modal" data-te-target="#modalCreatePlaylist" data-te-ripple-init data-te-ripple-color="light" class="inline-flex items-center cursor-pointer px-4 py-2 mt-2 bg-rose-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-rose-700 active:bg-rose-900 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150">
|
||||
Create
|
||||
</a>
|
||||
{{-- Empty State --}}
|
||||
<div class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 p-12 text-center">
|
||||
<div class="inline-flex h-20 w-20 items-center justify-center rounded-full bg-gray-100 dark:bg-neutral-800 mb-4">
|
||||
<i class="fa-solid fa-rectangle-list text-3xl text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">No playlists yet</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400 mb-6">Create your first playlist to organize your favorite episodes.</p>
|
||||
<button
|
||||
data-te-toggle="modal"
|
||||
data-te-target="#modalCreatePlaylist"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg bg-rose-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-rose-700 transition-colors shadow-sm shadow-rose-600/20">
|
||||
<i class="fa-solid fa-plus text-xs"></i>
|
||||
Create your first playlist
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +1,24 @@
|
||||
<x-app-layout>
|
||||
@include('partials.background')
|
||||
<div class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-10 flex flex-row">
|
||||
<div class="flex flex-col md:flex-row">
|
||||
@include('profile.partials.sidebar')
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
|
||||
<x-profile-layout>
|
||||
<div class="space-y-5">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<i class="fa-solid fa-rectangle-list text-rose-500"></i>
|
||||
{{ __('nav.playlists') }}
|
||||
</h2>
|
||||
<button
|
||||
data-te-toggle="modal"
|
||||
data-te-target="#modalCreatePlaylist"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg bg-rose-600 px-4 py-2 text-sm font-semibold text-white shadow-sm shadow-rose-600/20 hover:bg-rose-700 transition-colors">
|
||||
<i class="fa-solid fa-plus text-xs"></i>
|
||||
New Playlist
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Content --}}
|
||||
@include('profile.partials.user-playlists')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Modal --}}
|
||||
@include('modals.create-playlist')
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
</x-profile-layout>
|
||||
@@ -1,30 +1,126 @@
|
||||
<x-app-layout>
|
||||
@include('partials.background')
|
||||
<div class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-10 mb-14 flex flex-row">
|
||||
<div class="flex flex-col md:flex-row">
|
||||
@include('profile.partials.sidebar')
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 mt-8 md:mt-0 space-y-6">
|
||||
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
|
||||
<x-profile-layout>
|
||||
<div class="space-y-5">
|
||||
{{-- Header --}}
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<i class="fa-solid fa-gear text-rose-500"></i>
|
||||
Settings
|
||||
</h2>
|
||||
|
||||
{{-- Settings Sections --}}
|
||||
<div class="space-y-5">
|
||||
|
||||
{{-- Profile Information --}}
|
||||
<div class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 overflow-hidden">
|
||||
<div class="p-5 sm:p-6 border-b border-neutral-200/60 dark:border-neutral-800/60">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-rose-100 dark:bg-rose-900/30 text-rose-600 dark:text-rose-400">
|
||||
<i class="fa-solid fa-user-pen text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Profile Information</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Update your name, email and avatar</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 sm:p-6">
|
||||
@include('profile.partials.update-profile-information-form')
|
||||
</div>
|
||||
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
|
||||
</div>
|
||||
|
||||
{{-- Passkeys --}}
|
||||
<div class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 overflow-hidden">
|
||||
<div class="p-5 sm:p-6 border-b border-neutral-200/60 dark:border-neutral-800/60">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-sky-100 dark:bg-sky-900/30 text-sky-600 dark:text-sky-400">
|
||||
<i class="fa-solid fa-key text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Passkeys</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Manage WebAuthn passkeys for passwordless login</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 sm:p-6">
|
||||
<livewire:passkeys />
|
||||
</div>
|
||||
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
|
||||
</div>
|
||||
|
||||
{{-- Password --}}
|
||||
<div class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 overflow-hidden">
|
||||
<div class="p-5 sm:p-6 border-b border-neutral-200/60 dark:border-neutral-800/60">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400">
|
||||
<i class="fa-solid fa-lock text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Update Password</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Keep your account secure</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 sm:p-6">
|
||||
@include('profile.partials.update-password-form')
|
||||
</div>
|
||||
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
|
||||
</div>
|
||||
|
||||
{{-- Search Blacklist --}}
|
||||
<div class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 overflow-hidden">
|
||||
<div class="p-5 sm:p-6 border-b border-neutral-200/60 dark:border-neutral-800/60">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400">
|
||||
<i class="fa-solid fa-shield text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Search Blacklist</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Hide content with specific tags from search results</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 sm:p-6">
|
||||
@include('profile.partials.update-blacklist-form')
|
||||
</div>
|
||||
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
|
||||
</div>
|
||||
|
||||
{{-- Website Design --}}
|
||||
<div class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-black/5 dark:ring-white/5 overflow-hidden">
|
||||
<div class="p-5 sm:p-6 border-b border-neutral-200/60 dark:border-neutral-800/60">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400">
|
||||
<i class="fa-solid fa-object-group text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Website Design</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Customize your browsing experience</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 sm:p-6">
|
||||
@include('profile.partials.update-design-form')
|
||||
</div>
|
||||
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
|
||||
</div>
|
||||
|
||||
{{-- Danger Zone --}}
|
||||
<div class="rounded-2xl bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow-sm ring-1 ring-red-200/60 dark:ring-red-800/30 overflow-hidden">
|
||||
<div class="p-5 sm:p-6 border-b border-red-200/60 dark:border-red-800/30">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400">
|
||||
<i class="fa-solid fa-triangle-exclamation text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-red-700 dark:text-red-400">Danger Zone</h3>
|
||||
<p class="text-xs text-red-500 dark:text-red-400">Irreversible actions - proceed with caution</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 sm:p-6">
|
||||
@include('profile.partials.delete-user-form')
|
||||
</div>
|
||||
@include('profile.partials.delete-user-modal')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Delete Account Modal --}}
|
||||
@include('profile.partials.delete-user-modal')
|
||||
|
||||
@vite(['resources/js/user-blacklist.js'])
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
</x-profile-layout>
|
||||
@@ -1,9 +1,12 @@
|
||||
<x-app-layout>
|
||||
@include('partials.background')
|
||||
<div class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-10 flex flex-row">
|
||||
<div class="flex flex-col md:flex-row">
|
||||
@include('profile.partials.sidebar')
|
||||
<x-profile-layout>
|
||||
<div class="space-y-5">
|
||||
{{-- Header --}}
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<i class="fa-solid fa-eye text-rose-500"></i>
|
||||
{{ __('nav.watched') }}
|
||||
</h2>
|
||||
|
||||
{{-- Content from Livewire --}}
|
||||
@livewire('watched', ['user' => $user])
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
</x-profile-layout>
|
||||
Reference in New Issue
Block a user