Files
appcakes-builds/src/pages/NotificationsPage.tsx
T
2026-06-21 21:12:46 +00:00

242 lines
7.0 KiB
TypeScript

import React, { useState } from 'react';
import {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonPage,
IonSkeletonText,
IonTitle,
IonToolbar,
useIonViewWillEnter,
} from '@ionic/react';
import {
alertCircleOutline,
chevronBackOutline,
chevronForwardOutline,
notificationsOutline,
} from 'ionicons/icons';
import { useHistory } from 'react-router-dom';
import { supabase } from '../supabase';
import { useAuth } from '../contexts/AuthContext';
import '../styles/activity.css';
import '../styles/recipients.css';
type NotificationRow = {
id: string;
title: string;
body: string;
priority: string;
type: string;
read_at: string | null;
created_at: string;
order_id: string | null;
voucher_id: string | null;
};
const NotificationsPage: React.FC = () => {
const history = useHistory();
const { user } = useAuth();
const [notifications, setNotifications] = useState<NotificationRow[]>([]);
const [loading, setLoading] = useState(true);
const [markingAll, setMarkingAll] = useState(false);
const [error, setError] = useState<string | null>(null);
useIonViewWillEnter(() => {
void loadNotifications();
});
const showError = (message: string) => {
setError(message);
setTimeout(() => setError(null), 4000);
};
const loadNotifications = async () => {
if (!user) return;
setLoading(true);
setError(null);
const { data, error: loadError } = await supabase
.from('notifications')
.select(
'id,title,body,priority,type,read_at,created_at,order_id,voucher_id'
)
.eq('user_id', user.id)
.order('created_at', { ascending: false });
if (loadError) {
showError(loadError.message);
setLoading(false);
return;
}
setNotifications((data ?? []) as NotificationRow[]);
setLoading(false);
};
const markRead = async (notificationId: string) => {
const readAt = new Date().toISOString();
setNotifications((current) =>
current.map((item) =>
item.id === notificationId ? { ...item, read_at: readAt } : item
)
);
await supabase.functions.invoke('mark-notification-read', {
body: { notificationId },
});
};
const handleOpenNotification = async (notification: NotificationRow) => {
if (!notification.read_at) {
await markRead(notification.id);
}
if (notification.voucher_id) {
history.push(`/voucher/${notification.voucher_id}`);
} else if (notification.order_id) {
history.push(`/activity/${notification.order_id}`);
}
};
const handleMarkAllRead = async () => {
if (!user) return;
setMarkingAll(true);
const readAt = new Date().toISOString();
const { error: updateError } = await supabase
.from('notifications')
.update({ read_at: readAt })
.eq('user_id', user.id)
.is('read_at', null);
if (updateError) {
showError(updateError.message);
} else {
setNotifications((current) =>
current.map((item) => ({ ...item, read_at: item.read_at ?? readAt }))
);
}
setMarkingAll(false);
};
return (
<IonPage style={{ backgroundColor: '#fafafa' }}>
<IonHeader className="ion-no-border">
<IonToolbar
style={
{
'--background': '#fafafa',
'--border-width': '0px',
} as React.CSSProperties
}
>
<IonButtons slot="start">
<IonButton
fill="clear"
onClick={() =>
history.length > 1
? history.goBack()
: history.replace('/profile')
}
aria-label="Go back"
>
<IonIcon icon={chevronBackOutline} slot="icon-only" />
</IonButton>
</IonButtons>
<IonTitle style={{ fontSize: 18, fontWeight: 700 }}>
Notifications
</IonTitle>
<IonButtons slot="end">
<IonButton
className="notifications-header-btn"
fill="clear"
disabled={markingAll}
onClick={handleMarkAllRead}
>
Mark all
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent style={{ '--background': '#fafafa' } as React.CSSProperties}>
{error && (
<p style={{ margin: '12px 20px', color: '#dc2626', fontSize: 13 }}>
{error}
</p>
)}
{loading ? (
<div className="notifications-list-card">
{[1, 2, 3].map((item) => (
<div className="notification-row" key={item}>
<IonSkeletonText
animated
style={{
width: 44,
height: 44,
borderRadius: 12,
flexShrink: 0,
}}
/>
<div style={{ flex: 1 }}>
<IonSkeletonText
animated
style={{ width: '65%', height: 15 }}
/>
<IonSkeletonText
animated
style={{ width: '90%', height: 12 }}
/>
</div>
</div>
))}
</div>
) : notifications.length === 0 ? (
<div className="empty-state-card" style={{ marginTop: 20 }}>
<IonIcon icon={notificationsOutline} className="esc-icon" />
<h2 className="esc-title">No notifications yet</h2>
<p className="esc-msg">
Important payment and voucher updates will appear here.
</p>
</div>
) : (
<div className="notifications-list-card">
{notifications.map((notification) => (
<button
key={notification.id}
type="button"
className="notification-row"
onClick={() => handleOpenNotification(notification)}
style={{
width: '100%',
border: 'none',
background: 'transparent',
textAlign: 'left',
}}
>
{!notification.read_at && <span className="nr-unread-dot" />}
<div className={`nr-icon-box ${notification.priority}`}>
<IonIcon icon={alertCircleOutline} />
</div>
<div className="nr-content">
<h3 className="nr-title">{notification.title}</h3>
<p className="nr-body">{notification.body}</p>
<span className="nr-time">
{new Date(notification.created_at).toLocaleString()}
</span>
</div>
<IonIcon
icon={chevronForwardOutline}
className="nr-link-icon"
/>
</button>
))}
</div>
)}
</IonContent>
</IonPage>
);
};
export default NotificationsPage;