Skip to content

Commit

Permalink
got slideshow working
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisBrownie55 committed Dec 27, 2020
1 parent 7c75c7b commit 13353a7
Show file tree
Hide file tree
Showing 17 changed files with 547 additions and 328 deletions.
2 changes: 1 addition & 1 deletion .eslintcache

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .idea/codestream.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/components/CharacterCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function CharacterCard({character}) {
const imageResource = useMemo(() => {
if (character.files.length > 0) return createResource(fetchImageURL(uid, character.files[0]))
return null
}, [character.files])
}, [character.files, uid])

return (
<figure className="CharacterCard">
Expand Down
8 changes: 4 additions & 4 deletions src/components/auto-expanding-textarea.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {useEffect, useRef} from 'react'
import {emptyObject} from '../shared/empty'

function autoExpand(element, lineHeight) {
element.style.overflow = 'unset'
Expand All @@ -17,7 +18,7 @@ function autoExpand(element, lineHeight) {
element.style.overflow = 'hidden'
}

function configureAutoExpantion(element, lineHeight) {
function configureAutoExpansion(element, lineHeight) {
// save textarea value for later
const savedValue = element.value

Expand All @@ -29,18 +30,17 @@ function configureAutoExpantion(element, lineHeight) {
element.value = savedValue
autoExpand(element, lineHeight)
}

export function AutoExpandingTextarea({
fontSize = 16,
lineHeight = fontSize,
minimumRows,
style = {},
style = emptyObject,
onChange,
...props
}) {
const ref = useRef(null)
useEffect(() => {
configureAutoExpantion(ref.current, lineHeight)
configureAutoExpansion(ref.current, lineHeight)
}, [ref, lineHeight])

function handleChange(event) {
Expand Down
4 changes: 2 additions & 2 deletions src/components/center.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Stack} from '@fluentui/react'
import {emptyObject} from '../shared/empty'

const empty = {}
export function Center({style = empty, ...props}) {
export function Center({style = emptyObject, ...props}) {
return (
<Stack
verticalAlign="center"
Expand Down
82 changes: 78 additions & 4 deletions src/components/character-parts.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import {memo, Suspense} from 'react'
import {memo, Suspense, useState} from 'react'

import xss from 'xss'
import marked from 'marked'
import {motion} from 'framer-motion'
import {Spinner} from '@fluentui/react'
import {Label, Spinner} from '@fluentui/react'

import {Center} from './center.js'
import {colors} from '../shared/theme.js'
import {artworkWrapperStyles} from './slideshow-parts.js'
import {FadeLayout} from './page-transitions'
import {AutoExpandingTextarea} from './auto-expanding-textarea'
import {debounce} from 'mini-debounce'
import {useId} from '@uifabric/react-hooks'

export const CharacterStory = memo(({story}) => (
<div className="Character__story" dangerouslySetInnerHTML={{__html: xss(marked(story))}} />
))

const empty = {}
/**
* A component to facilitate in reduction of repeating layout code for pages that display character info.
*
* @param {{
* mode: 'display',
Expand Down Expand Up @@ -103,3 +105,75 @@ export function CharacterLayout({slideshow, name, story, actions, mode, onSubmit

return wrapped
}

export function CharacterInput({className, multiline, label, ...props}) {
const [status, setStatus] = useState('idle')
const focusHandlers = {
onFocus() {
setStatus('focus')
},
onBlur() {
setStatus('idle')
},
}

return (
<div className={'CharacterInput ' + (className ?? '')}>
{label && (
<Label htmlFor={props.id} style={{textDecoration: status === 'focus' ? 'underline' : 'none'}}>
{label}
</Label>
)}
{multiline ? (
<AutoExpandingTextarea fontSize={20} lineHeight={25} minimumRows={5} {...props} {...focusHandlers} />
) : (
<input {...props} {...focusHandlers} />
)}
</div>
)
}

const debouncedSetItem = debounce((key, value) => localStorage.setItem(key, value), 100)
export function createValueStorer(key, setter) {
return event => {
const {value} = event.target

setter(value)
debouncedSetItem(key, value)
}
}

export function CharacterNameInput({storageKey, setter, value}) {
const id = useId('name')
return (
<CharacterInput
id={id}
label="Name"
placeholder="Lumiére"
onChange={createValueStorer(storageKey, setter)}
value={value}
required
/>
)
}
export function CharacterStoryInput({storageKey, setter, value}) {
const id = useId('story')
return (
<CharacterInput
id={id}
label="Character Story"
placeholder="Tell your characters story and explain their background…"
className="CharacterStoryInput"
onChange={createValueStorer(storageKey, setter)}
value={value}
multiline
required
/>
)
}

export function clearStorageKeys(...keys) {
for (const key of keys) {
localStorage.setItem(key, '')
}
}
189 changes: 189 additions & 0 deletions src/components/slideshow-parts.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import {ActionButton} from './action-button'
import {colors} from '../shared/theme'
import {useMachine} from '@xstate/react'
import {uploadSlideshowMachine} from '../shared/machines'
import {useDropzone} from 'react-dropzone'
import {FontIcon, Text} from '@fluentui/react'
import {useId} from '@uifabric/react-hooks'
import {emptyArray} from '../shared/empty'
import {Center} from './center'

export const artworkWrapperStyles = {
position: 'relative',
Expand Down Expand Up @@ -47,6 +54,7 @@ const previousButtonStyles = {
height: 46,
borderRadius: 0,
borderBottomRightRadius: 8,
zIndex: 2,
}
export function PreviousButton(props) {
return (
Expand All @@ -61,3 +69,184 @@ export function PreviousButton(props) {
/>
)
}

export const artistSVGStyles = {
width: 298,
height: 220,
marginBottom: 5,
}
export const removeButtonStyles = {
position: 'absolute',
top: 0,
right: 0,
width: 46,
height: 46,
borderRadius: 0,
borderBottomLeftRadius: 8,
}
export const dropZoneMessages = {
accepted: 'File is valid, drop it to upload it!',
rejected: 'This file is not a valid photo.',
idle: 'Drop your character photos above.',
}

/**
* Handles Suspense part of rendering a pre-existing photo for the character editor
* @param {ResourceReader<string>} resource
* @returns {JSX.Element}
* @constructor
*/
function PreExistingPhoto({resource}) {
return <img src={resource.read()} alt="" style={artworkStyles} />
}
/**
*
* @param {[{id: string, resource: ResourceReader<string>, scheduledForRemoval: boolean}]} preExistingPhotos
* @returns {{preExistingPhotos: ([]|*), fileRejections: FileRejection[], isFileDialogActive: boolean, dropMessage: JSX.Element, inputRef: React.RefObject<HTMLInputElement>, isDragReject: boolean, isFocused: boolean, getInputProps(props?: DropzoneInputProps): DropzoneInputProps, acceptedFiles: File[], slideshowSection: JSX.Element, draggedFiles: File[], isDragAccept: boolean, rootRef: React.RefObject<HTMLElement>, getRootProps(props?: DropzoneRootProps): DropzoneRootProps, dropID: string, isDragActive: boolean, files, open(): void}}
*/
export function useSlideshow(preExistingPhotos = emptyArray) {
const [state, send] = useMachine(
uploadSlideshowMachine.withContext({
...uploadSlideshowMachine.context,
preExistingPhotos,
}),
)
const dropzone = useDropzone({
accept: 'image/*',
onDropAccepted(acceptedFiles) {
// handle cancelled file operations
const filesWithPreview = acceptedFiles.map(file => {
file.preview = URL.createObjectURL(file)
return file
})
send('ADDED_PHOTOS', {data: filesWithPreview})
},
})

const previousButton = <PreviousButton onClick={() => send('PREVIOUS')} />
const nextButton = <NextButton onClick={() => send('NEXT')} />

let slideshowSection
if (state.matches('noPhotos')) {
slideshowSection = (
<div
style={{
position: 'relative',
display: 'flex',
justifyContent: 'center',
width: 'calc(100% - 31px * 2)',
padding: '45px 31px 0',
}}
>
<img
src="/artist-and-art.svg"
alt="Artist looking at her art"
className="drop-target"
{...dropzone.getRootProps({style: artistSVGStyles})}
/>
</div>
)
} else if (state.matches('newPhotosPage')) {
// TODO: Make drop target accessible
slideshowSection = (
<div style={artworkWrapperStyles}>
{previousButton}
<FontIcon
iconName="Add"
className="drop-target"
{...dropzone.getRootProps({style: {color: colors.realPink, fontSize: 118}})}
/>
</div>
)
} else if (state.matches('newPhotos')) {
slideshowSection = (
<div style={artworkWrapperStyles}>
{state.context.currentPage > 0 || state.context.preExistingPhotos.length > 0 ? previousButton : null}
<img src={state.context.files[state.context.currentPage].preview} alt="" style={artworkStyles} />
<ActionButton
iconName="Delete"
variant="round-orange"
type="button"
title="Remove photo"
aria-label="Remove photo"
onClick={() => send('REMOVED_PHOTO')}
style={removeButtonStyles}
/>
{nextButton}
</div>
)
} else if (state.matches('preExistingPhotos')) {
const currentPhoto = state.context.preExistingPhotos[state.context.currentPage]
slideshowSection = (
<div style={artworkWrapperStyles}>
{state.context.currentPage > 0 ? previousButton : null}
<PreExistingPhoto resource={currentPhoto.resource} />
{currentPhoto.scheduledForRemoval ? (
<Center style={{position: 'absolute', background: 'hsl(0, 0%, 0%, 0.5)'}}>
<p style={{color: colors.light, fontSize: 24, letterSpacing: 1}}>Scheduled for removal</p>
</Center>
) : null}
{!currentPhoto.scheduledForRemoval ? (
<ActionButton
key="delete"
iconName="Delete"
variant="round-orange"
type="button"
title="Schedule for removal"
aria-label="Schedule for removal"
onClick={() => send('SCHEDULE_FOR_REMOVAL')}
style={removeButtonStyles}
/>
) : (
<ActionButton
key="undo"
iconName="Undo"
variant="round-orange"
type="button"
title="Cancel removal"
aria-label="Cancel removal"
onClick={() => send('CANCEL_REMOVAL')}
style={removeButtonStyles}
/>
)
}
{nextButton}
</div>
)
}

let dropMessageContent
if (dropzone.isDragReject) dropMessageContent = dropZoneMessages.rejected
else if (dropzone.isDragAccept) dropMessageContent = dropZoneMessages.accepted
else dropMessageContent = dropZoneMessages.idle

const dropID = useId('drop')
let dropMessage
/* TODO: align center on Desktop sizes */
if (state.matches('photos'))
dropMessage = (
<Text variant="higherTitle" style={{textAlign: 'right', padding: '0 31px', marginTop: 10}}>
Edit your character photos above.
</Text>
)
else
dropMessage = (
<Text
as="label"
htmlFor={dropID}
variant="higherTitle"
style={{textAlign: 'right', padding: '0 31px', marginTop: 10}}
>
{dropMessageContent}
</Text>
)

return {
...dropzone,
dropID,
dropMessage,
slideshowSection,
files: state.context.files,
preExistingPhotos: state.context.preExistingPhotos,
}
}
Loading

0 comments on commit 13353a7

Please sign in to comment.