Initial commit: Animexe Laravel platform

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:01:48 +03:00
co-authored by Claude Opus 4.8
commit a63515cfc6
366 changed files with 74773 additions and 0 deletions
@@ -0,0 +1,300 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Anime;
use App\Models\AnimeRating;
use App\Models\AnimeFollow;
use App\Models\ContinueWatching;
use App\Models\EpisodeVote;
use App\Models\EpisodeNote;
use App\Models\UserNotification;
use App\Models\UserAchievement;
use App\Models\Achievement;
use App\Models\Watchlist;
use App\Models\AnimeRequest;
use App\Models\AnimeRequestVote;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class UserApiController extends Controller
{
// ── Watchlist ──────────────────────────────────────────────────────────────
public function watchlist(Request $request)
{
$status = $request->input('status'); // watching|completed|plan_to_watch|dropped
$query = Watchlist::where('user_id', $request->user()->id)
->with('anime:id,title,slug,cover_image,rating,episode_count,status,release_year');
if ($status) $query->where('status', $status);
$items = $query->orderByDesc('updated_at')->paginate(24);
return response()->json([
'data' => collect($items->items())->map(fn($w) => [
'id' => $w->id,
'status' => $w->status,
'anime' => $w->anime ? [
'id' => $w->anime->id,
'title' => $w->anime->title,
'slug' => $w->anime->slug,
'cover_url' => \App\Support\MediaUrl::fromStoragePath($w->anime->cover_image),
'rating' => $w->anime->rating,
'episode_count' => $w->anime->episode_count,
'status' => $w->anime->status,
'release_year' => $w->anime->release_year,
] : null,
]),
'total' => $items->total(),
'last_page'=> $items->lastPage(),
]);
}
public function watchlistToggle(Request $request, Anime $anime)
{
$user = $request->user();
$status = $request->input('status', 'plan_to_watch');
$existing = Watchlist::where('user_id', $user->id)->where('anime_id', $anime->id)->first();
if ($existing) {
if ($existing->status === $status) {
$existing->delete();
return response()->json(['in_watchlist' => false, 'status' => null]);
}
$existing->update(['status' => $status]);
return response()->json(['in_watchlist' => true, 'status' => $status]);
}
Watchlist::create(['user_id' => $user->id, 'anime_id' => $anime->id, 'status' => $status]);
return response()->json(['in_watchlist' => true, 'status' => $status]);
}
// ── Continue Watching ──────────────────────────────────────────────────────
public function continueWatchingUpdate(Request $request)
{
$data = $request->validate([
'anime_id' => 'required|exists:animes,id',
'season_number' => 'required|integer|min:1',
'episode_number' => 'required|integer|min:1',
'percent_complete' => 'required|numeric|min:0|max:100',
]);
ContinueWatching::updateOrCreate(
['user_id' => $request->user()->id, 'anime_id' => $data['anime_id']],
[
'season_number' => $data['season_number'],
'episode_number' => $data['episode_number'],
'percent_complete' => $data['percent_complete'],
]
);
return response()->json(['ok' => true]);
}
// ── Anime Rate ─────────────────────────────────────────────────────────────
public function animeRate(Request $request, Anime $anime)
{
$data = $request->validate(['rating' => 'required|numeric|min:1|max:10']);
AnimeRating::updateOrCreate(
['user_id' => $request->user()->id, 'anime_id' => $anime->id],
['rating' => $data['rating']]
);
$avg = AnimeRating::where('anime_id', $anime->id)->avg('rating');
$anime->update(['rating' => round($avg, 1)]);
return response()->json(['rating' => $data['rating'], 'avg' => round($avg, 1)]);
}
// ── Follow ─────────────────────────────────────────────────────────────────
public function followToggle(Request $request, Anime $anime)
{
$user = $request->user();
$existing = AnimeFollow::where('user_id', $user->id)->where('anime_id', $anime->id)->first();
if ($existing) {
$existing->delete();
return response()->json(['following' => false]);
}
AnimeFollow::create(['user_id' => $user->id, 'anime_id' => $anime->id]);
return response()->json(['following' => true]);
}
// ── Notifications ──────────────────────────────────────────────────────────
public function notifications(Request $request)
{
$items = UserNotification::where('user_id', $request->user()->id)
->orderByDesc('created_at')->paginate(20);
// Mark all as read
UserNotification::where('user_id', $request->user()->id)->whereNull('read_at')->update(['read_at' => now()]);
return response()->json([
'data' => collect($items->items())->map(fn($n) => [
'id' => $n->id,
'type' => $n->type,
'data' => $n->data ?? [],
'is_read' => $n->is_read,
'created_at' => $n->created_at?->diffForHumans(),
]),
'total' => $items->total(),
'last_page'=> $items->lastPage(),
]);
}
public function notificationsCount(Request $request)
{
if (!$request->user()) return response()->json(['count' => 0]);
$count = UserNotification::where('user_id', $request->user()->id)->whereNull('read_at')->count();
return response()->json(['count' => $count]);
}
// ── Achievements ───────────────────────────────────────────────────────────
public function achievements(Request $request)
{
$all = Achievement::orderBy('points')->get();
$earned = UserAchievement::where('user_id', $request->user()->id)->pluck('achievement_id')->toArray();
$total = UserAchievement::where('user_id', $request->user()->id)->join('achievements','achievements.id','=','user_achievements.achievement_id')->sum('achievements.points');
return response()->json([
'total_points' => (int)$total,
'data' => $all->map(fn($a) => [
'id' => $a->id,
'name' => $a->name,
'description' => $a->description,
'icon' => $a->icon,
'points' => $a->points,
'earned' => in_array($a->id, $earned),
'earned_at' => in_array($a->id, $earned)
? UserAchievement::where('user_id', $request->user()->id)->where('achievement_id', $a->id)->value('created_at')?->toISOString()
: null,
]),
]);
}
// ── Episode Notes ──────────────────────────────────────────────────────────
public function noteStore(Request $request, $episodeId)
{
$data = $request->validate(['note' => 'required|string|max:1000']);
$note = EpisodeNote::create([
'user_id' => $request->user()->id,
'episode_id' => $episodeId,
'note' => $data['note'],
]);
return response()->json(['id' => $note->id, 'note' => $note->note, 'created_at' => $note->created_at?->toISOString()], 201);
}
public function noteDelete(Request $request, $noteId)
{
$note = EpisodeNote::where('id', $noteId)->where('user_id', $request->user()->id)->firstOrFail();
$note->delete();
return response()->json(['ok' => true]);
}
public function episodeNotesList(Request $request, $episodeId)
{
$notes = EpisodeNote::where('user_id', $request->user()->id)
->where('episode_id', $episodeId)
->orderByDesc('created_at')->get();
return response()->json($notes->map(fn($n) => [
'id' => $n->id,
'note' => $n->note,
'created_at' => $n->created_at?->toISOString(),
]));
}
// ── Anime Requests ─────────────────────────────────────────────────────────
public function requestIndex(Request $request)
{
$items = AnimeRequest::withCount('votes')
->orderByDesc('votes_count')->orderByDesc('created_at')->paginate(20);
return response()->json([
'data' => collect($items->items())->map(fn($r) => [
'id' => $r->id,
'title' => $r->title,
'note' => $r->note,
'status' => $r->status,
'votes_count' => $r->votes_count,
'created_at' => $r->created_at?->diffForHumans(),
'user_voted' => $request->user()
? AnimeRequestVote::where('user_id', $request->user()->id)->where('anime_request_id', $r->id)->exists()
: false,
]),
'total' => $items->total(),
'last_page'=> $items->lastPage(),
]);
}
public function requestStore(Request $request)
{
$data = $request->validate([
'title' => 'required|string|max:200',
'note' => 'nullable|string|max:500',
]);
$req = AnimeRequest::create([
'user_id' => $request->user()->id,
'title' => $data['title'],
'note' => $data['note'] ?? null,
'status' => 'pending',
]);
return response()->json(['id' => $req->id, 'title' => $req->title], 201);
}
public function requestVote(Request $request, AnimeRequest $animeRequest)
{
$user = $request->user();
$existing = AnimeRequestVote::where('user_id', $user->id)->where('anime_request_id', $animeRequest->id)->first();
if ($existing) {
$existing->delete();
return response()->json(['voted' => false, 'votes' => $animeRequest->votes()->count()]);
}
AnimeRequestVote::create(['user_id' => $user->id, 'anime_request_id' => $animeRequest->id]);
return response()->json(['voted' => true, 'votes' => $animeRequest->votes()->count()]);
}
// ── Profile Stats ──────────────────────────────────────────────────────────
public function profileStats(Request $request)
{
$userId = $request->user()->id;
$watchlistCount = Watchlist::where('user_id', $userId)->count();
$completedCount = Watchlist::where('user_id', $userId)->where('status', 'completed')->count();
$notifCount = UserNotification::where('user_id', $userId)->where('is_read', false)->count();
$achPoints = UserAchievement::where('user_id', $userId)
->join('achievements','achievements.id','=','user_achievements.achievement_id')
->sum('achievements.points');
$achCount = UserAchievement::where('user_id', $userId)->count();
$commentCount = \App\Models\Comment::where('user_id', $userId)->count();
return response()->json([
'watchlist_count' => $watchlistCount,
'completed_count' => $completedCount,
'notif_count' => (int)$notifCount,
'achievement_points'=> (int)$achPoints,
'achievement_count' => $achCount,
'comment_count' => $commentCount,
]);
}
}