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,465 @@
<?php
namespace App\Http\Controllers\Frontend;
use App\Http\Controllers\Controller;
use App\Models\Anime;
use App\Models\Episode;
use App\Models\Watchlist;
use App\Models\EpisodeVote;
use App\Models\AnimeRating;
use App\Models\AnimeRequest;
use App\Models\AnimeRequestVote;
use App\Models\ContinueWatching;
use App\Models\UserAchievement;
use App\Models\AnimeFollow;
use App\Models\UserNotification;
use App\Models\EpisodeNote;
use App\Services\AchievementService;
use Illuminate\Http\Request;
class UserFeatureController extends Controller
{
// ── Watchlist ─────────────────────────────────────────────────────────────
public function watchlistIndex()
{
$items = Watchlist::where('user_id', auth()->id())
->with(['anime.genres'])
->orderByDesc('created_at')
->get()
->groupBy('status');
$continues = ContinueWatching::where('user_id', auth()->id())
->with(['anime', 'episode'])
->where('percent_complete', '<', 95)
->orderByDesc('updated_at')
->limit(20)
->get();
$achievements = UserAchievement::where('user_id', auth()->id())
->with('achievement')
->orderByDesc('earned_at')
->get();
return view('frontend.profile', compact('items', 'continues', 'achievements'));
}
public function watchlistToggle(Request $request, Anime $anime)
{
$this->requireAuth();
$status = $request->input('status', 'plan');
if (!array_key_exists($status, Watchlist::STATUSES)) {
$status = 'plan';
}
$existing = Watchlist::where('user_id', auth()->id())
->where('anime_id', $anime->id)
->first();
if ($existing) {
if ($existing->status === $status) {
$existing->delete();
$inList = false;
$newStatus = null;
} else {
$existing->update(['status' => $status]);
$inList = true;
$newStatus = $status;
}
} else {
Watchlist::create([
'user_id' => auth()->id(),
'anime_id' => $anime->id,
'status' => $status,
'created_at' => now(),
]);
$inList = true;
$newStatus = $status;
}
$newlyEarned = AchievementService::check(auth()->user());
return response()->json([
'in_list' => $inList,
'status' => $newStatus,
'status_label' => $newStatus ? (Watchlist::STATUSES[$newStatus] ?? '') : null,
'achievements' => array_map(fn($a) => ['title' => $a->title, 'icon' => $a->icon, 'color' => $a->color], $newlyEarned),
]);
}
// ── Watchlist Export ─────────────────────────────────────────────────────
public function watchlistExport(Request $request)
{
$user = auth()->user();
if (!$user->hasPerk('watchlist_export')) {
abort(403, 'Bu özellik için premium üyelik gerekiyor.');
}
$format = in_array($request->query('format'), ['csv', 'json']) ? $request->query('format') : 'json';
$items = Watchlist::where('user_id', $user->id)
->with('anime:id,title,mal_score,genres')
->orderBy('status')
->orderByDesc('created_at')
->get()
->map(fn($w) => [
'title' => $w->anime->title ?? '',
'status' => $w->status,
'added_at' => $w->created_at?->toDateString(),
'mal_score' => $w->anime->mal_score ?? null,
]);
if ($format === 'csv') {
$csv = "title,status,added_at,mal_score\n";
foreach ($items as $row) {
$csv .= '"' . str_replace('"', '""', $row['title']) . '",'
. $row['status'] . ','
. $row['added_at'] . ','
. $row['mal_score'] . "\n";
}
return response($csv, 200, [
'Content-Type' => 'text/csv; charset=utf-8',
'Content-Disposition' => 'attachment; filename="watchlist.csv"',
]);
}
return response()->json($items, 200, [
'Content-Disposition' => 'attachment; filename="watchlist.json"',
]);
}
// ── Episode Vote ──────────────────────────────────────────────────────────
public function episodeVote(Request $request, Episode $episode)
{
$this->requireAuth();
$vote = $request->input('vote') == 1 ? 1 : -1;
$existing = EpisodeVote::where('user_id', auth()->id())
->where('episode_id', $episode->id)
->first();
if ($existing) {
if ($existing->vote === $vote) {
$existing->delete(); // toggle off
} else {
$existing->update(['vote' => $vote]);
}
} else {
EpisodeVote::create([
'user_id' => auth()->id(),
'episode_id' => $episode->id,
'vote' => $vote,
'created_at' => now(),
]);
}
$likes = EpisodeVote::where('episode_id', $episode->id)->where('vote', 1)->count();
$dislikes = EpisodeVote::where('episode_id', $episode->id)->where('vote', -1)->count();
$myVote = EpisodeVote::where('user_id', auth()->id())->where('episode_id', $episode->id)->value('vote');
return response()->json([
'likes' => $likes,
'dislikes' => $dislikes,
'my_vote' => $myVote,
]);
}
// ── Anime Rating ─────────────────────────────────────────────────────────
public function animeRate(Request $request, Anime $anime)
{
$this->requireAuth();
$rating = (int) $request->input('rating');
if ($rating < 1 || $rating > 10) {
return response()->json(['error' => 'Geçersiz puan'], 422);
}
AnimeRating::updateOrCreate(
['user_id' => auth()->id(), 'anime_id' => $anime->id],
['rating' => $rating]
);
$avg = AnimeRating::where('anime_id', $anime->id)->avg('rating');
$count = AnimeRating::where('anime_id', $anime->id)->count();
// Anime tablosunu güncelle (ağırlıklı ortalama)
$anime->update(['rating' => round($avg, 1)]);
$newlyEarned = AchievementService::check(auth()->user());
return response()->json([
'avg' => round($avg, 1),
'count' => $count,
'my_rating' => $rating,
'achievements' => array_map(fn($a) => ['title' => $a->title, 'icon' => $a->icon, 'color' => $a->color], $newlyEarned),
]);
}
// ── Continue Watching (güncelleme) ───────────────────────────────────────
public function continueWatchingUpdate(Request $request)
{
if (!auth()->check()) {
return response()->json(['ok' => false]);
}
$data = $request->validate([
'anime_id' => 'required|integer',
'episode_id' => 'required|integer',
'season_number' => 'required|integer',
'episode_number' => 'required|integer',
'seconds' => 'required|integer|min:0',
'total' => 'nullable|integer|min:0',
'percent' => 'nullable|integer|min:0|max:100',
]);
$userId = auth()->id();
ContinueWatching::updateOrCreate(
['user_id' => $userId, 'anime_id' => $data['anime_id']],
[
'episode_id' => $data['episode_id'],
'season_number' => $data['season_number'],
'episode_number' => $data['episode_number'],
'seconds_watched' => $data['seconds'],
'total_seconds' => $data['total'] ?? 0,
'percent_complete'=> $data['percent'] ?? 0,
'updated_at' => now(),
]
);
// stream_history perki yoksa en eski kayıtları silerek 30 limiti uygula
if (!auth()->user()->hasPerk('stream_history')) {
$count = ContinueWatching::where('user_id', $userId)->count();
if ($count > 30) {
$idsToDelete = ContinueWatching::where('user_id', $userId)
->orderBy('updated_at')
->limit($count - 30)
->pluck('id');
ContinueWatching::whereIn('id', $idsToDelete)->delete();
}
}
// Başarım kontrolü (her 5 bölümde bir — performans için)
if ($data['seconds'] % 300 < 35) {
AchievementService::check(auth()->user());
}
return response()->json(['ok' => true]);
}
// ── Anime İsteği ─────────────────────────────────────────────────────────
public function requestIndex()
{
$requests = AnimeRequest::withCount('votes')
->whereIn('status', ['pending', 'approved', 'added'])
->orderByDesc('vote_count')
->orderByDesc('created_at')
->paginate(20);
$myRequests = auth()->check()
? AnimeRequest::where('user_id', auth()->id())->orderByDesc('id')->limit(5)->get()
: collect();
$votedIds = [];
if (auth()->check()) {
$votedIds = AnimeRequestVote::where('user_id', auth()->id())
->pluck('anime_request_id')->toArray();
}
return view('frontend.anime-request', compact('requests', 'myRequests', 'votedIds'));
}
public function requestStore(Request $request)
{
$this->requireAuth();
$data = $request->validate([
'title' => 'required|string|max:200',
'original_title' => 'nullable|string|max:200',
'note' => 'nullable|string|max:1000',
]);
// Benzer istek var mı?
$existing = AnimeRequest::whereRaw('LOWER(title) = ?', [strtolower($data['title'])])->first();
if ($existing) {
// Oy ekle
$voted = AnimeRequestVote::where('anime_request_id', $existing->id)
->where('user_id', auth()->id())
->exists();
if (!$voted) {
AnimeRequestVote::create(['anime_request_id' => $existing->id, 'user_id' => auth()->id(), 'created_at' => now()]);
$existing->increment('vote_count');
}
return response()->json(['ok' => true, 'merged' => true, 'request_id' => $existing->id, 'vote_count' => $existing->fresh()->vote_count]);
}
$req = AnimeRequest::create([
'user_id' => auth()->id(),
'title' => $data['title'],
'original_title' => $data['original_title'] ?? null,
'note' => $data['note'] ?? null,
'status' => 'pending',
'vote_count' => 1,
]);
AnimeRequestVote::create(['anime_request_id' => $req->id, 'user_id' => auth()->id(), 'created_at' => now()]);
AchievementService::check(auth()->user());
return response()->json(['ok' => true, 'merged' => false, 'request_id' => $req->id, 'vote_count' => 1]);
}
public function requestVote(AnimeRequest $animeRequest)
{
$this->requireAuth();
$voted = AnimeRequestVote::where('anime_request_id', $animeRequest->id)
->where('user_id', auth()->id())
->exists();
if ($voted) {
AnimeRequestVote::where('anime_request_id', $animeRequest->id)
->where('user_id', auth()->id())
->delete();
$animeRequest->decrement('vote_count');
$isVoted = false;
} else {
AnimeRequestVote::create(['anime_request_id' => $animeRequest->id, 'user_id' => auth()->id(), 'created_at' => now()]);
$animeRequest->increment('vote_count');
$isVoted = true;
}
return response()->json(['ok' => true, 'voted' => $isVoted, 'vote_count' => $animeRequest->fresh()->vote_count]);
}
// ── Anime Takip ──────────────────────────────────────────────────────────
public function followToggle(Anime $anime)
{
$this->requireAuth();
$userId = auth()->id();
$existing = AnimeFollow::where('user_id', $userId)->where('anime_id', $anime->id)->first();
if ($existing) {
$existing->delete();
$following = false;
} else {
AnimeFollow::create(['user_id' => $userId, 'anime_id' => $anime->id]);
$following = true;
}
$count = AnimeFollow::where('anime_id', $anime->id)->count();
return response()->json(['following' => $following, 'count' => $count]);
}
// ── Bildirimler ───────────────────────────────────────────────────────────
public function notificationsIndex()
{
$this->requireAuth();
$notifications = UserNotification::where('user_id', auth()->id())
->orderByDesc('created_at')
->paginate(30);
// Görüntülenince hepsini okundu yap
UserNotification::where('user_id', auth()->id())
->whereNull('read_at')
->update(['read_at' => now()]);
return view('frontend.notifications', compact('notifications'));
}
public function notificationsCount()
{
if (!auth()->check()) {
return response()->json(['count' => 0]);
}
$count = UserNotification::where('user_id', auth()->id())->whereNull('read_at')->count();
return response()->json(['count' => $count]);
}
// ── Bölüm Notları ─────────────────────────────────────────────────────────
public function noteStore(Request $request, Episode $episode)
{
$this->requireAuth();
$data = $request->validate([
'content' => 'required|string|max:500',
'timestamp_at' => 'nullable|integer|min:0',
]);
$note = EpisodeNote::create([
'user_id' => auth()->id(),
'episode_id' => $episode->id,
'anime_id' => $episode->anime_id,
'content' => $data['content'],
'timestamp_at' => $data['timestamp_at'] ?? null,
]);
return response()->json([
'ok' => true,
'note' => [
'id' => $note->id,
'content' => $note->content,
'timestamp_label' => $note->timestamp_label,
'timestamp_at' => $note->timestamp_at,
'created_at' => $note->created_at->format('d.m.Y H:i'),
],
]);
}
public function noteDelete(EpisodeNote $note)
{
$this->requireAuth();
if ($note->user_id !== auth()->id()) {
abort(403);
}
$note->delete();
return response()->json(['ok' => true]);
}
public function episodeNotesList(Episode $episode)
{
$this->requireAuth();
$notes = EpisodeNote::where('user_id', auth()->id())
->where('episode_id', $episode->id)
->orderBy('timestamp_at')
->orderBy('created_at')
->get()
->map(fn($n) => [
'id' => $n->id,
'content' => $n->content,
'timestamp_label' => $n->timestamp_label,
'timestamp_at' => $n->timestamp_at,
'created_at' => $n->created_at->format('d.m.Y H:i'),
]);
return response()->json(['notes' => $notes]);
}
// ── Helper ───────────────────────────────────────────────────────────────
private function requireAuth()
{
if (!auth()->check()) {
abort(401);
}
}
}