Gisle Aune
4 years ago
10 changed files with 534 additions and 37 deletions
-
297package-lock.json
-
4package.json
-
6src/components/layout/Layout.module.sass
-
1src/components/logs/LogFilterSelector.module.sass
-
144src/components/logs/LogFilterSelector.tsx
-
24src/components/logs/LogList.tsx
-
59src/hooks/LogFilterChoiceContext.tsx
-
2src/lib/rpdata/logs.ts
-
30src/pages/logs/index.tsx
-
4src/styles/global.css
@ -0,0 +1 @@ |
|||
.multiSelect |
@ -0,0 +1,144 @@ |
|||
import React, { useCallback, useContext, useEffect, useMemo } from "react"; |
|||
import chroma from "chroma-js"; |
|||
import Select, { createFilter, Styles } from "react-select"; |
|||
import LogListContext from "../../hooks/LogListContext"; |
|||
import LogFilterChoiceContext from "../../hooks/LogFilterChoiceContext"; |
|||
import { Config } from "react-select/src/filters"; |
|||
|
|||
import cssStyles from "./LogFilterSelector.module.sass"; |
|||
|
|||
export default function LogFilterSelector(props: {}) { |
|||
const {filter, updateFilter} = useContext(LogListContext); |
|||
const {channelNames, eventNames, characters} = useContext(LogFilterChoiceContext); |
|||
|
|||
const options = useMemo(() => [ |
|||
...channelNames.map(n => ({type: 0, color: "#77BB77", value: n, label: n})), |
|||
...eventNames.map(n => ({type: 1, color: "#BB7777", value: n, label: n})), |
|||
...characters.map(c => ({type: 2, color: "#3377BB", value: c.id, label: c.name})), |
|||
], [channelNames, eventNames, characters]) |
|||
|
|||
const value = useMemo(() => { |
|||
const value = []; |
|||
if (filter.characters && filter.characters.length > 0) { |
|||
value.push(...filter.characters.map(ch => options.find(c => c.type === 2 && c.value === ch))) |
|||
} |
|||
if (filter.events && filter.events.length > 0) { |
|||
value.push(...filter.events.map(v => options.find(c => c.type === 1 && c.value === v))) |
|||
} |
|||
if (filter.channels && filter.channels.length > 0) { |
|||
value.push(...filter.channels.map(v => options.find(c => c.type === 0 && c.value === v))) |
|||
} |
|||
|
|||
return value |
|||
}, [filter, options]) |
|||
|
|||
const setValue = useCallback((value: {type:number, value:string}[] ) => { |
|||
if (value == null) { |
|||
value = []; |
|||
} |
|||
|
|||
updateFilter({ |
|||
...filter, |
|||
channels: value.filter(v => v.type === 0).map(v => v.value), |
|||
events: value.filter(v => v.type === 1).map(v => v.value), |
|||
characters: value.filter(v => v.type === 2).map(v => v.value), |
|||
}) |
|||
}, [filter, updateFilter]); |
|||
|
|||
return ( |
|||
<Select |
|||
className={cssStyles.multiSelect} |
|||
closeMenuOnSelect={false} |
|||
styles={colourStyles} |
|||
filterOption={createFilter(filterConfig)} |
|||
defaultValue={[]} |
|||
isMulti={true} |
|||
options={options} |
|||
value={value} |
|||
onChange={setValue} |
|||
theme={defaultTheme => ({ |
|||
...defaultTheme, |
|||
colors: { |
|||
...defaultTheme.colors, |
|||
...customTheme, |
|||
}, |
|||
})} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
const customTheme = { |
|||
"primary": "#333333", |
|||
"primary75": "#333333", |
|||
"primary50": "#333333", |
|||
"primary25": "#333333", |
|||
"danger": "#000000", |
|||
"dangerLight": "#000000", |
|||
"neutral0": "#000000", |
|||
"neutral5": "#000000", |
|||
"neutral10": "#000000", |
|||
"neutral20": "#000000", |
|||
"neutral30": "#000000", |
|||
"neutral40": "#000000", |
|||
"neutral50": "#000000", |
|||
"neutral60": "#000000", |
|||
"neutral70": "#000000", |
|||
"neutral80": "#000000", |
|||
"neutral90": "#000000", |
|||
} |
|||
|
|||
const filterConfig: Config = { |
|||
ignoreCase: true, |
|||
ignoreAccents: true, |
|||
trim: true, |
|||
matchFrom: 'start', |
|||
}; |
|||
|
|||
const colourStyles: Styles = { |
|||
control: styles => ({ ...styles, backgroundColor: 'rgba(0, 0, 0, 0.25)' }), |
|||
option: (styles, { data, isDisabled, isFocused, isSelected }) => { |
|||
const color = chroma(data.color); |
|||
return { |
|||
...styles, |
|||
backgroundColor: isDisabled |
|||
? null |
|||
: isSelected |
|||
? data.color |
|||
: isFocused |
|||
? color.alpha(0.1).css() |
|||
: null, |
|||
color: isDisabled |
|||
? '#ccc' |
|||
: isSelected |
|||
? chroma.contrast(color, 'white') > 2 |
|||
? 'white' |
|||
: 'black' |
|||
: data.color, |
|||
cursor: isDisabled ? 'not-allowed' : 'default', |
|||
|
|||
':active': { |
|||
...styles[':active'], |
|||
backgroundColor: !isDisabled && (isSelected ? data.color : color.alpha(0.3).css()), |
|||
}, |
|||
}; |
|||
}, |
|||
multiValue: (styles, { data }) => { |
|||
const color = chroma(data.color); |
|||
return { |
|||
...styles, |
|||
backgroundColor: color.alpha(0.1).css(), |
|||
}; |
|||
}, |
|||
multiValueLabel: (styles, { data }) => ({ |
|||
...styles, |
|||
color: data.color, |
|||
}), |
|||
multiValueRemove: (styles, { data }) => ({ |
|||
...styles, |
|||
color: data.color, |
|||
':hover': { |
|||
backgroundColor: data.color, |
|||
color: 'white', |
|||
}, |
|||
}), |
|||
}; |
@ -0,0 +1,24 @@ |
|||
import React, { useContext, useEffect } from "react"; |
|||
|
|||
import LogListContext from "../../hooks/LogListContext"; |
|||
|
|||
export default function LogList() { |
|||
const {headers, filter, updateFilter} = useContext(LogListContext); |
|||
|
|||
useEffect(() => { |
|||
const timeout = setTimeout(() => { |
|||
updateFilter({...filter, limit: 10}); |
|||
}, 3000); |
|||
|
|||
return () => clearTimeout(timeout); |
|||
}, [updateFilter]); |
|||
|
|||
return ( |
|||
<> |
|||
<ol> |
|||
{headers.map(l => <li key={l.shortId}>{l.title || `${l.channelName} — ${l.date}`}</li>)} |
|||
</ol> |
|||
<pre>{JSON.stringify(filter, null, 4)}</pre> |
|||
</> |
|||
); |
|||
} |
@ -0,0 +1,59 @@ |
|||
import React, { useState, useCallback, useEffect, useContext } from "react"; |
|||
import { LogHeaderCharacter } from "../lib/rpdata/logs"; |
|||
import LogListContext from "./LogListContext"; |
|||
|
|||
export interface LogFilterChoiceContextData { |
|||
characters: LogHeaderCharacter[] |
|||
channelNames: string[] |
|||
eventNames: string[] |
|||
} |
|||
|
|||
const LogFilterChoiceContext: React.Context<LogFilterChoiceContextData> = React.createContext({ |
|||
characters: [], |
|||
channelNames: [], |
|||
eventNames: [], |
|||
}); |
|||
|
|||
interface LogFilterChoiceContextProviderProps { |
|||
children: JSX.Element | JSX.Element[] |
|||
} |
|||
|
|||
export function LogFilterChoiceContextProvider(props: LogFilterChoiceContextProviderProps) { |
|||
const {headers} = useContext(LogListContext); |
|||
const [value, setValue] = useState<LogFilterChoiceContextData>({ |
|||
characters: [], |
|||
channelNames: [], |
|||
eventNames: [], |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
const characters = new Map<string, LogHeaderCharacter>(); |
|||
const channelNames = new Set<string>(); |
|||
const eventNames = new Set<string>(); |
|||
|
|||
for (const header of headers) { |
|||
for (const character of header.characters) { |
|||
characters.set(character.id, character); |
|||
} |
|||
|
|||
channelNames.add(header.channelName); |
|||
if (header.eventName) { |
|||
eventNames.add(header.eventName); |
|||
} |
|||
} |
|||
|
|||
setValue(c => c.characters.length < characters.size ? { |
|||
channelNames: Array.from(channelNames.values()), |
|||
eventNames: Array.from(eventNames.values()), |
|||
characters: Array.from(characters.values()), |
|||
} : c); |
|||
}, [headers]) |
|||
|
|||
return ( |
|||
<LogFilterChoiceContext.Provider value={value}> |
|||
{props.children} |
|||
</LogFilterChoiceContext.Provider> |
|||
); |
|||
} |
|||
|
|||
export default LogFilterChoiceContext |
Write
Preview
Loading…
Cancel
Save
Reference in new issue