update
This commit is contained in:
@@ -5,8 +5,11 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkComment;
|
||||
use App\Models\User;
|
||||
use App\Models\UserMention;
|
||||
use App\Notifications\ArtworkCommentedNotification;
|
||||
use App\Notifications\ArtworkMentionedNotification;
|
||||
use App\Services\ContentSanitizer;
|
||||
use App\Services\LegacySmileyMapper;
|
||||
use App\Support\AvatarUrl;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -113,6 +116,7 @@ class ArtworkCommentController extends Controller
|
||||
Cache::forget('comments.latest.all.page1');
|
||||
|
||||
$comment->load(['user', 'user.profile']);
|
||||
$this->notifyRecipients($artwork, $comment, $request->user(), $parentId ? (int) $parentId : null);
|
||||
|
||||
// Record activity event (fire-and-forget; never break the response)
|
||||
try {
|
||||
@@ -204,6 +208,8 @@ class ArtworkCommentController extends Controller
|
||||
'display' => $user?->username ?? $user?->name ?? 'User',
|
||||
'profile_url' => $user?->username ? '/@' . $user->username : '/profile/' . $userId,
|
||||
'avatar_url' => AvatarUrl::forUser($userId, $avatarHash, 64),
|
||||
'level' => (int) ($user?->level ?? 1),
|
||||
'rank' => (string) ($user?->rank ?? 'Newbie'),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -217,4 +223,48 @@ class ArtworkCommentController extends Controller
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function notifyRecipients(Artwork $artwork, ArtworkComment $comment, User $actor, ?int $parentId): void
|
||||
{
|
||||
$notifiedUserIds = [];
|
||||
$creatorId = (int) ($artwork->user_id ?? 0);
|
||||
|
||||
if ($creatorId > 0 && $creatorId !== (int) $actor->id) {
|
||||
$creator = User::query()->find($creatorId);
|
||||
if ($creator) {
|
||||
$creator->notify(new ArtworkCommentedNotification($artwork, $comment, $actor));
|
||||
$notifiedUserIds[] = (int) $creator->id;
|
||||
}
|
||||
}
|
||||
|
||||
if ($parentId) {
|
||||
$parentUserId = (int) (ArtworkComment::query()->whereKey($parentId)->value('user_id') ?? 0);
|
||||
if ($parentUserId > 0 && $parentUserId !== (int) $actor->id && ! in_array($parentUserId, $notifiedUserIds, true)) {
|
||||
$parentUser = User::query()->find($parentUserId);
|
||||
if ($parentUser) {
|
||||
$parentUser->notify(new ArtworkCommentedNotification($artwork, $comment, $actor));
|
||||
$notifiedUserIds[] = (int) $parentUser->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
User::query()
|
||||
->whereIn(
|
||||
'id',
|
||||
UserMention::query()
|
||||
->where('comment_id', (int) $comment->id)
|
||||
->pluck('mentioned_user_id')
|
||||
->map(fn ($id) => (int) $id)
|
||||
->unique()
|
||||
->all()
|
||||
)
|
||||
->get()
|
||||
->each(function (User $mentionedUser) use ($artwork, $comment, $actor): void {
|
||||
if ((int) $mentionedUser->id === (int) $actor->id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mentionedUser->notify(new ArtworkMentionedNotification($artwork, $comment, $actor));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Events\Achievements\AchievementCheckRequested;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Notifications\ArtworkLikedNotification;
|
||||
use App\Services\FollowService;
|
||||
use App\Services\UserStatsService;
|
||||
use App\Services\XPService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -14,11 +18,25 @@ use Illuminate\Support\Facades\Schema;
|
||||
|
||||
final class ArtworkInteractionController extends Controller
|
||||
{
|
||||
public function bookmark(Request $request, int $artworkId): JsonResponse
|
||||
{
|
||||
$this->toggleSimple(
|
||||
request: $request,
|
||||
table: 'artwork_bookmarks',
|
||||
keyColumns: ['user_id', 'artwork_id'],
|
||||
keyValues: ['user_id' => (int) $request->user()->id, 'artwork_id' => $artworkId],
|
||||
insertPayload: ['created_at' => now(), 'updated_at' => now()],
|
||||
requiredTable: 'artwork_bookmarks'
|
||||
);
|
||||
|
||||
return response()->json($this->statusPayload((int) $request->user()->id, $artworkId));
|
||||
}
|
||||
|
||||
public function favorite(Request $request, int $artworkId): JsonResponse
|
||||
{
|
||||
$state = $request->boolean('state', true);
|
||||
|
||||
$this->toggleSimple(
|
||||
$changed = $this->toggleSimple(
|
||||
request: $request,
|
||||
table: 'artwork_favourites',
|
||||
keyColumns: ['user_id', 'artwork_id'],
|
||||
@@ -33,7 +51,7 @@ final class ArtworkInteractionController extends Controller
|
||||
$creatorId = (int) DB::table('artworks')->where('id', $artworkId)->value('user_id');
|
||||
if ($creatorId) {
|
||||
$svc = app(UserStatsService::class);
|
||||
if ($state) {
|
||||
if ($state && $changed) {
|
||||
$svc->incrementFavoritesReceived($creatorId);
|
||||
$svc->setLastActiveAt((int) $request->user()->id);
|
||||
|
||||
@@ -46,7 +64,7 @@ final class ArtworkInteractionController extends Controller
|
||||
targetId: $artworkId,
|
||||
);
|
||||
} catch (\Throwable) {}
|
||||
} else {
|
||||
} elseif (! $state && $changed) {
|
||||
$svc->decrementFavoritesReceived($creatorId);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +74,7 @@ final class ArtworkInteractionController extends Controller
|
||||
|
||||
public function like(Request $request, int $artworkId): JsonResponse
|
||||
{
|
||||
$this->toggleSimple(
|
||||
$changed = $this->toggleSimple(
|
||||
request: $request,
|
||||
table: 'artwork_likes',
|
||||
keyColumns: ['user_id', 'artwork_id'],
|
||||
@@ -67,6 +85,20 @@ final class ArtworkInteractionController extends Controller
|
||||
|
||||
$this->syncArtworkStats($artworkId);
|
||||
|
||||
if ($request->boolean('state', true) && $changed) {
|
||||
$creatorId = (int) DB::table('artworks')->where('id', $artworkId)->value('user_id');
|
||||
$actorId = (int) $request->user()->id;
|
||||
if ($creatorId > 0 && $creatorId !== $actorId) {
|
||||
app(XPService::class)->awardArtworkLikeReceived($creatorId, $artworkId, $actorId);
|
||||
$creator = \App\Models\User::query()->find($creatorId);
|
||||
$artwork = Artwork::query()->find($artworkId);
|
||||
if ($creator && $artwork) {
|
||||
$creator->notify(new ArtworkLikedNotification($artwork, $request->user()));
|
||||
}
|
||||
event(new AchievementCheckRequested($creatorId));
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($this->statusPayload((int) $request->user()->id, $artworkId));
|
||||
}
|
||||
|
||||
@@ -104,8 +136,10 @@ final class ArtworkInteractionController extends Controller
|
||||
return response()->json(['message' => 'Cannot follow yourself'], 422);
|
||||
}
|
||||
|
||||
$svc = app(FollowService::class);
|
||||
$state = $request->boolean('state', true);
|
||||
$svc = app(FollowService::class);
|
||||
$state = $request->has('state')
|
||||
? $request->boolean('state')
|
||||
: ! $request->isMethod('delete');
|
||||
|
||||
if ($state) {
|
||||
$svc->follow($actorId, $userId);
|
||||
@@ -148,7 +182,7 @@ final class ArtworkInteractionController extends Controller
|
||||
array $keyValues,
|
||||
array $insertPayload,
|
||||
string $requiredTable
|
||||
): void {
|
||||
): bool {
|
||||
if (! Schema::hasTable($requiredTable)) {
|
||||
abort(422, 'Interaction unavailable');
|
||||
}
|
||||
@@ -163,10 +197,13 @@ final class ArtworkInteractionController extends Controller
|
||||
if ($state) {
|
||||
if (! $query->exists()) {
|
||||
DB::table($table)->insert(array_merge($keyValues, $insertPayload));
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
$query->delete();
|
||||
return $query->delete() > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function syncArtworkStats(int $artworkId): void
|
||||
@@ -194,6 +231,10 @@ final class ArtworkInteractionController extends Controller
|
||||
|
||||
private function statusPayload(int $viewerId, int $artworkId): array
|
||||
{
|
||||
$isBookmarked = Schema::hasTable('artwork_bookmarks')
|
||||
? DB::table('artwork_bookmarks')->where('user_id', $viewerId)->where('artwork_id', $artworkId)->exists()
|
||||
: false;
|
||||
|
||||
$isFavorited = Schema::hasTable('artwork_favourites')
|
||||
? DB::table('artwork_favourites')->where('user_id', $viewerId)->where('artwork_id', $artworkId)->exists()
|
||||
: false;
|
||||
@@ -206,15 +247,21 @@ final class ArtworkInteractionController extends Controller
|
||||
? (int) DB::table('artwork_favourites')->where('artwork_id', $artworkId)->count()
|
||||
: 0;
|
||||
|
||||
$bookmarks = Schema::hasTable('artwork_bookmarks')
|
||||
? (int) DB::table('artwork_bookmarks')->where('artwork_id', $artworkId)->count()
|
||||
: 0;
|
||||
|
||||
$likes = Schema::hasTable('artwork_likes')
|
||||
? (int) DB::table('artwork_likes')->where('artwork_id', $artworkId)->count()
|
||||
: 0;
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'is_bookmarked' => $isBookmarked,
|
||||
'is_favorited' => $isFavorited,
|
||||
'is_liked' => $isLiked,
|
||||
'stats' => [
|
||||
'bookmarks' => $bookmarks,
|
||||
'favorites' => $favorites,
|
||||
'likes' => $likes,
|
||||
],
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Services\ArtworkStatsService;
|
||||
use App\Services\XPService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -26,7 +27,10 @@ use Illuminate\Http\Request;
|
||||
*/
|
||||
final class ArtworkViewController extends Controller
|
||||
{
|
||||
public function __construct(private readonly ArtworkStatsService $stats) {}
|
||||
public function __construct(
|
||||
private readonly ArtworkStatsService $stats,
|
||||
private readonly XPService $xp,
|
||||
) {}
|
||||
|
||||
public function __invoke(Request $request, int $id): JsonResponse
|
||||
{
|
||||
@@ -52,6 +56,16 @@ final class ArtworkViewController extends Controller
|
||||
// Defer to Redis when available, fall back to direct DB increment.
|
||||
$this->stats->incrementViews((int) $artwork->id, 1, defer: true);
|
||||
|
||||
$viewerId = $request->user()?->id;
|
||||
if ($artwork->user_id !== null && (int) $artwork->user_id !== (int) ($viewerId ?? 0)) {
|
||||
$this->xp->awardArtworkViewReceived(
|
||||
(int) $artwork->user_id,
|
||||
(int) $artwork->id,
|
||||
$viewerId,
|
||||
(string) $request->ip(),
|
||||
);
|
||||
}
|
||||
|
||||
// Mark this session so the artwork is not counted again.
|
||||
if ($request->hasSession()) {
|
||||
$request->session()->put($sessionKey, true);
|
||||
|
||||
@@ -36,6 +36,10 @@ final class CommunityActivityController extends Controller
|
||||
|
||||
private function resolveFilter(Request $request): string
|
||||
{
|
||||
if ($request->filled('type') && ! $request->filled('filter')) {
|
||||
return (string) $request->query('type', 'all');
|
||||
}
|
||||
|
||||
if ($request->boolean('following') && ! $request->filled('filter')) {
|
||||
return 'following';
|
||||
}
|
||||
|
||||
35
app/Http/Controllers/Api/LeaderboardController.php
Normal file
35
app/Http/Controllers/Api/LeaderboardController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Leaderboard;
|
||||
use App\Services\LeaderboardService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class LeaderboardController extends Controller
|
||||
{
|
||||
public function creators(Request $request, LeaderboardService $leaderboards): JsonResponse
|
||||
{
|
||||
return response()->json(
|
||||
$leaderboards->getLeaderboard(Leaderboard::TYPE_CREATOR, (string) $request->query('period', 'weekly'))
|
||||
);
|
||||
}
|
||||
|
||||
public function artworks(Request $request, LeaderboardService $leaderboards): JsonResponse
|
||||
{
|
||||
return response()->json(
|
||||
$leaderboards->getLeaderboard(Leaderboard::TYPE_ARTWORK, (string) $request->query('period', 'weekly'))
|
||||
);
|
||||
}
|
||||
|
||||
public function stories(Request $request, LeaderboardService $leaderboards): JsonResponse
|
||||
{
|
||||
return response()->json(
|
||||
$leaderboards->getLeaderboard(Leaderboard::TYPE_STORY, (string) $request->query('period', 'weekly'))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Services\Posts\NotificationDigestService;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -14,48 +14,24 @@ use App\Http\Controllers\Controller;
|
||||
*/
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
public function __construct(private NotificationDigestService $digest) {}
|
||||
public function __construct(private NotificationService $notifications) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
$page = max(1, (int) $request->query('page', 1));
|
||||
|
||||
$notifications = $user->notifications()
|
||||
->latest()
|
||||
->limit(200) // aggregate from last 200 raw notifs
|
||||
->get();
|
||||
|
||||
$digested = $this->digest->aggregate($notifications);
|
||||
|
||||
// Simple manual pagination on the digested array
|
||||
$perPage = 20;
|
||||
$total = count($digested);
|
||||
$sliced = array_slice($digested, ($page - 1) * $perPage, $perPage);
|
||||
$unread = $user->unreadNotifications()->count();
|
||||
|
||||
return response()->json([
|
||||
'data' => array_values($sliced),
|
||||
'unread_count' => $unread,
|
||||
'meta' => [
|
||||
'total' => $total,
|
||||
'current_page' => $page,
|
||||
'last_page' => (int) ceil($total / $perPage) ?: 1,
|
||||
'per_page' => $perPage,
|
||||
],
|
||||
]);
|
||||
return response()->json(
|
||||
$this->notifications->listForUser($request->user(), (int) $request->query('page', 1), 20)
|
||||
);
|
||||
}
|
||||
|
||||
public function readAll(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->unreadNotifications()->update(['read_at' => now()]);
|
||||
$this->notifications->markAllRead($request->user());
|
||||
return response()->json(['message' => 'All notifications marked as read.']);
|
||||
}
|
||||
|
||||
public function markRead(Request $request, string $id): JsonResponse
|
||||
{
|
||||
$notif = $request->user()->notifications()->findOrFail($id);
|
||||
$notif->markAsRead();
|
||||
$this->notifications->markRead($request->user(), $id);
|
||||
return response()->json(['message' => 'Notification marked as read.']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ class PostCommentController extends Controller
|
||||
'username' => $comment->user->username,
|
||||
'name' => $comment->user->name,
|
||||
'avatar' => $comment->user->profile?->avatar_url ?? null,
|
||||
'level' => (int) ($comment->user->level ?? 1),
|
||||
'rank' => (string) ($comment->user->rank ?? 'Newbie'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\User;
|
||||
use Carbon\CarbonInterface;
|
||||
use App\Services\ThumbnailPresenter;
|
||||
use App\Services\ThumbnailService;
|
||||
use App\Support\AvatarUrl;
|
||||
use App\Support\UsernamePolicy;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -57,20 +56,9 @@ final class ProfileApiController extends Controller
|
||||
$perPage = 24;
|
||||
$paginator = $query->cursorPaginate($perPage);
|
||||
|
||||
$data = collect($paginator->items())->map(function (Artwork $art) {
|
||||
$present = ThumbnailPresenter::present($art, 'md');
|
||||
return [
|
||||
'id' => $art->id,
|
||||
'name' => $art->title,
|
||||
'thumb' => $present['url'],
|
||||
'thumb_srcset' => $present['srcset'] ?? $present['url'],
|
||||
'width' => $art->width,
|
||||
'height' => $art->height,
|
||||
'username' => $art->user->username ?? null,
|
||||
'uname' => $art->user->username ?? $art->user->name ?? 'Skinbase',
|
||||
'published_at' => $art->published_at,
|
||||
];
|
||||
})->values();
|
||||
$data = collect($paginator->items())
|
||||
->map(fn (Artwork $art) => $this->mapArtworkCardPayload($art))
|
||||
->values();
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
@@ -85,7 +73,8 @@ final class ProfileApiController extends Controller
|
||||
*/
|
||||
public function favourites(Request $request, string $username): JsonResponse
|
||||
{
|
||||
if (! Schema::hasTable('user_favorites')) {
|
||||
$favouriteTable = $this->resolveFavouriteTable();
|
||||
if ($favouriteTable === null) {
|
||||
return response()->json(['data' => [], 'next_cursor' => null, 'has_more' => false]);
|
||||
}
|
||||
|
||||
@@ -95,16 +84,18 @@ final class ProfileApiController extends Controller
|
||||
}
|
||||
|
||||
$perPage = 24;
|
||||
$cursor = $request->input('cursor');
|
||||
$offset = max(0, (int) base64_decode((string) $request->input('cursor', ''), true));
|
||||
|
||||
$favIds = DB::table('user_favorites as uf')
|
||||
->join('artworks as a', 'a.id', '=', 'uf.artwork_id')
|
||||
->where('uf.user_id', $user->id)
|
||||
$favIds = DB::table($favouriteTable . ' as af')
|
||||
->join('artworks as a', 'a.id', '=', 'af.artwork_id')
|
||||
->where('af.user_id', $user->id)
|
||||
->whereNull('a.deleted_at')
|
||||
->where('a.is_public', true)
|
||||
->where('a.is_approved', true)
|
||||
->orderByDesc('uf.created_at')
|
||||
->offset($cursor ? (int) base64_decode($cursor) : 0)
|
||||
->whereNotNull('a.published_at')
|
||||
->orderByDesc('af.created_at')
|
||||
->orderByDesc('af.artwork_id')
|
||||
->offset($offset)
|
||||
->limit($perPage + 1)
|
||||
->pluck('a.id');
|
||||
|
||||
@@ -120,24 +111,14 @@ final class ProfileApiController extends Controller
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
$data = $favIds->filter(fn ($id) => $indexed->has($id))->map(function ($id) use ($indexed) {
|
||||
$art = $indexed[$id];
|
||||
$present = ThumbnailPresenter::present($art, 'md');
|
||||
return [
|
||||
'id' => $art->id,
|
||||
'name' => $art->title,
|
||||
'thumb' => $present['url'],
|
||||
'thumb_srcset' => $present['srcset'] ?? $present['url'],
|
||||
'width' => $art->width,
|
||||
'height' => $art->height,
|
||||
'username' => $art->user->username ?? null,
|
||||
'uname' => $art->user->username ?? $art->user->name ?? 'Skinbase',
|
||||
];
|
||||
})->values();
|
||||
$data = $favIds
|
||||
->filter(fn ($id) => $indexed->has($id))
|
||||
->map(fn ($id) => $this->mapArtworkCardPayload($indexed[$id]))
|
||||
->values();
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
'next_cursor' => null, // Simple offset pagination for now
|
||||
'next_cursor' => $hasMore ? base64_encode((string) ($offset + $perPage)) : null,
|
||||
'has_more' => $hasMore,
|
||||
]);
|
||||
}
|
||||
@@ -174,4 +155,48 @@ final class ProfileApiController extends Controller
|
||||
$normalized = UsernamePolicy::normalize($username);
|
||||
return User::query()->whereRaw('LOWER(username) = ?', [$normalized])->first();
|
||||
}
|
||||
|
||||
private function resolveFavouriteTable(): ?string
|
||||
{
|
||||
foreach (['artwork_favourites', 'user_favorites', 'artworks_favourites', 'favourites'] as $table) {
|
||||
if (Schema::hasTable($table)) {
|
||||
return $table;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function mapArtworkCardPayload(Artwork $art): array
|
||||
{
|
||||
$present = ThumbnailPresenter::present($art, 'md');
|
||||
|
||||
return [
|
||||
'id' => $art->id,
|
||||
'name' => $art->title,
|
||||
'thumb' => $present['url'],
|
||||
'thumb_srcset' => $present['srcset'] ?? $present['url'],
|
||||
'width' => $art->width,
|
||||
'height' => $art->height,
|
||||
'username' => $art->user->username ?? null,
|
||||
'uname' => $art->user->username ?? $art->user->name ?? 'Skinbase',
|
||||
'published_at' => $this->formatIsoDate($art->published_at),
|
||||
];
|
||||
}
|
||||
|
||||
private function formatIsoDate(mixed $value): ?string
|
||||
{
|
||||
if ($value instanceof CarbonInterface) {
|
||||
return $value->toISOString();
|
||||
}
|
||||
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return $value->format(DATE_ATOM);
|
||||
}
|
||||
|
||||
return is_string($value) ? $value : null;
|
||||
}
|
||||
}
|
||||
|
||||
34
app/Http/Controllers/Api/SocialActivityController.php
Normal file
34
app/Http/Controllers/Api/SocialActivityController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\ActivityService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class SocialActivityController extends Controller
|
||||
{
|
||||
public function __construct(private readonly ActivityService $activity) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$filter = (string) $request->query('filter', 'all');
|
||||
|
||||
if ($this->activity->requiresAuthentication($filter) && ! $request->user()) {
|
||||
return response()->json(['error' => 'Unauthenticated'], 401);
|
||||
}
|
||||
|
||||
return response()->json(
|
||||
$this->activity->communityFeed(
|
||||
viewer: $request->user(),
|
||||
filter: $filter,
|
||||
page: (int) $request->query('page', 1),
|
||||
perPage: (int) $request->query('per_page', 20),
|
||||
actorUserId: $request->filled('user_id') ? (int) $request->query('user_id') : null,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
212
app/Http/Controllers/Api/SocialCompatibilityController.php
Normal file
212
app/Http/Controllers/Api/SocialCompatibilityController.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\Story;
|
||||
use App\Models\StoryBookmark;
|
||||
use App\Services\SocialService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
final class SocialCompatibilityController extends Controller
|
||||
{
|
||||
public function __construct(private readonly SocialService $social) {}
|
||||
|
||||
public function like(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validate([
|
||||
'entity_type' => ['required', 'string', 'in:artwork,story'],
|
||||
'entity_id' => ['required', 'integer'],
|
||||
'state' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
$state = array_key_exists('state', $payload)
|
||||
? (bool) $payload['state']
|
||||
: ! $request->isMethod('delete');
|
||||
|
||||
if ($payload['entity_type'] === 'story') {
|
||||
$story = Story::published()->findOrFail((int) $payload['entity_id']);
|
||||
|
||||
$result = $this->social->toggleStoryLike($request->user(), $story, $state);
|
||||
|
||||
return response()->json([
|
||||
'ok' => (bool) ($result['ok'] ?? true),
|
||||
'liked' => (bool) ($result['liked'] ?? false),
|
||||
'likes_count' => (int) ($result['likes_count'] ?? 0),
|
||||
'is_liked' => (bool) ($result['liked'] ?? false),
|
||||
'stats' => [
|
||||
'likes' => (int) ($result['likes_count'] ?? 0),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$artworkId = (int) $payload['entity_id'];
|
||||
abort_unless(Artwork::public()->published()->whereKey($artworkId)->exists(), 404);
|
||||
|
||||
return app(ArtworkInteractionController::class)->like(
|
||||
$request->merge(['state' => $state]),
|
||||
$artworkId,
|
||||
);
|
||||
}
|
||||
|
||||
public function comments(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validate([
|
||||
'entity_type' => ['required', 'string', 'in:artwork,story'],
|
||||
'entity_id' => ['required', 'integer'],
|
||||
'content' => [$request->isMethod('get') ? 'nullable' : 'required', 'string', 'min:1', 'max:10000'],
|
||||
'parent_id' => ['nullable', 'integer'],
|
||||
]);
|
||||
|
||||
if ($payload['entity_type'] === 'story') {
|
||||
if ($request->isMethod('get')) {
|
||||
$story = Story::published()->findOrFail((int) $payload['entity_id']);
|
||||
|
||||
return response()->json(
|
||||
$this->social->listStoryComments($story, $request->user()?->id, (int) $request->query('page', 1), 20)
|
||||
);
|
||||
}
|
||||
|
||||
$story = Story::published()->findOrFail((int) $payload['entity_id']);
|
||||
$comment = $this->social->addStoryComment(
|
||||
$request->user(),
|
||||
$story,
|
||||
(string) $payload['content'],
|
||||
isset($payload['parent_id']) ? (int) $payload['parent_id'] : null,
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->social->formatComment($comment, (int) $request->user()->id, true),
|
||||
], 201);
|
||||
}
|
||||
|
||||
$artworkId = (int) $payload['entity_id'];
|
||||
abort_unless(Artwork::public()->published()->whereKey($artworkId)->exists(), 404);
|
||||
|
||||
if ($request->isMethod('get')) {
|
||||
return app(ArtworkCommentController::class)->index($request, $artworkId);
|
||||
}
|
||||
|
||||
return app(ArtworkCommentController::class)->store(
|
||||
$request->merge([
|
||||
'content' => $payload['content'],
|
||||
'parent_id' => $payload['parent_id'] ?? null,
|
||||
]),
|
||||
$artworkId,
|
||||
);
|
||||
}
|
||||
|
||||
public function bookmark(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validate([
|
||||
'entity_type' => ['required', 'string', 'in:artwork,story'],
|
||||
'entity_id' => ['required', 'integer'],
|
||||
'state' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
$state = array_key_exists('state', $payload)
|
||||
? (bool) $payload['state']
|
||||
: ! $request->isMethod('delete');
|
||||
|
||||
if ($payload['entity_type'] === 'story') {
|
||||
$story = Story::published()->findOrFail((int) $payload['entity_id']);
|
||||
|
||||
$result = $this->social->toggleStoryBookmark($request->user(), $story, $state);
|
||||
|
||||
return response()->json([
|
||||
'ok' => (bool) ($result['ok'] ?? true),
|
||||
'bookmarked' => (bool) ($result['bookmarked'] ?? false),
|
||||
'bookmarks_count' => (int) ($result['bookmarks_count'] ?? 0),
|
||||
'is_bookmarked' => (bool) ($result['bookmarked'] ?? false),
|
||||
'stats' => [
|
||||
'bookmarks' => (int) ($result['bookmarks_count'] ?? 0),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$artworkId = (int) $payload['entity_id'];
|
||||
abort_unless(Artwork::public()->published()->whereKey($artworkId)->exists(), 404);
|
||||
|
||||
return app(ArtworkInteractionController::class)->bookmark(
|
||||
$request->merge(['state' => $state]),
|
||||
$artworkId,
|
||||
);
|
||||
}
|
||||
|
||||
public function bookmarks(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validate([
|
||||
'entity_type' => ['nullable', 'string', 'in:artwork,story'],
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:50'],
|
||||
]);
|
||||
|
||||
$perPage = (int) ($payload['per_page'] ?? 20);
|
||||
$userId = (int) $request->user()->id;
|
||||
$type = $payload['entity_type'] ?? null;
|
||||
|
||||
$items = collect();
|
||||
|
||||
if ($type === null || $type === 'artwork') {
|
||||
$items = $items->concat(
|
||||
Schema::hasTable('artwork_bookmarks')
|
||||
? DB::table('artwork_bookmarks')
|
||||
->join('artworks', 'artworks.id', '=', 'artwork_bookmarks.artwork_id')
|
||||
->where('artwork_bookmarks.user_id', $userId)
|
||||
->where('artworks.is_public', true)
|
||||
->where('artworks.is_approved', true)
|
||||
->select([
|
||||
'artwork_bookmarks.created_at as saved_at',
|
||||
'artworks.id',
|
||||
'artworks.title',
|
||||
'artworks.slug',
|
||||
])
|
||||
->latest('artwork_bookmarks.created_at')
|
||||
->limit($perPage)
|
||||
->get()
|
||||
->map(fn ($row) => [
|
||||
'type' => 'artwork',
|
||||
'id' => (int) $row->id,
|
||||
'title' => (string) $row->title,
|
||||
'url' => route('art.show', ['id' => (int) $row->id, 'slug' => Str::slug((string) ($row->slug ?: $row->title)) ?: (string) $row->id]),
|
||||
'saved_at' => Carbon::parse($row->saved_at)->toIso8601String(),
|
||||
])
|
||||
: collect()
|
||||
);
|
||||
}
|
||||
|
||||
if ($type === null || $type === 'story') {
|
||||
$items = $items->concat(
|
||||
StoryBookmark::query()
|
||||
->with('story:id,slug,title')
|
||||
->where('user_id', $userId)
|
||||
->latest('created_at')
|
||||
->limit($perPage)
|
||||
->get()
|
||||
->filter(fn (StoryBookmark $bookmark) => $bookmark->story !== null)
|
||||
->map(fn (StoryBookmark $bookmark) => [
|
||||
'type' => 'story',
|
||||
'id' => (int) $bookmark->story->id,
|
||||
'title' => (string) $bookmark->story->title,
|
||||
'url' => route('stories.show', ['slug' => $bookmark->story->slug]),
|
||||
'saved_at' => $bookmark->created_at?->toIso8601String(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'data' => $items
|
||||
->sortByDesc('saved_at')
|
||||
->take($perPage)
|
||||
->values()
|
||||
->all(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
58
app/Http/Controllers/Api/StoryCommentController.php
Normal file
58
app/Http/Controllers/Api/StoryCommentController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Story;
|
||||
use App\Models\StoryComment;
|
||||
use App\Services\SocialService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class StoryCommentController extends Controller
|
||||
{
|
||||
public function __construct(private readonly SocialService $social) {}
|
||||
|
||||
public function index(Request $request, int $storyId): JsonResponse
|
||||
{
|
||||
$story = Story::published()->findOrFail($storyId);
|
||||
|
||||
return response()->json(
|
||||
$this->social->listStoryComments($story, $request->user()?->id, (int) $request->query('page', 1), 20)
|
||||
);
|
||||
}
|
||||
|
||||
public function store(Request $request, int $storyId): JsonResponse
|
||||
{
|
||||
$story = Story::published()->findOrFail($storyId);
|
||||
|
||||
$payload = $request->validate([
|
||||
'content' => ['required', 'string', 'min:1', 'max:10000'],
|
||||
'parent_id' => ['nullable', 'integer'],
|
||||
]);
|
||||
|
||||
$comment = $this->social->addStoryComment(
|
||||
$request->user(),
|
||||
$story,
|
||||
(string) $payload['content'],
|
||||
isset($payload['parent_id']) ? (int) $payload['parent_id'] : null,
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->social->formatComment($comment, $request->user()->id, true),
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, int $storyId, int $commentId): JsonResponse
|
||||
{
|
||||
$comment = StoryComment::query()
|
||||
->where('story_id', $storyId)
|
||||
->findOrFail($commentId);
|
||||
|
||||
$this->social->deleteStoryComment($request->user(), $comment);
|
||||
|
||||
return response()->json(['message' => 'Comment deleted.']);
|
||||
}
|
||||
}
|
||||
34
app/Http/Controllers/Api/StoryInteractionController.php
Normal file
34
app/Http/Controllers/Api/StoryInteractionController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Story;
|
||||
use App\Services\SocialService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class StoryInteractionController extends Controller
|
||||
{
|
||||
public function __construct(private readonly SocialService $social) {}
|
||||
|
||||
public function like(Request $request, int $storyId): JsonResponse
|
||||
{
|
||||
$story = Story::published()->findOrFail($storyId);
|
||||
|
||||
return response()->json(
|
||||
$this->social->toggleStoryLike($request->user(), $story, $request->boolean('state', true))
|
||||
);
|
||||
}
|
||||
|
||||
public function bookmark(Request $request, int $storyId): JsonResponse
|
||||
{
|
||||
$story = Story::published()->findOrFail($storyId);
|
||||
|
||||
return response()->json(
|
||||
$this->social->toggleStoryBookmark($request->user(), $story, $request->boolean('state', true))
|
||||
);
|
||||
}
|
||||
}
|
||||
18
app/Http/Controllers/Api/UserAchievementsController.php
Normal file
18
app/Http/Controllers/Api/UserAchievementsController.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\AchievementService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class UserAchievementsController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, AchievementService $achievements): JsonResponse
|
||||
{
|
||||
return response()->json($achievements->summary($request->user()->id));
|
||||
}
|
||||
}
|
||||
18
app/Http/Controllers/Api/UserXpController.php
Normal file
18
app/Http/Controllers/Api/UserXpController.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\XPService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class UserXpController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, XPService $xp): JsonResponse
|
||||
{
|
||||
return response()->json($xp->summary($request->user()->id));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user