starting to add stuff for lunch time reminders
This commit is contained in:
@@ -4,6 +4,8 @@ import '@/styles/globals.css';
|
||||
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';
|
||||
import {
|
||||
ConvexClientProvider,
|
||||
LunchReminder,
|
||||
NotificationsPermission,
|
||||
ThemeProvider,
|
||||
TVModeProvider,
|
||||
} from '@/components/providers';
|
||||
@@ -22,11 +24,11 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
export const metadata: Metadata = generateMetadata();
|
||||
|
||||
export default function RootLayout({
|
||||
const RootLayout = async ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
}>) => {
|
||||
return (
|
||||
<ConvexAuthNextjsServerProvider>
|
||||
<PlausibleProvider
|
||||
@@ -47,7 +49,11 @@ export default function RootLayout({
|
||||
<TVModeProvider>
|
||||
<Header />
|
||||
{children}
|
||||
<Toaster />
|
||||
<NotificationsPermission />
|
||||
<LunchReminder
|
||||
lunchTime='15:24'
|
||||
/>
|
||||
<Toaster richColors />
|
||||
</TVModeProvider>
|
||||
</ConvexClientProvider>
|
||||
</ThemeProvider>
|
||||
@@ -57,3 +63,4 @@ export default function RootLayout({
|
||||
</ConvexAuthNextjsServerProvider>
|
||||
);
|
||||
}
|
||||
export default RootLayout;
|
||||
|
@@ -1,4 +1,6 @@
|
||||
export { ConvexClientProvider } from './ConvexClientProvider';
|
||||
export { LunchReminder } from './lunch-reminder';
|
||||
export { NotificationsPermission } from './notification-permission';
|
||||
export {
|
||||
ThemeProvider,
|
||||
ThemeToggle,
|
||||
|
72
apps/next/src/components/providers/lunch-reminder.tsx
Normal file
72
apps/next/src/components/providers/lunch-reminder.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useMutation } from 'convex/react';
|
||||
import { api } from '~/convex/_generated/api';
|
||||
|
||||
type Props = {
|
||||
lunchTime: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
const nextOccurrenceMs = (hhmm: string, from = new Date()): number => {
|
||||
const [hStr, mStr] = hhmm.split(':');
|
||||
const target = new Date(from);
|
||||
target.setHours(Number(hStr), Number(mStr), 0, 0);
|
||||
if (target <= from) target.setDate(target.getDate() + 1);
|
||||
return target.getTime() - from.getTime();
|
||||
};
|
||||
|
||||
export const LunchReminder = ({ lunchTime, enabled = true }: Props) => {
|
||||
const setStatus = useMutation(api.statuses.create);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
console.log('LunchReminder is running!')
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
const schedule = () => {
|
||||
const ms = nextOccurrenceMs(lunchTime);
|
||||
console.log('Ms = ', ms)
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
if (
|
||||
'Notification' in window &&
|
||||
Notification.permission === 'granted'
|
||||
) {
|
||||
new Notification('Lunch time?', {
|
||||
body: 'Update your status to "At lunch"?',
|
||||
tag: 'tech-tracker-lunch',
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
|
||||
toast('Lunch time?', {
|
||||
description:
|
||||
'Would you like to set your status to "At lunch"?',
|
||||
action: {
|
||||
label: 'Set to lunch',
|
||||
onClick: () => void setStatus({ message: 'At lunch' }),
|
||||
},
|
||||
cancel: {
|
||||
label: 'Not now',
|
||||
onClick: () => console.log('User declined lunch suggestion'),
|
||||
},
|
||||
id: 'lunch-reminder',
|
||||
});
|
||||
schedule();
|
||||
})();
|
||||
}, ms);
|
||||
};
|
||||
|
||||
schedule();
|
||||
return () => {
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
};
|
||||
}, [enabled, lunchTime, setStatus]);
|
||||
|
||||
return null;
|
||||
};
|
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const STORAGE_KEY = 'notif.permission.prompted.v1';
|
||||
|
||||
export const NotificationsPermission = () => {
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (!('Notification' in window)) return;
|
||||
|
||||
// Only ask once; tweak logic to your taste.
|
||||
const prompted = localStorage.getItem(STORAGE_KEY) === '1';
|
||||
if (prompted) return;
|
||||
|
||||
if (Notification.permission === 'default') {
|
||||
toast('Enable system notifications?', {
|
||||
id: 'enable-notifications',
|
||||
description:
|
||||
'Get a native notification at lunch time (optional).',
|
||||
action: {
|
||||
label: 'Enable',
|
||||
onClick: () => {
|
||||
// Must be called during the click handler.
|
||||
const p = Notification.requestPermission();
|
||||
p.then((perm) => {
|
||||
localStorage.setItem(STORAGE_KEY, '1');
|
||||
if (perm === 'granted') {
|
||||
toast.success('System notifications enabled');
|
||||
} else {
|
||||
toast('Okay, we will use in-app toasts instead.');
|
||||
}
|
||||
}).catch(() => {
|
||||
localStorage.setItem(STORAGE_KEY, '1');
|
||||
toast('Failed to request notification permission.');
|
||||
});
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
label: 'Not now',
|
||||
onClick: () => {
|
||||
localStorage.setItem(STORAGE_KEY, '1');
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
@@ -8,7 +8,8 @@ import { banSuspiciousIPs } from '@/lib/middleware/ban-suspicious-ips';
|
||||
const isSignInPage = createRouteMatcher(['/signin']);
|
||||
const isProtectedRoute = createRouteMatcher(['/', '/profile']);
|
||||
|
||||
export default convexAuthNextjsMiddleware(async (request, { convexAuth }) => {
|
||||
export default convexAuthNextjsMiddleware(
|
||||
async (request, { convexAuth }) => {
|
||||
const banResponse = banSuspiciousIPs(request);
|
||||
if (banResponse) return banResponse;
|
||||
if (isSignInPage(request) && (await convexAuth.isAuthenticated())) {
|
||||
@@ -17,7 +18,9 @@ export default convexAuthNextjsMiddleware(async (request, { convexAuth }) => {
|
||||
if (isProtectedRoute(request) && !(await convexAuth.isAuthenticated())) {
|
||||
return nextjsMiddlewareRedirect(request, '/signin');
|
||||
}
|
||||
});
|
||||
},
|
||||
{ cookieConfig: { maxAge: 60 * 60 * 24 * 30 }},
|
||||
);
|
||||
|
||||
export const config = {
|
||||
// The following matcher runs middleware on all routes
|
||||
|
Reference in New Issue
Block a user