diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..be37aba --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,30 @@ +{ + "name": "Port Nimara CRM", + "short_name": "Port Nimara", + "description": "Marina/port management CRM", + "start_url": "/", + "display": "standalone", + "background_color": "#f2f2f2", + "theme_color": "#0f172a", + "orientation": "any", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-512-maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/src/app/(dashboard)/[portSlug]/admin/webhooks/page.tsx b/src/app/(dashboard)/[portSlug]/admin/webhooks/page.tsx index be44d1d..af97986 100644 --- a/src/app/(dashboard)/[portSlug]/admin/webhooks/page.tsx +++ b/src/app/(dashboard)/[portSlug]/admin/webhooks/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useCallback, useEffect, useState } from 'react'; +import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { PageHeader } from '@/components/shared/page-header'; import { Badge } from '@/components/ui/badge'; @@ -47,8 +48,8 @@ export default function WebhooksPage() { try { const result = await apiFetch<{ data: Webhook[] }>('/api/v1/admin/webhooks'); setWebhooks(result.data); - } catch { - // ignore + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Failed to load webhooks'); } finally { setLoading(false); } @@ -63,9 +64,10 @@ export default function WebhooksPage() { try { await apiFetch(`/api/v1/admin/webhooks/${deleteTarget.id}`, { method: 'DELETE' }); setDeleteTarget(null); + toast.success('Webhook deleted'); void loadWebhooks(); - } catch { - // ignore + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Failed to delete webhook'); } } @@ -78,8 +80,8 @@ export default function WebhooksPage() { ); setNewSecret({ webhookId, secret: result.data.secret, masked: result.data.secretMasked }); void loadWebhooks(); - } catch { - // ignore + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Failed to regenerate secret'); } finally { setRegenerating(null); } @@ -91,9 +93,10 @@ export default function WebhooksPage() { method: 'PATCH', body: { isActive: !webhook.isActive }, }); + toast.success(webhook.isActive ? 'Webhook disabled' : 'Webhook enabled'); void loadWebhooks(); - } catch { - // ignore + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Failed to toggle webhook'); } } diff --git a/src/app/(dashboard)/error.tsx b/src/app/(dashboard)/error.tsx new file mode 100644 index 0000000..42425eb --- /dev/null +++ b/src/app/(dashboard)/error.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { useEffect } from 'react'; +import Link from 'next/link'; +import { AlertCircle, RotateCcw } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; + +interface ErrorProps { + error: Error & { digest?: string }; + reset: () => void; +} + +export default function DashboardError({ error, reset }: ErrorProps) { + useEffect(() => { + // Forward to the browser console so the dev sees the stack while the + // user sees the friendly UI. The server already wrote an error_events + // row through the page-level error pipeline. + + console.error('Dashboard render error:', error); + }, [error]); + + return ( +
+
+
+
+ +
+

Something went wrong

+
+ +

+ The page hit an unexpected error. The team has been notified + {error.digest ? ' (ref: ' : '.'} + {error.digest ? {error.digest} : null} + {error.digest ? ').' : ''} +

+ +
+ + + Back to dashboard + +
+
+
+ ); +} diff --git a/src/app/(portal)/portal/documents/document-download-button.tsx b/src/app/(portal)/portal/documents/document-download-button.tsx index a63bcb1..78d567f 100644 --- a/src/app/(portal)/portal/documents/document-download-button.tsx +++ b/src/app/(portal)/portal/documents/document-download-button.tsx @@ -2,6 +2,8 @@ import { useState } from 'react'; import { Download, Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; + import { Button } from '@/components/ui/button'; interface DocumentDownloadButtonProps { @@ -16,25 +18,20 @@ export function DocumentDownloadButton({ documentId }: DocumentDownloadButtonPro try { const res = await fetch(`/api/portal/documents/${documentId}/download`); if (!res.ok) { - alert('Unable to download document. Please try again.'); + toast.error('Unable to download document. Please try again.'); return; } - const data = await res.json() as { url: string }; + const data = (await res.json()) as { url: string }; window.open(data.url, '_blank', 'noopener,noreferrer'); } catch { - alert('Unable to download document. Please check your connection.'); + toast.error('Unable to download document. Please check your connection.'); } finally { setLoading(false); } } return ( -