feat: add expiration date to share tokens and enhance error handling for expired links

This commit is contained in:
Daniel Volz
2025-12-28 17:47:49 +01:00
parent 0e52a03f7a
commit abffd66e9c
10 changed files with 182 additions and 10 deletions
+62 -3
View File
@@ -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>
+9 -1
View File
@@ -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}}"
}
}
}
+9 -1
View File
@@ -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}}"
}
}
}
+48
View File
@@ -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 {