diff --git a/.gitignore b/.gitignore index 7ca3d6eb..37560a94 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ npm-debug.log yarn-error.log yarn.lock +pnpm-lock.yml /vendor composer.phar diff --git a/app/Actions/User/UpdateUserProfileAction.php b/app/Actions/User/UpdateUserProfileAction.php new file mode 100644 index 00000000..482de213 --- /dev/null +++ b/app/Actions/User/UpdateUserProfileAction.php @@ -0,0 +1,25 @@ +update($data); + + if ($user->email !== $currentUserEmail) { + $user->email_verified_at = null; + $user->save(); + + event(new EmailAddressWasChanged($user)); + } + + return $user; + } +} diff --git a/app/Console/Commands/UpdateUserSocialAccount.php b/app/Console/Commands/UpdateUserSocialAccount.php new file mode 100644 index 00000000..8a92efdb --- /dev/null +++ b/app/Console/Commands/UpdateUserSocialAccount.php @@ -0,0 +1,35 @@ +info('Start updating users social account...'); + + foreach (User::verifiedUsers()->get() as $user) { + $this->info('Updating '.$user->username.'...'); + $user->update([ + 'twitter_profile' => $this->formatTwitterHandle($user->twitter_profile), + 'github_profile' => $this->formatGithubHandle($user->github_profile), + 'linkedin_profile' => $this->formatLinkedinHandle($user->linkedin_profile), + ]); + $this->line(''); + } + + $this->info('All done!'); + } +} diff --git a/app/Http/Controllers/Cpanel/AnalyticsController.php b/app/Http/Controllers/Cpanel/AnalyticsController.php deleted file mode 100644 index 7a929851..00000000 --- a/app/Http/Controllers/Cpanel/AnalyticsController.php +++ /dev/null @@ -1,16 +0,0 @@ -addHour(), fn () => User::verifiedUsers()->latest()->limit(15)->get()); - $latestArticles = Cache::remember('last-posts', now()->addHour(), fn () => Article::latest()->limit(2)->get()); - - return view('cpanel.dashboard', [ - 'latestArticles' => $latestArticles, - 'users' => $users, - ]); - } -} diff --git a/app/Http/Controllers/Cpanel/UserController.php b/app/Http/Controllers/Cpanel/UserController.php deleted file mode 100644 index 1775a601..00000000 --- a/app/Http/Controllers/Cpanel/UserController.php +++ /dev/null @@ -1,19 +0,0 @@ - User::verifiedUsers()->latest()->paginate(15), - ]); - } -} diff --git a/app/Http/Controllers/NotchPayCallBackController.php b/app/Http/Controllers/NotchPayCallBackController.php index 4d35bdb3..0e552047 100644 --- a/app/Http/Controllers/NotchPayCallBackController.php +++ b/app/Http/Controllers/NotchPayCallBackController.php @@ -11,6 +11,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; +use NotchPay\Exceptions\ApiException; use NotchPay\NotchPay; use NotchPay\Payment; @@ -40,6 +41,7 @@ public function __invoke(Request $request): RedirectResponse } else { // @ToDO Envoie de mail de notification de remerciement pour le sponsoring si l'utilisateur est dans la base de données event(new SponsoringPaymentInitialize($transaction)); + Cache::forget(key: 'sponsors'); session()->flash( @@ -48,7 +50,7 @@ public function __invoke(Request $request): RedirectResponse ); } - } catch (\NotchPay\Exceptions\ApiException $e) { + } catch (ApiException $e) { Log::error($e->getMessage()); session()->flash( key: 'error', diff --git a/app/Http/Controllers/ReplyAbleController.php b/app/Http/Controllers/ReplyAbleController.php index 073d8572..c797bea1 100644 --- a/app/Http/Controllers/ReplyAbleController.php +++ b/app/Http/Controllers/ReplyAbleController.php @@ -9,9 +9,12 @@ final class ReplyAbleController extends Controller { - public function redirect(int $id, string $type): RedirectResponse + public function __invoke(int $id, string $type): RedirectResponse { - $reply = Reply::where('replyable_id', $id)->where('replyable_type', $type)->firstOrFail(); + $reply = Reply::query() + ->where('replyable_id', $id) + ->where('replyable_type', $type) + ->firstOrFail(); return redirect(route_to_reply_able($reply->replyAble)); } diff --git a/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php index 794a0f52..102a1c16 100644 --- a/app/Http/Controllers/SubscriptionController.php +++ b/app/Http/Controllers/SubscriptionController.php @@ -14,7 +14,7 @@ public function unsubscribe(Subscribe $subscription): RedirectResponse /** @var \App\Models\Thread $thread */ $thread = $subscription->subscribeAble; - $thread->subscribes()->where('user_id', $subscription->user->id)->delete(); // @phpstan-ignore-line + $thread->subscribes()->where('user_id', $subscription->user->id)->delete(); session()->flash('status', __('Vous êtes maintenant désabonné de ce sujet.')); @@ -23,7 +23,10 @@ public function unsubscribe(Subscribe $subscription): RedirectResponse public function redirect(int $id, string $type): RedirectResponse { - $subscribe = Subscribe::where('subscribeable_id', $id)->where('subscribeable_type', $type)->firstOrFail(); + $subscribe = Subscribe::query() + ->where('subscribeable_id', $id) + ->where('subscribeable_type', $type) + ->firstOrFail(); return redirect(route_to_reply_able($subscribe->subscribeAble)); } diff --git a/app/Http/Controllers/User/ProfileController.php b/app/Http/Controllers/User/ProfileController.php deleted file mode 100644 index 42fbb3ef..00000000 --- a/app/Http/Controllers/User/ProfileController.php +++ /dev/null @@ -1,53 +0,0 @@ -whereBelongsTo($user) - ->published() - ->recent() - ->limit(5) - ->get(); - - $threads = Thread::whereBelongsTo($user) - ->orderByDesc('created_at') - ->limit(5) - ->get(); - - $discussions = Discussion::with('tags') - ->whereBelongsTo($user) - ->limit(5) - ->get(); - - return view('user.profile', [ - 'user' => $user, - 'articles' => $articles, - 'threads' => $threads, - 'discussions' => $discussions, - 'activities' => [], - ]); - } - - if ($request->user()) { - return redirect()->route('profile', $request->user()->username); - } - - abort(404); - } -} diff --git a/app/Http/Controllers/User/SettingController.php b/app/Http/Controllers/User/SettingController.php deleted file mode 100644 index 5381663a..00000000 --- a/app/Http/Controllers/User/SettingController.php +++ /dev/null @@ -1,94 +0,0 @@ -email; - - $user->update([ - 'name' => $request->name, - 'email' => $request->email, - 'username' => mb_strtolower($request->username), - 'bio' => trim(strip_tags((string) $request->bio)), - 'twitter_profile' => $request->twitter_profile, - 'github_profile' => $request->github_profile, - 'linkedin_profile' => $request->linkedin_profile, - 'phone_number' => $request->phone_number, - 'location' => $request->location, - 'website' => $request->website, - ]); - - if ($request->email !== $emailAddress) { - $user->email_verified_at = null; - $user->save(); - - event(new EmailAddressWasChanged($user)); - } - - session()->flash('status', __('Paramètres enregistrés avec succès! Si vous avez changé votre adresse e-mail, vous recevrez une adresse e-mail pour la reconfirmer.')); - - return redirect()->route('user.settings'); - } - - public function password(): View - { - return view('user.settings.password', [ - 'sessions' => Cache::remember('login-sessions', now()->addDays(5), function () { - return DB::table('sessions') - ->where('user_id', auth()->id()) - ->orderBy('last_activity', 'desc') - ->limit(3) - ->get() - ->map(fn ($session) => (object) [ - 'agent' => $this->createAgent($session), - 'ip_address' => $session->ip_address, - 'is_current_device' => $session->id === request()->session()->getId(), - 'last_active' => Carbon::createFromTimestamp($session->last_activity)->diffForHumans(), - 'location' => Location::get($session->ip_address), - ]); - }), - ]); - } - - public function updatePassword(UpdatePasswordRequest $request): RedirectResponse - { - Auth::user()->update(['password' => Hash::make($request->password)]); // @phpstan-ignore-line - - session()->flash('status', __('Votre mot de passe a été changé avec succès.')); - - return redirect()->back(); - } - - protected function createAgent(mixed $session): mixed - { - return tap(new Agent, function ($agent) use ($session): void { - $agent->setUserAgent($session->user_agent); - }); - } -} diff --git a/app/Http/Requests/UpdateProfileRequest.php b/app/Http/Requests/UpdateProfileRequest.php index 3e0dd640..7872e691 100644 --- a/app/Http/Requests/UpdateProfileRequest.php +++ b/app/Http/Requests/UpdateProfileRequest.php @@ -19,7 +19,7 @@ public function rules(): array return [ 'name' => 'required|max:255', 'email' => 'required|email|max:255|unique:users,email,'.Auth::id(), - 'username' => 'required|alpha_dash|max:255|unique:users,username,'.Auth::id(), + 'username' => 'required|alpha_dash|max:30|unique:users,username,'.Auth::id(), 'twitter_profile' => 'max:255|nullable|unique:users,twitter_profile,'.Auth::id(), 'github_profile' => 'max:255|nullable|unique:users,github_profile,'.Auth::id(), 'bio' => 'nullable|max:160', diff --git a/app/Livewire/Components/User/Activities.php b/app/Livewire/Components/User/Activities.php new file mode 100644 index 00000000..ba82654f --- /dev/null +++ b/app/Livewire/Components/User/Activities.php @@ -0,0 +1,22 @@ + Activity::latestFeed($this->user), + ]); + } +} diff --git a/app/Livewire/User/Settings/Notifications.php b/app/Livewire/Components/User/Notifications.php similarity index 50% rename from app/Livewire/User/Settings/Notifications.php rename to app/Livewire/Components/User/Notifications.php index 406251f2..cce0149e 100644 --- a/app/Livewire/User/Settings/Notifications.php +++ b/app/Livewire/Components/User/Notifications.php @@ -2,44 +2,55 @@ declare(strict_types=1); -namespace App\Livewire\User\Settings; +namespace App\Livewire\Components\User; use App\Models\Subscribe; use Filament\Notifications\Notification; use Illuminate\Contracts\View\View; -use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Computed; use Livewire\Component; +/** + * @property Subscribe $subscribe + */ final class Notifications extends Component { - use AuthorizesRequests; - - public string $subscribeId; + public ?string $subscribeId = null; public function unsubscribe(string $subscribeId): void { $this->subscribeId = $subscribeId; - // @phpstan-ignore-next-line $this->subscribe->delete(); Notification::make() ->title(__('Désabonnement')) ->body(__('Vous êtes maintenant désabonné de cet fil.')) ->success() - ->duration(5000) + ->duration(3500) ->send(); } - public function getSubscribeProperty(): Subscribe + #[Computed] + public function subscribe(): Subscribe { - return Subscribe::where('uuid', $this->subscribeId)->firstOrFail(); + return Subscribe::query()->where('uuid', $this->subscribeId)->firstOrFail(); + } + + public function redirectToSubscription(int $id, string $type): void + { + $subscribe = Subscribe::query() + ->where('subscribeable_id', $id) + ->where('subscribeable_type', $type) + ->firstOrFail(); + + $this->redirect(route_to_reply_able($subscribe->subscribeAble), navigate: true); } public function render(): View { - return view('livewire.user.settings.notifications', [ + return view('livewire.components.user.notifications', [ 'subscriptions' => Auth::user()->subscriptions, // @phpstan-ignore-line ]); } diff --git a/app/Livewire/Components/User/Password.php b/app/Livewire/Components/User/Password.php new file mode 100644 index 00000000..b6b19a24 --- /dev/null +++ b/app/Livewire/Components/User/Password.php @@ -0,0 +1,89 @@ +form->fill(); + } + + public function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\TextInput::make('current_password') + ->label(__('validation.attributes.current_password')) + ->password() + ->currentPassword() + ->required() + ->visible(fn () => Auth::user()?->hasPassword()), + Forms\Components\TextInput::make('password') + ->label(__('validation.attributes.password')) + ->helperText(__('pages/account.settings.password_helpText')) + ->password() + ->revealable() + ->required() + ->rules(fn () => [ + RulesPassword::min(8) + ->mixedCase() + ->symbols() + ->letters() + ->numbers() + ->uncompromised(), + ]) + ->confirmed(), + Forms\Components\TextInput::make('password_confirmation') + ->label(__('validation.attributes.password_confirmation')) + ->password() + ->revealable() + ->required(), + ]) + ->statePath('data') + ->model(Auth::user()); + } + + public function changePassword(): void + { + $this->validate(); + + // @phpstan-ignore-next-line + Auth::user()->update([ + 'password' => Hash::make( + value: data_get($this->form->getState(), 'password') + ), + ]); + + Notification::make() + ->success() + ->title(__('notifications.user.password_changed')) + ->duration(3500) + ->send(); + } + + public function render(): View + { + return view('livewire.components.user.password'); + } +} diff --git a/app/Livewire/Components/User/Preferences.php b/app/Livewire/Components/User/Preferences.php new file mode 100644 index 00000000..7bb70c5e --- /dev/null +++ b/app/Livewire/Components/User/Preferences.php @@ -0,0 +1,42 @@ +theme = get_current_theme(); + } + + public function updatedTheme(string $value): void + { + $this->user->settings(['theme' => $value]); + + $this->redirectRoute('settings', navigate: true); + } + + public function render(): View + { + return view('livewire.components.user.preferences'); + } +} diff --git a/app/Livewire/Components/User/Profile.php b/app/Livewire/Components/User/Profile.php new file mode 100644 index 00000000..847348df --- /dev/null +++ b/app/Livewire/Components/User/Profile.php @@ -0,0 +1,182 @@ +form->fill($this->user->toArray()); + + $this->currentUserEmail = $this->user->email; + } + + public function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\Section::make(__('pages/account.settings.profile_title')) + ->description(__('pages/account.settings.profile_description')) + ->aside() + ->schema([ + Forms\Components\SpatieMediaLibraryFileUpload::make('avatar') + ->label(__('validation.attributes.avatar')) + ->collection('avatar') + ->helperText(__('pages/account.settings.avatar_description')) + ->image() + ->avatar() + ->maxSize(1024), + Forms\Components\TextInput::make('username') + ->label(__('validation.attributes.username')) + ->prefix('laravel.cm/user/@') + ->required() + ->unique(ignoreRecord: true) + ->maxLength(30) + ->rules(['lowercase', 'alpha_dash']), + Forms\Components\Textarea::make('bio') + ->label(__('validation.attributes.bio')) + ->hint(__('global.characters', ['number' => 160])) + ->maxLength(160) + ->afterStateUpdated(fn (?string $state) => trim(strip_tags((string) $state))) + ->helperText(__('pages/account.settings.bio_description')), + Forms\Components\TextInput::make('website') + ->label(__('validation.attributes.website')) + ->prefixIcon('untitledui-globe') + ->placeholder('https://laravel.cm') + ->url(), + ]), + + Forms\Components\Section::make(__('pages/account.settings.personal_information_title')) + ->description(__('pages/account.settings.personal_information_description')) + ->aside() + ->schema([ + Forms\Components\TextInput::make('name') + ->label(__('validation.attributes.last_name')) + ->required(), + Forms\Components\TextInput::make('email') + ->label(__('validation.attributes.email')) + ->suffixIcon(fn () => $this->user->hasVerifiedEmail() ? 'heroicon-m-check-circle' : 'heroicon-m-exclamation-triangle') + ->suffixIconColor(fn () => $this->user->hasVerifiedEmail() ? 'success' : 'warning') + ->HelperText(fn () => ! $this->user->hasVerifiedEmail() ? __('pages/account.settings.unverified_mail') : null) + ->email() + ->unique(ignoreRecord: true) + ->required(), + Forms\Components\TextInput::make('location') + ->label(__('validation.attributes.location')), + PhoneInput::make('phone_number') + ->label(__('validation.attributes.phone')), + ]), + + Forms\Components\Section::make(__('pages/account.settings.social_network_title')) + ->description(__('pages/account.settings.social_network_description')) + ->aside() + ->schema([ + Forms\Components\TextInput::make('github_profile') + ->label(__('GitHub')) + ->placeholder('laravelcm') + ->unique(ignoreRecord: true) + ->maxLength(255) + ->afterStateUpdated(fn (Forms\Set $set, ?string $state) => $set('github_profile', $this->formatGithubHandle($state))) + ->prefix( + fn (): HtmlString => new HtmlString(Blade::render(<<<'Blade' +
+ Blade)) + ), + Forms\Components\TextInput::make('twitter_profile') + ->label(__('Twitter')) + ->helperText(__('pages/account.settings.twitter_helper_text')) + ->unique(ignoreRecord: true) + ->maxLength(255) + ->afterStateUpdated(fn (Forms\Set $set, ?string $state) => $set('twitter_profile', $this->formatTwitterHandle($state))) + ->prefix( + fn (): HtmlString => new HtmlString(Blade::render(<<<'Blade' + + Blade)) + ), + Forms\Components\TextInput::make('linkedin_profile') + ->label(__('LinkedIn')) + ->placeholder('laravelcm') + ->unique(ignoreRecord: true) + ->maxLength(255) + ->afterStateUpdated(fn (Forms\Set $set, ?string $state) => $set('linkedin_profile', $this->formatLinkedinHandle($state))) + ->prefix( + fn (): HtmlString => new HtmlString(Blade::render(<<<'Blade' + + Blade)) + ) + ->helperText(fn (): HtmlString => new HtmlString(Blade::render( + <<<'Blade' +
+
- {!! $article->excerpt(100) !!} -
- - {{ __('Lire l\'article') }} -+
{!! $discussion->excerpt(175) !!}
diff --git a/resources/views/components/discussions/summary.blade.php b/resources/views/components/discussions/summary.blade.php index 0a066ea2..ac4ebc13 100644 --- a/resources/views/components/discussions/summary.blade.php +++ b/resources/views/components/discussions/summary.blade.php @@ -25,7 +25,7 @@ {!! $discussion->excerpt(175) !!}a commenté il y a 2 jours
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tincidunt nunc ipsum tempor purus vitae - id. Morbi in vestibulum nec varius... -
+ {{ $user->name }} +a commenté il y a 2 jours
+...
- {{ __('a créé l\'article') }} - - {{ $activity->subject->title }} - -
-- {{ __('a démarré une conversation') }} - - {{ $activity->subject->title }} - -
-- {{ __('a répondu au sujet') }} - subject->replyAble->slug()}#reply-{$activity->subject->id}") }}" - class="font-medium text-primary-600 hover:text-primary-600-hover" - > - {{ $activity->subject->replyAble->title }} - -
-{{ $activity->subject->excerpt() }}
-- a lancé le sujet - - {{ $activity->subject->title }} - -
-+ {{ $slot }} +
+- {{ __('Aucune activité pour le moment.') }} -
-{{ __('PNG, JPG, GIF up to 1MB') }}
-