diff --git a/public/campaign-icons/apple-touch-icon.png b/public/campaign-icons/apple-touch-icon.png new file mode 100644 index 00000000..7ad1be1b Binary files /dev/null and b/public/campaign-icons/apple-touch-icon.png differ diff --git a/public/campaign-icons/icon-192.png b/public/campaign-icons/icon-192.png new file mode 100644 index 00000000..1e1f8560 Binary files /dev/null and b/public/campaign-icons/icon-192.png differ diff --git a/public/campaign-icons/icon-512.png b/public/campaign-icons/icon-512.png new file mode 100644 index 00000000..3f73342b Binary files /dev/null and b/public/campaign-icons/icon-512.png differ diff --git a/public/campaign-icons/icon-maskable-512.png b/public/campaign-icons/icon-maskable-512.png new file mode 100644 index 00000000..f149edb5 Binary files /dev/null and b/public/campaign-icons/icon-maskable-512.png differ diff --git a/src/app/(campaign)/campanha/(app)/layout.tsx b/src/app/(campaign)/campanha/(app)/layout.tsx index 0ed58918..08b70ce4 100644 --- a/src/app/(campaign)/campanha/(app)/layout.tsx +++ b/src/app/(campaign)/campanha/(app)/layout.tsx @@ -3,6 +3,7 @@ import React from 'react' import { CampaignBottomNav } from '@/components/campaign/CampaignBottomNav' import { CampaignSidebar } from '@/components/campaign/CampaignSidebar' +import { InstallPwaToast } from '@/components/campaign/InstallPwaToast' import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/Sidebar' import { Toaster } from '@/components/ui/Toaster' import { getCampaignUser } from '@/utilities/campaignAuth' @@ -38,6 +39,7 @@ export default async function CampaignAppLayout({ children }: { children: React. + ) diff --git a/src/app/(campaign)/campanha/manifest.webmanifest/route.ts b/src/app/(campaign)/campanha/manifest.webmanifest/route.ts new file mode 100644 index 00000000..ffc2b243 --- /dev/null +++ b/src/app/(campaign)/campanha/manifest.webmanifest/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server' + +import { CAMPAIGN_WEB_MANIFEST } from '@/utilities/campaignPwa' + +export const dynamic = 'force-static' + +export function GET(): NextResponse { + return NextResponse.json(CAMPAIGN_WEB_MANIFEST, { + headers: { + 'Content-Type': 'application/manifest+json; charset=utf-8', + 'Cache-Control': 'no-cache', + }, + }) +} diff --git a/src/app/(campaign)/campanha/offline/page.tsx b/src/app/(campaign)/campanha/offline/page.tsx new file mode 100644 index 00000000..d5f01ecf --- /dev/null +++ b/src/app/(campaign)/campanha/offline/page.tsx @@ -0,0 +1,35 @@ +import { WifiOff } from 'lucide-react' +import type { Metadata } from 'next' +import Link from 'next/link' + +import { Button } from '@/components/ui/button' + +export const metadata: Metadata = { + title: 'Offline | Campanha', + robots: { + index: false, + follow: false, + }, +} + +export default function CampaignOfflinePage() { + return ( +
+
+
+
+
+

Você está offline

+

+ Sem conexão no momento. Abra novamente quando a rede voltar, ou tente uma página que + você já visitou nesta sessão. +

+
+ +
+
+ ) +} diff --git a/src/app/(campaign)/campanha/sw.js/route.ts b/src/app/(campaign)/campanha/sw.js/route.ts new file mode 100644 index 00000000..ac8523a2 --- /dev/null +++ b/src/app/(campaign)/campanha/sw.js/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from 'next/server' + +import { + buildCampaignServiceWorkerScript, + CAMPAIGN_PWA_SCOPE, + resolveCampaignPwaBuildId, +} from '@/utilities/campaignPwa' + +// Built once per deploy; build id in the script body still invalidates SW updates. +export const dynamic = 'force-static' + +export function GET(): NextResponse { + const script = buildCampaignServiceWorkerScript(resolveCampaignPwaBuildId()) + + return new NextResponse(script, { + headers: { + 'Content-Type': 'text/javascript; charset=utf-8', + 'Cache-Control': 'no-cache', + 'Service-Worker-Allowed': CAMPAIGN_PWA_SCOPE, + }, + }) +} diff --git a/src/app/(campaign)/layout.tsx b/src/app/(campaign)/layout.tsx index 0f7d9d84..01962fe3 100644 --- a/src/app/(campaign)/layout.tsx +++ b/src/app/(campaign)/layout.tsx @@ -1,8 +1,10 @@ -import type { Metadata } from 'next' +import type { Metadata, Viewport } from 'next' import { Inter } from 'next/font/google' import React from 'react' +import { RegisterServiceWorker } from '@/components/campaign/RegisterServiceWorker' import { VoteEstimateFocusProvider } from '@/components/campaign/VoteEstimateFocusProvider' +import { CAMPAIGN_PWA_MANIFEST_PATH, CAMPAIGN_PWA_THEME_COLOR } from '@/utilities/campaignPwa' import '../(frontend)/styles.css' @@ -14,6 +16,19 @@ export const metadata: Metadata = { index: false, follow: false, }, + manifest: CAMPAIGN_PWA_MANIFEST_PATH, + appleWebApp: { + capable: true, + title: 'Campanha', + statusBarStyle: 'default', + }, + icons: { + apple: '/campaign-icons/apple-touch-icon.png', + }, +} + +export const viewport: Viewport = { + themeColor: CAMPAIGN_PWA_THEME_COLOR, } export default function CampanhaRootLayout({ children }: { children: React.ReactNode }) { @@ -26,6 +41,7 @@ export default function CampanhaRootLayout({ children }: { children: React.React > {children} + ) diff --git a/src/components/campaign/CampaignSidebar.tsx b/src/components/campaign/CampaignSidebar.tsx index 528bece8..c79abda5 100644 --- a/src/components/campaign/CampaignSidebar.tsx +++ b/src/components/campaign/CampaignSidebar.tsx @@ -1,5 +1,6 @@ 'use client' +import type { FormEvent } from 'react' import Link from 'next/link' import { usePathname } from 'next/navigation' @@ -21,6 +22,7 @@ import { useSidebar, } from '@/components/ui/Sidebar' import type { CampaignUser } from '@/payload-types' +import { clearCampaignPwaCaches } from '@/utilities/campaignPwaClient' export type CampaignSidebarUser = { name: string @@ -38,6 +40,12 @@ export const CampaignSidebar = ({ user }: { user: CampaignSidebarUser }) => { const pathname = usePathname() const { isMobile, setOpenMobile } = useSidebar() + const handleLogout = async (event: FormEvent) => { + event.preventDefault() + await clearCampaignPwaCaches() + await logoutCampaign() + } + return ( @@ -82,7 +90,7 @@ export const CampaignSidebar = ({ user }: { user: CampaignSidebarUser }) => { {user.email} ) : null} -
+