diff --git a/app/Actions/Signature/CreateSignature.php b/app/Actions/Signature/CreateSignature.php new file mode 100644 index 00000000..2aedeb2e --- /dev/null +++ b/app/Actions/Signature/CreateSignature.php @@ -0,0 +1,19 @@ +signatures()->create([ + 'name' => data_get($data, 'name'), + 'content' => data_get($data, 'content'), + ]); + } +} diff --git a/app/Actions/Signature/DeleteSignature.php b/app/Actions/Signature/DeleteSignature.php new file mode 100644 index 00000000..896e0616 --- /dev/null +++ b/app/Actions/Signature/DeleteSignature.php @@ -0,0 +1,15 @@ +delete(); + } +} diff --git a/app/Actions/Signature/UpdateSignature.php b/app/Actions/Signature/UpdateSignature.php new file mode 100644 index 00000000..f02319ac --- /dev/null +++ b/app/Actions/Signature/UpdateSignature.php @@ -0,0 +1,20 @@ +update([ + 'name' => data_get($data, 'name'), + 'content' => data_get($data, 'content'), + ]); + + return $signature; + } +} diff --git a/app/Http/Controllers/Api/SignatureController.php b/app/Http/Controllers/Api/SignatureController.php new file mode 100644 index 00000000..e03ef901 --- /dev/null +++ b/app/Http/Controllers/Api/SignatureController.php @@ -0,0 +1,58 @@ +workspace->signatures()->latest()->get(); + + return SignatureResource::collection($signatures); + } + + public function store(StoreSignatureRequest $request): JsonResponse + { + $signature = CreateSignature::execute($request->workspace, $request->validated()); + + return (new SignatureResource($signature)) + ->response() + ->setStatusCode(Response::HTTP_CREATED); + } + + public function update(UpdateSignatureRequest $request, WorkspaceSignature $signature): SignatureResource + { + if ($signature->workspace_id !== $request->workspace->id) { + abort(Response::HTTP_NOT_FOUND); + } + + $signature = UpdateSignature::execute($signature, $request->validated()); + + return new SignatureResource($signature); + } + + public function destroy(Request $request, WorkspaceSignature $signature): JsonResponse + { + if ($signature->workspace_id !== $request->workspace->id) { + abort(Response::HTTP_NOT_FOUND); + } + + DeleteSignature::execute($signature); + + return response()->json(null, Response::HTTP_NO_CONTENT); + } +} diff --git a/app/Http/Controllers/App/WorkspaceSignatureController.php b/app/Http/Controllers/App/WorkspaceSignatureController.php new file mode 100644 index 00000000..f0d583ca --- /dev/null +++ b/app/Http/Controllers/App/WorkspaceSignatureController.php @@ -0,0 +1,113 @@ +user()->currentWorkspace; + + if (! $workspace) { + return redirect()->route('app.workspaces.create'); + } + + $this->authorize('createPost', $workspace); + + $signatures = $workspace->signatures() + ->when($request->input('search'), fn ($query, $search) => $query->where('name', 'ilike', "%{$search}%")) + ->latest() + ->paginate(config('app.pagination.default')); + + return Inertia::render('signatures/Index', [ + 'workspace' => $workspace, + 'signatures' => Inertia::scroll(fn () => $signatures), + 'filters' => [ + 'search' => $request->input('search', ''), + ], + ]); + } + + public function store(Request $request): RedirectResponse + { + $workspace = $request->user()->currentWorkspace; + + if (! $workspace) { + return redirect()->route('app.workspaces.create'); + } + + $this->authorize('createPost', $workspace); + + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'content' => ['required', 'string'], + ]); + + CreateSignature::execute($workspace, $validated); + + session()->flash('flash.banner', __('signatures.flash.created')); + session()->flash('flash.bannerStyle', 'success'); + + return redirect()->route('app.signatures.index'); + } + + public function update(Request $request, WorkspaceSignature $signature): RedirectResponse + { + $workspace = $request->user()->currentWorkspace; + + if (! $workspace) { + return redirect()->route('app.workspaces.create'); + } + + $this->authorize('createPost', $workspace); + + if ($signature->workspace_id !== $workspace->id) { + abort(404); + } + + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'content' => ['required', 'string'], + ]); + + UpdateSignature::execute($signature, $validated); + + session()->flash('flash.banner', __('signatures.flash.updated')); + session()->flash('flash.bannerStyle', 'success'); + + return redirect()->route('app.signatures.index'); + } + + public function destroy(Request $request, WorkspaceSignature $signature): RedirectResponse + { + $workspace = $request->user()->currentWorkspace; + + if (! $workspace) { + return redirect()->route('app.workspaces.create'); + } + + $this->authorize('createPost', $workspace); + + if ($signature->workspace_id !== $workspace->id) { + abort(404); + } + + DeleteSignature::execute($signature); + + session()->flash('flash.banner', __('signatures.flash.deleted')); + session()->flash('flash.bannerStyle', 'success'); + + return redirect()->route('app.signatures.index'); + } +} diff --git a/app/Http/Requests/Api/Signature/StoreSignatureRequest.php b/app/Http/Requests/Api/Signature/StoreSignatureRequest.php new file mode 100644 index 00000000..ee83a5f5 --- /dev/null +++ b/app/Http/Requests/Api/Signature/StoreSignatureRequest.php @@ -0,0 +1,26 @@ + + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'content' => ['required', 'string'], + ]; + } +} diff --git a/app/Http/Requests/Api/Signature/UpdateSignatureRequest.php b/app/Http/Requests/Api/Signature/UpdateSignatureRequest.php new file mode 100644 index 00000000..1d12b586 --- /dev/null +++ b/app/Http/Requests/Api/Signature/UpdateSignatureRequest.php @@ -0,0 +1,26 @@ + + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'content' => ['required', 'string'], + ]; + } +} diff --git a/app/Http/Resources/Api/SignatureResource.php b/app/Http/Resources/Api/SignatureResource.php new file mode 100644 index 00000000..879b3aec --- /dev/null +++ b/app/Http/Resources/Api/SignatureResource.php @@ -0,0 +1,25 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'content' => $this->content, + 'created_at' => $this->created_at->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at->format('Y-m-d H:i:s'), + ]; + } +} diff --git a/app/Mcp/Tools/Signature/CreateSignatureTool.php b/app/Mcp/Tools/Signature/CreateSignatureTool.php new file mode 100644 index 00000000..81ed8d60 --- /dev/null +++ b/app/Mcp/Tools/Signature/CreateSignatureTool.php @@ -0,0 +1,37 @@ +validate([ + 'name' => ['required', 'string', 'max:255'], + 'content' => ['required', 'string'], + ]); + + $signature = CreateSignature::execute($request->user()->currentWorkspace, $validated); + + return Response::structured($signature->toArray()); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'name' => $schema->string()->required()->description('The signature name.'), + 'content' => $schema->string()->required()->description('The signature content (hashtags, links, custom text — anything you want to append to posts).'), + ]; + } +} diff --git a/app/Mcp/Tools/Signature/DeleteSignatureTool.php b/app/Mcp/Tools/Signature/DeleteSignatureTool.php new file mode 100644 index 00000000..ae2dade6 --- /dev/null +++ b/app/Mcp/Tools/Signature/DeleteSignatureTool.php @@ -0,0 +1,39 @@ +user()->current_workspace_id) + ->find(data_get($request->validate(['signature_id' => ['required', 'string']]), 'signature_id')); + + if (! $signature) { + return Response::error('Signature not found.'); + } + + DeleteSignature::execute($signature); + + return Response::structured(['deleted' => true]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'signature_id' => $schema->string()->required()->description('The signature ID to delete.'), + ]; + } +} diff --git a/app/Mcp/Tools/Signature/ListSignaturesTool.php b/app/Mcp/Tools/Signature/ListSignaturesTool.php new file mode 100644 index 00000000..31d2e33e --- /dev/null +++ b/app/Mcp/Tools/Signature/ListSignaturesTool.php @@ -0,0 +1,24 @@ +user()->currentWorkspace->signatures()->latest()->get(); + + return Response::structured($signatures->toArray()); + } +} diff --git a/app/Mcp/Tools/Signature/UpdateSignatureTool.php b/app/Mcp/Tools/Signature/UpdateSignatureTool.php new file mode 100644 index 00000000..0747cf16 --- /dev/null +++ b/app/Mcp/Tools/Signature/UpdateSignatureTool.php @@ -0,0 +1,47 @@ +validate([ + 'signature_id' => ['required', 'string'], + 'name' => ['required', 'string', 'max:255'], + 'content' => ['required', 'string'], + ]); + + $signature = WorkspaceSignature::where('workspace_id', $request->user()->current_workspace_id) + ->find(data_get($validated, 'signature_id')); + + if (! $signature) { + return Response::error('Signature not found.'); + } + + $signature = UpdateSignature::execute($signature, $validated); + + return Response::structured($signature->toArray()); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'signature_id' => $schema->string()->required()->description('The signature ID.'), + 'name' => $schema->string()->required()->description('The new name.'), + 'content' => $schema->string()->required()->description('The new content (hashtags, links, custom text).'), + ]; + } +} diff --git a/app/Models/WorkspaceSignature.php b/app/Models/WorkspaceSignature.php new file mode 100644 index 00000000..8437a1ec --- /dev/null +++ b/app/Models/WorkspaceSignature.php @@ -0,0 +1,29 @@ + */ + use HasFactory, HasUuids, SoftDeletes; + + protected $fillable = [ + 'workspace_id', + 'name', + 'content', + ]; + + public function workspace(): BelongsTo + { + return $this->belongsTo(Workspace::class); + } +} diff --git a/database/factories/WorkspaceSignatureFactory.php b/database/factories/WorkspaceSignatureFactory.php new file mode 100644 index 00000000..9c2dd4e3 --- /dev/null +++ b/database/factories/WorkspaceSignatureFactory.php @@ -0,0 +1,29 @@ + + */ +class WorkspaceSignatureFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'workspace_id' => Workspace::factory(), + 'name' => fake()->words(2, true), + 'content' => '#'.implode(' #', fake()->words(5)), + ]; + } +} diff --git a/database/migrations/2026_01_17_152319_create_workspace_signatures_table.php b/database/migrations/2026_01_17_152319_create_workspace_signatures_table.php new file mode 100644 index 00000000..75d5b4ee --- /dev/null +++ b/database/migrations/2026_01_17_152319_create_workspace_signatures_table.php @@ -0,0 +1,33 @@ +uuid('id')->primary(); + $table->foreignUuid('workspace_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->text('content'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('workspace_signatures'); + } +}; diff --git a/lang/en/signatures.php b/lang/en/signatures.php new file mode 100644 index 00000000..c03b3269 --- /dev/null +++ b/lang/en/signatures.php @@ -0,0 +1,54 @@ + 'Signatures', + 'description' => 'Create reusable signatures to quickly append to your posts', + 'search' => 'Search signatures...', + 'new' => 'New signature', + 'empty_title' => 'No signatures yet', + 'empty_description' => 'Create signatures to quickly append hashtags, links, or any reusable text to your posts', + 'no_search_results' => 'No signatures match your search', + 'try_different_search' => 'Try a different keyword or clear the search.', + 'table' => [ + 'name' => 'Name', + 'content' => 'Content', + 'created_at' => 'Created', + ], + + 'create' => [ + 'title' => 'Create signature', + 'description' => 'Give your signature a name and the content to append (hashtags, links, custom text — anything you reuse).', + 'name' => 'Name', + 'name_placeholder' => 'e.g. Marketing, Travel, Brand sign-off', + 'content' => 'Content', + 'content_placeholder' => "#marketing #socialmedia\nLearn more: https://yourbrand.com", + 'content_hint' => 'Hashtags, links, custom intros, signoffs — anything you append to posts.', + 'submit' => 'Create signature', + 'submitting' => 'Creating...', + ], + + 'edit' => [ + 'title' => 'Edit signature', + 'description' => 'Update the name and content for this signature.', + 'name' => 'Name', + 'name_placeholder' => 'e.g. Marketing, Travel, Brand sign-off', + 'content' => 'Content', + 'content_placeholder' => "#marketing #socialmedia\nLearn more: https://yourbrand.com", + 'content_hint' => 'Hashtags, links, custom intros, signoffs — anything you append to posts.', + 'submit' => 'Save changes', + 'submitting' => 'Saving...', + ], + + 'delete' => [ + 'title' => 'Delete signature', + 'description' => 'Are you sure you want to delete this signature? This action cannot be undone.', + 'confirm' => 'Delete', + 'cancel' => 'Cancel', + ], + + 'flash' => [ + 'created' => 'Signature created.', + 'updated' => 'Signature updated.', + 'deleted' => 'Signature deleted.', + ], +]; diff --git a/lang/es/signatures.php b/lang/es/signatures.php new file mode 100644 index 00000000..099d7891 --- /dev/null +++ b/lang/es/signatures.php @@ -0,0 +1,54 @@ + 'Firmas', + 'description' => 'Crea firmas reutilizables para añadir rápidamente a tus posts', + 'search' => 'Buscar firmas...', + 'new' => 'Nueva firma', + 'empty_title' => 'Aún no hay firmas', + 'empty_description' => 'Crea firmas para añadir hashtags, links o cualquier texto reutilizable a tus posts', + 'no_search_results' => 'Ninguna firma coincide con tu búsqueda', + 'try_different_search' => 'Prueba otra palabra clave o limpia la búsqueda.', + 'table' => [ + 'name' => 'Nombre', + 'content' => 'Contenido', + 'created_at' => 'Creado', + ], + + 'create' => [ + 'title' => 'Crear firma', + 'description' => 'Dale un nombre a tu firma y el contenido para añadir (hashtags, links, texto libre — lo que reutilizas).', + 'name' => 'Nombre', + 'name_placeholder' => 'ej: Marketing, Viaje, Cierre de marca', + 'content' => 'Contenido', + 'content_placeholder' => "#marketing #socialmedia\nMás info: https://tumarca.com", + 'content_hint' => 'Hashtags, links, intros, cierres — cualquier cosa que añades a los posts.', + 'submit' => 'Crear firma', + 'submitting' => 'Creando...', + ], + + 'edit' => [ + 'title' => 'Editar firma', + 'description' => 'Actualiza el nombre y el contenido de esta firma.', + 'name' => 'Nombre', + 'name_placeholder' => 'ej: Marketing, Viaje, Cierre de marca', + 'content' => 'Contenido', + 'content_placeholder' => "#marketing #socialmedia\nMás info: https://tumarca.com", + 'content_hint' => 'Hashtags, links, intros, cierres — cualquier cosa que añades a los posts.', + 'submit' => 'Guardar cambios', + 'submitting' => 'Guardando...', + ], + + 'delete' => [ + 'title' => 'Eliminar firma', + 'description' => '¿Seguro que quieres eliminar esta firma? Esta acción no se puede deshacer.', + 'confirm' => 'Eliminar', + 'cancel' => 'Cancelar', + ], + + 'flash' => [ + 'created' => 'Firma creada.', + 'updated' => 'Firma actualizada.', + 'deleted' => 'Firma eliminada.', + ], +]; diff --git a/lang/pt-BR/signatures.php b/lang/pt-BR/signatures.php new file mode 100644 index 00000000..f1d998da --- /dev/null +++ b/lang/pt-BR/signatures.php @@ -0,0 +1,54 @@ + 'Assinaturas', + 'description' => 'Crie assinaturas reutilizáveis pra anexar rapidamente nos seus posts', + 'search' => 'Buscar assinaturas...', + 'new' => 'Nova assinatura', + 'empty_title' => 'Nenhuma assinatura ainda', + 'empty_description' => 'Crie assinaturas pra anexar hashtags, links ou qualquer texto reutilizável nos seus posts', + 'no_search_results' => 'Nenhuma assinatura corresponde à busca', + 'try_different_search' => 'Tente outra palavra-chave ou limpe a busca.', + 'table' => [ + 'name' => 'Nome', + 'content' => 'Conteúdo', + 'created_at' => 'Criado em', + ], + + 'create' => [ + 'title' => 'Criar assinatura', + 'description' => 'Dê um nome à sua assinatura e o conteúdo pra anexar (hashtags, links, texto livre — o que você reutiliza).', + 'name' => 'Nome', + 'name_placeholder' => 'ex: Marketing, Viagem, Encerramento da marca', + 'content' => 'Conteúdo', + 'content_placeholder' => "#marketing #socialmedia\nSaiba mais: https://suamarca.com", + 'content_hint' => 'Hashtags, links, intros, assinaturas — qualquer coisa que você anexa nos posts.', + 'submit' => 'Criar assinatura', + 'submitting' => 'Criando...', + ], + + 'edit' => [ + 'title' => 'Editar assinatura', + 'description' => 'Atualize o nome e o conteúdo desta assinatura.', + 'name' => 'Nome', + 'name_placeholder' => 'ex: Marketing, Viagem, Encerramento da marca', + 'content' => 'Conteúdo', + 'content_placeholder' => "#marketing #socialmedia\nSaiba mais: https://suamarca.com", + 'content_hint' => 'Hashtags, links, intros, assinaturas — qualquer coisa que você anexa nos posts.', + 'submit' => 'Salvar alterações', + 'submitting' => 'Salvando...', + ], + + 'delete' => [ + 'title' => 'Deletar assinatura', + 'description' => 'Tem certeza que quer deletar esta assinatura? Esta ação não pode ser desfeita.', + 'confirm' => 'Deletar', + 'cancel' => 'Cancelar', + ], + + 'flash' => [ + 'created' => 'Assinatura criada.', + 'updated' => 'Assinatura atualizada.', + 'deleted' => 'Assinatura deletada.', + ], +]; diff --git a/resources/js/components/posts/SignaturesModal.vue b/resources/js/components/posts/SignaturesModal.vue new file mode 100644 index 00000000..9c6fa34a --- /dev/null +++ b/resources/js/components/posts/SignaturesModal.vue @@ -0,0 +1,68 @@ + + + diff --git a/resources/js/components/signatures/CreateDialog.vue b/resources/js/components/signatures/CreateDialog.vue new file mode 100644 index 00000000..5396423b --- /dev/null +++ b/resources/js/components/signatures/CreateDialog.vue @@ -0,0 +1,95 @@ + + +