Added PWA support (#40)

This commit is contained in:
Doh
2025-01-15 20:07:23 -05:00
committed by GitHub
parent 1bb968b7c1
commit 71b9d1b408
32 changed files with 1274 additions and 312 deletions

49
app/actions/push.ts Normal file
View File

@@ -0,0 +1,49 @@
'use server'
import webpush from 'web-push'
webpush.setVapidDetails(
'mailto:mydohsimpson@gmail.com',
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
)
interface PushSubscriptionWithKeys extends PushSubscription {
keys: {
p256dh: string
auth: string
}
}
let subscription: PushSubscriptionWithKeys | null = null
export async function subscribeUser(sub: PushSubscriptionWithKeys) {
subscription = sub
return { success: true }
}
export async function unsubscribeUser() {
subscription = null
return { success: true }
}
export async function sendNotification(message: string) {
if (!subscription) {
throw new Error('No subscription available')
}
try {
await webpush.sendNotification(
subscription,
JSON.stringify({
title: 'HabitTrove',
body: message,
icon: '/icon.png',
})
)
return { success: true }
} catch (error) {
console.error('Error sending push notification:', error)
return { success: false, error: 'Failed to send notification' }
}
}

View File

@@ -3,9 +3,7 @@ import HabitCalendar from '@/components/HabitCalendar'
export default function CalendarPage() {
return (
<Layout>
<HabitCalendar />
</Layout>
<HabitCalendar />
)
}

View File

@@ -3,8 +3,6 @@ import CoinsManager from '@/components/CoinsManager'
export default function CoinsPage() {
return (
<Layout>
<CoinsManager />
</Layout>
<CoinsManager />
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -3,9 +3,7 @@ import HabitList from '@/components/HabitList'
export default function HabitsPage() {
return (
<Layout>
<HabitList />
</Layout>
<HabitList />
)
}

View File

@@ -1,17 +1,19 @@
import './globals.css'
import { Inter } from 'next/font/google'
import { DM_Sans } from 'next/font/google'
import { Toaster } from '@/components/ui/toaster'
import { JotaiProvider } from '@/components/jotai-providers'
import { Suspense } from 'react'
import { JotaiHydrate } from '@/components/jotai-hydrate'
import { loadSettings, loadHabitsData, loadCoinsData, loadWishlistData } from './actions/data'
import Layout from '@/components/Layout'
import { Toaster } from '@/components/ui/toaster'
// Inter (clean, modern, excellent readability)
const inter = Inter({
subsets: ['latin'],
weight: ['400', '500', '600', '700']
})
//
// const inter = Inter({
// subsets: ['latin'],
// weight: ['400', '500', '600', '700']
// })
// Clean and contemporary
const dmSans = DM_Sans({
subsets: ['latin'],
@@ -25,7 +27,7 @@ export const metadata = {
description: 'Track your habits and get rewarded',
}
export const dynamic = 'force-dynamic' // needed to prevent nextjs from caching the loadSettings function in Layout component
export const dynamic = 'force-dynamic' // needed to prevent nextjs from caching the load... functions in Layout component
export default async function RootLayout({
children,
@@ -42,6 +44,23 @@ export default async function RootLayout({
return (
<html lang="en">
<body className={activeFont.className}>
<script
dangerouslySetInnerHTML={{
__html: `
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('ServiceWorker registration successful');
})
.catch(err => {
console.log('ServiceWorker registration failed: ', err);
});
});
}
`,
}}
/>
<JotaiProvider>
<Suspense fallback="loading">
<JotaiHydrate
@@ -52,7 +71,9 @@ export default async function RootLayout({
wishlist: initialWishlist
}}
>
{children}
<Layout>
{children}
</Layout>
</JotaiHydrate>
</Suspense>
</JotaiProvider>

25
app/manifest.ts Normal file
View File

@@ -0,0 +1,25 @@
import type { MetadataRoute } from 'next'
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'HabitTrove',
short_name: 'HabitTrove',
description: 'Gamified habit tracking application',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#000000',
icons: [
{
src: '/icons/web-app-manifest-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/icons/web-app-manifest-512x512.png',
sizes: '512x512',
type: 'image/png',
},
],
}
}

View File

@@ -1,11 +1,8 @@
import Layout from '@/components/Layout'
import Dashboard from '@/components/Dashboard'
export default function Home() {
return (
<Layout>
<Dashboard />
</Layout>
<Dashboard />
)
}

View File

@@ -1,9 +0,0 @@
import Layout from '@/components/Layout'
export default function SettingsLayout({
children,
}: {
children: React.ReactNode
}) {
return <Layout>{children}</Layout>
}

View File

@@ -3,9 +3,7 @@ import WishlistManager from '@/components/WishlistManager'
export default function WishlistPage() {
return (
<Layout>
<WishlistManager />
</Layout>
<WishlistManager />
)
}