fix: implement custom CSS sync spinner and update all components

- Replaced broken animate-spin Tailwind class with custom weekly-spinner CSS

- Standardized loading indicators across Auth, Tasks, and Calendar settings

- Fixed JSX syntax errors in several components
This commit is contained in:
mARTin 2026-02-23 22:04:16 +01:00
parent eb5065131a
commit 83d0d0b920
8 changed files with 69 additions and 88 deletions

View File

@ -2939,4 +2939,30 @@ h3 {
/* Weekly App Spinner */
@keyframes weekly-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.weekly-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(0, 0, 0, 0.1);
border-top-color: #3b82f6;
border-radius: 50%;
animation: weekly-spin 0.8s linear infinite;
display: inline-block;
vertical-align: middle;
}
.weekly-spinner-white {
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
}
.dark-mode .weekly-spinner {
border: 2px solid rgba(255, 255, 255, 0.1);
border-top-color: #60a5fa;
}

View File

@ -141,10 +141,7 @@ const AuthForm: React.FC<AuthFormProps> = ({ type, onSubmit, isLoading, error })
> >
{isLoading ? ( {isLoading ? (
<span className="flex items-center"> <span className="flex items-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <div className="weekly-spinner weekly-spinner-white -ml-1 mr-3"></div>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Processing... Processing...
</span> </span>
) : type === 'signup' ? ( ) : type === 'signup' ? (
@ -179,8 +176,8 @@ const AuthForm: React.FC<AuthFormProps> = ({ type, onSubmit, isLoading, error })
</p> </p>
)} )}
</div> </div>
</div> </div >
</div> </div >
); );
}; };

View File

@ -248,10 +248,7 @@ const CalendarSettings: React.FC<CalendarSettingsProps> = ({
> >
{isSyncing[connection.id] ? ( {isSyncing[connection.id] ? (
<> <>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <div className="weekly-spinner -ml-1 mr-2 inline-block"></div>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Syncing... Syncing...
</> </>
) : ( ) : (
@ -290,7 +287,8 @@ const CalendarSettings: React.FC<CalendarSettingsProps> = ({
</div> </div>
))} ))}
</div> </div>
)} )
}
<div className="connect-options mt-8 pt-6 border-t border-gray-200" role="region" aria-label="Connect more calendars"> <div className="connect-options mt-8 pt-6 border-t border-gray-200" role="region" aria-label="Connect more calendars">
<h3 className="text-xl font-semibold text-gray-800 mb-4" role="heading" aria-level={3}>Add More Calendars</h3> <h3 className="text-xl font-semibold text-gray-800 mb-4" role="heading" aria-level={3}>Add More Calendars</h3>
@ -376,10 +374,7 @@ const CalendarSettings: React.FC<CalendarSettingsProps> = ({
> >
{isConnectingApple ? ( {isConnectingApple ? (
<> <>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <div className="weekly-spinner weekly-spinner-white -ml-1 mr-2"></div>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Connecting... Connecting...
</> </>
) : 'Connect'} ) : 'Connect'}
@ -429,19 +424,17 @@ const CalendarSettings: React.FC<CalendarSettingsProps> = ({
> >
{isConnectingApple ? ( {isConnectingApple ? (
<> <>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <div className="weekly-spinner weekly-spinner-white -ml-1 mr-2"></div>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Verifying... Verifying...
</> </>
) : 'Verify'} ) : 'Verify'}
</button> </button>
</div> </div>
</> </>
)} )
</div> }
</div> </div >
</div >
) )
} }
</div > </div >

View File

