Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/components/CalendarView.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../context/AuthContext';
import { TaskModal } from './TaskModal';
import { SkeletonLoader } from './SkeletonLoader';
import { toast } from 'react-toastify';

const getWeekStart = (date = new Date()) => {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../context/AuthContext';
import { TaskModal } from './TaskModal';
import { SkeletonLoader } from './SkeletonLoader';
import { toast } from 'react-toastify';

const getWeekStart = (date = new Date()) => {
Expand Down Expand Up @@ -330,7 +331,7 @@ const Dashboard = () => {

<div className="tasks-grid">
{loading ? (
<div className="loading"><div className="spinner"></div></div>
<SkeletonLoader count={6} />
) : tasks.length === 0 ? (
<div className="empty-state" style={{ gridColumn: '1 / -1' }}>
<div className="empty-icon">📋</div>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/MyTasks.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../context/AuthContext';
import { TaskModal } from './TaskModal';
import { SkeletonLoader } from './SkeletonLoader';
import { toast } from 'react-toastify';

const getWeekStart = (date = new Date()) => {
Expand Down Expand Up @@ -176,7 +177,7 @@ const MyTasks = () => {
</div>

{loading ? (
<div className="loading"><div className="spinner"></div></div>
<SkeletonLoader count={6} />
) : filteredTasks.length === 0 ? (
<div className="empty-state">
<div className="empty-icon">📋</div>
Expand Down
99 changes: 99 additions & 0 deletions frontend/src/components/SkeletonLoader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
export const SkeletonLoader = ({ count = 3 }) => {
return (
<>
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="skeleton-card" style={{
padding: '20px',
background: 'var(--bg-card)',
borderRadius: '12px',
marginBottom: '12px',
animation: 'skeleton-pulse 1.5s ease-in-out infinite'
}}>
<div style={{
height: '20px',
width: '60%',
background: 'var(--bg-tertiary)',
borderRadius: '4px',
marginBottom: '12px'
}} />
<div style={{
height: '14px',
width: '80%',
background: 'var(--bg-tertiary)',
borderRadius: '4px',
marginBottom: '8px'
}} />
<div style={{
height: '14px',
width: '40%',
background: 'var(--bg-tertiary)',
borderRadius: '4px'
}} />
</div>
))}
<style>{`
@keyframes skeleton-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
`}</style>
</>
);
};

export const SkeletonTable = ({ rows = 5, cols = 4 }) => {
return (
<div style={{ padding: '20px', background: 'var(--bg-card)', borderRadius: '12px' }}>
<div style={{ display: 'flex', gap: '16px', marginBottom: '16px' }}>
{Array.from({ length: cols }).map((_, i) => (
<div key={i} style={{
height: '16px',
flex: 1,
background: 'var(--bg-tertiary)',
borderRadius: '4px'
}} />
))}
</div>
{Array.from({ length: rows }).map((_, rowIdx) => (
<div key={rowIdx} style={{ display: 'flex', gap: '16px', marginBottom: '12px' }}>
{Array.from({ length: cols }).map((_, colIdx) => (
<div key={colIdx} style={{
height: '14px',
flex: 1,
background: 'var(--bg-tertiary)',
borderRadius: '4px',
opacity: 1 - (rowIdx * 0.1)
}} />
))}
</div>
))}
<style>{`
@keyframes skeleton-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
`}</style>
</div>
);
};

export const LoadingSpinner = ({ size = 'medium' }) => {
const sizes = { small: '20px', medium: '40px', large: '60px' };
return (
<div className="loading-spinner" style={{
width: sizes[size],
height: sizes[size],
border: '3px solid var(--bg-tertiary)',
borderTop: '3px solid var(--accent-primary)',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}}>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
};
15 changes: 15 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@
box-sizing: border-box;
}

@keyframes skeleton-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}

body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-primary);
Expand Down