Skip to content

Commit

Permalink
feat: landing & config page UX improvements (#235)
Browse files Browse the repository at this point in the history
* feat: add explainer with links to helper-ui

* feat: allow setting dnsResolvers in config. show defaults. require urls

* fix: remove second param from root.render

* fix: e2e tests and redirect page load-content btn

* fix: reset config values without page reload

* refactor: colors and about section

- moved About to the bottom and made it more friendly
- adjusted colors to improve contrast
- header to match other web utiities we have

* chore: remove unnecessary code

* test: fix header text comparison

---------

Co-authored-by: Marcin Rataj <[email protected]>
  • Loading branch information
SgtPooki and lidel authored May 14, 2024
1 parent 1ffdd3a commit fb9b04e
Show file tree
Hide file tree
Showing 20 changed files with 183 additions and 61 deletions.
2 changes: 1 addition & 1 deletion src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'

export default ({ handleSubmit, requestPath, setRequestPath }): JSX.Element => (
<form id='add-file' onSubmit={handleSubmit}>
<label htmlFor='inputContent' className='f5 ma0 pb2 aqua fw4 db'>CID, Content Path, or URL</label>
<label htmlFor='inputContent' className='f5 ma0 pb2 teal fw4 db'>CID, Content Path, or URL</label>
<input
className='input-reset bn black-80 bg-white pa3 w-100 mb3'
id='inputContent'
Expand Down
32 changes: 18 additions & 14 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@ import ipfsLogo from '../ipfs-logo.svg'
export default function Header (): JSX.Element {
const { gotoPage } = React.useContext(RouteContext)
return (
<header className='e2e-header flex items-center pa3 bg-navy bb bw3 b--aqua justify-between'>
<a href='https://ipfs.io' title='home'>
<img alt='IPFS logo' src={ipfsLogo} style={{ height: 50 }} className='v-top' />
</a>
<span className='e2e-header-title white f3'>IPFS Service Worker Gateway</span>
<button className='e2e-header-config-button'
onClick={() => {
gotoPage('/ipfs-sw-config')
}}
style={{ border: 'none', background: 'none', cursor: 'pointer' }}
>
{/* https://isotropic.co/tool/hex-color-to-css-filter/ to #ffffff */}
<img alt='Config gear icon' src={gearIcon} style={{ height: 50, filter: 'invert(100%) sepia(100%) saturate(0%) hue-rotate(275deg) brightness(103%) contrast(103%)' }} className='v-top' />
</button>
<header className='e2e-header flex items-center pa3 bg-navy bb bw3 b--aqua tc justify-between'>
<div>
<a href='https://ipfs.tech' title='IPFS Project' target="_blank" rel="noopener noreferrer" aria-label="Open IPFS Project's website">
<img alt='IPFS logo' src={ipfsLogo} style={{ height: 50 }} className='v-top' />
</a>
</div>
<div className='pb1 ma0 inline-flex items-center'>
<h1 className='e2e-header-title f3 fw2 montserrat aqua ttu'>Service Worker Gateway <small className="gray">(beta)</small></h1>
<button className='e2e-header-config-button pl3'
onClick={() => {
gotoPage('/ipfs-sw-config')
}}
style={{ border: 'none', background: 'none', cursor: 'pointer' }}
>
<img alt='Config gear icon' src={gearIcon} style={{ height: 50 }} className='v-top' />
</button>
</div>

</header>
)
}
2 changes: 1 addition & 1 deletion src/components/collapsible.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function Collapsible ({ children, collapsedLabel, expandedLabel, collapse
<React.Fragment>
<input type="checkbox" className="dn" name="collapsible" id={`collapsible-${cId}`} onClick={() => { setCollapsed(!isCollapsed) }} />
<label htmlFor={`collapsible-${cId}`} className="e2e-collapsible-button collapsible__item-label db pv3 link black hover-blue pointer blue">{isCollapsed ? collapsedLabel : expandedLabel}</label>
<div className={`bb b--black-20 ${isCollapsed ? 'dn' : ''}`}>
<div className={`${isCollapsed ? 'dn' : ''}`}>
{children}
</div>
</React.Fragment>
Expand Down
12 changes: 6 additions & 6 deletions src/components/input-validator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ function FormatHelp (): React.JSX.Element {
<tbody>
<tr>
<td>UNIX-like Content Path</td>
<td><pre className="di">/ipfs/cid/..</pre></td>
<td><pre className="di pl3">/ipfs/cid/..</pre></td>
</tr>
<tr>
<td>HTTP Gateway URL</td>
<td><pre className="di">https://ipfs.io/ipfs/cid..</pre></td>
<td><pre className="di pl3">https://ipfs.io/ipfs/cid..</pre></td>
</tr>
<tr>
<td>Native IPFS URL</td>
<td><pre className="di">ipfs://cid/..</pre></td>
<td><pre className="di pl3">ipfs://cid/..</pre></td>
</tr>
</tbody>
</table>
Expand All @@ -30,11 +30,11 @@ function FormatHelp (): React.JSX.Element {
function ValidationMessage ({ cidOrPeerIdOrDnslink, requestPath, protocol, children }): React.JSX.Element {
let errorElement: React.JSX.Element | null = null
if (requestPath == null || requestPath === '') {
errorElement = <span>Enter a valid IPFS/IPNS path.</span>
errorElement = <span><big className="f3"></big> Enter a valid IPFS/IPNS content path.</span>
} else if (protocol !== 'ipfs' && protocol !== 'ipns') {
errorElement = <FormatHelp />
} else if (cidOrPeerIdOrDnslink == null || cidOrPeerIdOrDnslink === '') {
const contentType = protocol === 'ipfs' ? 'CID' : 'PeerId or DnsLink'
const contentType = protocol === 'ipfs' ? 'CID' : 'PeerID or DNSLink'
errorElement = <span>Content identifier missing. Add a {contentType} to your path</span>
} else if (protocol === 'ipfs') {
try {
Expand Down Expand Up @@ -81,7 +81,7 @@ export default function InputValidator ({ requestPath }: { requestPath: string }
<div>
<ValidationMessage protocol={protocol} cidOrPeerIdOrDnslink={cidOrPeerIdOrDnslink} requestPath={requestPath}>
<a className="db" href={swPath} target="_blank">
<button id="load-directly" className='button-reset pv3 tc bn bg-animate bg-black-80 hover-bg-aqua white pointer w-100'>Load content</button>
<button id="load-directly" className='button-reset pv3 tc bn bg-animate bg-teal-muted hover-bg-navy-muted white pointer f4 w-100'>Load content</button>
</a>
</ValidationMessage>
</div>
Expand Down
7 changes: 6 additions & 1 deletion src/components/local-storage-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface LocalStorageInputProps extends React.DetailedHTMLProps<React.HT
placeholder?: string
defaultValue: string
validationFn?(value: string): Error | null
resetKey: number
}

const defaultValidationFunction = (value: string): Error | null => {
Expand All @@ -16,10 +17,14 @@ const defaultValidationFunction = (value: string): Error | null => {
return err as Error
}
}
export default ({ localStorageKey, label, placeholder, validationFn, defaultValue, ...props }: LocalStorageInputProps): JSX.Element => {
export default ({ resetKey, localStorageKey, label, placeholder, validationFn, defaultValue, ...props }: LocalStorageInputProps): JSX.Element => {
const [value, setValue] = useState(localStorage.getItem(localStorageKey) ?? defaultValue)
const [error, setError] = useState<null | Error>(null)

useEffect(() => {
setValue(localStorage.getItem(localStorageKey) ?? defaultValue)
}, [resetKey])

if (validationFn == null) {
validationFn = defaultValidationFunction
}
Expand Down
9 changes: 7 additions & 2 deletions src/components/local-storage-toggle.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
/**
Inspiration from https://dev.to/codebubb/create-a-simple-on-off-slide-toggle-with-css-db8
*/
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import './local-storage-toggle.css'

interface LocalStorageToggleProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
localStorageKey: string
offLabel: string
onLabel: string
resetKey: number
}

export const LocalStorageToggle: React.FC<LocalStorageToggleProps> = ({ localStorageKey, onLabel = 'Off', offLabel = 'On', ...props }) => {
export const LocalStorageToggle: React.FC<LocalStorageToggleProps> = ({ resetKey, localStorageKey, onLabel = 'Off', offLabel = 'On', ...props }) => {
const [isChecked, setIsChecked] = useState(() => {
const savedValue = localStorage.getItem(localStorageKey)
return savedValue === 'true'
})

useEffect(() => {
setIsChecked(localStorage.getItem(localStorageKey) === 'true')
}, [resetKey])

const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const newValue = event.target.checked
setIsChecked(newValue)
Expand Down
6 changes: 3 additions & 3 deletions src/components/sw-ready-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ interface ServiceWorkerReadyButtonProps extends ButtonProps {
export const ServiceWorkerReadyButton = ({ className, label, waitingLabel, ...props }: ServiceWorkerReadyButtonProps): JSX.Element => {
const { isServiceWorkerRegistered } = useContext(ServiceWorkerContext)

const buttonClasses = new Set(['button-reset', 'pv3', 'tc', 'bn', 'white', 'w-100', 'cursor-disabled', 'bg-gray'])
const buttonClasses = new Set(['button-reset', 'pv3', 'tc', 'bn', 'white', 'cursor-disabled', 'bg-gray'])
if (isServiceWorkerRegistered) {
buttonClasses.delete('bg-gray')
buttonClasses.delete('cursor-disabled')
buttonClasses.add('bg-animate')
buttonClasses.add('bg-black-80')
buttonClasses.add('hover-bg-aqua')
buttonClasses.add('bg-teal-muted')
buttonClasses.add('hover-bg-navy-muted')
buttonClasses.add('pointer')
}

Expand Down
2 changes: 1 addition & 1 deletion src/gear-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,5 @@ root.render(
<RouterProvider routes={routes}>
<App />
</RouterProvider>
</React.StrictMode>,
container
</React.StrictMode>
)
39 changes: 35 additions & 4 deletions src/lib/config-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,57 @@ import type { ComponentLogger } from '@libp2p/logger'
export interface ConfigDb extends BaseDbConfig {
gateways: string[]
routers: string[]
dnsJsonResolvers: Record<string, string>
autoReload: boolean
debug: string
}

export const defaultGateways = ['https://trustless-gateway.link']
export const defaultRouters = ['https://delegated-ipfs.dev']
export const defaultDnsJsonResolvers = {
'.': 'https://delegated-ipfs.dev/dns-query'
}

const configDb = new GenericIDB<ConfigDb>('helia-sw', 'config')

export async function loadConfigFromLocalStorage (): Promise<void> {
if (typeof globalThis.localStorage !== 'undefined') {
await configDb.open()
const localStorage = globalThis.localStorage
const localStorageGatewaysString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.gateways) ?? '["https://trustless-gateway.link"]'
const localStorageRoutersString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.routers) ?? '["https://delegated-ipfs.dev"]'
const localStorageGatewaysString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.gateways) ?? JSON.stringify(defaultGateways)
const localStorageRoutersString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.routers) ?? JSON.stringify(defaultRouters)
const localStorageDnsResolvers = localStorage.getItem(LOCAL_STORAGE_KEYS.config.dnsJsonResolvers) ?? JSON.stringify(defaultDnsJsonResolvers)
const autoReload = localStorage.getItem(LOCAL_STORAGE_KEYS.config.autoReload) === 'true'
const debug = localStorage.getItem(LOCAL_STORAGE_KEYS.config.debug) ?? ''
const gateways = JSON.parse(localStorageGatewaysString)
const routers = JSON.parse(localStorageRoutersString)
const dnsJsonResolvers = JSON.parse(localStorageDnsResolvers)
debugLib.enable(debug)

await configDb.put('gateways', gateways)
await configDb.put('routers', routers)
await configDb.put('dnsJsonResolvers', dnsJsonResolvers)
await configDb.put('autoReload', autoReload)
await configDb.put('debug', debug)
configDb.close()
}
}

export async function resetConfig (): Promise<void> {
await configDb.open()
localStorage.removeItem(LOCAL_STORAGE_KEYS.config.gateways)
await configDb.put('gateways', defaultGateways)
localStorage.removeItem(LOCAL_STORAGE_KEYS.config.routers)
await configDb.put('routers', defaultRouters)
localStorage.removeItem(LOCAL_STORAGE_KEYS.config.dnsJsonResolvers)
await configDb.put('dnsJsonResolvers', defaultDnsJsonResolvers)
localStorage.removeItem(LOCAL_STORAGE_KEYS.config.autoReload)
await configDb.put('autoReload', false)
localStorage.removeItem(LOCAL_STORAGE_KEYS.config.debug)
await configDb.put('debug', '')
configDb.close()
}

export async function setConfig (config: ConfigDb, logger: ComponentLogger): Promise<void> {
const log = logger.forComponent('set-config')
debugLib.enable(config.debug ?? '') // set debug level first.
Expand All @@ -40,17 +65,17 @@ export async function setConfig (config: ConfigDb, logger: ComponentLogger): Pro
await configDb.open()
await configDb.put('gateways', config.gateways)
await configDb.put('routers', config.routers)
await configDb.put('dnsJsonResolvers', config.dnsJsonResolvers)
await configDb.put('autoReload', config.autoReload)
await configDb.put('debug', config.debug ?? '')
configDb.close()
}

const defaultGateways = ['https://trustless-gateway.link']
const defaultRouters = ['https://delegated-ipfs.dev']
export async function getConfig (logger: ComponentLogger): Promise<ConfigDb> {
const log = logger.forComponent('get-config')
let gateways: string[] = defaultGateways
let routers: string[] = defaultRouters
let dnsJsonResolvers: Record<string, string> = defaultDnsJsonResolvers
let autoReload = false
let debug = ''

Expand All @@ -61,6 +86,8 @@ export async function getConfig (logger: ComponentLogger): Promise<ConfigDb> {

routers = await configDb.get('routers')

dnsJsonResolvers = await configDb.get('dnsJsonResolvers')

autoReload = await configDb.get('autoReload') ?? false
debug = await configDb.get('debug') ?? ''
configDb.close()
Expand All @@ -76,11 +103,15 @@ export async function getConfig (logger: ComponentLogger): Promise<ConfigDb> {
if (routers == null || routers.length === 0) {
routers = [...defaultRouters]
}
if (dnsJsonResolvers == null || Object.keys(dnsJsonResolvers).length === 0) {
dnsJsonResolvers = { ...defaultDnsJsonResolvers }
}

// always return the config, even if we failed to load it.
return {
gateways,
routers,
dnsJsonResolvers,
autoReload,
debug
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const LOCAL_STORAGE_KEYS = {
gateways: getLocalStorageKey('config', 'gateways'),
routers: getLocalStorageKey('config', 'routers'),
autoReload: getLocalStorageKey('config', 'autoReload'),
dnsJsonResolvers: getLocalStorageKey('config', 'dnsJsonResolvers'),
debug: getLocalStorageKey('config', 'debug')
},
forms: {
Expand Down
Loading

0 comments on commit fb9b04e

Please sign in to comment.