feat: add tomorrow weather brief notifications
This commit is contained in:
@@ -12,7 +12,7 @@ import { DEFAULT_STATION_ID } from "@/lib/constants";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { getProvinceForSelection } from "@/lib/provinces";
|
||||
import { useWeatherStore } from "@/lib/store";
|
||||
import { buildWeatherBrief } from "@/lib/weather-brief";
|
||||
import { buildTomorrowWeatherBrief, buildWeatherBrief } from "@/lib/weather-brief";
|
||||
|
||||
export function WeatherBriefCard({ latitude, longitude, locationName }: { latitude?: number; longitude?: number; locationName: string }) {
|
||||
const { language, t } = useI18n();
|
||||
@@ -26,6 +26,10 @@ export function WeatherBriefCard({ latitude, longitude, locationName }: { latitu
|
||||
if (!forecast) return null;
|
||||
return buildWeatherBrief({ forecast, warnings, province, countyTeryt: selectedLocation?.countyTeryt, locationName, language });
|
||||
}, [forecast, language, locationName, province, selectedLocation?.countyTeryt, warnings]);
|
||||
const tomorrowBrief = useMemo(() => {
|
||||
if (!forecast) return null;
|
||||
return buildTomorrowWeatherBrief({ forecast, warnings, province, countyTeryt: selectedLocation?.countyTeryt, locationName, language });
|
||||
}, [forecast, language, locationName, province, selectedLocation?.countyTeryt, warnings]);
|
||||
|
||||
if (!Number.isFinite(latitude) || !Number.isFinite(longitude) || isPending) {
|
||||
return <LoadingSkeleton className="h-48" />;
|
||||
@@ -72,6 +76,20 @@ export function WeatherBriefCard({ latitude, longitude, locationName }: { latitu
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{tomorrowBrief && (
|
||||
<div className="mt-5 border-t border-border/65 pt-4">
|
||||
<p className="section-kicker">{t("brief.tomorrowLabel")}</p>
|
||||
<h3 className="mt-1 text-sm font-semibold">{tomorrowBrief.headline}</h3>
|
||||
<ul className="mt-3 space-y-2">
|
||||
{tomorrowBrief.body.slice(0, 3).map((line) => (
|
||||
<li key={line} className="rounded-card border border-border/60 bg-surface-muted/45 px-3 py-2 text-sm leading-6">
|
||||
{line}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-4 flex items-center gap-2 text-xs leading-5 text-muted">
|
||||
<BellRing className="size-3.5 text-accent" aria-hidden="true" />
|
||||
{t("brief.pushHint")}
|
||||
|
||||
@@ -69,10 +69,12 @@ export function SettingsPage() {
|
||||
const selectedLocation = useWeatherStore((state) => state.selectedLocation);
|
||||
const notificationsEnabled = useWeatherStore((state) => state.warningNotificationsEnabled);
|
||||
const morningBriefEnabled = useWeatherStore((state) => state.morningBriefNotificationsEnabled);
|
||||
const tomorrowBriefEnabled = useWeatherStore((state) => state.tomorrowBriefNotificationsEnabled);
|
||||
const provinceMode = useWeatherStore((state) => state.warningNotificationProvinceMode);
|
||||
const manualProvince = useWeatherStore((state) => state.warningNotificationProvince);
|
||||
const setNotificationsEnabled = useWeatherStore((state) => state.setWarningNotificationsEnabled);
|
||||
const setMorningBriefEnabled = useWeatherStore((state) => state.setMorningBriefNotificationsEnabled);
|
||||
const setTomorrowBriefEnabled = useWeatherStore((state) => state.setTomorrowBriefNotificationsEnabled);
|
||||
const setProvinceMode = useWeatherStore((state) => state.setWarningNotificationProvinceMode);
|
||||
const setManualProvince = useWeatherStore((state) => state.setWarningNotificationProvince);
|
||||
|
||||
@@ -119,6 +121,7 @@ export function SettingsPage() {
|
||||
if (!subscription || cancelled) return;
|
||||
await savePushSubscription(subscription, province, language, {
|
||||
morningBriefEnabled,
|
||||
tomorrowBriefEnabled,
|
||||
latitude: notificationLatitude,
|
||||
longitude: notificationLongitude,
|
||||
locationName: notificationLocationName,
|
||||
@@ -130,7 +133,7 @@ export function SettingsPage() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [effectiveProvince, language, morningBriefEnabled, notificationCountyTeryt, notificationLatitude, notificationLocationName, notificationLongitude, notificationStatus, notificationsEnabled, vapidPublicKey]);
|
||||
}, [effectiveProvince, language, morningBriefEnabled, notificationCountyTeryt, notificationLatitude, notificationLocationName, notificationLongitude, notificationStatus, notificationsEnabled, tomorrowBriefEnabled, vapidPublicKey]);
|
||||
|
||||
const notificationStatusLabel = useMemo(() => {
|
||||
switch (notificationStatus) {
|
||||
@@ -184,6 +187,7 @@ export function SettingsPage() {
|
||||
});
|
||||
await savePushSubscription(subscription, effectiveProvince, language, {
|
||||
morningBriefEnabled,
|
||||
tomorrowBriefEnabled,
|
||||
latitude: notificationLatitude,
|
||||
longitude: notificationLongitude,
|
||||
locationName: notificationLocationName,
|
||||
@@ -211,6 +215,7 @@ export function SettingsPage() {
|
||||
}
|
||||
setNotificationsEnabled(false);
|
||||
setMorningBriefEnabled(false);
|
||||
setTomorrowBriefEnabled(false);
|
||||
setNotificationMessage(t("settings.notifications.disableSuccess"));
|
||||
} catch {
|
||||
setNotificationMessage(t("settings.notifications.saveError"));
|
||||
@@ -244,6 +249,27 @@ export function SettingsPage() {
|
||||
if (!subscription) return;
|
||||
await savePushSubscription(subscription, effectiveProvince, language, {
|
||||
morningBriefEnabled: enabled,
|
||||
tomorrowBriefEnabled,
|
||||
latitude: notificationLatitude,
|
||||
longitude: notificationLongitude,
|
||||
locationName: notificationLocationName,
|
||||
countyTeryt: notificationCountyTeryt,
|
||||
});
|
||||
} catch {
|
||||
setNotificationMessage(t("settings.notifications.saveError"));
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTomorrowBriefPreference(enabled: boolean) {
|
||||
setTomorrowBriefEnabled(enabled);
|
||||
if (!notificationsEnabled || !effectiveProvince || notificationStatus !== "granted") return;
|
||||
try {
|
||||
const registration = await navigator.serviceWorker?.getRegistration();
|
||||
const subscription = await registration?.pushManager.getSubscription();
|
||||
if (!subscription) return;
|
||||
await savePushSubscription(subscription, effectiveProvince, language, {
|
||||
morningBriefEnabled,
|
||||
tomorrowBriefEnabled: enabled,
|
||||
latitude: notificationLatitude,
|
||||
longitude: notificationLongitude,
|
||||
locationName: notificationLocationName,
|
||||
@@ -368,6 +394,23 @@ export function SettingsPage() {
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="mt-3 flex items-start gap-3 rounded-card border border-border/60 bg-surface px-3 py-3 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tomorrowBriefEnabled}
|
||||
disabled={!notificationsEnabled}
|
||||
onChange={(event) => updateTomorrowBriefPreference(event.target.checked)}
|
||||
className="mt-1 accent-accent"
|
||||
/>
|
||||
<span>
|
||||
<span className="flex items-center gap-2 font-medium">
|
||||
<Clock3 className="size-3.5 text-accent" aria-hidden="true" />
|
||||
{t("settings.notifications.tomorrowBrief")}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs leading-5 text-muted">{t("settings.notifications.tomorrowBriefDescription")}</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-2 sm:flex-row">
|
||||
<Button type="button" disabled={(!notificationsEnabled && !canUsePush) || isSavingSubscription} onClick={notificationsEnabled ? disableWarningNotifications : enableWarningNotifications}>
|
||||
<Bell className="size-4" />
|
||||
|
||||
Reference in New Issue
Block a user