Compare commits

..

28 Commits

Author SHA1 Message Date
af739e3c88 Remove old comments repo in composer.json 2026-01-18 18:49:59 +01:00
273ed65a8d Remove laravel sail 2026-01-18 18:44:59 +01:00
ccfd5b996b Replace captcha system 2026-01-18 18:37:08 +01:00
e5ef197ed6 Add user roles system 2026-01-16 23:14:47 +01:00
c0be2e294a Refactor routes 2026-01-16 23:01:34 +01:00
823a284fbc Fix PHP 8.4 deprecation warning 2026-01-11 16:39:14 +01:00
67e601d0c4 Add ability to properly set locale (session/account) 2026-01-11 16:33:13 +01:00
7e4ebd91ad Remove vluzrmos/language-detector package 2026-01-11 16:31:16 +01:00
4dc5dee2b9 Update dependencies 2026-01-11 15:46:33 +01:00
5310908b0c Update login button on mobile 2026-01-11 00:21:05 +01:00
4b05b3db6d Fix cache not flushed after comment delete by admin 2026-01-10 23:24:31 +01:00
df47a926e4 Fix comment mass delete 2026-01-10 23:21:29 +01:00
1e9e95f35f Fix admin comment moderation 2026-01-10 22:35:35 +01:00
2aa76baafd Fix account deletion anonymizing comments 2026-01-10 22:35:21 +01:00
aa50bb1f72 Merge pull request 'Replace Comment System' (#4) from comment-system into main
Reviewed-on: #4
2026-01-10 21:16:55 +00:00
dfedf4058e Fix rate limit and make it more strict (1 message in 5 minutes) 2026-01-10 22:15:50 +01:00
268e3eb4c2 Add Notification for comments 2026-01-10 22:00:09 +01:00
ab61574956 Fix comment depth chain check 2026-01-10 21:59:53 +01:00
81038b6c26 Add id to comment, so it can autoscroll to that notification 2026-01-10 21:59:08 +01:00
e949ba955a Add rate limiter to comment system 2026-01-10 21:04:26 +01:00
819e2fde27 Misc changes 2026-01-10 20:33:35 +01:00
3259e2197b Update design comments home page 2026-01-10 19:45:19 +01:00
b133db0573 Add likes to comments 2026-01-10 19:41:23 +01:00
41c34e6d89 Fix style 2026-01-10 19:15:32 +01:00
db6da608aa Add comments to home page 2026-01-10 19:11:28 +01:00
13b70fdf23 Misc changes 2026-01-10 18:55:53 +01:00
cfd6af59fb Add Profile Comment Search (Livewire) 2026-01-10 18:55:47 +01:00
7810cd53fb Add comments to Hentai 2026-01-10 18:54:48 +01:00
60 changed files with 2385 additions and 2497 deletions

11
app/Enums/UserRole.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum UserRole: string
{
case ADMINISTRATOR = 'admin';
case MODERATOR = 'moderator';
case SUPPORTER = 'supporter';
case BANNED = 'banned';
}

View File

@@ -2,6 +2,7 @@
namespace App\Helpers; namespace App\Helpers;
use App\Models\Comment;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Hentai; use App\Models\Hentai;
use App\Models\PopularMonthly; use App\Models\PopularMonthly;
@@ -126,7 +127,7 @@ class CacheHelper
public static function getLatestComments() public static function getLatestComments()
{ {
return Cache::remember("latest_comments", now()->addMinutes(60), function () { return Cache::remember("latest_comments", now()->addMinutes(60), function () {
return DB::table('comments')->latest()->take(10)->get(); return Comment::latest()->take(10)->get();
}); });
} }
} }

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Enums\UserRole;
use App\Models\User; use App\Models\User;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -31,11 +32,11 @@ class UserController extends Controller
switch ($validated['action']) { switch ($validated['action']) {
case 'ban': case 'ban':
$user->update(['is_banned' => 1]); $user->addRole(UserRole::BANNED);
alert()->success('Banned', 'User has been banned.'); alert()->success('Banned', 'User has been banned.');
break; break;
case 'unban': case 'unban':
$user->update(['is_banned' => 0]); $user->removeRole(UserRole::BANNED);
alert()->success('Unbanned', 'User has been unbanned.'); alert()->success('Unbanned', 'User has been unbanned.');
break; break;
default: default:

View File

@@ -8,6 +8,8 @@ use App\Models\Episode;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use GrantHolle\Altcha\Rules\ValidAltcha;
class DownloadApiController extends Controller class DownloadApiController extends Controller
{ {
/** /**
@@ -16,11 +18,12 @@ class DownloadApiController extends Controller
public function getDownload(Request $request) public function getDownload(Request $request)
{ {
$validated = $request->validate([ $validated = $request->validate([
'episode_id' => 'required', 'episode_id' => ['required'],
'captcha' => 'required|captcha' 'captcha' => ['required', new ValidAltcha],
]); ]);
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail(); $episode = Episode::where('id', $request->input('episode_id'))
->firstOrFail();
// Increase download count, as we assume the user // Increase download count, as we assume the user
// downloads after submitting the captcha // downloads after submitting the captcha

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Enums\UserRole;
use App\Models\User; use App\Models\User;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@@ -88,10 +89,7 @@ class DiscordAuthController extends Controller
// User is not in the guild // User is not in the guild
if ($response->status() === 404) { if ($response->status() === 404) {
$user->update([ $user->removeRole(UserRole::SUPPORTER);
'is_patreon' => false,
]);
return; return;
} }
@@ -110,21 +108,15 @@ class DiscordAuthController extends Controller
$discordRoles = $response->json('roles', []); $discordRoles = $response->json('roles', []);
$patreonRoles = config('discord.patreon_roles', []); $patreonRoles = config('discord.patreon_roles', []);
$isPatreon = false; // If intersect of array is empty, then the user doesn't have the role
foreach($patreonRoles as $patreonRole) $hasSupporterRole = !empty(array_intersect($discordRoles, $patreonRoles));
{
if (in_array($patreonRole, $discordRoles, true)) { if (!$hasSupporterRole) {
$isPatreon = true; // Remove role if not found
break; $user->removeRole(UserRole::SUPPORTER);
} return;
} }
// Only update if something actually changed $user->addRole(UserRole::SUPPORTER);
if ($user->is_patreon !== $isPatreon) {
$user->update([
'is_patreon' => $isPatreon,
]);
} }
} }
}

View File

@@ -11,6 +11,8 @@ use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules; use Illuminate\Validation\Rules;
use GrantHolle\Altcha\Rules\ValidAltcha;
class RegisteredUserController extends Controller class RegisteredUserController extends Controller
{ {
/** /**
@@ -24,6 +26,7 @@ class RegisteredUserController extends Controller
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()], 'password' => ['required', 'confirmed', Rules\Password::defaults()],
'altcha' => ['required', new ValidAltcha],
]); ]);
$user = User::create([ $user = User::create([

View File

@@ -5,6 +5,8 @@ namespace App\Http\Controllers;
use App\Models\Contact; use App\Models\Contact;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use GrantHolle\Altcha\Rules\ValidAltcha;
class ContactController extends Controller class ContactController extends Controller
{ {
/** /**
@@ -25,7 +27,7 @@ class ContactController extends Controller
'email' => 'required|max:50', 'email' => 'required|max:50',
'message' => 'required|max:1000', 'message' => 'required|max:1000',
'subject' => 'required|max:50', 'subject' => 'required|max:50',
'captcha' => 'required|captcha', 'altcha' => ['required', new ValidAltcha],
]); ]);
$contact = new Contact(); $contact = new Contact();
@@ -37,9 +39,4 @@ class ContactController extends Controller
return back()->with('status', 'contact-submitted'); return back()->with('status', 'contact-submitted');
} }
public function reloadCaptcha(): \Illuminate\Http\JsonResponse
{
return response()->json(['captcha'=> captcha_img()]);
}
} }

View File

@@ -111,11 +111,13 @@ class HomeController extends Controller
*/ */
public function updateLanguage(Request $request): \Illuminate\Http\RedirectResponse public function updateLanguage(Request $request): \Illuminate\Http\RedirectResponse
{ {
if(! in_array($request->language, config('lang-detector.languages'))) { abort_unless(in_array($request->language, config('app.supported_locales'), true), 404);
return redirect()->back();
}
Cookie::queue(Cookie::forever('locale', $request->language)); session(['locale' => $request->language]);
if (Auth::check()) {
Auth::user()->update(['locale' => $request->language]);
}
return redirect()->back(); return redirect()->back();
} }

View File

@@ -3,8 +3,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Playlist;
use App\Models\PlaylistEpisode;
use App\Models\User; use App\Models\User;
use App\Http\Requests\ProfileUpdateRequest; use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
@@ -153,7 +151,7 @@ class ProfileController extends Controller
} }
// Update comments to deleted user // Update comments to deleted user
DB::table('comments')->where('commenter_id', '=', $user->id)->update(['commenter_id' => 1]); DB::table('comments')->where('user_id', '=', $user->id)->update(['user_id' => 1]);
// Delete Profile Picture // Delete Profile Picture
if ($user->avatar) { if ($user->avatar) {

View File

@@ -37,6 +37,7 @@ class Kernel extends HttpKernel
\App\Http\Middleware\VerifyCsrfToken::class, \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\IsBanned::class, \App\Http\Middleware\IsBanned::class,
\App\Http\Middleware\SetLocale::class,
], ],
'api' => [ 'api' => [
@@ -58,6 +59,7 @@ class Kernel extends HttpKernel
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'auth.admin' => \App\Http\Middleware\IsAdmin::class, 'auth.admin' => \App\Http\Middleware\IsAdmin::class,
'auth.moderator' => \App\Http\Middleware\IsModerator::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,

View File

@@ -1,28 +1,14 @@
<?php namespace app\Http\Middleware; <?php namespace app\Http\Middleware;
use App\Enums\UserRole;
use Closure; use Closure;
use Illuminate\Contracts\Auth\Guard; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin { class IsAdmin {
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
* @return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
@@ -30,15 +16,14 @@ class IsAdmin {
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle($request, Closure $next) public function handle(Request $request, Closure $next): Response
{ {
if( ! $this->auth->user()->is_admin) if(Auth::check() && Auth::user()->hasRole(UserRole::ADMINISTRATOR))
{ {
session()->flash('error_msg','This resource is restricted to Administrators!');
return redirect()->route('home.index');
}
return $next($request); return $next($request);
} }
session()->flash('error_msg','This resource is restricted to Administrators!');
return redirect()->route('home.index');
}
} }

View File

@@ -1,8 +1,11 @@
<?php namespace app\Http\Middleware; <?php namespace app\Http\Middleware;
use App\Enums\UserRole;
use Closure; use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Contracts\Auth\Guard; use Symfony\Component\HttpFoundation\Response;
class IsBanned { class IsBanned {
@@ -13,9 +16,9 @@ class IsBanned {
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle($request, Closure $next) public function handle(Request $request, Closure $next): Response
{ {
if(auth()->check() && auth()->user()->is_banned == 1) if(Auth::check() && Auth::user()->hasRole(UserRole::BANNED))
{ {
Auth::logout(); Auth::logout();
$request->session()->invalidate(); $request->session()->invalidate();

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use App\Enums\UserRole;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class IsModerator
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (Auth::check() && Auth::user()->hasRole(UserRole::MODERATOR))
{
return $next($request);
}
session()->flash('error_msg','This resource is restricted to Administrators!');
return redirect()->route('home.index');
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// 1. Logged-in user preference
if (Auth::check() && Auth::user()->locale) {
App::setLocale(Auth::user()->locale);
return $next($request);
}
// 2. Session (guest or user override)
if (session()->has('locale') && in_array($request->language, config('app.supported_locales'), true)) {
App::setLocale(session('locale'));
return $next($request);
}
// 3. Browser language
$locale = $request->getPreferredLanguage(config('app.supported_locales'));
if ($locale) {
App::setLocale($locale);
}
return $next($request);
}
}

View File

@@ -9,6 +9,8 @@ use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use GrantHolle\Altcha\Rules\ValidAltcha;
class LoginRequest extends FormRequest class LoginRequest extends FormRequest
{ {
/** /**
@@ -29,6 +31,7 @@ class LoginRequest extends FormRequest
return [ return [
'email' => ['required', 'string', 'email'], 'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'], 'password' => ['required', 'string'],
'altcha' => ['required', new ValidAltcha],
]; ];
} }

View File

@@ -2,6 +2,7 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\Comment;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -24,13 +25,19 @@ class AdminCommentSearch extends Component
$this->resetPage(); $this->resetPage();
} }
public function deleteComment($commentId)
{
$comment = Comment::where('id', (int) $commentId)->firstOrFail();
$comment->delete();
cache()->flush();
}
public function render() public function render()
{ {
$comments = DB::table('comments') $comments = Comment::when($this->search !== '', fn ($query) => $query->where('body', 'LIKE', "%$this->search%"))
->join('users', 'comments.commenter_id', '=', 'users.id') ->when($this->userSearch !== '', fn ($query) => $query->whereHas('user', fn ($query) => $query->where('name', 'LIKE', "%{$this->userSearch}%")))
->select('comments.*', 'users.name') ->orderBy('created_at', 'DESC')
->when($this->search !== '', fn ($query) => $query->where('comment', 'LIKE', "%$this->search%"))
->when($this->userSearch !== '', fn ($query) => $query->where('name', 'LIKE', "%$this->userSearch%"))
->paginate(12); ->paginate(12);
return view('livewire.admin-comment-search', [ return view('livewire.admin-comment-search', [

View File

@@ -2,14 +2,14 @@
namespace App\Livewire; namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\Comment;
use App\Models\User; use App\Models\User;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use Livewire\Attributes\Url; use Livewire\Attributes\Url;
use Illuminate\Support\Facades\DB;
class AdminUserSearch extends Component class AdminUserSearch extends Component
{ {
use WithPagination; use WithPagination;
@@ -18,7 +18,7 @@ class AdminUserSearch extends Component
public $search = ''; public $search = '';
#[Url(history: true)] #[Url(history: true)]
public $filtered = ['true']; public $discordId = '';
#[Url(history: true)] #[Url(history: true)]
public $patreon = []; public $patreon = [];
@@ -31,8 +31,7 @@ class AdminUserSearch extends Component
$user = User::where('id', $userID) $user = User::where('id', $userID)
->firstOrFail(); ->firstOrFail();
DB::table('comments') Comment::where('user_id', $user->id)
->where('commenter_id', '=', $user->id)
->delete(); ->delete();
cache()->flush(); cache()->flush();
@@ -40,10 +39,10 @@ class AdminUserSearch extends Component
public function render() public function render()
{ {
$users = User::when($this->filtered !== [], fn ($query) => $query->where('id', '>=', 10000)) $users = User::when($this->patreon !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::SUPPORTER->value))
->when($this->patreon !== [], fn ($query) => $query->where('is_patreon', 1)) ->when($this->banned !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::BANNED->value))
->when($this->banned !== [], fn ($query) => $query->where('is_banned', 1))
->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%')) ->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
->when($this->discordId !== '', fn ($query) => $query->where('discord_id', '=', $this->discordId))
->paginate(20); ->paginate(20);
return view('livewire.admin-user-search', [ return view('livewire.admin-user-search', [

View File

@@ -1,10 +1,22 @@
<?php <?php
namespace App\Livewire; namespace App\Livewire;
use App\Models\User;
use App\Models\Episode;
use App\Notifications\CommentNotification;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Maize\Markable\Models\Like;
class Comment extends Component class Comment extends Component
{ {
use AuthorizesRequests; use AuthorizesRequests;
@@ -13,6 +25,10 @@ class Comment extends Component
public $isReplying = false; public $isReplying = false;
public $likeCount = 0;
public $liked = false;
public $replyState = [ public $replyState = [
'body' => '' 'body' => ''
]; ];
@@ -62,20 +78,50 @@ class Comment extends Component
public function postReply() public function postReply()
{ {
if (! $this->comment->depth() < 2) { if (!($this->comment->depth() < 2)) {
$this->addError('replyState.body', "Too many sub comments.");
return; return;
} }
$user = auth()->user();
$rateLimitKey = "send-comment:{$user->id}";
$rateLimitMinutes = 60 * 5; // 5 minutes
if (RateLimiter::tooManyAttempts($rateLimitKey, 1)) {
$seconds = RateLimiter::availableIn($rateLimitKey);
$this->addError('replyState.body', "Too many comments. Try again in {$seconds} seconds.");
return;
}
RateLimiter::hit($rateLimitKey, $rateLimitMinutes);
$this->validate([ $this->validate([
'replyState.body' => 'required' 'replyState.body' => 'required'
]); ]);
$reply = $this->comment->children()->make($this->replyState); $reply = $this->comment->children()->make($this->replyState);
$reply->user()->associate(auth()->user()); $reply->user()->associate($user);
$reply->commentable()->associate($this->comment->commentable); $reply->commentable()->associate($this->comment->commentable);
$reply->save(); $reply->save();
// Notify if Episode and if not the same user
if ($reply->commentable_type == Episode::class && $user->id !== $reply->parent->user->id) {
$episode = Episode::where('id', $reply->commentable_id)
->firstOrFail();
$url = route('hentai.index', ['title' => $episode->slug]);
$reply->parent->user->notify(
new CommentNotification(
"{$user->name} replied to your comment.",
Str::limit($reply->body, 50),
"{$url}#comment-{$reply->id}"
)
);
}
$this->replyState = [ $this->replyState = [
'body' => '' 'body' => ''
]; ];
@@ -85,6 +131,34 @@ class Comment extends Component
$this->dispatch('refresh')->self(); $this->dispatch('refresh')->self();
} }
public function like()
{
if (! Auth::check()) {
return;
}
Like::toggle($this->comment, User::where('id', Auth::user()->id)->firstOrFail());
Cache::forget('commentLikes'.$this->comment->id);
if ($this->liked) {
$this->liked = false;
$this->likeCount--;
return;
}
$this->liked = true;
$this->likeCount++;
}
public function mount()
{
if (Auth::check()) {
$this->likeCount = $this->comment->likeCount();
$this->liked = Like::has($this->comment, User::where('id', Auth::user()->id)->firstOrFail());
}
}
public function render() public function render()
{ {
return view('livewire.comment'); return view('livewire.comment');

View File

@@ -5,6 +5,8 @@ namespace App\Livewire;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use Illuminate\Support\Facades\RateLimiter;
class Comments extends Component class Comments extends Component
{ {
use WithPagination; use WithPagination;
@@ -29,8 +31,21 @@ class Comments extends Component
'newCommentState.body' => 'required' 'newCommentState.body' => 'required'
]); ]);
$user = auth()->user();
$rateLimitKey = "send-comment:{$user->id}";
$rateLimitMinutes = 60 * 5; // 5 minutes
if (RateLimiter::tooManyAttempts($rateLimitKey, 1)) {
$seconds = RateLimiter::availableIn($rateLimitKey);
$this->addError('newCommentState.body', "Too many comments. Try again in {$seconds} seconds.");
return;
}
RateLimiter::hit($rateLimitKey, $rateLimitMinutes);
$comment = $this->model->comments()->make($this->newCommentState); $comment = $this->model->comments()->make($this->newCommentState);
$comment->user()->associate(auth()->user()); $comment->user()->associate($user);
$comment->save(); $comment->save();
$this->newCommentState = [ $this->newCommentState = [
@@ -47,7 +62,7 @@ class Comments extends Component
->with('user', 'children.user', 'children.children') ->with('user', 'children.user', 'children.children')
->parent() ->parent()
->latest() ->latest()
->paginate(3); ->paginate(50);
return view('livewire.comments', [ return view('livewire.comments', [
'comments' => $comments 'comments' => $comments

View File

@@ -73,9 +73,9 @@ class DownloadsSearch extends Component
$types[] = 'FHD'; $types[] = 'FHD';
} elseif ($label === 'FHD 48fps') { } elseif ($label === 'FHD 48fps') {
$types[] = 'FHDi'; $types[] = 'FHDi';
} elseif ($label === 'UHD' && auth()->user()->is_patreon) { } elseif ($label === 'UHD' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
$types[] = 'UHD'; $types[] = 'UHD';
} elseif ($label === 'UHD 48fps' && auth()->user()->is_patreon) { } elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
$types[] = 'UHDi'; $types[] = 'UHDi';
} }
} }
@@ -98,7 +98,7 @@ class DownloadsSearch extends Component
public function mount() public function mount()
{ {
if (!auth()->user()->is_patreon) { if (!auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
return; return;
} }

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Livewire;
use App\Models\Comment;
use Livewire\Component;
use Livewire\WithPagination;
class UserComments extends Component
{
use WithPagination;
public $model;
public $commentSearch;
public $order = 'created_at_desc';
public function render()
{
$orderby = 'created_at';
$orderdirection = 'desc';
switch ($this->order) {
case 'created_at_desc':
$orderby = 'created_at';
$orderdirection = 'desc';
break;
case 'created_at_asc':
$orderby = 'created_at';
$orderdirection = 'asc';
break;
default:
$orderby = 'created_at';
$orderdirection = 'desc';
}
$comments = Comment::where('user_id', $this->model->id)
->when($this->commentSearch != '', fn ($query) => $query->where('body', 'like', '%'.$this->commentSearch.'%'))
->orderBy($orderby, $orderdirection)
->paginate(10);
return view('livewire.user-comments', [
'comments' => $comments
]);
}
}

View File

@@ -8,9 +8,16 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Maize\Markable\Markable;
use Maize\Markable\Models\Like;
class Comment extends Model class Comment extends Model
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes, Markable;
protected static $marks = [
Like::class
];
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@@ -58,4 +65,12 @@ class Comment extends Model
? $this->parent->depth() + 1 ? $this->parent->depth() + 1
: 0; : 0;
} }
/**
* Get cached like count
*/
public function likeCount(): int
{
return cache()->remember('commentLikes' . $this->id, 300, fn() => $this->likes->count());
}
} }

View File

@@ -36,6 +36,11 @@ class Hentai extends Model implements Sitemapable
return $this->episodes->first()->title; return $this->episodes->first()->title;
} }
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
/** /**
* Has a Gallery. * Has a Gallery.
*/ */

View File

@@ -3,6 +3,8 @@
namespace App\Models; namespace App\Models;
//use Illuminate\Contracts\Auth\MustVerifyEmail; //use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\UserRole;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
@@ -25,7 +27,6 @@ class User extends Authenticatable
'email', 'email',
'password', 'password',
'locale', 'locale',
'is_banned',
// Discord // Discord
'discord_id', 'discord_id',
'discord_avatar', 'discord_avatar',
@@ -54,7 +55,7 @@ class User extends Authenticatable
'name' => 'string', 'name' => 'string',
'email' => 'string', 'email' => 'string',
'locale' => 'string', 'locale' => 'string',
'roles' => 'json', 'roles' => 'array',
'tag_blacklist' => 'array', 'tag_blacklist' => 'array',
// Discord // Discord
'discord_id' => 'integer', 'discord_id' => 'integer',
@@ -89,9 +90,17 @@ class User extends Authenticatable
/** /**
* Has Many Comments. * Has Many Comments.
*/ */
public function comments()
{
return $this->hasMany(Comment::class, 'user_id');
}
/**
* Get Comment Count.
*/
public function commentCount(): int public function commentCount(): int
{ {
return DB::table('comments')->where('commenter_id', $this->id)->count(); return cache()->remember('userComments' . $this->id, 300, fn() => $this->comments->count());
} }
/** /**
@@ -111,4 +120,44 @@ class User extends Authenticatable
return asset('images/default-avatar.webp'); return asset('images/default-avatar.webp');
} }
/**
* Check if user has a specific role
*/
public function hasRole(UserRole $role): bool
{
return in_array($role->value, $this->roles ?? [], true);
}
/**
* Add Role to User
*/
public function addRole(UserRole $role): void
{
if ($this->hasRole($role)) {
return;
}
// Get all current roles
$roles = $this->roles ?? [];
// Add new role
$roles[] = $role->value;
$this->roles = $roles;
$this->save();
}
/**
* Remove Role from User
*/
public function removeRole(UserRole $role): void
{
if (!$this->hasRole($role)) {
return;
}
$this->roles = array_diff($this->roles, [$role->value]);
$this->save();
}
} }

View File

@@ -34,8 +34,8 @@ class EpisodeService
Request $request, Request $request,
Hentai $hentai, Hentai $hentai,
int $episodeNumber, int $episodeNumber,
Studios $studio = null, ?Studios $studio = null,
Episode $referenceEpisode = null ?Episode $referenceEpisode = null
): Episode ): Episode
{ {
$episode = new Episode(); $episode = new Episode();

View File

@@ -1,7 +1,7 @@
{ {
"name": "laravel/laravel", "name": "w33b/hstream",
"type": "project", "type": "project",
"description": "The skeleton application for the Laravel framework.", "description": "The website of hstream.moe",
"keywords": [ "keywords": [
"laravel", "laravel",
"framework" "framework"
@@ -9,45 +9,38 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"grantholle/laravel-altcha": "^2.1",
"guzzlehttp/guzzle": "^7.8.1", "guzzlehttp/guzzle": "^7.8.1",
"hisorange/browser-detect": "^5.0", "hisorange/browser-detect": "^5.0",
"http-interop/http-factory-guzzle": "^1.2", "http-interop/http-factory-guzzle": "^1.2",
"intervention/image": "^3.9", "intervention/image": "^3.11",
"intervention/image-laravel": "^1.3", "intervention/image-laravel": "^1.5",
"laravel/framework": "^11.0", "laravel/framework": "^12.0",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.2",
"laravel/scout": "^10.20", "laravel/scout": "^10.20",
"laravel/socialite": "^5.24", "laravel/socialite": "^5.24",
"laravel/tinker": "^2.10", "laravel/tinker": "^2.10",
"livewire/livewire": "^3.6.4", "livewire/livewire": "^3.7.0",
"maize-tech/laravel-markable": "^2.3.0", "maize-tech/laravel-markable": "^2.3.0",
"meilisearch/meilisearch-php": "^1.16", "meilisearch/meilisearch-php": "^1.16",
"mews/captcha": "3.4.4",
"predis/predis": "^2.2", "predis/predis": "^2.2",
"realrashid/sweet-alert": "^7.2", "realrashid/sweet-alert": "^7.2",
"rtconner/laravel-tagging": "^4.1", "rtconner/laravel-tagging": "^5.0",
"socialiteproviders/discord": "^4.2", "socialiteproviders/discord": "^4.2",
"spatie/laravel-discord-alerts": "^1.5", "spatie/laravel-discord-alerts": "^1.8",
"spatie/laravel-sitemap": "^7.3", "spatie/laravel-sitemap": "^7.3"
"vluzrmos/language-detector": "^2.3"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "^3.14.7", "barryvdh/laravel-debugbar": "^3.16",
"fakerphp/faker": "^1.24.0", "fakerphp/faker": "^1.24.0",
"laravel/breeze": "^2.3", "laravel/breeze": "^2.3",
"laravel/pint": "^1.18", "laravel/pint": "^1.18",
"laravel/sail": "^1.38",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^8.1", "nunomaduro/collision": "^8.1",
"phpunit/phpunit": "^11.4", "phpunit/phpunit": "^11.4",
"spatie/laravel-ignition": "^2.0" "spatie/laravel-ignition": "^2.0"
}, },
"repositories": [ "repositories": [],
{
"type": "vcs",
"url": "https://github.com/renatokira/comments.git"
}
],
"autoload": { "autoload": {
"exclude-from-classmap": [], "exclude-from-classmap": [],
"psr-4": { "psr-4": {

2033
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -111,6 +111,18 @@ return [
'faker_locale' => 'en_US', 'faker_locale' => 'en_US',
/*
|--------------------------------------------------------------------------
| Supported Locales
|--------------------------------------------------------------------------
|
| This is used to display the supported locales by this app, it also is
| used to verify session data and requests in the SetLocale Middleware
|
*/
'supported_locales' => ['en', 'de', 'fr'],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Encryption Key | Encryption Key

View File

@@ -1,50 +0,0 @@
<?php
return [
'characters' => ['2', '3', '4', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'X', 'Y', 'Z'],
'default' => [
'length' => 5,
'width' => 120,
'height' => 36,
'quality' => 90,
'math' => false,
'expire' => 60,
'encrypt' => false,
],
'math' => [
'length' => 9,
'width' => 120,
'height' => 36,
'quality' => 90,
'math' => true,
],
'flat' => [
'length' => 6,
'width' => 160,
'height' => 46,
'quality' => 90,
'lines' => 6,
'bgImage' => false,
'bgColor' => '#ecf2f4',
'fontColors' => ['#2c3e50', '#c0392b', '#16a085', '#c0392b', '#8e44ad', '#303f9f', '#f57c00', '#795548'],
'contrast' => -5,
],
'mini' => [
'length' => 3,
'width' => 60,
'height' => 32,
],
'inverse' => [
'length' => 5,
'width' => 120,
'height' => 36,
'quality' => 90,
'sensitive' => true,
'angle' => 12,
'sharpen' => 10,
'blur' => 2,
'invert' => true,
'contrast' => -5,
]
];

View File

@@ -6,13 +6,13 @@ return [
'guild_id' => 802233383710228550, 'guild_id' => 802233383710228550,
'patreon_roles' => [ 'patreon_roles' => [
841798154999169054, // ???? '841798154999169054', // ????
803329707650187364, // Tier-5 '803329707650187364', // Tier-5
803327903659196416, // ???? '803327903659196416', // ????
803325441942356059, // Tier-3 '803325441942356059', // Tier-3
803322725576736858, // Tier-2 '803322725576736858', // Tier-2
802270568912519198, // Tier-1 '802270568912519198', // Tier-1
802234830384267315 // admin '802234830384267315' // admin
], ],
'discord_bot_token' => env('DISCORD_BOT_TOKEN'), 'discord_bot_token' => env('DISCORD_BOT_TOKEN'),

View File

@@ -1,46 +0,0 @@
<?php
return [
/*
* Indicates whenever should autodetect and apply the language of the request.
*/
'autodetect' => env('LANG_DETECTOR_AUTODETECT', true),
/*
* Default driver to use to detect the request language.
*
* Available: browser, subdomain, uri.
*/
'driver' => env('LANG_DETECTOR_DRIVER', 'browser'),
/*
* Used on subdomain and uri drivers. That indicates which segment should be used
* to verify the language.
*/
'segment' => env('LANG_DETECTOR_SEGMENT', 0),
/*
* Languages available on the application.
*
* You could use parse_langs_to_array to use the string syntax
* or just use the array of languages with its aliases.
*/
'languages' => parse_langs_to_array(
env('LANG_DETECTOR_LANGUAGES', ['en', 'de', 'fr'])
),
/*
* Indicates if should store detected locale on cookies
*/
'cookie' => (bool) env('LANG_DETECTOR_COOKIE', true),
/*
* Indicates if should encrypt cookie
*/
'cookie_encrypt' => (bool) env('LANG_DETECTOR_COOKIE_ENCRYPT', false),
/*
* Cookie name
*/
'cookie_name' => env('LANG_DETECTOR_COOKIE', 'locale'),
];

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('locale', 10)
->nullable()
->after('discord_avatar');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('locale');
});
}
};

View File

@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Migrate supporters
DB::table('users')->where('is_patreon', 1)->update([
'roles' => DB::raw("JSON_ARRAY('supporter')")
]);
// Migrate banned
DB::table('users')->where('is_banned', 1)->update([
'roles' => DB::raw("JSON_ARRAY('banned')")
]);
// Migrate admins
DB::table('users')->where('is_admin', 1)->update([
'roles' => DB::raw("JSON_ARRAY('admin')")
]);
// Drop columns
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
$table->dropColumn('is_patreon');
$table->dropColumn('is_banned');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('users')->update(['roles' => null]);
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(0);
$table->boolean('is_patreon')->default(0);
$table->boolean('is_banned')->default(0);
});
}
};

View File

@@ -1,74 +0,0 @@
services:
laravel.test:
build:
context: './vendor/laravel/sail/runtimes/8.3'
dockerfile: Dockerfile
args:
WWWGROUP: '${WWWGROUP}'
MYSQL_CLIENT: mariadb-client
image: 'sail-8.3/app'
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
- '${APP_PORT:-80}:80'
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
environment:
WWWUSER: '${WWWUSER}'
LARAVEL_SAIL: 1
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
IGNITION_LOCAL_SITES_PATH: '${PWD}'
volumes:
- '.:/var/www/html'
networks:
- sail
depends_on:
- mariadb
- redis
mariadb:
image: 'mariadb:11'
ports:
- '${FORWARD_DB_PORT:-3306}:3306'
environment:
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
MYSQL_ROOT_HOST: '%'
MYSQL_DATABASE: '${DB_DATABASE}'
MYSQL_USER: '${DB_USERNAME}'
MYSQL_PASSWORD: '${DB_PASSWORD}'
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
volumes:
- 'sail-mariadb:/var/lib/mysql'
- './vendor/laravel/sail/database/mariadb/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
networks:
- sail
healthcheck:
test:
- CMD
- healthcheck.sh
- '--connect'
- '--innodb_initialized'
retries: 3
timeout: 5s
redis:
image: 'redis:alpine'
ports:
- '${FORWARD_REDIS_PORT:-6379}:6379'
volumes:
- 'sail-redis:/data'
networks:
- sail
healthcheck:
test:
- CMD
- redis-cli
- ping
retries: 3
timeout: 5s
networks:
sail:
driver: bridge
volumes:
sail-mariadb:
driver: local
sail-redis:
driver: local

1313
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@
"@fortawesome/fontawesome-free": "^6.5.1", "@fortawesome/fontawesome-free": "^6.5.1",
"@jellyfin/libass-wasm": "^4.1.1", "@jellyfin/libass-wasm": "^4.1.1",
"@yaireo/tagify": "^4.21.2", "@yaireo/tagify": "^4.21.2",
"altcha": "^2.3.0",
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"dashjs": "^5.0.0", "dashjs": "^5.0.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",

View File

@@ -123,3 +123,32 @@ input:checked~.dot {
src: url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff2) format('woff2'), url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff) format('woff'); src: url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff2) format('woff2'), url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff) format('woff');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
/* Captcha */
:root {
--altcha-border-width: 1px;
--altcha-border-radius: 0.375rem;
--altcha-color-base: #333;
--altcha-color-border: #a0a0a0;
--altcha-color-text: #fff;
--altcha-color-border-focus: currentColor;
--altcha-color-error-text: #f23939;
--altcha-color-footer-bg: #141414;
--altcha-max-width: 260px;
}
.altcha-footer {
border-bottom-left-radius: 0.375rem;
border-bottom-right-radius: 0.375rem;
}
input[type="checkbox"] {
background-color: #ffffff;
border-color: #a0a0a0;
color: rgb(225,29,72);
}
input[type="checkbox"]:checked {
background-color: rgb(225,29,72);
box-shadow: 0 0 0 0px #fff, 0 0 0 calc(2px + 0px) rgba(246, 59, 118, 0.5), 0 0 #0000;
}

View File

@@ -12,6 +12,9 @@ import {
initTE, initTE,
} from "tw-elements"; } from "tw-elements";
// Captcha
import 'altcha';
// import Alpine from 'alpinejs'; // import Alpine from 'alpinejs';
// window.Alpine = Alpine; // window.Alpine = Alpine;

View File

@@ -6,7 +6,7 @@
<div class="mb-4 rounded-lg bg-success-400 px-6 py-5 text-base text-success-800 mt-5" role="alert"> <div class="mb-4 rounded-lg bg-success-400 px-6 py-5 text-base text-success-800 mt-5" role="alert">
{{ $alert->text }} {{ $alert->text }}
@auth @auth
@if(Auth::user()->is_admin) @if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-success-900"> <form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-success-900">
@csrf @csrf
@method('delete') @method('delete')
@@ -21,7 +21,7 @@
<div class="mb-4 rounded-lg bg-danger-400 px-6 py-5 text-base text-danger-800 mt-5" role="alert"> <div class="mb-4 rounded-lg bg-danger-400 px-6 py-5 text-base text-danger-800 mt-5" role="alert">
{{ $alert->text }} {{ $alert->text }}
@auth @auth
@if(Auth::user()->is_admin) @if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-danger-900"> <form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-danger-900">
@csrf @csrf
@method('delete') @method('delete')

View File

@@ -1,5 +1,5 @@
@auth @auth
@if(Auth::user()->is_admin) @if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<div class="relative p-5 bg-white dark:bg-neutral-700/40 rounded-lg overflow-hidden z-10"> <div class="relative p-5 bg-white dark:bg-neutral-700/40 rounded-lg overflow-hidden z-10">
<div class="float-left"> <div class="float-left">
<a data-te-toggle="modal" data-te-target="#modalUploadEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap"> <a data-te-toggle="modal" data-te-target="#modalUploadEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap">

View File

@@ -69,6 +69,11 @@
</label> </label>
</div> </div>
<div class="block">
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4"> <div class="flex items-center justify-end mt-4">
@if (Route::has('password.request')) @if (Route::has('password.request'))
<a class="underline text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 dark:focus:ring-offset-neutral-800" href="{{ route('password.request') }}"> <a class="underline text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 dark:focus:ring-offset-neutral-800" href="{{ route('password.request') }}">
@@ -127,6 +132,11 @@
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" /> <x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div> </div>
<div class="block">
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4"> <div class="flex items-center justify-end mt-4">
<x-primary-button class="ms-4"> <x-primary-button class="ms-4">
{{ __('Register') }} {{ __('Register') }}

View File

@@ -33,19 +33,7 @@
<x-input-error class="mt-2" :messages="$errors->get('message')" /> <x-input-error class="mt-2" :messages="$errors->get('message')" />
</div> </div>
<div> <altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
<x-input-label for="message" :value="__('Captcha')" />
<div class="flex pt-2">
<div id="captchaImg">
{!! captcha_img() !!}
</div>
<button type="button" class="inline-flex items-center ml-2 px-2 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150" id="reloadcaptcha">
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
<br>
<x-text-input id="captcha" class="block " type="text" name="captcha" required />
</div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<x-primary-button>{{ __('Submit') }}</x-primary-button> <x-primary-button>{{ __('Submit') }}</x-primary-button>
@@ -65,18 +53,4 @@
@endif @endif
</div> </div>
</form> </form>
<script>
function reloadCaptcha() {
window.axios.get('/reload-captcha').then(function(response) {
if (response.status == 200) {
document.querySelector("#captchaImg").innerHTML = response.data.captcha;
}
}).catch(function(error) {
console.log(error);
});
}
document.querySelector("#reloadcaptcha").addEventListener("click", reloadCaptcha);
</script>
</section> </section>

View File

@@ -2,14 +2,14 @@
{{ __('home.latest-comments') }} {{ __('home.latest-comments') }}
</p> </p>
<div class="grid grid-cols-1 gap-2 md:grid-cols-2"> <div class="grid gap-2 grid-cols-1 xl:grid-cols-2">
@foreach ($latestComments as $comment) @foreach ($latestComments as $comment)
@if ($comment->commentable_type == 'App\Models\Episode') @if ($comment->commentable_type == \App\Models\Episode::class)
@php $episode = cache()->rememberForever('commentEpisode'.$comment->commentable_id, fn () => App\Models\Episode::with('gallery')->where('id', $comment->commentable_id)->first()); @endphp @php $episode = cache()->rememberForever('commentEpisode'.$comment->commentable_id, fn () => App\Models\Episode::with('gallery')->where('id', $comment->commentable_id)->first()); @endphp
<div id="comments" class="flex p-4 bg-white rounded-lg dark:bg-neutral-950"> <div id="comments" class="flex p-4 bg-white rounded-lg dark:bg-neutral-950">
<div <div
class="w-[20vw] mr-5 p-1 md:p-2 mb-8 relative transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300"> class="w-[15vw] mr-5 p-1 md:p-2 mb-4 relative transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300">
<a class="hidden hover:text-blue-600 xl:block" <a class="hidden 2xl:block"
href="{{ route('hentai.index', ['title' => $episode->slug]) }}"> href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000" <img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000"
class="block object-cover object-center relative z-20 rounded-lg aspect-video" class="block object-cover object-center relative z-20 rounded-lg aspect-video"
@@ -18,28 +18,28 @@
class="absolute right-2 top-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30"> class="absolute right-2 top-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
{{ $episode->getResolution() }}</p> {{ $episode->getResolution() }}</p>
<div class="absolute w-[95%] grid grid-cols-1 text-center"> <div class="absolute w-[95%] grid grid-cols-1 text-center">
<p class="text-sm text-center text-black dark:text-white">{{ $episode->title }} - <p class="text-sm text-center text-black dark:text-white truncate">{{ $episode->title }} -
{{ $episode->episode }}</p> {{ $episode->episode }}</p>
</div> </div>
</a> </a>
<a class="block hover:text-blue-600 xl:hidden" <a class="block 2xl:hidden"
href="{{ route('hentai.index', ['title' => $episode->slug]) }}"> href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000" <img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000"
class="block object-cover object-center relative z-20 rounded-lg" class="block object-cover object-center relative z-20 rounded-lg"
src="{{ $episode->cover_url }}"></img> src="{{ $episode->cover_url }}"></img>
</a> </a>
</div> </div>
<div class="w-[60vw]"> <div class="w-[60vw] pt-4 bg-neutral-100 dark:bg-neutral-800 rounded-lg pl-4">
{{--@include('partials.comment', ['comment' => $comment])--}} @include('partials.comment', ['comment' => $comment])
</div> </div>
</div> </div>
@elseif($comment->commentable_type == 'App\Models\Hentai') @elseif($comment->commentable_type == \App\Models\Hentai::class)
@php $hentai = cache()->rememberForever('commentHentai'.$comment->commentable_id, fn () => App\Models\Hentai::with('gallery', 'episodes')->where('id', $comment->commentable_id)->first()); @endphp @php $hentai = cache()->rememberForever('commentHentai'.$comment->commentable_id, fn () => App\Models\Hentai::with('gallery', 'episodes')->where('id', $comment->commentable_id)->first()); @endphp
<div id="comments" class="flex p-4 bg-white rounded-lg dark:bg-neutral-950"> <div id="comments" class="flex p-4 bg-white rounded-lg dark:bg-neutral-950">
<div <div
class="w-[20vw] mr-5 p-1 md:p-2 mb-8 relative transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300"> class="w-[15vw] mr-5 p-1 md:p-2 mb-8 relative transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300">
<a class="hover:text-blue-600" href="{{ route('hentai.index', ['title' => $hentai->slug]) }}"> <a class="hidden 2xl:block" href="{{ route('hentai.index', ['title' => $hentai->slug]) }}">
<img alt="{{ $hentai->episodes->first()->title }}" loading="lazy" width="1000" <img alt="{{ $hentai->episodes->first()->title }}" loading="lazy" width="1000"
class="block object-cover object-center relative z-20 rounded-lg aspect-video" class="block object-cover object-center relative z-20 rounded-lg aspect-video"
src="{{ $hentai->gallery->first()->thumbnail_url }}"></img> src="{{ $hentai->gallery->first()->thumbnail_url }}"></img>
@@ -47,13 +47,20 @@
class="absolute right-2 top-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30"> class="absolute right-2 top-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
{{ $hentai->episodes->first()->getResolution() }}</p> {{ $hentai->episodes->first()->getResolution() }}</p>
<div class="absolute w-[95%] grid grid-cols-1 text-center"> <div class="absolute w-[95%] grid grid-cols-1 text-center">
<p class="text-sm text-center text-black dark:text-white"> <p class="text-sm text-center text-black dark:text-white truncate">
{{ $hentai->episodes->first()->title }}</p> {{ $hentai->episodes->first()->title }}</p>
</div> </div>
</a> </a>
<a class="block 2xl:hidden"
href="{{ route('hentai.index', ['title' => $hentai->slug]) }}">
<img alt="{{ $hentai->episodes->first()->title }}" loading="lazy" width="1000"
class="block object-cover object-center relative z-20 rounded-lg"
src="{{ $hentai->episodes->first()->cover_url }}"></img>
</a>
</div> </div>
<div class="w-[60vw]"> <div class="w-[60vw] pt-4 bg-neutral-100 dark:bg-neutral-800 rounded-lg pl-4">
{{--@include('partials.comment', ['comment' => $comment])--}} @include('partials.comment', ['comment' => $comment])
</div> </div>
</div> </div>
@endif @endif

View File

@@ -163,7 +163,7 @@
<i class="fa-solid fa-gear"></i> {{ __('nav.settings') }} <i class="fa-solid fa-gear"></i> {{ __('nav.settings') }}
</x-dropdown-link> </x-dropdown-link>
@if (Auth::user()->is_admin) @if (Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<x-dropdown-link href="{{ route('admin.upload.index') }}"> <x-dropdown-link href="{{ route('admin.upload.index') }}">
<i class="fa-solid fa-user-tie"></i> Admin <i class="fa-solid fa-user-tie"></i> Admin
</x-dropdown-link> </x-dropdown-link>
@@ -296,8 +296,8 @@
<div class="pb-1 text-center w-full"> <div class="pb-1 text-center w-full">
<x-responsive-nav-link :href="route('login')"> <x-responsive-nav-link :href="route('login')">
<div <div
class="relative bg-blue-700 hover:bg-blue-600 text-white font-bold px-4 h-10 rounded text-center p-[10px]"> class="relative bg-rose-700 hover:bg-rose-600 text-white font-bold px-4 h-10 rounded text-center p-[10px]">
<i class="fa-brands fa-discord"></i> {{ __('nav.login') }} <i class="fa-solid fa-arrow-right-to-bracket"></i> {{ __('nav.login') }}
</div> </div>
</x-responsive-nav-link> </x-responsive-nav-link>
</div> </div>

View File

@@ -25,6 +25,8 @@
placeholder="Search..." placeholder="Search..."
> >
</th> </th>
<th scope="col" class="px-6 py-3">
</th>
<th scope="col" class="px-6 py-3"> <th scope="col" class="px-6 py-3">
Actions Actions
</th> </th>
@@ -34,17 +36,18 @@
@foreach($comments as $comment) @foreach($comments as $comment)
<tr wire:key="comment-{{ $comment->id }}" class="bg-white border-t dark:bg-neutral-800 dark:border-pink-700"> <tr wire:key="comment-{{ $comment->id }}" class="bg-white border-t dark:bg-neutral-800 dark:border-pink-700">
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ $comment->name }} {{ $comment->user->name }}
</td> </td>
<th scope="row" class="px-6 py-4 font-medium text-gray-900 dark:text-white max-w-lg"> <th scope="row" class="px-6 py-4 font-medium text-gray-900 dark:text-white max-w-lg">
{{ $comment->comment }} {{ $comment->body }}
</th>
<th scope="row" class="px-6 py-4 font-medium text-gray-900 dark:text-white max-w-lg">
{{ $comment->created_at }}
</th> </th>
<td class="px-6 py-4"> <td class="px-6 py-4">
<a href="{{ route('comments.destroy', $comment->id) }}" onclick="event.preventDefault();document.getElementById('comment-delete-form-{{ $comment->id }}').submit();" class="inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150 mt-2">@lang('comments::comments.delete')</a> <button wire:click="deleteComment({{$comment->id}})" type="button" class="inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150 mt-2">
<form id="comment-delete-form-{{ $comment->id }}" action="{{ route('comments.destroy', $comment->id) }}" method="POST" style="display: none;"> Delete
@method('DELETE') </button>
@csrf
</form>
</td> </td>
</tr> </tr>
@endforeach @endforeach

View File

@@ -6,12 +6,16 @@
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-pink-700 dark:text-neutral-200 "> <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-pink-700 dark:text-neutral-200 ">
<tr> <tr>
<th scope="col" class="px-6 py-3"> <th scope="col" class="px-6 py-3">
Discord-ID ID
</th>
<th scope="col" class="px-6 py-3">
Discord ID
<input <input
class="w-4 h-4 ml-2 text-rose-600 bg-gray-100 border-gray-300 rounded focus:ring-rose-500 dark:focus:ring-rose-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" wire:model.live.debounce.600ms="discordId"
type="checkbox" type="search"
wire:model.live="filtered" id="discord-search"
value="true" class="ml-2 w-32 h-7 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900"
placeholder="Search..."
> >
</th> </th>
<th scope="col" class="px-6 py-3"> <th scope="col" class="px-6 py-3">
@@ -59,14 +63,17 @@
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"> <th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ $user->id }} {{ $user->id }}
</th> </th>
<td class="px-6 py-4">
{{ $user->discord_id ?? 'n/a' }}
</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ $user->name }} {{ $user->name }}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ $user->is_patreon ? 'Yes' : 'No' }} {{ $user->hasRole(\App\Enums\UserRole::SUPPORTER) ? 'Yes' : 'No' }}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ $user->is_banned ? 'Yes' : 'No' }} {{ $user->hasRole(\App\Enums\UserRole::BANNED) ? 'Yes' : 'No' }}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ $user->created_at->format('Y-m-d') }} {{ $user->created_at->format('Y-m-d') }}
@@ -78,9 +85,9 @@
<form method="POST" action="{{ route('admin.user.update') }}"> <form method="POST" action="{{ route('admin.user.update') }}">
@csrf @csrf
<input type="hidden" value="{{ $user->id }}" name="id"> <input type="hidden" value="{{ $user->id }}" name="id">
<input type="hidden" value="{{ $user->is_banned ? 'unban' : 'ban' }}" name="action"> <input type="hidden" value="{{ $user->hasRole(\App\Enums\UserRole::BANNED) ? 'unban' : 'ban' }}" name="action">
<button type="submit" class="inline-block w-full rounded bg-rose-600 pl-[4px] pr-[4px] p-[1px] text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600"> <button type="submit" class="inline-block w-full rounded bg-rose-600 pl-[4px] pr-[4px] p-[1px] text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">
{{ $user->is_banned ? 'Unban' : 'Ban' }} {{ $user->hasRole(\App\Enums\UserRole::BANNED) ? 'Unban' : 'Ban' }}
</button> </button>
</form> </form>
<button wire:click="deleteUserComments('{{ $user->id }}')" class="inline-block w-full rounded bg-red-600 pl-[4px] pr-[4px] p-[1px] text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600"> <button wire:click="deleteUserComments('{{ $user->id }}')" class="inline-block w-full rounded bg-red-600 pl-[4px] pr-[4px] p-[1px] text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">

