refactor: settings redesign, Spanish translations, language system, strict_types

Settings pages:
- Redesign layout to match Sendkit (max-w-4xl, space-y-12, Separator sections)
- Merge Members page into Workspace settings with Table, invite Dialog, ConfirmDeleteModal
- Add workspace logo upload/delete routes and controller methods
- Translate all hardcoded strings in Workspace.vue modals

Language system:
- Drop languages table, replace language_id FK with locale string column on users
- Create config/languages.php for available languages and default locale
- Add Spanish (es) translations (13 files)
- Simplify HandleInertiaRequests, ProfileController, RegisteredUserController

Code quality:
- Add declare(strict_types=1) to all PHP files
- Fix MastodonPublisher using wrong attribute (filename -> original_filename)
- Fix HasMediaTest for new has_photo/photo_url accessors
- Fix PublishToSocialPlatformTest type error revealed by strict_types
- Remove orphaned Language model from AppServiceProvider morph map
- Update User TypeScript interface (has_photo, photo_url, locale)
- Eager load media relation on workspaces to prevent N+1
- Add 8 new tests for workspace logo upload/delete
- Update workspace settings test to assert members/invitations props

All 710 tests passing.
This commit is contained in:
Paulo Castellano 2026-03-30 00:20:43 -03:00
parent 5693ac4fa2
commit 56b8c92e72
255 changed files with 2117 additions and 429 deletions

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Broadcasting;
use App\Models\Post;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Concerns;
use Illuminate\Contracts\Validation\Rule;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Concerns;
use App\Models\User;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Enums\SocialAccount\Status;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Jobs\PublishPost;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Enums\Media;
enum Type: string

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Enums\Post;
enum Status: string

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Enums\PostPlatform;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Enums\SocialAccount;
use App\Enums\Media\Type as MediaType;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Enums\SocialAccount;
enum Status: string

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Enums\User;
enum Persona: string

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Enums\User;
enum Setup: string

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Enums\UserWorkspace;
enum Role: string

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Events;
use App\Models\PostPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Events;
use App\Models\User;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Exceptions;
use Exception;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;

View file

@ -74,15 +74,13 @@ public function deletePhoto(Request $request): RedirectResponse
public function updateLanguage(Request $request): RedirectResponse
{
$request->validate([
'language_id' => ['required', 'exists:languages,id'],
'locale' => ['required', 'string', 'in:'.implode(',', array_keys(config('languages.available')))],
]);
$request->user()->update([
'language_id' => $request->language_id,
'locale' => $request->locale,
]);
$request->user()->refresh();
session()->flash('flash.banner', __('settings.flash.language_updated'));
session()->flash('flash.bannerStyle', 'success');

View file

@ -86,8 +86,25 @@ public function settings(Request $request): Response|RedirectResponse
->mapWithKeys(fn ($tz) => [$tz => $tz])
->toArray();
$members = $workspace->members()
->get()
->map(fn ($member) => [
'id' => $member->id,
'name' => $member->name,
'email' => $member->email,
'role' => $member->pivot->role,
'is_owner' => $member->id === $workspace->user_id,
]);
$invitations = $workspace->invites()
->select('id', 'email', 'role')
->latest()
->get();
return Inertia::render('settings/Workspace', [
'workspace' => $workspace,
'members' => $members,
'invitations' => $invitations,
'timezones' => $timezones,
]);
}

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -6,7 +6,6 @@
use App\Enums\User\Setup;
use App\Http\Controllers\Controller;
use App\Models\Language;
use App\Models\User;
use App\Models\Workspace;
use App\Rules\Timezone;
@ -48,14 +47,12 @@ public function store(Request $request): RedirectResponse
$isInviteRegistration = str_contains($request->input('redirect', ''), '/invites/');
$user = DB::transaction(function () use ($request, $isInviteRegistration) {
$defaultLanguage = Language::where('code', 'en')->first();
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => $request->password,
'setup' => $isInviteRegistration ? Setup::Completed : Setup::Role,
'language_id' => $defaultLanguage?->id,
'locale' => config('languages.default'),
'email_verified_at' => $isInviteRegistration ? now() : null,
]);

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware\App;
use Closure;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware\App;
use App\Enums\User\Setup;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware\App;
use Closure;

View file

@ -6,10 +6,8 @@
use App\Http\Resources\App\HandleInertiaRequests\AuthUserResource;
use App\Http\Resources\App\HandleInertiaRequests\AuthWorkspaceResource;
use App\Models\Language;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
@ -28,11 +26,11 @@ public function share(Request $request): array
{
$user = $request->user();
if ($user?->language) {
App::setLocale($user->language->code);
if ($user?->locale) {
App::setLocale($user->locale);
}
$currentWorkspace = $user?->currentWorkspace;
$currentWorkspace = $user?->currentWorkspace?->load('media');
return [
...parent::share($request),
@ -41,7 +39,7 @@ public function share(Request $request): array
'user' => $user ? AuthUserResource::make($user) : null,
'currentWorkspace' => $currentWorkspace ? AuthWorkspaceResource::make($currentWorkspace, $user) : null,
'workspaces' => $user
? $user->workspaces()->get()->map(fn ($ws) => AuthWorkspaceResource::summary($ws))
? $user->workspaces()->with('media')->get()->map(fn ($ws) => AuthWorkspaceResource::summary($ws))
: [],
],
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
@ -49,7 +47,10 @@ public function share(Request $request): array
'applicationUrl' => config('app.url'),
'env' => config('app.env'),
'locale' => app()->getLocale(),
'languages' => fn () => Cache::remember('languages:public', 3600, fn () => Language::query()->orderBy('name')->get()->toArray()),
'languages' => collect(config('languages.available'))->map(fn ($name, $code) => [
'code' => $code,
'name' => $name,
])->values()->all(),
'selfHosted' => config('trypost.self_hosted'),
];
}

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\App\Invite;
use App\Enums\UserWorkspace\Role as WorkspaceRole;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\App\Media;
use Illuminate\Foundation\Http\FormRequest;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\App\Media;
use Illuminate\Foundation\Http\FormRequest;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\App\Post;
use Illuminate\Foundation\Http\FormRequest;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\App\Settings;
use App\Concerns\PasswordValidationRules;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\App\Settings;
use App\Concerns\PasswordValidationRules;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\App\Settings;
use App\Concerns\ProfileValidationRules;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\App\Workspace;
use Illuminate\Foundation\Http\FormRequest;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\App\Workspace;
use App\Rules\Timezone;

