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 @@ + + + diff --git a/resources/js/components/PhoneMockup.vue b/resources/js/components/PhoneMockup.vue new file mode 100644 index 00000000..0f053aba --- /dev/null +++ b/resources/js/components/PhoneMockup.vue @@ -0,0 +1,68 @@ + + + diff --git a/resources/js/components/posts/PostForm.vue b/resources/js/components/posts/PostForm.vue new file mode 100644 index 00000000..2b9f10ca --- /dev/null +++ b/resources/js/components/posts/PostForm.vue @@ -0,0 +1,419 @@ + + +