From efe0be0db05e6e4d677005c07ddfabdcfad27c6f Mon Sep 17 00:00:00 2001 From: nelson kenzo tamashiro Date: Sun, 29 Mar 2026 18:53:33 -0300 Subject: [PATCH 1/2] feat: notifications --- .../Commands/SendSystemNotification.php | 40 ++++ .../Notification/NotificationController.php | 145 ++++++++++++ .../API/Payment/WooviWebhookController.php | 3 + api/app/Http/Controllers/Controller.php | 1 + api/app/Http/Middleware/SetLocale.php | 22 ++ .../Notification/NotificationResource.php | 36 +++ api/app/Notifications/CampaignApproved.php | 37 +++ api/app/Notifications/DonationReceived.php | 38 ++++ api/app/Notifications/SystemNotification.php | 35 +++ api/app/Notifications/WithdrawalProcessed.php | 37 +++ api/app/Services/Campaign/CampaignService.php | 6 + .../Notification/NotificationService.php | 45 ++++ .../Withdrawal/WithdrawalProcessor.php | 3 + api/bootstrap/app.php | 3 + ...3_29_212125_create_notifications_table.php | 31 +++ api/lang/en/notifications.php | 22 ++ api/lang/es/notifications.php | 22 ++ api/lang/pt/notifications.php | 22 ++ api/public/api-docs.json | 215 ++++++++++++++++++ api/routes/api.php | 6 + web/i18n/en.json | 8 + web/i18n/es.json | 8 + web/i18n/pt.json | 8 + .../notifications/notification-item.tsx | 31 +-- .../notifications/notifications-list.tsx | 114 ++++------ web/src/app/profile/notifications/page.tsx | 6 - 26 files changed, 850 insertions(+), 94 deletions(-) create mode 100644 api/app/Console/Commands/SendSystemNotification.php create mode 100644 api/app/Http/Controllers/API/Notification/NotificationController.php create mode 100644 api/app/Http/Middleware/SetLocale.php create mode 100644 api/app/Http/Resources/Notification/NotificationResource.php create mode 100644 api/app/Notifications/CampaignApproved.php create mode 100644 api/app/Notifications/DonationReceived.php create mode 100644 api/app/Notifications/SystemNotification.php create mode 100644 api/app/Notifications/WithdrawalProcessed.php create mode 100644 api/app/Services/Notification/NotificationService.php create mode 100644 api/database/migrations/2026_03_29_212125_create_notifications_table.php create mode 100644 api/lang/en/notifications.php create mode 100644 api/lang/es/notifications.php create mode 100644 api/lang/pt/notifications.php diff --git a/api/app/Console/Commands/SendSystemNotification.php b/api/app/Console/Commands/SendSystemNotification.php new file mode 100644 index 0000000..3a08318 --- /dev/null +++ b/api/app/Console/Commands/SendSystemNotification.php @@ -0,0 +1,40 @@ +option('title-key'); + $messageKey = $this->option('message-key'); + + if (empty($titleKey) || empty($messageKey)) { + $this->error('Both --title-key and --message-key are required.'); + + return self::FAILURE; + } + + $notification = new SystemNotification($titleKey, $messageKey); + + $this->info('Sending system notification to all users...'); + + $notificationService->notifyAllUsers($notification); + + $this->info('System notification dispatched successfully.'); + + return self::SUCCESS; + } +} diff --git a/api/app/Http/Controllers/API/Notification/NotificationController.php b/api/app/Http/Controllers/API/Notification/NotificationController.php new file mode 100644 index 0000000..bfb6bd8 --- /dev/null +++ b/api/app/Http/Controllers/API/Notification/NotificationController.php @@ -0,0 +1,145 @@ + []]], + tags: ['Notifications'], + parameters: [ + new OA\Parameter( + name: 'page', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer'), + description: 'Page number' + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Notifications retrieved successfully', + content: new OA\JsonContent( + type: 'object', + properties: [ + new OA\Property( + property: 'data', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/NotificationResource') + ), + new OA\Property( + property: 'meta', + type: 'object', + properties: [ + new OA\Property(property: 'unreadCount', type: 'integer'), + ] + ), + ] + ) + ), + ] + )] + public function index(Request $request) + { + $user = $request->user(); + $notifications = $this->notificationService->listForUser($user); + $unreadCount = $this->notificationService->unreadCount($user); + + return NotificationResource::collection($notifications) + ->additional(['meta' => ['unreadCount' => $unreadCount]]); + } + + #[OA\Post( + operationId: 'markNotificationAsRead', + path: '/api/notifications/{id}/read', + summary: 'Mark a notification as read', + security: [['bearerAuth' => []]], + tags: ['Notifications'], + parameters: [ + new OA\Parameter( + name: 'id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string', format: 'uuid'), + description: 'Notification ID' + ), + ], + responses: [ + new OA\Response(response: 204, description: 'Notification marked as read'), + ] + )] + public function markAsRead(Request $request, string $id) + { + $this->notificationService->markAsRead($request->user(), $id); + + return response()->noContent(); + } + + #[OA\Post( + operationId: 'markAllNotificationsAsRead', + path: '/api/notifications/read-all', + summary: 'Mark all notifications as read', + security: [['bearerAuth' => []]], + tags: ['Notifications'], + responses: [ + new OA\Response(response: 204, description: 'All notifications marked as read'), + ] + )] + public function markAllAsRead(Request $request) + { + $this->notificationService->markAllAsRead($request->user()); + + return response()->noContent(); + } + + #[OA\Delete( + operationId: 'deleteNotification', + path: '/api/notifications/{id}', + summary: 'Delete a notification', + security: [['bearerAuth' => []]], + tags: ['Notifications'], + parameters: [ + new OA\Parameter( + name: 'id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string', format: 'uuid'), + description: 'Notification ID' + ), + ], + responses: [ + new OA\Response(response: 204, description: 'Notification deleted'), + ] + )] + public function destroy(Request $request, string $id) + { + $this->notificationService->delete($request->user(), $id); + + return response()->noContent(); + } +} diff --git a/api/app/Http/Controllers/API/Payment/WooviWebhookController.php b/api/app/Http/Controllers/API/Payment/WooviWebhookController.php index 668b981..12b2915 100644 --- a/api/app/Http/Controllers/API/Payment/WooviWebhookController.php +++ b/api/app/Http/Controllers/API/Payment/WooviWebhookController.php @@ -7,6 +7,7 @@ use App\Events\DonationPaid; use App\Http\Controllers\Controller; use App\Models\Donation; +use App\Notifications\DonationReceived; use Illuminate\Http\Request; use OpenPix\PhpSdk\Client; @@ -80,6 +81,8 @@ public function handleChargePaidWebhook(Request $request) event(new DonationPaid($donation->external_reference)); + $donation->campaign->user->notify(new DonationReceived($donation->load('campaign'))); + return response()->json(['message' => 'Success.']); } diff --git a/api/app/Http/Controllers/Controller.php b/api/app/Http/Controllers/Controller.php index 11d2e84..3e7e31e 100644 --- a/api/app/Http/Controllers/Controller.php +++ b/api/app/Http/Controllers/Controller.php @@ -29,6 +29,7 @@ #[OA\Tag(name: 'Withdrawals', description: 'Withdrawal management endpoints')] #[OA\Tag(name: 'Reports', description: 'Report management endpoints')] #[OA\Tag(name: 'Leaderboard', description: 'Leaderboard endpoints')] +#[OA\Tag(name: 'Notifications', description: 'Notification management endpoints')] abstract class Controller { // diff --git a/api/app/Http/Middleware/SetLocale.php b/api/app/Http/Middleware/SetLocale.php new file mode 100644 index 0000000..618c21f --- /dev/null +++ b/api/app/Http/Middleware/SetLocale.php @@ -0,0 +1,22 @@ +getPreferredLanguage(['en', 'es', 'pt']); + + App::setLocale($locale ?? 'pt'); + + return $next($request); + } +} diff --git a/api/app/Http/Resources/Notification/NotificationResource.php b/api/app/Http/Resources/Notification/NotificationResource.php new file mode 100644 index 0000000..4468f50 --- /dev/null +++ b/api/app/Http/Resources/Notification/NotificationResource.php @@ -0,0 +1,36 @@ +data; + + return [ + 'id' => $this->id, + 'type' => $data['type'], + 'title' => __($data['titleKey'], $data['params'] ?? []), + 'message' => __($data['messageKey'], $data['params'] ?? []), + 'timestamp' => $this->created_at->toIso8601String(), + 'isRead' => $this->read_at !== null, + ]; + } +} diff --git a/api/app/Notifications/CampaignApproved.php b/api/app/Notifications/CampaignApproved.php new file mode 100644 index 0000000..4acc85e --- /dev/null +++ b/api/app/Notifications/CampaignApproved.php @@ -0,0 +1,37 @@ + 'campaign', + 'titleKey' => 'notifications.campaign.title', + 'messageKey' => 'notifications.campaign.message', + 'params' => [ + 'campaign' => $this->campaign->title, + 'campaignSlug' => $this->campaign->slug, + ], + ]; + } +} diff --git a/api/app/Notifications/DonationReceived.php b/api/app/Notifications/DonationReceived.php new file mode 100644 index 0000000..fe1a98d --- /dev/null +++ b/api/app/Notifications/DonationReceived.php @@ -0,0 +1,38 @@ + 'donation', + 'titleKey' => 'notifications.donation.title', + 'messageKey' => 'notifications.donation.message', + 'params' => [ + 'amount' => 'R$ '.number_format($this->donation->amount_cents / 100, 2, ',', '.'), + 'campaign' => $this->donation->campaign->title, + 'campaignSlug' => $this->donation->campaign->slug, + ], + ]; + } +} diff --git a/api/app/Notifications/SystemNotification.php b/api/app/Notifications/SystemNotification.php new file mode 100644 index 0000000..fea2fba --- /dev/null +++ b/api/app/Notifications/SystemNotification.php @@ -0,0 +1,35 @@ + 'system', + 'titleKey' => $this->titleKey, + 'messageKey' => $this->messageKey, + 'params' => $this->params, + ]; + } +} diff --git a/api/app/Notifications/WithdrawalProcessed.php b/api/app/Notifications/WithdrawalProcessed.php new file mode 100644 index 0000000..289a5da --- /dev/null +++ b/api/app/Notifications/WithdrawalProcessed.php @@ -0,0 +1,37 @@ + 'withdrawal', + 'titleKey' => 'notifications.withdrawal.title', + 'messageKey' => 'notifications.withdrawal.message', + 'params' => [ + 'amount' => 'R$ '.number_format($this->withdrawal->amountCents / 100, 2, ',', '.'), + 'campaign' => $this->withdrawal->campaign->title, + ], + ]; + } +} diff --git a/api/app/Services/Campaign/CampaignService.php b/api/app/Services/Campaign/CampaignService.php index d87a635..b9817bb 100644 --- a/api/app/Services/Campaign/CampaignService.php +++ b/api/app/Services/Campaign/CampaignService.php @@ -6,6 +6,7 @@ use App\Models\Campaign; use App\Models\User; +use App\Notifications\CampaignApproved; use App\Services\Donation\DonationService; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Str; @@ -104,9 +105,14 @@ public function create(array $data, User $user): Campaign public function update(Campaign $campaign, array $data): Campaign { + $previousStatus = $campaign->status; $data = $this->mapPersistentEntity($data); $campaign->update($data); + if ($campaign->status === Campaign::STATUS_OPEN && $previousStatus !== Campaign::STATUS_OPEN) { + $campaign->user->notify(new CampaignApproved($campaign)); + } + return $campaign; } diff --git a/api/app/Services/Notification/NotificationService.php b/api/app/Services/Notification/NotificationService.php new file mode 100644 index 0000000..c815a12 --- /dev/null +++ b/api/app/Services/Notification/NotificationService.php @@ -0,0 +1,45 @@ +notifications()->latest()->paginate(15); + } + + public function unreadCount(User $user): int + { + return $user->unreadNotifications()->count(); + } + + public function markAsRead(User $user, string $notificationId): void + { + $user->notifications()->findOrFail($notificationId)->markAsRead(); + } + + public function markAllAsRead(User $user): void + { + $user->unreadNotifications->markAsRead(); + } + + public function delete(User $user, string $notificationId): void + { + $user->notifications()->findOrFail($notificationId)->delete(); + } + + public function notifyAllUsers(Notification $notification): void + { + User::chunk(100, function ($users) use ($notification) { + NotificationFacade::send($users, $notification); + }); + } +} diff --git a/api/app/Services/Withdrawal/WithdrawalProcessor.php b/api/app/Services/Withdrawal/WithdrawalProcessor.php index d3aea39..fe71f7c 100644 --- a/api/app/Services/Withdrawal/WithdrawalProcessor.php +++ b/api/app/Services/Withdrawal/WithdrawalProcessor.php @@ -5,6 +5,7 @@ namespace App\Services\Withdrawal; use App\Models\Withdrawal; +use App\Notifications\WithdrawalProcessed; use Illuminate\Support\Str; use OpenPix\PhpSdk\Client; use OpenPix\PhpSdk\Request; @@ -51,5 +52,7 @@ public function process(Withdrawal $withdrawal) $withdrawal->status = 'paid'; $withdrawal->campaign()->decrement('available_balance_cents', $withdrawal->amountCents); $withdrawal->save(); + + $withdrawal->campaign->user->notify(new WithdrawalProcessed($withdrawal->load('campaign'))); } } diff --git a/api/bootstrap/app.php b/api/bootstrap/app.php index 930c5c3..9de13d6 100644 --- a/api/bootstrap/app.php +++ b/api/bootstrap/app.php @@ -14,6 +14,9 @@ ) ->withMiddleware(function (Middleware $middleware) { $middleware->statefulApi(); + $middleware->api(append: [ + \App\Http\Middleware\SetLocale::class, + ]); }) ->withExceptions(function (Exceptions $exceptions) { // diff --git a/api/database/migrations/2026_03_29_212125_create_notifications_table.php b/api/database/migrations/2026_03_29_212125_create_notifications_table.php new file mode 100644 index 0000000..d738032 --- /dev/null +++ b/api/database/migrations/2026_03_29_212125_create_notifications_table.php @@ -0,0 +1,31 @@ +uuid('id')->primary(); + $table->string('type'); + $table->morphs('notifiable'); + $table->text('data'); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('notifications'); + } +}; diff --git a/api/lang/en/notifications.php b/api/lang/en/notifications.php new file mode 100644 index 0000000..5b3dc9e --- /dev/null +++ b/api/lang/en/notifications.php @@ -0,0 +1,22 @@ + [ + 'title' => 'New donation received!', + 'message' => 'You received a donation of :amount on the campaign ":campaign"', + ], + 'campaign' => [ + 'title' => 'Campaign approved', + 'message' => 'Your campaign ":campaign" has been approved and is now active!', + ], + 'withdrawal' => [ + 'title' => 'Withdrawal processed', + 'message' => 'Your withdrawal of :amount has been processed successfully.', + ], + 'system' => [ + 'policy_update' => [ + 'title' => 'Policy update', + 'message' => 'We have updated our privacy policy. Click to learn more.', + ], + ], +]; diff --git a/api/lang/es/notifications.php b/api/lang/es/notifications.php new file mode 100644 index 0000000..a79f0b0 --- /dev/null +++ b/api/lang/es/notifications.php @@ -0,0 +1,22 @@ + [ + 'title' => '¡Nueva donación recibida!', + 'message' => 'Recibiste una donación de :amount en la campaña ":campaign"', + ], + 'campaign' => [ + 'title' => 'Campaña aprobada', + 'message' => '¡Tu campaña ":campaign" fue aprobada y ya está activa!', + ], + 'withdrawal' => [ + 'title' => 'Retiro procesado', + 'message' => 'Tu retiro de :amount fue procesado con éxito.', + ], + 'system' => [ + 'policy_update' => [ + 'title' => 'Actualización de política', + 'message' => 'Actualizamos nuestra política de privacidad. Haz clic para saber más.', + ], + ], +]; diff --git a/api/lang/pt/notifications.php b/api/lang/pt/notifications.php new file mode 100644 index 0000000..526b40e --- /dev/null +++ b/api/lang/pt/notifications.php @@ -0,0 +1,22 @@ + [ + 'title' => 'Nova doação recebida!', + 'message' => 'Você recebeu uma doação de :amount na campanha ":campaign"', + ], + 'campaign' => [ + 'title' => 'Campanha aprovada', + 'message' => 'Sua campanha ":campaign" foi aprovada e já está ativa!', + ], + 'withdrawal' => [ + 'title' => 'Saque processado', + 'message' => 'Seu saque de :amount foi processado com sucesso.', + ], + 'system' => [ + 'policy_update' => [ + 'title' => 'Atualização de política', + 'message' => 'Atualizamos nossa política de privacidade. Clique para saber mais.', + ], + ], +]; diff --git a/api/public/api-docs.json b/api/public/api-docs.json index 701b014..f822e66 100644 --- a/api/public/api-docs.json +++ b/api/public/api-docs.json @@ -823,6 +823,140 @@ } } }, + "/api/notifications": { + "get": { + "tags": [ + "Notifications" + ], + "summary": "List authenticated user notifications", + "operationId": "listNotifications", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "Page number", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Notifications retrieved successfully", + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationResource" + } + }, + "meta": { + "properties": { + "unreadCount": { + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/notifications/{id}/read": { + "post": { + "tags": [ + "Notifications" + ], + "summary": "Mark a notification as read", + "operationId": "markNotificationAsRead", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Notification ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Notification marked as read" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/notifications/read-all": { + "post": { + "tags": [ + "Notifications" + ], + "summary": "Mark all notifications as read", + "operationId": "markAllNotificationsAsRead", + "responses": { + "204": { + "description": "All notifications marked as read" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/notifications/{id}": { + "delete": { + "tags": [ + "Notifications" + ], + "summary": "Delete a notification", + "operationId": "deleteNotification", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Notification ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Notification deleted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/api/reports": { "get": { "tags": [ @@ -1005,6 +1139,44 @@ } } }, + "/api/profile/avatar": { + "post": { + "tags": [ + "Profile" + ], + "summary": "Upload user avatar", + "operationId": "uploadAvatar", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "avatar": { + "description": "Avatar image file (jpg, jpeg, png, max 4MB)", + "type": "string", + "format": "binary" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Avatar uploaded successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResource" + } + } + } + } + } + } + }, "/api/campaigns/{campaign}/withdrawals": { "get": { "tags": [ @@ -1773,6 +1945,10 @@ }, "name": { "type": "string" + }, + "avatarUrl": { + "type": "string", + "nullable": true } }, "type": "object", @@ -1983,6 +2159,37 @@ }, "type": "object" }, + "NotificationResource": { + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "donation", + "campaign", + "withdrawal", + "system" + ] + }, + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "isRead": { + "type": "boolean" + } + }, + "type": "object" + }, "PaymentResource": { "properties": { "method": { @@ -2101,6 +2308,10 @@ "type": "string", "nullable": true }, + "avatarUrl": { + "type": "string", + "nullable": true + }, "favoriteCampaignsCount": { "type": "integer", "nullable": true @@ -2228,6 +2439,10 @@ { "name": "Leaderboard", "description": "Leaderboard endpoints" + }, + { + "name": "Notifications", + "description": "Notification management endpoints" } ] } \ No newline at end of file diff --git a/api/routes/api.php b/api/routes/api.php index dee2187..684e732 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -8,6 +8,7 @@ use App\Http\Controllers\API\Comment\CommentController; use App\Http\Controllers\API\Comment\CommentReactionController; use App\Http\Controllers\API\Donation\DonationController; +use App\Http\Controllers\API\Notification\NotificationController; use App\Http\Controllers\API\Payment\WooviWebhookController; use App\Http\Controllers\API\ReportController; use App\Http\Controllers\API\User\OAuthController; @@ -41,3 +42,8 @@ Route::get('/leaderboard/campaigns', [LeaderboardController::class, 'topCampaigns']); Route::get('/leaderboard/donors', [LeaderboardController::class, 'topDonors']); Route::get('/leaderboard/creators', [LeaderboardController::class, 'topCreators']); + +Route::get('/notifications', [NotificationController::class, 'index']); +Route::post('/notifications/read-all', [NotificationController::class, 'markAllAsRead']); +Route::post('/notifications/{id}/read', [NotificationController::class, 'markAsRead']); +Route::delete('/notifications/{id}', [NotificationController::class, 'destroy']); diff --git a/web/i18n/en.json b/web/i18n/en.json index 9148d0e..cb11299 100644 --- a/web/i18n/en.json +++ b/web/i18n/en.json @@ -1182,5 +1182,13 @@ "description": "If you have questions about our Privacy Policy or about the processing of your personal data, contact our Data Protection Officer (DPO).", "button": "Contact us" } + }, + "notifications_page": { + "title": "Notifications", + "subtitle": "Follow all updates about your campaigns and donations.", + "new_count": "{count, plural, one {# new} other {# new}}", + "mark_all_read": "Mark all as read", + "mark_as_read": "Mark as read", + "empty": "You don't have any notifications at the moment." } } \ No newline at end of file diff --git a/web/i18n/es.json b/web/i18n/es.json index fbec062..b09bd64 100644 --- a/web/i18n/es.json +++ b/web/i18n/es.json @@ -1182,5 +1182,13 @@ "description": "Si tienes preguntas sobre nuestra Política de Privacidad o sobre el tratamiento de tus datos personales, contacta con nuestro Responsable de Protección de Datos (DPO).", "button": "Contáctanos" } + }, + "notifications_page": { + "title": "Notificaciones", + "subtitle": "Sigue todas las actualizaciones sobre tus campañas y donaciones.", + "new_count": "{count, plural, one {# nueva} other {# nuevas}}", + "mark_all_read": "Marcar todas como leídas", + "mark_as_read": "Marcar como leída", + "empty": "No tienes notificaciones en este momento." } } \ No newline at end of file diff --git a/web/i18n/pt.json b/web/i18n/pt.json index bc218f6..21cd195 100644 --- a/web/i18n/pt.json +++ b/web/i18n/pt.json @@ -1182,5 +1182,13 @@ "description": "Se você tiver dúvidas sobre nossa Política de Privacidade ou sobre o tratamento de seus dados pessoais, entre em contato com nosso Encarregado de Proteção de Dados (DPO).", "button": "Fale conosco" } + }, + "notifications_page": { + "title": "Notificações", + "subtitle": "Acompanhe todas as atualizações sobre suas campanhas e doações.", + "new_count": "{count, plural, one {# nova} other {# novas}}", + "mark_all_read": "Marcar todas como lidas", + "mark_as_read": "Marcar como lida", + "empty": "Você não tem notificações no momento." } } \ No newline at end of file diff --git a/web/src/app/profile/notifications/notification-item.tsx b/web/src/app/profile/notifications/notification-item.tsx index 052522c..f5ef0de 100644 --- a/web/src/app/profile/notifications/notification-item.tsx +++ b/web/src/app/profile/notifications/notification-item.tsx @@ -2,21 +2,12 @@ import { Card, CardBody, Button } from '@heroui/react'; import { Heart, Megaphone, DollarSign, AlertCircle, Trash2 } from 'lucide-react'; +import { useTranslations } from 'next-intl'; import { dayjs } from '@/lib/dayjs'; +import type { NotificationResource, NotificationResourceTypeEnumKey } from '@/lib/http/generated/models/NotificationResource'; -type NotificationType = 'donation' | 'campaign' | 'system' | 'withdrawal'; - -type Notification = { - id: string; - type: NotificationType; - title: string; - message: string; - timestamp: string; - isRead: boolean; -}; - -const notificationIcons: Record = { +const notificationIcons: Record = { donation: , campaign: , withdrawal: , @@ -35,12 +26,14 @@ const formatTimestamp = (timestamp: string) => { }; type NotificationItemProps = { - notification: Notification; + notification: NotificationResource; onMarkAsRead: (id: string) => void; onDelete: (id: string) => void; }; const NotificationItem = ({ notification, onMarkAsRead, onDelete }: NotificationItemProps) => { + const t = useTranslations('notifications_page'); + return (
- {notificationIcons[notification.type]} + {notificationIcons[notification.type!]}
@@ -72,7 +65,7 @@ const NotificationItem = ({ notification, onMarkAsRead, onDelete }: Notification

- {formatTimestamp(notification.timestamp)} + {formatTimestamp(notification.timestamp!)}
{!notification.isRead && ( @@ -80,10 +73,10 @@ const NotificationItem = ({ notification, onMarkAsRead, onDelete }: Notification size="sm" variant="flat" color="primary" - onPress={() => onMarkAsRead(notification.id)} + onPress={() => onMarkAsRead(notification.id!)} className="text-xs" > - Marcar como lida + {t('mark_as_read')} )} @@ -104,4 +97,4 @@ const NotificationItem = ({ notification, onMarkAsRead, onDelete }: Notification ); }; -export { NotificationItem, type Notification, type NotificationType }; +export { NotificationItem }; diff --git a/web/src/app/profile/notifications/notifications-list.tsx b/web/src/app/profile/notifications/notifications-list.tsx index a29fcfc..18cb314 100644 --- a/web/src/app/profile/notifications/notifications-list.tsx +++ b/web/src/app/profile/notifications/notifications-list.tsx @@ -1,75 +1,47 @@ 'use client'; -import { useState } from 'react'; -import { Card, CardBody, Button, Chip } from '@heroui/react'; +import { Card, CardBody, Button, Chip, Spinner } from '@heroui/react'; import { Bell, Check } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { useQueryClient } from '@tanstack/react-query'; -import { NotificationItem, type Notification } from './notification-item'; - -const initialNotifications: Notification[] = [ - { - id: '1', - type: 'donation', - title: 'Nova doação recebida!', - message: 'Você recebeu uma doação de R$ 50,00 na campanha "Ajuda para Maria"', - timestamp: '2025-02-08T10:30:00', - isRead: false, - }, - { - id: '2', - type: 'campaign', - title: 'Campanha aprovada', - message: 'Sua campanha "Tratamento do João" foi aprovada e já está ativa!', - timestamp: '2025-02-07T15:45:00', - isRead: false, - }, - { - id: '3', - type: 'withdrawal', - title: 'Saque processado', - message: 'Seu saque de R$ 300,00 foi processado com sucesso.', - timestamp: '2025-02-06T09:20:00', - isRead: true, - }, - { - id: '4', - type: 'donation', - title: 'Nova doação recebida!', - message: 'Você recebeu uma doação de R$ 100,00 na campanha "Reforma escola"', - timestamp: '2025-02-05T14:10:00', - isRead: true, - }, - { - id: '5', - type: 'system', - title: 'Atualização de política', - message: 'Atualizamos nossa política de privacidade. Clique para saber mais.', - timestamp: '2025-02-04T11:00:00', - isRead: true, - }, -]; +import { useListNotifications, listNotificationsQueryKey } from '@/lib/http/generated/hooks/useListNotifications'; +import { useMarkNotificationAsRead } from '@/lib/http/generated/hooks/useMarkNotificationAsRead'; +import { useMarkAllNotificationsAsRead } from '@/lib/http/generated/hooks/useMarkAllNotificationsAsRead'; +import { useDeleteNotification } from '@/lib/http/generated/hooks/useDeleteNotification'; +import { NotificationItem } from './notification-item'; const NotificationsList = () => { - const [notifications, setNotifications] = useState(initialNotifications); + const t = useTranslations('notifications_page'); + const queryClient = useQueryClient(); + + const { data, isLoading } = useListNotifications(); - const unreadCount = notifications.filter((n) => !n.isRead).length; + const notifications = data?.data ?? []; + const unreadCount = data?.meta?.unreadCount ?? 0; - const markAsRead = (id: string) => { - setNotifications((prev) => - prev.map((notif) => - notif.id === id ? { ...notif, isRead: true } : notif - ) - ); + const invalidate = () => { + queryClient.invalidateQueries({ queryKey: listNotificationsQueryKey() }); }; - const markAllAsRead = () => { - setNotifications((prev) => - prev.map((notif) => ({ ...notif, isRead: true })) - ); + const { mutate: markAsRead } = useMarkNotificationAsRead({ + mutation: { onSuccess: invalidate }, + }); + + const { mutate: markAllAsRead } = useMarkAllNotificationsAsRead({ + mutation: { onSuccess: invalidate }, + }); + + const { mutate: deleteNotif } = useDeleteNotification({ + mutation: { onSuccess: invalidate }, + }); + + const handleMarkAsRead = (id: string) => { + markAsRead({ id }); }; - const deleteNotification = (id: string) => { - setNotifications((prev) => prev.filter((notif) => notif.id !== id)); + const handleDelete = (id: string) => { + deleteNotif({ id }); }; return ( @@ -77,15 +49,15 @@ const NotificationsList = () => {

- Notificações + {t('title')} {unreadCount > 0 && ( - {unreadCount} nova{unreadCount > 1 ? 's' : ''} + {t('new_count', { count: unreadCount })} )}

- Acompanhe todas as atualizações sobre suas campanhas e doações. + {t('subtitle')}

{unreadCount > 0 && ( @@ -93,19 +65,23 @@ const NotificationsList = () => { color="primary" variant="flat" startContent={} - onPress={markAllAsRead} + onPress={() => markAllAsRead()} > - Marcar todas como lidas + {t('mark_all_read')} )}
- {notifications.length === 0 ? ( + {isLoading ? ( +
+ +
+ ) : notifications.length === 0 ? (

- Você não tem notificações no momento. + {t('empty')}

@@ -115,8 +91,8 @@ const NotificationsList = () => { ))}
diff --git a/web/src/app/profile/notifications/page.tsx b/web/src/app/profile/notifications/page.tsx index 7a4120b..4fe7c2f 100644 --- a/web/src/app/profile/notifications/page.tsx +++ b/web/src/app/profile/notifications/page.tsx @@ -2,12 +2,6 @@ import { ProfileSidebar } from '../profile-sidebar'; import { NotificationsList } from './notifications-list'; const NotificationsPage = () => { - const userData = { - name: 'Cristiano', - followedCampaigns: 0, - donationsCount: 3, - }; - return (
From 08cd4d4cc698af62953eac47011e2f8cdc2877db Mon Sep 17 00:00:00 2001 From: gitnlsn <11997916+gitnlsn@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:58:11 +0000 Subject: [PATCH 2/2] style: apply automatic code formatting --- api/app/Services/Notification/NotificationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/app/Services/Notification/NotificationService.php b/api/app/Services/Notification/NotificationService.php index c815a12..6959ff4 100644 --- a/api/app/Services/Notification/NotificationService.php +++ b/api/app/Services/Notification/NotificationService.php @@ -6,8 +6,8 @@ use App\Models\User; use Illuminate\Contracts\Pagination\LengthAwarePaginator; -use Illuminate\Support\Facades\Notification as NotificationFacade; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Notification as NotificationFacade; final class NotificationService {