diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index ad8a4a7..67bfe19 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -281,9 +281,18 @@ export default function DashboardPage() { )} {address && !loading && loadError && streams.length === 0 && ( -

- Couldn't reach the StreamGive API. Is the backend running? -

+
+

+ Couldn't load your streams. Please check your connection and try again. +

+ +
)} {address && !loading && !loadError && streams.length === 0 && ( diff --git a/src/app/embed/[ngoId]/page.tsx b/src/app/embed/[ngoId]/page.tsx index 0f3af99..decd706 100644 --- a/src/app/embed/[ngoId]/page.tsx +++ b/src/app/embed/[ngoId]/page.tsx @@ -30,7 +30,7 @@ export default async function EmbedDonatePage({ params }: Props) { return (
- Couldn't reach the StreamGive API. + Couldn't load the donation form. Please try refreshing the page.
); diff --git a/src/app/icon.svg b/src/app/icon.svg new file mode 100644 index 0000000..d0656bb --- /dev/null +++ b/src/app/icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/app/impact/page.tsx b/src/app/impact/page.tsx index b2aea0a..4044c28 100644 --- a/src/app/impact/page.tsx +++ b/src/app/impact/page.tsx @@ -23,6 +23,8 @@ const POLL_INTERVAL_MS = 20_000; export default function ImpactPage() { const [impact, setImpact] = useState(null); const [loadError, setLoadError] = useState(false); + // Incrementing this triggers the effect to remount and retry immediately. + const [retryCount, setRetryCount] = useState(0); useEffect(() => { // Guards every setState below against a tick that is still in flight @@ -63,7 +65,7 @@ export default function ImpactPage() { clearInterval(interval); activeController.abort(); }; - }, []); + }, [retryCount]); return ( <> @@ -75,9 +77,18 @@ export default function ImpactPage() {

{loadError && !impact && ( -

- Couldn't reach the StreamGive API. Is the backend running? -

+
+

+ Couldn't load platform stats. Please check your connection and try again. +

+ +
)} {/* Keep the last good numbers on screen when a later poll fails; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 484eb56..59378d7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -19,6 +19,10 @@ const geistMono = Geist_Mono({ const DESCRIPTION = 'Recurring, streaming donations for verified NGOs on Stellar.'; export const metadata: Metadata = { + // metadataBase is required for Next.js to resolve relative URLs used in + // openGraph.images and twitter.images into fully-qualified URLs for crawlers. + // Falls back to the local dev origin; set NEXT_PUBLIC_APP_URL in production. + metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3001'), title: { default: 'StreamGive', // Child routes set just their own segment (e.g. "Explore NGOs") and @@ -27,15 +31,25 @@ export const metadata: Metadata = { template: '%s — StreamGive', }, description: DESCRIPTION, + // Browser toolbar / PWA chrome colour (matches the manifest and the logo). + themeColor: [ + { media: '(prefers-color-scheme: light)', color: '#0d9488' }, + { media: '(prefers-color-scheme: dark)', color: '#0f172a' }, + ], openGraph: { title: 'StreamGive', description: DESCRIPTION, type: 'website', + siteName: 'StreamGive', + // /opengraph-image is generated by src/app/opengraph-image.tsx. + images: [{ url: '/opengraph-image', width: 1200, height: 630, alt: 'StreamGive' }], }, twitter: { - card: 'summary', + // summary_large_image renders the OG image as a large card, not a thumbnail. + card: 'summary_large_image', title: 'StreamGive', description: DESCRIPTION, + images: ['/opengraph-image'], }, }; diff --git a/src/app/manifest.ts b/src/app/manifest.ts new file mode 100644 index 0000000..300b193 --- /dev/null +++ b/src/app/manifest.ts @@ -0,0 +1,32 @@ +import type { MetadataRoute } from 'next'; + +/** + * Web app manifest — served at /manifest.webmanifest. + * Enables "Add to Home Screen" on mobile and provides browser-level theming. + * Next.js wires up the automatically when this file exists. + */ +export default function manifest(): MetadataRoute.Manifest { + return { + name: 'StreamGive', + short_name: 'StreamGive', + description: 'Recurring, streaming donations for verified NGOs on Stellar.', + start_url: '/', + display: 'standalone', + background_color: '#ffffff', + theme_color: '#0d9488', + icons: [ + { + src: '/icon.svg', + sizes: 'any', + type: 'image/svg+xml', + purpose: 'any', + }, + { + src: '/icon.svg', + sizes: 'any', + type: 'image/svg+xml', + purpose: 'maskable', + }, + ], + }; +} diff --git a/src/app/ngo-admin/page.tsx b/src/app/ngo-admin/page.tsx index 9b705cf..980b7dd 100644 --- a/src/app/ngo-admin/page.tsx +++ b/src/app/ngo-admin/page.tsx @@ -97,9 +97,18 @@ export default function NgoAdminPage() { )} {address && !loading && loadError && ( -

- Couldn't reach the StreamGive API. Is the backend running? -

+
+

+ Couldn't load your NGO data. Please check your connection and try again. +

+ +
)} {address && !loading && !loadError && ngo === null && ( diff --git a/src/app/ngos/[id]/donate/page.tsx b/src/app/ngos/[id]/donate/page.tsx index 9aeecb9..a14c624 100644 --- a/src/app/ngos/[id]/donate/page.tsx +++ b/src/app/ngos/[id]/donate/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from 'next'; +import Link from 'next/link'; import { notFound } from 'next/navigation'; import { CreateStreamForm } from '@/components/donate/CreateStreamForm'; @@ -26,8 +27,14 @@ export default async function DonatePage({ params }: Props) {

- Couldn't reach the StreamGive API. Is the backend running? + Couldn't load this NGO's details. Please check your connection and try again.

+ + Retry +