Skip to content

Commit 8703c1b

Browse files
committed
sync page improved
1 parent 631af31 commit 8703c1b

File tree

10 files changed

+559
-316
lines changed

10 files changed

+559
-316
lines changed
Lines changed: 27 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,44 @@
1-
import { useEffect, useRef, useState } from 'react';
2-
import { styled, keyframes, css } from 'styled-components';
3-
import { MdSignalWifiOff, MdCloudOff } from 'react-icons/md';
1+
import { useEffect, useRef } from 'react';
42
import { useOnline } from '../hooks/useOnline';
5-
import { lighten } from 'polished';
63
import toast from 'react-hot-toast';
74
import { StoreEvents, useStore } from '@tomic/react';
85

9-
/** Tracks WebSocket connection state and whether the server was ever reached. */
10-
function useServerConnection() {
6+
/**
7+
* No longer renders a visible element. Just shows friendly toasts
8+
* when connection state changes. The sync page handles detailed status.
9+
*/
10+
export function NetworkIndicator() {
11+
const isOnline = useOnline();
1112
const store = useStore();
12-
const [connected, setConnected] = useState(store.serverConnected);
13-
const wasEverConnected = useRef(store.serverConnected);
13+
const wasEverConnected = useRef(false);
1414

1515
useEffect(() => {
16-
const unsub = store.on(StoreEvents.ConnectionChanged, (isConnected: boolean) => {
17-
setConnected(isConnected);
18-
19-
if (isConnected) {
20-
wasEverConnected.current = true;
21-
}
22-
});
16+
const unsub = store.on(
17+
StoreEvents.ConnectionChanged,
18+
(connected: boolean) => {
19+
if (connected) {
20+
wasEverConnected.current = true;
21+
toast.success('Connected to server', { duration: 2000 });
22+
} else if (wasEverConnected.current) {
23+
toast('Working offline — your changes are saved locally', {
24+
icon: '\uD83D\uDCBE',
25+
duration: 4000,
26+
});
27+
}
28+
},
29+
);
2330

2431
return unsub;
2532
}, [store]);
2633

27-
return { connected, wasEverConnected: wasEverConnected.current };
28-
}
29-
30-
export function NetworkIndicator() {
31-
const isOnline = useOnline();
32-
const { connected: isWSConnected, wasEverConnected } = useServerConnection();
33-
const isConnected = isOnline && isWSConnected;
34-
35-
const label = !isOnline
36-
? 'No internet connection'
37-
: wasEverConnected
38-
? 'Server connection lost — reconnecting…'
39-
: 'Running in offline mode';
40-
4134
useEffect(() => {
4235
if (!isOnline) {
43-
toast.error('You are offline, changes might not be persisted.');
36+
toast('No internet — your changes are saved locally', {
37+
icon: '\uD83D\uDCBE',
38+
duration: 4000,
39+
});
4440
}
4541
}, [isOnline]);
4642

47-
useEffect(() => {
48-
if (!isWSConnected && wasEverConnected) {
49-
toast.error('Connection to server lost, reconnecting...');
50-
}
51-
}, [isWSConnected, wasEverConnected]);
52-
53-
const Icon = wasEverConnected ? MdSignalWifiOff : MdCloudOff;
54-
55-
return (
56-
<Wrapper shown={!isConnected} $neverConnected={!wasEverConnected} aria-hidden={isConnected} aria-label={label}>
57-
<Icon aria-hidden />
58-
<Label>{label}</Label>
59-
</Wrapper>
60-
);
61-
}
62-
63-
interface WrapperProps {
64-
shown: boolean;
43+
return null;
6544
}
66-
67-
const pulse = keyframes`
68-
0% {
69-
opacity: 1;
70-
filter: drop-shadow(0 0 5px var(--shadow-color));
71-
}
72-
100% {
73-
opacity: 0.8;
74-
filter: drop-shadow(0 0 0 var(--shadow-color));
75-
}
76-
`;
77-
78-
const Label = styled.span`
79-
font-size: 0.8rem;
80-
font-weight: 500;
81-
white-space: nowrap;
82-
max-width: 0;
83-
overflow: hidden;
84-
opacity: 0;
85-
transition:
86-
max-width 0.25s ease,
87-
opacity 0.2s ease,
88-
margin 0.25s ease;
89-
margin-left: 0;
90-
`;
91-
92-
interface WrapperAllProps extends WrapperProps {
93-
$neverConnected?: boolean;
94-
}
95-
96-
const Wrapper = styled.div<WrapperAllProps>`
97-
--shadow-color: ${p => lighten(0.15, p.$neverConnected ? p.theme.colors.textLight : p.theme.colors.alert)};
98-
position: fixed;
99-
bottom: 1.2rem;
100-
right: 2rem;
101-
z-index: ${({ theme }) => theme.zIndex.networkIndicator};
102-
font-size: 1.5rem;
103-
color: ${p => p.$neverConnected ? p.theme.colors.textLight : p.theme.colors.alert};
104-
pointer-events: ${p => (p.shown ? 'auto' : 'none')};
105-
transition:
106-
opacity 0.1s ease-in-out,
107-
border-radius 0.25s ease,
108-
padding 0.25s ease;
109-
opacity: ${p => (p.shown ? 1 : 0)};
110-
111-
background-color: ${p => p.theme.colors.bg};
112-
border: 1px solid ${p => p.$neverConnected ? p.theme.colors.bg2 : p.theme.colors.alert};
113-
border-radius: 2rem;
114-
display: flex;
115-
align-items: center;
116-
box-shadow: ${p => p.theme.boxShadowSoft};
117-
padding: 0.5rem;
118-
cursor: default;
119-
120-
svg {
121-
flex-shrink: 0;
122-
animation: ${p => p.$neverConnected ? 'none' : css`${pulse} 1.5s alternate ease-in-out infinite`};
123-
animation-play-state: ${p => (p.shown ? 'running' : 'paused')};
124-
}
125-
126-
&:hover ${Label}, &:focus-within ${Label} {
127-
max-width: 16rem;
128-
opacity: 1;
129-
margin-left: 0.5rem;
130-
}
131-
`;
Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { useEffect, useState, type JSX } from 'react';
22
import { StoreEvents, type StoreSyncStatus, useStore } from '@tomic/react';
3-
import { FaGlobe } from 'react-icons/fa6';
3+
import {
4+
FaWifi,
5+
FaArrowsRotate,
6+
FaCircleExclamation,
7+
} from 'react-icons/fa6';
8+
import { MdSignalWifiOff } from 'react-icons/md';
49
import { styled, keyframes } from 'styled-components';
510
import { paths } from '../../routes/paths';
611
import { SideBarMenuItem } from './SideBarMenuItem';
@@ -32,59 +37,55 @@ export function SyncMenuItem({
3237
};
3338
}, [store]);
3439

