mirror of
https://github.com/Dvorinka/excalidraw-full.git
synced 2026-06-05 06:32:56 +00:00
feat(editor): implement autosave functionality and enhance UI
Docker Images / Build and push (push) Failing after 17s
Docker Images / Build and push (push) Failing after 17s
Implements an autosave mechanism to prevent data loss by periodically sending snapshots of the drawing to the backend. This includes new API endpoints on the server and updated frontend services. Additionally, improves the editor experience with: - Enhanced CSRF protection and origin validation in the backend. - Fix for React "Maximum update depth exceeded" error during scene mutations using a mutation guard. - New presentation slide thumbnails and navigation UI. - Expanded template library with various brainstorming layouts. - Refined dashboard statistics and layout styling. - Improved sidebar logo using SVG for better scaling.
This commit is contained in:
@@ -85,10 +85,12 @@
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island);
|
||||
transition: box-shadow 0.15s ease;
|
||||
transition: all 0.2s var(--ease-out);
|
||||
background: var(--island-bg-color);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +100,9 @@
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
padding: var(--space-5);
|
||||
min-height: 150px;
|
||||
min-height: 140px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.statTop {
|
||||
@@ -106,44 +110,55 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: var(--space-3);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.statIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-primary-light);
|
||||
border: 1px solid var(--default-border-color);
|
||||
}
|
||||
|
||||
.sparkline {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
margin-top: var(--space-2);
|
||||
background: var(--color-surface-low);
|
||||
border: 1px solid var(--color-gray-20);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: var(--text-3xl);
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
font-family: var(--ui-font);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-muted);
|
||||
margin-top: var(--space-1);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.statBarTrack {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: var(--color-gray-20);
|
||||
border-radius: var(--border-radius-full);
|
||||
margin-top: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.statBarFill {
|
||||
height: 100%;
|
||||
border-radius: var(--border-radius-full);
|
||||
transition: width 0.6s var(--ease-out);
|
||||
}
|
||||
|
||||
.progressBarWrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
margin-top: var(--space-3);
|
||||
border-radius: var(--border-radius-full);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -326,20 +341,26 @@
|
||||
|
||||
.activityItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) 0;
|
||||
padding: var(--space-3) var(--space-2);
|
||||
border-bottom: 1px solid var(--color-gray-20);
|
||||
transition: background 0.15s ease;
|
||||
border-radius: var(--border-radius-md);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-surface-low);
|
||||
}
|
||||
}
|
||||
|
||||
.activityAvatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: var(--border-radius-full);
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
display: flex;
|
||||
@@ -348,17 +369,19 @@
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border: 2px solid var(--island-bg-color);
|
||||
box-shadow: var(--shadow-island);
|
||||
}
|
||||
|
||||
.activityInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.activityText {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-gray-80);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.activityTime {
|
||||
|
||||
@@ -9,43 +9,15 @@ import styles from './Dashboard.module.scss';
|
||||
|
||||
const ACTIVITY_LIMIT = 5;
|
||||
|
||||
const ProgressBar: React.FC<{ value: number; max: number; color?: string }> = ({ value, max, color = '#6965db' }) => {
|
||||
const StatBar: React.FC<{ value: number; max: number; color: string }> = ({ value, max, color }) => {
|
||||
const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0;
|
||||
return (
|
||||
<div className={styles.progressBarWrap} aria-hidden="true">
|
||||
<div className={styles.progressBarBg} />
|
||||
<div className={styles.progressBarFill} style={{ width: `${pct}%`, background: color }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MiniSparkline: React.FC<{ data: number[]; color?: string }> = ({ data, color = '#6965db' }) => {
|
||||
if (!data.length) return null;
|
||||
const w = 140;
|
||||
const h = 40;
|
||||
const max = Math.max(...data, 1);
|
||||
const min = Math.min(...data, 0);
|
||||
const range = max - min || 1;
|
||||
const stepX = w / (data.length - 1 || 1);
|
||||
|
||||
const points = data.map((v, i) => {
|
||||
const x = i * stepX;
|
||||
const y = h - ((v - min) / range) * (h - 4) - 2;
|
||||
return `${x},${y}`;
|
||||
}).join(' ');
|
||||
|
||||
return (
|
||||
<svg className={styles.sparkline} viewBox={`0 0 ${w} ${h}`} aria-hidden="true">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
points={points}
|
||||
opacity="0.5"
|
||||
<div className={styles.statBarTrack} aria-hidden="true">
|
||||
<div
|
||||
className={styles.statBarFill}
|
||||
style={{ width: `${pct}%`, backgroundColor: color }}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -113,13 +85,6 @@ export const Dashboard: React.FC = () => {
|
||||
const storageMax = Math.max(Number(statsData.storage_bytes), 1024 * 1024);
|
||||
|
||||
const statColors = ['#6965db', '#339af0', '#40c057', '#fcc419', '#ff6b6b'];
|
||||
const sparkData = [
|
||||
[2, 4, 3, 8, 5, 9, statsData.drawings],
|
||||
[1, 2, 3, 3, 4, 5, statsData.projects + statsData.folders],
|
||||
[1, 1, 1, 1, 2, 2, statsData.teams],
|
||||
[5, 8, 12, 15, 20, 25, statsData.revisions],
|
||||
[1024, 2048, 4096, 8192, 16384, 32768, Number(statsData.storage_bytes)],
|
||||
];
|
||||
|
||||
const stats = [
|
||||
{ label: t('dashboard.stats.drawings'), value: statsData.drawings, chartValue: statsData.drawings, max: maxStat, icon: FileText, color: statColors[0] },
|
||||
@@ -172,18 +137,17 @@ export const Dashboard: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className={styles.statsGrid}>
|
||||
{stats.map((stat, idx) => (
|
||||
{stats.map((stat) => (
|
||||
<Card key={stat.label} className={styles.statCardWrapper}>
|
||||
<CardContent className={styles.statCard}>
|
||||
<div className={styles.statTop}>
|
||||
<div className={styles.statIcon} style={{ color: stat.color, borderColor: stat.color }}>
|
||||
<stat.icon size={22} />
|
||||
</div>
|
||||
<ProgressBar value={stat.chartValue} max={stat.max} color={stat.color} />
|
||||
</div>
|
||||
<div className={styles.statValue} style={{ color: stat.color }}>{stat.value}</div>
|
||||
<div className={styles.statLabel}>{stat.label}</div>
|
||||
<MiniSparkline data={sparkData[idx]} color={stat.color} />
|
||||
<StatBar value={stat.chartValue} max={stat.max} color={stat.color} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user