build: 444813b5-5daf-49b2-bf1a-7e4caeab4e17
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user