|
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'; |
4 | 2 | import { useOnline } from '../hooks/useOnline'; |
5 | | -import { lighten } from 'polished'; |
6 | 3 | import toast from 'react-hot-toast'; |
7 | 4 | import { StoreEvents, useStore } from '@tomic/react'; |
8 | 5 |
|
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(); |
11 | 12 | const store = useStore(); |
12 | | - const [connected, setConnected] = useState(store.serverConnected); |
13 | | - const wasEverConnected = useRef(store.serverConnected); |
| 13 | + const wasEverConnected = useRef(false); |
14 | 14 |
|
15 | 15 | 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 | + ); |
23 | 30 |
|
24 | 31 | return unsub; |
25 | 32 | }, [store]); |
26 | 33 |
|
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 | | - |
41 | 34 | useEffect(() => { |
42 | 35 | 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 | + }); |
44 | 40 | } |
45 | 41 | }, [isOnline]); |
46 | 42 |
|
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; |
65 | 44 | } |
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 | | -`; |
0 commit comments