react-icons/fa#FaEye TypeScript Examples
The following examples show how to use
react-icons/fa#FaEye.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: AnnotationList.tsx From slim with Apache License 2.0 | 6 votes |
render (): React.ReactNode {
const items = this.props.rois.map((roi, index) => (
<AnnotationItem
key={roi.uid}
roi={roi}
index={index}
isVisible={this.props.visibleRoiUIDs.includes(roi.uid)}
onVisibilityChange={this.props.onVisibilityChange}
/>
))
return (
<>
<div style={{ paddingLeft: '14px', paddingTop: '7px', paddingBottom: '7px' }}>
<Switch
size='small'
onChange={this.handleVisibilityChange}
checked={this.props.visibleRoiUIDs.length > 0}
checkedChildren={<FaEye />}
unCheckedChildren={<FaEyeSlash />}
/>
</div>
<Menu
selectedKeys={this.props.selectedRoiUIDs}
onSelect={this.handleMenuItemSelection}
onClick={this.handleMenuItemSelection}
>
{items}
</Menu>
</>
)
}
Example #2
Source File: Icons.tsx From crosshare with GNU Affero General Public License v3.0 | 6 votes |
CheckOrReveal = ({
x,
y,
reveal,
}: {
x: number;
y: number;
reveal: boolean;
}) => {
if (reveal) {
return (
<FaEye x={x} y={y} size={32} fill="currentColor" stroke="currentColor" />
);
}
return (
<FaCheck x={x} y={y} size={32} fill="currentColor" stroke="currentColor" />
);
}
Example #3
Source File: VideoTeaser.tsx From 3Speak-app with GNU General Public License v3.0 | 5 votes |
export function VideoTeaser(props: any) {
const [video_info, setVideoInfo] = useState<any>({})
const [thumbnail, setThumbnail] = useState('')
const reflink = useMemo(() => {
return Reflink.parse(props.reflink)
}, [])
useEffect(() => {
const load = async () => {
setVideoInfo(await AccountService.permalinkToVideoInfo(props.reflink))
setThumbnail(await VideoService.getThumbnailURL(props.reflink))
}
void load()
}, [])
return (
<div className="video-card-list">
<div className="teaser_holder video-card-image">
<div className="card-label">
{(() => {
const pattern = DateTime.compile('mm:ss')
return DateTime.format(new Date(video_info.meta.duration * 1000), pattern)
})()}
</div>
<a href={`#/watch/${props.reflink}`}>
<img className="img-fluid bg-dark" src={thumbnail} alt="" />
</a>
</div>
<span className="video-card-body">
<div className="video-title">
<a
href={`#/watch/${props.reflink}`}
style={{ textOverflow: 'ellipsis', overflow: 'nowrap' }}
>
{video_info.title}
</a>
</div>
<div className="video-page">
<a href={`#/user/${reflink.source.value}:${reflink.root}`}>{reflink.root}</a>
</div>
<div className="video-view">
<FaEye /> Unknown views
<span>
<FaCalendarAlt />
{(() => {
if (video_info.creation) {
const dateBest = convert(
(new Date(new Date().toUTCString()) as any) / 1 -
Date.parse(video_info.creation) / 1,
)
.from('ms')
.toBest()
if (Math.round(dateBest.val) >= 2) {
return `${Math.round(dateBest.val)} ${dateBest.plural} ago`
} else {
return `${Math.round(dateBest.val)} ${dateBest.singular} ago`
}
}
})()}
</span>
</div>
</span>
</div>
)
}
Example #4
Source File: IPMenu.tsx From iplocate with MIT License | 5 votes |
IPMenu: React.FC<Props> = (props) => {
const { ips, onSetCurrentIp, onToggleIpVisible, onRemoveIp } = props;
if (ips.length === 0) return null;
return (
<Wrapper>
{ips.map((ip) => (
<Row key={ip.traits.ipAddress}>
<RowText onClick={() => onSetCurrentIp(ip)}>
{ip.traits.ipAddress}
</RowText>
<div>
<RowAction
onClick={() => onRemoveIp(ip)}
aria-label="remove ip address"
>
<FaTrash />
</RowAction>
{ip.hidden ? (
<RowAction
onClick={() => onToggleIpVisible(ip)}
aria-label="toggle ip visibility on"
>
<FaEyeSlash />
</RowAction>
) : (
<RowAction
onClick={() => onToggleIpVisible(ip)}
aria-label="toggle ip visibility off"
>
<FaEye />
</RowAction>
)}
</div>
</Row>
))}
</Wrapper>
);
}
Example #5
Source File: notebook-post.tsx From portfolio with MIT License | 4 votes |
NotebookPost: React.SFC<PostProps> = () => {
const textColor = useColorModeValue("gray.500", "gray.200");
const post = articles[4];
return (
<>
<VStack mt={0} mb={6} spacing={1} align="start">
<Heading as="h1" fontSize="3xl" lineHeight="shorter" fontWeight="bold">
{post.title}
</Heading>
<Divider
orientation="horizontal"
opacity={1}
borderBottomWidth={0}
height={"1px"}
bg={"gray.200"}
/>
</VStack>
<Flex
justifyContent={"space-between"}
flexDirection={["column", "row", "row"]}
>
<HStack spacing={2} isInline>
<Text fontSize="sm" fontWeight="400" color={textColor}>
{post.published}
</Text>
<Text fontSize="sm" fontWeight="400" color={textColor}>
•
</Text>
<Tooltip hasArrow label="Views" placement="top">
<Flex alignItems="center">
<Text
fontSize="sm"
noOfLines={1}
fontWeight="400"
align="left"
color={textColor}
>
{post.views}
</Text>
<Icon as={FaEye} ml={1} color={textColor} />
</Flex>
</Tooltip>
<Text fontSize="sm" fontWeight="600" color={textColor}>
•
</Text>
<Tooltip hasArrow label="Read time" placement="top">
<Text
fontSize="sm"
noOfLines={1}
fontWeight="400"
align="left"
color={textColor}
>
{post.readTime}
</Text>
</Tooltip>
</HStack>
<HStack spacing={1} alignItems="center">
{post.tags.map(tag => (
<Tag
size="sm"
padding="0 3px"
key={tag}
colorScheme={getTagColor(tag)}
>
{tag}
</Tag>
))}
</HStack>
</Flex>
<HStack align="end" mt={5}>
<Link href={post.live} isExternal>
<Button
ml={2}
variant="outline"
size={["sm"]}
color={useColorModeValue("green.600", "green.200")}
bg={useColorModeValue("white", "gray.800")}
leftIcon={<BiLinkExternal size={18} />}
>
Demo
</Button>
</Link>
<Link href={post.github_url} isExternal>
<Button
ml={2}
variant="outline"
size={["sm"]}
color={useColorModeValue("green.600", "green.200")}
bg={useColorModeValue("white", "gray.800")}
leftIcon={<FiGithub size={18} />}
>
Github link
</Button>
</Link>
</HStack>
<Box height={["35vh", "45vh", "55vh", "70vh"]} marginTop={5}>
<Carousel images={post.images} />
</Box>
<VStack spacing={5} align={"start"} mt={6}>
<Header fontSize={"xl"} mt={0} mb={0}>
What will you learn?
</Header>
<Box fontSize="md">
<UnorderedList textAlign="left" paddingLeft={5} m={0}>
<ListItem>How to create a CRUD app with react</ListItem>
<ListItem>How to create a responsive app using ChakraUi</ListItem>
<ListItem>How to use animations with framer-motion</ListItem>
<ListItem>How to create slider with framer-motion</ListItem>
</UnorderedList>
</Box>
</VStack>
<VStack spacing={5} align={"start"} mt={6}>
<Header fontSize={"xl"} mt={0} mb={0}>
Built with
</Header>
<Box fontSize="md">
<UnorderedList textAlign="left" paddingLeft={5} m={0}>
<ListItem>
Programming language -{" "}
<Link
href="https://www.typescriptlang.org/"
isExternal
color={"blue.500"}
>
Typescript
</Link>
</ListItem>
<ListItem>
Front-end library -{" "}
<Link
href="https://github.com/facebook/react/"
isExternal
color={"blue.500"}
>
React
</Link>
</ListItem>
<ListItem>
UI components -{" "}
<Link href="https://chakra-ui.com/" isExternal color={"blue.500"}>
Chakra-ui
</Link>
</ListItem>
<ListItem>
Animation library -{" "}
<Link
href="https://www.framer.com/motion/"
isExternal
color={"blue.500"}
>
Framer motion
</Link>
</ListItem>
<ListItem>
Notes display -{" "}
<Link
href="https://github.com/tsuyoshiwada/react-stack-grid"
isExternal
color={"blue.500"}
>
react-stack-grid
</Link>
</ListItem>
<ListItem>
Forms Validation -{" "}
<Link
href="https://react-hook-form.com/"
isExternal
color={"blue.500"}
>
React hook form
</Link>
</ListItem>
<ListItem>
Icons -{" "}
<Link
href="https://react-icons.github.io/react-icons/"
isExternal
color={"blue.500"}
>
React icons
</Link>
</ListItem>
<ListItem>
Images placeholder -{" "}
<Link href="https://blurha.sh/" isExternal color={"blue.500"}>
blurhash
</Link>
</ListItem>
<ListItem>
Progressive image loading -{" "}
<Link
href="https://github.com/FormidableLabs/react-progressive-image"
isExternal
color={"blue.500"}
>
react-progressive-image
</Link>
</ListItem>
</UnorderedList>
</Box>
</VStack>
</>
);
}
Example #6
Source File: post-card.tsx From portfolio with MIT License | 4 votes |
PostCard: React.SFC<PostCardProps> = ({ article }) => {
const textColor = useColorModeValue("gray.500", "gray.200");
const devIcon = useColorModeValue(dev, dev2);
return (
<CardTransition>
<VStack
spacing={1}
p={4}
isExternal
_hover={{ shadow: "md", textDecoration: "none" }}
borderWidth="1px"
position="relative"
rounded="md"
bg={useColorModeValue("white", "gray.800")}
align="left"
>
{article.external ? (
<Tooltip hasArrow label="Dev.to" placement="top">
<Image
src={devIcon}
width="2rem"
height="2rem"
position="absolute"
color="#cbd5e0"
right="0.5rem"
top="-14px"
/>
</Tooltip>
) : (
<Tooltip hasArrow label="mahmad.me" placement="top">
<Box position="absolute" color="#cbd5e0" right="0.5rem" top="-14px">
<Badge ml="1" variant="solid" colorScheme="blackAlpha">
Website
</Badge>
</Box>
</Tooltip>
)}
<Heading fontSize="lg" align="left" mt={0}>
{article.external ? (
<Text as={Link} href={article.link} target="_blank">
{article.title}
</Text>
) : (
<Link as={NavLink} to={article.link}>
{article.title}
</Link>
)}
{article.isNew && (
<Badge
ml="1"
mb="1"
colorScheme="green"
fontSize="0.7em"
lineHeight={1.5}
>
New
</Badge>
)}
</Heading>
<HStack spacing={2} isInline>
<Tooltip hasArrow label="Published" placement="top">
<Text fontSize="sm" fontWeight="400" color={textColor}>
{article.published}
</Text>
</Tooltip>
<Text fontSize="sm" fontWeight="400" color={textColor}>
•
</Text>
<Tooltip hasArrow label="Views" placement="top">
<Flex alignItems="center">
<Text
fontSize="sm"
noOfLines={1}
fontWeight="400"
align="left"
color={textColor}
>
{article.views}
</Text>
<Icon as={FaEye} ml={1} color={textColor} />
</Flex>
</Tooltip>
<Text fontSize="sm" fontWeight="600" color={textColor}>
•
</Text>
<Tooltip hasArrow label="Read time" placement="top">
<Text
fontSize="sm"
noOfLines={1}
fontWeight="400"
align="left"
color={textColor}
>
{article.readTime}
</Text>
</Tooltip>
<HStack spacing={1} alignItems="center" d={["none", "none", "flex"]}>
{article.tags.map(tag => (
<Tag
size="sm"
padding="0 3px"
key={tag}
colorScheme={getTagColor(tag)}
>
{tag}
</Tag>
))}
</HStack>
</HStack>
<HStack spacing={1} alignItems="center" d={["flex", "flex", "none"]}>
{article.tags.map(tag => (
<Tag
size="sm"
padding="0 3px"
key={tag}
colorScheme={getTagColor(tag)}
>
{tag}
</Tag>
))}
</HStack>
<Text align="left" fontSize="md" noOfLines={4} color={textColor}>
{article.desc}
</Text>
</VStack>
</CardTransition>
);
}
Example #7
Source File: InputField.tsx From hub with Apache License 2.0 | 4 votes |
InputField = forwardRef((props: Props, ref: Ref<RefInputField>) => {
const input = useRef<HTMLInputElement>(null);
const [isValid, setIsValid] = useState<boolean | null>(null);
const [inputValue, setInputValue] = useState(props.value || '');
const [invalidText, setInvalidText] = useState(!isUndefined(props.invalidText) ? props.invalidText.default : '');
const [isCheckingAvailability, setIsCheckingAvailability] = useState(false);
const [isCheckingPwdStrength, setIsCheckingPwdStrength] = useState(false);
const [pwdStrengthError, setPwdStrengthError] = useState<string | null>(null);
const [activeType, setActiveType] = useState<string>(props.type);
const [validateTimeout, setValidateTimeout] = useState<NodeJS.Timeout | null>(null);
useImperativeHandle(ref, () => ({
checkIsValid(): Promise<boolean> {
return isValidField();
},
reset: () => {
setInputValue('');
},
getValue(): string {
return inputValue;
},
checkValidity(): boolean {
return input.current ? input.current!.checkValidity() : true;
},
updateValue(newValue: string): void {
setInputValue(newValue);
},
}));
const checkValidity = (): boolean => {
let isInputValid = true;
if (input.current) {
isInputValid = input.current!.checkValidity();
if (!isInputValid && !isUndefined(props.invalidText)) {
let errorTxt = props.invalidText.default;
const validityState: ValidityState | undefined = input.current.validity;
if (!isUndefined(validityState)) {
if (validityState.typeMismatch && !isUndefined(props.invalidText.typeMismatch)) {
errorTxt = props.invalidText.typeMismatch;
} else if (validityState.tooShort && !isUndefined(props.invalidText.tooShort)) {
errorTxt = props.invalidText.tooShort;
} else if (validityState.patternMismatch && !isUndefined(props.invalidText.patternMismatch)) {
errorTxt = props.invalidText.patternMismatch;
} else if (validityState.typeMismatch && !isUndefined(props.invalidText.typeMismatch)) {
errorTxt = props.invalidText.typeMismatch;
} else if (validityState.rangeUnderflow && !isUndefined(props.invalidText.rangeUnderflow)) {
errorTxt = props.invalidText.rangeUnderflow;
} else if (validityState.rangeOverflow && !isUndefined(props.invalidText.rangeOverflow)) {
errorTxt = props.invalidText.rangeOverflow;
} else if (validityState.customError && !isUndefined(props.invalidText.customError)) {
if (!isUndefined(props.excludedValues) && props.excludedValues.includes(input.current.value)) {
errorTxt = props.invalidText.excluded;
} else {
errorTxt = props.invalidText.customError;
}
}
}
setInvalidText(errorTxt);
}
setIsValid(isInputValid);
if (!isUndefined(props.setValidationStatus)) {
props.setValidationStatus(false);
}
}
return isInputValid;
};
const isValidField = async (): Promise<boolean> => {
if (input.current) {
const value = input.current!.value;
if (value !== '') {
if (!isUndefined(props.excludedValues) && props.excludedValues.includes(value)) {
input.current!.setCustomValidity('Value is excluded');
} else if (!isUndefined(props.checkAvailability) && !props.checkAvailability.excluded.includes(value)) {
setIsCheckingAvailability(true);
try {
const isAvailable = await API.checkAvailability({
resourceKind: props.checkAvailability.resourceKind,
value: value,
});
if (!isNull(input.current)) {
if (isAvailable) {
input.current!.setCustomValidity(props.checkAvailability!.isAvailable ? 'Already taken' : '');
} else {
input.current!.setCustomValidity(props.checkAvailability!.isAvailable ? '' : 'Resource is not valid');
}
}
} catch {
if (!isNull(input.current)) {
input.current!.setCustomValidity(props.checkAvailability!.isAvailable ? 'Already taken' : '');
}
}
setIsCheckingAvailability(false);
} else if (props.checkPasswordStrength) {
setIsCheckingPwdStrength(true);
try {
await API.checkPasswordStrength(value);
if (!isNull(input.current)) {
input.current!.setCustomValidity('');
setPwdStrengthError(null);
}
} catch (e: any) {
if (!isNull(input.current) && e.message) {
setPwdStrengthError(e.message);
input.current!.setCustomValidity(e.message);
}
}
setIsCheckingPwdStrength(false);
} else {
if (!isNull(input.current)) {
input.current!.setCustomValidity('');
}
}
}
}
return checkValidity();
};
const handleOnBlur = (): void => {
if (!isUndefined(props.validateOnBlur) && props.validateOnBlur && input.current) {
cleanTimeout(); // On blur we clean timeout if it's necessary
isValidField();
}
};
const handleOnChange = (e: ChangeEvent<HTMLInputElement>): void => {
setInputValue(e.target.value);
if (!isUndefined(props.onChange)) {
props.onChange(e);
}
};
const cleanTimeout = () => {
if (!isNull(validateTimeout)) {
clearTimeout(validateTimeout);
setValidateTimeout(null);
}
};
useEffect(() => {
const isInputFocused = input.current === document.activeElement;
if (isInputFocused && !isUndefined(props.validateOnChange) && props.validateOnChange) {
cleanTimeout();
setValidateTimeout(
setTimeout(() => {
isValidField();
}, VALIDATION_DELAY)
);
}
return () => {
if (validateTimeout) {
clearTimeout(validateTimeout);
}
};
}, [inputValue]); /* eslint-disable-line react-hooks/exhaustive-deps */
return (
<div className={`${props.smallBottomMargin ? 'mb-3' : 'mb-4'} position-relative ${props.className}`}>
{!isUndefined(props.label) && (
<label htmlFor={props.name} className={`form-label fw-bold ${styles.label}`}>
<span className="fw-bold">{props.label}</span>
{!isUndefined(props.labelLegend) && <>{props.labelLegend}</>}
</label>
)}
<input
data-testid={`${props.name}Input`}
ref={input}
type={activeType}
id={props.name}
name={props.name}
value={inputValue}
className={classnames('form-control', props.inputClassName, { 'is-invalid': !isNull(isValid) && !isValid })}
placeholder={props.placeholder}
required={props.required}
minLength={props.minLength}
maxLength={props.maxLength}
min={props.min}
max={props.max}
pattern={props.pattern}
autoComplete={props.autoComplete}
readOnly={props.readOnly || false}
onChange={handleOnChange}
onBlur={handleOnBlur}
onKeyDown={props.onKeyDown}
autoFocus={props.autoFocus}
disabled={props.disabled}
spellCheck="false"
/>
{props.type === 'password' && props.visiblePassword && (
<button
type="button"
className={classnames('btn btn-link position-absolute bottom-0', styles.revealBtn, {
'text-muted': activeType === 'password',
'text-secondary': activeType !== 'password',
})}
onClick={() => setActiveType(activeType === 'password' ? 'text' : 'password')}
aria-label={`${activeType === 'password' ? 'Hide' : 'Show'} password`}
>
{activeType === 'password' ? <FaEyeSlash /> : <FaEye />}
</button>
)}
{(isCheckingAvailability || isCheckingPwdStrength) && (
<div className={`position-absolute ${styles.spinner}`}>
<span className="spinner-border spinner-border-sm text-primary" />
</div>
)}
{!isUndefined(props.validText) && (
<div className={`valid-feedback mt-0 ${styles.inputFeedback}`}>{props.validText}</div>
)}
{!isUndefined(invalidText) && isNull(pwdStrengthError) && (
<div className={`invalid-feedback mt-0 ${styles.inputFeedback}`}>{invalidText}</div>
)}
{!isNull(pwdStrengthError) && (
<div className={`invalid-feedback mt-0 ${styles.inputPwdStrengthError}`}>
{capitalizeFirstLetter(pwdStrengthError)}
</div>
)}
{!isUndefined(props.additionalInfo) && <div className="alert p-0 mt-4">{props.additionalInfo}</div>}
</div>
);
})
Example #8
Source File: AnnotationGroupItem.tsx From slim with Apache License 2.0 | 4 votes |
render (): React.ReactNode {
const identifier = `Annotation Group ${this.props.annotationGroup.number}`
const attributes: Array<{ name: string, value: string }> = [
{
name: 'Label',
value: this.props.annotationGroup.label
},
{
name: 'Algorithm Name',
value: this.props.annotationGroup.algorithmName
},
{
name: 'Property category',
value: this.props.annotationGroup.propertyCategory.CodeMeaning
},
{
name: 'Property type',
value: this.props.annotationGroup.propertyType.CodeMeaning
}
]
const index = this.props.metadata.AnnotationGroupSequence.findIndex(
item => (item.AnnotationGroupUID === this.props.annotationGroup.uid)
)
const item = this.props.metadata.AnnotationGroupSequence[index]
const measurementsSequence = item.MeasurementsSequence ?? []
const measurementOptions = measurementsSequence.map(measurementItem => {
const name = measurementItem.ConceptNameCodeSequence[0]
const key = `${name.CodingSchemeDesignator}-${name.CodeValue}`
return (
<Select.Option
key={key}
value={key}
dropdownMatchSelectWidth={false}
size='small'
disabled={!this.props.isVisible}
>
{name.CodeMeaning}
</Select.Option>
)
})
const settings = (
<div>
<Row justify='start' align='middle' gutter={[8, 8]}>
<Col span={6}>
Opacity
</Col>
<Col span={12}>
<Slider
range={false}
min={0}
max={1}
step={0.01}
value={this.state.currentStyle.opacity}
onChange={this.handleOpacityChange}
/>
</Col>
<Col span={6}>
<InputNumber
min={0}
max={1}
size='small'
step={0.1}
style={{ width: '65px' }}
value={this.state.currentStyle.opacity}
onChange={this.handleOpacityChange}
/>
</Col>
</Row>
<Divider plain>
Exploration
</Divider>
<Row justify='start' align='middle' gutter={[8, 8]}>
<Col span={8}>
Measurement
</Col>
<Col span={16}>
<Select
style={{ minWidth: '65px', width: '90%' }}
onSelect={this.handleMeasurementSelection}
key='annotation-group-measurements'
defaultValue={undefined}
>
{measurementOptions}
</Select>
</Col>
</Row>
</div>
)
const {
annotationGroup,
defaultStyle,
isVisible,
metadata,
onVisibilityChange,
onStyleChange,
...otherProps
} = this.props
return (
<Menu.Item
style={{ height: '100%', paddingLeft: '3px' }}
key={this.props.annotationGroup.uid}
{...otherProps}
>
<Space align='start'>
<div style={{ paddingLeft: '14px' }}>
<Space direction='vertical' align='end'>
<Switch
size='small'
onChange={this.handleVisibilityChange}
checked={this.props.isVisible}
checkedChildren={<FaEye />}
unCheckedChildren={<FaEyeSlash />}
/>
<Popover
placement='left'
content={settings}
overlayStyle={{ width: '350px' }}
title='Display Settings'
>
<Button
type='primary'
shape='circle'
icon={<SettingOutlined />}
/>
</Popover>
</Space>
</div>
<Description
header={identifier}
attributes={attributes}
selectable
hasLongValues
/>
</Space>
</Menu.Item>
)
}
Example #9
Source File: AnnotationItem.tsx From slim with Apache License 2.0 | 4 votes |
render (): React.ReactNode {
const identifier = `ROI ${this.props.index + 1}`
const attributes: Array<{ name: string, value: string }> = []
/**
* This hack is required for Menu.Item to work properly:
* https://github.com/react-component/menu/issues/142
*/
const { isVisible, onVisibilityChange, ...otherProps } = this.props
this.props.roi.evaluations.forEach((
item: (
dcmjs.sr.valueTypes.TextContentItem |
dcmjs.sr.valueTypes.CodeContentItem
)
) => {
const nameValue = item.ConceptNameCodeSequence[0].CodeValue
const nameMeaning = item.ConceptNameCodeSequence[0].CodeMeaning
const name = `${nameMeaning}`
if (item.ValueType === dcmjs.sr.valueTypes.ValueTypes.CODE) {
const codeContentItem = item as dcmjs.sr.valueTypes.CodeContentItem
const valueMeaning = codeContentItem.ConceptCodeSequence[0].CodeMeaning
// For consistency with Segment and Annotation Group
if (nameValue === '276214006') {
attributes.push({
name: 'Property category',
value: `${valueMeaning}`
})
} else if (nameValue === '121071') {
attributes.push({
name: 'Property type',
value: `${valueMeaning}`
})
} else if (nameValue === '111001') {
attributes.push({
name: 'Algorithm Name',
value: `${valueMeaning}`
})
} else {
attributes.push({
name: name,
value: `${valueMeaning}`
})
}
} else if (item.ValueType === dcmjs.sr.valueTypes.ValueTypes.TEXT) {
const textContentItem = item as dcmjs.sr.valueTypes.TextContentItem
attributes.push({
name: name,
value: textContentItem.TextValue
})
}
})
this.props.roi.measurements.forEach(item => {
const nameMeaning = item.ConceptNameCodeSequence[0].CodeMeaning
const name = `${nameMeaning}`
const seq = item.MeasuredValueSequence[0]
const value = seq.NumericValue.toPrecision(6)
const unit = seq.MeasurementUnitsCodeSequence[0].CodeValue
attributes.push({
name: name,
value: `${value} ${unit}`
})
})
return (
<Space align='start'>
<div style={{ paddingLeft: '14px' }}>
<Switch
size='small'
onChange={this.handleVisibilityChange}
checked={this.props.isVisible}
checkedChildren={<FaEye />}
unCheckedChildren={<FaEyeSlash />}
/>
</div>
<Menu.Item
style={{ height: '100%', paddingLeft: '3px' }}
key={this.props.roi.uid}
{...otherProps}
>
<Description
header={identifier}
attributes={attributes}
selectable
hasLongValues
/>
</Menu.Item>
</Space>
)
}
Example #10
Source File: MappingItem.tsx From slim with Apache License 2.0 | 4 votes |
render (): React.ReactNode {
const identifier = `Mapping ${this.props.mapping.number}`
const attributes: Array<{ name: string, value: string }> = [
{
name: 'Label',
value: this.props.mapping.label
}
]
const settings = (
<div>
<Row justify='center' align='middle'>
<Col span={6}>
Opacity
</Col>
<Col span={12}>
<Slider
range={false}
min={0}
max={1}
step={0.01}
value={this.state.currentStyle.opacity}
onChange={this.handleOpacityChange}
/>
</Col>
<Col span={6}>
<InputNumber
min={0}
max={1}
size='small'
step={0.1}
style={{ width: '65px' }}
value={this.state.currentStyle.opacity}
onChange={this.handleOpacityChange}
/>
</Col>
</Row>
</div>
)
/**
* This hack is required for Menu.Item to work properly:
* https://github.com/react-component/menu/issues/142
*/
const {
defaultStyle,
isVisible,
mapping,
metadata,
onVisibilityChange,
onStyleChange,
...otherProps
} = this.props
return (
<Menu.Item
style={{ height: '100%', paddingLeft: '3px' }}
key={this.props.mapping.uid}
{...otherProps}
>
<Space align='start'>
<div style={{ paddingLeft: '14px' }}>
<Space direction='vertical' align='end' size={100}>
<Space direction='vertical' align='end'>
<Switch
size='small'
onChange={this.handleVisibilityChange}
checked={this.props.isVisible}
checkedChildren={<FaEye />}
unCheckedChildren={<FaEyeSlash />}
/>
<Popover
placement='left'
content={settings}
overlayStyle={{ width: '350px' }}
title='Display Settings'
>
<Button
type='primary'
shape='circle'
icon={<SettingOutlined />}
/>
</Popover>
</Space>
</Space>
</div>
<Description
header={identifier}
attributes={attributes}
selectable
hasLongValues
/>
</Space>
</Menu.Item>
)
}
Example #11
Source File: SegmentItem.tsx From slim with Apache License 2.0 | 4 votes |
render (): React.ReactNode {
const attributes: Array<{ name: string, value: string }> = [
{
name: 'Property Category',
value: this.props.segment.propertyCategory.CodeMeaning
},
{
name: 'Property Type',
value: this.props.segment.propertyType.CodeMeaning
},
{
name: 'Algorithm Name',
value: this.props.segment.algorithmName
}
]
const settings = (
<div>
<Row justify='center' align='middle'>
<Col span={6}>
Opacity
</Col>
<Col span={12}>
<Slider
range={false}
min={0}
max={1}
step={0.01}
value={this.state.currentStyle.opacity}
onChange={this.handleOpacityChange}
/>
</Col>
<Col span={6}>
<InputNumber
min={0}
max={1}
size='small'
step={0.1}
style={{ width: '65px' }}
value={this.state.currentStyle.opacity}
onChange={this.handleOpacityChange}
/>
</Col>
</Row>
</div>
)
/**
* This hack is required for Menu.Item to work properly:
* https://github.com/react-component/menu/issues/142
*/
const {
defaultStyle,
isVisible,
segment,
metadata,
onVisibilityChange,
onStyleChange,
...otherProps
} = this.props
return (
<Menu.Item
style={{ height: '100%', paddingLeft: '3px' }}
key={this.props.segment.uid}
{...otherProps}
>
<Space align='start'>
<div style={{ paddingLeft: '14px' }}>
<Space direction='vertical' align='end'>
<Switch
size='small'
onChange={this.handleVisibilityChange}
checked={this.props.isVisible}
checkedChildren={<FaEye />}
unCheckedChildren={<FaEyeSlash />}
/>
<Popover
placement='left'
content={settings}
overlayStyle={{ width: '350px' }}
title='Display Settings'
>
<Button
type='primary'
shape='circle'
icon={<SettingOutlined />}
/>
</Popover>
</Space>
</div>
<Description
header={this.props.segment.label}
attributes={attributes}
selectable
hasLongValues
/>
</Space>
</Menu.Item>
)
}
Example #12
Source File: SlideViewer.tsx From slim with Apache License 2.0 | 4 votes |
render (): React.ReactNode {
const rois: dmv.roi.ROI[] = []
const segments: dmv.segment.Segment[] = []
const mappings: dmv.mapping.ParameterMapping[] = []
const annotationGroups: dmv.annotation.AnnotationGroup[] = []
rois.push(...this.volumeViewer.getAllROIs())
segments.push(...this.volumeViewer.getAllSegments())
mappings.push(...this.volumeViewer.getAllParameterMappings())
annotationGroups.push(...this.volumeViewer.getAllAnnotationGroups())
const openSubMenuItems = ['specimens', 'opticalpaths', 'annotations']
let report: React.ReactNode
const dataset = this.state.generatedReport
if (dataset !== undefined) {
report = <Report dataset={dataset} />
}
let annotationMenuItems: React.ReactNode
if (rois.length > 0) {
annotationMenuItems = (
<AnnotationList
rois={rois}
selectedRoiUIDs={this.state.selectedRoiUIDs}
visibleRoiUIDs={this.state.visibleRoiUIDs}
onSelection={this.handleAnnotationSelection}
onVisibilityChange={this.handleAnnotationVisibilityChange}
/>
)
}
const findingOptions = this.findingOptions.map(finding => {
return (
<Select.Option
key={finding.CodeValue}
value={finding.CodeValue}
>
{finding.CodeMeaning}
</Select.Option>
)
})
const geometryTypeOptionsMapping: { [key: string]: React.ReactNode } = {
point: <Select.Option key='point' value='point'>Point</Select.Option>,
circle: <Select.Option key='circle' value='circle'>Circle</Select.Option>,
box: <Select.Option key='box' value='box'>Box</Select.Option>,
polygon: <Select.Option key='polygon' value='polygon'>Polygon</Select.Option>,
line: <Select.Option key='line' value='line'>Line</Select.Option>,
freehandpolygon: (
<Select.Option key='freehandpolygon' value='freehandpolygon'>
Polygon (freehand)
</Select.Option>
),
freehandline: (
<Select.Option key='freehandline' value='freehandline'>
Line (freehand)
</Select.Option>
)
}
const selections: React.ReactNode[] = [
(
<Select
style={{ minWidth: 130 }}
onSelect={this.handleAnnotationFindingSelection}
key='annotation-finding'
defaultActiveFirstOption
>
{findingOptions}
</Select>
)
]
const selectedFinding = this.state.selectedFinding
if (selectedFinding !== undefined) {
const key = _buildKey(selectedFinding)
this.evaluationOptions[key].forEach(evaluation => {
const evaluationOptions = evaluation.values.map(code => {
return (
<Select.Option
key={code.CodeValue}
value={code.CodeValue}
label={evaluation.name}
>
{code.CodeMeaning}
</Select.Option>
)
})
selections.push(
<>
{evaluation.name.CodeMeaning}
<Select
style={{ minWidth: 130 }}
onSelect={this.handleAnnotationEvaluationSelection}
allowClear
onClear={this.handleAnnotationEvaluationClearance}
defaultActiveFirstOption={false}
>
{evaluationOptions}
</Select>
</>
)
})
const geometryTypeOptions = this.geometryTypeOptions[key].map(name => {
return geometryTypeOptionsMapping[name]
})
selections.push(
<Select
style={{ minWidth: 130 }}
onSelect={this.handleAnnotationGeometryTypeSelection}
key='annotation-geometry-type'
>
{geometryTypeOptions}
</Select>
)
selections.push(
<Checkbox
onChange={this.handleAnnotationMeasurementActivation}
key='annotation-measurement'
>
measure
</Checkbox>
)
}
const specimenMenu = (
<Menu.SubMenu key='specimens' title='Specimens'>
<SpecimenList
metadata={this.props.slide.volumeImages[0]}
showstain={false}
/>
</Menu.SubMenu>
)
const equipmentMenu = (
<Menu.SubMenu key='equipment' title='Equipment'>
<Equipment metadata={this.props.slide.volumeImages[0]} />
</Menu.SubMenu>
)
const defaultOpticalPathStyles: {
[identifier: string]: {
opacity: number
color?: number[]
limitValues?: number[]
}
} = {}
const opticalPathMetadata: {
[identifier: string]: dmv.metadata.VLWholeSlideMicroscopyImage[]
} = {}
const opticalPaths = this.volumeViewer.getAllOpticalPaths()
opticalPaths.sort((a, b) => {
if (a.identifier < b.identifier) {
return -1
} else if (a.identifier > b.identifier) {
return 1
}
return 0
})
opticalPaths.forEach(opticalPath => {
const identifier = opticalPath.identifier
const metadata = this.volumeViewer.getOpticalPathMetadata(identifier)
opticalPathMetadata[identifier] = metadata
const style = this.volumeViewer.getOpticalPathStyle(identifier)
defaultOpticalPathStyles[identifier] = style
})
const opticalPathMenu = (
<Menu.SubMenu key='opticalpaths' title='Optical Paths'>
<OpticalPathList
metadata={opticalPathMetadata}
opticalPaths={opticalPaths}
defaultOpticalPathStyles={defaultOpticalPathStyles}
visibleOpticalPathIdentifiers={this.state.visibleOpticalPathIdentifiers}
activeOpticalPathIdentifiers={this.state.activeOpticalPathIdentifiers}
onOpticalPathVisibilityChange={this.handleOpticalPathVisibilityChange}
onOpticalPathStyleChange={this.handleOpticalPathStyleChange}
onOpticalPathActivityChange={this.handleOpticalPathActivityChange}
selectedPresentationStateUID={this.state.selectedPresentationStateUID}
/>
</Menu.SubMenu>
)
let presentationStateMenu
console.log('DEBUG: ', this.state.presentationStates)
if (this.state.presentationStates.length > 0) {
const presentationStateOptions = this.state.presentationStates.map(
presentationState => {
return (
<Select.Option
key={presentationState.SOPInstanceUID}
value={presentationState.SOPInstanceUID}
dropdownMatchSelectWidth={false}
size='small'
>
{presentationState.ContentDescription}
</Select.Option>
)
}
)
presentationStateMenu = (
<Menu.SubMenu key='presentationStates' title='Presentation States'>
<Space align='center' size={20} style={{ padding: '14px' }}>
<Select
style={{ minWidth: 200, maxWidth: 200 }}
onSelect={this.handlePresentationStateSelection}
key='presentation-states'
defaultValue={this.props.selectedPresentationStateUID}
value={this.state.selectedPresentationStateUID}
>
{presentationStateOptions}
</Select>
<Tooltip title='Reset'>
<Btn
icon={<UndoOutlined />}
type='primary'
onClick={this.handlePresentationStateReset}
/>
</Tooltip>
</Space>
</Menu.SubMenu>
)
}
let segmentationMenu
if (segments.length > 0) {
const defaultSegmentStyles: {
[segmentUID: string]: {
opacity: number
}
} = {}
const segmentMetadata: {
[segmentUID: string]: dmv.metadata.Segmentation[]
} = {}
const segments = this.volumeViewer.getAllSegments()
segments.forEach(segment => {
defaultSegmentStyles[segment.uid] = this.volumeViewer.getSegmentStyle(
segment.uid
)
segmentMetadata[segment.uid] = this.volumeViewer.getSegmentMetadata(
segment.uid
)
})
segmentationMenu = (
<Menu.SubMenu key='segmentations' title='Segmentations'>
<SegmentList
segments={segments}
metadata={segmentMetadata}
defaultSegmentStyles={defaultSegmentStyles}
visibleSegmentUIDs={this.state.visibleSegmentUIDs}
onSegmentVisibilityChange={this.handleSegmentVisibilityChange}
onSegmentStyleChange={this.handleSegmentStyleChange}
/>
</Menu.SubMenu>
)
openSubMenuItems.push('segmentations')
}
let parametricMapMenu
if (mappings.length > 0) {
const defaultMappingStyles: {
[mappingUID: string]: {
opacity: number
}
} = {}
const mappingMetadata: {
[mappingUID: string]: dmv.metadata.ParametricMap[]
} = {}
mappings.forEach(mapping => {
defaultMappingStyles[mapping.uid] = this.volumeViewer.getParameterMappingStyle(
mapping.uid
)
mappingMetadata[mapping.uid] = this.volumeViewer.getParameterMappingMetadata(
mapping.uid
)
})
parametricMapMenu = (
<Menu.SubMenu key='parmetricmaps' title='Parametric Maps'>
<MappingList
mappings={mappings}
metadata={mappingMetadata}
defaultMappingStyles={defaultMappingStyles}
visibleMappingUIDs={this.state.visibleMappingUIDs}
onMappingVisibilityChange={this.handleMappingVisibilityChange}
onMappingStyleChange={this.handleMappingStyleChange}
/>
</Menu.SubMenu>
)
openSubMenuItems.push('parametricmaps')
}
let annotationGroupMenu
if (annotationGroups.length > 0) {
const defaultAnnotationGroupStyles: {
[annotationGroupUID: string]: {
opacity: number
}
} = {}
const annotationGroupMetadata: {
[annotationGroupUID: string]: dmv.metadata.MicroscopyBulkSimpleAnnotations
} = {}
const annotationGroups = this.volumeViewer.getAllAnnotationGroups()
annotationGroups.forEach(annotationGroup => {
defaultAnnotationGroupStyles[annotationGroup.uid] = this.volumeViewer.getAnnotationGroupStyle(
annotationGroup.uid
)
annotationGroupMetadata[annotationGroup.uid] = this.volumeViewer.getAnnotationGroupMetadata(
annotationGroup.uid
)
})
annotationGroupMenu = (
<Menu.SubMenu key='annotationGroups' title='Annotation Groups'>
<AnnotationGroupList
annotationGroups={annotationGroups}
metadata={annotationGroupMetadata}
defaultAnnotationGroupStyles={defaultAnnotationGroupStyles}
visibleAnnotationGroupUIDs={this.state.visibleAnnotationGroupUIDs}
onAnnotationGroupVisibilityChange={this.handleAnnotationGroupVisibilityChange}
onAnnotationGroupStyleChange={this.handleAnnotationGroupStyleChange}
/>
</Menu.SubMenu>
)
openSubMenuItems.push('annotationGroups')
}
let toolbar
let toolbarHeight = '0px'
if (this.props.enableAnnotationTools) {
toolbar = (
<Row>
<Button
tooltip='Draw ROI [d]'
icon={FaDrawPolygon}
onClick={this.handleRoiDrawing}
isSelected={this.state.isRoiDrawingActive}
/>
<Button
tooltip='Modify ROIs [m]'
icon={FaHandPointer}
onClick={this.handleRoiModification}
isSelected={this.state.isRoiModificationActive}
/>
<Button
tooltip='Translate ROIs [t]'
icon={FaHandPaper}
onClick={this.handleRoiTranslation}
isSelected={this.state.isRoiTranslationActive}
/>
<Button
tooltip='Remove selected ROI [r]'
onClick={this.handleRoiRemoval}
icon={FaTrash}
/>
<Button
tooltip='Show/Hide ROIs [v]'
icon={this.state.areRoisHidden ? FaEye : FaEyeSlash}
onClick={this.handleRoiVisibilityChange}
isSelected={this.state.areRoisHidden}
/>
<Button
tooltip='Save ROIs [s]'
icon={FaSave}
onClick={this.handleReportGeneration}
/>
</Row>
)
toolbarHeight = '50px'
}
/* It would be nicer to use the ant Spin component, but that causes issues
* with the positioning of the viewport.
*/
let loadingDisplay = 'none'
if (this.state.isLoading) {
loadingDisplay = 'block'
}
return (
<Layout style={{ height: '100%' }} hasSider>
<Layout.Content style={{ height: '100%' }}>
{toolbar}
<div className='dimmer' style={{ display: loadingDisplay }} />
<div className='spinner' style={{ display: loadingDisplay }} />
<div
style={{
height: `calc(100% - ${toolbarHeight})`,
overflow: 'hidden'
}}
ref={this.volumeViewportRef}
/>
<Modal
visible={this.state.isAnnotationModalVisible}
title='Configure annotations'
onOk={this.handleAnnotationConfigurationCompletion}
onCancel={this.handleAnnotationConfigurationCancellation}
okText='Select'
>
<Space align='start' direction='vertical'>
{selections}
</Space>
</Modal>
<Modal
visible={this.state.isReportModalVisible}
title='Verify and save report'
onOk={this.handleReportVerification}
onCancel={this.handleReportCancellation}
okText='Save'
>
{report}
</Modal>
</Layout.Content>
<Layout.Sider
width={300}
reverseArrow
style={{
borderLeft: 'solid',
borderLeftWidth: 0.25,
overflow: 'hidden',
background: 'none'
}}
>
<Menu
mode='inline'
defaultOpenKeys={openSubMenuItems}
style={{ height: '100%' }}
inlineIndent={14}
forceSubMenuRender
>
<Menu.SubMenu key='label' title='Slide label'>
<Menu.Item style={{ height: '100%' }}>
<div
style={{ height: '220px' }}
ref={this.labelViewportRef}
/>
</Menu.Item>
</Menu.SubMenu>
{specimenMenu}
{equipmentMenu}
{opticalPathMenu}
{presentationStateMenu}
<Menu.SubMenu key='annotations' title='Annotations'>
{annotationMenuItems}
</Menu.SubMenu>
{annotationGroupMenu}
{segmentationMenu}
{parametricMapMenu}
</Menu>
</Layout.Sider>
</Layout>
)
}
Example #13
Source File: Cell.tsx From crosshare with GNU Affero General Public License v3.0 | 4 votes |
Cell = memo(function Cell(props: CellProps) {
let bg = 'var(--cell-bg)';
if (props.isEnteringRebus && props.active) {
/* noop */
} else if (props.isBlock && props.active) {
bg =
'repeating-linear-gradient(-45deg,var(--cell-wall),var(--cell-wall) 10px,var(--primary) 10px,var(--primary) 20px);';
} else if (props.isBlock) {
bg = 'var(--cell-wall)';
} else if (props.cellColor !== undefined) {
bg = 'rgba(241, 167, 45, ' + props.cellColor + ')';
} else if (props.isEnteringRebus) {
/* noop */
} else if (props.active) {
bg = 'var(--primary)';
} else if (props.entryCell) {
bg = 'var(--lighter)';
} else if (props.refedCell) {
bg = 'var(--secondary)';
}
const cellSize = props.squareWidth / props.gridWidth;
const filledValue =
props.active && props.isEnteringRebus
? props.rebusValue || ''
: props.value.trim();
const value =
props.active && props.isEnteringRebus
? filledValue
: filledValue || props.autofill;
let boxShadow = '';
if (props.isEnteringRebus && props.active) {
boxShadow = 'inset 0 0 0 0.1em var(--primary)';
} else if (props.highlightCell) {
boxShadow = 'inset 0 0 0 0.02em var(--black)';
} else if (props.cellColor !== undefined) {
if (props.active) {
boxShadow = 'inset 0 0 0 0.05em var(--black)';
} else if (props.entryCell) {
boxShadow = 'inset 0 0 0 0.02em var(--black)';
}
}
return (
<div
css={{
width: 100 / props.gridWidth + '%',
paddingBottom: 100 / props.gridWidth + '%',
float: 'left',
position: 'relative',
margin: 0,
overflow: 'hidden',
}}
>
{/* eslint-disable-next-line */}
<div
aria-label={'cell' + props.row + 'x' + props.column}
onClick={() => props.onClick({ row: props.row, col: props.column })}
css={{
userSelect: 'none',
position: 'absolute',
width: '100%',
height: '100%',
fontSize: cellSize,
...(props.hidden &&
props.active && {
background:
'repeating-linear-gradient(-45deg,var(--cell-bg),var(--cell-bg) 10px,var(--primary) 10px,var(--primary) 20px)',
}),
...(!props.hidden && {
borderLeft: '1px solid var(--cell-wall)',
borderTop: '1px solid var(--cell-wall)',
...((props.row === props.gridHeight - 1 || props.hiddenBottom) && {
borderBottom: '1px solid var(--cell-wall)',
}),
...(props.barBottom &&
props.row !== props.gridHeight - 1 && {
borderBottom: '0.05em solid var(--cell-wall)',
}),
...((props.column === props.gridWidth - 1 || props.hiddenRight) && {
borderRight: '1px solid var(--cell-wall)',
}),
...(props.barRight &&
props.column !== props.gridWidth - 1 && {
borderRight: '0.05em solid var(--cell-wall)',
}),
background: bg,
...(boxShadow && { boxShadow }),
}),
}}
>
{!props.isBlock || (props.isEnteringRebus && props.active) ? (
<>
<div
css={{
position: 'absolute',
left: '0.1em',
top: 0,
fontWeight: 'bold',
lineHeight: '1em',
color: props.active ? 'var(--onprimary)' : 'var(--text)',
fontSize: '0.25em',
}}
>
{props.wasRevealed ? (
<div
css={{
position: 'absolute',
left: '1.85em',
top: '-0.1em',
fontSize: '1.2em',
color: 'var(--verified)',
}}
>
<FaEye />
</div>
) : (
''
)}
{props.number}
</div>
<div
css={{
color: props.isVerified
? 'var(--verified)'
: filledValue
? props.active && !props.isEnteringRebus
? 'var(--onprimary)'
: 'var(--text)'
: 'var(--autofill)',
textAlign: 'center',
lineHeight: '1.2em',
fontSize: '0.9em',
}}
>
{props.isWrong ? (
<div
css={{
position: 'absolute',
zIndex: 2,
left: '0.03em',
top: '-0.1em',
color: 'var(--error)',
fontSize: '1em',
}}
>
<FaSlash />
</div>
) : (
''
)}
{props.highlight === 'circle' ? (
<div
css={{
zIndex: 0,
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0,
border: '1px solid var(--black)',
borderRadius: '50%',
}}
></div>
) : (
''
)}
{props.highlight === 'shade' ? (
<div
css={{
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0,
backgroundColor: 'var(--shade-highlight)',
}}
></div>
) : (
''
)}
<div
css={{
fontSize: 1.0 / Math.max(value.length - 0.4, 1) + 'em',
}}
>
{props.active && props.isEnteringRebus ? (
<>
{value}
<Cursor />
</>
) : (
value
)}
</div>
</div>
</>
) : (
''
)}
</div>
</div>
);
})
Example #14
Source File: Puzzle.tsx From crosshare with GNU Affero General Public License v3.0 | 4 votes |
Puzzle = ({
loadingPlayState,
puzzle,
play,
...props
}: PuzzleProps & AuthPropsOptional) => {
const [state, dispatch] = useReducer(
puzzleReducer,
{
type: 'puzzle',
wasEntryClick: false,
active: { col: 0, row: 0, dir: Direction.Across },
grid: addClues(
fromCells({
mapper: (e) => e,
width: puzzle.size.cols,
height: puzzle.size.rows,
cells: play
? play.g
: puzzle.grid.map((s) => (s === BLOCK ? BLOCK : ' ')),
vBars: new Set(puzzle.vBars),
hBars: new Set(puzzle.hBars),
allowBlockEditing: false,
highlighted: new Set(puzzle.highlighted),
highlight: puzzle.highlight,
hidden: new Set(puzzle.hidden),
}),
puzzle.clues
),
showExtraKeyLayout: false,
answers: puzzle.grid,
alternateSolutions: puzzle.alternateSolutions,
verifiedCells: new Set<number>(play ? play.vc : []),
wrongCells: new Set<number>(play ? play.wc : []),
revealedCells: new Set<number>(play ? play.rc : []),
downsOnly: play?.do || false,
isEnteringRebus: false,
rebusValue: '',
success: play ? play.f : false,
ranSuccessEffects: play ? play.f : false,
filled: false,
autocheck: false,
dismissedKeepTrying: false,
dismissedSuccess: false,
moderating: false,
showingEmbedOverlay: false,
displaySeconds: play ? play.t : 0,
bankedSeconds: play ? play.t : 0,
ranMetaSubmitEffects: false,
...(play &&
play.ct_rv && {
contestRevealed: true,
contestSubmitTime: play.ct_t?.toMillis(),
}),
...(play &&
play.ct_sub && {
ranMetaSubmitEffects: true,
contestPriorSubmissions: play.ct_pr_subs,
contestDisplayName: play.ct_n,
contestSubmission: play.ct_sub,
contestEmail: play.ct_em,
contestSubmitTime: play.ct_t?.toMillis(),
}),
currentTimeWindowStart: 0,
didCheat: play ? play.ch : false,
clueView: false,
cellsUpdatedAt: play ? play.ct : puzzle.grid.map(() => 0),
cellsIterationCount: play ? play.uc : puzzle.grid.map(() => 0),
cellsEverMarkedWrong: new Set<number>(play ? play.we : []),
loadedPlayState: !loadingPlayState,
waitToResize: true,
isEditable(cellIndex) {
return !this.verifiedCells.has(cellIndex) && !this.success;
},
},
advanceActiveToNonBlock
);
const authContext = useContext(AuthContext);
useEffect(() => {
if (!authContext.notifications?.length) {
return;
}
for (const notification of authContext.notifications) {
if (notification.r) {
// shouldn't be possible but be defensive
continue;
}
if (!isNewPuzzleNotification(notification)) {
continue;
}
if (notification.p === puzzle.id) {
App.firestore()
.collection('n')
.doc(notification.id)
.update({ r: true });
return;
}
}
}, [authContext.notifications, puzzle.id]);
useEffect(() => {
if (loadingPlayState === false) {
const action: LoadPlayAction = {
type: 'LOADPLAY',
play: play,
prefs: props.prefs,
isAuthor: props.user ? props.user.uid === puzzle.authorId : false,
};
dispatch(action);
}
}, [loadingPlayState, play, props.user, props.prefs, puzzle.authorId]);
// Every (unpaused) second dispatch a tick action which updates the display time
useEffect(() => {
function tick() {
if (state.currentTimeWindowStart) {
dispatch({ type: 'TICKACTION' });
}
}
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [state.currentTimeWindowStart, dispatch]);
// Pause when page goes out of focus
function prodPause() {
if (process.env.NODE_ENV !== 'development') {
dispatch({ type: 'PAUSEACTION' });
writePlayToDBIfNeeded();
}
}
useEventListener('blur', prodPause);
const [muted, setMuted] = usePersistedBoolean('muted', false);
const [toggleKeyboard, setToggleKeyboard] = usePersistedBoolean(
'keyboard',
false
);
// Set up music player for success song
const [audioContext, initAudioContext] = useContext(CrosshareAudioContext);
const playSuccess = useRef<(() => void) | null>(null);
useEffect(() => {
if (!audioContext) {
return initAudioContext();
}
if (!playSuccess.current && !muted && audioContext) {
fetch('/success.mp3')
.then((response) => response.arrayBuffer())
.then((buffer) => {
audioContext.decodeAudioData(buffer, (audioBuffer) => {
playSuccess.current = () => {
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();
};
});
});
}
}, [muted, audioContext, initAudioContext]);
const writePlayToDBIfNeeded = useCallback(
async (user?: firebase.User) => {
console.log('doing write play');
if (!state.loadedPlayState) {
return;
}
if (puzzle.contestAnswers?.length) {
// For a meta we need to have run both to skip
if (state.ranSuccessEffects && state.ranMetaSubmitEffects) {
return;
}
} else {
// For a reg puzzle skip if success effects have run
if (state.ranSuccessEffects) {
return;
}
}
const u = user || props.user;
if (!u) {
return;
}
if (!isDirty(u, puzzle.id)) {
return;
}
writePlayToDB(u, puzzle.id)
.then(() => {
console.log('Finished writing play state to db');
})
.catch((reason) => {
console.error('Failed to write play: ', reason);
});
},
[
puzzle.id,
puzzle.contestAnswers,
props.user,
state.ranMetaSubmitEffects,
state.ranSuccessEffects,
state.loadedPlayState,
]
);
const cachePlayForUser = useCallback(
(user: firebase.User | undefined) => {
if (!state.loadedPlayState) {
return;
}
const updatedAt = TimestampClass.now();
const playTime =
state.currentTimeWindowStart === 0
? state.bankedSeconds
: state.bankedSeconds +
(new Date().getTime() - state.currentTimeWindowStart) / 1000;
const playForUser: PlayWithoutUserT = {
c: puzzle.id,
n: puzzle.title,
ua: updatedAt,
g: Array.from(state.grid.cells),
ct: Array.from(state.cellsUpdatedAt),
uc: Array.from(state.cellsIterationCount),
vc: Array.from(state.verifiedCells),
wc: Array.from(state.wrongCells),
we: Array.from(state.cellsEverMarkedWrong),
rc: Array.from(state.revealedCells),
t: playTime,
ch: state.didCheat,
do: state.downsOnly,
f: state.success,
...(state.contestRevealed && {
ct_rv: state.contestRevealed,
ct_t:
state.contestSubmitTime !== undefined
? TimestampClass.fromMillis(state.contestSubmitTime)
: undefined,
ct_n: state.contestDisplayName,
}),
...(state.contestSubmission && {
ct_sub: state.contestSubmission,
ct_pr_subs: state.contestPriorSubmissions || [],
ct_t:
state.contestSubmitTime !== undefined
? TimestampClass.fromMillis(state.contestSubmitTime)
: undefined,
ct_n: state.contestDisplayName,
...(state.contestEmail && {
ct_em: state.contestEmail,
}),
}),
};
cachePlay(user, puzzle.id, playForUser);
},
[
state.downsOnly,
state.loadedPlayState,
puzzle.id,
state.cellsEverMarkedWrong,
state.cellsIterationCount,
state.cellsUpdatedAt,
state.didCheat,
state.grid.cells,
state.revealedCells,
state.success,
state.verifiedCells,
state.wrongCells,
puzzle.title,
state.bankedSeconds,
state.currentTimeWindowStart,
state.contestSubmission,
state.contestSubmitTime,
state.contestEmail,
state.contestDisplayName,
state.contestRevealed,
state.contestPriorSubmissions,
]
);
useEffect(() => {
cachePlayForUser(props.user);
}, [props.user, cachePlayForUser]);
const router = useRouter();
useEffect(() => {
const listener = () => {
writePlayToDBIfNeeded();
};
window.addEventListener('beforeunload', listener);
router.events.on('routeChangeStart', listener);
return () => {
window.removeEventListener('beforeunload', listener);
router.events.off('routeChangeStart', listener);
};
}, [writePlayToDBIfNeeded, router]);
const { addToast } = useSnackbar();
useEffect(() => {
if (
(state.contestSubmission || state.contestRevealed) &&
!state.ranMetaSubmitEffects
) {
const action: RanMetaSubmitEffectsAction = { type: 'RANMETASUBMIT' };
dispatch(action);
if (props.user) {
cachePlayForUser(props.user);
writePlayToDBIfNeeded(props.user);
} else {
signInAnonymously().then((u) => {
cachePlayForUser(u);
writePlayToDBIfNeeded(u);
});
}
}
}, [
cachePlayForUser,
state.contestSubmission,
state.contestRevealed,
state.ranMetaSubmitEffects,
props.user,
writePlayToDBIfNeeded,
]);
useEffect(() => {
if (state.success && !state.ranSuccessEffects) {
const action: RanSuccessEffectsAction = { type: 'RANSUCCESS' };
dispatch(action);
if (props.user) {
cachePlayForUser(props.user);
writePlayToDBIfNeeded(props.user);
} else {
signInAnonymously().then((u) => {
cachePlayForUser(u);
writePlayToDBIfNeeded(u);
});
}
let delay = 0;
if (state.bankedSeconds <= 60) {
addToast('? Solved in under a minute!');
delay += 500;
}
if (!state.didCheat && state.downsOnly) {
addToast('? Solved downs-only!', delay);
} else if (!state.didCheat) {
addToast('? Solved without check/reveal!', delay);
}
if (!muted && playSuccess.current) {
playSuccess.current();
}
}
}, [
addToast,
cachePlayForUser,
muted,
props.user,
state.bankedSeconds,
state.didCheat,
state.downsOnly,
state.ranSuccessEffects,
state.success,
writePlayToDBIfNeeded,
]);
const physicalKeyboardHandler = useCallback(
(e: KeyboardEvent) => {
// Disable keyboard when paused / loading play
if (!(state.success && state.dismissedSuccess)) {
if (loadingPlayState || !state.currentTimeWindowStart) {
return;
}
}
const mkey = fromKeyboardEvent(e);
if (isSome(mkey)) {
const kpa: KeypressAction = { type: 'KEYPRESS', key: mkey.value };
dispatch(kpa);
e.preventDefault();
}
},
[
dispatch,
loadingPlayState,
state.currentTimeWindowStart,
state.success,
state.dismissedSuccess,
]
);
useEventListener('keydown', physicalKeyboardHandler);
const pasteHandler = useCallback(
(e: ClipboardEvent) => {
const tagName = (e.target as HTMLElement)?.tagName?.toLowerCase();
if (tagName === 'textarea' || tagName === 'input') {
return;
}
const pa: PasteAction = {
type: 'PASTE',
content: e.clipboardData?.getData('Text') || '',
};
dispatch(pa);
e.preventDefault();
},
[dispatch]
);
useEventListener('paste', pasteHandler);
let [entry, cross] = entryAndCrossAtPosition(state.grid, state.active);
if (entry === null && cross !== null) {
dispatch({ type: 'CHANGEDIRECTION' });
[entry, cross] = [cross, entry];
}
const keyboardHandler = useCallback(
(key: string) => {
const mkey = fromKeyString(key);
if (isSome(mkey)) {
const kpa: KeypressAction = { type: 'KEYPRESS', key: mkey.value };
dispatch(kpa);
}
},
[dispatch]
);
const { acrossEntries, downEntries } = useMemo(() => {
return {
acrossEntries: state.grid.entries.filter(
(e) => e.direction === Direction.Across
),
downEntries: state.grid.entries.filter(
(e) => e.direction === Direction.Down
),
};
}, [state.grid.entries]);
const isEmbed = useContext(EmbedContext);
/* `clueMap` is a map from ENTRYWORD => '5D: This is the clue' - we use this
* for comment clue tooltips. */
const clueMap = useMemo(() => {
return getEntryToClueMap(state.grid, state.answers);
}, [state.grid, state.answers]);
/* `refs` is a set of referenced entry indexes for each entry in the grid - we use this
* for grid highlights when an entry is selected.
*
* `refPositions` is an array for each entry of [reffedEntry, clueTextStart, clueTextEnd] tuples
*/
const [refs, refPositions] = useMemo(() => {
return getRefs(state.grid);
}, [state.grid]);
const scrollToCross = useMatchMedia(SMALL_AND_UP_RULES);
const overlayBaseProps: PuzzleOverlayBaseProps = {
publishTime: puzzle.isPrivateUntil || puzzle.publishTime,
coverImage: props.coverImage,
profilePicture: props.profilePicture,
downsOnly: state.downsOnly,
clueMap: clueMap,
user: props.user,
nextPuzzle: props.nextPuzzle,
puzzle: puzzle,
isMuted: muted,
solveTime: state.displaySeconds,
didCheat: state.didCheat,
dispatch: dispatch,
};
let puzzleView: ReactNode;
const entryIdx = entryIndexAtPosition(state.grid, state.active);
let refed: Set<number> = new Set();
if (entryIdx !== null) {
refed = refs[entryIdx] || new Set();
}
const shouldConceal =
state.currentTimeWindowStart === 0 &&
!(state.success && state.dismissedSuccess);
if (state.clueView) {
puzzleView = (
<TwoCol
left={
<ClueList
isEnteringRebus={state.isEnteringRebus}
rebusValue={state.rebusValue}
wasEntryClick={state.wasEntryClick}
allEntries={state.grid.entries}
refPositions={refPositions}
refed={refed}
dimCompleted={true}
active={state.active}
grid={state.grid}
showEntries={true}
conceal={shouldConceal}
header={t`Across`}
entries={acrossEntries}
current={entry?.index}
cross={cross?.index}
scrollToCross={scrollToCross}
dispatch={dispatch}
downsOnly={state.downsOnly && !state.success}
/>
}
right={
<ClueList
isEnteringRebus={state.isEnteringRebus}
rebusValue={state.rebusValue}
wasEntryClick={state.wasEntryClick}
allEntries={state.grid.entries}
refPositions={refPositions}
refed={refed}
dimCompleted={true}
active={state.active}
grid={state.grid}
showEntries={true}
conceal={shouldConceal}
header={t`Down`}
entries={downEntries}
current={entry?.index}
cross={cross?.index}
scrollToCross={scrollToCross}
dispatch={dispatch}
downsOnly={state.downsOnly && !state.success}
/>
}
/>
);
} else {
puzzleView = (
<SquareAndCols
leftIsActive={state.active.dir === Direction.Across}
waitToResize={state.waitToResize}
dispatch={dispatch}
aspectRatio={state.grid.width / state.grid.height}
square={(width: number, _height: number) => {
return (
<GridView
isEnteringRebus={state.isEnteringRebus}
rebusValue={state.rebusValue}
squareWidth={width}
grid={state.grid}
active={state.active}
entryRefs={refs}
dispatch={dispatch}
revealedCells={state.revealedCells}
verifiedCells={state.verifiedCells}
wrongCells={state.wrongCells}
showAlternates={state.success ? state.alternateSolutions : null}
answers={state.answers}
/>
);
}}
header={
<div
css={{
height: SQUARE_HEADER_HEIGHT,
fontSize: 18,
lineHeight: '24px',
backgroundColor: 'var(--lighter)',
overflowY: 'scroll',
scrollbarWidth: 'none',
display: 'flex',
}}
>
{entry ? (
<div css={{ margin: 'auto 1em' }}>
<span
css={{
fontWeight: 'bold',
paddingRight: '0.5em',
}}
>
{entry.labelNumber}
{entry.direction === Direction.Across ? 'A' : 'D'}
</span>
<span
css={{
color: shouldConceal ? 'transparent' : 'var(--text)',
textShadow: shouldConceal
? '0 0 1em var(--conceal-text)'
: '',
}}
>
<ClueText
refPositions={refPositions}
entryIndex={entry.index}
allEntries={state.grid.entries}
grid={state.grid}
downsOnly={state.downsOnly && !state.success}
/>
</span>
</div>
) : (
''
)}
</div>
}
left={
<ClueList
wasEntryClick={state.wasEntryClick}
scrollToCross={scrollToCross}
allEntries={state.grid.entries}
refPositions={refPositions}
refed={refed}
dimCompleted={true}
active={state.active}
grid={state.grid}
showEntries={false}
conceal={shouldConceal}
header={t`Across`}
entries={acrossEntries}
current={entry?.index}
cross={cross?.index}
dispatch={dispatch}
downsOnly={state.downsOnly && !state.success}
/>
}
right={
<ClueList
wasEntryClick={state.wasEntryClick}
scrollToCross={scrollToCross}
allEntries={state.grid.entries}
refPositions={refPositions}
refed={refed}
dimCompleted={true}
active={state.active}
grid={state.grid}
showEntries={false}
conceal={shouldConceal}
header={t`Down`}
entries={downEntries}
current={entry?.index}
cross={cross?.index}
dispatch={dispatch}
downsOnly={state.downsOnly && !state.success}
/>
}
/>
);
}
const checkRevealMenus = useMemo(
() => (
<>
<TopBarDropDown icon={<FaEye />} text={t`Reveal`}>
{() => (
<>
<TopBarDropDownLink
icon={<RevealSquare />}
text={t`Reveal Square`}
onClick={() => {
const ca: CheatAction = {
type: 'CHEAT',
unit: CheatUnit.Square,
isReveal: true,
};
dispatch(ca);
}}
/>
<TopBarDropDownLink
icon={<RevealEntry />}
text={t`Reveal Word`}
onClick={() => {
const ca: CheatAction = {
type: 'CHEAT',
unit: CheatUnit.Entry,
isReveal: true,
};
dispatch(ca);
}}
/>
<TopBarDropDownLink
icon={<RevealPuzzle />}
text={t`Reveal Puzzle`}
onClick={() => {
const ca: CheatAction = {
type: 'CHEAT',
unit: CheatUnit.Puzzle,
isReveal: true,
};
dispatch(ca);
}}
/>
</>
)}
</TopBarDropDown>
{!state.autocheck ? (
<TopBarDropDown icon={<FaCheck />} text={t`Check`}>
{() => (
<>
<TopBarDropDownLink
icon={<FaCheckSquare />}
text={t`Autocheck`}
onClick={() => {
const action: ToggleAutocheckAction = {
type: 'TOGGLEAUTOCHECK',
};
dispatch(action);
}}
/>
<TopBarDropDownLink
icon={<CheckSquare />}
text={t`Check Square`}
onClick={() => {
const ca: CheatAction = {
type: 'CHEAT',
unit: CheatUnit.Square,
};
dispatch(ca);
}}
/>
<TopBarDropDownLink
icon={<CheckEntry />}
text={t`Check Word`}
onClick={() => {
const ca: CheatAction = {
type: 'CHEAT',
unit: CheatUnit.Entry,
};
dispatch(ca);
}}
/>
<TopBarDropDownLink
icon={<CheckPuzzle />}
text={t`Check Puzzle`}
onClick={() => {
const ca: CheatAction = {
type: 'CHEAT',
unit: CheatUnit.Puzzle,
};
dispatch(ca);
}}
/>
</>
)}
</TopBarDropDown>
) : (
<TopBarLink
icon={<FaCheckSquare />}
text={t`Autochecking`}
onClick={() => {
const action: ToggleAutocheckAction = { type: 'TOGGLEAUTOCHECK' };
dispatch(action);
}}
/>
)}
</>
),
[state.autocheck]
);
const moreMenu = useMemo(
() => (
<>
<TopBarDropDown icon={<FaEllipsisH />} text={t`More`}>
{() => (
<>
{!state.success ? (
<TopBarDropDownLink
icon={<Rebus />}
text={t`Enter Rebus`}
shortcutHint={<EscapeKey />}
onClick={() => {
const kpa: KeypressAction = {
type: 'KEYPRESS',
key: { k: KeyK.Escape },
};
dispatch(kpa);
}}
/>
) : (
''
)}
{muted ? (
<TopBarDropDownLink
icon={<FaVolumeUp />}
text={t`Unmute`}
onClick={() => setMuted(false)}
/>
) : (
<TopBarDropDownLink
icon={<FaVolumeMute />}
text={t`Mute`}
onClick={() => setMuted(true)}
/>
)}
<TopBarDropDownLink
icon={<FaKeyboard />}
text={t`Toggle Keyboard`}
onClick={() => setToggleKeyboard(!toggleKeyboard)}
/>
{props.isAdmin ? (
<>
<TopBarDropDownLink
icon={<FaGlasses />}
text="Moderate"
onClick={() => dispatch({ type: 'TOGGLEMODERATING' })}
/>
<TopBarDropDownLinkA
href="/admin"
icon={<FaUserLock />}
text="Admin"
/>
</>
) : (
''
)}
{props.isAdmin || props.user?.uid === puzzle.authorId ? (
<>
<TopBarDropDownLinkA
href={`/stats/${puzzle.id}`}
icon={<IoMdStats />}
text={t`Stats`}
/>
<TopBarDropDownLinkA
href={`/edit/${puzzle.id}`}
icon={<FaEdit />}
text={t`Edit`}
/>
{!isEmbed ? (
<TopBarDropDownLink
icon={<ImEmbed />}
text={t`Embed`}
onClick={() => dispatch({ type: 'TOGGLEEMBEDOVERLAY' })}
/>
) : (
''
)}
</>
) : (
''
)}
<TopBarDropDownLinkSimpleA
href={'/api/pdf/' + puzzle.id}
icon={<FaPrint />}
text={t`Print Puzzle`}
/>
{puzzle.hBars.length || puzzle.vBars.length ? (
''
) : (
<TopBarDropDownLinkSimpleA
href={'/api/puz/' + puzzle.id}
icon={<FaRegFile />}
text={t`Download .puz File`}
/>
)}
<TopBarDropDownLinkA
href="/account"
icon={<FaUser />}
text={t`Account / Settings`}
/>
<TopBarDropDownLinkA
href="/construct"
icon={<FaHammer />}
text={t`Construct a Puzzle`}
/>
</>
)}
</TopBarDropDown>
</>
),
[
muted,
props.isAdmin,
props.user?.uid,
puzzle,
setMuted,
state.success,
toggleKeyboard,
setToggleKeyboard,
isEmbed,
]
);
const description = puzzle.blogPost
? puzzle.blogPost.slice(0, 160) + '...'
: puzzle.clues.map(getClueText).slice(0, 10).join('; ');
const locale = router.locale || 'en';
return (
<>
<Global
styles={FULLSCREEN_CSS}
/>
<Head>
<title>{puzzle.title} | Crosshare crossword puzzle</title>
<I18nTags
locale={locale}
canonicalPath={`/crosswords/${puzzle.id}/${slugify(puzzle.title)}`}
/>
<meta key="og:title" property="og:title" content={puzzle.title} />
<meta
key="og:description"
property="og:description"
content={description}
/>
<meta
key="og:image"
property="og:image"
content={'https://crosshare.org/api/ogimage/' + puzzle.id}
/>
<meta key="og:image:width" property="og:image:width" content="1200" />
<meta key="og:image:height" property="og:image:height" content="630" />
<meta
key="og:image:alt"
property="og:image:alt"
content="An image of the puzzle grid"
/>
<meta key="description" name="description" content={description} />
</Head>
<div
css={{
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<div css={{ flex: 'none' }}>
<TopBar title={puzzle.title}>
{!loadingPlayState ? (
!state.success ? (
<>
<TopBarLink
icon={<FaPause />}
hoverText={t`Pause Game`}
text={timeString(state.displaySeconds, true)}
onClick={() => {
dispatch({ type: 'PAUSEACTION' });
writePlayToDBIfNeeded();
}}
keepText={true}
/>
<TopBarLink
icon={state.clueView ? <SpinnerFinished /> : <FaListOl />}
text={state.clueView ? t`Grid` : t`Clues`}
onClick={() => {
const a: ToggleClueViewAction = {
type: 'TOGGLECLUEVIEW',
};
dispatch(a);
}}
/>
{checkRevealMenus}
{moreMenu}
</>
) : (
<>
<TopBarLink
icon={<FaComment />}
text={
puzzle.contestAnswers?.length
? !isMetaSolution(
state.contestSubmission,
puzzle.contestAnswers
) && !state.contestRevealed
? t`Contest Prompt / Submission`
: t`Comments / Leaderboard`
: t`Show Comments`
}
onClick={() => dispatch({ type: 'UNDISMISSSUCCESS' })}
/>
{moreMenu}
</>
)
) : (
moreMenu
)}
</TopBar>
</div>
{state.filled && !state.success && !state.dismissedKeepTrying ? (
<KeepTryingOverlay dispatch={dispatch} />
) : (
''
)}
{state.success && !state.dismissedSuccess ? (
<PuzzleOverlay
{...overlayBaseProps}
overlayType={OverlayType.Success}
contestSubmission={state.contestSubmission}
contestHasPrize={puzzle.contestHasPrize}
contestRevealed={state.contestRevealed}
contestRevealDelay={puzzle.contestRevealDelay}
/>
) : (
''
)}
{state.moderating ? (
<ModeratingOverlay puzzle={puzzle} dispatch={dispatch} />
) : (
''
)}
{state.showingEmbedOverlay && props.user ? (
<EmbedOverlay user={props.user} puzzle={puzzle} dispatch={dispatch} />
) : (
''
)}
{state.currentTimeWindowStart === 0 &&
!state.success &&
!(state.filled && !state.dismissedKeepTrying) ? (
state.bankedSeconds === 0 ? (
<PuzzleOverlay
{...overlayBaseProps}
overlayType={OverlayType.BeginPause}
dismissMessage={t`Begin Puzzle`}
message={t`Ready to get started?`}
loadingPlayState={loadingPlayState || !state.loadedPlayState}
/>
) : (
<PuzzleOverlay
{...overlayBaseProps}
overlayType={OverlayType.BeginPause}
dismissMessage={t`Resume`}
message={t`Your puzzle is paused`}
loadingPlayState={loadingPlayState || !state.loadedPlayState}
/>
)
) : (
''
)}
<div
css={{
flex: '1 1 auto',
overflow: 'scroll',
scrollbarWidth: 'none',
position: 'relative',
}}
>
{puzzleView}
</div>
<div css={{ flex: 'none', width: '100%' }}>
<Keyboard
toggleKeyboard={toggleKeyboard}
keyboardHandler={keyboardHandler}
muted={muted}
showExtraKeyLayout={state.showExtraKeyLayout}
includeBlockKey={false}
/>
</div>
</div>
</>
);
}