diff --git a/.dockerignore b/.dockerignore index 2d5e6308..04dda16a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,8 +4,9 @@ node_modules # Laravel .mcp.json .env.example +.env.testing phpstan.neon -phpunit.xml +phpunit.*.xml rector.php pint.json tests diff --git a/.env.example b/.env.example index 85fdb66c..f1fcbfaa 100644 --- a/.env.example +++ b/.env.example @@ -2,16 +2,29 @@ APP_NAME="LaravelCameroun" APP_ENV=local APP_KEY= APP_DEBUG=true +APP_DOMAIN=laravelcm.local APP_URL=http://laravelcm.local +ASSET_URL=https://laravelcm.local APP_LOCALE=fr +APP_FALLBACK_LOCALE=fr APP_PORT=8080 APP_SERVICE=laravelcm -FILAMENT_PATH=cp LOG_CHANNEL=stack LOG_STACK=single,nightwatch LOG_LEVEL=debug +RAY_HOST=ray@buggregator +RAY_PORT=8000 +SENTRY_LARAVEL_DSN=http://sentry@buggregator:8000/1 +SENTRY_TRACES_SAMPLE_RATE=1.0 +VAR_DUMPER_FORMAT=server +VAR_DUMPER_SERVER=tcp://buggregator:9912 +INSPECTOR_URL=http://inspector@buggregator:8000 +INSPECTOR_API_KEY=test +INSPECTOR_INGESTION_KEY=1test +INSPECTOR_ENABLE=true + DB_CONNECTION=pgsql DB_HOST=pgsql DB_PORT=5432 @@ -20,10 +33,16 @@ DB_USERNAME=sail DB_PASSWORD=password BROADCAST_DRIVER=log -CACHE_DRIVER=file +MEDIA_DISK=media +FILESYSTEM_DISK=${MEDIA_DISK} +FILAMENT_FILESYSTEM_DISK=${MEDIA_DISK} +FILAMENT_PATH=cpanel + QUEUE_CONNECTION=database +BROADCAST_CONNECTION=log +CACHE_DRIVER=file SESSION_DRIVER=database -SESSION_LIFETIME=120 +SESSION_LIFETIME=1400 MEMCACHED_HOST=127.0.0.1 @@ -41,10 +60,13 @@ MAIL_FROM_ADDRESS=no-reply@laravel.cm MAIL_FROM_NAME="${APP_NAME}" MAIL_SUPPORT=mail-support@laravel.cm -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= +AWS_ACCESS_KEY_ID=sail +AWS_SECRET_ACCESS_KEY=password AWS_DEFAULT_REGION=us-east-1 -AWS_BUCKET= +AWS_BUCKET=laravelcm +AWS_ENDPOINT="http://minio:9000" +AWS_URL="http://localhost:9000/laravelcm" +AWS_USE_PATH_STYLE_ENDPOINT=true PUSHER_APP_ID= PUSHER_APP_KEY= @@ -57,29 +79,20 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= GITHUB_REDIRECT=${APP_URL}/auth/github/callback - +GITHUB_FINE_GRAINED_TOKEN= MARKDOWNX_GIPHY_API_KEY= TORCHLIGHT_TOKEN= TORCHLIGHT_THEME=moonlight-ii -UNSPLASH_ACCESS_KEY= - TELEGRAM_BOT_TOKEN= TELEGRAM_CHANNEL= - -MEDIA_DISK=media -FILAMENT_FILESYSTEM_DISK=${MEDIA_DISK} - -NOTCHPAY_PUBLIC_KEY= - TWITTER_CONSUMER_KEY=your-consumer-key TWITTER_CONSUMER_SECRET=your-consumer-secret TWITTER_ACCESS_TOKEN=your-accesss_token TWITTER_ACCESS_SECRET=your-access-token-secret - -GOOGLE_RECAPTCHA_SITE_KEY=your-recaptcha-site-key -GOOGLE_RECAPTCHA_SECRET_KEY=your-secret-key - -GITHUB_FINE_GRAINED_TOKEN= +UNSPLASH_ACCESS_KEY= +NOTCHPAY_PUBLIC_KEY= +NIGHTWATCH_TOKEN= +NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1 SCOUT_DRIVER=typesense TYPESENSE_HOST=typesense @@ -87,9 +100,6 @@ TYPESENSE_PORT=8108 TYPESENSE_PROTOCOL=http TYPESENSE_API_KEY=xyz -NIGHTWATCH_TOKEN= -NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1 - # SSH Tunnel Configuration for Database Migration SSH_TUNNEL_USER= SSH_TUNNEL_HOSTNAME= diff --git a/.env.testing b/.env.testing new file mode 100644 index 00000000..79ffb865 --- /dev/null +++ b/.env.testing @@ -0,0 +1,42 @@ +APP_NAME="LaravelCameroun" +APP_ENV=testing +APP_KEY=base64:NXoQgjw2ZlOxnGbo5ZRhYgTdM6xLYsgYElNAgcTQJkE= +APP_DEBUG=false +APP_DOMAIN=laravelcm.test +APP_URL=http://laravelcm.test +ASSET_URL=http://laravelcm.test +APP_LOCALE=fr +APP_FALLBACK_LOCALE=fr + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_LEVEL=error + +# Base de données PostgreSQL pour les tests +DB_CONNECTION=pgsql +DB_HOST=pgsql +DB_PORT=5432 +DB_DATABASE=testing +DB_USERNAME=sail +DB_PASSWORD=password + +# Cache et sessions pour les tests +CACHE_DRIVER=array +SESSION_DRIVER=array +QUEUE_CONNECTION=sync + +# Mail en mode array pour les tests +MAIL_MAILER=array + +# Désactiver les services externes en mode test +TELESCOPE_ENABLED=false +RAY_ENABLED=false +SENTRY_LARAVEL_DSN= +INSPECTOR_ENABLE=false + +# Optimisations pour les tests +BCRYPT_ROUNDS=4 + +# Répertoires pour les tests (éviter les problèmes de permissions) +VIEW_COMPILED_PATH=/tmp/views +LOG_CHANNEL=null \ No newline at end of file diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 65491c85..68295b4c 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -8,7 +8,7 @@ runs: uses: shivammathur/setup-php@v2 with: php-version: "8.4" - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, bcmath, soap, intl, gd, exif, iconv, imagick + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, soap, intl, gd, exif, iconv, imagick tools: composer:v2 coverage: none - name: ℹ Setup Problem Matches diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 48fd65ee..c1c5a549 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,6 +15,21 @@ jobs: pest: runs-on: ubuntu-22.04 + services: + postgres: + image: postgres:17-alpine + env: + POSTGRES_PASSWORD: password + POSTGRES_USER: sail + POSTGRES_DB: testing + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - name: 👀 Checkout uses: actions/checkout@v5 @@ -27,4 +42,4 @@ jobs: - name: 🧱 Build JS Dependencies run: yarn build - name: 🕵️‍♂️ Run Pest Tests - run: ./vendor/bin/pest + run: ./vendor/bin/pest --configuration=phpunit.ci.xml --parallel --processes=4 --bail diff --git a/Dockerfile b/Dockerfile index a31d1aaf..6b7eefd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,10 +8,6 @@ FROM ghcr.io/yieldstudio/php:${PHP_VERSION}-frankenphp AS base ENV HEALTHCHECK_PATH="/up" -## Uncomment if you need to install additional PHP extensions -# USER root -# RUN install-php-extensions bcmath gd - ############################################ # Development Image ############################################ diff --git a/app/Console/Commands/AssignUserRole.php b/app/Console/Commands/AssignUserRole.php index 045333af..f2fedef7 100644 --- a/app/Console/Commands/AssignUserRole.php +++ b/app/Console/Commands/AssignUserRole.php @@ -17,7 +17,7 @@ public function handle(): void { $this->info('Assigning user role to all users...'); - foreach (User::withoutRole()->get() as $user) { + foreach (User::query()->scopes('withoutRole')->get() as $user) { $user->assignRole('user'); } diff --git a/app/Console/Commands/PublishArticles.php b/app/Console/Commands/PublishArticles.php index e46e2401..fcaf1663 100644 --- a/app/Console/Commands/PublishArticles.php +++ b/app/Console/Commands/PublishArticles.php @@ -21,8 +21,9 @@ public function handle(): void foreach ($articles as $article) { /** @var Article $article */ - $article->published_at = $article->submitted_at; - $article->save(); + $article->update([ + 'published_at' => $article->submitted_at, + ]); } $count = $articles->count(); diff --git a/app/Console/Commands/SendUnVerifiedMails.php b/app/Console/Commands/SendUnVerifiedMails.php index 35e1151f..db06a711 100644 --- a/app/Console/Commands/SendUnVerifiedMails.php +++ b/app/Console/Commands/SendUnVerifiedMails.php @@ -17,7 +17,7 @@ final class SendUnVerifiedMails extends Command public function handle(): void { - foreach (User::unVerifiedUsers()->get() as $user) { + foreach (User::query()->scopes('unVerifiedUsers')->get() as $user) { Mail::to($user)->send(new SendMailToUnVerifiedUsers($user)); } } diff --git a/app/Contracts/HasCachedMediaInterface.php b/app/Contracts/HasCachedMediaInterface.php new file mode 100644 index 00000000..44906c2e --- /dev/null +++ b/app/Contracts/HasCachedMediaInterface.php @@ -0,0 +1,16 @@ + $channels */ final class ChannelsSelector extends Component { @@ -19,7 +20,7 @@ final class ChannelsSelector extends Component public function selectedChannel(int $channelId): void { - $this->slug = Channel::query()->find($channelId)?->slug; + $this->slug = $this->channels->firstWhere('id', $channelId)?->slug; $this->dispatch('channelUpdated', channelId: $channelId); } @@ -34,17 +35,17 @@ public function resetChannel(): void #[Computed] public function currentChannel(): ?Channel { - return filled($this->slug) ? Channel::findBySlug($this->slug) : null; + return $this->channels->firstWhere('slug', $this->slug); + } + + #[Computed(persist: true, seconds: 3600 * 24 * 30, cache: true)] + public function channels(): Collection + { + return Channel::with('items')->whereNull('parent_id')->get(); } public function render(): View { - return view('livewire.components.channels-selector', [ - 'channels' => Cache::remember( - 'channels', - now()->addMonth(), - fn () => Channel::with('items')->whereNull('parent_id')->get() - ), - ]); + return view('livewire.components.channels-selector'); } } diff --git a/app/Livewire/Components/Slideovers/ArticleForm.php b/app/Livewire/Components/Slideovers/ArticleForm.php index 0958f535..1acd983a 100644 --- a/app/Livewire/Components/Slideovers/ArticleForm.php +++ b/app/Livewire/Components/Slideovers/ArticleForm.php @@ -25,7 +25,7 @@ use Laravelcm\LivewireSlideOvers\SlideOverComponent; /** - * @property Form $form + * @property-read Form $form */ final class ArticleForm extends SlideOverComponent implements HasForms { @@ -40,7 +40,7 @@ public function mount(?int $articleId = null): void { // @phpstan-ignore-next-line $this->article = filled($articleId) - ? Article::query()->findOrFail($articleId) + ? Article::with('tags')->findOrFail($articleId) : new Article; $this->form->fill(array_merge($this->article->toArray(), [ @@ -55,11 +55,6 @@ public static function panelMaxWidth(): string return '6xl'; } - public static function closePanelOnEscape(): bool - { - return false; - } - public static function closePanelOnClickAway(): bool { return false; diff --git a/app/Livewire/Components/Slideovers/DiscussionForm.php b/app/Livewire/Components/Slideovers/DiscussionForm.php index 906ed765..1ed49ff9 100644 --- a/app/Livewire/Components/Slideovers/DiscussionForm.php +++ b/app/Livewire/Components/Slideovers/DiscussionForm.php @@ -65,7 +65,7 @@ public function form(Form $form): Form ->relationship( name: 'tags', titleAttribute: 'name', - modifyQueryUsing: fn ($query) => $query->whereJsonContains('concerns', 'discussion') + modifyQueryUsing: fn ($query) => $query->whereRaw("jsonb_exists(concerns::jsonb, ?)", ['discussion']) ) ->required() ->minItems(1) @@ -136,11 +136,6 @@ public static function panelMaxWidth(): string return '2xl'; } - public static function closePanelOnEscape(): bool - { - return false; - } - public static function closePanelOnClickAway(): bool { return false; diff --git a/app/Livewire/Components/Slideovers/ThreadForm.php b/app/Livewire/Components/Slideovers/ThreadForm.php index 9bce6af5..3fae788d 100644 --- a/app/Livewire/Components/Slideovers/ThreadForm.php +++ b/app/Livewire/Components/Slideovers/ThreadForm.php @@ -36,7 +36,7 @@ final class ThreadForm extends SlideOverComponent implements HasForms public function mount(?int $threadId = null): void { $this->thread = filled($threadId) - ? Thread::query()->findOrFail($threadId) + ? Thread::with('channels')->findOrFail($threadId) : new Thread; $this->form->fill(array_merge($this->thread->toArray(), [ @@ -50,11 +50,6 @@ public static function panelMaxWidth(): string return '2xl'; } - public static function closePanelOnEscape(): bool - { - return false; - } - public static function closePanelOnClickAway(): bool { return false; diff --git a/app/Livewire/Components/User/Articles.php b/app/Livewire/Components/User/Articles.php index e84661da..3ef4b253 100644 --- a/app/Livewire/Components/User/Articles.php +++ b/app/Livewire/Components/User/Articles.php @@ -29,7 +29,8 @@ final class Articles extends Component implements HasActions, HasForms #[Computed] public function articles(): LengthAwarePaginator { - return Article::with('tags', 'reactions') + return Article::with('tags') + ->withCount('reactions') ->where('user_id', Auth::id()) ->latest() ->paginate(10); diff --git a/app/Livewire/Components/User/Threads.php b/app/Livewire/Components/User/Threads.php index 618108f9..e975a871 100644 --- a/app/Livewire/Components/User/Threads.php +++ b/app/Livewire/Components/User/Threads.php @@ -32,8 +32,9 @@ final class Threads extends Component implements HasActions, HasForms #[Computed] public function threads(): LengthAwarePaginator { - return Thread::with('channels', 'solutionReply', 'replies') + return Thread::with('channels', 'solutionReply') ->scopes('withViewsCount') + ->withCount('replies') ->where('user_id', Auth::id()) ->latest() ->paginate(10); diff --git a/app/Livewire/Pages/Account/Dashboard.php b/app/Livewire/Pages/Account/Dashboard.php index c411c4d3..dede46b7 100644 --- a/app/Livewire/Pages/Account/Dashboard.php +++ b/app/Livewire/Pages/Account/Dashboard.php @@ -18,7 +18,7 @@ final class Dashboard extends Component #[Computed] public function user(): User { - return User::with('providers') + return User::query() ->scopes('withCounts') ->find(Auth::id()); } diff --git a/app/Livewire/Pages/Account/Profile.php b/app/Livewire/Pages/Account/Profile.php index 2e8e4ed1..cfc635a6 100644 --- a/app/Livewire/Pages/Account/Profile.php +++ b/app/Livewire/Pages/Account/Profile.php @@ -26,8 +26,8 @@ public function articles(): Collection ttl: now()->addDays(3), callback: fn () => Article::with('media', 'tags') ->select('id', 'title', 'slug', 'body', 'published_at') - ->whereBelongsTo($this->user) ->published() + ->whereBelongsTo($this->user) ->recent() ->limit(5) ->get() diff --git a/app/Livewire/Pages/Articles/Index.php b/app/Livewire/Pages/Articles/Index.php index 87136565..cf2ab99f 100644 --- a/app/Livewire/Pages/Articles/Index.php +++ b/app/Livewire/Pages/Articles/Index.php @@ -8,35 +8,37 @@ use App\Models\Tag; use App\Traits\WithLocale; use Illuminate\Contracts\View\View; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Livewire\Component; -use Livewire\WithoutUrlPagination; use Livewire\WithPagination; final class Index extends Component { use WithLocale; - use WithoutUrlPagination; use WithPagination; public function render(): View { return view('livewire.pages.articles.index', [ - 'articles' => Article::with(['tags', 'user', 'user.transactions', 'user.media', 'media']) // @phpstan-ignore-line + // @phpstan-ignore-next-line + 'articles' => Article::with([ + 'tags', + 'user:id,username,name,avatar_type', + 'media', + ]) ->withCount(['views', 'reactions']) - ->orderByDesc('sponsored_at') - ->orderByDesc('published_at') ->published() - ->notPinned() + ->orderByDesc('published_at') ->forLocale($this->locale) ->simplePaginate(21), - 'tags' => Cache::remember( key: 'articles.tags', ttl: now()->addWeek(), - callback: fn () => Tag::query()->whereHas('articles', function ($query): void { + callback: fn (): Collection => Tag::query()->whereHas('articles', function ($query): void { $query->published(); // @phpstan-ignore-line - })->orderBy('name')->get() + })->orderBy('name') + ->get() ), ]) ->title(__('pages/article.title')); diff --git a/app/Livewire/Pages/Articles/SinglePost.php b/app/Livewire/Pages/Articles/SinglePost.php index 44abeef0..f654c822 100644 --- a/app/Livewire/Pages/Articles/SinglePost.php +++ b/app/Livewire/Pages/Articles/SinglePost.php @@ -8,6 +8,7 @@ use App\Models\User; use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; use Livewire\Component; final class SinglePost extends Component @@ -19,7 +20,10 @@ public function mount(): void /** @var User $user */ $user = Auth::user(); - $article = $this->article->load(['media', 'user'])->loadCount('views'); + $article = Cache::rememberForever( + key: 'article.'.$this->article->id, + callback: fn (): Article => $this->article->load('user:id,name,username,avatar_type', 'tags', 'media')->loadCount('views'), + ); abort_unless( $article->isPublished() || ($user && $article->isAuthoredBy($user)) || ($user && $user->hasAnyRole(['admin', 'moderator'])), // @phpstan-ignore-line diff --git a/app/Livewire/Pages/Articles/SingleTag.php b/app/Livewire/Pages/Articles/SingleTag.php index 97d72f08..e107b44c 100644 --- a/app/Livewire/Pages/Articles/SingleTag.php +++ b/app/Livewire/Pages/Articles/SingleTag.php @@ -18,23 +18,16 @@ final class SingleTag extends Component public Tag $tag; - public function mount(): void - { - $this->locale = config('app.locale'); - } - public function render(): View { return view('livewire.pages.articles.tag', [ - 'articles' => Article::with(['tags', 'user', 'user.transactions']) // @phpstan-ignore-line + 'articles' => Article::with(['tags', 'user:id,name,username,avatar_type']) // @phpstan-ignore-line ->whereHas('tags', function ($query): void { $query->where('id', $this->tag->id); }) ->withCount(['views', 'reactions']) - ->orderByDesc('sponsored_at') - ->orderByDesc('published_at') ->published() - ->notPinned() + ->orderByDesc('published_at') ->forLocale($this->locale) ->paginate($this->perPage), ])->title($this->tag->name); diff --git a/app/Livewire/Pages/Discussions/Index.php b/app/Livewire/Pages/Discussions/Index.php index 8198ae94..f8c913ae 100644 --- a/app/Livewire/Pages/Discussions/Index.php +++ b/app/Livewire/Pages/Discussions/Index.php @@ -9,6 +9,7 @@ use App\Models\Tag; use App\Traits\WithLocale; use Illuminate\Contracts\View\View; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Livewire\Attributes\Url; use Livewire\Component; @@ -53,21 +54,24 @@ public function tagExists(string $tag): bool public function render(): View { /** @var DiscussionQueryBuilder $query */ - $query = Discussion::with(['tags', 'replies', 'user', 'user.media', 'reactions']) // @phpstan-ignore-line - ->withCount('replies') + $query = Discussion::with([ // @phpstan-ignore-line + 'tags', + 'user:id,name,username,avatar_type', + ]) + ->withCount(['reactions', 'replies']) ->forLocale($this->locale) ->notPinned(); $tags = Cache::remember( key: 'discussions.tags', ttl: now()->addWeek(), - callback: fn () => Tag::query() + callback: fn (): Collection => Tag::query() ->whereJsonContains('concerns', ['discussion']) ->orderBy('name') ->get() ); - if (! blank($this->currentTag)) { + if (filled($this->currentTag)) { $query->forTag($this->currentTag); // @phpstan-ignore-line } diff --git a/app/Livewire/Pages/Discussions/SingleDiscussion.php b/app/Livewire/Pages/Discussions/SingleDiscussion.php index 2d54a76c..b785b66b 100644 --- a/app/Livewire/Pages/Discussions/SingleDiscussion.php +++ b/app/Livewire/Pages/Discussions/SingleDiscussion.php @@ -14,6 +14,7 @@ use Filament\Forms\Concerns\InteractsWithForms; use Filament\Forms\Contracts\HasForms; use Illuminate\Contracts\View\View; +use Illuminate\Support\Facades\Cache; use Livewire\Component; final class SingleDiscussion extends Component implements HasActions, HasForms @@ -25,7 +26,14 @@ final class SingleDiscussion extends Component implements HasActions, HasForms public function mount(): void { - views($this->discussion)->cooldown(now()->addHours(2))->record(); + /** @var Discussion $discussion */ + $discussion = Cache::remember( + key: 'discussion-'.$this->discussion->id, + ttl: now()->addDays(3), + callback: fn (): Discussion => $this->discussion->load('user:id,name,username,avatar_type', 'tags') + ); + + views($discussion)->cooldown(now()->addHours(2))->record(); // @phpstan-ignore-next-line seo() @@ -36,7 +44,7 @@ public function mount(): void ->twitterDescription($this->discussion->excerpt(100)) ->withUrl(); - $this->discussion->load('tags', 'replies', 'reactions', 'user', 'user.media'); + $this->discussion = $discussion; } public function editAction(): Action @@ -80,7 +88,6 @@ public function deleteAction(): Action ->requiresConfirmation() ->successNotificationTitle(__('notifications.discussion.deleted')) ->action(function (): void { - app(DeleteDiscussionAction::class)->execute($this->discussion); $this->redirectRoute('discussions.index', navigate: true); diff --git a/app/Livewire/Pages/Forum/Index.php b/app/Livewire/Pages/Forum/Index.php index 07cbddab..4c3f8bb7 100644 --- a/app/Livewire/Pages/Forum/Index.php +++ b/app/Livewire/Pages/Forum/Index.php @@ -46,11 +46,11 @@ final class Index extends Component public string $search = ''; - public int $perPage = 30; + public int $perPage = 20; public function mount(): void { - if (! blank($this->channel)) { + if (filled($this->channel)) { $this->currentChannel = Channel::findBySlug($this->channel); } } @@ -73,11 +73,10 @@ public function redirectToLogin(): void protected function applyPopular(Builder $query): Builder { - if (! blank($this->popular)) { - return $query // @phpstan-ignore-line - ->withCount('replies') + if (filled($this->popular)) { + return $query ->orderByDesc('replies_count') - ->OrderByViews(); + ->orderByDesc('views_count'); } return $query; @@ -85,7 +84,7 @@ protected function applyPopular(Builder $query): Builder protected function applySearch(Builder $query): Builder { - if (! blank($this->search)) { + if (filled($this->search)) { return $query->where(function (Builder $query): void { $query->where('title', 'like', '%'.$this->search.'%'); }); @@ -98,10 +97,12 @@ protected function applySolved(Builder $query): Builder { if (filled($this->solved)) { // @phpstan-ignore-next-line - return match ($this->solved) { + $query = match ($this->solved) { 'no' => $query->scopes('unresolved'), 'yes' => $query->scopes('resolved'), }; + + return $query->withCount('replies')->withViewsCount(); } return $query; @@ -165,8 +166,17 @@ protected function applySorting(Builder $query): Builder public function render(): View { - $query = Thread::with(['channels', 'channels.parent', 'user', 'user.media']) - ->withCount('replies'); + $query = Thread::with([ + 'channels', + 'channels.parent', + 'user:id,username,name,avatar_type', + 'user.providers:id,user_id,provider,avatar', + 'user.media', + ]); + + if (blank($this->solved)) { + $query->withCount('replies')->withViewsCount(); + } $query = $this->applyChannel($query); $query = $this->applySearch($query); @@ -177,9 +187,7 @@ public function render(): View $query = $this->applyUnAnswer($query); $query = $this->applySorting($query); - $threads = $query - ->scopes('withViewsCount') - ->paginate($this->perPage); + $threads = $query->paginate($this->perPage); return view('livewire.pages.forum.index', [ 'threads' => $threads, diff --git a/app/Livewire/Pages/Home.php b/app/Livewire/Pages/Home.php index e33bfeef..a3c8182f 100644 --- a/app/Livewire/Pages/Home.php +++ b/app/Livewire/Pages/Home.php @@ -6,7 +6,6 @@ use App\Models\Article; use App\Models\Discussion; -use App\Models\Plan; use App\Models\Thread; use Illuminate\Contracts\View\View; use Illuminate\Support\Collection; @@ -17,42 +16,41 @@ final class Home extends Component { public function render(): View { - $ttl = now()->addDays(2); + $ttl = now()->addHours(6); return view('livewire.pages.home', [ - 'plans' => Cache::remember( - key: 'plans', - ttl: now()->addYear(), - callback: fn () => Plan::query()->developer()->get() - ), - 'latestArticles' => Cache::remember( - key: 'latestArticles', + 'articles' => Cache::tags('articles')->remember( + key: 'home.articles', ttl: $ttl, - callback: fn (): Collection => Article::with(['tags', 'media', 'user', 'user.transactions', 'user.media']) // @phpstan-ignore-line + callback: fn (): Collection => Article::with(['tags', 'media']) + ->latest('published_at') ->published() - ->orderByDesc('sponsored_at') - ->orderByDesc('published_at') - ->orderByViews() - ->trending() ->limit(4) ->get() ), - 'latestThreads' => Cache::remember( - key: 'latestThreads', + 'threads' => Cache::tags('threads')->remember( + key: 'home.threads', ttl: $ttl, - callback: fn (): Collection => Thread::with(['user', 'user.transactions', 'user.media']) + callback: fn (): Collection => Thread::with([ + 'user:id,username,name,avatar_type', + 'user.media', + 'user.providers:id,user_id,provider,avatar', + ]) ->whereNull('solution_reply_id') ->whereBetween('threads.created_at', [now()->subMonths(3), now()]) - ->inRandomOrder() + ->latest() ->limit(4) ->get() ), - 'latestDiscussions' => Cache::remember( - key: 'latestDiscussions', + 'discussions' => Cache::tags('discussions')->remember( + key: 'home.discussions', ttl: $ttl, - callback: fn (): Collection => Discussion::with(['user', 'user.transactions', 'user.media']) // @phpstan-ignore-line + callback: fn (): Collection => Discussion::with([ + 'user:id,username,name,avatar_type', + 'user.media', + 'user.providers:id,user_id,provider,avatar', + ]) ->recent() - ->orderByViews() ->limit(3) ->get() ), diff --git a/app/Models/Article.php b/app/Models/Article.php index 060485da..ff4865b6 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -9,12 +9,14 @@ use App\Models\Traits\HasAuthor; use App\Models\Traits\HasLocaleScope; use App\Models\Traits\HasSlug; +use App\Observers\ArticleObserver; use App\Traits\HasTags; use App\Traits\Reactable; use App\Traits\RecordsActivity; use Carbon\Carbon; use CyrildeWit\EloquentViewable\Contracts\Viewable; use CyrildeWit\EloquentViewable\InteractsWithViews; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; @@ -25,19 +27,19 @@ /** * @property-read int $id - * @property string $title - * @property string $slug - * @property string $body - * @property bool $show_toc - * @property bool $is_pinned - * @property int $is_sponsored - * @property string|null $canonical_url - * @property string|null $reason - * @property int|null $tweet_id - * @property int $user_id - * @property string|null $locale + * @property-read string $title + * @property-read string $slug + * @property-read string $body + * @property-read bool $show_toc + * @property-read bool $is_pinned + * @property-read int $is_sponsored + * @property-read string|null $canonical_url + * @property-read string|null $reason + * @property-read int|null $tweet_id + * @property-read int $user_id + * @property-read string|null $locale * @property-read User $user - * @property \Illuminate\Support\Carbon|null $published_at + * @property-read \Illuminate\Support\Carbon|null $published_at * @property-read \Illuminate\Support\Carbon|null $submitted_at * @property-read \Illuminate\Support\Carbon|null $approved_at * @property-read \Illuminate\Support\Carbon|null $shared_at @@ -47,6 +49,7 @@ * @property-read \Illuminate\Support\Carbon $updated_at * @property \Illuminate\Database\Eloquent\Collection $tags */ +#[ObservedBy(ArticleObserver::class)] final class Article extends Model implements HasMedia, ReactableInterface, Sitemapable, Viewable { use HasAuthor; @@ -95,6 +98,18 @@ public function toSitemapTag(): Url ->setPriority(0.5); } + public function registerMediaCollections(): void + { + $this->addMediaCollection('media') + ->singleFile() + ->acceptsMimeTypes([ + 'image/jpg', + 'image/png', + 'image/webp', + 'image/avif', + ]); + } + public function excerpt(int $limit = 110): string { return Str::limit(strip_tags((string) md_to_html($this->body)), $limit); @@ -195,13 +210,6 @@ public function isNotAwaitingApproval(): bool return ! $this->isAwaitingApproval(); } - public function registerMediaCollections(): void - { - $this->addMediaCollection('media') - ->singleFile() - ->acceptsMimeTypes(['image/jpg', 'image/jpeg', 'image/png']); - } - public function markAsShared(): void { $this->update(['shared_at' => now()]); diff --git a/app/Models/Channel.php b/app/Models/Channel.php index e337d9c7..afb44073 100644 --- a/app/Models/Channel.php +++ b/app/Models/Channel.php @@ -16,11 +16,14 @@ /** * @property-read int $id - * @property string $name - * @property string $slug - * @property array $description - * @property string $color - * @property int | null $parent_id + * @property-read string $name + * @property-read string $slug + * @property-read string|null $description + * @property-read string $color + * @property-read int|null $parent_id + * @property-read Channel|null $parent + * @property-read \Illuminate\Support\Collection $items + * @property-read \Illuminate\Support\Collection $threads */ final class Channel extends Model { @@ -39,6 +42,7 @@ protected static function boot(): void self::saving(function (self $channel): void { /** @var self $record */ $record = self::query()->find($channel->parent_id); + if ($channel->parent_id && $record->exists() && $record->parent_id) { throw CannotAddChannelToChild::childChannelCannotBeParent($channel); } diff --git a/app/Models/Discussion.php b/app/Models/Discussion.php index 689d0366..f43763f9 100644 --- a/app/Models/Discussion.php +++ b/app/Models/Discussion.php @@ -21,7 +21,6 @@ use Carbon\Carbon; use CyrildeWit\EloquentViewable\Contracts\Viewable; use CyrildeWit\EloquentViewable\InteractsWithViews; -use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; @@ -30,17 +29,17 @@ /** * @property-read int $id - * @property string $title - * @property string $slug - * @property string $body - * @property bool $locked - * @property bool $is_pinned - * @property string|null $locale - * @property int $user_id + * @property-read string $title + * @property-read string $slug + * @property-read string $body + * @property-read bool $locked + * @property-read bool $is_pinned + * @property-read string|null $locale + * @property-read int $user_id * @property-read int $count_all_replies_with_child + * @property-read User $user * @property-read \Illuminate\Support\Carbon $created_at * @property-read \Illuminate\Support\Carbon $updated_at - * @property-read User $user * @property-read \Illuminate\Support\Collection $spamReports * @property-read \Illuminate\Support\Collection $replies * @property-read \Illuminate\Support\Collection $tags @@ -72,22 +71,6 @@ protected function casts(): array ]; } - protected function countAllRepliesWithChild(): Attribute - { - return Attribute::make( - get: function () { - $count = $this->replies->count(); - - foreach ($this->replies()->withCount('allChildReplies')->get() as $reply) { - /** @var Reply $reply */ - $count += $reply->all_child_replies_count; - } - - return $count; - } - ); - } - public function newEloquentBuilder($query): DiscussionQueryBuilder { return new DiscussionQueryBuilder($query); diff --git a/app/Models/Thread.php b/app/Models/Thread.php index 5f68d5b6..c65426d6 100644 --- a/app/Models/Thread.php +++ b/app/Models/Thread.php @@ -219,13 +219,14 @@ public function removeChannels(): void /** * @param Builder $query + * @return Builder */ #[Scope] - protected function channel(Builder $query, Channel $channel): void + protected function channel(Builder $query, Channel $channel): Builder { - $query->whereHas('channels', function ($query) use ($channel): void { + return $query->whereHas('channels', function ($query) use ($channel): void { if ($channel->hasItems()) { - $query->whereIn('channels.id', array_merge([$channel->id], $channel->items->modelKeys())); + $query->whereIn('channels.id', array_merge([$channel->id], $channel->items->modelKeys())); // @phpstan-ignore-line } else { $query->where('channels.slug', $channel->slug); } @@ -243,20 +244,22 @@ protected function recent(Builder $query): void /** * @param Builder $query + * @return Builder */ #[Scope] - protected function resolved(Builder $query): void + protected function resolved(Builder $query): Builder { - $query->feedQuery()->whereNotNull('solution_reply_id'); + return $query->whereNotNull('solution_reply_id'); } /** * @param Builder $query + * @return Builder */ #[Scope] - protected function unresolved(Builder $query): void + protected function unresolved(Builder $query): Builder { - $query->feedQuery()->whereNull('solution_reply_id'); + return $query->whereNull('solution_reply_id'); } /** @@ -267,25 +270,27 @@ protected function unresolved(Builder $query): void #[Scope] protected function filter(Builder $builder, Request $request, array $filters = []): Builder { - return (new ThreadFilters($request))->add($filters)->filter($builder); + return new ThreadFilters($request)->add($filters)->filter($builder); } /** * @param Builder $query + * @return Builder */ #[Scope] - protected function active(Builder $query): void + protected function active(Builder $query): Builder { - $query->whereHas('replies'); + return $query->whereHas('replies'); } /** * @param Builder $query + * @return Builder */ #[Scope] - protected function feedQuery(Builder $query): void + protected function feedQuery(Builder $query): Builder { - $query->with([ + return $query->with([ 'solutionReply', 'replies', 'reactions', @@ -300,8 +305,8 @@ protected function feedQuery(Builder $query): void ->orderBy('latest_creation', 'DESC') ->groupBy('threads.id') ->select('threads.*', DB::raw(' - CASE WHEN COALESCE(MAX(replies.created_at), 0) > threads.created_at - THEN COALESCE(MAX(replies.created_at), 0) + CASE WHEN COALESCE(MAX(replies.created_at), threads.created_at) > threads.created_at + THEN COALESCE(MAX(replies.created_at), threads.created_at) ELSE threads.created_at END AS latest_creation ')); diff --git a/app/Models/Traits/HasSlug.php b/app/Models/Traits/HasSlug.php index 4b3135e0..d8a7f320 100644 --- a/app/Models/Traits/HasSlug.php +++ b/app/Models/Traits/HasSlug.php @@ -16,7 +16,7 @@ protected function slug(): Attribute public static function findBySlug(string $slug): static { - return static::where('slug', $slug)->firstOrFail(); + return static::query()->where('slug', $slug)->firstOrFail(); } private function generateUniqueSlug(string $value): string diff --git a/app/Models/User.php b/app/Models/User.php index 593be96a..05adb35e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,12 +4,14 @@ namespace App\Models; +use App\Contracts\HasCachedMediaInterface; use App\Enums\TransactionStatus; use App\Observers\UserObserver; use App\Traits\HasProfilePhoto; use App\Traits\HasSettings; use App\Traits\HasUsername; use App\Traits\Reacts; +use Database\Factories\UserFactory; use Filament\Models\Contracts\FilamentUser; use Filament\Models\Contracts\HasAvatar; use Filament\Models\Contracts\HasName; @@ -35,20 +37,20 @@ /** * @property-read int $id - * @property string $name - * @property string $email - * @property string $username - * @property string $avatar_type - * @property string $profile_photo_url - * @property string|null $location - * @property string|null $phone_number - * @property string|null $github_profile - * @property string|null $twitter_profile - * @property string|null $linkedin_profile - * @property string|null $bio - * @property string|null $website - * @property string|null $banned_reason - * @property array $settings + * @property-read string $name + * @property-read string $email + * @property-read string $username + * @property-read string $avatar_type + * @property-read string $profile_photo_url + * @property-read string|null $location + * @property-read string|null $phone_number + * @property-read string|null $github_profile + * @property-read string|null $twitter_profile + * @property-read string|null $linkedin_profile + * @property-read string|null $bio + * @property-read string|null $website + * @property-read string|null $banned_reason + * @property-read array|null $settings * @property-read \Illuminate\Support\Carbon|null $email_verified_at * @property-read \Illuminate\Support\Carbon|null $last_login_at * @property-read \Illuminate\Support\Carbon|null $banned_at @@ -64,10 +66,12 @@ * @property-read int $discussions_count */ #[ObservedBy(UserObserver::class)] -final class User extends Authenticatable implements FilamentUser, HasAvatar, HasMedia, HasName, MustVerifyEmail +final class User extends Authenticatable implements FilamentUser, HasAvatar, HasCachedMediaInterface, HasMedia, HasName, MustVerifyEmail { - use Gamify; + /** @use HasFactory */ use HasFactory; + + use Gamify; use HasPlanSubscriptions; use HasProfilePhoto; use HasRoles; @@ -87,10 +91,6 @@ final class User extends Authenticatable implements FilamentUser, HasAvatar, Has 'last_active_at', ]; - protected $with = [ - 'providers', - ]; - protected function casts(): array { return [ @@ -130,7 +130,7 @@ protected function IsSponsor(): Attribute public function hasProvider(string $provider): bool { - return array_any($this->providers, fn ($p): bool => $p->provider === $provider); // @phpstan-ignore-line + return $this->providers->contains(fn ($p): bool => $p->provider === $provider); } public function hasEnterprise(): bool @@ -325,11 +325,12 @@ public function notBanned(): bool /** * @param Builder $query + * @return Builder */ #[Scope] - protected function hasActivity(Builder $query): void + protected function hasActivity(Builder $query): Builder { - $query->where(function ($query): void { + return $query->where(function ($query): void { $query->has('threads') ->orHas('replyAble'); }); @@ -337,49 +338,54 @@ protected function hasActivity(Builder $query): void /** * @param Builder $query + * @return Builder */ #[Scope] - protected function moderators(Builder $query): void + protected function moderators(Builder $query): Builder { - $query->whereHas('roles', function ($query): void { + return $query->whereHas('roles', function ($query): void { $query->where('name', '<>', 'user'); }); } /** * @param Builder $query + * @return Builder */ #[Scope] - protected function withoutRole(Builder $query): void + protected function withoutRole(Builder $query): Builder { - $query->whereDoesntHave('roles'); + return $query->whereDoesntHave('roles'); } /** * @param Builder $query + * @return Builder */ #[Scope] - protected function verifiedUsers(Builder $query): void + protected function verifiedUsers(Builder $query): Builder { - $query->whereNotNull('email_verified_at'); + return $query->whereNotNull('email_verified_at'); } /** * @param Builder $query + * @return Builder */ #[Scope] - protected function unVerifiedUsers(Builder $query): void + protected function unVerifiedUsers(Builder $query): Builder { - $query->whereNull('email_verified_at'); + return $query->whereNull('email_verified_at'); } /** * @param Builder $query + * @return Builder */ #[Scope] - protected function mostSolutions(Builder $query, ?int $inLastDays = null): void + protected function mostSolutions(Builder $query, ?int $inLastDays = null): Builder { - $query->withCount(['replyAble as solutions_count' => function ($query) use ($inLastDays) { + return $query->withCount(['replyAble as solutions_count' => function ($query) use ($inLastDays) { $query->where('replyable_type', 'thread') ->join('threads', 'threads.solution_reply_id', '=', 'replies.id'); @@ -393,11 +399,12 @@ protected function mostSolutions(Builder $query, ?int $inLastDays = null): void /** * @param Builder $query + * @return Builder */ #[Scope] - protected function mostSubmissions(Builder $query, ?int $inLastDays = null): void + protected function mostSubmissions(Builder $query, ?int $inLastDays = null): Builder { - $query->withCount(['articles as articles_count' => function ($query) use ($inLastDays) { + return $query->withCount(['articles as articles_count' => function ($query) use ($inLastDays) { if (filled($inLastDays)) { $query->where('articles.approved_at', '>', now()->subDays($inLastDays)); } @@ -408,29 +415,32 @@ protected function mostSubmissions(Builder $query, ?int $inLastDays = null): voi /** * @param Builder $query + * @return Builder */ #[Scope] - protected function mostSolutionsInLastDays(Builder $query, int $days): void + protected function mostSolutionsInLastDays(Builder $query, int $days): Builder { - $query->mostSolutions($days); + return $query->mostSolutions($days); } /** * @param Builder $query + * @return Builder */ #[Scope] - protected function mostSubmissionsInLastDays(Builder $query, int $days): void + protected function mostSubmissionsInLastDays(Builder $query, int $days): Builder { - $query->mostSubmissions($days); + return $query->mostSubmissions($days); } /** * @param Builder $query + * @return Builder */ #[Scope] - protected function withCounts(Builder $query): void + protected function withCounts(Builder $query): Builder { - $query->withCount([ + return $query->withCount([ 'discussions as discussions_count', 'articles as articles_count', 'threads as threads_count', @@ -442,29 +452,32 @@ protected function withCounts(Builder $query): void /** * @param Builder $query + * @return Builder */ #[Scope] - protected function topContributors(Builder $query): void + protected function topContributors(Builder $query): Builder { - $query->withCount(['discussions'])->orderByDesc('discussions_count'); + return $query->withCount('discussions')->orderByDesc('discussions_count'); } /** * @param Builder $query + * @return Builder */ #[Scope] - protected function isBanned(Builder $query): void + protected function isBanned(Builder $query): Builder { - $query->whereNotNull('banned_at'); + return $query->whereNotNull('banned_at'); } /** * @param Builder $query + * @return Builder */ #[Scope] - protected function isNotBanned(Builder $query): void + protected function isNotBanned(Builder $query): Builder { - $query->whereNull('banned_at'); + return $query->whereNull('banned_at'); } /** diff --git a/app/Observers/ArticleObserver.php b/app/Observers/ArticleObserver.php new file mode 100644 index 00000000..516621ea --- /dev/null +++ b/app/Observers/ArticleObserver.php @@ -0,0 +1,37 @@ +invalidateCaches($article); + } + + public function updated(Article $article): void + { + $this->invalidateCaches($article); + } + + public function deleting(Article $article): void + { + $this->invalidateCaches($article); + } + + private function invalidateCaches(Article $article): void + { + $cacheService = app(CacheInvalidationService::class); + + Cache::forget('article.'.$article->id); + + $cacheService->invalidateByPattern('articles.blog.'); + $cacheService->invalidateByPattern('article.'.$article->id); + } +} diff --git a/app/Observers/MediaObserver.php b/app/Observers/MediaObserver.php new file mode 100644 index 00000000..d2e16b29 --- /dev/null +++ b/app/Observers/MediaObserver.php @@ -0,0 +1,35 @@ +flushModelMediaCache($media); + } + + public function updated(Media $media): void + { + $this->flushModelMediaCache($media); + } + + public function deleted(Media $media): void + { + $this->flushModelMediaCache($media); + } + + private function flushModelMediaCache(Media $media): void + { + $model = $media->model; + + if ($model instanceof HasCachedMediaInterface) { + $model->flushMediaCache($media->collection_name); + } + } +} diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php index 609f857e..0541a78a 100644 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -10,20 +10,24 @@ final class UserObserver { public function updated(User $user): void { + if ($user->isDirty(['avatar_type', 'name'])) { + $user->flushAvatarCache(); + } + + $avatar_type = 'avatar'; + $media = $user->getMedia('avatar')->first(); if ($media) { - $user->avatar_type = 'storage'; + $avatar_type = 'storage'; } if (! $media && $user->providers->isNotEmpty()) { - $user->avatar_type = $user->providers->first()->provider; - } - - if (! $media && $user->providers->isEmpty()) { - $user->avatar_type = 'avatar'; + $avatar_type = $user->providers->first()->provider; } - $user->saveQuietly(); + $user->saveQuietly([ + 'avatar_type' => $avatar_type, + ]); } } diff --git a/app/Services/CacheInvalidationService.php b/app/Services/CacheInvalidationService.php new file mode 100644 index 00000000..89f875dd --- /dev/null +++ b/app/Services/CacheInvalidationService.php @@ -0,0 +1,83 @@ + + + Copyright © 2024-2025, Shopper Labs SARL. All rights reserved. + + Universy App™ is licensed under the Elastic License 2.0. For more details, + see https://github.com/shopperlabs/institute-app/blob/main/LICENSE. + + Notice: + + - You may not provide the software to third parties as a hosted or managed + service, where the service provides users with access to any substantial set of + the features or functionality of the software. + - You may not move, change, disable, or circumvent the license key functionality + in the software, and you may not remove or obscure any functionality in the + software that is protected by the license key. + - You may not alter, remove, or obscure any licensing, copyright, or other notices + of the licensor in the software. Any use of the licensor’s trademarks is subject + to applicable law. + - Shopper Labs SARL respects the intellectual property rights of others and expects the + same in return. Institute App™ are registered trademarks of + Shopper Labs SARL, and we are committed to enforcing and protecting our trademarks + vigorously. + - The software solution, including services, infrastructure, and code, is offered as a + Software as a Service (SaaS) by Shopper Labs SARL. + - Use of this software implies agreement to the license terms and conditions as stated + in the Elastic License 2.0. + + For more information or inquiries, please visit our website at + https://www.shopperlabs.co or contact us via email at contact@shopperlabs.co. + + +*/ + +declare(strict_types=1); + +namespace App\Services; + +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Redis; + +final class CacheInvalidationService +{ + public function invalidateByPattern(string $pattern): void + { + $driver = Cache::getDefaultDriver(); + + match ($driver) { + 'redis' => $this->invalidateRedisPattern($pattern), + 'database' => $this->invalidateDatabasePattern($pattern), + default => $this->invalidateFallbackPattern(), + }; + } + + private function invalidateRedisPattern(string $pattern): void + { + try { + $connection = config('cache.stores.redis.connection', 'cache'); + $keys = Redis::connection($connection)->keys($pattern.'*'); + + if (! empty($keys)) { + Redis::connection($connection)->del($keys); + } + } catch (\Exception $e) { + $this->invalidateFallbackPattern(); + } + } + + private function invalidateDatabasePattern(string $pattern): void + { + DB::table(config('cache.stores.database.table', 'cache')) + ->where('key', 'like', $pattern.'%') + ->delete(); + } + + private function invalidateFallbackPattern(): void + { + Cache::flush(); + } +} diff --git a/app/Services/MediaCacheService.php b/app/Services/MediaCacheService.php new file mode 100644 index 00000000..33678fd2 --- /dev/null +++ b/app/Services/MediaCacheService.php @@ -0,0 +1,58 @@ +buildCacheKey($model, $collection, $conversion); + + return Cache::remember( + $cacheKey, + $model->getCacheTtl(), + fn (): ?string => $this->resolveMediaUrl($model, $collection, $conversion) + ); + } + + public function flushCache(HasCachedMediaInterface&HasMedia $model, ?string $collection = null): void + { + $collections = filled($collection) ? Arr::wrap($collection) : $model->getMediaCollections(); + + foreach ($collections as $collectionName) { + $this->flushCollectionCache($model, $collectionName); + } + } + + private function resolveMediaUrl(HasMedia $model, string $collection, ?string $conversion = null): ?string + { + $media = $model->getFirstMedia($collection); // @phpstan-ignore-line + + if (! $media) { + return null; + } + + return filled($conversion) ? $media->getUrl($conversion) : $media->getUrl(); + } + + private function buildCacheKey(HasCachedMediaInterface $model, string $collection, ?string $conversion = null): string + { + $suffix = filled($conversion) ? ".{$conversion}" : ''; + + return "{$model->getCacheKey($collection)}{$suffix}"; + } + + private function flushCollectionCache(HasCachedMediaInterface $model, string $collection): void + { + $baseKey = $model->getCacheKey($collection); + + app(CacheInvalidationService::class)->invalidateByPattern($baseKey); + } +} diff --git a/app/Traits/HasCachedMedia.php b/app/Traits/HasCachedMedia.php new file mode 100644 index 00000000..986c5baf --- /dev/null +++ b/app/Traits/HasCachedMedia.php @@ -0,0 +1,40 @@ +getKey(), + $collection + ); + } + + public function getCacheTtl(): \DateTimeInterface + { + return now()->addYear(); + } + + public function flushMediaCache(?string $collection = null): void + { + app(MediaCacheService::class)->flushCache($this, $collection); + } + + public function getMediaCollections(): array + { + return ['default']; + } + + public function getCachedMediaUrl(string $collection, ?string $conversion = null): ?string + { + return app(MediaCacheService::class)->getCachedMediaUrl($this, $collection, $conversion); + } +} diff --git a/app/Traits/HasProfilePhoto.php b/app/Traits/HasProfilePhoto.php index 23f6d870..3bb4c9d9 100644 --- a/app/Traits/HasProfilePhoto.php +++ b/app/Traits/HasProfilePhoto.php @@ -6,29 +6,52 @@ use App\Models\SocialAccount; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Support\Facades\Cache; trait HasProfilePhoto { + use HasCachedMedia; + protected function profilePhotoUrl(): Attribute { - return Attribute::get(function (): ?string { - if ($this->avatar_type === 'storage') { - return $this->getFirstMediaUrl('avatar'); - } + return Attribute::get(fn (): string => Cache::remember( + "user.{$this->id}.profile_photo_url", + now()->addYear(), + fn (): string => $this->resolveProfilePhotoUrl() + )); + } + + public function getMediaCollections(): array + { + return ['avatar']; + } + + public function flushAvatarCache(): void + { + Cache::forget("user.{$this->id}.profile_photo_url"); + + $this->flushMediaCache('avatar'); + } + + private function resolveProfilePhotoUrl(): string + { + if ($this->avatar_type === 'storage') { + return $this->getCachedMediaUrl('avatar') ?? $this->defaultProfilePhotoUrl(); + } + + if (! in_array($this->avatar_type, ['avatar', 'storage'])) { + $this->loadMissing('providers'); - if (! in_array($this->avatar_type, ['avatar', 'storage'])) { - /** @var SocialAccount $social_avatar */ - $social_avatar = $this->providers->firstWhere('provider', $this->avatar_type); + /** @var SocialAccount $social_avatar */ + $social_avatar = $this->providers->firstWhere('provider', $this->avatar_type); - // @phpstan-ignore-next-line - return $social_avatar ? $social_avatar->avatar : $this->defaultProfilePhotoUrl(); - } + return $social_avatar->avatar ?? $this->defaultProfilePhotoUrl(); + } - return $this->defaultProfilePhotoUrl(); - }); + return $this->defaultProfilePhotoUrl(); } - protected function defaultProfilePhotoUrl(): ?string + protected function defaultProfilePhotoUrl(): string { return 'https://ui-avatars.com/api/?name='.urlencode($this->name).'&color=065F46&background=D1FAE5'; } diff --git a/app/Traits/HasSettings.php b/app/Traits/HasSettings.php index 87d93661..8c2c8798 100644 --- a/app/Traits/HasSettings.php +++ b/app/Traits/HasSettings.php @@ -15,13 +15,11 @@ public function setting(string $key, string $default): mixed return $default; } - public function settings(array $revisions, bool $save = true): self + public function settings(array $revisions): self { - $this->settings = array_merge($this->settings ?? [], $revisions); - - if ($save) { - $this->save(); - } + $this->update([ + 'settings' => array_merge($this->settings ?? [], $revisions), + ]); return $this; } diff --git a/app/Traits/HasUsername.php b/app/Traits/HasUsername.php index dd184bf9..9e26cfde 100644 --- a/app/Traits/HasUsername.php +++ b/app/Traits/HasUsername.php @@ -15,7 +15,7 @@ protected function username(): Attribute public static function findByUsername(string $username): self { - return static::where('username', $username)->firstOrFail(); + return static::query()->where('username', $username)->firstOrFail(); } private function generateUniqueUsername(string $value): string diff --git a/app/Traits/Reactable.php b/app/Traits/Reactable.php index 340805a3..a04b4621 100644 --- a/app/Traits/Reactable.php +++ b/app/Traits/Reactable.php @@ -15,6 +15,13 @@ trait Reactable { public function getReactionsSummary(): Collection { + if ($this->relationLoaded('reactions') && $this->reactions->isNotEmpty()) { + return $this->reactions->groupBy('name')->map(fn ($group): array => [ // @phpstan-ignore-line + 'name' => $group->first()->name, // @phpstan-ignore-line + 'count' => $group->sum('count') ?: $group->count(), + ])->values(); + } + return $this->reactions() ->getQuery() ->select('name', DB::raw('count(*) as count')) diff --git a/app/View/Composers/InactiveDiscussionsComposer.php b/app/View/Composers/InactiveDiscussionsComposer.php index 4bdac45d..baa43952 100644 --- a/app/View/Composers/InactiveDiscussionsComposer.php +++ b/app/View/Composers/InactiveDiscussionsComposer.php @@ -5,6 +5,7 @@ namespace App\View\Composers; use App\Models\Discussion; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\View\View; @@ -13,9 +14,15 @@ final class InactiveDiscussionsComposer public function compose(View $view): void { $discussions = Cache::remember( - key: 'inactive_discussions', + key: 'discussions.inactive', ttl: now()->addWeek(), - callback: fn () => Discussion::with('user', 'user.media')->noComments()->limit(5)->get() + callback: fn (): Collection => Discussion::with([ + 'user:id,username,name,avatar_type', + 'user.media', + ]) + ->noComments() + ->limit(5) + ->get() ); $view->with('discussions', $discussions); diff --git a/app/View/Composers/ModeratorsComposer.php b/app/View/Composers/ModeratorsComposer.php index 8c57633c..2d0f939d 100644 --- a/app/View/Composers/ModeratorsComposer.php +++ b/app/View/Composers/ModeratorsComposer.php @@ -17,7 +17,7 @@ public function compose(View $view): void Cache::remember( key: 'moderators', ttl: now()->addMonths(6), - callback: fn () => User::moderators()->get() + callback: fn () => User::query()->scopes('moderators')->get() ) ); } diff --git a/app/View/Composers/ProfileUsersComposer.php b/app/View/Composers/ProfileUsersComposer.php index d2b3a119..6fe235fb 100644 --- a/app/View/Composers/ProfileUsersComposer.php +++ b/app/View/Composers/ProfileUsersComposer.php @@ -5,6 +5,7 @@ namespace App\View\Composers; use App\Models\User; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\View\View; @@ -15,9 +16,12 @@ public function compose(View $view): void $view->with( 'users', Cache::remember( - key: 'avatar_users', + key: 'avatars', ttl: now()->addWeek(), - callback: fn () => User::verifiedUsers()->inRandomOrder()->take(10)->get() + callback: fn (): Collection => User::query()->scopes('verifiedUsers') + ->inRandomOrder() + ->take(10) + ->get() ) ); } diff --git a/app/View/Composers/TopContributorsComposer.php b/app/View/Composers/TopContributorsComposer.php index 3094fb8c..0ce5bf59 100644 --- a/app/View/Composers/TopContributorsComposer.php +++ b/app/View/Composers/TopContributorsComposer.php @@ -16,11 +16,11 @@ public function compose(View $view): void key: 'contributors', ttl: now()->addWeek(), callback: fn () => User::with('media') - ->withCount('discussions') + ->select('id', 'username', 'name', 'avatar_type') ->scopes('topContributors') + ->limit(5) ->get() ->filter(fn (User $contributor): bool => $contributor->discussions_count >= 1) - ->take(5) ); $view->with('topContributors', $topContributors); diff --git a/composer.json b/composer.json index 83025310..8f7704e5 100644 --- a/composer.json +++ b/composer.json @@ -41,10 +41,10 @@ "livewire/volt": "^1.6", "mckenziearts/blade-untitledui-icons": "^1.4", "notchpay/notchpay-php": "^1.6", + "predis/predis": "^3.2", "ramsey/uuid": "^4.7.4", "spatie/laravel-data": "^4.10", "spatie/laravel-feed": "^4.2.1", - "spatie/laravel-google-fonts": "^1.2.3", "spatie/laravel-permission": "^6.10.0", "spatie/laravel-sitemap": "^7.3", "stevebauman/location": "^7.4.0", @@ -101,7 +101,7 @@ ], "dev:install": [ "./vendor/bin/sail up -d", - "./vendor/bin/sail artisan migrate --seed", + "./vendor/bin/sail artisan migrate", "./vendor/bin/sail npm install --no-update-notifier", "./vendor/bin/sail stop" ], @@ -118,7 +118,10 @@ "types": "phpstan analyse --memory-limit=-1", "rector": "./vendor/bin/rector", "rector:preview": "./vendor/bin/rector --dry-run", - "test": "./vendor/bin/sail pest" + "test": [ + "Composer\\Config::disableProcessTimeout", + "./vendor/bin/sail artisan test --parallel --processes=4" + ] }, "config": { "optimize-autoloader": true, diff --git a/composer.lock b/composer.lock index c87d12a0..0de1d077 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2edfd9d18edfbefc90804a7d87bbce42", + "content-hash": "385c5568979dfa8b9579bf223bf15492", "packages": [ { "name": "abraham/twitteroauth", @@ -68,814 +68,6 @@ }, "time": "2023-10-28T20:17:00+00:00" }, - { - "name": "amphp/amp", - "version": "v3.1.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/amp.git", - "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/fa0ab33a6f47a82929c38d03ca47ebb71086a93f", - "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "phpunit/phpunit": "^9", - "psalm/phar": "5.23.1" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php", - "src/Future/functions.php", - "src/Internal/functions.php" - ], - "psr-4": { - "Amp\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - } - ], - "description": "A non-blocking concurrency framework for PHP applications.", - "homepage": "https://amphp.org/amp", - "keywords": [ - "async", - "asynchronous", - "awaitable", - "concurrency", - "event", - "event-loop", - "future", - "non-blocking", - "promise" - ], - "support": { - "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v3.1.1" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-08-27T21:42:00+00:00" - }, - { - "name": "amphp/byte-stream", - "version": "v2.1.2", - "source": { - "type": "git", - "url": "https://github.com/amphp/byte-stream.git", - "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", - "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/parser": "^1.1", - "amphp/pipeline": "^1", - "amphp/serialization": "^1", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2.3" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "5.22.1" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php", - "src/Internal/functions.php" - ], - "psr-4": { - "Amp\\ByteStream\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A stream abstraction to make working with non-blocking I/O simple.", - "homepage": "https://amphp.org/byte-stream", - "keywords": [ - "amp", - "amphp", - "async", - "io", - "non-blocking", - "stream" - ], - "support": { - "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-03-16T17:10:27+00:00" - }, - { - "name": "amphp/cache", - "version": "v2.0.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/cache.git", - "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", - "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/serialization": "^1", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\Cache\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - } - ], - "description": "A fiber-aware cache API based on Amp and Revolt.", - "homepage": "https://amphp.org/cache", - "support": { - "issues": "https://github.com/amphp/cache/issues", - "source": "https://github.com/amphp/cache/tree/v2.0.1" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-04-19T03:38:06+00:00" - }, - { - "name": "amphp/dns", - "version": "v2.4.0", - "source": { - "type": "git", - "url": "https://github.com/amphp/dns.git", - "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", - "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/cache": "^2", - "amphp/parser": "^1", - "amphp/process": "^2", - "daverandom/libdns": "^2.0.2", - "ext-filter": "*", - "ext-json": "*", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "5.20" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Dns\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Chris Wright", - "email": "addr@daverandom.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - } - ], - "description": "Async DNS resolution for Amp.", - "homepage": "https://github.com/amphp/dns", - "keywords": [ - "amp", - "amphp", - "async", - "client", - "dns", - "resolve" - ], - "support": { - "issues": "https://github.com/amphp/dns/issues", - "source": "https://github.com/amphp/dns/tree/v2.4.0" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-01-19T15:43:40+00:00" - }, - { - "name": "amphp/parallel", - "version": "v2.3.2", - "source": { - "type": "git", - "url": "https://github.com/amphp/parallel.git", - "reference": "321b45ae771d9c33a068186b24117e3cd1c48dce" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/321b45ae771d9c33a068186b24117e3cd1c48dce", - "reference": "321b45ae771d9c33a068186b24117e3cd1c48dce", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/cache": "^2", - "amphp/parser": "^1", - "amphp/pipeline": "^1", - "amphp/process": "^2", - "amphp/serialization": "^1", - "amphp/socket": "^2", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" - }, - "type": "library", - "autoload": { - "files": [ - "src/Context/functions.php", - "src/Context/Internal/functions.php", - "src/Ipc/functions.php", - "src/Worker/functions.php" - ], - "psr-4": { - "Amp\\Parallel\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Stephen Coakley", - "email": "me@stephencoakley.com" - } - ], - "description": "Parallel processing component for Amp.", - "homepage": "https://github.com/amphp/parallel", - "keywords": [ - "async", - "asynchronous", - "concurrent", - "multi-processing", - "multi-threading" - ], - "support": { - "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.3.2" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-08-27T21:55:40+00:00" - }, - { - "name": "amphp/parser", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/parser.git", - "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", - "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", - "shasum": "" - }, - "require": { - "php": ">=7.4" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\Parser\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A generator parser to make streaming parsers simple.", - "homepage": "https://github.com/amphp/parser", - "keywords": [ - "async", - "non-blocking", - "parser", - "stream" - ], - "support": { - "issues": "https://github.com/amphp/parser/issues", - "source": "https://github.com/amphp/parser/tree/v1.1.1" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-03-21T19:16:53+00:00" - }, - { - "name": "amphp/pipeline", - "version": "v1.2.3", - "source": { - "type": "git", - "url": "https://github.com/amphp/pipeline.git", - "reference": "7b52598c2e9105ebcddf247fc523161581930367" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", - "reference": "7b52598c2e9105ebcddf247fc523161581930367", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "php": ">=8.1", - "revolt/event-loop": "^1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\Pipeline\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Asynchronous iterators and operators.", - "homepage": "https://amphp.org/pipeline", - "keywords": [ - "amp", - "amphp", - "async", - "io", - "iterator", - "non-blocking" - ], - "support": { - "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.2.3" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-03-16T16:33:53+00:00" - }, - { - "name": "amphp/process", - "version": "v2.0.3", - "source": { - "type": "git", - "url": "https://github.com/amphp/process.git", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Process\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A fiber-aware process manager based on Amp and Revolt.", - "homepage": "https://amphp.org/process", - "support": { - "issues": "https://github.com/amphp/process/issues", - "source": "https://github.com/amphp/process/tree/v2.0.3" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-04-19T03:13:44+00:00" - }, - { - "name": "amphp/serialization", - "version": "v1.0.0", - "source": { - "type": "git", - "url": "https://github.com/amphp/serialization.git", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "phpunit/phpunit": "^9 || ^8 || ^7" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Serialization\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Serialization tools for IPC and data storage in PHP.", - "homepage": "https://github.com/amphp/serialization", - "keywords": [ - "async", - "asynchronous", - "serialization", - "serialize" - ], - "support": { - "issues": "https://github.com/amphp/serialization/issues", - "source": "https://github.com/amphp/serialization/tree/master" - }, - "time": "2020-03-25T21:39:07+00:00" - }, - { - "name": "amphp/socket", - "version": "v2.3.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/socket.git", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/dns": "^2", - "ext-openssl": "*", - "kelunik/certificate": "^1.1", - "league/uri": "^6.5 | ^7", - "league/uri-interfaces": "^2.3 | ^7", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "amphp/process": "^2", - "phpunit/phpunit": "^9", - "psalm/phar": "5.20" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php", - "src/Internal/functions.php", - "src/SocketAddress/functions.php" - ], - "psr-4": { - "Amp\\Socket\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@gmail.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", - "homepage": "https://github.com/amphp/socket", - "keywords": [ - "amp", - "async", - "encryption", - "non-blocking", - "sockets", - "tcp", - "tls" - ], - "support": { - "issues": "https://github.com/amphp/socket/issues", - "source": "https://github.com/amphp/socket/tree/v2.3.1" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-04-21T14:33:03+00:00" - }, - { - "name": "amphp/sync", - "version": "v2.3.0", - "source": { - "type": "git", - "url": "https://github.com/amphp/sync.git", - "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", - "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/pipeline": "^1", - "amphp/serialization": "^1", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "5.23" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Sync\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Stephen Coakley", - "email": "me@stephencoakley.com" - } - ], - "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", - "homepage": "https://github.com/amphp/sync", - "keywords": [ - "async", - "asynchronous", - "mutex", - "semaphore", - "synchronization" - ], - "support": { - "issues": "https://github.com/amphp/sync/issues", - "source": "https://github.com/amphp/sync/tree/v2.3.0" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-08-03T19:31:26+00:00" - }, { "name": "anourvalar/eloquent-serialize", "version": "1.3.4", @@ -1124,16 +316,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.356.19", + "version": "3.356.23", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "4dbc87e689c4e4a6655d6b7e3d02ce2f4e87aa5c" + "reference": "e9253cf6073f06080a7458af54e18fc474f0c864" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/4dbc87e689c4e4a6655d6b7e3d02ce2f4e87aa5c", - "reference": "4dbc87e689c4e4a6655d6b7e3d02ce2f4e87aa5c", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e9253cf6073f06080a7458af54e18fc474f0c864", + "reference": "e9253cf6073f06080a7458af54e18fc474f0c864", "shasum": "" }, "require": { @@ -1215,9 +407,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.356.19" + "source": "https://github.com/aws/aws-sdk-php/tree/3.356.23" }, - "time": "2025-09-16T18:17:04+00:00" + "time": "2025-09-22T18:10:31+00:00" }, { "name": "blade-ui-kit/blade-heroicons", @@ -1712,16 +904,16 @@ }, { "name": "composer/composer", - "version": "2.8.11", + "version": "2.8.12", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "00e1a3396eea67033775c4a49c772376f45acd73" + "reference": "3e38919bc9a2c3c026f2151b5e56d04084ce8f0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/00e1a3396eea67033775c4a49c772376f45acd73", - "reference": "00e1a3396eea67033775c4a49c772376f45acd73", + "url": "https://api.github.com/repos/composer/composer/zipball/3e38919bc9a2c3c026f2151b5e56d04084ce8f0b", + "reference": "3e38919bc9a2c3c026f2151b5e56d04084ce8f0b", "shasum": "" }, "require": { @@ -1732,20 +924,20 @@ "composer/semver": "^3.3", "composer/spdx-licenses": "^1.5.7", "composer/xdebug-handler": "^2.0.2 || ^3.0.3", - "justinrainbow/json-schema": "^6.3.1", + "justinrainbow/json-schema": "^6.5.1", "php": "^7.2.5 || ^8.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "react/promise": "^2.11 || ^3.3", + "react/promise": "^3.3", "seld/jsonlint": "^1.4", "seld/phar-utils": "^1.2", "seld/signal-handler": "^2.0", - "symfony/console": "^5.4.35 || ^6.3.12 || ^7.0.3", - "symfony/filesystem": "^5.4.35 || ^6.3.12 || ^7.0.3", - "symfony/finder": "^5.4.35 || ^6.3.12 || ^7.0.3", + "symfony/console": "^5.4.47 || ^6.4.25 || ^7.1.10", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.1.10", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.1.10", "symfony/polyfill-php73": "^1.24", "symfony/polyfill-php80": "^1.24", "symfony/polyfill-php81": "^1.24", - "symfony/process": "^5.4.35 || ^6.3.12 || ^7.0.3" + "symfony/process": "^5.4.47 || ^6.4.25 || ^7.1.10" }, "require-dev": { "phpstan/phpstan": "^1.11.8", @@ -1753,7 +945,7 @@ "phpstan/phpstan-phpunit": "^1.4.0", "phpstan/phpstan-strict-rules": "^1.6.0", "phpstan/phpstan-symfony": "^1.4.0", - "symfony/phpunit-bridge": "^6.4.3 || ^7.0.1" + "symfony/phpunit-bridge": "^6.4.25 || ^7.3.3" }, "suggest": { "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", @@ -1806,7 +998,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.8.11" + "source": "https://github.com/composer/composer/tree/2.8.12" }, "funding": [ { @@ -1818,7 +1010,7 @@ "type": "github" } ], - "time": "2025-08-21T09:29:39+00:00" + "time": "2025-09-19T11:41:59+00:00" }, { "name": "composer/metadata-minifier", @@ -2344,85 +1536,41 @@ "illuminate/support": "^9.0|^10.0|^11.0|^12.0", "php": "^8.0" }, - "require-dev": { - "livewire/livewire": "^3.0", - "livewire/volt": "^1.3", - "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", - "phpunit/phpunit": "^9.0|^10.0|^11.5.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "DanHarrin\\LivewireRateLimiting\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dan Harrin", - "email": "dan@danharrin.com" - } - ], - "description": "Apply rate limiters to Laravel Livewire actions.", - "homepage": "https://github.com/danharrin/livewire-rate-limiting", - "support": { - "issues": "https://github.com/danharrin/livewire-rate-limiting/issues", - "source": "https://github.com/danharrin/livewire-rate-limiting" - }, - "funding": [ - { - "url": "https://github.com/danharrin", - "type": "github" - } - ], - "time": "2025-02-21T08:52:11+00:00" - }, - { - "name": "daverandom/libdns", - "version": "v2.1.0", - "source": { - "type": "git", - "url": "https://github.com/DaveRandom/LibDNS.git", - "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", - "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "Required for IDN support" + "require-dev": { + "livewire/livewire": "^3.0", + "livewire/volt": "^1.3", + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "phpunit/phpunit": "^9.0|^10.0|^11.5.3" }, "type": "library", "autoload": { - "files": [ - "src/functions.php" - ], "psr-4": { - "LibDNS\\": "src/" + "DanHarrin\\LivewireRateLimiting\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "DNS protocol implementation written in pure PHP", - "keywords": [ - "dns" + "authors": [ + { + "name": "Dan Harrin", + "email": "dan@danharrin.com" + } ], + "description": "Apply rate limiters to Laravel Livewire actions.", + "homepage": "https://github.com/danharrin/livewire-rate-limiting", "support": { - "issues": "https://github.com/DaveRandom/LibDNS/issues", - "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + "issues": "https://github.com/danharrin/livewire-rate-limiting/issues", + "source": "https://github.com/danharrin/livewire-rate-limiting" }, - "time": "2024-04-12T12:12:48+00:00" + "funding": [ + { + "url": "https://github.com/danharrin", + "type": "github" + } + ], + "time": "2025-02-21T08:52:11+00:00" }, { "name": "dflydev/dot-access-data", @@ -2954,7 +2102,7 @@ }, { "name": "filament/actions", - "version": "v3.3.38", + "version": "v3.3.39", "source": { "type": "git", "url": "https://github.com/filamentphp/actions.git", @@ -3007,7 +2155,7 @@ }, { "name": "filament/filament", - "version": "v3.3.38", + "version": "v3.3.39", "source": { "type": "git", "url": "https://github.com/filamentphp/panels.git", @@ -3072,7 +2220,7 @@ }, { "name": "filament/forms", - "version": "v3.3.38", + "version": "v3.3.39", "source": { "type": "git", "url": "https://github.com/filamentphp/forms.git", @@ -3128,7 +2276,7 @@ }, { "name": "filament/infolists", - "version": "v3.3.38", + "version": "v3.3.39", "source": { "type": "git", "url": "https://github.com/filamentphp/infolists.git", @@ -3179,7 +2327,7 @@ }, { "name": "filament/notifications", - "version": "v3.3.38", + "version": "v3.3.39", "source": { "type": "git", "url": "https://github.com/filamentphp/notifications.git", @@ -3231,7 +2379,7 @@ }, { "name": "filament/spatie-laravel-media-library-plugin", - "version": "v3.3.38", + "version": "v3.3.39", "source": { "type": "git", "url": "https://github.com/filamentphp/spatie-laravel-media-library-plugin.git", @@ -3269,7 +2417,7 @@ }, { "name": "filament/spatie-laravel-translatable-plugin", - "version": "v3.3.38", + "version": "v3.3.39", "source": { "type": "git", "url": "https://github.com/filamentphp/spatie-laravel-translatable-plugin.git", @@ -3314,7 +2462,7 @@ }, { "name": "filament/support", - "version": "v3.3.38", + "version": "v3.3.39", "source": { "type": "git", "url": "https://github.com/filamentphp/support.git", @@ -3373,16 +2521,16 @@ }, { "name": "filament/tables", - "version": "v3.3.38", + "version": "v3.3.39", "source": { "type": "git", "url": "https://github.com/filamentphp/tables.git", - "reference": "20ce6217382785df7b39b8473644c1bfe967963c" + "reference": "2e1e3aeeeccd6b74e5d038325af52635d1108e4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/tables/zipball/20ce6217382785df7b39b8473644c1bfe967963c", - "reference": "20ce6217382785df7b39b8473644c1bfe967963c", + "url": "https://api.github.com/repos/filamentphp/tables/zipball/2e1e3aeeeccd6b74e5d038325af52635d1108e4c", + "reference": "2e1e3aeeeccd6b74e5d038325af52635d1108e4c", "shasum": "" }, "require": { @@ -3421,11 +2569,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-08-12T13:15:31+00:00" + "time": "2025-09-17T10:47:13+00:00" }, { "name": "filament/widgets", - "version": "v3.3.38", + "version": "v3.3.39", "source": { "type": "git", "url": "https://github.com/filamentphp/widgets.git", @@ -4713,64 +3861,6 @@ }, "time": "2025-09-09T09:42:27+00:00" }, - { - "name": "kelunik/certificate", - "version": "v1.1.3", - "source": { - "type": "git", - "url": "https://github.com/kelunik/certificate.git", - "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", - "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", - "shasum": "" - }, - "require": { - "ext-openssl": "*", - "php": ">=7.0" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "phpunit/phpunit": "^6 | 7 | ^8 | ^9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Kelunik\\Certificate\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Access certificate details and transform between different formats.", - "keywords": [ - "DER", - "certificate", - "certificates", - "openssl", - "pem", - "x509" - ], - "support": { - "issues": "https://github.com/kelunik/certificate/issues", - "source": "https://github.com/kelunik/certificate/tree/v1.1.3" - }, - "time": "2023-02-03T21:26:53+00:00" - }, { "name": "kirschbaum-development/eloquent-power-joins", "version": "4.2.8", @@ -5097,16 +4187,16 @@ }, { "name": "laravel/framework", - "version": "v12.29.0", + "version": "v12.30.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "a9e4c73086f5ba38383e9c1d74b84fe46aac730b" + "reference": "7f61e8679f9142f282a0184ac7ef9e3834bfd023" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/a9e4c73086f5ba38383e9c1d74b84fe46aac730b", - "reference": "a9e4c73086f5ba38383e9c1d74b84fe46aac730b", + "url": "https://api.github.com/repos/laravel/framework/zipball/7f61e8679f9142f282a0184ac7ef9e3834bfd023", + "reference": "7f61e8679f9142f282a0184ac7ef9e3834bfd023", "shasum": "" }, "require": { @@ -5134,7 +4224,7 @@ "monolog/monolog": "^3.0", "nesbot/carbon": "^3.8.4", "nunomaduro/termwind": "^2.0", - "phiki/phiki": "v2.0.0", + "phiki/phiki": "^2.0.0", "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", @@ -5313,7 +4403,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-09-16T14:15:03+00:00" + "time": "2025-09-18T21:07:07+00:00" }, { "name": "laravel/helpers", @@ -5374,16 +4464,16 @@ }, { "name": "laravel/nightwatch", - "version": "v1.13.6", + "version": "v1.13.7", "source": { "type": "git", "url": "https://github.com/laravel/nightwatch.git", - "reference": "0dbf0329e6c99b797a66895f261340ea26c836e8" + "reference": "27d7b307c81db97de48c31a7bd3700e32541b03b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/nightwatch/zipball/0dbf0329e6c99b797a66895f261340ea26c836e8", - "reference": "0dbf0329e6c99b797a66895f261340ea26c836e8", + "url": "https://api.github.com/repos/laravel/nightwatch/zipball/27d7b307c81db97de48c31a7bd3700e32541b03b", + "reference": "27d7b307c81db97de48c31a7bd3700e32541b03b", "shasum": "" }, "require": { @@ -5457,7 +4547,7 @@ "issues": "https://github.com/laravel/nightwatch/issues", "source": "https://github.com/laravel/nightwatch" }, - "time": "2025-09-16T02:23:53+00:00" + "time": "2025-09-20T07:10:50+00:00" }, { "name": "laravel/octane", @@ -8216,24 +7306,26 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v3.0.0", + "version": "v3.1.0", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + "reference": "5cba5793151a917ce03cbc0452b375fa4008849e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/5cba5793151a917ce03cbc0452b375fa4008849e", + "reference": "5cba5793151a917ce03cbc0452b375fa4008849e", "shasum": "" }, "require": { "php": "^8" }, "require-dev": { - "phpunit/phpunit": "^9", - "vimeo/psalm": "^4|^5" + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" }, "type": "library", "autoload": { @@ -8279,7 +7371,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2024-05-08T12:36:18+00:00" + "time": "2025-09-22T19:58:27+00:00" }, { "name": "paragonie/random_compat", @@ -8333,16 +7425,16 @@ }, { "name": "phiki/phiki", - "version": "v2.0.0", + "version": "v2.0.4", "source": { "type": "git", "url": "https://github.com/phikiphp/phiki.git", - "reference": "461f6dd7e91dc3a95463b42f549ac7d0aab4702f" + "reference": "160785c50c01077780ab217e5808f00ab8f05a13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phikiphp/phiki/zipball/461f6dd7e91dc3a95463b42f549ac7d0aab4702f", - "reference": "461f6dd7e91dc3a95463b42f549ac7d0aab4702f", + "url": "https://api.github.com/repos/phikiphp/phiki/zipball/160785c50c01077780ab217e5808f00ab8f05a13", + "reference": "160785c50c01077780ab217e5808f00ab8f05a13", "shasum": "" }, "require": { @@ -8388,7 +7480,7 @@ "description": "Syntax highlighting using TextMate grammars in PHP.", "support": { "issues": "https://github.com/phikiphp/phiki/issues", - "source": "https://github.com/phikiphp/phiki/tree/v2.0.0" + "source": "https://github.com/phikiphp/phiki/tree/v2.0.4" }, "funding": [ { @@ -8400,7 +7492,7 @@ "type": "other" } ], - "time": "2025-08-28T18:20:27+00:00" + "time": "2025-09-20T17:21:02+00:00" }, { "name": "phpdocumentor/reflection", @@ -8881,6 +7973,69 @@ }, "time": "2025-08-30T15:50:23+00:00" }, + { + "name": "predis/predis", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/predis/predis.git", + "reference": "9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/predis/predis/zipball/9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8", + "reference": "9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.0|^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.3", + "phpstan/phpstan": "^1.9", + "phpunit/phpcov": "^6.0 || ^8.0", + "phpunit/phpunit": "^8.0 || ~9.4.4" + }, + "suggest": { + "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Till Krüss", + "homepage": "https://till.im", + "role": "Maintainer" + } + ], + "description": "A flexible and feature-complete Redis/Valkey client for PHP.", + "homepage": "http://github.com/predis/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "support": { + "issues": "https://github.com/predis/predis/issues", + "source": "https://github.com/predis/predis/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/tillkruss", + "type": "github" + } + ], + "time": "2025-08-06T06:41:24+00:00" + }, { "name": "propaganistas/laravel-phone", "version": "6.0.2", @@ -9416,16 +8571,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.10", + "version": "v0.12.12", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" + "reference": "cd23863404a40ccfaf733e3af4db2b459837f7e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", - "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/cd23863404a40ccfaf733e3af4db2b459837f7e7", + "reference": "cd23863404a40ccfaf733e3af4db2b459837f7e7", "shasum": "" }, "require": { @@ -9488,9 +8643,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.12" }, - "time": "2025-08-04T12:39:37+00:00" + "time": "2025-09-20T13:46:31+00:00" }, { "name": "ralouphie/getallheaders", @@ -9763,78 +8918,6 @@ ], "time": "2025-08-19T18:57:03+00:00" }, - { - "name": "revolt/event-loop", - "version": "v1.0.7", - "source": { - "type": "git", - "url": "https://github.com/revoltphp/event-loop.git", - "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", - "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "require-dev": { - "ext-json": "*", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.15" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Revolt\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "ceesjank@gmail.com" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Rock-solid event loop for concurrent PHP applications.", - "keywords": [ - "async", - "asynchronous", - "concurrency", - "event", - "event-loop", - "non-blocking", - "scheduler" - ], - "support": { - "issues": "https://github.com/revoltphp/event-loop/issues", - "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" - }, - "time": "2025-01-25T19:27:39+00:00" - }, { "name": "ryangjchandler/blade-capture-directive", "version": "v1.1.0", @@ -10717,100 +9800,18 @@ ], "time": "2025-04-17T09:49:53+00:00" }, - { - "name": "spatie/laravel-google-fonts", - "version": "1.4.4", - "source": { - "type": "git", - "url": "https://github.com/spatie/laravel-google-fonts.git", - "reference": "f37fc6100912bcfeac6a503692745b2dee258aa5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-google-fonts/zipball/f37fc6100912bcfeac6a503692745b2dee258aa5", - "reference": "f37fc6100912bcfeac6a503692745b2dee258aa5", - "shasum": "" - }, - "require": { - "guzzlehttp/guzzle": "^7.3|^7.2", - "illuminate/contracts": "^8.37|^9.0|^10.0|^11.0|^12.0", - "php": "^8.0", - "spatie/laravel-package-tools": "^1.7.0" - }, - "require-dev": { - "brianium/paratest": "^6.3|^7.4", - "nunomaduro/collision": "^5.4|^6.0|^8.0", - "orchestra/testbench": "^6.17|^7.0|^8.0|^9.0|^10.0", - "pestphp/pest": "^1.22|^2.34|^3.7", - "spatie/laravel-ray": "^1.17", - "spatie/pest-plugin-snapshots": "^1.1|^2.1", - "spatie/phpunit-snapshot-assertions": "^4.2|^5.1" - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "GoogleFonts": "Spatie\\GoogleFonts\\GoogleFontsFacade" - }, - "providers": [ - "Spatie\\GoogleFonts\\GoogleFontsServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Spatie\\GoogleFonts\\": "src", - "Spatie\\GoogleFonts\\Database\\Factories\\": "database/factories" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Sebastian De Deyne", - "email": "sebastian@spatie.be", - "role": "Developer" - }, - { - "name": "Freek Van der herten", - "email": "freek@spatie.be", - "role": "Developer" - } - ], - "description": "Manage self-hosted Google Fonts in Laravel apps", - "homepage": "https://github.com/spatie/laravel-google-fonts", - "keywords": [ - "google fonts", - "laravel", - "laravel-google-fonts", - "spatie" - ], - "support": { - "issues": "https://github.com/spatie/laravel-google-fonts/issues", - "source": "https://github.com/spatie/laravel-google-fonts/tree/1.4.4" - }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2025-09-08T07:47:12+00:00" - }, { "name": "spatie/laravel-medialibrary", - "version": "11.14.0", + "version": "11.15.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-medialibrary.git", - "reference": "7050a0d041be8c5c5ef5886967fbbbe578a54296" + "reference": "9d1e9731d36817d1649bc584b2c40c0c9d4bcfac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/7050a0d041be8c5c5ef5886967fbbbe578a54296", - "reference": "7050a0d041be8c5c5ef5886967fbbbe578a54296", + "url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/9d1e9731d36817d1649bc584b2c40c0c9d4bcfac", + "reference": "9d1e9731d36817d1649bc584b2c40c0c9d4bcfac", "shasum": "" }, "require": { @@ -10848,6 +9849,7 @@ "phpstan/extension-installer": "^1.3.1", "spatie/laravel-ray": "^1.33", "spatie/pdf-to-image": "^2.2|^3.0", + "spatie/pest-expectations": "^1.13", "spatie/pest-plugin-snapshots": "^2.1" }, "suggest": { @@ -10894,7 +9896,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-medialibrary/issues", - "source": "https://github.com/spatie/laravel-medialibrary/tree/11.14.0" + "source": "https://github.com/spatie/laravel-medialibrary/tree/11.15.0" }, "funding": [ { @@ -10906,7 +9908,7 @@ "type": "github" } ], - "time": "2025-08-19T08:01:56+00:00" + "time": "2025-09-19T06:51:45+00:00" }, { "name": "spatie/laravel-package-tools", @@ -11269,27 +10271,26 @@ }, { "name": "spatie/php-structure-discoverer", - "version": "2.3.1", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/spatie/php-structure-discoverer.git", - "reference": "42f4d731d3dd4b3b85732e05a8c1928fcfa2f4bc" + "reference": "6c46e069349c7f2f6ebbe00429332c9e6b70fa92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/42f4d731d3dd4b3b85732e05a8c1928fcfa2f4bc", - "reference": "42f4d731d3dd4b3b85732e05a8c1928fcfa2f4bc", + "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/6c46e069349c7f2f6ebbe00429332c9e6b70fa92", + "reference": "6c46e069349c7f2f6ebbe00429332c9e6b70fa92", "shasum": "" }, "require": { - "amphp/amp": "^v3.0", - "amphp/parallel": "^2.2", "illuminate/collections": "^10.0|^11.0|^12.0", "php": "^8.1", "spatie/laravel-package-tools": "^1.4.3", "symfony/finder": "^6.0|^7.0" }, "require-dev": { + "amphp/parallel": "^2.2", "illuminate/console": "^10.0|^11.0|^12.0", "laravel/pint": "^1.0", "nunomaduro/collision": "^7.0|^8.0", @@ -11302,6 +10303,9 @@ "phpunit/phpunit": "^9.5|^10.0|^11.5.3", "spatie/laravel-ray": "^1.26" }, + "suggest": { + "amphp/parallel": "When you want to use the Parallel discover worker" + }, "type": "library", "extra": { "laravel": { @@ -11336,7 +10340,7 @@ ], "support": { "issues": "https://github.com/spatie/php-structure-discoverer/issues", - "source": "https://github.com/spatie/php-structure-discoverer/tree/2.3.1" + "source": "https://github.com/spatie/php-structure-discoverer/tree/2.3.2" }, "funding": [ { @@ -11344,20 +10348,20 @@ "type": "github" } ], - "time": "2025-02-14T10:18:38+00:00" + "time": "2025-09-22T14:58:17+00:00" }, { "name": "spatie/robots-txt", - "version": "2.5.1", + "version": "2.5.2", "source": { "type": "git", "url": "https://github.com/spatie/robots-txt.git", - "reference": "ef85dfaa48372c0a7fdfb144592f95de1a2e9b79" + "reference": "1b59dde3fd4e1b71967b40841369c6e9779282f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/robots-txt/zipball/ef85dfaa48372c0a7fdfb144592f95de1a2e9b79", - "reference": "ef85dfaa48372c0a7fdfb144592f95de1a2e9b79", + "url": "https://api.github.com/repos/spatie/robots-txt/zipball/1b59dde3fd4e1b71967b40841369c6e9779282f3", + "reference": "1b59dde3fd4e1b71967b40841369c6e9779282f3", "shasum": "" }, "require": { @@ -11392,7 +10396,7 @@ ], "support": { "issues": "https://github.com/spatie/robots-txt/issues", - "source": "https://github.com/spatie/robots-txt/tree/2.5.1" + "source": "https://github.com/spatie/robots-txt/tree/2.5.2" }, "funding": [ { @@ -11404,7 +10408,7 @@ "type": "github" } ], - "time": "2025-07-01T07:07:44+00:00" + "time": "2025-09-19T10:37:01+00:00" }, { "name": "spatie/temporary-directory", @@ -15783,16 +14787,16 @@ }, { "name": "larastan/larastan", - "version": "v3.7.1", + "version": "v3.7.2", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "2e653fd19585a825e283b42f38378b21ae481cc7" + "reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/2e653fd19585a825e283b42f38378b21ae481cc7", - "reference": "2e653fd19585a825e283b42f38378b21ae481cc7", + "url": "https://api.github.com/repos/larastan/larastan/zipball/a761859a7487bd7d0cb8b662a7538a234d5bb5ae", + "reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae", "shasum": "" }, "require": { @@ -15806,7 +14810,7 @@ "illuminate/pipeline": "^11.44.2 || ^12.4.1", "illuminate/support": "^11.44.2 || ^12.4.1", "php": "^8.2", - "phpstan/phpstan": "^2.1.23" + "phpstan/phpstan": "^2.1.28" }, "require-dev": { "doctrine/coding-standard": "^13", @@ -15860,7 +14864,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.7.1" + "source": "https://github.com/larastan/larastan/tree/v3.7.2" }, "funding": [ { @@ -15868,39 +14872,39 @@ "type": "github" } ], - "time": "2025-09-10T19:42:11+00:00" + "time": "2025-09-19T09:03:05+00:00" }, { "name": "laravel/boost", - "version": "v1.1.4", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "70f909465bf73dad7e791fad8b7716b3b2712076" + "reference": "85f7de54a6b60f684fc9f7f6df5ad94f4f7d0d24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/70f909465bf73dad7e791fad8b7716b3b2712076", - "reference": "70f909465bf73dad7e791fad8b7716b3b2712076", + "url": "https://api.github.com/repos/laravel/boost/zipball/85f7de54a6b60f684fc9f7f6df5ad94f4f7d0d24", + "reference": "85f7de54a6b60f684fc9f7f6df5ad94f4f7d0d24", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "^7.9", - "illuminate/console": "^10.0|^11.0|^12.0", - "illuminate/contracts": "^10.0|^11.0|^12.0", - "illuminate/routing": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "laravel/mcp": "^0.1.1", - "laravel/prompts": "^0.1.9|^0.3", - "laravel/roster": "^0.2.5", + "guzzlehttp/guzzle": "^7.10", + "illuminate/console": "^10.49.0|^11.45.3|^12.28.1", + "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", + "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", + "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", + "laravel/mcp": "^0.2.0", + "laravel/prompts": "0.1.25|^0.3.6", + "laravel/roster": "^0.2.6", "php": "^8.1" }, "require-dev": { - "laravel/pint": "^1.14", - "mockery/mockery": "^1.6", - "orchestra/testbench": "^8.22.0|^9.0|^10.0", - "pestphp/pest": "^2.0|^3.0", - "phpstan/phpstan": "^2.0" + "laravel/pint": "1.20", + "mockery/mockery": "^1.6.12", + "orchestra/testbench": "^8.36.0|^9.15.0|^10.6", + "pestphp/pest": "^2.36.0|^3.8.4", + "phpstan/phpstan": "^2.1.27" }, "type": "library", "extra": { @@ -15922,7 +14926,7 @@ "license": [ "MIT" ], - "description": "Laravel Boost accelerates AI-assisted development to generate high-quality, Laravel-specific code.", + "description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.", "homepage": "https://github.com/laravel/boost", "keywords": [ "ai", @@ -15933,7 +14937,7 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2025-09-04T12:16:09+00:00" + "time": "2025-09-18T13:05:07+00:00" }, { "name": "laravel/breeze", @@ -15998,31 +15002,37 @@ }, { "name": "laravel/mcp", - "version": "v0.1.1", + "version": "v0.2.0", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713" + "reference": "56fade6882756d5828cc90b86611d29616c2d754" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/6d6284a491f07c74d34f48dfd999ed52c567c713", - "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713", + "url": "https://api.github.com/repos/laravel/mcp/zipball/56fade6882756d5828cc90b86611d29616c2d754", + "reference": "56fade6882756d5828cc90b86611d29616c2d754", "shasum": "" }, "require": { - "illuminate/console": "^10.0|^11.0|^12.0", - "illuminate/contracts": "^10.0|^11.0|^12.0", - "illuminate/http": "^10.0|^11.0|^12.0", - "illuminate/routing": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "illuminate/validation": "^10.0|^11.0|^12.0", - "php": "^8.1|^8.2" + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/console": "^10.49.0|^11.45.3|^12.28.1", + "illuminate/container": "^10.49.0|^11.45.3|^12.28.1", + "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", + "illuminate/http": "^10.49.0|^11.45.3|^12.28.1", + "illuminate/json-schema": "^12.28.1", + "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", + "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", + "illuminate/validation": "^10.49.0|^11.45.3|^12.28.1", + "php": "^8.1" }, "require-dev": { - "laravel/pint": "^1.14", - "orchestra/testbench": "^8.22.0|^9.0|^10.0", - "phpstan/phpstan": "^2.0" + "laravel/pint": "1.20.0", + "orchestra/testbench": "^8.36.0|^9.15.0|^10.6.0", + "pestphp/pest": "^2.36.0|^3.8.4|^4.1.0", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.1.7" }, "type": "library", "extra": { @@ -16038,8 +15048,6 @@ "autoload": { "psr-4": { "Laravel\\Mcp\\": "src/", - "Workbench\\App\\": "workbench/app/", - "Laravel\\Mcp\\Tests\\": "tests/", "Laravel\\Mcp\\Server\\": "src/Server/" } }, @@ -16047,10 +15055,15 @@ "license": [ "MIT" ], - "description": "The easiest way to add MCP servers to your Laravel app.", + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Rapidly build MCP servers for your Laravel applications.", "homepage": "https://github.com/laravel/mcp", "keywords": [ - "dev", "laravel", "mcp" ], @@ -16058,7 +15071,7 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2025-08-16T09:50:43+00:00" + "time": "2025-09-18T12:58:47+00:00" }, { "name": "laravel/pail", @@ -16141,16 +15154,16 @@ }, { "name": "laravel/pint", - "version": "v1.25.0", + "version": "v1.25.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96" + "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96", - "reference": "595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96", + "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", + "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", "shasum": "" }, "require": { @@ -16203,20 +15216,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-09-17T01:36:44+00:00" + "time": "2025-09-19T02:57:12+00:00" }, { "name": "laravel/roster", - "version": "v0.2.6", + "version": "v0.2.8", "source": { "type": "git", "url": "https://github.com/laravel/roster.git", - "reference": "5615acdf860c5a5c61d04aba44f2d3312550c514" + "reference": "832a6db43743bf08a58691da207f977ec8dc43aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/roster/zipball/5615acdf860c5a5c61d04aba44f2d3312550c514", - "reference": "5615acdf860c5a5c61d04aba44f2d3312550c514", + "url": "https://api.github.com/repos/laravel/roster/zipball/832a6db43743bf08a58691da207f977ec8dc43aa", + "reference": "832a6db43743bf08a58691da207f977ec8dc43aa", "shasum": "" }, "require": { @@ -16264,7 +15277,7 @@ "issues": "https://github.com/laravel/roster/issues", "source": "https://github.com/laravel/roster" }, - "time": "2025-09-04T07:31:39+00:00" + "time": "2025-09-22T13:28:47+00:00" }, { "name": "laravel/sail", @@ -17228,16 +16241,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.26", + "version": "2.1.28", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "b13345001a8553ec405b7741be0c6b8d7f8b5bf5" + "reference": "578fa296a166605d97b94091f724f1257185d278" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b13345001a8553ec405b7741be0c6b8d7f8b5bf5", - "reference": "b13345001a8553ec405b7741be0c6b8d7f8b5bf5", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/578fa296a166605d97b94091f724f1257185d278", + "reference": "578fa296a166605d97b94091f724f1257185d278", "shasum": "" }, "require": { @@ -17282,7 +16295,7 @@ "type": "github" } ], - "time": "2025-09-16T11:33:46+00:00" + "time": "2025-09-19T08:58:49+00:00" }, { "name": "phpunit/php-code-coverage", @@ -18253,16 +17266,16 @@ }, { "name": "sebastian/exporter", - "version": "6.3.0", + "version": "6.3.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + "reference": "8f67e53d3fcaf53105f95cc14f1630493d0fa2e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/8f67e53d3fcaf53105f95cc14f1630493d0fa2e6", + "reference": "8f67e53d3fcaf53105f95cc14f1630493d0fa2e6", "shasum": "" }, "require": { @@ -18276,7 +17289,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.1-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -18319,15 +17332,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-12-05T09:17:50+00:00" + "time": "2025-09-22T05:34:00+00:00" }, { "name": "sebastian/global-state", diff --git a/config/google-fonts.php b/config/google-fonts.php deleted file mode 100644 index 6646df06..00000000 --- a/config/google-fonts.php +++ /dev/null @@ -1,46 +0,0 @@ - [ - 'default' => 'https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=Lexend:wght@700&family=JetBrains+Mono', - ], - - /* - * This disk will be used to store local Google Fonts. The public disk - * is the default because it can be served over HTTP with storage:link. - */ - 'disk' => 'public', - - /* - * Prepend all files that are written to the selected disk with this path. - * This allows separating the fonts from other data in the public disk. - */ - 'path' => 'fonts', - - /* - * By default, CSS will be inlined to reduce the amount of round trips - * browsers need to make in order to load the requested font files. - */ - 'inline' => true, - - /* - * When something goes wrong fonts are loaded directly from Google. - * With fallback disabled, this package will throw an exception. - */ - 'fallback' => ! env('APP_DEBUG'), - - /* - * This user agent will be used to request the stylesheet from Google Fonts. - * This is the Safari 14 user agent that only targets modern browsers. If - * you want to target older browsers, use different user agent string. - */ - 'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15', - -]; diff --git a/config/lcm.php b/config/lcm.php index f034385c..b6b4aa1e 100644 --- a/config/lcm.php +++ b/config/lcm.php @@ -3,7 +3,13 @@ declare(strict_types=1); return [ - + /* + |-------------------------------------------------------------------------- + | Free Sharable Ads + |-------------------------------------------------------------------------- + | + | + */ 'ads' => [ [ 'url' => 'https://github.com/mckenziearts/laravel-notify/?utm_source=laravel.cm&utm_medium=sidebar-widget', @@ -19,12 +25,45 @@ ], ], - 'supported_locales' => ['fr', 'en'], + /* + |-------------------------------------------------------------------------- + | Email Support + |-------------------------------------------------------------------------- + | + */ + 'members' => [ + [ + 'name' => 'Arthur Monney', + 'title' => 'Développeur Fullstack', + 'avatar' => 'https://avatars.githubusercontent.com/u/14105989?v=4', + 'social_links' => [ + 'twitter' => 'https://twitter.com/MonneyArthur', + 'github' => 'https://github.com/mckenziearts', + 'linkedin' => 'https://www.linkedin.com/in/arthurmonney', + ], + ], + [ + 'name' => 'Fabrice Yopa', + 'title' => 'Co-Founder & CTO IS Dev Experts', + 'avatar' => 'https://avatars.githubusercontent.com/u/4902424?v=4', + 'social_links' => [ + 'twitter' => 'https://twitter.com/yopafabrice', + 'github' => 'https://github.com/fabriceyopa', + 'linkedin' => 'https://www.linkedin.com/in/fabriceyopa', + ], + ], + ], - 'spa_url' => env('FRONTEND_APP_URL', 'http://localhost:4200'), + 'supported_locales' => ['fr', 'en'], 'notch-pay-public-token' => env('NOTCHPAY_PUBLIC_KEY', null), + /* + |-------------------------------------------------------------------------- + | Email Support + |-------------------------------------------------------------------------- + | + */ 'support_email' => env('MAIL_SUPPORT', 'support@laravel.cm'), ]; diff --git a/config/services.php b/config/services.php index 2038ee4f..b821149c 100644 --- a/config/services.php +++ b/config/services.php @@ -57,13 +57,8 @@ 'channel' => env('TELEGRAM_CHANNEL'), ], - 'google' => [ - 'recaptcha' => [ - 'site_key' => env('GOOGLE_RECAPTCHA_SITE_KEY'), - 'secret_key' => env('GOOGLE_RECAPTCHA_SECRET_KEY'), - 'version' => 'v3', - 'score' => 0.5, - ], + 'notch-pay' => [ + 'public_key' => env('NOTCHPAY_PUBLIC_KEY'), ], ]; diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 0788242e..a68a0f8a 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -21,6 +21,7 @@ public function definition(): array 'email_verified_at' => now(), 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'remember_token' => Str::random(10), + 'avatar_type' => 'avatar', ]; } diff --git a/database/migrations/2024_11_04_194431_add_description_columns_to_channels_table.php b/database/migrations/2024_11_04_194431_add_description_columns_to_channels_table.php index b236c930..84b022df 100644 --- a/database/migrations/2024_11_04_194431_add_description_columns_to_channels_table.php +++ b/database/migrations/2024_11_04_194431_add_description_columns_to_channels_table.php @@ -17,7 +17,7 @@ public function up(): void public function down(): void { - Schema::table('channels', function (Blueprint $table): void { + Schema::table('channels', static function (Blueprint $table): void { $table->dropColumn('description'); }); } diff --git a/database/migrations/2025_09_20_183656_convert_project_json_columns_to_jsonb.php b/database/migrations/2025_09_20_183656_convert_project_json_columns_to_jsonb.php new file mode 100644 index 00000000..de39e2b9 --- /dev/null +++ b/database/migrations/2025_09_20_183656_convert_project_json_columns_to_jsonb.php @@ -0,0 +1,74 @@ +jsonb('concerns')->change(); + }); + + Schema::table('channels', static function (Blueprint $table): void { + $table->jsonb('description')->nullable()->change(); + }); + + Schema::table('users', static function (Blueprint $table): void { + $table->jsonb('settings')->nullable()->change(); + }); + + Schema::table('plans', static function (Blueprint $table): void { + $table->jsonb('name')->change(); + $table->jsonb('description')->nullable()->change(); + }); + + Schema::table('enterprises', static function (Blueprint $table): void { + $table->jsonb('settings')->nullable()->change(); + }); + + Schema::table('activities', static function (Blueprint $table): void { + $table->jsonb('data')->nullable()->change(); + }); + + Schema::table('transactions', static function (Blueprint $table): void { + $table->jsonb('metadata')->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('tags', static function (Blueprint $table): void { + $table->json('concerns')->change(); + }); + + Schema::table('channels', static function (Blueprint $table): void { + $table->json('description')->nullable()->change(); + }); + + Schema::table('users', static function (Blueprint $table): void { + $table->json('settings')->nullable()->change(); + }); + + Schema::table('plans', static function (Blueprint $table): void { + $table->json('name')->change(); + $table->json('description')->nullable()->change(); + }); + + Schema::table('enterprises', static function (Blueprint $table): void { + $table->json('settings')->nullable()->change(); + }); + + Schema::table('activities', static function (Blueprint $table): void { + $table->json('data')->nullable()->change(); + }); + + Schema::table('transactions', static function (Blueprint $table): void { + $table->json('metadata')->nullable()->change(); + }); + } +}; diff --git a/docker-compose.yml b/docker-compose.yml index 67b6dbb9..7d70476f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: traefik: - image: 'traefik:v3.5' + image: traefik:v3.5 command: - '--api.dashboard=true' - '--api.insecure=true' @@ -9,11 +9,13 @@ services: - '--providers.docker.network=traefik' - '--entrypoints.web.address=:80' - '--entrypoints.websecure.address=:443' + - '--entrypoints.devtools.address=:8000' - '--serverstransport.insecureskipverify=true' ports: - '80:80' - '443:443' - '8080:8080' + - '8000:8000' volumes: - '/var/run/docker.sock:/var/run/docker.sock:ro' - 'traefik-letsencrypt:/letsencrypt' @@ -79,11 +81,11 @@ services: - traefik labels: - traefik.enable=true - - 'traefik.http.routers.laravelcm.rule=Host(`${APP_DOMAIN:-laravelcm.local}`)' + - traefik.http.routers.laravelcm.rule=Host(`${APP_DOMAIN:-laravelcm.local}`) - traefik.http.routers.laravelcm.entrypoints=websecure - traefik.http.routers.laravelcm.tls=true - - 'traefik.http.services.laravelcm.loadbalancer.server.port=${APP_PORT:-8080}' - - 'traefik.http.routers.laravelcm-insecure.rule=Host(`${APP_DOMAIN:-laravelcm.local}`)' + - traefik.http.services.laravelcm.loadbalancer.server.port=${APP_PORT:-8080} + - traefik.http.routers.laravelcm-insecure.rule=Host(`${APP_DOMAIN:-laravelcm.local}`) - traefik.http.routers.laravelcm-insecure.entrypoints=web - traefik.http.routers.laravelcm-insecure.middlewares=redirect-to-https - traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https @@ -225,6 +227,18 @@ services: - '${PUSHER_METRICS_PORT:-9601}:9601' networks: - sail + buggregator: + image: ghcr.io/buggregator/server:latest + labels: + - traefik.enable=true + - traefik.http.routers.laravelcm-buggregator.rule=Host(`${APP_DOMAIN:-laravelcm.local}`) + - traefik.http.routers.laravelcm-buggregator.entrypoints=devtools + - traefik.http.routers.laravelcm-buggregator.tls=true + - traefik.http.routers.laravelcm-buggregator.service=laravelcm-buggregator + - traefik.http.services.laravelcm-buggregator.loadbalancer.server.port=8000 + networks: + - sail + - traefik networks: sail: driver: bridge diff --git a/lang/en/pages/about.php b/lang/en/pages/about.php index 8342b6c9..491dcc49 100644 --- a/lang/en/pages/about.php +++ b/lang/en/pages/about.php @@ -7,8 +7,7 @@ 'title' => 'About us', 'description' => 'We build an Open Source community of learners and teachers', 'second_description_part_one' => 'Everyone teaches, everyone learns', - 'second_description_part_two' => '. That\'s the spirit behind the community. A community that aims to grow and - gives everyone the chance to share their knowledge and learn.', + 'second_description_part_two' => '. That\'s the spirit behind the community. A community that aims to grow and gives everyone the chance to share their knowledge and learn.', 'stats' => [ 'member_discord' => 'Members on Discord', @@ -20,8 +19,7 @@ 'first_description' => 'Launched in June 2018, Laravel CM quickly began to grow and kick off its activities activities with a first Meetup for its overall presentation and objectives. This Meetup registered over 100 participants', - 'second_description' => 'During this event, we recorded the participation of companies such as - such as:', + 'second_description' => 'During this event, we recorded the participation of companies such as:', 'list' => [ 'one' => [ 'title' => 'incubator', @@ -51,8 +49,7 @@ ], 'our_team' => [ - 'title' => 'Meet the Team', + 'title' => 'The Team', 'description' => 'Laravel Cameroun is a Meetup idea that was initiated and then transformed into a community of developers.', ], - ]; diff --git a/lang/en/pages/discussion.php b/lang/en/pages/discussion.php index 2e7ce3b7..68e53264 100644 --- a/lang/en/pages/discussion.php +++ b/lang/en/pages/discussion.php @@ -6,7 +6,7 @@ 'title' => 'All discussion topics', 'contributors' => [ - 'top' => 'Top Contributors', + 'top' => 'Top contributors', 'description' => 'The people who started the most discussions on the site.', ], 'empty' => 'Discussions without comments', diff --git a/lang/fr/pages/about.php b/lang/fr/pages/about.php index b6fe4878..09db3db9 100644 --- a/lang/fr/pages/about.php +++ b/lang/fr/pages/about.php @@ -51,7 +51,7 @@ ], 'our_team' => [ - 'title' => 'Voir l\'équipe', + 'title' => 'Contributeurs', 'description' => ' Laravel Cameroun est une idée de Meetup qui a été initiée puis transformée en une communauté de développeurs', ], diff --git a/lang/fr/pages/discussion.php b/lang/fr/pages/discussion.php index 0ef633c7..98152bd6 100644 --- a/lang/fr/pages/discussion.php +++ b/lang/fr/pages/discussion.php @@ -6,7 +6,7 @@ 'title' => 'Tous les sujets de discussion', 'contributors' => [ - 'top' => 'Top Contributeurs', + 'top' => 'Top contributeurs', 'description' => 'Les personnes qui ont lancé le plus de discussions sur le site.', ], 'empty' => 'Discussions sans commentaires', diff --git a/package-lock.json b/package-lock.json index 5534945e..c341f394 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,9 @@ "requires": true, "packages": { "": { + "dependencies": { + "@tailwindplus/elements": "^1.0.13" + }, "devDependencies": { "@alpinejs/collapse": "^3.14.3", "@alpinejs/intersect": "^3.6.1", @@ -2222,6 +2225,12 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@tailwindplus/elements": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tailwindplus/elements/-/elements-1.0.13.tgz", + "integrity": "sha512-Y0K4D47rf/+Bnj2NLPl+AJykujDsEtcF+CL1DiVnE/qjmxA50+FSACNKmoyAPURq6RF/IrS0YIb8xUjtvstCSw==", + "license": "SEE LICENSE IN LICENSE.md" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", diff --git a/package.json b/package.json index b6db61df..ff394a72 100644 --- a/package.json +++ b/package.json @@ -27,5 +27,8 @@ "tailwindcss": "^3.4.10", "tippy.js": "^6.3.7", "vite": "^6.3.5" + }, + "dependencies": { + "@tailwindplus/elements": "^1.0.13" } } diff --git a/phpunit.ci.xml b/phpunit.ci.xml new file mode 100644 index 00000000..123810b7 --- /dev/null +++ b/phpunit.ci.xml @@ -0,0 +1,37 @@ + + + + + ./tests/Feature + + + ./app-modules/*/tests + + + + + + + + + + + + + + + + + + + + + + + + + + ./app + + + \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 7480489f..a5e78db8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,17 +10,24 @@ - + - - + + + + + + + + + diff --git a/resources/css/base.css b/resources/css/base.css index 39cd6435..ba9e27cc 100644 --- a/resources/css/base.css +++ b/resources/css/base.css @@ -6,6 +6,38 @@ input { width: 100%; } display: none !important; } +@font-face { + font-family: 'Rota'; + src: url('../fonts/rota/rota-semibold-webfont.woff2') format('woff2'), + url('../fonts/rota/rota-semibold-webfont.woff') format('woff'); + font-weight: 600; + font-style: swap; +} + +@font-face { + font-family: 'Rota'; + src: url('../fonts/rota/rota-regular-webfont.woff2') format('woff2'), + url('../fonts/rota/rota-regular-webfont.woff') format('woff'); + font-weight: 400; + font-style: swap; +} + +@font-face { + font-family: 'Rota'; + src: url('../fonts/rota/rota-medium-webfont.woff2') format('woff2'), + url('../fonts/rota/rota-medium-webfont.woff') format('woff'); + font-weight: 500; + font-style: swap; +} + +@font-face { + font-family: 'Rota'; + src: url('../fonts/rota/rota-bold-webfont.woff2') format('woff2'), + url('../fonts/rota/rota-bold-webfont.woff') format('woff'); + font-weight: 700; + font-style: swap; +} + .hide-scroll::-webkit-scrollbar { display: none; } diff --git a/resources/css/header.css b/resources/css/header.css index 836da972..8c7741d7 100644 --- a/resources/css/header.css +++ b/resources/css/header.css @@ -1,7 +1,7 @@ .header.is-fixed { - @apply fixed inset-x-0 bg-white/80 transition-all duration-300; + @apply fixed inset-x-0 bg-white/80 transition-all duration-300; } .header.is-hidden { - transform: translateY(-100%); + transform: translateY(-100%); } diff --git a/resources/css/torchlight.css b/resources/css/torchlight.css index 7a3336fc..de7addb8 100644 --- a/resources/css/torchlight.css +++ b/resources/css/torchlight.css @@ -3,8 +3,8 @@ overflow-x-auto is recommended. */ pre { - @apply my-4 overflow-x-auto rounded-lg; - padding: 0 !important; + @apply my-4 overflow-x-auto rounded-lg; + padding: 0 !important; } /* @@ -14,14 +14,14 @@ pre { colors extend edge to edge. */ pre code.torchlight { - @apply block min-w-max py-6 max-h-[43.75rem]; + @apply block min-w-max py-6 max-h-[43.75rem]; } /* Horizontal line padding. */ pre code.torchlight .line { - @apply px-5; + @apply px-5; } /* @@ -30,64 +30,64 @@ pre code.torchlight .line { */ pre code.torchlight .line-number, pre code.torchlight .summary-caret { - @apply mr-4; - user-select: none; + @apply mr-4; + user-select: none; } .torchlight.has-focus-lines .line:not(.line-focus) { - transition: - filter 0.35s, - opacity 0.35s; - filter: blur(0.095rem); - opacity: 0.65; + transition: + filter 0.35s, + opacity 0.35s; + filter: blur(0.095rem); + opacity: 0.65; } .torchlight.has-focus-lines:hover .line:not(.line-focus) { - filter: blur(0px); - opacity: 1; + filter: blur(0px); + opacity: 1; } .torchlight summary:focus { - outline: none; + outline: none; } /* Hide the default markers, as we provide our own */ .torchlight details > summary::marker, .torchlight details > summary::-webkit-details-marker { - display: none; + display: none; } .torchlight details .summary-caret::after { - pointer-events: none; + pointer-events: none; } /* Add spaces to keep everything aligned */ .torchlight .summary-caret-empty::after, .torchlight details .summary-caret-middle::after, .torchlight details .summary-caret-end::after { - content: ' '; + content: ' '; } /* Show a minus sign when the block is open. */ .torchlight details[open] .summary-caret-start::after { - content: '-'; + content: '-'; } /* And a plus sign when the block is closed. */ .torchlight details:not([open]) .summary-caret-start::after { - content: '+'; + content: '+'; } /* Hide the [...] indicator when open. */ .torchlight details[open] .summary-hide-when-open { - display: none; + display: none; } /* Show the [...] indicator when closed. */ .torchlight details:not([open]) .summary-hide-when-open { - display: initial; + display: initial; } .code-block .copyBtn { - @apply absolute right-2 top-2 outline-none; + @apply absolute right-2 top-2 outline-none; } diff --git a/resources/fonts/rota/rota-bold-webfont.woff b/resources/fonts/rota/rota-bold-webfont.woff new file mode 100644 index 00000000..77433225 Binary files /dev/null and b/resources/fonts/rota/rota-bold-webfont.woff differ diff --git a/resources/fonts/rota/rota-bold-webfont.woff2 b/resources/fonts/rota/rota-bold-webfont.woff2 new file mode 100644 index 00000000..3e975ed9 Binary files /dev/null and b/resources/fonts/rota/rota-bold-webfont.woff2 differ diff --git a/resources/fonts/rota/rota-medium-webfont.woff b/resources/fonts/rota/rota-medium-webfont.woff new file mode 100644 index 00000000..86333f7e Binary files /dev/null and b/resources/fonts/rota/rota-medium-webfont.woff differ diff --git a/resources/fonts/rota/rota-medium-webfont.woff2 b/resources/fonts/rota/rota-medium-webfont.woff2 new file mode 100644 index 00000000..048eb799 Binary files /dev/null and b/resources/fonts/rota/rota-medium-webfont.woff2 differ diff --git a/resources/fonts/rota/rota-regular-webfont.woff b/resources/fonts/rota/rota-regular-webfont.woff new file mode 100644 index 00000000..dc7825fc Binary files /dev/null and b/resources/fonts/rota/rota-regular-webfont.woff differ diff --git a/resources/fonts/rota/rota-regular-webfont.woff2 b/resources/fonts/rota/rota-regular-webfont.woff2 new file mode 100644 index 00000000..f498c4ae Binary files /dev/null and b/resources/fonts/rota/rota-regular-webfont.woff2 differ diff --git a/resources/fonts/rota/rota-semibold-webfont.woff b/resources/fonts/rota/rota-semibold-webfont.woff new file mode 100644 index 00000000..6570e9bc Binary files /dev/null and b/resources/fonts/rota/rota-semibold-webfont.woff differ diff --git a/resources/fonts/rota/rota-semibold-webfont.woff2 b/resources/fonts/rota/rota-semibold-webfont.woff2 new file mode 100644 index 00000000..8b0c3033 Binary files /dev/null and b/resources/fonts/rota/rota-semibold-webfont.woff2 differ diff --git a/resources/js/app.js b/resources/js/app.js index 449d001d..e51fa292 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,6 +1,7 @@ import { Livewire, Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm' import '../../vendor/laravelcm/livewire-slide-overs/resources/js/slide-over'; +import '@tailwindplus/elements' import intersect from '@alpinejs/intersect' import Tooltip from '@ryangjchandler/alpine-tooltip' import collapse from '@alpinejs/collapse' diff --git a/resources/views/components/articles/card.blade.php b/resources/views/components/articles/card.blade.php index b83eb4d0..15b1a971 100644 --- a/resources/views/components/articles/card.blade.php +++ b/resources/views/components/articles/card.blade.php @@ -4,16 +4,17 @@ 'iconLink' => false, ]) -
$isSummary]) -> +
$isSummary +])> @php $media = ! empty($article->getFirstMediaUrl('media')) ? $article->getFirstMediaUrl('media') : asset('images/socialcard.png') @endphp - @if(! $isSummary) + @if (! $isSummary)
diff --git a/resources/views/components/forum/thread-summary.blade.php b/resources/views/components/forum/thread-summary.blade.php index 2568da78..0609e643 100644 --- a/resources/views/components/forum/thread-summary.blade.php +++ b/resources/views/components/forum/thread-summary.blade.php @@ -4,7 +4,7 @@
- +
- +
diff --git a/resources/views/components/forum/thread.blade.php b/resources/views/components/forum/thread.blade.php index a6f279a9..9c45548c 100644 --- a/resources/views/components/forum/thread.blade.php +++ b/resources/views/components/forum/thread.blade.php @@ -4,10 +4,10 @@ ])
- +
-