40+
const icon = getSyncIcon(status);
41+
const label = getSyncLabel(status);
42+
3543
return (
3644
<SideBarMenuItem
37-
icon={
38-
status.syncInProgress ? (
39-
<Spinner aria-hidden />
40-
) : status.serverConnected ? (
41-
<FaGlobe title='Connected to server over WebSocket' />
42-
) : (
43-
<OfflineIcon title='Offline / server connection unavailable'>
44-
<FaGlobe />
45-
</OfflineIcon>
46-
)
47-
}
45+
icon={icon}
4846
label='Sync'
49-
helper='Inspect sync and connection state'
47+
helper={label}
5048
path={paths.sync}
5149
onClick={onClick}
5250
/>
5351
);
5452
}
5553

56-
const spin = keyframes`
57-
from {
58-
transform: rotate(0deg);
54+
function getSyncIcon(status: StoreSyncStatus): JSX.Element {
55+
if (status.syncInProgress) {
56+
return <SpinningIcon aria-hidden><FaArrowsRotate /></SpinningIcon>;
5957
}
60-
to {
61-
transform: rotate(360deg);
58+
59+
if (!status.serverConnected) {
60+
return <MdSignalWifiOff title='Offline' />;
6261
}
63-
`;
6462

65-
const Spinner = styled.span`
66-
width: 0.9rem;
67-
height: 0.9rem;
68-
border: 2px solid currentColor;
69-
border-right-color: transparent;
70-
border-radius: 50%;
71-
animation: ${spin} 0.8s linear infinite;
63+
if (status.pendingDirtyCount > 0) {
64+
return <WarningIcon><FaCircleExclamation title='Changes pending' /></WarningIcon>;
65+
}
66+
67+
return <FaWifi title='Connected' />;
68+
}
69+
70+
function getSyncLabel(status: StoreSyncStatus): string {
71+
if (status.syncInProgress) return 'Syncing...';
72+
if (!status.serverConnected) return 'Offline';
73+
if (status.pendingDirtyCount > 0) return `${status.pendingDirtyCount} changes pending`;
74+
75+
return 'Connected';
76+
}
77+
78+
const spin = keyframes`
79+
from { transform: rotate(0deg); }
80+
to { transform: rotate(360deg); }
7281
`;
7382

74-
const OfflineIcon = styled.span`
75-
position: relative;
83+
const SpinningIcon = styled.span`
7684
display: inline-flex;
85+
animation: ${spin} 1s linear infinite;
86+
`;
7787

78-
&::after {
79-
content: '';
80-
position: absolute;
81-
left: 50%;
82-
top: -2px;
83-
width: 2px;
84-
height: calc(100% + 4px);
85-
background: currentColor;
86-
transform: translateX(-50%) rotate(35deg);
87-
transform-origin: center;
88-
border-radius: 999px;
89-
}
88+
const WarningIcon = styled.span`
89+
color: ${p => p.theme.colors.warning};
90+
display: inline-flex;
9091
`;

0 commit comments

Comments
 (0)