Skip to content

Commit 8d39de7

Browse files
committed
refactor(react-components): make it SSR friendly
1 parent 5e60df5 commit 8d39de7

23 files changed

Lines changed: 550 additions & 101 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"prettier": "^2.8.7",
2222
"tsup": "^8.0.1",
2323
"turbo": "^1.8.6",
24+
"vite": "7.3.1",
2425
"vite-tsconfig-paths": "^6.1.1",
2526
"vitest": "^4.0.16"
2627
},

packages/extended-sdk/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@
2121
"build": "tsup --config ../tsup.config.ts",
2222
"prepack": "clean-package",
2323
"clean": "rm -rf dist .turbo node_modules",
24-
"postpack": "mv -fv package.json.backup package.json",
25-
"test": "vitest run --config ../../vitest.config.ts --passWithNoTests"
24+
"postpack": "mv -fv package.json.backup package.json"
2625
},
2726
"main": "src/index.ts",
2827
"devDependencies": {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined'
2+
3+
export const getStorageItem = (key: string) => {
4+
if (!isBrowser()) return null
5+
6+
try {
7+
return window.localStorage.getItem(key)
8+
} catch {
9+
return null
10+
}
11+
}
12+
13+
export const setStorageItem = (key: string, value: string) => {
14+
if (!isBrowser()) return
15+
16+
try {
17+
window.localStorage.setItem(key, value)
18+
} catch {}
19+
}
20+
21+
export const removeStorageItem = (key: string) => {
22+
if (!isBrowser()) return
23+
24+
try {
25+
window.localStorage.removeItem(key)
26+
} catch {}
27+
}
28+
29+
export const getLocationHash = () => {
30+
if (!isBrowser()) return ''
31+
32+
return window.location.hash ? window.location.hash.slice(1) : ''
33+
}
34+
35+
export const replaceLocationHash = (value: string) => {
36+
if (!isBrowser()) return
37+
38+
document.location.hash = value
39+
}
40+
41+
export const clearLocationHash = () => {
42+
if (!isBrowser()) return
43+
44+
window.history.pushState({}, '', document.location.pathname)
45+
}
46+
47+
export const hasEthereumProvider = () => {
48+
if (!isBrowser()) return false
49+
return 'ethereum' in window
50+
}
51+
52+
export const canUseWorkers = () => {
53+
return typeof Worker !== 'undefined' && typeof Blob !== 'undefined' && typeof URL !== 'undefined'
54+
}
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { PublishedElection } from '@vocdoni/sdk'
21
import { ComponentPropsWithoutRef } from 'react'
32
import { useComponents } from '~components/context/useComponents'
43
import { useElection } from '~providers'
4+
import { getElectionDescription } from '~providers/election/normalized'
55

66
export const ElectionDescription = (props: ComponentPropsWithoutRef<'div'> & Record<string, unknown>) => {
77
const { election } = useElection()
88
const { ElectionDescription: Slot } = useComponents()
9+
const description = getElectionDescription(election)
910

10-
if (!election || !(election instanceof PublishedElection) || !election.description?.default) {
11+
if (!election || !description) {
1112
return null
1213
}
1314

14-
return <Slot {...props} description={election.description.default} />
15+
return <Slot {...props} description={description} />
1516
}

packages/react-components/src/components/Election/Election.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { PublishedElection } from '@vocdoni/sdk'
21
import { useComponents } from '~components/context/useComponents'
32
import { ElectionProvider, ElectionProviderComponentProps, useElection } from '~providers'
3+
import { getElectionField, isPublishedElectionLike } from '~providers/election/normalized'
44
import { ElectionActions } from './Actions'
55
import { ElectionDescription } from './Description'
66
import { ElectionHeader } from './Header'
@@ -41,7 +41,7 @@ const ElectionBody = () => {
4141
<HR />
4242
<ElectionQuestions />
4343
<VoteButton />
44-
{election instanceof PublishedElection && election.get('census.type') === 'spreadsheet' && connected ? (
44+
{isPublishedElectionLike(election) && getElectionField(election, 'census.type') === 'spreadsheet' && connected ? (
4545
<SpreadsheetAccess />
4646
) : null}
4747
<ElectionResults />

packages/react-components/src/components/Election/ElectionTitle.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@ vi.mock('../../providers', () => ({
99

1010
vi.mock('@vocdoni/sdk', () => ({
1111
PublishedElection: class PublishedElection {},
12+
InvalidElection: class InvalidElection {},
1213
}))
1314

1415
describe('ElectionTitle', () => {
15-
it('renders election title', () => {
16+
it('renders election title from serialized election data', () => {
1617
render(
1718
<ComponentsProvider>
1819
<ElectionTitle />
1920
</ComponentsProvider>
2021
)
2122

22-
expect(screen.getByText('0x1')).toBeInTheDocument()
23+
expect(screen.getByText('My Election')).toBeInTheDocument()
2324
})
2425
})

packages/react-components/src/components/Election/Schedule.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { ElectionStatus, PublishedElection } from '@vocdoni/sdk'
1+
import { ElectionStatus } from '@vocdoni/sdk'
22
import { format as dformat, formatDistance } from 'date-fns'
33
import { ComponentPropsWithoutRef } from 'react'
44
import { useComponents } from '~components/context/useComponents'
55
import { useReactComponentsLocalize } from '~i18n/localize'
66
import { useElection } from '~providers'
7+
import { getElectionDate, getElectionStatus } from '~providers/election/normalized'
78

89
export type ElectionScheduleProps = ComponentPropsWithoutRef<'p'> &
910
Record<string, unknown> & {
@@ -21,13 +22,15 @@ export const ElectionSchedule = ({
2122
const { election } = useElection()
2223
const t = useReactComponentsLocalize()
2324
const { ElectionSchedule: Slot } = useComponents()
25+
const startDate = getElectionDate(election, 'startDate')
26+
const endDate = getElectionDate(election, 'endDate')
27+
const creationTime = getElectionDate(election, 'creationTime')
28+
const status = getElectionStatus(election)
2429

25-
if (!election || !(election instanceof PublishedElection)) return null
30+
if (!election || !startDate || !endDate || !status) return null
2631

2732
const getRemaining = (): string => {
28-
const endDate = election.endDate
29-
const startDate = election.startDate
30-
switch (election.status) {
33+
switch (status) {
3134
case ElectionStatus.ONGOING:
3235
case ElectionStatus.RESULTS:
3336
if (endDate < new Date()) {
@@ -56,15 +59,15 @@ export const ElectionSchedule = ({
5659
}
5760

5861
let text = t('schedule.from_begin_to_end', {
59-
begin: dformat(new Date(election.startDate), format),
60-
end: dformat(new Date(election.endDate), format),
62+
begin: dformat(new Date(startDate), format),
63+
end: dformat(new Date(endDate), format),
6164
})
6265

6366
if (showRemaining) {
6467
text = getRemaining()
65-
} else if (showCreatedAt) {
68+
} else if (showCreatedAt && creationTime) {
6669
text = t('schedule.created', {
67-
distance: formatDistance(election.creationTime, new Date(), { addSuffix: true }),
70+
distance: formatDistance(creationTime, new Date(), { addSuffix: true }),
6871
})
6972
}
7073

packages/react-components/src/components/Election/SpreadsheetAccess.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Wallet } from '@ethersproject/wallet'
2-
import { PublishedElection, VocdoniSDKClient } from '@vocdoni/sdk'
2+
import { VocdoniSDKClient } from '@vocdoni/sdk'
33
import { useEffect, useState } from 'react'
44
import { useForm } from 'react-hook-form'
55
import { useComponents } from '~components/context/useComponents'
66
import { useReactComponentsLocalize } from '~i18n/localize'
77
import { errorToString, useClient, useElection, walletFromRow } from '~providers'
8+
import { clearLocationHash, getLocationHash, replaceLocationHash } from '~providers/browser'
9+
import { getElectionField, isPublishedElectionLike } from '~providers/election/normalized'
810

911
type MetaSpecs = {
1012
[name: string]: {
@@ -24,6 +26,7 @@ export const SpreadsheetAccess = ({ hashPrivateKey, className }: SpreadsheetAcce
2426
const [open, setOpen] = useState(false)
2527
const [loading, setLoading] = useState(false)
2628
const [formError, setFormError] = useState<string>()
29+
const [privkey, setPrivkey] = useState('')
2730
const { SpreadsheetAccess: Slot } = useComponents()
2831
const localize = useReactComponentsLocalize()
2932
const { env } = useClient()
@@ -36,8 +39,11 @@ export const SpreadsheetAccess = ({ hashPrivateKey, className }: SpreadsheetAcce
3639
reset,
3740
} = useForm<any>()
3841

39-
const shouldRender = election instanceof PublishedElection && election.get('census.type') === 'spreadsheet'
40-
const privkey = window.location.hash ? window.location.hash.split('#')[1] : ''
42+
const shouldRender = isPublishedElectionLike(election) && getElectionField(election, 'census.type') === 'spreadsheet'
43+
44+
useEffect(() => {
45+
setPrivkey(getLocationHash())
46+
}, [])
4147

4248
useEffect(() => {
4349
;(async () => {
@@ -143,7 +149,7 @@ export const SpreadsheetAccess = ({ hashPrivateKey, className }: SpreadsheetAcce
143149

144150
setClient(client)
145151
if (hashPrivateKey) {
146-
document.location.hash = wallet.privateKey
152+
replaceLocationHash(wallet.privateKey)
147153
}
148154
reset()
149155
setOpen(false)
@@ -155,7 +161,7 @@ export const SpreadsheetAccess = ({ hashPrivateKey, className }: SpreadsheetAcce
155161
}
156162

157163
const logout = () => {
158-
window.history.pushState({}, '', document.location.pathname)
164+
clearLocationHash()
159165
clearClient()
160166
}
161167

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { ElectionStatus, InvalidElection, PublishedElection } from '@vocdoni/sdk'
1+
import { ElectionStatus } from '@vocdoni/sdk'
22
import { ComponentPropsWithoutRef } from 'react'
33
import { useComponents } from '~components/context/useComponents'
44
import { useReactComponentsLocalize } from '~i18n/localize'
55
import { useElection } from '~providers'
6+
import { getElectionStatus, isInvalidElectionLike } from '~providers/election/normalized'
67

78
export const ElectionStatusBadge = (props: ComponentPropsWithoutRef<'span'> & Record<string, unknown>) => {
89
const { election } = useElection()
@@ -12,25 +13,20 @@ export const ElectionStatusBadge = (props: ComponentPropsWithoutRef<'span'> & Re
1213
if (!election) return null
1314

1415
let tone: 'success' | 'warning' | 'danger' = 'success'
16+
const status = getElectionStatus(election)
1517

16-
if (
17-
election instanceof PublishedElection &&
18-
[ElectionStatus.PAUSED, ElectionStatus.ENDED].includes(election.status)
19-
) {
18+
if (status && ([ElectionStatus.PAUSED, ElectionStatus.ENDED] as string[]).includes(status)) {
2019
tone = 'warning'
2120
}
2221

2322
if (
24-
election instanceof InvalidElection ||
25-
[ElectionStatus.CANCELED, ElectionStatus.PROCESS_UNKNOWN].includes(election.status)
23+
isInvalidElectionLike(election) ||
24+
(status && ([ElectionStatus.CANCELED, ElectionStatus.PROCESS_UNKNOWN] as string[]).includes(status))
2625
) {
2726
tone = 'danger'
2827
}
2928

30-
const label =
31-
election instanceof PublishedElection && election.status
32-
? localize(`statuses.${election.status.toLowerCase()}`)
33-
: localize('statuses.invalid')
29+
const label = status ? localize(`statuses.${status.toLowerCase()}`) : localize('statuses.invalid')
3430

3531
return <Slot {...props} tone={tone} label={label} />
3632
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { PublishedElection } from '@vocdoni/sdk'
21
import { ComponentPropsWithoutRef } from 'react'
32
import { useComponents } from '~components/context/useComponents'
43
import { useElection } from '~providers'
4+
import { getElectionTitle } from '~providers/election/normalized'
55

66
export const ElectionTitle = (props: ComponentPropsWithoutRef<'h1'> & Record<string, unknown>) => {
77
const { election } = useElection()
88
const { ElectionTitle: Slot } = useComponents()
99

1010
if (!election) return null
1111

12-
const title = election instanceof PublishedElection ? election.title?.default : election.id
12+
const title = getElectionTitle(election)
1313
return <Slot {...props} title={title} />
1414
}

0 commit comments

Comments
 (0)