View File

@@ -1,15 +1,15 @@
<div> <div>
<div class="flex"> <div class="flex" id="comment-{{ $comment->id }}">
<div class="flex-shrink-0 mr-4"> <div class="flex-shrink-0 mr-4">
<img class="h-10 w-10 rounded-full" src="{{ $comment->user->getAvatar() }}" alt="{{ $comment->user->name }}"> <img class="h-10 w-10 rounded-full" src="{{ $comment->user->getAvatar() }}" alt="{{ $comment->user->name }}">
</div> </div>
<div class="flex-grow"> <div class="flex-grow">
<div class="flex gap-2"> <div class="flex gap-2">
<p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p> <p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p>
@if($comment->user->is_admin) @if($comment->user->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a> <a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a>
@endif @endif
@if($comment->user->is_patreon) @if($comment->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"></i></a> <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"></i></a>
@endif @endif
</div> </div>
@@ -28,7 +28,7 @@
</div> </div>
<div class="mt-3 flex items-center justify-between"> <div class="mt-3 flex items-center justify-between">
<button type="submit" <button type="submit"
class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md shadow-sm text-white bg-rose-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-rose-500"> class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md shadow-sm text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-rose-500">
Edit Edit
</button> </button>
</div> </div>
@@ -37,10 +37,28 @@
<div class="text-gray-700 dark:text-gray-200">{!! $comment->presenter()->markdownBody() !!}</div> <div class="text-gray-700 dark:text-gray-200">{!! $comment->presenter()->markdownBody() !!}</div>
@endif @endif
</div> </div>
<div class="mt-2 space-x-2"> <div class="mt-2 space-x-2 flex flex-row">
<span class="text-gray-500 dark:text-gray-300 font-medium"> <span class="text-gray-500 dark:text-gray-300">
{{ $comment->presenter()->relativeCreatedAt() }} {{ $comment->presenter()->relativeCreatedAt() }}
</span> </span>
@guest
<span data-te-toggle="tooltip" title="Please login to like the episode" class="text-gray-800 cursor-pointer dark:text-gray-200">
<i class="fa-regular fa-heart"></i> {{ $comment->likeCount() }}
</span>
@endguest
@auth
<!-- Like Button -->
<button class="text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap" wire:click="like">
@if ($liked)
<i class="fa-solid fa-heart text-rose-600"></i> {{ $likeCount }}
@else
<i class="fa-solid fa-heart"></i> {{ $likeCount }}
@endif
</button>
@endauth
@auth @auth
@if ($comment->depth() < 2) @if ($comment->depth() < 2)
<button wire:click="$toggle('isReplying')" type="button" class="text-gray-900 dark:text-gray-100 font-medium"> <button wire:click="$toggle('isReplying')" type="button" class="text-gray-900 dark:text-gray-100 font-medium">

View File

@@ -4,19 +4,8 @@
<div class="px-4 py-5 sm:px-6"> <div class="px-4 py-5 sm:px-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-200">Comments</h2> <h2 class="text-lg font-medium text-gray-900 dark:text-gray-200">Comments</h2>
</div> </div>
<div class="px-4 py-6 sm:px-6"> <div>
<div class="space-y-8"> <!-- Comment Input -->
@if ($comments->isNotEmpty())
@foreach($comments as $comment)
<livewire:comment :comment="$comment" :key="$comment->id"/>
@endforeach
{{ $comments->links() }}
@else
<p class="text-gray-900 dark:text-gray-200">No comments yet.</p>
@endif
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-neutral-800 px-4 py-6 sm:px-6"> <div class="bg-gray-50 dark:bg-neutral-800 px-4 py-6 sm:px-6">
@auth @auth
<div class="flex"> <div class="flex">
@@ -28,7 +17,7 @@
<div> <div>
<label for="comment" class="sr-only">Comment body</label> <label for="comment" class="sr-only">Comment body</label>
<textarea id="comment" name="comment" rows="3" <textarea id="comment" name="comment" rows="3"
class="bg-white dark:bg-neutral-700 shadow-sm block w-full focus:ring-rose-500 focus:border-rose-500 border-gray-300 dark:border-gray-400/40 text-gray-900 dark:text-gray-200 placeholder:text-gray-400 rounded-md class="peer block min-h-[auto] w-full border-1 bg-transparent px-3 py-[0.32rem] leading-[1.6] outline-none transition-all duration-200 ease-linear dark:placeholder:text-neutral-200 border-gray-300 dark:border-neutral-950 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm
@error('newCommentState.body') border-red-500 @enderror" @error('newCommentState.body') border-red-500 @enderror"
placeholder="Write something" wire:model.defer="newCommentState.body"></textarea> placeholder="Write something" wire:model.defer="newCommentState.body"></textarea>
@error('newCommentState.body') @error('newCommentState.body')
@@ -50,5 +39,22 @@
<p class="text-gray-900 dark:text-gray-200">Log in to comment.</p> <p class="text-gray-900 dark:text-gray-200">Log in to comment.</p>
@endguest @endguest
</div> </div>
<!-- Comments -->
<div class="px-4 py-6 sm:px-6">
<div class="space-y-8">
@if ($comments->isNotEmpty())
@foreach($comments as $comment)
<livewire:comment :comment="$comment" :key="$comment->id"/>
@endforeach
{{ $comments->links('pagination::tailwind') }}
@else
<p class="text-gray-900 dark:text-gray-200">No comments yet.</p>
@endif
</div>
</div>
</div>
</div>
</div> </div>
</section> </section>

View File

@@ -0,0 +1,130 @@
<div class="py-3 relative">
<div class="mx-auto sm:px-6 lg:px-8 space-y-6 max-w-[100%] lg:max-w-[90%] xl:max-w-[80%] 2xl:max-w-[90%] relative z-10">
<!-- Search Filter -->
<div class="p-4 sm:p-8 bg-white/30 dark:bg-neutral-950/40 shadow sm:rounded-lg backdrop-blur relative z-100">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 ">
<!-- Title -->
<div>
<label for="live-search"
class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative right-2 left-0 sm:left-2 transition-all">
<div class="absolute inset-y-0 left-2 flex items-center pl-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<input wire:model.live.debounce.600ms="commentSearch" type="search" id="live-search"
class="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900"
placeholder="Search comment..." required>
<div class="absolute right-0 top-[11px]" wire:loading>
<svg aria-hidden="true"
class="inline w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-pink-600"
viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor" />
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill" />
</svg>
</div>
</div>
</div>
<!-- Ordering -->
<div>
<div class="relative right-2 left-0 sm:left-2 transition-all">
<div class="absolute inset-y-0 left-2 flex items-center pl-3 pointer-events-none">
<i class="fa-solid fa-sort text-gray-500 dark:text-gray-400"></i>
</div>
<select wire:model.live="order"
class="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900">
<option value="created_at_desc">Created DESC</option>
<option value="created_at_asc">Created ASC</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div
class="relative pt-5 mx-auto sm:px-6 lg:px-8 space-y-6 text-gray-900 dark:text-white max-w-[100%] lg:max-w-[90%] xl:max-w-[80%] 2xl:max-w-[90%] 2xl:w-[50vw]">
<div class="flex flex-col">
<div class="overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 sm:px-6 lg:px-8">
<div class="overflow-hidden">
<!-- Desktop -->
<div class="w-full text-left text-sm font-light">
<!-- Header -->
<div
class="flex bg-white/30 dark:bg-neutral-950/40 backdrop-blur font-medium dark:border-neutral-500 border-b rounded-tl-lg rounded-tr-lg">
<div class="flex-1 px-6 py-4 text-center">Comment</div>
</div>
<!-- Rows -->
@foreach ($comments as $comment)
<div wire:key="comment-{{ $comment->id }}"
class="flex flex-col sm:flex-row items-center border-b bg-white dark:bg-neutral-950 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-neutral-800">
<!-- Image -->
<div class="flex w-fit sm:w-56">
@if($comment->commentable_type == \App\Models\Episode::class)
@php $episode = \App\Models\Episode::find($comment->commentable_id); @endphp
<div class="relative p-1 w-full transition duration-300 ease-in-out md:p-2 md:hover:-translate-y-1 md:hover:scale-110">
<a href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy" width="1000"
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
src="{{ $episode->gallery->first()->thumbnail_url }}" />
<p class="absolute left-1 md:left-2 bottom-1 md:bottom-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
<i class="fa-regular fa-eye"></i> {{ $episode->viewCountFormatted() }}
<i class="fa-regular fa-heart"></i> {{ $episode->likeCount() }}
<i class="fa-regular fa-comment"></i> {{ $episode->commentCount() }}
</p>
</a>
</div>
@elseif($comment->commentable_type == \App\Models\Hentai::class)
@php
$hentai = \App\Models\Hentai::find($comment->commentable_id);
$episode = $hentai->episodes->first();
@endphp
<div class="relative p-1 w-full transition duration-300 ease-in-out md:p-2 md:hover:-translate-y-1 md:hover:scale-110">
<a href="{{ route('hentai.index', ['title' => $hentai->slug]) }}">
<img alt="{{ $episode->title }}" loading="lazy" width="1000"
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
src="{{ $episode->gallery->first()->thumbnail_url }}" />
</a>
</div>
@endif
</div>
<!-- Body -->
<div class="flex text-lg flex-1 items-center space-x-2 px-3 py-2 bg-neutral-200 dark:bg-neutral-900 h-[115px] rounded-lg sm:mr-2">
{!! $comment->presenter()->markdownBody() !!}
</div>
<div class="space-x-2 sm:mr-2 w-24">
<span class="text-gray-500 dark:text-gray-300 font-medium">
{{ $comment->presenter()->relativeCreatedAt() }}
</span>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
{{ $comments->links('pagination::tailwind') }}
</div>
</div>

View File

@@ -52,7 +52,7 @@
<br> <br>
@php $download = $episode->getDownloadByType('UHD'); @endphp @php $download = $episode->getDownloadByType('UHD'); @endphp
@isset($download) @isset($download)
@if (!Auth::user()->is_patreon) @if (!Auth::user()->hasRole(\App\Enums\UserRole::SUPPORTER))
@if (config('hstream.free_downloads')) @if (config('hstream.free_downloads'))
<p class="font-bold text-gray-800 dark:text-gray-200"> <p class="font-bold text-gray-800 dark:text-gray-200">
<i class="fa-solid fa-lock-open pr-[4px] text-yellow-600"></i> 4k <i class="fa-solid fa-lock-open pr-[4px] text-yellow-600"></i> 4k
@@ -91,7 +91,7 @@
<br> <br>
@if ($episode->interpolated_uhd) @if ($episode->interpolated_uhd)
@if (!Auth::user()->is_patreon) @if (!Auth::user()->hasRole(\App\Enums\UserRole::SUPPORTER))
@if (config('hstream.free_downloads')) @if (config('hstream.free_downloads'))
<p class="font-bold text-gray-800 dark:text-gray-200"> <p class="font-bold text-gray-800 dark:text-gray-200">
<i class="fa-solid fa-lock-open pr-[4px] text-yellow-600"></i> 4k 48fps <i class="fa-solid fa-lock-open pr-[4px] text-yellow-600"></i> 4k 48fps

View File

@@ -8,18 +8,7 @@
<p id="message" class="text-red-600"> <p id="message" class="text-red-600">
</p> </p>
<div class="flex pt-2"> <div class="flex pt-2">
<div id="captchaImg"> <altcha-widget id="altcha" challengeurl="/altcha-challenge"></altcha-widget>
{!! captcha_img() !!}
</div>
<button type="button" class="inline-flex items-center ml-2 px-2 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150" id="reloadcaptcha" >
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
<div class="flex pt-2 mt-1">
<x-text-input id="captcha_text" class="block " type="text" name="captcha_text"/>
<button type="button" class="inline-flex items-center ml-2 px-2 -pt-1 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" id="submitcaptcha" >
Submit
</button>
</div> </div>
<br> <br>
<p class="text-gray-800 dark:text-gray-200 text-sm"> <p class="text-gray-800 dark:text-gray-200 text-sm">
@@ -51,21 +40,12 @@
<script> <script>
var downloadCounter = 0; var downloadCounter = 0;
function reloadCaptcha() {
window.axios.get('/reload-captcha').then(function (response) {
if (response.status == 200) {
document.querySelector("#captchaImg").innerHTML = response.data.captcha;
}
}).catch(function (error) {
console.log(error);
});
}
function submitCaptcha() { function submitCaptcha(captchaToken) {
document.querySelector("#message").innerHTML = ''; document.querySelector("#message").innerHTML = '';
window.axios.post('/get-download', { window.axios.post('/get-download', {
captcha: document.getElementById('captcha_text').value, episode_id: document.getElementById('e_id').value,
episode_id: document.getElementById('e_id').value captcha: captchaToken,
}).then(function (response) { }).then(function (response) {
document.querySelector("#captcharequired").style.display = "none"; document.querySelector("#captcharequired").style.display = "none";
document.querySelector("#captchsolved").style.display = "block"; document.querySelector("#captchsolved").style.display = "block";
@@ -89,6 +69,16 @@
document.querySelector("#downloadEpisode").addEventListener("click", increaseDownloadCounter); document.querySelector("#downloadEpisode").addEventListener("click", increaseDownloadCounter);
document.querySelector("#reloadcaptcha").addEventListener("click", reloadCaptcha); document.addEventListener("DOMContentLoaded", () => {
document.querySelector("#submitcaptcha").addEventListener("click", submitCaptcha); const altcha = document.querySelector("#altcha");
altcha.addEventListener("statechange", (ev) => {
if (ev.detail.state === "verified") {
submitCaptcha(ev.detail.payload);
// Remove captcha from DOM
altcha.remove();
}
});
});
</script> </script>

View File

@@ -0,0 +1,26 @@
<div>
<div class="flex">
<div class="flex-shrink-0 mr-4">
<img class="h-10 w-10 rounded-full" src="{{ $comment->user->getAvatar() }}" alt="{{ $comment->user->name }}">
</div>
<div class="flex-grow">
<div class="flex gap-2">
<p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p>
@if($comment->user->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a>
@endif
@if($comment->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"></i></a>
@endif
</div>
<div class="mt-1 flex-grow w-full">
<div class="text-gray-700 dark:text-gray-200">{!! $comment->presenter()->markdownBody() !!}</div>
</div>
<div class="mt-2 space-x-2">
<span class="text-gray-500 dark:text-gray-300 font-medium">
{{ $comment->presenter()->relativeCreatedAt() }}
</span>
</div>
</div>
</div>
</div>

View File

@@ -12,63 +12,10 @@
class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-20 mt-[65px] flex flex-row justify-center xl:justify-normal"> class="relative max-w-[120rem] mx-auto sm:px-6 lg:px-8 space-y-6 pt-20 mt-[65px] flex flex-row justify-center xl:justify-normal">
<div class="flex flex-col xl:flex-row"> <div class="flex flex-col xl:flex-row">
@include('profile.partials.sidebar') @include('profile.partials.sidebar')
<div class="pb-2 space-y-6 max-w-7xl px-0 md:px-6 lg:px-8 pt-8 xl:pt-0"> <div class="pb-2 space-y-6 max-w-7xl px-0 md:px-6 lg:px-8 pt-8 xl:pt-0 flex flex-row">
@php <livewire:user-comments :model="$user"/>
$episode_ids = array_unique(
DB::table('comments')
->where('commenter_id', $user->id)
->get()
->pluck('commentable_id')
->toArray(),
);
@endphp
@foreach ($episode_ids as $episode_id)
@php $episode = App\Models\Episode::where('id', $episode_id)->first(); @endphp
<div
class="flex flex-col 2xl:flex-row p-5 bg-white/40 dark:bg-neutral-950/40 backdrop-blur rounded-lg items-center">
<div
class="w-[20vw] mr-5 p-1 md:p-2 mb-8 relative transition ease-in-out hover:-translate-y-1 hover:scale-110 duration-300">
<a class="hidden hover:text-blue-600 md:block"
href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy"
width="1000"
class="block object-cover object-center relative z-20 rounded-lg aspect-video"
src="{{ $episode->gallery->first()->thumbnail_url }}"></img>
<p
class="absolute right-2 top-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
{{ $episode->getResolution() }}</p>
<p
class="absolute left-2 bottom-2 bg-rose-700/70 !text-white rounded-bl-lg rounded-tr-lg p-1 pr-2 pl-2 font-semibold text-sm z-30">
<i class="fa-regular fa-eye"></i> {{ $episode->viewCountFormatted() }} <i
class="fa-regular fa-heart"></i> {{ $episode->likeCount() }} <i
class="fa-regular fa-comment"></i>
{{ $episode->commentCount() }}
</p>
<div class="absolute w-[95%] grid grid-cols-1 text-center">
<p class="text-sm text-center text-black dark:text-white">
{{ $episode->title }} - {{ $episode->episode }}</p>
</div>
</a>
<a class="block hover:text-blue-600 md:hidden"
href="{{ route('hentai.index', ['title' => $episode->slug]) }}">
<img alt="{{ $episode->title }} - {{ $episode->episode }}" loading="lazy"
width="1000"
class="block object-cover object-center relative z-20 rounded-lg"
src="{{ $episode->cover_url }}"></img>
<div class="relative w-[95%] grid grid-cols-1 text-center">
<p class="text-sm text-center text-black dark:text-white">
{{ $episode->title }} - {{ $episode->episode }}</p>
</div>
</a>
</div>
<div class="md:w-[60vw]">
{{--@comments(['model' => $episode])--}}
</div>
</div>
@endforeach
</div> </div>
</div> </div>
</div> </div>

View File

@@ -16,7 +16,7 @@
@include('series.partials.episodes') @include('series.partials.episodes')
@include('series.partials.comments') <livewire:comments :model="$hentai"/>
</div> </div>
@include('series.partials.popular') @include('series.partials.popular')

View File

@@ -1,8 +0,0 @@
<div class="bg-transparent rounded-lg overflow-hidden bg-white dark:bg-neutral-800 p-5">
<div id="comments" class="grid grid-cols-1 bg-transparent rounded-lg">
<p class="leading-normal font-bold text-lg text-rose-600">
{{ __('home.latest-comments') }}
</p>
{{--@comments(['model' => $hentai])--}}
</div>
</div>

View File

@@ -46,7 +46,7 @@
@include('modals.share') @include('modals.share')
@auth @auth
@if(Auth::user()->is_admin) @if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
@include('admin.modals.upload-episode') @include('admin.modals.upload-episode')
@include('admin.modals.add-subtitles') @include('admin.modals.add-subtitles')
@include('admin.modals.edit-episode') @include('admin.modals.edit-episode')

View File

@@ -4,7 +4,7 @@
<div class="flex flex-col py-5 pl-24"> <div class="flex flex-col py-5 pl-24">
<strong class="text-slate-900 text-xl font-bold dark:text-slate-200"> <strong class="text-slate-900 text-xl font-bold dark:text-slate-200">
{{ $user->name }} {{ $user->name }}
@if ($user->is_patreon) @if ($user->hasRole(\App\Enums\UserRole::SUPPORTER))
<a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i <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> class="fa-solid fa-hand-holding-heart text-rose-600 animate-pulse"></i></a>
@endif @endif

62
routes/admin.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
use App\Http\Controllers\Admin\AlertController;
use App\Http\Controllers\Admin\ContactController;
use App\Http\Controllers\Admin\CommentsController;
use App\Http\Controllers\Admin\EpisodeController;
use App\Http\Controllers\Admin\ReleaseController;
use App\Http\Controllers\Admin\UserController;
use App\Http\Controllers\Admin\SubtitleController;
use App\Http\Controllers\Admin\SiteBackgroundController;
use App\Http\Controllers\Api\AdminApiController;
use Illuminate\Support\Facades\Route;
/*
|---------------------------------------------------------------------------------
| Admin Routes
|---------------------------------------------------------------------------------
*/
Route::group(['middleware' => ['auth', 'auth.admin']], function () {
// Site alerts
Route::get('/admin/alert', [AlertController::class, 'index'])->name('admin.alert.index');
Route::post('/admin/alert', [AlertController::class, 'store'])->name('admin.alert.create');
Route::delete('/admin/alert/{alert_id}', [AlertController::class, 'delete'])->name('admin.alert.delete');
// Users
Route::get('/admin/users', [UserController::class, 'index'])->name('admin.user.index');
Route::post('/admin/users', [UserController::class, 'update'])->name('admin.user.update');
// Comments
Route::get('/admin/comments', [CommentsController::class, 'index'])->name('admin.comments.index');
// Contact page overview
Route::get('/admin/contact', [ContactController::class, 'index'])->name('admin.contact.index');
Route::delete('/admin/contact/{contact_id}', [ContactController::class, 'delete'])->name('admin.contact.delete');
// Site background settings
Route::get('/admin/background', [SiteBackgroundController::class, 'index'])->name('admin.background.index');
Route::post('/admin/background', [SiteBackgroundController::class, 'create'])->name('admin.background.create');
Route::put('/admin/background', [SiteBackgroundController::class, 'update'])->name('admin.background.update');
Route::delete('/admin/background', [SiteBackgroundController::class, 'delete'])->name('admin.background.delete');
// Release
Route::get('/admin/release', [ReleaseController::class, 'index'])->name('admin.upload.index');
Route::post('/admin/release/upload', [ReleaseController::class, 'store'])->name('admin.upload');
// Episode
Route::post('/admin/episode/upload', [EpisodeController::class, 'store'])->name('admin.upload.episode');
Route::post('/admin/episode/edit', [EpisodeController::class, 'update'])->name('admin.edit');
// Get Tags used for Upload Form
Route::get('/admin/tags', [AdminApiController::class, 'getTags'])->name('admin.tags');
Route::get('/admin/studios', [AdminApiController::class, 'getStudios'])->name('admin.studios');
// Get Tags for editing Episode
Route::get('/admin/tags/{episode_id}', [AdminApiController::class, 'getEpisodeTags'])->name('admin.tags.episode');
Route::get('/admin/studio/{episode_id}', [AdminApiController::class, 'getEpisodeStudio'])->name('admin.studio.episode');
// Subtitles
Route::get('/admin/subtitles/{episode_id}', [AdminApiController::class, 'getSubtitles'])->name('admin.subtitles');
Route::post('/admin/add-new-subtitle', [SubtitleController::class, 'store'])->name('admin.add.new.subtitle');
Route::post('/admin/update-subtitles', [SubtitleController::class, 'update'])->name('admin.update.subtitles');
});

45
routes/user.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
use App\Http\Controllers\HomeController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\PlaylistController;
use App\Http\Controllers\Api\UserApiController;
use Illuminate\Support\Facades\Route;
/*
|---------------------------------------------------------------------------------
| User Routes
|---------------------------------------------------------------------------------
*/
Route::middleware('auth')->group(function () {
Route::get('/user/profile', [ProfileController::class, 'index'])->name('profile.show');
Route::get('/user/comments', [ProfileController::class, 'comments'])->name('profile.comments');
Route::get('/user/likes', [ProfileController::class, 'likes'])->name('profile.likes');
Route::get('/user/watched', [ProfileController::class, 'watched'])->name('user.watched');
// Notifications
Route::get('/user/notifications', [App\Http\Controllers\NotificationController::class, 'index'])->name('profile.notifications');
Route::delete('/user/notifications', [App\Http\Controllers\NotificationController::class, 'delete'])->name('profile.notifications.delete');
// User Profile Actions
Route::get('/user/settings', [ProfileController::class, 'settings'])->name('profile.settings');
Route::patch('/user/settings', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/user/delete', [ProfileController::class, 'destroy'])->name('profile.delete');
Route::post('/user/settings', [ProfileController::class, 'saveSettings'])->name('profile.settings.save');
Route::get('/user/blacklist', [UserApiController::class, 'getBlacklist'])->name('profile.blacklist');
Route::post('/user/blacklist', [ProfileController::class, 'saveBlacklist'])->name('profile.blacklist.save');
// Playlist Routes for User Page
Route::get('/user/playlists', [PlaylistController::class, 'playlists'])->name('profile.playlists');
Route::get('/user/playlist/{playlist_id}', [PlaylistController::class, 'showPlaylist'])->name('profile.playlist.show');
Route::post('/create-playlist', [PlaylistController::class, 'createPlaylist'])->name('profile.playlists.create');
Route::delete('/user/playlist/{playlist_id}', [PlaylistController::class, 'deletePlaylist'])->name('profile.playlist.delete');
Route::post('/user/playlist-episode', [PlaylistController::class, 'deleteEpisodeFromPlaylist'])->name('playlist.delete.episode');
// Playlist Routes for Modals on Stream Page
Route::post('/hentai/add-to-playlist', [PlaylistController::class, 'addPlaylistApi'])->name('hentai.playlists.add');
Route::post('/hentai/create-playlist', [PlaylistController::class, 'createPlaylistApi'])->name('hentai.playlists.create');
// Download Page
Route::get('/download-search', [HomeController::class, 'downloadSearch'])->name('download.search');
});

View File

@@ -3,14 +3,12 @@
use App\Http\Controllers\ContactController; use App\Http\Controllers\ContactController;
use App\Http\Controllers\HomeController; use App\Http\Controllers\HomeController;
use App\Http\Controllers\PlaylistController; use App\Http\Controllers\PlaylistController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\StreamController; use App\Http\Controllers\StreamController;
use App\Http\Controllers\UserController;
use App\Http\Controllers\Api\AdminApiController;
use App\Http\Controllers\Api\DownloadApiController; use App\Http\Controllers\Api\DownloadApiController;
use App\Http\Controllers\Api\HentaiApiController; use App\Http\Controllers\Api\HentaiApiController;
use App\Http\Controllers\Api\StreamApiController; use App\Http\Controllers\Api\StreamApiController;
use App\Http\Controllers\Api\UserApiController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
/* /*
@@ -40,100 +38,15 @@ Route::post('/search', [HomeController::class, 'searchRedirect'])->name('hentai.
Route::get('/contact', [ContactController::class, 'index'])->name('contact.index'); Route::get('/contact', [ContactController::class, 'index'])->name('contact.index');
Route::post('/contact', [ContactController::class, 'store'])->name('contact.store'); Route::post('/contact', [ContactController::class, 'store'])->name('contact.store');
// Public Playlistts // Public Playlists
Route::get('/playlists', [PlaylistController::class, 'index'])->name('playlist.index'); Route::get('/playlists', [PlaylistController::class, 'index'])->name('playlist.index');
Route::get('/playlist/{playlist_id}', [PlaylistController::class, 'show'])->name('playlist.show'); Route::get('/playlist/{playlist_id}', [PlaylistController::class, 'show'])->name('playlist.show');
// Captcha Reload
Route::get('/reload-captcha', [ContactController::class, 'reloadCaptcha']);
// Download // Download
Route::post('/get-download', [DownloadApiController::class, 'getDownload']); Route::post('/get-download', [DownloadApiController::class, 'getDownload']);
Route::post('/update-language', [HomeController::class, 'updateLanguage'])->name('update.language'); Route::post('/update-language', [HomeController::class, 'updateLanguage'])->name('update.language');
// User Routes require __DIR__.'/user.php';
Route::middleware('auth')->group(function () { require __DIR__.'/admin.php';
Route::get('/user/profile', [ProfileController::class, 'index'])->name('profile.show');
Route::get('/user/comments', [ProfileController::class, 'comments'])->name('profile.comments');
Route::get('/user/likes', [ProfileController::class, 'likes'])->name('profile.likes');
Route::get('/user/watched', [ProfileController::class, 'watched'])->name('user.watched');
// Notifications
Route::get('/user/notifications', [App\Http\Controllers\NotificationController::class, 'index'])->name('profile.notifications');
Route::delete('/user/notifications', [App\Http\Controllers\NotificationController::class, 'delete'])->name('profile.notifications.delete');
// User Profile Actions
Route::get('/user/settings', [ProfileController::class, 'settings'])->name('profile.settings');
Route::patch('/user/settings', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/user/delete', [ProfileController::class, 'destroy'])->name('profile.delete');
Route::post('/user/settings', [ProfileController::class, 'saveSettings'])->name('profile.settings.save');
Route::get('/user/blacklist', [UserApiController::class, 'getBlacklist'])->name('profile.blacklist');
Route::post('/user/blacklist', [ProfileController::class, 'saveBlacklist'])->name('profile.blacklist.save');
// Playlist Routes for User Page
Route::get('/user/playlists', [PlaylistController::class, 'playlists'])->name('profile.playlists');
Route::get('/user/playlist/{playlist_id}', [PlaylistController::class, 'showPlaylist'])->name('profile.playlist.show');
Route::post('/create-playlist', [PlaylistController::class, 'createPlaylist'])->name('profile.playlists.create');
Route::delete('/user/playlist/{playlist_id}', [PlaylistController::class, 'deletePlaylist'])->name('profile.playlist.delete');
Route::post('/user/playlist-episode', [PlaylistController::class, 'deleteEpisodeFromPlaylist'])->name('playlist.delete.episode');
// Playlist Routes for Modals on Stream Page
Route::post('/hentai/add-to-playlist', [PlaylistController::class, 'addPlaylistApi'])->name('hentai.playlists.add');
Route::post('/hentai/create-playlist', [PlaylistController::class, 'createPlaylistApi'])->name('hentai.playlists.create');
// Download Page
Route::get('/download-search', [HomeController::class, 'downloadSearch'])->name('download.search');
});
/*
|---------------------------------------------------------------------------------
| Admin Pages
|---------------------------------------------------------------------------------
*/
Route::group(['middleware' => ['auth', 'auth.admin']], function () {
// Site alerts
Route::get('/admin/alert', [App\Http\Controllers\Admin\AlertController::class, 'index'])->name('admin.alert.index');
Route::post('/admin/alert', [App\Http\Controllers\Admin\AlertController::class, 'store'])->name('admin.alert.create');
Route::delete('/admin/alert/{alert_id}', [App\Http\Controllers\Admin\AlertController::class, 'delete'])->name('admin.alert.delete');
// Users
Route::get('/admin/users', [App\Http\Controllers\Admin\UserController::class, 'index'])->name('admin.user.index');
Route::post('/admin/users', [App\Http\Controllers\Admin\UserController::class, 'update'])->name('admin.user.update');
// Comments
Route::get('/admin/comments', [App\Http\Controllers\Admin\CommentsController::class, 'index'])->name('admin.comments.index');
// Contact page overview
Route::get('/admin/contact', [App\Http\Controllers\Admin\ContactController::class, 'index'])->name('admin.contact.index');
Route::delete('/admin/contact/{contact_id}', [App\Http\Controllers\Admin\ContactController::class, 'delete'])->name('admin.contact.delete');
// Site background settings
Route::get('/admin/background', [App\Http\Controllers\Admin\SiteBackgroundController::class, 'index'])->name('admin.background.index');
Route::post('/admin/background', [App\Http\Controllers\Admin\SiteBackgroundController::class, 'create'])->name('admin.background.create');
Route::put('/admin/background', [App\Http\Controllers\Admin\SiteBackgroundController::class, 'update'])->name('admin.background.update');
Route::delete('/admin/background', [App\Http\Controllers\Admin\SiteBackgroundController::class, 'delete'])->name('admin.background.delete');
// Release
Route::get('/admin/release', [App\Http\Controllers\Admin\ReleaseController::class, 'index'])->name('admin.upload.index');
Route::post('/admin/release/upload', [App\Http\Controllers\Admin\ReleaseController::class, 'store'])->name('admin.upload');
// Episode
Route::post('/admin/episode/upload', [App\Http\Controllers\Admin\EpisodeController::class, 'store'])->name('admin.upload.episode');
Route::post('/admin/episode/edit', [App\Http\Controllers\Admin\EpisodeController::class, 'update'])->name('admin.edit');
// Get Tags used for Upload Form
Route::get('/admin/tags', [AdminApiController::class, 'getTags'])->name('admin.tags');
Route::get('/admin/studios', [AdminApiController::class, 'getStudios'])->name('admin.studios');
// Get Tags for editing Episode
Route::get('/admin/tags/{episode_id}', [AdminApiController::class, 'getEpisodeTags'])->name('admin.tags.episode');
Route::get('/admin/studio/{episode_id}', [AdminApiController::class, 'getEpisodeStudio'])->name('admin.studio.episode');
// Subtitles
Route::get('/admin/subtitles/{episode_id}', [AdminApiController::class, 'getSubtitles'])->name('admin.subtitles');
Route::post('/admin/add-new-subtitle', [App\Http\Controllers\Admin\SubtitleController::class, 'store'])->name('admin.add.new.subtitle');
Route::post('/admin/update-subtitles', [App\Http\Controllers\Admin\SubtitleController::class, 'update'])->name('admin.update.subtitles');
});
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';