✨(front) add CalendarList subcomponents
Add modular subcomponents for CalendarList including item menu, list item, create/edit modal, delete confirmation and subscription URL display modals. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* CalendarItemMenu component.
|
||||||
|
* Context menu for calendar item actions (edit, delete).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useRef, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import type { CalendarItemMenuProps } from "./types";
|
||||||
|
|
||||||
|
export const CalendarItemMenu = ({
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onSubscription,
|
||||||
|
onClose,
|
||||||
|
}: CalendarItemMenuProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
onEdit();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
onDelete();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubscription = () => {
|
||||||
|
if (onSubscription) {
|
||||||
|
onSubscription();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={menuRef} className="calendar-list__menu">
|
||||||
|
<button className="calendar-list__menu-item" onClick={handleEdit}>
|
||||||
|
<span className="material-icons">edit</span>
|
||||||
|
{t('calendar.list.edit')}
|
||||||
|
</button>
|
||||||
|
{onSubscription && (
|
||||||
|
<button className="calendar-list__menu-item" onClick={handleSubscription}>
|
||||||
|
<span className="material-icons">link</span>
|
||||||
|
{t('calendar.list.subscription')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="calendar-list__menu-item calendar-list__menu-item--danger"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<span className="material-icons">delete</span>
|
||||||
|
{t('calendar.list.delete')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* CalendarListItem components.
|
||||||
|
* Display individual calendar items in the list.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Checkbox } from "@gouvfr-lasuite/cunningham-react";
|
||||||
|
|
||||||
|
import { CalendarItemMenu } from "./CalendarItemMenu";
|
||||||
|
import type { CalendarListItemProps, SharedCalendarListItemProps } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CalendarListItem - Displays a user-owned calendar.
|
||||||
|
*/
|
||||||
|
export const CalendarListItem = ({
|
||||||
|
calendar,
|
||||||
|
isVisible,
|
||||||
|
isMenuOpen,
|
||||||
|
onToggleVisibility,
|
||||||
|
onMenuToggle,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onSubscription,
|
||||||
|
onCloseMenu,
|
||||||
|
}: CalendarListItemProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="calendar-list__item">
|
||||||
|
<div className="calendar-list__item-checkbox">
|
||||||
|
<Checkbox
|
||||||
|
checked={isVisible}
|
||||||
|
onChange={() => onToggleVisibility(calendar.url)}
|
||||||
|
label=""
|
||||||
|
aria-label={`${t('calendar.list.showCalendar')} ${calendar.displayName || ''}`}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="calendar-list__color"
|
||||||
|
style={{ backgroundColor: calendar.color }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="calendar-list__name"
|
||||||
|
title={calendar.displayName || undefined}
|
||||||
|
>
|
||||||
|
{calendar.displayName || 'Sans nom'}
|
||||||
|
</span>
|
||||||
|
<div className="calendar-list__item-actions">
|
||||||
|
<button
|
||||||
|
className="calendar-list__options-btn"
|
||||||
|
onClick={(e) => onMenuToggle(calendar.url, e)}
|
||||||
|
aria-label="Options"
|
||||||
|
>
|
||||||
|
<span className="material-icons">more_horiz</span>
|
||||||
|
</button>
|
||||||
|
{isMenuOpen && (
|
||||||
|
<CalendarItemMenu
|
||||||
|
onEdit={() => onEdit(calendar)}
|
||||||
|
onDelete={() => onDelete(calendar)}
|
||||||
|
onSubscription={onSubscription ? () => onSubscription(calendar) : undefined}
|
||||||
|
onClose={onCloseMenu}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SharedCalendarListItem - Displays a shared calendar.
|
||||||
|
*/
|
||||||
|
export const SharedCalendarListItem = ({
|
||||||
|
calendar,
|
||||||
|
isVisible,
|
||||||
|
onToggleVisibility,
|
||||||
|
}: SharedCalendarListItemProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="calendar-list__item">
|
||||||
|
<div className="calendar-list__item-checkbox">
|
||||||
|
<Checkbox
|
||||||
|
checked={isVisible}
|
||||||
|
onChange={() => onToggleVisibility(String(calendar.id))}
|
||||||
|
label=""
|
||||||
|
aria-label={`${t('calendar.list.showCalendar')} ${calendar.name}`}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="calendar-list__color"
|
||||||
|
style={{ backgroundColor: calendar.color }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="calendar-list__name" title={calendar.name}>
|
||||||
|
{calendar.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
/**
|
||||||
|
* CalendarModal component.
|
||||||
|
* Handles creation and editing of calendars, including sharing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalSize,
|
||||||
|
TextArea,
|
||||||
|
} from "@gouvfr-lasuite/cunningham-react";
|
||||||
|
|
||||||
|
import { DEFAULT_COLORS } from "./constants";
|
||||||
|
import type { CalendarModalProps } from "./types";
|
||||||
|
|
||||||
|
export const CalendarModal = ({
|
||||||
|
isOpen,
|
||||||
|
mode,
|
||||||
|
calendar,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
onShare,
|
||||||
|
}: CalendarModalProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [color, setColor] = useState(DEFAULT_COLORS[0]);
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Share state
|
||||||
|
const [shareEmail, setShareEmail] = useState("");
|
||||||
|
const [isSharing, setIsSharing] = useState(false);
|
||||||
|
const [shareSuccess, setShareSuccess] = useState<string | null>(null);
|
||||||
|
const [shareError, setShareError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Reset form when modal opens or calendar changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
if (mode === "edit" && calendar) {
|
||||||
|
setName(calendar.displayName || "");
|
||||||
|
setColor(calendar.color || DEFAULT_COLORS[0]);
|
||||||
|
setDescription(calendar.description || "");
|
||||||
|
} else {
|
||||||
|
setName("");
|
||||||
|
setColor(DEFAULT_COLORS[0]);
|
||||||
|
setDescription("");
|
||||||
|
}
|
||||||
|
setError(null);
|
||||||
|
setShareEmail("");
|
||||||
|
setShareSuccess(null);
|
||||||
|
setShareError(null);
|
||||||
|
}
|
||||||
|
}, [isOpen, mode, calendar]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!name.trim()) {
|
||||||
|
setError(t('calendar.createCalendar.nameRequired'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await onSave(name.trim(), color, description.trim() || undefined);
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : t('api.error.unexpected'));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShare = async () => {
|
||||||
|
if (!shareEmail.trim() || !onShare) return;
|
||||||
|
|
||||||
|
// Basic email validation
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(shareEmail.trim())) {
|
||||||
|
setShareError(t('calendar.shareCalendar.invalidEmail'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSharing(true);
|
||||||
|
setShareError(null);
|
||||||
|
setShareSuccess(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await onShare(shareEmail.trim());
|
||||||
|
if (result.success) {
|
||||||
|
setShareSuccess(
|
||||||
|
t('calendar.shareCalendar.success', { email: shareEmail.trim() })
|
||||||
|
);
|
||||||
|
setShareEmail("");
|
||||||
|
} else {
|
||||||
|
setShareError(result.error || t('calendar.shareCalendar.error'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setShareError(
|
||||||
|
err instanceof Error ? err.message : t('calendar.shareCalendar.error')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsSharing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setName("");
|
||||||
|
setColor(DEFAULT_COLORS[0]);
|
||||||
|
setDescription("");
|
||||||
|
setError(null);
|
||||||
|
setShareEmail("");
|
||||||
|
setShareSuccess(null);
|
||||||
|
setShareError(null);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const title =
|
||||||
|
mode === "create"
|
||||||
|
? t('calendar.createCalendar.title')
|
||||||
|
: t('calendar.editCalendar.title');
|
||||||
|
|
||||||
|
const saveLabel =
|
||||||
|
mode === "create"
|
||||||
|
? t('calendar.createCalendar.create')
|
||||||
|
: t('calendar.editCalendar.save');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
size={ModalSize.MEDIUM}
|
||||||
|
title={title}
|
||||||
|
rightActions={
|
||||||
|
<>
|
||||||
|
<Button color="neutral" onClick={handleClose} disabled={isLoading}>
|
||||||
|
{t('calendar.event.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="brand"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isLoading || !name.trim()}
|
||||||
|
>
|
||||||
|
{isLoading ? "..." : saveLabel}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="calendar-modal__content">
|
||||||
|
{error && <div className="calendar-modal__error">{error}</div>}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label={t('calendar.createCalendar.name')}
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="calendar-modal__field">
|
||||||
|
<label className="calendar-modal__label">
|
||||||
|
{t('calendar.createCalendar.color')}
|
||||||
|
</label>
|
||||||
|
<div className="calendar-modal__colors">
|
||||||
|
{DEFAULT_COLORS.map((c) => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
type="button"
|
||||||
|
className={`calendar-modal__color-btn ${
|
||||||
|
color === c ? 'calendar-modal__color-btn--selected' : ''
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: c }}
|
||||||
|
onClick={() => setColor(c)}
|
||||||
|
aria-label={c}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
label={t('calendar.createCalendar.description')}
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Share section - only visible in edit mode */}
|
||||||
|
{mode === "edit" && onShare && (
|
||||||
|
<div className="calendar-modal__share-section">
|
||||||
|
<div className="calendar-modal__share-divider" />
|
||||||
|
<label className="calendar-modal__label">
|
||||||
|
<span className="material-icons">person_add</span>
|
||||||
|
{t('calendar.shareCalendar.title')}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{shareSuccess && (
|
||||||
|
<div className="calendar-modal__success">{shareSuccess}</div>
|
||||||
|
)}
|
||||||
|
{shareError && (
|
||||||
|
<div className="calendar-modal__error">{shareError}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="calendar-modal__share-input-row">
|
||||||
|
<Input
|
||||||
|
label=""
|
||||||
|
value={shareEmail}
|
||||||
|
onChange={(e) => setShareEmail(e.target.value)}
|
||||||
|
placeholder={t('calendar.shareCalendar.emailPlaceholder')}
|
||||||
|
fullWidth
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleShare();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
color="brand"
|
||||||
|
onClick={handleShare}
|
||||||
|
disabled={isSharing || !shareEmail.trim()}
|
||||||
|
>
|
||||||
|
{isSharing ? "..." : t('calendar.shareCalendar.share')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="calendar-modal__share-hint">
|
||||||
|
{t('calendar.shareCalendar.hint')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* DeleteConfirmModal component.
|
||||||
|
* Confirms calendar deletion.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button, Modal, ModalSize } from "@gouvfr-lasuite/cunningham-react";
|
||||||
|
|
||||||
|
import type { DeleteConfirmModalProps } from "./types";
|
||||||
|
|
||||||
|
export const DeleteConfirmModal = ({
|
||||||
|
isOpen,
|
||||||
|
calendarName,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
isLoading,
|
||||||
|
}: DeleteConfirmModalProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onCancel}
|
||||||
|
size={ModalSize.SMALL}
|
||||||
|
title={t('calendar.deleteCalendar.title')}
|
||||||
|
rightActions={
|
||||||
|
<>
|
||||||
|
<Button color="neutral" onClick={onCancel} disabled={isLoading}>
|
||||||
|
{t('calendar.event.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button color="error" onClick={onConfirm} disabled={isLoading}>
|
||||||
|
{isLoading ? "..." : t('calendar.deleteCalendar.confirm')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>{t('calendar.deleteCalendar.message', { name: calendarName })}</p>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* SubscriptionUrlModal component.
|
||||||
|
* Displays the subscription URL for iCal export with copy and regenerate options.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button, Modal, ModalSize } from "@gouvfr-lasuite/cunningham-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useCreateSubscriptionToken,
|
||||||
|
useDeleteSubscriptionToken,
|
||||||
|
useSubscriptionToken,
|
||||||
|
} from "../../hooks/useCalendars";
|
||||||
|
|
||||||
|
interface SubscriptionUrlModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
caldavPath: string;
|
||||||
|
calendarName: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubscriptionUrlModal = ({
|
||||||
|
isOpen,
|
||||||
|
caldavPath,
|
||||||
|
calendarName,
|
||||||
|
onClose,
|
||||||
|
}: SubscriptionUrlModalProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [showRegenerateConfirm, setShowRegenerateConfirm] = useState(false);
|
||||||
|
const [hasTriedCreate, setHasTriedCreate] = useState(false);
|
||||||
|
|
||||||
|
const { token, tokenError, isLoading } = useSubscriptionToken(caldavPath);
|
||||||
|
const createToken = useCreateSubscriptionToken();
|
||||||
|
const deleteToken = useDeleteSubscriptionToken();
|
||||||
|
|
||||||
|
// Use token from query or from mutation result (whichever is available)
|
||||||
|
const displayToken = token || createToken.data;
|
||||||
|
// Show error from token fetch or from creation failure
|
||||||
|
const hasRealError = tokenError || (createToken.error && hasTriedCreate);
|
||||||
|
const isRegenerating = deleteToken.isPending || createToken.isPending;
|
||||||
|
const showLoading = isLoading || createToken.isPending;
|
||||||
|
|
||||||
|
// Get appropriate error message based on error type
|
||||||
|
const getErrorMessage = (): string => {
|
||||||
|
if (tokenError) {
|
||||||
|
switch (tokenError.type) {
|
||||||
|
case "permission_denied":
|
||||||
|
return t("calendar.subscription.errorPermission");
|
||||||
|
case "network_error":
|
||||||
|
return t("calendar.subscription.errorNetwork");
|
||||||
|
case "server_error":
|
||||||
|
return t("calendar.subscription.errorServer");
|
||||||
|
default:
|
||||||
|
return t("calendar.subscription.error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t("calendar.subscription.error");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset hasTriedCreate when modal closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setHasTriedCreate(false);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Create token on first open if none exists (only try once)
|
||||||
|
// We also try to create if there was an error (404 means no token exists)
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
isOpen &&
|
||||||
|
!token &&
|
||||||
|
!isLoading &&
|
||||||
|
!createToken.isPending &&
|
||||||
|
!hasTriedCreate
|
||||||
|
) {
|
||||||
|
setHasTriedCreate(true);
|
||||||
|
createToken.mutate({ caldavPath, calendarName });
|
||||||
|
}
|
||||||
|
}, [isOpen, token, isLoading, createToken, caldavPath, calendarName, hasTriedCreate]);
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
const url = displayToken?.url;
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(url);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch {
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement("textarea");
|
||||||
|
textArea.value = url;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegenerate = async () => {
|
||||||
|
setShowRegenerateConfirm(false);
|
||||||
|
await deleteToken.mutateAsync(caldavPath);
|
||||||
|
await createToken.mutateAsync({ caldavPath, calendarName });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen && !showRegenerateConfirm}
|
||||||
|
onClose={onClose}
|
||||||
|
size={ModalSize.MEDIUM}
|
||||||
|
title={t("calendar.subscription.title")}
|
||||||
|
rightActions={
|
||||||
|
<Button color="brand" onClick={onClose}>
|
||||||
|
{t("calendar.subscription.close")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="subscription-modal">
|
||||||
|
<p className="subscription-modal__description">
|
||||||
|
{t("calendar.subscription.description", { name: calendarName })}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{showLoading ? (
|
||||||
|
<div className="subscription-modal__loading">
|
||||||
|
{t("calendar.subscription.loading")}
|
||||||
|
</div>
|
||||||
|
) : hasRealError && !displayToken ? (
|
||||||
|
<div className="subscription-modal__error">
|
||||||
|
{getErrorMessage()}
|
||||||
|
</div>
|
||||||
|
) : displayToken?.url ? (
|
||||||
|
<>
|
||||||
|
<div className="subscription-modal__url-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
value={displayToken.url}
|
||||||
|
className="subscription-modal__url-input"
|
||||||
|
onClick={(e) => (e.target as HTMLInputElement).select()}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
color="brand"
|
||||||
|
onClick={handleCopy}
|
||||||
|
disabled={isRegenerating}
|
||||||
|
>
|
||||||
|
{copied
|
||||||
|
? t("calendar.subscription.copied")
|
||||||
|
: t("calendar.subscription.copy")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="subscription-modal__warning">
|
||||||
|
<span className="material-icons subscription-modal__warning-icon">
|
||||||
|
warning
|
||||||
|
</span>
|
||||||
|
<p>{t("calendar.subscription.warning")}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="subscription-modal__actions">
|
||||||
|
<Button
|
||||||
|
color="neutral"
|
||||||
|
onClick={() => setShowRegenerateConfirm(true)}
|
||||||
|
disabled={isRegenerating}
|
||||||
|
>
|
||||||
|
{t("calendar.subscription.regenerate")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Regenerate confirmation modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={showRegenerateConfirm}
|
||||||
|
onClose={() => setShowRegenerateConfirm(false)}
|
||||||
|
size={ModalSize.SMALL}
|
||||||
|
title={t("calendar.subscription.regenerateConfirm.title")}
|
||||||
|
rightActions={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
color="neutral"
|
||||||
|
onClick={() => setShowRegenerateConfirm(false)}
|
||||||
|
disabled={isRegenerating}
|
||||||
|
>
|
||||||
|
{t("calendar.event.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="error"
|
||||||
|
onClick={handleRegenerate}
|
||||||
|
disabled={isRegenerating}
|
||||||
|
>
|
||||||
|
{isRegenerating
|
||||||
|
? "..."
|
||||||
|
: t("calendar.subscription.regenerateConfirm.confirm")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>{t("calendar.subscription.regenerateConfirm.message")}</p>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user