View file

@ -21,7 +21,7 @@ public static function make(User $user): array
'photo_url' => $user->photo_url,
'email_verified_at' => $user->email_verified_at?->toIso8601String(),
'current_workspace_id' => $user->current_workspace_id,
'language_id' => $user->language_id,
'locale' => $user->locale,
'created_at' => $user->created_at->toIso8601String(),
'updated_at' => $user->updated_at->toIso8601String(),
];

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\Post;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Enums\SocialAccount\Status;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Mail;
use App\Models\SocialAccount;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Mail;
use App\Models\SocialAccount;
@ -23,7 +25,7 @@ public function __construct(
public Workspace $workspace,
public Collection $disconnectedAccounts
) {
$this->locale = $this->workspace->owner?->language?->code ?? 'en';
$this->locale = $this->workspace->owner?->locale ?? 'en';
}
public function envelope(): Envelope

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Mail;
use App\Models\WorkspaceInvite as WorkspaceInviteModel;

View file

@ -1,23 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Language extends Model
{
use HasFactory, HasUuids;
protected $fillable = [
'name',
'code',
];
public function users(): HasMany
{
return $this->hasMany(User::class);
}
}

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Enums\Media\Type as MediaType;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Enums\Post\Status as PostStatus;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Enums\PostPlatform\ContentType;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Enums\SocialAccount\Platform as SocialPlatform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models\Traits;
use App\Models\Media;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models\Traits;
use App\Models\Workspace;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Enums\User\Persona;
@ -10,7 +12,6 @@
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Cashier\Billable;
@ -32,7 +33,7 @@ class User extends Authenticatable implements MustVerifyEmail
'setup',
'persona',
'current_workspace_id',
'language_id',
'locale',
'email_verified_at',
];
@ -80,11 +81,11 @@ protected function casts(): array
}
/**
* Get the user's language.
* Get the user's locale code (e.g. 'en', 'es', 'pt-br').
*/
public function language(): BelongsTo
public function getLocaleCode(): string
{
return $this->belongsTo(Language::class);
return $this->locale ?? 'en';
}
/**

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Models\Traits\HasMedia;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Database\Factories\WorkspaceHashtagFactory;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Database\Factories\WorkspaceLabelFactory;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Enums\UserWorkspace\Role as WorkspaceRole;

View file

@ -1,9 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Listeners\StripeEventListener;
use App\Models\Language;
use App\Models\Media;
use App\Models\Post;
use App\Models\PostPlatform;
@ -74,7 +75,6 @@ public function boot(): void
protected function configureMorphMap(): void
{
Relation::enforceMorphMap([
'language' => Language::class,
'media' => Media::class,
'post' => Post::class,
'postPlatform' => PostPlatform::class,

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\Facades\Gate;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Models\User;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Exceptions\TokenExpiredException;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Enums\SocialAccount\Platform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Enums\PostPlatform\ContentType;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Enums\PostPlatform\ContentType;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Enums\PostPlatform\ContentType;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Enums\PostPlatform\ContentType;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Enums\SocialAccount\Platform;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Exceptions\TokenExpiredException;
@ -21,7 +23,7 @@ public function publish(PostPlatform $postPlatform): array
// Upload media first (max 4)
foreach ($medias->take(4) as $media) {
$mediaId = $this->uploadMedia($account, $instance, $media->url, $media->filename);
$mediaId = $this->uploadMedia($account, $instance, $media->url, $media->original_filename);
if ($mediaId) {
$mediaIds[] = $mediaId;
}

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Enums\PostPlatform\ContentType;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Exceptions\TokenExpiredException;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Exceptions\TokenExpiredException;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Exceptions\TokenExpiredException;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Services\Social;
use App\Exceptions\TokenExpiredException;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Socialite;
use GuzzleHttp\RequestOptions;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Socialite;
use SocialiteProviders\LinkedIn\Provider;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use App\Models\User;
return [

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Str;
return [

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Laravel\Cashier\Console\WebhookCommand;
use Laravel\Cashier\Invoices\DompdfInvoiceRenderer;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* @see https://github.com/pionl/laravel-chunk-upload
*/

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Str;
use Pdo\Mysql;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Str;
return [

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

35
config/languages.php Normal file
View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
return [
/*
|--------------------------------------------------------------------------
| Available Languages
|--------------------------------------------------------------------------
|
| List of languages available in the application. Each entry maps a locale
| code to a display name. Add or remove entries here to control which
| languages appear in the language switcher and are accepted by the API.
|
*/
'available' => [
'en' => 'English',
'es' => 'Español',
'pt-br' => 'Português',
],
/*
|--------------------------------------------------------------------------
| Default Language
|--------------------------------------------------------------------------
|
| The default locale code assigned to new users.
|
*/
'default' => 'en',
];

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

Some files were not shown because too many files have changed in this diff Show more