+

{{ $thread->subject() }} @@ -47,6 +47,6 @@ class="inline-flex items-center rounded-xl gap-1 px-2 py-0.5 font-medium bg-prim

- +
diff --git a/resources/views/components/layouts/footer.blade.php b/resources/views/components/layouts/footer.blade.php index 25b225f0..fc7c343e 100644 --- a/resources/views/components/layouts/footer.blade.php +++ b/resources/views/components/layouts/footer.blade.php @@ -1,4 +1,4 @@ -
+

{{ __('global.footer.title') }}

@@ -26,9 +26,6 @@ class="ml-2 size-6 rounded-full"
- @if(isHolidayWeek()) + @if (isHolidayWeek())
Christmas decoration @@ -27,6 +27,7 @@ class="group inline-flex items-center"
@endif +
diff --git a/resources/views/livewire/components/user/threads.blade.php b/resources/views/livewire/components/user/threads.blade.php index 7169b2aa..8d7a8b82 100644 --- a/resources/views/livewire/components/user/threads.blade.php +++ b/resources/views/livewire/components/user/threads.blade.php @@ -12,7 +12,7 @@
@foreach ($this->threads as $thread) - + @can('update', $thread) {{ $this->editAction()(['id' => $thread->id]) }} diff --git a/resources/views/livewire/pages/account/profile.blade.php b/resources/views/livewire/pages/account/profile.blade.php index 64685d9f..14ec5e6b 100644 --- a/resources/views/livewire/pages/account/profile.blade.php +++ b/resources/views/livewire/pages/account/profile.blade.php @@ -140,13 +140,13 @@ class="relative z-20 inline-flex h-8 w-full cursor-pointer items-center justify-
- +
- +
- +
- @if($tag->description) + + @if ($tag->description)

{{ $tag->description }}

@@ -40,7 +41,7 @@ class="size-4 text-gray-400 dark:text-gray-500"
@foreach ($articles as $article) - + @endforeach
diff --git a/resources/views/livewire/pages/auth/register.blade.php b/resources/views/livewire/pages/auth/register.blade.php index 53191571..64b3107b 100644 --- a/resources/views/livewire/pages/auth/register.blade.php +++ b/resources/views/livewire/pages/auth/register.blade.php @@ -1,7 +1,6 @@ validate([ diff --git a/resources/views/livewire/pages/discussions/index.blade.php b/resources/views/livewire/pages/discussions/index.blade.php index c8364176..cfd5de4b 100644 --- a/resources/views/livewire/pages/discussions/index.blade.php +++ b/resources/views/livewire/pages/discussions/index.blade.php @@ -8,10 +8,10 @@ class="bg-white rounded-xl ring-1 ring-gray-200/60 dark:bg-gray-800 dark:ring-wh >
@@ -61,10 +61,10 @@ class="size-8 lg:hidden"
-