feat: redesign recurring edit dialogs with Google Calendar style and i18n
- Redesigned drag/resize and modal recurring edit dialogs with radio buttons (This event / This and following / All events) with descriptions - Added German/English i18n to both recurring edit dialogs - Repeat-on day buttons now respect weekStartDay setting (Mon/Sun start) - Added language prop to CalendarEventModal v1.70.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
74a8c9a9a9
commit
948d239867
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.69.0",
|
"version": "1.70.0",
|
||||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -40,6 +40,8 @@ interface CalendarEventModalProps {
|
|||||||
initialDate?: Date;
|
initialDate?: Date;
|
||||||
initialStartTime?: string;
|
initialStartTime?: string;
|
||||||
connections: any[];
|
connections: any[];
|
||||||
|
weekStartDay?: number; // 0=Sunday, 1=Monday
|
||||||
|
language?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (eventData: any) => Promise<void>;
|
onSave: (eventData: any) => Promise<void>;
|
||||||
onDelete?: (eventId: string, calendarId: string, deleteMode?: string) => Promise<void>;
|
onDelete?: (eventId: string, calendarId: string, deleteMode?: string) => Promise<void>;
|
||||||
@ -50,6 +52,8 @@ export default function CalendarEventModal({
|
|||||||
initialDate,
|
initialDate,
|
||||||
initialStartTime,
|
initialStartTime,
|
||||||
connections,
|
connections,
|
||||||
|
weekStartDay = 0,
|
||||||
|
language = 'en',
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
onDelete
|
onDelete
|
||||||
@ -223,6 +227,7 @@ export default function CalendarEventModal({
|
|||||||
const [isDeleteConfirming, setIsDeleteConfirming] = useState(false);
|
const [isDeleteConfirming, setIsDeleteConfirming] = useState(false);
|
||||||
const [showRecurringDeleteOptions, setShowRecurringDeleteOptions] = useState(false);
|
const [showRecurringDeleteOptions, setShowRecurringDeleteOptions] = useState(false);
|
||||||
const [showRecurringEditOptions, setShowRecurringEditOptions] = useState(false);
|
const [showRecurringEditOptions, setShowRecurringEditOptions] = useState(false);
|
||||||
|
const [recurringEditMode, setRecurringEditMode] = useState<'this' | 'future' | 'all'>('this');
|
||||||
|
|
||||||
const handleDelete = async (mode?: string) => {
|
const handleDelete = async (mode?: string) => {
|
||||||
if (!event?.id || !onDelete) return;
|
if (!event?.id || !onDelete) return;
|
||||||
@ -503,26 +508,30 @@ export default function CalendarEventModal({
|
|||||||
<div style={{ ...rowStyle, flexDirection: 'column', alignItems: 'flex-start', gap: '6px' }}>
|
<div style={{ ...rowStyle, flexDirection: 'column', alignItems: 'flex-start', gap: '6px' }}>
|
||||||
<span style={labelStyle}>Repeat on</span>
|
<span style={labelStyle}>Repeat on</span>
|
||||||
<div style={{ display: 'flex', gap: '4px', paddingLeft: '0' }}>
|
<div style={{ display: 'flex', gap: '4px', paddingLeft: '0' }}>
|
||||||
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => (
|
{(() => {
|
||||||
<button
|
const allDays = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
||||||
key={i}
|
const dayIndices = Array.from({ length: 7 }, (_, i) => (i + weekStartDay) % 7);
|
||||||
onClick={() => {
|
return dayIndices.map(dow => (
|
||||||
setCustomDays(prev =>
|
<button
|
||||||
prev.includes(i) ? (prev.length > 1 ? prev.filter(d => d !== i) : prev) : [...prev, i]
|
key={dow}
|
||||||
);
|
onClick={() => {
|
||||||
}}
|
setCustomDays(prev =>
|
||||||
style={{
|
prev.includes(dow) ? (prev.length > 1 ? prev.filter(d => d !== dow) : prev) : [...prev, dow]
|
||||||
width: '30px', height: '30px', borderRadius: '50%',
|
);
|
||||||
border: customDays.includes(i) ? '2px solid #3b82f6' : '1px solid var(--weekly-border, #ddd)',
|
}}
|
||||||
backgroundColor: customDays.includes(i) ? '#3b82f6' : 'transparent',
|
style={{
|
||||||
color: customDays.includes(i) ? 'white' : 'var(--weekly-text)',
|
width: '30px', height: '30px', borderRadius: '50%',
|
||||||
fontSize: '0.75rem', fontWeight: 600, cursor: 'pointer',
|
border: customDays.includes(dow) ? '2px solid #3b82f6' : '1px solid var(--weekly-border, #ddd)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
backgroundColor: customDays.includes(dow) ? '#3b82f6' : 'transparent',
|
||||||
}}
|
color: customDays.includes(dow) ? 'white' : 'var(--weekly-text)',
|
||||||
>
|
fontSize: '0.75rem', fontWeight: 600, cursor: 'pointer',
|
||||||
{day}
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
</button>
|
}}
|
||||||
))}
|
>
|
||||||
|
{allDays[dow]}
|
||||||
|
</button>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -848,55 +857,87 @@ export default function CalendarEventModal({
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!showRecurringEditOptions ? (
|
<button
|
||||||
<button
|
className="weekly-btn-primary"
|
||||||
className="weekly-btn-primary"
|
onClick={() => handleSubmit()}
|
||||||
onClick={() => handleSubmit()}
|
disabled={isSaving || isDeleting}
|
||||||
disabled={isSaving || isDeleting}
|
style={{
|
||||||
style={{
|
padding: '6px 16px', borderRadius: '6px', fontSize: '0.85rem',
|
||||||
padding: '6px 16px', borderRadius: '6px', fontSize: '0.85rem',
|
fontWeight: 600, backgroundColor: '#3b82f6', color: 'white',
|
||||||
fontWeight: 600, backgroundColor: '#3b82f6', color: 'white',
|
border: 'none', cursor: 'pointer',
|
||||||
border: 'none', cursor: 'pointer',
|
opacity: isSaving || isDeleting ? 0.7 : 1
|
||||||
opacity: isSaving || isDeleting ? 0.7 : 1
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{isSaving ? 'Saving...' : 'Save'}
|
||||||
{isSaving ? 'Saving...' : 'Save'}
|
</button>
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
|
||||||
<span style={{ fontSize: '0.75rem', color: 'var(--weekly-text-light)', marginRight: '2px' }}>Save:</span>
|
|
||||||
{[
|
|
||||||
{ mode: 'this', label: 'This event' },
|
|
||||||
{ mode: 'all', label: 'All events' },
|
|
||||||
].map(({ mode, label }) => (
|
|
||||||
<button
|
|
||||||
key={mode}
|
|
||||||
onClick={() => handleSubmit(mode)}
|
|
||||||
disabled={isSaving}
|
|
||||||
style={{
|
|
||||||
padding: '4px 10px', fontSize: '0.78rem', fontWeight: 600,
|
|
||||||
color: 'white', background: mode === 'this' ? '#3b82f6' : '#6366f1',
|
|
||||||
border: 'none', borderRadius: '5px',
|
|
||||||
cursor: 'pointer', opacity: isSaving ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isSaving ? '...' : label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
onClick={() => setShowRecurringEditOptions(false)}
|
|
||||||
style={{
|
|
||||||
padding: '4px 6px', fontSize: '0.75rem', color: 'var(--weekly-text-light)',
|
|
||||||
background: 'none', border: 'none', cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Recurring edit mode overlay */}
|
||||||
|
{showRecurringEditOptions && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed', inset: 0, zIndex: 3000,
|
||||||
|
display: 'flex', justifyContent: 'center', alignItems: 'center',
|
||||||
|
background: 'rgba(0,0,0,0.35)', backdropFilter: 'blur(2px)',
|
||||||
|
}} onClick={() => setShowRecurringEditOptions(false)}>
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--weekly-bg, white)', color: 'var(--weekly-text, #333)',
|
||||||
|
borderRadius: 12, width: '90%', maxWidth: 420,
|
||||||
|
boxShadow: '0 12px 40px rgba(0,0,0,0.25)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}} onClick={e => e.stopPropagation()}>
|
||||||
|
<div style={{
|
||||||
|
padding: '16px 20px 12px', fontWeight: 700, fontSize: '0.95rem',
|
||||||
|
borderBottom: '2px solid #3b82f6',
|
||||||
|
}}>
|
||||||
|
{language === 'de' ? 'Wiederkehrendes Ereignis bearbeiten' : 'Edit recurring event'}
|
||||||
|
<button onClick={() => setShowRecurringEditOptions(false)} style={{
|
||||||
|
float: 'right', background: 'none', border: 'none', cursor: 'pointer',
|
||||||
|
color: 'var(--weekly-text-light)', padding: 0,
|
||||||
|
}}><X size={18} /></button>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '12px 20px' }}>
|
||||||
|
{([
|
||||||
|
{ value: 'this' as const, title: language === 'de' ? 'Nur dieses Ereignis' : 'This event', desc: language === 'de' ? 'Alle anderen Ereignisse der Serie bleiben unverändert.' : 'All other events in the series stay the same.' },
|
||||||
|
{ value: 'future' as const, title: language === 'de' ? 'Dieses und folgende Ereignisse' : 'This and following events', desc: language === 'de' ? 'Dieses und alle zukünftigen Ereignisse der Serie werden geändert.' : 'This and all future events in the series will be changed.' },
|
||||||
|
{ value: 'all' as const, title: language === 'de' ? 'Alle Ereignisse' : 'All events', desc: language === 'de' ? 'Alle Ereignisse der Serie werden geändert.' : 'All events in the series will be changed.' },
|
||||||
|
]).map(opt => (
|
||||||
|
<label key={opt.value} style={{
|
||||||
|
display: 'flex', gap: 12, padding: '10px 4px', cursor: 'pointer',
|
||||||
|
borderBottom: '1px solid var(--weekly-border, #eee)',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
}} onClick={() => setRecurringEditMode(opt.value)}>
|
||||||
|
<input
|
||||||
|
type="radio" name="recurringEditMode"
|
||||||
|
checked={recurringEditMode === opt.value}
|
||||||
|
onChange={() => setRecurringEditMode(opt.value)}
|
||||||
|
style={{ marginTop: 3, accentColor: '#3b82f6' }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: '0.88rem' }}>{opt.title}</div>
|
||||||
|
<div style={{ fontSize: '0.78rem', opacity: 0.6, marginTop: 2 }}>{opt.desc}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
padding: '12px 20px', display: 'flex', justifyContent: 'flex-end', gap: 8,
|
||||||
|
}}>
|
||||||
|
<button onClick={() => setShowRecurringEditOptions(false)} style={{
|
||||||
|
padding: '8px 16px', fontSize: '0.85rem', fontWeight: 600,
|
||||||
|
background: 'none', border: 'none', cursor: 'pointer',
|
||||||
|
color: 'var(--weekly-text-light)',
|
||||||
|
}}>{language === 'de' ? 'Abbrechen' : 'Cancel'}</button>
|
||||||
|
<button onClick={() => handleSubmit(recurringEditMode)} disabled={isSaving} style={{
|
||||||
|
padding: '8px 20px', fontSize: '0.85rem', fontWeight: 600,
|
||||||
|
background: '#3b82f6', color: 'white', border: 'none',
|
||||||
|
borderRadius: 8, cursor: 'pointer',
|
||||||
|
opacity: isSaving ? 0.6 : 1,
|
||||||
|
}}>{isSaving ? (language === 'de' ? 'Speichern...' : 'Saving...') : (language === 'de' ? 'Speichern' : 'Save')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2247,6 +2247,7 @@ export default function WeeklyView() {
|
|||||||
originalStartTime: string;
|
originalStartTime: string;
|
||||||
originalEndTime: string;
|
originalEndTime: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [recurringDragEditMode, setRecurringDragEditMode] = useState<'this' | 'future' | 'all'>('this');
|
||||||
|
|
||||||
// Dark Mode Persistence & Class Toggle
|
// Dark Mode Persistence & Class Toggle
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
@ -3820,6 +3821,7 @@ export default function WeeklyView() {
|
|||||||
// Check if this is a recurring event — if so, ask before saving
|
// Check if this is a recurring event — if so, ask before saving
|
||||||
const draggedEvent = calendarEvents.find(e => e.id === eventDragState.eventId);
|
const draggedEvent = calendarEvents.find(e => e.id === eventDragState.eventId);
|
||||||
if (draggedEvent?.isRecurring) {
|
if (draggedEvent?.isRecurring) {
|
||||||
|
setRecurringDragEditMode('this');
|
||||||
setPendingRecurringDrag({
|
setPendingRecurringDrag({
|
||||||
eventId: eventDragState.eventId,
|
eventId: eventDragState.eventId,
|
||||||
calendarId: eventDragState.calendarId || '',
|
calendarId: eventDragState.calendarId || '',
|
||||||
@ -3870,7 +3872,7 @@ export default function WeeklyView() {
|
|||||||
}, [eventDragState, effectiveCellDuration, calendarEvents]);
|
}, [eventDragState, effectiveCellDuration, calendarEvents]);
|
||||||
|
|
||||||
// Handle recurring event drag confirm (this/all)
|
// Handle recurring event drag confirm (this/all)
|
||||||
const handleRecurringDragConfirm = async (editMode: 'this' | 'all') => {
|
const handleRecurringDragConfirm = async (editMode: 'this' | 'future' | 'all') => {
|
||||||
if (!pendingRecurringDrag) return;
|
if (!pendingRecurringDrag) return;
|
||||||
const { eventId, calendarId, recurringEventId, startTime, endTime, originalStartTime, originalEndTime } = pendingRecurringDrag;
|
const { eventId, calendarId, recurringEventId, startTime, endTime, originalStartTime, originalEndTime } = pendingRecurringDrag;
|
||||||
try {
|
try {
|
||||||
@ -9153,6 +9155,8 @@ export default function WeeklyView() {
|
|||||||
initialDate={calendarEventModal.initialDate}
|
initialDate={calendarEventModal.initialDate}
|
||||||
initialStartTime={calendarEventModal.initialStartTime}
|
initialStartTime={calendarEventModal.initialStartTime}
|
||||||
connections={connections}
|
connections={connections}
|
||||||
|
weekStartDay={weekStartDay}
|
||||||
|
language={language}
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
setCalendarEventModal({ ...calendarEventModal, isOpen: false })
|
setCalendarEventModal({ ...calendarEventModal, isOpen: false })
|
||||||
}
|
}
|
||||||
@ -9166,57 +9170,58 @@ export default function WeeklyView() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
position: 'fixed', inset: 0, zIndex: 2000,
|
position: 'fixed', inset: 0, zIndex: 2000,
|
||||||
display: 'flex', justifyContent: 'center', alignItems: 'center',
|
display: 'flex', justifyContent: 'center', alignItems: 'center',
|
||||||
background: 'rgba(0,0,0,0.3)', backdropFilter: 'blur(2px)',
|
background: 'rgba(0,0,0,0.35)', backdropFilter: 'blur(2px)',
|
||||||
}}
|
}} onClick={handleRecurringDragCancel}>
|
||||||
onClick={handleRecurringDragCancel}
|
<div style={{
|
||||||
>
|
background: darkMode ? '#1e1e1e' : 'white',
|
||||||
<div
|
color: darkMode ? '#eee' : '#333',
|
||||||
style={{
|
borderRadius: 12, width: '90%', maxWidth: 420,
|
||||||
background: darkMode ? '#1e1e1e' : 'white',
|
boxShadow: '0 12px 40px rgba(0,0,0,0.25)',
|
||||||
color: darkMode ? '#eee' : '#333',
|
overflow: 'hidden',
|
||||||
borderRadius: 12, padding: '20px 24px',
|
}} onClick={e => e.stopPropagation()}>
|
||||||
boxShadow: '0 8px 30px rgba(0,0,0,0.25)',
|
<div style={{
|
||||||
minWidth: 260, textAlign: 'center',
|
padding: '16px 20px 12px', fontWeight: 700, fontSize: '0.95rem',
|
||||||
}}
|
borderBottom: '2px solid #3b82f6',
|
||||||
onClick={e => e.stopPropagation()}
|
}}>
|
||||||
>
|
|
||||||
<div style={{ fontSize: '0.9rem', fontWeight: 600, marginBottom: 4 }}>
|
|
||||||
{language === 'de' ? 'Wiederkehrendes Ereignis bearbeiten' : 'Edit recurring event'}
|
{language === 'de' ? 'Wiederkehrendes Ereignis bearbeiten' : 'Edit recurring event'}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '0.78rem', opacity: 0.7, marginBottom: 16 }}>
|
<div style={{ padding: '12px 20px' }}>
|
||||||
{language === 'de' ? 'Änderung anwenden auf:' : 'Apply change to:'}
|
{([
|
||||||
|
{ value: 'this' as const, title: language === 'de' ? 'Nur dieses Ereignis' : 'This event', desc: language === 'de' ? 'Alle anderen Ereignisse der Serie bleiben unverändert.' : 'All other events in the series stay the same.' },
|
||||||
|
{ value: 'future' as const, title: language === 'de' ? 'Dieses und folgende Ereignisse' : 'This and following events', desc: language === 'de' ? 'Dieses und alle zukünftigen Ereignisse der Serie werden geändert.' : 'This and all future events in the series will be changed.' },
|
||||||
|
{ value: 'all' as const, title: language === 'de' ? 'Alle Ereignisse' : 'All events', desc: language === 'de' ? 'Alle Ereignisse der Serie werden geändert.' : 'All events in the series will be changed.' },
|
||||||
|
]).map(opt => (
|
||||||
|
<label key={opt.value} style={{
|
||||||
|
display: 'flex', gap: 12, padding: '10px 4px', cursor: 'pointer',
|
||||||
|
borderBottom: `1px solid ${darkMode ? '#333' : '#eee'}`,
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
}} onClick={() => setRecurringDragEditMode(opt.value)}>
|
||||||
|
<input
|
||||||
|
type="radio" name="recurringDragEditMode"
|
||||||
|
checked={recurringDragEditMode === opt.value}
|
||||||
|
onChange={() => setRecurringDragEditMode(opt.value)}
|
||||||
|
style={{ marginTop: 3, accentColor: '#3b82f6' }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: '0.88rem' }}>{opt.title}</div>
|
||||||
|
<div style={{ fontSize: '0.78rem', opacity: 0.6, marginTop: 2 }}>{opt.desc}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
|
<div style={{
|
||||||
<button
|
padding: '12px 20px', display: 'flex', justifyContent: 'flex-end', gap: 8,
|
||||||
onClick={() => handleRecurringDragConfirm('this')}
|
}}>
|
||||||
style={{
|
<button onClick={handleRecurringDragCancel} style={{
|
||||||
padding: '8px 16px', fontSize: '0.82rem', fontWeight: 600,
|
padding: '8px 16px', fontSize: '0.85rem', fontWeight: 600,
|
||||||
color: 'white', background: '#3b82f6',
|
background: 'none', border: 'none', cursor: 'pointer',
|
||||||
border: 'none', borderRadius: 8, cursor: 'pointer',
|
color: darkMode ? '#aaa' : '#666',
|
||||||
}}
|
}}>{language === 'de' ? 'Abbrechen' : 'Cancel'}</button>
|
||||||
>
|
<button onClick={() => handleRecurringDragConfirm(recurringDragEditMode)} style={{
|
||||||
{language === 'de' ? 'Nur dieses' : 'This event'}
|
padding: '8px 20px', fontSize: '0.85rem', fontWeight: 600,
|
||||||
</button>
|
background: '#3b82f6', color: 'white', border: 'none',
|
||||||
<button
|
borderRadius: 8, cursor: 'pointer',
|
||||||
onClick={() => handleRecurringDragConfirm('all')}
|
}}>{language === 'de' ? 'Speichern' : 'Save'}</button>
|
||||||
style={{
|
|
||||||
padding: '8px 16px', fontSize: '0.82rem', fontWeight: 600,
|
|
||||||
color: 'white', background: '#6366f1',
|
|
||||||
border: 'none', borderRadius: 8, cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{language === 'de' ? 'Alle Ereignisse' : 'All events'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleRecurringDragCancel}
|
|
||||||
style={{
|
|
||||||
padding: '8px 16px', fontSize: '0.82rem', fontWeight: 500,
|
|
||||||
color: darkMode ? '#aaa' : '#666', background: darkMode ? '#333' : '#f0f0f0',
|
|
||||||
border: 'none', borderRadius: 8, cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{language === 'de' ? 'Abbrechen' : 'Cancel'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user