follow
This commit is contained in:
parent
548100af74
commit
1db50bc69a
|
|
@ -110,7 +110,6 @@ class ExpoPushChannel
|
|||
'tokens_count' => $tokens->count(),
|
||||
]);
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum FollowStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Accepted = 'accepted';
|
||||
case Rejected = 'rejected';
|
||||
case Blocked = 'blocked';
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ namespace App\Filament\Resources\CreditProducts\Schemas;
|
|||
|
||||
use App\Enums\CreditProductType;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
|
|
|
|||
|
|
@ -30,8 +30,7 @@ class BillingController extends Controller
|
|||
Request $request,
|
||||
CreditProduct $creditProduct,
|
||||
PaymentTransactionRecorder $transactions,
|
||||
): JsonResponse
|
||||
{
|
||||
): JsonResponse {
|
||||
abort_unless($creditProduct->is_active, 404);
|
||||
abort_unless(filled($creditProduct->stripe_price_id), 404);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\FollowStatus;
|
||||
use App\Http\Requests\FollowRequest;
|
||||
use App\Http\Resources\FollowResource;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
|
||||
class FollowController extends Controller
|
||||
{
|
||||
public function follow(FollowRequest $request, User $user): JsonResponse
|
||||
{
|
||||
$authUser = $request->user();
|
||||
|
||||
if ($authUser->id === $user->id) {
|
||||
return response()->json([
|
||||
'message' => __('api.follows.cannot_follow_self')
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Default to Accepted as is_private doesn't exist in the DB yet
|
||||
$status = FollowStatus::Accepted;
|
||||
|
||||
$authUser->following()->syncWithPivotValues([$user->id], ['status' => $status], false);
|
||||
|
||||
return response()->json([
|
||||
'message' => __('api.follows.followed'),
|
||||
'status' => $status,
|
||||
]);
|
||||
}
|
||||
|
||||
public function unfollow(FollowRequest $request, User $user): JsonResponse
|
||||
{
|
||||
$request->user()->following()->detach($user->id);
|
||||
|
||||
return response()->json([
|
||||
'message' => __('api.follows.unfollowed'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function accept(FollowRequest $request, User $user): JsonResponse
|
||||
{
|
||||
$updated = $request->user()->followers()->updateExistingPivot($user->id, [
|
||||
'status' => FollowStatus::Accepted,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => $updated ? __('api.follows.accepted') : __('api.follows.not_found'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function reject(FollowRequest $request, User $user): JsonResponse
|
||||
{
|
||||
$detached = $request->user()->followers()->detach($user->id);
|
||||
|
||||
return response()->json([
|
||||
'message' => $detached ? __('api.follows.rejected') : __('api.follows.not_found'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function followers(Request $request, User $user): AnonymousResourceCollection
|
||||
{
|
||||
$perPage = max(1, min($request->integer('per_page', 15), 100));
|
||||
|
||||
$followers = $user->followers()
|
||||
->wherePivot('status', FollowStatus::Accepted)
|
||||
->paginate($perPage);
|
||||
|
||||
return FollowResource::collection($followers);
|
||||
}
|
||||
|
||||
public function following(Request $request, User $user): AnonymousResourceCollection
|
||||
{
|
||||
$perPage = max(1, min($request->integer('per_page', 15), 100));
|
||||
|
||||
$following = $user->following()
|
||||
->wherePivot('status', FollowStatus::Accepted)
|
||||
->paginate($perPage);
|
||||
|
||||
return FollowResource::collection($following);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,8 +22,7 @@ class MealImageAnalysisController extends Controller
|
|||
MealImageAnalysisRequest $request,
|
||||
AiCreditService $aiCredits,
|
||||
AiUsageRecorder $aiUsageRecorder,
|
||||
): JsonResponse
|
||||
{
|
||||
): JsonResponse {
|
||||
$user = $request->user();
|
||||
$image = $request->file('image');
|
||||
$input = $this->analysisInput($image);
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ class MealPostController extends Controller
|
|||
$data = $request->validated();
|
||||
|
||||
if ($request->hasFile('image')) {
|
||||
$path = $request->file('image')->store('meal-posts', );
|
||||
$path = $request->file('image')->store('meal-posts');
|
||||
$data['image_url'] = $path;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\WorkoutSessionsRequest;
|
||||
use App\Http\Requests\WorkoutSessionsCalendarRequest;
|
||||
use App\Http\Requests\WorkoutSessionsRequest;
|
||||
use App\Http\Resources\WorkoutSessionsResource;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class FollowRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// No specific body rules for now as we use route model binding
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class FollowResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'avatarUrl' => $this->avatar_url ? asset(Storage::url($this->avatar_url)) : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ namespace App\Listeners;
|
|||
|
||||
use App\Enums\CreditLedgerEntryType;
|
||||
use App\Enums\CreditProductType;
|
||||
use App\Models\CreditProduct;
|
||||
use App\Models\CreditLedgerEntry;
|
||||
use App\Models\CreditProduct;
|
||||
use App\Models\User;
|
||||
use App\Services\AiCreditService;
|
||||
use App\Services\PaymentInvoiceEmailer;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\FollowStatus;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Follow extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'following_id',
|
||||
'follower_id',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => FollowStatus::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function follower(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'follower_id');
|
||||
}
|
||||
|
||||
public function following(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'following_id');
|
||||
}
|
||||
}
|
||||
|
|
@ -149,6 +149,20 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale
|
|||
return $this->morphMany(Report::class, 'reportable');
|
||||
}
|
||||
|
||||
public function followers(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(self::class, 'follows', 'following_id', 'follower_id')
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function following(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(self::class, 'follows', 'follower_id', 'following_id')
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function moderationCase(): MorphOne
|
||||
{
|
||||
return $this->morphOne(ModerationCase::class, 'caseable');
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ use Illuminate\Auth\Notifications\ResetPassword;
|
|||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
<?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::create('follows', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignUlid('follower_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignUlid('following_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('status')->default('accepted');
|
||||
$table->timestampsTz();
|
||||
$table->unique([
|
||||
'follower_id',
|
||||
'following_id',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('follows');
|
||||
}
|
||||
};
|
||||
|
|
@ -90,6 +90,14 @@ return [
|
|||
'missing_activity_scope' => 'The Strava connection must allow the activity:read or activity:read_all scope.',
|
||||
'disconnect_failed' => 'Unable to revoke Strava access right now. Please try again in a moment.',
|
||||
],
|
||||
'follows' => [
|
||||
'cannot_follow_self' => 'You cannot follow yourself.',
|
||||
'followed' => 'Follow request recorded.',
|
||||
'unfollowed' => 'Unfollowed successfully.',
|
||||
'accepted' => 'Follow request accepted.',
|
||||
'rejected' => 'Follow request rejected.',
|
||||
'not_found' => 'No request found.',
|
||||
],
|
||||
'validation' => [
|
||||
'image_required' => 'Add an image to analyze.',
|
||||
'image_file' => 'The file must be an image.',
|
||||
|
|
|
|||
|
|
@ -90,6 +90,14 @@ return [
|
|||
'missing_activity_scope' => 'La connexion Strava doit autoriser le scope activity:read ou activity:read_all.',
|
||||
'disconnect_failed' => "Impossible de retirer l'accès Strava pour le moment. Réessaie dans quelques instants.",
|
||||
],
|
||||
'follows' => [
|
||||
'cannot_follow_self' => 'Vous ne pouvez pas vous suivre vous-même.',
|
||||
'followed' => 'Abonnement enregistré.',
|
||||
'unfollowed' => 'Désabonnement effectué.',
|
||||
'accepted' => 'Demande d’abonnement acceptée.',
|
||||
'rejected' => 'Demande d’abonnement refusée.',
|
||||
'not_found' => 'Aucune demande trouvée.',
|
||||
],
|
||||
'validation' => [
|
||||
'image_required' => 'Ajoute une image à analyser.',
|
||||
'image_file' => 'Le fichier doit être une image.',
|
||||
|
|
|
|||
|
|
@ -34,4 +34,3 @@ it('returns only the authenticated user meal posts', function () {
|
|||
'id' => $otherMealPost->id,
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue