diff --git a/app/Http/Controllers/MediaController.php b/app/Http/Controllers/MediaController.php
index 266c9e1c..85ac8a96 100644
--- a/app/Http/Controllers/MediaController.php
+++ b/app/Http/Controllers/MediaController.php
@@ -29,6 +29,7 @@ public function store(StoreMediaRequest $request): JsonResponse
return response()->json([
'id' => $media->id,
+ 'group_id' => $media->group_id,
'url' => $media->url,
'type' => $media->type->value,
'original_filename' => $media->original_filename,
@@ -71,6 +72,7 @@ public function storeChunked(Request $request): JsonResponse
return response()->json([
'done' => true,
'id' => $media->id,
+ 'group_id' => $media->group_id,
'url' => $media->url,
'type' => $media->type->value,
'original_filename' => $media->original_filename,
@@ -96,6 +98,21 @@ public function destroy(string $modelId, Media $media): JsonResponse
return response()->json(['success' => true]);
}
+ public function reorder(Request $request): JsonResponse
+ {
+ $request->validate([
+ 'media' => 'required|array',
+ 'media.*.id' => 'required|exists:medias,id',
+ 'media.*.order' => 'required|integer|min:0',
+ ]);
+
+ foreach ($request->input('media') as $item) {
+ Media::where('id', $item['id'])->update(['order' => $item['order']]);
+ }
+
+ return response()->json(['success' => true]);
+ }
+
public function duplicate(Media $media, Request $request): JsonResponse
{
$targets = $request->input('targets', []);
@@ -111,6 +128,7 @@ public function duplicate(Media $media, Request $request): JsonResponse
$model = $modelClass::findOrFail($modelId);
$duplicate = $model->media()->create([
+ 'group_id' => $media->group_id,
'collection' => $collection,
'type' => $media->type,
'path' => $media->path,
@@ -123,6 +141,7 @@ public function duplicate(Media $media, Request $request): JsonResponse
$duplicates[] = [
'id' => $duplicate->id,
+ 'group_id' => $duplicate->group_id,
'mediable_id' => $duplicate->mediable_id,
'mediable_type' => $duplicate->mediable_type,
'url' => $duplicate->url,
diff --git a/app/Http/Requests/StoreMediaRequest.php b/app/Http/Requests/StoreMediaRequest.php
index 70494dba..dba91163 100644
--- a/app/Http/Requests/StoreMediaRequest.php
+++ b/app/Http/Requests/StoreMediaRequest.php
@@ -8,9 +8,9 @@
class StoreMediaRequest extends FormRequest
{
protected array $allowedModels = [
- 'App\Models\PostPlatform',
- 'App\Models\Workspace',
- 'App\Models\User',
+ 'postPlatform',
+ 'workspace',
+ 'user',
];
public function authorize(): bool
diff --git a/app/Models/Media.php b/app/Models/Media.php
index bd3f2ece..a397147a 100644
--- a/app/Models/Media.php
+++ b/app/Models/Media.php
@@ -22,6 +22,7 @@ class Media extends Model
protected $fillable = [
'mediable_id',
'mediable_type',
+ 'group_id',
'collection',
'type',
'path',
@@ -64,7 +65,14 @@ public function getTemporaryUrl(int $expirationMinutes = 60): string
public function delete(): bool
{
- Storage::delete($this->path);
+ // Only delete the file if no other media records use the same path
+ $otherMediaWithSamePath = static::where('path', $this->path)
+ ->where('id', '!=', $this->id)
+ ->exists();
+
+ if (! $otherMediaWithSamePath) {
+ Storage::delete($this->path);
+ }
return parent::delete();
}
diff --git a/app/Models/Traits/HasMedia.php b/app/Models/Traits/HasMedia.php
index 43f9f4b7..13d9c9b9 100644
--- a/app/Models/Traits/HasMedia.php
+++ b/app/Models/Traits/HasMedia.php
@@ -64,7 +64,7 @@ public function getFallbackAvatarUrl(string $seed): string
* Add media to a collection.
* If the collection is configured as 'single', it will clear existing media first.
*/
- public function addMedia(UploadedFile $file, string $collection = 'default', array $meta = []): Media
+ public function addMedia(UploadedFile $file, string $collection = 'default', array $meta = [], ?string $groupId = null): Media
{
if ($this->isSingleMediaCollection($collection)) {
$this->clearMediaCollection($collection);
@@ -80,6 +80,7 @@ public function addMedia(UploadedFile $file, string $collection = 'default', arr
Storage::put($path, file_get_contents($file->getPathname()));
return $this->media()->create([
+ 'group_id' => $groupId ?? Str::uuid()->toString(),
'collection' => $collection,
'type' => $type,
'path' => $path,
@@ -94,7 +95,7 @@ public function addMedia(UploadedFile $file, string $collection = 'default', arr
/**
* Add media from a file path (used for chunked uploads).
*/
- public function addMediaFromPath(string $filePath, string $originalFilename, string $collection = 'default', array $meta = []): Media
+ public function addMediaFromPath(string $filePath, string $originalFilename, string $collection = 'default', array $meta = [], ?string $groupId = null): Media
{
if ($this->isSingleMediaCollection($collection)) {
$this->clearMediaCollection($collection);
@@ -120,6 +121,7 @@ public function addMediaFromPath(string $filePath, string $originalFilename, str
}
return $this->media()->create([
+ 'group_id' => $groupId ?? Str::uuid()->toString(),
'collection' => $collection,
'type' => $type,
'path' => $storagePath,
diff --git a/database/migrations/2026_01_14_232320_create_medias_table.php b/database/migrations/2026_01_14_232320_create_medias_table.php
index 5a9bd388..099b0062 100644
--- a/database/migrations/2026_01_14_232320_create_medias_table.php
+++ b/database/migrations/2026_01_14_232320_create_medias_table.php
@@ -13,6 +13,7 @@ public function up(): void
{
Schema::create('medias', function (Blueprint $table) {
$table->uuid('id')->primary();
+ $table->uuid('group_id')->nullable()->index();
$table->uuidMorphs('mediable');
$table->string('collection')->default('default');
$table->string('type');
diff --git a/lang/en/auth.php b/lang/en/auth.php
new file mode 100644
index 00000000..6598e2c0
--- /dev/null
+++ b/lang/en/auth.php
@@ -0,0 +1,20 @@
+ 'These credentials do not match our records.',
+ 'password' => 'The provided password is incorrect.',
+ 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
+
+];
diff --git a/lang/en/pagination.php b/lang/en/pagination.php
new file mode 100644
index 00000000..d4814118
--- /dev/null
+++ b/lang/en/pagination.php
@@ -0,0 +1,19 @@
+ '« Previous',
+ 'next' => 'Next »',
+
+];
diff --git a/lang/en/passwords.php b/lang/en/passwords.php
new file mode 100644
index 00000000..fad3a7d7
--- /dev/null
+++ b/lang/en/passwords.php
@@ -0,0 +1,22 @@
+ 'Your password has been reset.',
+ 'sent' => 'We have emailed your password reset link.',
+ 'throttled' => 'Please wait before retrying.',
+ 'token' => 'This password reset token is invalid.',
+ 'user' => "We can't find a user with that email address.",
+
+];
diff --git a/lang/en/validation.php b/lang/en/validation.php
new file mode 100644
index 00000000..63ec29a1
--- /dev/null
+++ b/lang/en/validation.php
@@ -0,0 +1,200 @@
+ 'The :attribute field must be accepted.',
+ 'accepted_if' => 'The :attribute field must be accepted when :other is :value.',
+ 'active_url' => 'The :attribute field must be a valid URL.',
+ 'after' => 'The :attribute field must be a date after :date.',
+ 'after_or_equal' => 'The :attribute field must be a date after or equal to :date.',
+ 'alpha' => 'The :attribute field must only contain letters.',
+ 'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.',
+ 'alpha_num' => 'The :attribute field must only contain letters and numbers.',
+ 'any_of' => 'The :attribute field is invalid.',
+ 'array' => 'The :attribute field must be an array.',
+ 'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
+ 'before' => 'The :attribute field must be a date before :date.',
+ 'before_or_equal' => 'The :attribute field must be a date before or equal to :date.',
+ 'between' => [
+ 'array' => 'The :attribute field must have between :min and :max items.',
+ 'file' => 'The :attribute field must be between :min and :max kilobytes.',
+ 'numeric' => 'The :attribute field must be between :min and :max.',
+ 'string' => 'The :attribute field must be between :min and :max characters.',
+ ],
+ 'boolean' => 'The :attribute field must be true or false.',
+ 'can' => 'The :attribute field contains an unauthorized value.',
+ 'confirmed' => 'The :attribute field confirmation does not match.',
+ 'contains' => 'The :attribute field is missing a required value.',
+ 'current_password' => 'The password is incorrect.',
+ 'date' => 'The :attribute field must be a valid date.',
+ 'date_equals' => 'The :attribute field must be a date equal to :date.',
+ 'date_format' => 'The :attribute field must match the format :format.',
+ 'decimal' => 'The :attribute field must have :decimal decimal places.',
+ 'declined' => 'The :attribute field must be declined.',
+ 'declined_if' => 'The :attribute field must be declined when :other is :value.',
+ 'different' => 'The :attribute field and :other must be different.',
+ 'digits' => 'The :attribute field must be :digits digits.',
+ 'digits_between' => 'The :attribute field must be between :min and :max digits.',
+ 'dimensions' => 'The :attribute field has invalid image dimensions.',
+ 'distinct' => 'The :attribute field has a duplicate value.',
+ 'doesnt_contain' => 'The :attribute field must not contain any of the following: :values.',
+ 'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.',
+ 'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.',
+ 'email' => 'The :attribute field must be a valid email address.',
+ 'encoding' => 'The :attribute field must be encoded in :encoding.',
+ 'ends_with' => 'The :attribute field must end with one of the following: :values.',
+ 'enum' => 'The selected :attribute is invalid.',
+ 'exists' => 'The selected :attribute is invalid.',
+ 'extensions' => 'The :attribute field must have one of the following extensions: :values.',
+ 'file' => 'The :attribute field must be a file.',
+ 'filled' => 'The :attribute field must have a value.',
+ 'gt' => [
+ 'array' => 'The :attribute field must have more than :value items.',
+ 'file' => 'The :attribute field must be greater than :value kilobytes.',
+ 'numeric' => 'The :attribute field must be greater than :value.',
+ 'string' => 'The :attribute field must be greater than :value characters.',
+ ],
+ 'gte' => [
+ 'array' => 'The :attribute field must have :value items or more.',
+ 'file' => 'The :attribute field must be greater than or equal to :value kilobytes.',
+ 'numeric' => 'The :attribute field must be greater than or equal to :value.',
+ 'string' => 'The :attribute field must be greater than or equal to :value characters.',
+ ],
+ 'hex_color' => 'The :attribute field must be a valid hexadecimal color.',
+ 'image' => 'The :attribute field must be an image.',
+ 'in' => 'The selected :attribute is invalid.',
+ 'in_array' => 'The :attribute field must exist in :other.',
+ 'in_array_keys' => 'The :attribute field must contain at least one of the following keys: :values.',
+ 'integer' => 'The :attribute field must be an integer.',
+ 'ip' => 'The :attribute field must be a valid IP address.',
+ 'ipv4' => 'The :attribute field must be a valid IPv4 address.',
+ 'ipv6' => 'The :attribute field must be a valid IPv6 address.',
+ 'json' => 'The :attribute field must be a valid JSON string.',
+ 'list' => 'The :attribute field must be a list.',
+ 'lowercase' => 'The :attribute field must be lowercase.',
+ 'lt' => [
+ 'array' => 'The :attribute field must have less than :value items.',
+ 'file' => 'The :attribute field must be less than :value kilobytes.',
+ 'numeric' => 'The :attribute field must be less than :value.',
+ 'string' => 'The :attribute field must be less than :value characters.',
+ ],
+ 'lte' => [
+ 'array' => 'The :attribute field must not have more than :value items.',
+ 'file' => 'The :attribute field must be less than or equal to :value kilobytes.',
+ 'numeric' => 'The :attribute field must be less than or equal to :value.',
+ 'string' => 'The :attribute field must be less than or equal to :value characters.',
+ ],
+ 'mac_address' => 'The :attribute field must be a valid MAC address.',
+ 'max' => [
+ 'array' => 'The :attribute field must not have more than :max items.',
+ 'file' => 'The :attribute field must not be greater than :max kilobytes.',
+ 'numeric' => 'The :attribute field must not be greater than :max.',
+ 'string' => 'The :attribute field must not be greater than :max characters.',
+ ],
+ 'max_digits' => 'The :attribute field must not have more than :max digits.',
+ 'mimes' => 'The :attribute field must be a file of type: :values.',
+ 'mimetypes' => 'The :attribute field must be a file of type: :values.',
+ 'min' => [
+ 'array' => 'The :attribute field must have at least :min items.',
+ 'file' => 'The :attribute field must be at least :min kilobytes.',
+ 'numeric' => 'The :attribute field must be at least :min.',
+ 'string' => 'The :attribute field must be at least :min characters.',
+ ],
+ 'min_digits' => 'The :attribute field must have at least :min digits.',
+ 'missing' => 'The :attribute field must be missing.',
+ 'missing_if' => 'The :attribute field must be missing when :other is :value.',
+ 'missing_unless' => 'The :attribute field must be missing unless :other is :value.',
+ 'missing_with' => 'The :attribute field must be missing when :values is present.',
+ 'missing_with_all' => 'The :attribute field must be missing when :values are present.',
+ 'multiple_of' => 'The :attribute field must be a multiple of :value.',
+ 'not_in' => 'The selected :attribute is invalid.',
+ 'not_regex' => 'The :attribute field format is invalid.',
+ 'numeric' => 'The :attribute field must be a number.',
+ 'password' => [
+ 'letters' => 'The :attribute field must contain at least one letter.',
+ 'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.',
+ 'numbers' => 'The :attribute field must contain at least one number.',
+ 'symbols' => 'The :attribute field must contain at least one symbol.',
+ 'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.',
+ ],
+ 'present' => 'The :attribute field must be present.',
+ 'present_if' => 'The :attribute field must be present when :other is :value.',
+ 'present_unless' => 'The :attribute field must be present unless :other is :value.',
+ 'present_with' => 'The :attribute field must be present when :values is present.',
+ 'present_with_all' => 'The :attribute field must be present when :values are present.',
+ 'prohibited' => 'The :attribute field is prohibited.',
+ 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.',
+ 'prohibited_if_accepted' => 'The :attribute field is prohibited when :other is accepted.',
+ 'prohibited_if_declined' => 'The :attribute field is prohibited when :other is declined.',
+ 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.',
+ 'prohibits' => 'The :attribute field prohibits :other from being present.',
+ 'regex' => 'The :attribute field format is invalid.',
+ 'required' => 'The :attribute field is required.',
+ 'required_array_keys' => 'The :attribute field must contain entries for: :values.',
+ 'required_if' => 'The :attribute field is required when :other is :value.',
+ 'required_if_accepted' => 'The :attribute field is required when :other is accepted.',
+ 'required_if_declined' => 'The :attribute field is required when :other is declined.',
+ 'required_unless' => 'The :attribute field is required unless :other is in :values.',
+ 'required_with' => 'The :attribute field is required when :values is present.',
+ 'required_with_all' => 'The :attribute field is required when :values are present.',
+ 'required_without' => 'The :attribute field is required when :values is not present.',
+ 'required_without_all' => 'The :attribute field is required when none of :values are present.',
+ 'same' => 'The :attribute field must match :other.',
+ 'size' => [
+ 'array' => 'The :attribute field must contain :size items.',
+ 'file' => 'The :attribute field must be :size kilobytes.',
+ 'numeric' => 'The :attribute field must be :size.',
+ 'string' => 'The :attribute field must be :size characters.',
+ ],
+ 'starts_with' => 'The :attribute field must start with one of the following: :values.',
+ 'string' => 'The :attribute field must be a string.',
+ 'timezone' => 'The :attribute field must be a valid timezone.',
+ 'unique' => 'The :attribute has already been taken.',
+ 'uploaded' => 'The :attribute failed to upload.',
+ 'uppercase' => 'The :attribute field must be uppercase.',
+ 'url' => 'The :attribute field must be a valid URL.',
+ 'ulid' => 'The :attribute field must be a valid ULID.',
+ 'uuid' => 'The :attribute field must be a valid UUID.',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Validation Language Lines
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify custom validation messages for attributes using the
+ | convention "attribute.rule" to name the lines. This makes it quick to
+ | specify a specific custom language line for a given attribute rule.
+ |
+ */
+
+ 'custom' => [
+ 'attribute-name' => [
+ 'rule-name' => 'custom-message',
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Validation Attributes
+ |--------------------------------------------------------------------------
+ |
+ | The following language lines are used to swap our attribute placeholder
+ | with something more reader friendly such as "E-Mail Address" instead
+ | of "email". This simply helps us make our message more expressive.
+ |
+ */
+
+ 'attributes' => [],
+
+];
diff --git a/lang/pt-BR/auth.php b/lang/pt-BR/auth.php
new file mode 100644
index 00000000..cbdb9c92
--- /dev/null
+++ b/lang/pt-BR/auth.php
@@ -0,0 +1,20 @@
+ 'Essas credenciais não correspondem aos nossos registros.',
+ 'password' => 'A senha fornecida está incorreta.',
+ 'throttle' => 'Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.',
+
+];
diff --git a/lang/pt-BR/pagination.php b/lang/pt-BR/pagination.php
new file mode 100644
index 00000000..4deabd6f
--- /dev/null
+++ b/lang/pt-BR/pagination.php
@@ -0,0 +1,19 @@
+ '« Anterior',
+ 'next' => 'Próximo »',
+
+];
diff --git a/lang/pt-BR/passwords.php b/lang/pt-BR/passwords.php
new file mode 100644
index 00000000..49fa8f07
--- /dev/null
+++ b/lang/pt-BR/passwords.php
@@ -0,0 +1,22 @@
+ 'Sua senha foi redefinida.',
+ 'sent' => 'Enviamos o link de redefinição de senha por e-mail.',
+ 'throttled' => 'Por favor, aguarde antes de tentar novamente.',
+ 'token' => 'Este token de redefinição de senha é inválido.',
+ 'user' => "Não conseguimos encontrar um usuário com esse endereço de e-mail.",
+
+];
diff --git a/lang/pt-BR/validation.php b/lang/pt-BR/validation.php
new file mode 100644
index 00000000..05d4442e
--- /dev/null
+++ b/lang/pt-BR/validation.php
@@ -0,0 +1,200 @@
+ 'O campo :attribute deve ser aceito.',
+ 'accepted_if' => 'O campo :attribute deve ser aceito quando :other for :value.',
+ 'active_url' => 'O campo :attribute deve ser uma URL válida.',
+ 'after' => 'O campo :attribute deve ser uma data posterior a :date.',
+ 'after_or_equal' => 'O campo :attribute deve ser uma data posterior ou igual a :date.',
+ 'alpha' => 'O campo :attribute deve conter apenas letras.',
+ 'alpha_dash' => 'O campo :attribute deve conter apenas letras, números, hifens e underscores.',
+ 'alpha_num' => 'O campo :attribute deve conter apenas letras e números.',
+ 'any_of' => 'O campo :attribute é inválido.',
+ 'array' => 'O campo :attribute deve ser um array.',
+ 'ascii' => 'O campo :attribute deve conter apenas caracteres alfanuméricos e símbolos de um byte.',
+ 'before' => 'O campo :attribute deve ser uma data anterior a :date.',
+ 'before_or_equal' => 'O campo :attribute deve ser uma data anterior ou igual a :date.',
+ 'between' => [
+ 'array' => 'O campo :attribute deve ter entre :min e :max itens.',
+ 'file' => 'O campo :attribute deve estar entre :min e :max kilobytes.',
+ 'numeric' => 'O campo :attribute deve estar entre :min e :max.',
+ 'string' => 'O campo :attribute deve estar entre :min e :max caracteres.',
+ ],
+ 'boolean' => 'O campo :attribute deve ser verdadeiro ou falso.',
+ 'can' => 'O campo :attribute contém um valor não autorizado.',
+ 'confirmed' => 'A confirmação do campo :attribute não corresponde.',
+ 'contains' => 'O campo :attribute está faltando um valor obrigatório.',
+ 'current_password' => 'A senha está incorreta.',
+ 'date' => 'O campo :attribute deve ser uma data válida.',
+ 'date_equals' => 'O campo :attribute deve ser uma data igual a :date.',
+ 'date_format' => 'O campo :attribute deve corresponder ao formato :format.',
+ 'decimal' => 'O campo :attribute deve ter :decimal casas decimais.',
+ 'declined' => 'O campo :attribute deve ser recusado.',
+ 'declined_if' => 'O campo :attribute deve ser recusado quando :other for :value.',
+ 'different' => 'O campo :attribute e :other devem ser diferentes.',
+ 'digits' => 'O campo :attribute deve ter :digits dígitos.',
+ 'digits_between' => 'O campo :attribute deve ter entre :min e :max dígitos.',
+ 'dimensions' => 'O campo :attribute deve ter dimensões de imagem válidas.',
+ 'distinct' => 'O campo :attribute tem um valor duplicado.',
+ 'doesnt_contain' => 'O campo :attribute não deve conter nenhum dos seguintes: :values.',
+ 'doesnt_end_with' => 'O campo :attribute não deve terminar com nenhum dos seguintes: :values.',
+ 'doesnt_start_with' => 'O campo :attribute não deve começar com nenhum dos seguintes: :values.',
+ 'email' => 'O campo :attribute deve ser um endereço de e-mail válido.',
+ 'encoding' => 'O campo :attribute deve ser codificado em :encoding.',
+ 'ends_with' => 'O campo :attribute deve terminar com um dos seguintes: :values.',
+ 'enum' => 'O :attribute selecionado é inválido.',
+ 'exists' => 'O :attribute selecionado é inválido.',
+ 'extensions' => 'O campo :attribute deve ter uma das seguintes extensões: :values.',
+ 'file' => 'O campo :attribute deve ser um arquivo.',
+ 'filled' => 'O campo :attribute deve ter um valor.',
+ 'gt' => [
+ 'array' => 'O campo :attribute deve ter mais de :value itens.',
+ 'file' => 'O campo :attribute deve ser maior que :value kilobytes.',
+ 'numeric' => 'O campo :attribute deve ser maior que :value.',
+ 'string' => 'O campo :attribute deve ser maior que :value caracteres.',
+ ],
+ 'gte' => [
+ 'array' => 'O campo :attribute deve ter :value itens ou mais.',
+ 'file' => 'O campo :attribute deve ser maior ou igual a :value kilobytes.',
+ 'numeric' => 'O campo :attribute deve ser maior ou igual a :value.',
+ 'string' => 'O campo :attribute deve ser maior ou igual a :value caracteres.',
+ ],
+ 'hex_color' => 'O campo :attribute deve ser uma cor hexadecimal válida.',
+ 'image' => 'O campo :attribute deve ser uma imagem.',
+ 'in' => 'O :attribute selecionado é inválido.',
+ 'in_array' => 'O campo :attribute deve existir em :other.',
+ 'in_array_keys' => 'O campo :attribute deve conter pelo menos uma das seguintes chaves: :values.',
+ 'integer' => 'O campo :attribute deve ser um inteiro.',
+ 'ip' => 'O campo :attribute deve ser um endereço IP válido.',
+ 'ipv4' => 'O campo :attribute deve ser um endereço IPv4 válido.',
+ 'ipv6' => 'O campo :attribute deve ser um endereço IPv6 válido.',
+ 'json' => 'O campo :attribute deve ser uma string JSON válida.',
+ 'list' => 'O campo :attribute deve ser uma lista.',
+ 'lowercase' => 'O campo :attribute deve estar em minúsculas.',
+ 'lt' => [
+ 'array' => 'O campo :attribute deve ter menos de :value itens.',
+ 'file' => 'O campo :attribute deve ser menor que :value kilobytes.',
+ 'numeric' => 'O campo :attribute deve ser menor que :value.',
+ 'string' => 'O campo :attribute deve ser menor que :value caracteres.',
+ ],
+ 'lte' => [
+ 'array' => 'O campo :attribute deve ter :value itens ou menos.',
+ 'file' => 'O campo :attribute deve ser menor ou igual a :value kilobytes.',
+ 'numeric' => 'O campo :attribute deve ser menor ou igual a :value.',
+ 'string' => 'O campo :attribute deve ser menor ou igual a :value caracteres.',
+ ],
+ 'mac_address' => 'O campo :attribute deve ser um endereço MAC válido.',
+ 'max' => [
+ 'array' => 'O campo :attribute deve ter no máximo :max itens.',
+ 'file' => 'O campo :attribute deve ter no máximo :max kilobytes.',
+ 'numeric' => 'O campo :attribute deve ter no máximo :max.',
+ 'string' => 'O campo :attribute deve ter no máximo :max caracteres.',
+ ],
+ 'max_digits' => 'O campo :attribute não deve ter mais que :max dígitos.',
+ 'mimes' => 'O campo :attribute deve ser um arquivo do tipo: :values.',
+ 'mimetypes' => 'O campo :attribute deve ser um arquivo do tipo: :values.',
+ 'min' => [
+ 'array' => 'O campo :attribute deve ter pelo menos :min itens.',
+ 'file' => 'O campo :attribute deve ter pelo menos :min kilobytes.',
+ 'numeric' => 'O campo :attribute deve ter pelo menos :min.',
+ 'string' => 'O campo :attribute deve ter pelo menos :min caracteres.',
+ ],
+ 'min_digits' => 'O campo :attribute deve ter pelo menos :min dígitos.',
+ 'missing' => 'O campo :attribute deve estar ausente.',
+ 'missing_if' => 'O campo :attribute deve estar ausente quando :other for :value.',
+ 'missing_unless' => 'O campo :attribute deve estar ausente a menos que :other seja :value.',
+ 'missing_with' => 'O campo :attribute deve estar ausente quando :values estiver presente.',
+ 'missing_with_all' => 'O campo :attribute deve estar ausente quando :values estiverem presentes.',
+ 'multiple_of' => 'O campo :attribute deve ser um múltiplo de :value.',
+ 'not_in' => 'O :attribute selecionado é inválido.',
+ 'not_regex' => 'O formato do campo :attribute é inválido.',
+ 'numeric' => 'O campo :attribute deve ser um número.',
+ 'password' => [
+ 'letters' => 'O campo :attribute deve conter pelo menos uma letra.',
+ 'mixed' => 'O campo :attribute deve conter pelo menos uma letra maiúscula e uma minúscula.',
+ 'numbers' => 'O campo :attribute deve conter pelo menos um número.',
+ 'symbols' => 'O campo :attribute deve conter pelo menos um símbolo.',
+ 'uncompromised' => 'O :attribute fornecido apareceu em um vazamento de dados. Por favor, escolha um :attribute diferente.',
+ ],
+ 'present' => 'O campo :attribute deve estar presente.',
+ 'present_if' => 'O campo :attribute deve estar presente quando :other for :value.',
+ 'present_unless' => 'O campo :attribute deve estar presente a menos que :other seja :value.',
+ 'present_with' => 'O campo :attribute deve estar presente quando :values estiver presente.',
+ 'present_with_all' => 'O campo :attribute deve estar presente quando :values estiverem presentes.',
+ 'prohibited' => 'O campo :attribute é proibido.',
+ 'prohibited_if' => 'O campo :attribute é proibido quando :other for :value.',
+ 'prohibited_if_accepted' => 'O campo :attribute é proibido quando :other for aceito.',
+ 'prohibited_if_declined' => 'O campo :attribute é proibido quando :other for recusado.',
+ 'prohibited_unless' => 'O campo :attribute é proibido a menos que :other esteja em :values.',
+ 'prohibits' => 'O campo :attribute proíbe :other de estar presente.',
+ 'regex' => 'O formato do campo :attribute é inválido.',
+ 'required' => 'O campo :attribute é obrigatório.',
+ 'required_array_keys' => 'O campo :attribute deve conter entradas para: :values.',
+ 'required_if' => 'O campo :attribute é obrigatório quando :other for :value.',
+ 'required_if_accepted' => 'O campo :attribute é obrigatório quando :other for aceito.',
+ 'required_if_declined' => 'O campo :attribute é obrigatório quando :other for recusado.',
+ 'required_unless' => 'O campo :attribute é obrigatório a menos que :other esteja em :values.',
+ 'required_with' => 'O campo :attribute é obrigatório quando :values estiver presente.',
+ 'required_with_all' => 'O campo :attribute é obrigatório quando :values estiverem presentes.',
+ 'required_without' => 'O campo :attribute é obrigatório quando :values não estiver presente.',
+ 'required_without_all' => 'O campo :attribute é obrigatório quando nenhum dos :values estiver presente.',
+ 'same' => 'O campo :attribute deve ser igual a :other.',
+ 'size' => [
+ 'array' => 'O campo :attribute deve conter :size itens.',
+ 'file' => 'O campo :attribute deve ter :size kilobytes.',
+ 'numeric' => 'O campo :attribute deve ser :size.',
+ 'string' => 'O campo :attribute deve ter :size caracteres.',
+ ],
+ 'starts_with' => 'O campo :attribute deve começar com um dos seguintes: :values.',
+ 'string' => 'O campo :attribute deve ser uma string.',
+ 'timezone' => 'O campo :attribute deve ser um fuso horário válido.',
+ 'unique' => 'O :attribute já foi utilizado.',
+ 'uploaded' => 'O :attribute falhou ao ser enviado.',
+ 'uppercase' => 'O campo :attribute deve estar em maiúsculo.',
+ 'url' => 'O campo :attribute deve ser uma URL válida.',
+ 'ulid' => 'O campo :attribute deve ser um ULID válido.',
+ 'uuid' => 'O campo :attribute deve ser um UUID válido.',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Validation Language Lines
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify custom validation messages for attributes using the
+ | convention "attribute.rule" to name the lines. This makes it quick to
+ | specify a specific custom language line for a given attribute rule.
+ |
+ */
+
+ 'custom' => [
+ 'attribute-name' => [
+ 'rule-name' => 'custom-message',
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Validation Attributes
+ |--------------------------------------------------------------------------
+ |
+ | The following language lines are used to swap our attribute placeholder
+ | with something more reader friendly such as "E-Mail Address" instead
+ | of "email". This simply helps us make our message more expressive.
+ |
+ */
+
+ 'attributes' => [],
+
+];
diff --git a/resources/css/app.css b/resources/css/app.css
index b382c84e..86d315e7 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -19,111 +19,111 @@ @layer utilities {
}
:root {
- --background: #fcfcfc;
+ --background: #f7f9f3;
--foreground: #000000;
--card: #ffffff;
--card-foreground: #000000;
- --popover: #fcfcfc;
+ --popover: #ffffff;
--popover-foreground: #000000;
- --primary: #000000;
+ --primary: #4f46e5;
--primary-foreground: #ffffff;
- --secondary: #ebebeb;
- --secondary-foreground: #000000;
- --muted: #f5f5f5;
- --muted-foreground: #525252;
- --accent: #ebebeb;
+ --secondary: #14b8a6;
+ --secondary-foreground: #ffffff;
+ --muted: #f0f0f0;
+ --muted-foreground: #333333;
+ --accent: #f59e0b;
--accent-foreground: #000000;
- --destructive: #e54b4f;
+ --destructive: #ef4444;
--destructive-foreground: #ffffff;
- --border: #e4e4e4;
- --input: #ebebeb;
- --ring: #000000;
- --chart-1: #ffae04;
- --chart-2: #2d62ef;
- --chart-3: #a4a4a4;
- --chart-4: #e4e4e4;
- --chart-5: #747474;
- --sidebar: #fcfcfc;
+ --border: #000000;
+ --input: #737373;
+ --ring: #a5b4fc;
+ --chart-1: #4f46e5;
+ --chart-2: #14b8a6;
+ --chart-3: #f59e0b;
+ --chart-4: #ec4899;
+ --chart-5: #22c55e;
+ --sidebar: #f7f9f3;
--sidebar-foreground: #000000;
- --sidebar-primary: #000000;
+ --sidebar-primary: #4f46e5;
--sidebar-primary-foreground: #ffffff;
- --sidebar-accent: #ebebeb;
+ --sidebar-accent: #f59e0b;
--sidebar-accent-foreground: #000000;
- --sidebar-border: #ebebeb;
- --sidebar-ring: #000000;
- --font-sans: Geist, sans-serif;
- --font-serif: Georgia, serif;
- --font-mono: Geist Mono, monospace;
- --radius: 0.5rem;
+ --sidebar-border: #000000;
+ --sidebar-ring: #a5b4fc;
+ --font-sans: DM Sans, sans-serif;
+ --font-serif: DM Sans, sans-serif;
+ --font-mono: Space Mono, monospace;
+ --radius: 1rem;
--shadow-x: 0px;
- --shadow-y: 1px;
- --shadow-blur: 2px;
+ --shadow-y: 0px;
+ --shadow-blur: 0px;
--shadow-spread: 0px;
- --shadow-opacity: 0.18;
- --shadow-color: hsl(0 0% 0%);
- --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
- --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
- --shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
- --shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
- --shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18);
- --shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18);
- --shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18);
- --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45);
- --tracking-normal: 0em;
+ --shadow-opacity: 0.05;
+ --shadow-color: #1a1a1a;
+ --shadow-2xs: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.03);
+ --shadow-xs: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.03);
+ --shadow-sm: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.05);
+ --shadow: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.05);
+ --shadow-md: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 2px 4px -1px hsl(0 0% 10.1961% / 0.05);
+ --shadow-lg: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 4px 6px -1px hsl(0 0% 10.1961% / 0.05);
+ --shadow-xl: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 8px 10px -1px hsl(0 0% 10.1961% / 0.05);
+ --shadow-2xl: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.13);
+ --tracking-normal: normal;
--spacing: 0.25rem;
}
.dark {
--background: #000000;
--foreground: #ffffff;
- --card: #090909;
+ --card: #1a212b;
--card-foreground: #ffffff;
- --popover: #121212;
+ --popover: #1a212b;
--popover-foreground: #ffffff;
- --primary: #ffffff;
+ --primary: #818cf8;
--primary-foreground: #000000;
- --secondary: #222222;
- --secondary-foreground: #ffffff;
- --muted: #1d1d1d;
- --muted-foreground: #a4a4a4;
- --accent: #333333;
- --accent-foreground: #ffffff;
- --destructive: #ff5b5b;
+ --secondary: #2dd4bf;
+ --secondary-foreground: #000000;
+ --muted: #333333;
+ --muted-foreground: #cccccc;
+ --accent: #fcd34d;
+ --accent-foreground: #000000;
+ --destructive: #f87171;
--destructive-foreground: #000000;
- --border: #242424;
- --input: #333333;
- --ring: #a4a4a4;
- --chart-1: #ffae04;
- --chart-2: #2671f4;
- --chart-3: #747474;
- --chart-4: #525252;
- --chart-5: #e4e4e4;
- --sidebar: #121212;
+ --border: #545454;
+ --input: #ffffff;
+ --ring: #818cf8;
+ --chart-1: #818cf8;
+ --chart-2: #2dd4bf;
+ --chart-3: #fcd34d;
+ --chart-4: #f472b6;
+ --chart-5: #4ade80;
+ --sidebar: #000000;
--sidebar-foreground: #ffffff;
- --sidebar-primary: #ffffff;
+ --sidebar-primary: #818cf8;
--sidebar-primary-foreground: #000000;
- --sidebar-accent: #333333;
- --sidebar-accent-foreground: #ffffff;
- --sidebar-border: #333333;
- --sidebar-ring: #a4a4a4;
- --font-sans: Geist, sans-serif;
- --font-serif: Georgia, serif;
- --font-mono: Geist Mono, monospace;
- --radius: 0.5rem;
+ --sidebar-accent: #fcd34d;
+ --sidebar-accent-foreground: #000000;
+ --sidebar-border: #ffffff;
+ --sidebar-ring: #818cf8;
+ --font-sans: DM Sans, sans-serif;
+ --font-serif: DM Sans, sans-serif;
+ --font-mono: Space Mono, monospace;
+ --radius: 1rem;
--shadow-x: 0px;
- --shadow-y: 1px;
- --shadow-blur: 2px;
+ --shadow-y: 0px;
+ --shadow-blur: 0px;
--shadow-spread: 0px;
- --shadow-opacity: 0.18;
- --shadow-color: hsl(0 0% 0%);
- --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
- --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
- --shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
- --shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
- --shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18);
- --shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18);
- --shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18);
- --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45);
+ --shadow-opacity: 0.05;
+ --shadow-color: #1a1a1a;
+ --shadow-2xs: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.03);
+ --shadow-xs: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.03);
+ --shadow-sm: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.05);
+ --shadow: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.05);
+ --shadow-md: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 2px 4px -1px hsl(0 0% 10.1961% / 0.05);
+ --shadow-lg: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 4px 6px -1px hsl(0 0% 10.1961% / 0.05);
+ --shadow-xl: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 8px 10px -1px hsl(0 0% 10.1961% / 0.05);
+ --shadow-2xl: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.13);
}
@theme inline {
@@ -177,6 +177,17 @@ @theme inline {
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
+
+ --tracking-tighter: calc(var(--tracking-normal) - 0.05em);
+ --tracking-tight: calc(var(--tracking-normal) - 0.025em);
+ --tracking-normal: var(--tracking-normal);
+ --tracking-wide: calc(var(--tracking-normal) + 0.025em);
+ --tracking-wider: calc(var(--tracking-normal) + 0.05em);
+ --tracking-widest: calc(var(--tracking-normal) + 0.1em);
+}
+
+body {
+ letter-spacing: var(--tracking-normal);
}
@layer base {
diff --git a/resources/js/components/AppSidebarHeader.vue b/resources/js/components/AppSidebarHeader.vue
index c0512420..29ae7e9b 100644
--- a/resources/js/components/AppSidebarHeader.vue
+++ b/resources/js/components/AppSidebarHeader.vue
@@ -15,12 +15,11 @@ withDefaults(
-
-
-
+
\ No newline at end of file
diff --git a/resources/js/components/DatePicker.vue b/resources/js/components/DatePicker.vue
index 28a3d3e5..f27f3fc2 100644
--- a/resources/js/components/DatePicker.vue
+++ b/resources/js/components/DatePicker.vue
@@ -1,158 +1,226 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ :
+
+
+
+
-
-
\ No newline at end of file
+
diff --git a/resources/js/components/MediaDropzone.vue b/resources/js/components/MediaDropzone.vue
new file mode 100644
index 00000000..e7ff4cb1
--- /dev/null
+++ b/resources/js/components/MediaDropzone.vue
@@ -0,0 +1,318 @@
+
+
+
+
+
+33
\ No newline at end of file
diff --git a/resources/js/components/posts/previews/BlueskyPreview.vue b/resources/js/components/posts/previews/BlueskyPreview.vue
index c889c77f..898629ec 100644
--- a/resources/js/components/posts/previews/BlueskyPreview.vue
+++ b/resources/js/components/posts/previews/BlueskyPreview.vue
@@ -1,14 +1,4 @@
-
-
+
\ No newline at end of file
diff --git a/resources/js/components/posts/previews/FacebookPreview.vue b/resources/js/components/posts/previews/FacebookPreview.vue
index 7c153169..0131e1af 100644
--- a/resources/js/components/posts/previews/FacebookPreview.vue
+++ b/resources/js/components/posts/previews/FacebookPreview.vue
@@ -1,13 +1,6 @@
-
-
+
\ No newline at end of file
diff --git a/resources/js/components/posts/previews/LinkedInPreview.vue b/resources/js/components/posts/previews/LinkedInPreview.vue
index ebf13774..b7ee20ec 100644
--- a/resources/js/components/posts/previews/LinkedInPreview.vue
+++ b/resources/js/components/posts/previews/LinkedInPreview.vue
@@ -1,16 +1,4 @@
-
-
+
\ No newline at end of file
diff --git a/resources/js/components/posts/previews/MastodonPreview.vue b/resources/js/components/posts/previews/MastodonPreview.vue
index f1c9d78d..9116d8ea 100644
--- a/resources/js/components/posts/previews/MastodonPreview.vue
+++ b/resources/js/components/posts/previews/MastodonPreview.vue
@@ -1,15 +1,4 @@
-
-
+
\ No newline at end of file
diff --git a/resources/js/components/posts/previews/PinterestPreview.vue b/resources/js/components/posts/previews/PinterestPreview.vue
index 224ca958..4d7cadda 100644
--- a/resources/js/components/posts/previews/PinterestPreview.vue
+++ b/resources/js/components/posts/previews/PinterestPreview.vue
@@ -1,20 +1,7 @@
-
-
-
-
-
- Type:
-
-
-
-
-
- Board:
-
-
-
-
-
- No boards found. Create a board on Pinterest first.
-
-
+
\ No newline at end of file
diff --git a/resources/js/components/posts/previews/TikTokPreview.vue b/resources/js/components/posts/previews/TikTokPreview.vue
index 750da203..de11730f 100644
--- a/resources/js/components/posts/previews/TikTokPreview.vue
+++ b/resources/js/components/posts/previews/TikTokPreview.vue
@@ -1,15 +1,6 @@
-