242 lines
7.0 KiB
TypeScript
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;
|