feat: add expiration date to share tokens and enhance error handling for expired links
This commit is contained in:
+62
-3
@@ -3181,15 +3181,38 @@ type SharedScheduleData = {
|
||||
};
|
||||
};
|
||||
|
||||
type ExpiredLinkData = {
|
||||
ownerUsername: string;
|
||||
takenBy: string;
|
||||
expiredAt: string;
|
||||
};
|
||||
|
||||
function SharedSchedule() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const { t, i18n } = useTranslation();
|
||||
const [data, setData] = useState<SharedScheduleData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [expiredData, setExpiredData] = useState<ExpiredLinkData | null>(null);
|
||||
const [takenDoses, setTakenDoses] = useState<Set<string>>(new Set());
|
||||
const [lightboxImage, setLightboxImage] = useState<{ url: string; name: string } | null>(null);
|
||||
const [showPastDays, setShowPastDays] = useState(false);
|
||||
const [theme, setTheme] = useState<"light" | "dark">(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return (localStorage.getItem("theme") as "light" | "dark") || "dark";
|
||||
}
|
||||
return "dark";
|
||||
});
|
||||
|
||||
// Apply theme to document
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
function toggleTheme() {
|
||||
setTheme((prev) => (prev === "dark" ? "light" : "dark"));
|
||||
}
|
||||
// Collapsed days state for SharedSchedule (token-specific localStorage)
|
||||
const [manuallyCollapsedDays, setManuallyCollapsedDays] = useState<Set<string>>(new Set());
|
||||
const [manuallyExpandedDays, setManuallyExpandedDays] = useState<Set<string>>(new Set());
|
||||
@@ -3332,17 +3355,27 @@ function SharedSchedule() {
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json);
|
||||
} else if (res.status === 410) {
|
||||
// Link expired - get owner info
|
||||
const json = await res.json();
|
||||
setExpiredData({
|
||||
ownerUsername: json.ownerUsername,
|
||||
takenBy: json.takenBy,
|
||||
expiredAt: json.expiredAt,
|
||||
});
|
||||
} else if (res.status === 404) {
|
||||
setError(t('share.notFound'));
|
||||
} else {
|
||||
setError("Share link not found or expired");
|
||||
setError(t('share.error'));
|
||||
}
|
||||
} catch {
|
||||
setError("Failed to load schedule");
|
||||
setError(t('share.error'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [token]);
|
||||
}, [token, t]);
|
||||
|
||||
// Build schedule from medications
|
||||
const schedule = useMemo(() => {
|
||||
@@ -3498,6 +3531,27 @@ function SharedSchedule() {
|
||||
);
|
||||
}
|
||||
|
||||
if (expiredData) {
|
||||
return (
|
||||
<div className="shared-schedule-page">
|
||||
<div className="shared-schedule-error expired">
|
||||
<h1>💊 MedAssist</h1>
|
||||
<div className="expired-icon">⏰</div>
|
||||
<h2>{t('share.expired.title')}</h2>
|
||||
<p className="expired-message">
|
||||
{t('share.expired.message', { takenBy: expiredData.takenBy })}
|
||||
</p>
|
||||
<p className="expired-contact">
|
||||
{t('share.expired.contact', { username: expiredData.ownerUsername })}
|
||||
</p>
|
||||
<p className="expired-date">
|
||||
{t('share.expired.expiredOn', { date: new Date(expiredData.expiredAt).toLocaleDateString(i18n.language) })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="shared-schedule-page">
|
||||
@@ -3514,6 +3568,11 @@ function SharedSchedule() {
|
||||
<div className="shared-schedule-container">
|
||||
<header className="shared-schedule-header">
|
||||
<h1>💊 {t('share.scheduleFor')} {data.takenBy}</h1>
|
||||
<div className="shared-schedule-header-actions">
|
||||
<button className="icon-btn" onClick={toggleTheme} title={theme === "dark" ? t('tooltips.lightMode') : t('tooltips.darkMode')}>
|
||||
{theme === "dark" ? "☀️" : "🌙"}
|
||||
</button>
|
||||
</div>
|
||||
<p className="shared-schedule-period">
|
||||
{t('share.period')}: {data.scheduleDays === 30 ? t('dashboard.schedules.1month') : data.scheduleDays === 90 ? t('dashboard.schedules.3months') : t('dashboard.schedules.6months')}
|
||||
</p>
|
||||
|
||||
@@ -330,6 +330,14 @@
|
||||
"scheduleFor": "Zeitplan für",
|
||||
"period": "Zeitraum",
|
||||
"noSchedule": "Keine geplanten Einnahmen gefunden.",
|
||||
"generatedBy": "Erstellt von"
|
||||
"generatedBy": "Erstellt von",
|
||||
"notFound": "Teilen-Link nicht gefunden",
|
||||
"error": "Zeitplan konnte nicht geladen werden",
|
||||
"expired": {
|
||||
"title": "Link abgelaufen",
|
||||
"message": "Dieser Teilen-Link für den Medikamentenplan von {{takenBy}} ist abgelaufen.",
|
||||
"contact": "Bitte kontaktiere {{username}} um einen neuen Link anzufordern.",
|
||||
"expiredOn": "Abgelaufen am: {{date}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,6 +332,14 @@
|
||||
"scheduleFor": "Schedule for",
|
||||
"period": "Period",
|
||||
"noSchedule": "No scheduled doses found.",
|
||||
"generatedBy": "Generated by"
|
||||
"generatedBy": "Generated by",
|
||||
"notFound": "Share link not found",
|
||||
"error": "Failed to load schedule",
|
||||
"expired": {
|
||||
"title": "Link Expired",
|
||||
"message": "This share link for {{takenBy}}'s medication schedule has expired.",
|
||||
"contact": "Please contact {{username}} to request a new link.",
|
||||
"expiredOn": "Expired on: {{date}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3581,11 +3581,59 @@ h3 .reminder-icon.info-tooltip {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* Expired link styling */
|
||||
.shared-schedule-error.expired {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.shared-schedule-error .expired-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.shared-schedule-error.expired h2 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--warning);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.shared-schedule-error .expired-message {
|
||||
font-size: 1.125rem;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.shared-schedule-error .expired-contact {
|
||||
font-size: 1rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.shared-schedule-error .expired-date {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.shared-schedule-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.shared-schedule-header-actions {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.shared-schedule-header h1 {
|
||||
|
||||
Reference in New Issue
Block a user