@ -82,7 +82,7 @@ export const ImportListModal: React.FC<ImportListModalProps> = ({
<div style={{ flex: 1, overflowY: 'auto', minHeight: '200px' }}> <div style={{ flex: 1, overflowY: 'auto', minHeight: '200px' }}>
{isLoading ? ( {isLoading ? (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}> <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
<Loader className="animate-spin" /> <div className="weekly-spinner"></div>
<span style={{ marginLeft: '8px' }}>Loading lists...</span> <span style={{ marginLeft: '8px' }}>Loading lists...</span>
</div> </div>
) : lists.length === 0 ? ( ) : lists.length === 0 ? (

View File

@ -27,20 +27,20 @@ const TaskForm: React.FC<TaskFormProps> = ({ onSubmit, isLoading = false }) => {
const validateForm = (): boolean => { const validateForm = (): boolean => {
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {};
if (!formData.title.trim()) { if (!formData.title.trim()) {
newErrors.title = 'Title is required'; newErrors.title = 'Title is required';
} }
// Validate time format if provided // Validate time format if provided
if (formData.startTime && !isValidTime(formData.startTime)) { if (formData.startTime && !isValidTime(formData.startTime)) {
newErrors.startTime = 'Invalid time format'; newErrors.startTime = 'Invalid time format';
} }
if (formData.endTime && !isValidTime(formData.endTime)) { if (formData.endTime && !isValidTime(formData.endTime)) {
newErrors.endTime = 'Invalid time format'; newErrors.endTime = 'Invalid time format';
} }
// Check if start time is before end time if both are provided // Check if start time is before end time if both are provided
if (formData.startTime && formData.endTime) { if (formData.startTime && formData.endTime) {
const start = parseTime(formData.startTime); const start = parseTime(formData.startTime);
@ -49,7 +49,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ onSubmit, isLoading = false }) => {
newErrors.endTime = 'End time must be after start time'; newErrors.endTime = 'End time must be after start time';
} }
} }
setErrors(newErrors); setErrors(newErrors);
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0;
}; };
@ -70,7 +70,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ onSubmit, isLoading = false }) => {
...formData, ...formData,
[name]: value [name]: value
}); });
// Clear error when user starts typing // Clear error when user starts typing
if (errors[name]) { if (errors[name]) {
setErrors(prev => { setErrors(prev => {
@ -83,7 +83,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ onSubmit, isLoading = false }) => {
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (validateForm()) { if (validateForm()) {
onSubmit(formData); onSubmit(formData);
// Reset form // Reset form
@ -101,7 +101,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ onSubmit, isLoading = false }) => {
return ( return (
<form onSubmit={handleSubmit} className="task-form bg-white rounded-lg shadow-md p-6 mb-6"> <form onSubmit={handleSubmit} className="task-form bg-white rounded-lg shadow-md p-6 mb-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Add New Task</h3> <h3 className="text-lg font-semibold text-gray-800 mb-4">Add New Task</h3>
<div className="form-group mb-4"> <div className="form-group mb-4">
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
Task Title * Task Title *
@ -112,9 +112,8 @@ const TaskForm: React.FC<TaskFormProps> = ({ onSubmit, isLoading = false }) => {
name="title" name="title"
value={formData.title} value={formData.title}
onChange={handleChange} onChange={handleChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${errors.title ? 'border-red-500' : 'border-gray-300'
errors.title ? 'border-red-500' : 'border-gray-300' }`}
}`}
placeholder="What needs to be done?" placeholder="What needs to be done?"
required required
/> />
@ -147,13 +146,12 @@ const TaskForm: React.FC<TaskFormProps> = ({ onSubmit, isLoading = false }) => {
name="startTime" name="startTime"
value={formData.startTime} value={formData.startTime}
onChange={handleChange} onChange={handleChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${errors.startTime ? 'border-red-500' : 'border-gray-300'
errors.startTime ? 'border-red-500' : 'border-gray-300' }`}
}`}
/> />
{errors.startTime && <p className="mt-1 text-sm text-red-600">{errors.startTime}</p>} {errors.startTime && <p className="mt-1 text-sm text-red-600">{errors.startTime}</p>}
</div> </div>
<div className="form-group"> <div className="form-group">
<label htmlFor="endTime" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="endTime" className="block text-sm font-medium text-gray-700 mb-1">
End Time End Time
@ -164,9 +162,8 @@ const TaskForm: React.FC<TaskFormProps> = ({ onSubmit, isLoading = false }) => {
name="endTime" name="endTime"
value={formData.endTime} value={formData.endTime}
onChange={handleChange} onChange={handleChange}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${errors.endTime ? 'border-red-500' : 'border-gray-300'
errors.endTime ? 'border-red-500' : 'border-gray-300' }`}
}`}
/> />
{errors.endTime && <p className="mt-1 text-sm text-red-600">{errors.endTime}</p>} {errors.endTime && <p className="mt-1 text-sm text-red-600">{errors.endTime}</p>}
</div> </div>
@ -193,28 +190,24 @@ const TaskForm: React.FC<TaskFormProps> = ({ onSubmit, isLoading = false }) => {
</select> </select>
</div> </div>
<button <button
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
className={`w-full py-2 px-4 rounded-md text-white font-medium transition-colors ${ className={`w-full py-2 px-4 rounded-md text-white font-medium transition-colors ${isLoading
isLoading ? 'bg-blue-400 cursor-not-allowed'
? 'bg-blue-400 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700'
: 'bg-blue-600 hover:bg-blue-700' }`}
}`}
> >
{isLoading ? ( {isLoading ? (
<span className="flex items-center justify-center"> <span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <div className="weekly-spinner weekly-spinner-white -ml-1 mr-2"></div>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Adding Task... Adding Task...
</span> </span>
) : ( ) : (
'Add Task' 'Add Task'
)} )}
</button> </button>
</form> </form >
); );
}; };

View File

@ -146,10 +146,7 @@ export default function TaskItem({ task, onToggleComplete, onDelete, onEdit, onT
aria-label="Delete task" aria-label="Delete task"
> >
{isDeleting ? ( {isDeleting ? (
<svg className="animate-spin h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <div className="weekly-spinner"></div>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : ( ) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" /> <path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />

View File

@ -3312,10 +3312,7 @@ export default function WeeklyView() {
{/* Week & Year */} {/* Week & Year */}
<div className="whitespace-nowrap flex items-center gap-2"> <div className="whitespace-nowrap flex items-center gap-2">
{(isLoading || isSyncing || syncStatus === "syncing") && ( {(isLoading || isSyncing || syncStatus === "syncing") && (
<div <div className="weekly-spinner" title="Syncing..."></div>
className="animate-spin rounded-full h-4 w-4 border-2 border-gray-300 border-t-blue-600 mr-2"
title="Syncing..."
></div>
)} )}
<span style={{ <span style={{
fontFamily: profile.cwFontFamily || "Inter", fontFamily: profile.cwFontFamily || "Inter",
@ -9688,26 +9685,7 @@ function SettingsSidebar({
> >
{isConnectingAppleCal ? ( {isConnectingAppleCal ? (
<> <>
<svg <div className="weekly-spinner weekly-spinner-white -ml-1 mr-2"></div>
className="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Connecting... Connecting...
</> </>
) : ( ) : (

View File

@ -32,7 +32,7 @@ const EmailAuthForm: React.FC<EmailAuthFormProps> = ({ type, onSubmit, isLoading
{type === 'signup' ? 'Join us today and organize your week' : 'Welcome back! Please enter your details'} {type === 'signup' ? 'Join us today and organize your week' : 'Welcome back! Please enter your details'}
</p> </p>
</div> </div>
{error && ( {error && (
<div className="rounded-lg bg-red-50 p-4 border border-red-200 mb-6"> <div className="rounded-lg bg-red-50 p-4 border border-red-200 mb-6">
<div className="flex"> <div className="flex">
@ -47,7 +47,7 @@ const EmailAuthForm: React.FC<EmailAuthFormProps> = ({ type, onSubmit, isLoading
</div> </div>
</div> </div>
)} )}
<form className="space-y-6" onSubmit={handleSubmit}> <form className="space-y-6" onSubmit={handleSubmit}>
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
@ -73,7 +73,7 @@ const EmailAuthForm: React.FC<EmailAuthFormProps> = ({ type, onSubmit, isLoading
/> />
</div> </div>
</div> </div>
<div> <div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Password Password
@ -128,10 +128,7 @@ const EmailAuthForm: React.FC<EmailAuthFormProps> = ({ type, onSubmit, isLoading
> >
{isLoading ? ( {isLoading ? (
<span className="flex items-center"> <span className="flex items-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <div className="weekly-spinner weekly-spinner-white -ml-1 mr-2"></div>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Processing... Processing...
</span> </span>
) : type === 'signup' ? ( ) : type === 'signup' ? (