diff --git a/.eslintcache b/.eslintcache index 1fe8f2d..d159ce9 100644 --- a/.eslintcache +++ b/.eslintcache @@ -1 +1 @@ -[{"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\index.js":"1","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\serviceWorker.js":"2","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\landing.js":"3","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\character.js":"4","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\home.js":"5","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\404.js":"6","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\transition-router.js":"7","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\authentication.js":"8","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\error-boundary.js":"9","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\center.js":"10","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\firebase.js":"11","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\theme.js":"12","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\new-character.js":"13","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\action-button.js":"14","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\CharacterCard.js":"15","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\profile-menu.js":"16","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\notifications.js":"17","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\auto-expanding-textarea.js":"18","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\saving-dialog.js":"19","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\machines.js":"20","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\profile-photo.js":"21","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\helpers.js":"22","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\errors.js":"23","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\resources.js":"24","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\slideshow-parts.js":"25","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\character-parts.js":"26","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\page-transitions.js":"27","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\edit-character.js":"28"},{"size":2650,"mtime":1608860983300,"results":"29","hashOfConfig":"30"},{"size":4602,"mtime":1594516065119,"results":"31","hashOfConfig":"30"},{"size":466,"mtime":1608868884660,"results":"32","hashOfConfig":"30"},{"size":3702,"mtime":1608923613576,"results":"33","hashOfConfig":"30"},{"size":4478,"mtime":1608842255022,"results":"34","hashOfConfig":"30"},{"size":840,"mtime":1608860849174,"results":"35","hashOfConfig":"30"},{"size":505,"mtime":1608839435364,"results":"36","hashOfConfig":"30"},{"size":4870,"mtime":1608759910815,"results":"37","hashOfConfig":"30"},{"size":893,"mtime":1594516064792,"results":"38","hashOfConfig":"30"},{"size":258,"mtime":1608660663637,"results":"39","hashOfConfig":"30"},{"size":4370,"mtime":1608868016667,"results":"40","hashOfConfig":"30"},{"size":1862,"mtime":1608759848430,"results":"41","hashOfConfig":"30"},{"size":7031,"mtime":1608842294940,"results":"42","hashOfConfig":"30"},{"size":683,"mtime":1608839405773,"results":"43","hashOfConfig":"30"},{"size":1573,"mtime":1608868016718,"results":"44","hashOfConfig":"30"},{"size":5359,"mtime":1608759910823,"results":"45","hashOfConfig":"30"},{"size":182,"mtime":1607895056959,"results":"46","hashOfConfig":"30"},{"size":1532,"mtime":1607895056909,"results":"47","hashOfConfig":"30"},{"size":1637,"mtime":1607895057050,"results":"48","hashOfConfig":"30"},{"size":7442,"mtime":1608918677663,"results":"49","hashOfConfig":"30"},{"size":1922,"mtime":1608384645639,"results":"50","hashOfConfig":"30"},{"size":1565,"mtime":1608660175185,"results":"51","hashOfConfig":"30"},{"size":894,"mtime":1595961850556,"results":"52","hashOfConfig":"30"},{"size":2908,"mtime":1608868016698,"results":"53","hashOfConfig":"30"},{"size":1116,"mtime":1608686631824,"results":"54","hashOfConfig":"30"},{"size":2351,"mtime":1608842254750,"results":"55","hashOfConfig":"30"},{"size":183,"mtime":1608842254808,"results":"56","hashOfConfig":"30"},{"size":989,"mtime":1608861337469,"results":"57","hashOfConfig":"30"},{"filePath":"58","messages":"59","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},"1jdzda6",{"filePath":"61","messages":"62","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"63","messages":"64","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"65","messages":"66","errorCount":0,"warningCount":6,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"67","messages":"68","errorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"69","usedDeprecatedRules":"60"},{"filePath":"70","messages":"71","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"72","messages":"73","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"74","messages":"75","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"76","messages":"77","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"78","messages":"79","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"80","messages":"81","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"82","messages":"83","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"84","messages":"85","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"86","usedDeprecatedRules":"60"},{"filePath":"87","messages":"88","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"89","usedDeprecatedRules":"60"},{"filePath":"90","messages":"91","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"92","usedDeprecatedRules":"60"},{"filePath":"93","messages":"94","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"95","messages":"96","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"97","messages":"98","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"99","messages":"100","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"101","messages":"102","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"103","messages":"104","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"105","messages":"106","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"107","messages":"108","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"109","messages":"110","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"111","messages":"112","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"113","messages":"114","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"115","usedDeprecatedRules":"60"},{"filePath":"116","messages":"117","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"60"},{"filePath":"118","messages":"119","errorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"120","usedDeprecatedRules":"60"},"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\index.js",[],["121","122"],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\serviceWorker.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\landing.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\character.js",["123","124","125","126","127","128"],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\home.js",["129","130","131"],"import {Suspense, unstable_SuspenseList as SuspenseList, useEffect, useState} from 'react'\n\nimport {useHistory} from 'react-router-dom'\nimport {motion} from 'framer-motion'\nimport {Text, Spinner} from '@fluentui/react'\n\nimport {Center} from '../components/center'\nimport {ActionButton} from '../components/action-button'\nimport {CharacterCard} from '../components/CharacterCard'\nimport {ProfileMenu, ProfileMenuItem} from '../components/profile-menu'\n\nimport {colors} from '../shared/theme'\nimport {auth, firestore, useCharacters, useUser} from '../shared/firebase.js'\nimport {createDocumentResource, useDocumentResource} from '../shared/resources.js'\n\nimport '../styles/profile-menu.css'\nimport 'wicg-inert'\n\n/**\n * @typedef {{\n * characterID: string,\n * files: [string],\n * name: string,\n * story: string\n * }} Character\n * @typedef {{characters: [Character]}} UserData\n */\n\n/**\n * Renders a list of ``'s from a document and a resource.\n * @param {{\n * \tdocumentRef: DocumentReference,\n * \tresource: ResourceReader\n * }} props\n * @returns {JSX.Element|[JSX.Element]}\n * @constructor\n */\nfunction CharacterCardList({documentRef, resource}) {\n\tconst characters = useCharacters()\n\n\t// Render all the Character Cards if there are any.\n\tif (characters.length > 0) {\n\t\tconst listOfCharacters = characters.map((character, index) => )\n\t\treturn (\n\t\t\t\n\t\t\t\t{listOfCharacters}\n\t\t\t\n\t\t)\n\t}\n\n\t// Otherwise, inform the user of how to create a character.\n\t// TODO: Add alt for pride-drawing.svg\n\treturn (\n\t\t\n\t\t\t\"\"\n\t\t\t\n\t\t\t\tTo get started, add some characters with the \"New\" button.\n\t\t\t\n\t\t\n\t)\n}\n\n/**\n * Renders the Home page's header with the profile image. It handles scroll animations.\n * @returns {JSX.Element}\n * @constructor\n */\nfunction ProfileHeader() {\n\tconst user = useUser()\n\tconst [status, setStatus] = useState('flat')\n\n\tuseEffect(() => {\n\t\tfunction handler() {\n\t\t\tsetStatus(prevStatus => {\n\t\t\t\tif (window.scrollY > 0) return 'floating'\n\t\t\t\treturn 'flat'\n\t\t\t})\n\t\t}\n\n\t\twindow.addEventListener('scroll', handler)\n\t\treturn () => {\n\t\t\twindow.removeEventListener('scroll', handler)\n\t\t}\n\t}, [])\n\n\tconst history = useHistory()\n\tfunction signOut() {\n\t\tauth.signOut().then(() => {\n\t\t\thistory.push('/login')\n\t\t})\n\t}\n\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t💛 Art Hub\n\t\t\t\n\t\t\t\n\t\t\t\tShare\n\t\t\t\tSettings\n\t\t\t\tHelp\n\t\t\t\tSign Out\n\t\t\t\n\t\t\n\t)\n}\n\n/**\n * Home page\n * @returns {JSX.Element}\n * @constructor\n */\nexport function Home() {\n\tconst history = useHistory()\n\tfunction openNewCharacterPage() {\n\t\thistory.push('/new-character')\n\t}\n\n\treturn (\n\t\t\n\t\t\t\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\tNew\n\t\t\t\t\n\t\t\t
\n\t\t\n\t)\n}\n","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\404.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\transition-router.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\authentication.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\error-boundary.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\center.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\firebase.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\theme.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\new-character.js",["132"],"import {useEffect, useState} from 'react'\n\nimport {motion} from 'framer-motion'\nimport {debounce} from 'mini-debounce'\nimport {useMachine} from '@xstate/react'\nimport {useHistory} from 'react-router-dom'\nimport {useId} from '@uifabric/react-hooks'\nimport {Text, Label, FontIcon} from '@fluentui/react'\nimport {useDropzone} from 'react-dropzone'\n\nimport {ActionButton} from '../components/action-button.js'\nimport {SavingDialog} from '../components/saving-dialog.js'\nimport {useUser} from '../shared/firebase.js'\nimport {newCharacterMachine, uploadSlideshowMachine} from '../shared/machines.js'\nimport {AutoExpandingTextarea} from '../components/auto-expanding-textarea.js'\nimport {NextButton, artworkStyles, artworkWrapperStyles, PreviousButton} from '../components/slideshow-parts.js'\nimport {CharacterLayout} from '../components/character-parts.js'\n\nimport {colors} from '../shared/theme.js'\nimport '../styles/new-character.css'\n\nconst artistSVGStyles = {\n\twidth: 298,\n\theight: 220,\n\tmarginBottom: 5,\n}\nconst removeButtonStyles = {\n\tposition: 'absolute',\n\ttop: 0,\n\tright: 0,\n\twidth: 46,\n\theight: 46,\n\tborderRadius: 0,\n\tborderBottomLeftRadius: 8,\n}\n\nfunction NewCharacterInput({className, multiline, label, ...props}) {\n\tconst [status, setStatus] = useState('idle')\n\tconst focusHandlers = {\n\t\tonFocus() {\n\t\t\tsetStatus('focus')\n\t\t},\n\t\tonBlur() {\n\t\t\tsetStatus('idle')\n\t\t},\n\t}\n\treturn (\n\t\t
\n\t\t\t{label && (\n\t\t\t\t\n\t\t\t)}\n\t\t\t{multiline ? (\n\t\t\t\t\n\t\t\t) : (\n\t\t\t\t\n\t\t\t)}\n\t\t
\n\t)\n}\n\nfunction clearStorage() {\n\tlocalStorage.setItem('character-name', '')\n\tlocalStorage.setItem('character-story', '')\n}\n\nconst debouncedSetItem = debounce((key, value) => localStorage.setItem(key, value), 100)\nfunction createValueStorer(key, setter) {\n\treturn event => {\n\t\tconst {value} = event.target\n\n\t\tsetter(value)\n\t\tdebouncedSetItem(key, value)\n\t}\n}\n\nconst dropZoneMessages = {\n\taccepted: 'File is valid, drop it to upload it!',\n\trejected: 'This file is not a valid photo.',\n\tidle: 'Drop your character photos above.',\n}\n\nfunction useSlideshow() {\n\tconst [state, send] = useMachine(uploadSlideshowMachine)\n\tconst dropzone = useDropzone({\n\t\taccept: 'image/*',\n\t\tonDropAccepted(acceptedFiles) {\n\t\t\t// handle cancelled file operations\n\t\t\tconst filesWithPreview = acceptedFiles.map(file => {\n\t\t\t\tfile.preview = URL.createObjectURL(file)\n\t\t\t\treturn file\n\t\t\t})\n\t\t\tsend('ADDED_PHOTOS', {data: filesWithPreview})\n\t\t},\n\t})\n\n\tconst previousButton = send('PREVIOUS')} />\n\tconst nextButton = send('NEXT')} />\n\n\tlet slideshowSection\n\tif (state.matches('noPhotos'))\n\t\tslideshowSection = (\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t)\n\telse if (state.matches('newPhoto'))\n\t\t// TODO: Make drop target accessible\n\t\tslideshowSection = (\n\t\t\t
\n\t\t\t\t{previousButton}\n\t\t\t\t\n\t\t\t
\n\t\t)\n\telse if (state.matches('photos'))\n\t\tslideshowSection = (\n\t\t\t
\n\t\t\t\t{state.context.currentPage > 0 && previousButton}\n\t\t\t\t\"\"\n\t\t\t\t send('REMOVED_PHOTO')}\n\t\t\t\t\tstyle={removeButtonStyles}\n\t\t\t\t/>\n\t\t\t\t{nextButton}\n\t\t\t
\n\t\t)\n\n\tlet dropMessageContent\n\tif (dropzone.isDragReject) dropMessageContent = dropZoneMessages.rejected\n\telse if (dropzone.isDragAccept) dropMessageContent = dropZoneMessages.accepted\n\telse dropMessageContent = dropZoneMessages.idle\n\n\tconst dropID = useId('drop')\n\tlet dropMessage\n\t/* TODO: align center on Desktop sizes */\n\tif (state.matches('photos'))\n\t\tdropMessage = (\n\t\t\t\n\t\t\t\tEdit your character photos above.\n\t\t\t\n\t\t)\n\telse\n\t\tdropMessage = (\n\t\t\t\n\t\t\t\t{dropMessageContent}\n\t\t\t\n\t\t)\n\n\treturn {\n\t\t...dropzone,\n\t\tdropID,\n\t\tdropMessage,\n\t\tslideshowSection,\n\t\tfiles: state.context.files,\n\t}\n}\n\nexport function NewCharacter() {\n\tconst nameFieldID = useId('name')\n\tconst storyFieldID = useId('character-story')\n\n\tconst user = useUser()\n\tconst [name, setName] = useState(localStorage.getItem('character-name') ?? '')\n\tconst [story, setStory] = useState(localStorage.getItem('character-story') ?? '')\n\tconst {getInputProps, slideshowSection, dropMessage, dropID, files} = useSlideshow()\n\n\tconst history = useHistory()\n\tfunction cancel() {\n\t\tclearStorage()\n\t\thistory.replace('/')\n\t}\n\n\tconst [saveState, send] = useMachine(newCharacterMachine)\n\tfunction save(event) {\n\t\tevent.preventDefault()\n\t\tsend('SAVE', {name, story, files, uid: user.uid})\n\t}\n\n\tuseEffect(() => {\n\t\tif (saveState.matches({finished: 'success'})) {\n\t\t\tclearStorage()\n\t\t\thistory.push(`/character/${saveState.context.characterID}`)\n\t\t}\n\t}, [history, saveState])\n\n\treturn (\n\t\t\n\t\t\t\t\t{slideshowSection}\n\t\t\t\t\t\n\t\t\t\t\t{dropMessage}\n\t\t\t\t\n\t\t\t}\n\t\t\tname={\n\t\t\t\t\n\t\t\t}\n\t\t\tstory={\n\t\t\t\t\n\t\t\t}\n\t\t\tactions={\n\t\t\t\t<>\n\t\t\t\t\t\n\t\t\t\t\t\tCancel\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tSave\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t}\n\t\t>\n\t\t\t\n\t\t\n\t)\n}\n","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\action-button.js",["133","134"],"import {FontIcon, Text} from '@fluentui/react'\nimport {motion} from 'framer-motion'\nimport {colors} from '../shared/theme'\nimport '../styles/action-button.css'\n\n/*\n * @param {{ variant: 'round' | 'flat' | 'bold-orange' | 'bold-pink' | 'danger', iconName: string }} options\n */\nexport function ActionButton({variant, iconName, children, className, ...props}) {\n\treturn (\n\t\t\n\t\t\t\n\t\t\t{children && (\n\t\t\t\t\n\t\t\t\t\t{children}\n\t\t\t\t\n\t\t\t)}\n\t\t\n\t)\n}\n","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\CharacterCard.js",["135"],"import {Suspense, useMemo} from 'react'\nimport {Link as RouterLink} from 'react-router-dom'\n\nimport {useUser} from '../shared/firebase.js'\nimport {createResource, fetchImageURL} from '../shared/resources.js'\n\nimport '../styles/character-card.css'\nimport {Center} from './center'\nimport {Spinner} from '@fluentui/react'\n\n/**\n * @param {{resource: ResourceReader, alt: string}} props\n * @constructor\n */\nfunction CharacterCardArt({resource, alt}) {\n\tconst imageURL = resource.read()\n\treturn {alt}\n}\n\n/**\n *\n * @param {{character: Character}} props\n * @returns {JSX.Element}\n * @constructor\n */\nexport function CharacterCard({character}) {\n\tconst {uid} = useUser()\n\tconst imageResource = useMemo(() => {\n\t\tif (character.files.length > 0) return createResource(fetchImageURL(uid, character.files[0]))\n\t\treturn null\n\t}, [character.files])\n\n\treturn (\n\t\t
\n\t\t\t{/*TODO: replace alt with alt from data*/}\n\t\t\t{imageResource ? (\n\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t) : (\n\t\t\t\t{character.name[0]}\n\t\t\t)}\n\n\t\t\t
\n\t\t\t\t

{character.name}

\n\t\t\t\t\n\t\t\t\t\tView Character\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t)\n}\n","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\profile-menu.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\notifications.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\auto-expanding-textarea.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\saving-dialog.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\machines.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\profile-photo.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\helpers.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\errors.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\resources.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\slideshow-parts.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\character-parts.js",["136","137"],"import {memo, Suspense} from 'react'\n\nimport xss from 'xss'\nimport marked from 'marked'\nimport {motion} from 'framer-motion'\nimport {Spinner} from '@fluentui/react'\n\nimport {Center} from './center.js'\nimport {colors} from '../shared/theme.js'\nimport {artworkWrapperStyles} from './slideshow-parts.js'\nimport {FadeLayout} from './page-transitions'\n\nexport const CharacterStory = memo(({story}) => (\n\t
\n))\n\nconst empty = {}\n/**\n *\n * @param {{\n * mode: 'display',\n * slideshow: JSX.Element,\n * name: JSX.Element,\n * story: JSX.Element,\n * actions: [JSX.Element],\n * children: any,\n * } | {\n * mode: 'edit',\n * onSubmit: function(React.SyntheticEvent),\n * slideshow: JSX.Element,\n * name: JSX.Element,\n * story: JSX.Element,\n * actions: [JSX.Element],\n * children: any,\n * }} props\n * @constructor\n */\nexport function CharacterLayout({slideshow, name, story, actions, mode, onSubmit, children}) {\n\tconst content = (\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t{slideshow}\n\t\t\t\t\n\n\t\t\t\t
\n\t\t\t\t\t{name}\n\t\t\t\t\t{story}\n\t\t\t\t
\n\t\t\t
\n\t\t\t\n\t\t\t\t{actions}\n\t\t\t\n\t\t\n\t)\n\n\tlet wrapped\n\tif (mode === 'display') {\n\t\twrapped = (\n\t\t\t\n\t\t\t\t{content}\n\t\t\t\t{children}\n\t\t\t\n\t\t)\n\t} else if (mode === 'edit') {\n\t\twrapped = (\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{content}\n\t\t\t\t
\n\t\t\t\t{children}\n\t\t\t
\n\t\t)\n\t} else {\n\t\tthrow new Error(`Invalid mode '${mode}' is not supported.`)\n\t}\n\n\treturn wrapped\n}\n","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\page-transitions.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\edit-character.js",["138","139","140"],"import {useHistory, useParams} from 'react-router-dom'\nimport {CharacterLayout} from '../components/character-parts.js'\nimport {useCharacterWithImages, useUser} from '../shared/firebase.js'\nimport {ActionButton} from '../components/action-button'\n\nfunction clearStorage() {}\n\nexport function EditCharacterPage() {\n\tconst {characterID: id} = useParams()\n\tconst {character, imageResources} = useCharacterWithImages(id)\n\n\tconst history = useHistory()\n\tfunction cancel() {\n\t\tclearStorage()\n\t\thistory.replace(`/character/${id}`)\n\t}\n\n\tfunction saveChanges(event) {\n\t\tevent.preventDefault()\n\t}\n\n\treturn (\n\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tCancel\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tSave\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t}\n\t\t/>\n\t)\n}\n",{"ruleId":"141","replacedBy":"142"},{"ruleId":"143","replacedBy":"144"},{"ruleId":"145","severity":1,"message":"146","line":1,"column":9,"nodeType":"147","messageId":"148","endLine":1,"endColumn":16},{"ruleId":"145","severity":1,"message":"149","line":8,"column":2,"nodeType":"147","messageId":"148","endLine":8,"endColumn":15},{"ruleId":"145","severity":1,"message":"150","line":13,"column":2,"nodeType":"147","messageId":"148","endLine":13,"endColumn":18},{"ruleId":"145","severity":1,"message":"151","line":17,"column":9,"nodeType":"147","messageId":"148","endLine":17,"endColumn":31},{"ruleId":"145","severity":1,"message":"152","line":17,"column":33,"nodeType":"147","messageId":"148","endLine":17,"endColumn":47},{"ruleId":"145","severity":1,"message":"153","line":17,"column":49,"nodeType":"147","messageId":"148","endLine":17,"endColumn":68},{"ruleId":"145","severity":1,"message":"154","line":13,"column":15,"nodeType":"147","messageId":"148","endLine":13,"endColumn":24},{"ruleId":"145","severity":1,"message":"151","line":14,"column":9,"nodeType":"147","messageId":"148","endLine":14,"endColumn":31},{"ruleId":"145","severity":1,"message":"153","line":14,"column":33,"nodeType":"147","messageId":"148","endLine":14,"endColumn":52},{"ruleId":"145","severity":1,"message":"155","line":3,"column":9,"nodeType":"147","messageId":"148","endLine":3,"endColumn":15},{"ruleId":"145","severity":1,"message":"155","line":2,"column":9,"nodeType":"147","messageId":"148","endLine":2,"endColumn":15},{"ruleId":"145","severity":1,"message":"156","line":3,"column":9,"nodeType":"147","messageId":"148","endLine":3,"endColumn":15},{"ruleId":"157","severity":1,"message":"158","line":31,"column":5,"nodeType":"159","endLine":31,"endColumn":22,"suggestions":"160"},{"ruleId":"145","severity":1,"message":"155","line":5,"column":9,"nodeType":"147","messageId":"148","endLine":5,"endColumn":15},{"ruleId":"145","severity":1,"message":"161","line":17,"column":7,"nodeType":"147","messageId":"148","endLine":17,"endColumn":12},{"ruleId":"145","severity":1,"message":"162","line":3,"column":33,"nodeType":"147","messageId":"148","endLine":3,"endColumn":40},{"ruleId":"145","severity":1,"message":"163","line":10,"column":9,"nodeType":"147","messageId":"148","endLine":10,"endColumn":18},{"ruleId":"145","severity":1,"message":"164","line":10,"column":20,"nodeType":"147","messageId":"148","endLine":10,"endColumn":34},"no-native-reassign",["165"],"no-negated-in-lhs",["166"],"no-unused-vars","'useMemo' is defined but never used.","Identifier","unusedVar","'fetchImageURL' is defined but never used.","'withUserResource' is defined but never used.","'createDocumentResource' is defined but never used.","'createResource' is defined but never used.","'useDocumentResource' is defined but never used.","'firestore' is defined but never used.","'motion' is defined but never used.","'colors' is defined but never used.","react-hooks/exhaustive-deps","React Hook useMemo has a missing dependency: 'uid'. Either include it or remove the dependency array.","ArrayExpression",["167"],"'empty' is assigned a value but never used.","'useUser' is defined but never used.","'character' is assigned a value but never used.","'imageResources' is assigned a value but never used.","no-global-assign","no-unsafe-negation",{"desc":"168","fix":"169"},"Update the dependencies array to be: [character.files, uid]",{"range":"170","text":"171"},[883,900],"[character.files, uid]"] \ No newline at end of file +[{"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\index.js":"1","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\serviceWorker.js":"2","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\landing.js":"3","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\character.js":"4","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\home.js":"5","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\404.js":"6","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\transition-router.js":"7","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\authentication.js":"8","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\error-boundary.js":"9","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\center.js":"10","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\firebase.js":"11","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\theme.js":"12","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\new-character.js":"13","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\action-button.js":"14","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\CharacterCard.js":"15","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\profile-menu.js":"16","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\notifications.js":"17","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\auto-expanding-textarea.js":"18","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\saving-dialog.js":"19","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\machines.js":"20","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\profile-photo.js":"21","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\helpers.js":"22","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\errors.js":"23","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\resources.js":"24","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\slideshow-parts.js":"25","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\character-parts.js":"26","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\page-transitions.js":"27","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\edit-character.js":"28","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\empty.js":"29"},{"size":2785,"mtime":1608943371586,"results":"30","hashOfConfig":"31"},{"size":4602,"mtime":1594516065119,"results":"32","hashOfConfig":"31"},{"size":466,"mtime":1608868884660,"results":"33","hashOfConfig":"31"},{"size":3705,"mtime":1608958387878,"results":"34","hashOfConfig":"31"},{"size":4478,"mtime":1608842255022,"results":"35","hashOfConfig":"31"},{"size":840,"mtime":1608860849174,"results":"36","hashOfConfig":"31"},{"size":505,"mtime":1608839435364,"results":"37","hashOfConfig":"31"},{"size":5157,"mtime":1608958387834,"results":"38","hashOfConfig":"31"},{"size":893,"mtime":1594516064792,"results":"39","hashOfConfig":"31"},{"size":291,"mtime":1609016032237,"results":"40","hashOfConfig":"31"},{"size":4510,"mtime":1608958388062,"results":"41","hashOfConfig":"31"},{"size":1862,"mtime":1608759848430,"results":"42","hashOfConfig":"31"},{"size":2314,"mtime":1608943650226,"results":"43","hashOfConfig":"31"},{"size":683,"mtime":1608839405773,"results":"44","hashOfConfig":"31"},{"size":1578,"mtime":1608926099114,"results":"45","hashOfConfig":"31"},{"size":5359,"mtime":1608759910823,"results":"46","hashOfConfig":"31"},{"size":182,"mtime":1607895056959,"results":"47","hashOfConfig":"31"},{"size":1584,"mtime":1609015954337,"results":"48","hashOfConfig":"31"},{"size":1637,"mtime":1607895057050,"results":"49","hashOfConfig":"31"},{"size":10062,"mtime":1609020176636,"results":"50","hashOfConfig":"31"},{"size":1922,"mtime":1608384645639,"results":"51","hashOfConfig":"31"},{"size":1821,"mtime":1608951226050,"results":"52","hashOfConfig":"31"},{"size":894,"mtime":1595961850556,"results":"53","hashOfConfig":"31"},{"size":2908,"mtime":1608996747116,"results":"54","hashOfConfig":"31"},{"size":6891,"mtime":1609027816292,"results":"55","hashOfConfig":"31"},{"size":4199,"mtime":1609016032246,"results":"56","hashOfConfig":"31"},{"size":183,"mtime":1608842254808,"results":"57","hashOfConfig":"31"},{"size":2235,"mtime":1609016159570,"results":"58","hashOfConfig":"31"},{"size":61,"mtime":1609015906081,"results":"59","hashOfConfig":"31"},{"filePath":"60","messages":"61","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1jdzda6",{"filePath":"62","messages":"63","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"65","messages":"66","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"67","messages":"68","errorCount":0,"warningCount":6,"fixableErrorCount":0,"fixableWarningCount":0,"source":"69","usedDeprecatedRules":"64"},{"filePath":"70","messages":"71","errorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"72","usedDeprecatedRules":"64"},{"filePath":"73","messages":"74","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"75","messages":"76","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"77","messages":"78","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"79","messages":"80","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"81","messages":"82","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"83","messages":"84","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"85","messages":"86","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"87","messages":"88","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"89","usedDeprecatedRules":"64"},{"filePath":"90","messages":"91","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"92","usedDeprecatedRules":"64"},{"filePath":"93","messages":"94","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"95","messages":"96","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"97","messages":"98","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"99","messages":"100","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"101","messages":"102","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"103","messages":"104","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"105","messages":"106","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"107","messages":"108","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"109","messages":"110","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"111","messages":"112","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"113","messages":"114","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"115","messages":"116","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"117","usedDeprecatedRules":"64"},{"filePath":"118","messages":"119","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"64"},{"filePath":"120","messages":"121","errorCount":0,"warningCount":5,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"122","messages":"123","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\index.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\serviceWorker.js",[],["124","125"],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\landing.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\character.js",["126","127","128","129","130","131"],"import {useMemo, useState} from 'react'\n\nimport {useMachine} from '@xstate/react'\nimport {useParams, useHistory} from 'react-router-dom'\nimport * as firebase from 'firebase'\n\nimport {\n\tfetchImageURL,\n\tfirestore,\n\tstorage,\n\tuseCharacterWithImages,\n\tuseUser,\n\twithUserResource,\n} from '../shared/firebase.js'\nimport {ActionButton} from '../components/action-button.js'\nimport {plainSlideshowMachine} from '../shared/machines.js'\nimport {createDocumentResource, createResource, useDocumentResource} from '../shared/resources.js'\nimport {artworkStyles, artworkWrapperStyles, NextButton, PreviousButton} from '../components/slideshow-parts.js'\nimport {CharacterStory, CharacterLayout} from '../components/character-parts.js'\n\nimport '../styles/character.css'\nimport {FadeLayout} from '../components/page-transitions'\nimport {Center} from '../components/center'\nimport {Spinner} from '@fluentui/react'\n\nfunction CharacterSlideshow({context, send, resources}) {\n\tif (resources.length === 0) return null\n\n\tconst url = resources[context.currentPage].read()\n\tconst previous = send('PREVIOUS')} />\n\tconst next = send('NEXT')} />\n\n\t// TODO: add real alt from character data\n\treturn (\n\t\t
\n\t\t\t{context.currentPage > 0 ? previous : null}\n\t\t\t\"Artwork\"\n\t\t\t{context.currentPage < context.numberOfImages - 1 ? next : null}\n\t\t
\n\t)\n}\n\n/**\n * @param {{userRef: DocumentRef, resource: ResourceReader}} props\n * @constructor\n */\nexport function CharacterPage() {\n\tconst {characterID: id} = useParams()\n\tconst {character, imageResources} = useCharacterWithImages(id)\n\n\tconst [slideshowState, send] = useMachine(\n\t\tplainSlideshowMachine.withContext({\n\t\t\t...plainSlideshowMachine.context,\n\t\t\tnumberOfImages: imageResources.length,\n\t\t}),\n\t)\n\n\tconst history = useHistory()\n\tfunction back() {\n\t\thistory.replace('/')\n\t}\n\n\tfunction edit() {\n\t\thistory.push(`/edit-character/${id}`)\n\t}\n\n\tconst {uid} = useUser()\n\tconst [status, setStatus] = useState('viewing')\n\tfunction deleteCharacter() {\n\t\tsetStatus('deleting')\n\n\t\t// Delete artwork first\n\t\tconst promises = []\n\t\tfor (const fileID of character.files) {\n\t\t\tpromises.push(\n\t\t\t\tstorage\n\t\t\t\t\t.ref()\n\t\t\t\t\t.child(`${uid}/${fileID}`)\n\t\t\t\t\t.delete()\n\t\t\t\t\t.catch(error => console.warn(`Failed to delete art ${uid}/${fileID}:`, error))\n\t\t\t)\n\t\t}\n\n\t\t// Then delete the character itself\n\t\tconst ref = firestore.collection('users').doc(uid)\n\t\tpromises.push(\n\t\t\tref\n\t\t\t\t.update({characters: firebase.firestore.FieldValue.arrayRemove(character)})\n\t\t\t\t.catch(error => console.warn(`Failed to delete character ${id}:`, error))\n\t\t)\n\n\t\tPromise.all(promises).finally(() => {\n\t\t\thistory.replace('/')\n\t\t})\n\t}\n\n\tif (status === 'deleting') {\n\t\treturn (\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t)\n\t}\n\n\treturn (\n\t\t}\n\t\t\tname={

{character.name}

}\n\t\t\tstory={}\n\t\t\tactions={\n\t\t\t\t<>\n\t\t\t\t\t\n\t\t\t\t\t\tDelete\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tBack\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tEdit\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t}\n\t\t/>\n\t)\n}\n","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\home.js",["132","133","134"],"import {Suspense, unstable_SuspenseList as SuspenseList, useEffect, useState} from 'react'\n\nimport {useHistory} from 'react-router-dom'\nimport {motion} from 'framer-motion'\nimport {Text, Spinner} from '@fluentui/react'\n\nimport {Center} from '../components/center'\nimport {ActionButton} from '../components/action-button'\nimport {CharacterCard} from '../components/CharacterCard'\nimport {ProfileMenu, ProfileMenuItem} from '../components/profile-menu'\n\nimport {colors} from '../shared/theme'\nimport {auth, firestore, useCharacters, useUser} from '../shared/firebase.js'\nimport {createDocumentResource, useDocumentResource} from '../shared/resources.js'\n\nimport '../styles/profile-menu.css'\nimport 'wicg-inert'\n\n/**\n * @typedef {{\n * characterID: string,\n * files: [string],\n * name: string,\n * story: string\n * }} Character\n * @typedef {{characters: [Character]}} UserData\n */\n\n/**\n * Renders a list of ``'s from a document and a resource.\n * @param {{\n * \tdocumentRef: DocumentReference,\n * \tresource: ResourceReader\n * }} props\n * @returns {JSX.Element|[JSX.Element]}\n * @constructor\n */\nfunction CharacterCardList({documentRef, resource}) {\n\tconst characters = useCharacters()\n\n\t// Render all the Character Cards if there are any.\n\tif (characters.length > 0) {\n\t\tconst listOfCharacters = characters.map((character, index) => )\n\t\treturn (\n\t\t\t\n\t\t\t\t{listOfCharacters}\n\t\t\t\n\t\t)\n\t}\n\n\t// Otherwise, inform the user of how to create a character.\n\t// TODO: Add alt for pride-drawing.svg\n\treturn (\n\t\t\n\t\t\t\"\"\n\t\t\t\n\t\t\t\tTo get started, add some characters with the \"New\" button.\n\t\t\t\n\t\t\n\t)\n}\n\n/**\n * Renders the Home page's header with the profile image. It handles scroll animations.\n * @returns {JSX.Element}\n * @constructor\n */\nfunction ProfileHeader() {\n\tconst user = useUser()\n\tconst [status, setStatus] = useState('flat')\n\n\tuseEffect(() => {\n\t\tfunction handler() {\n\t\t\tsetStatus(prevStatus => {\n\t\t\t\tif (window.scrollY > 0) return 'floating'\n\t\t\t\treturn 'flat'\n\t\t\t})\n\t\t}\n\n\t\twindow.addEventListener('scroll', handler)\n\t\treturn () => {\n\t\t\twindow.removeEventListener('scroll', handler)\n\t\t}\n\t}, [])\n\n\tconst history = useHistory()\n\tfunction signOut() {\n\t\tauth.signOut().then(() => {\n\t\t\thistory.push('/login')\n\t\t})\n\t}\n\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t💛 Art Hub\n\t\t\t\n\t\t\t\n\t\t\t\tShare\n\t\t\t\tSettings\n\t\t\t\tHelp\n\t\t\t\tSign Out\n\t\t\t\n\t\t\n\t)\n}\n\n/**\n * Home page\n * @returns {JSX.Element}\n * @constructor\n */\nexport function Home() {\n\tconst history = useHistory()\n\tfunction openNewCharacterPage() {\n\t\thistory.push('/new-character')\n\t}\n\n\treturn (\n\t\t\n\t\t\t\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\tNew\n\t\t\t\t\n\t\t\t
\n\t\t\n\t)\n}\n","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\404.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\transition-router.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\authentication.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\error-boundary.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\center.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\firebase.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\theme.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\new-character.js",["135","136"],"import {useEffect, useState} from 'react'\nimport {useMachine} from '@xstate/react'\nimport {useHistory} from 'react-router-dom'\nimport {useId} from '@uifabric/react-hooks'\nimport {FontIcon, Text} from '@fluentui/react'\nimport {useDropzone} from 'react-dropzone'\n\nimport {ActionButton} from '../components/action-button.js'\nimport {SavingDialog} from '../components/saving-dialog.js'\nimport {useUser} from '../shared/firebase.js'\nimport {newCharacterMachine, uploadSlideshowMachine} from '../shared/machines.js'\nimport {\n\tartistSVGStyles,\n\tartworkStyles,\n\tartworkWrapperStyles,\n\tdropZoneMessages,\n\tNextButton,\n\tPreviousButton,\n\tremoveButtonStyles,\n} from '../components/slideshow-parts.js'\nimport {\n\tCharacterLayout,\n\tCharacterNameInput,\n\tCharacterStoryInput,\n\tclearStorageKeys,\n} from '../components/character-parts.js'\n\nimport {colors} from '../shared/theme.js'\nimport '../styles/new-character.css'\n\nfunction useSlideshow() {\n\tconst [state, send] = useMachine(uploadSlideshowMachine)\n\tconst dropzone = useDropzone({\n\t\taccept: 'image/*',\n\t\tonDropAccepted(acceptedFiles) {\n\t\t\t// handle cancelled file operations\n\t\t\tconst filesWithPreview = acceptedFiles.map(file => {\n\t\t\t\tfile.preview = URL.createObjectURL(file)\n\t\t\t\treturn file\n\t\t\t})\n\t\t\tsend('ADDED_PHOTOS', {data: filesWithPreview})\n\t\t},\n\t})\n\n\tconst previousButton = send('PREVIOUS')} />\n\tconst nextButton = send('NEXT')} />\n\n\tlet slideshowSection\n\tif (state.matches('noPhotos'))\n\t\tslideshowSection = (\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t)\n\telse if (state.matches('newPhoto'))\n\t\t// TODO: Make drop target accessible\n\t\tslideshowSection = (\n\t\t\t
\n\t\t\t\t{previousButton}\n\t\t\t\t\n\t\t\t
\n\t\t)\n\telse if (state.matches('photos'))\n\t\tslideshowSection = (\n\t\t\t
\n\t\t\t\t{state.context.currentPage > 0 && previousButton}\n\t\t\t\t\"\"\n\t\t\t\t send('REMOVED_PHOTO')}\n\t\t\t\t\tstyle={removeButtonStyles}\n\t\t\t\t/>\n\t\t\t\t{nextButton}\n\t\t\t
\n\t\t)\n\n\tlet dropMessageContent\n\tif (dropzone.isDragReject) dropMessageContent = dropZoneMessages.rejected\n\telse if (dropzone.isDragAccept) dropMessageContent = dropZoneMessages.accepted\n\telse dropMessageContent = dropZoneMessages.idle\n\n\tconst dropID = useId('drop')\n\tlet dropMessage\n\t/* TODO: align center on Desktop sizes */\n\tif (state.matches('photos'))\n\t\tdropMessage = (\n\t\t\t\n\t\t\t\tEdit your character photos above.\n\t\t\t\n\t\t)\n\telse\n\t\tdropMessage = (\n\t\t\t\n\t\t\t\t{dropMessageContent}\n\t\t\t\n\t\t)\n\n\treturn {\n\t\t...dropzone,\n\t\tdropID,\n\t\tdropMessage,\n\t\tslideshowSection,\n\t\tfiles: state.context.files,\n\t}\n}\n\nexport function NewCharacter() {\n\tconst [name, setName] = useState(localStorage.getItem('character-name') ?? '')\n\tconst [story, setStory] = useState(localStorage.getItem('character-story') ?? '')\n\n\tconst user = useUser()\n\tconst {getInputProps, slideshowSection, dropMessage, dropID, files} = useSlideshow()\n\n\tconst history = useHistory()\n\tfunction cancel() {\n\t\tclearStorageKeys('character-name', 'character-story')\n\t\thistory.replace('/')\n\t}\n\n\tconst [saveState, send] = useMachine(newCharacterMachine)\n\tfunction save(event) {\n\t\tevent.preventDefault()\n\t\tsend('SAVE', {name, story, files, uid: user.uid})\n\t}\n\n\tuseEffect(() => {\n\t\tif (saveState.matches({finished: 'success'})) {\n\t\t\tclearStorageKeys('character-name', 'character-story')\n\t\t\thistory.push(`/character/${saveState.context.characterID}`)\n\t\t}\n\t}, [history, saveState])\n\n\treturn (\n\t\t\n\t\t\t\t\t{slideshowSection}\n\t\t\t\t\t\n\t\t\t\t\t{dropMessage}\n\t\t\t\t\n\t\t\t}\n\t\t\tname={}\n\t\t\tstory={}\n\t\t\tactions={\n\t\t\t\t<>\n\t\t\t\t\t\n\t\t\t\t\t\tCancel\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tSave\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t}\n\t\t>\n\t\t\t\n\t\t\n\t)\n}\n","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\action-button.js",["137","138"],"import {FontIcon, Text} from '@fluentui/react'\nimport {motion} from 'framer-motion'\nimport {colors} from '../shared/theme'\nimport '../styles/action-button.css'\n\n/*\n * @param {{ variant: 'round' | 'flat' | 'bold-orange' | 'bold-pink' | 'danger', iconName: string }} options\n */\nexport function ActionButton({variant, iconName, children, className, ...props}) {\n\treturn (\n\t\t\n\t\t\t\n\t\t\t{children && (\n\t\t\t\t\n\t\t\t\t\t{children}\n\t\t\t\t\n\t\t\t)}\n\t\t\n\t)\n}\n","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\CharacterCard.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\profile-menu.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\notifications.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\auto-expanding-textarea.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\saving-dialog.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\machines.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\profile-photo.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\helpers.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\errors.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\resources.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\slideshow-parts.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\character-parts.js",["139"],"import {memo, Suspense, useState} from 'react'\n\nimport xss from 'xss'\nimport marked from 'marked'\nimport {Label, Spinner} from '@fluentui/react'\n\nimport {Center} from './center.js'\nimport {colors} from '../shared/theme.js'\nimport {artworkWrapperStyles} from './slideshow-parts.js'\nimport {FadeLayout} from './page-transitions'\nimport {AutoExpandingTextarea} from './auto-expanding-textarea'\nimport {debounce} from 'mini-debounce'\nimport {useId} from '@uifabric/react-hooks'\n\nexport const CharacterStory = memo(({story}) => (\n\t
\n))\n\nconst empty = {}\n/**\n * A component to facilitate in reduction of repeating layout code for pages that display character info.\n *\n * @param {{\n * mode: 'display',\n * slideshow: JSX.Element,\n * name: JSX.Element,\n * story: JSX.Element,\n * actions: [JSX.Element],\n * children: any,\n * } | {\n * mode: 'edit',\n * onSubmit: function(React.SyntheticEvent),\n * slideshow: JSX.Element,\n * name: JSX.Element,\n * story: JSX.Element,\n * actions: [JSX.Element],\n * children: any,\n * }} props\n * @constructor\n */\nexport function CharacterLayout({slideshow, name, story, actions, mode, onSubmit, children}) {\n\tconst content = (\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t{slideshow}\n\t\t\t\t\n\n\t\t\t\t
\n\t\t\t\t\t{name}\n\t\t\t\t\t{story}\n\t\t\t\t
\n\t\t\t
\n\t\t\t\n\t\t\t\t{actions}\n\t\t\t\n\t\t\n\t)\n\n\tlet wrapped\n\tif (mode === 'display') {\n\t\twrapped = (\n\t\t\t\n\t\t\t\t{content}\n\t\t\t\t{children}\n\t\t\t\n\t\t)\n\t} else if (mode === 'edit') {\n\t\twrapped = (\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{content}\n\t\t\t\t
\n\t\t\t\t{children}\n\t\t\t
\n\t\t)\n\t} else {\n\t\tthrow new Error(`Invalid mode '${mode}' is not supported.`)\n\t}\n\n\treturn wrapped\n}\n\nexport function CharacterInput({className, multiline, label, ...props}) {\n\tconst [status, setStatus] = useState('idle')\n\tconst focusHandlers = {\n\t\tonFocus() {\n\t\t\tsetStatus('focus')\n\t\t},\n\t\tonBlur() {\n\t\t\tsetStatus('idle')\n\t\t},\n\t}\n\n\treturn (\n\t\t
\n\t\t\t{label && (\n\t\t\t\t\n\t\t\t)}\n\t\t\t{multiline ? (\n\t\t\t\t\n\t\t\t) : (\n\t\t\t\t\n\t\t\t)}\n\t\t
\n\t)\n}\n\nconst debouncedSetItem = debounce((key, value) => localStorage.setItem(key, value), 100)\nexport function createValueStorer(key, setter) {\n\treturn event => {\n\t\tconst {value} = event.target\n\n\t\tsetter(value)\n\t\tdebouncedSetItem(key, value)\n\t}\n}\n\nexport function CharacterNameInput({storageKey, setter, value}) {\n\tconst id = useId('name')\n\treturn (\n\t\t\n\t)\n}\nexport function CharacterStoryInput({storageKey, setter, value}) {\n\tconst id = useId('story')\n\treturn (\n\t\t\n\t)\n}\n\nexport function clearStorageKeys(...keys) {\n\tfor (const key of keys) {\n\t\tlocalStorage.setItem(key, '')\n\t}\n}\n","C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\components\\page-transitions.js",[],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\pages\\edit-character.js",["140","141","142","143","144"],"C:\\Users\\Christopher\\Workspace\\private-art-hub-project\\src\\shared\\empty.js",[],{"ruleId":"145","replacedBy":"146"},{"ruleId":"147","replacedBy":"148"},{"ruleId":"149","severity":1,"message":"150","line":1,"column":9,"nodeType":"151","messageId":"152","endLine":1,"endColumn":16},{"ruleId":"149","severity":1,"message":"153","line":8,"column":2,"nodeType":"151","messageId":"152","endLine":8,"endColumn":15},{"ruleId":"149","severity":1,"message":"154","line":13,"column":2,"nodeType":"151","messageId":"152","endLine":13,"endColumn":18},{"ruleId":"149","severity":1,"message":"155","line":17,"column":9,"nodeType":"151","messageId":"152","endLine":17,"endColumn":31},{"ruleId":"149","severity":1,"message":"156","line":17,"column":33,"nodeType":"151","messageId":"152","endLine":17,"endColumn":47},{"ruleId":"149","severity":1,"message":"157","line":17,"column":49,"nodeType":"151","messageId":"152","endLine":17,"endColumn":68},{"ruleId":"149","severity":1,"message":"158","line":13,"column":15,"nodeType":"151","messageId":"152","endLine":13,"endColumn":24},{"ruleId":"149","severity":1,"message":"155","line":14,"column":9,"nodeType":"151","messageId":"152","endLine":14,"endColumn":31},{"ruleId":"149","severity":1,"message":"157","line":14,"column":33,"nodeType":"151","messageId":"152","endLine":14,"endColumn":52},{"ruleId":"149","severity":1,"message":"159","line":14,"column":2,"nodeType":"151","messageId":"152","endLine":14,"endColumn":16},{"ruleId":"149","severity":1,"message":"160","line":18,"column":2,"nodeType":"151","messageId":"152","endLine":18,"endColumn":19},{"ruleId":"149","severity":1,"message":"161","line":2,"column":9,"nodeType":"151","messageId":"152","endLine":2,"endColumn":15},{"ruleId":"149","severity":1,"message":"162","line":3,"column":9,"nodeType":"151","messageId":"152","endLine":3,"endColumn":15},{"ruleId":"149","severity":1,"message":"163","line":19,"column":7,"nodeType":"151","messageId":"152","endLine":19,"endColumn":12},{"ruleId":"149","severity":1,"message":"164","line":1,"column":9,"nodeType":"151","messageId":"152","endLine":1,"endColumn":18},{"ruleId":"149","severity":1,"message":"165","line":3,"column":9,"nodeType":"151","messageId":"152","endLine":3,"endColumn":14},{"ruleId":"149","severity":1,"message":"166","line":28,"column":63,"nodeType":"151","messageId":"152","endLine":28,"endColumn":68},{"ruleId":"149","severity":1,"message":"167","line":28,"column":70,"nodeType":"151","messageId":"152","endLine":28,"endColumn":87},{"ruleId":"149","severity":1,"message":"168","line":41,"column":9,"nodeType":"151","messageId":"152","endLine":41,"endColumn":12},"no-native-reassign",["169"],"no-negated-in-lhs",["170"],"no-unused-vars","'useMemo' is defined but never used.","Identifier","unusedVar","'fetchImageURL' is defined but never used.","'withUserResource' is defined but never used.","'createDocumentResource' is defined but never used.","'createResource' is defined but never used.","'useDocumentResource' is defined but never used.","'firestore' is defined but never used.","'CharacterInput' is defined but never used.","'createValueStorer' is defined but never used.","'motion' is defined but never used.","'colors' is defined but never used.","'empty' is assigned a value but never used.","'useEffect' is defined but never used.","'useId' is defined but never used.","'files' is assigned a value but never used.","'preExistingPhotos' is assigned a value but never used.","'uid' is assigned a value but never used.","no-global-assign","no-unsafe-negation"] \ No newline at end of file diff --git a/.idea/codestream.xml b/.idea/codestream.xml index bd65334..be78838 100644 --- a/.idea/codestream.xml +++ b/.idea/codestream.xml @@ -2,6 +2,6 @@ \ No newline at end of file diff --git a/src/components/CharacterCard.js b/src/components/CharacterCard.js index 8dc3639..c4156d0 100644 --- a/src/components/CharacterCard.js +++ b/src/components/CharacterCard.js @@ -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 (
diff --git a/src/components/auto-expanding-textarea.js b/src/components/auto-expanding-textarea.js index 945d1dd..bce9e68 100644 --- a/src/components/auto-expanding-textarea.js +++ b/src/components/auto-expanding-textarea.js @@ -1,4 +1,5 @@ import {useEffect, useRef} from 'react' +import {emptyObject} from '../shared/empty' function autoExpand(element, lineHeight) { element.style.overflow = 'unset' @@ -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 @@ -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) { diff --git a/src/components/center.js b/src/components/center.js index 82b208b..83195c9 100644 --- a/src/components/center.js +++ b/src/components/center.js @@ -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 ( (
)) -const empty = {} /** + * A component to facilitate in reduction of repeating layout code for pages that display character info. * * @param {{ * mode: 'display', @@ -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 ( +
+ {label && ( + + )} + {multiline ? ( + + ) : ( + + )} +
+ ) +} + +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 ( + + ) +} +export function CharacterStoryInput({storageKey, setter, value}) { + const id = useId('story') + return ( + + ) +} + +export function clearStorageKeys(...keys) { + for (const key of keys) { + localStorage.setItem(key, '') + } +} diff --git a/src/components/slideshow-parts.js b/src/components/slideshow-parts.js index acab4d7..18c87e3 100644 --- a/src/components/slideshow-parts.js +++ b/src/components/slideshow-parts.js @@ -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', @@ -47,6 +54,7 @@ const previousButtonStyles = { height: 46, borderRadius: 0, borderBottomRightRadius: 8, + zIndex: 2, } export function PreviousButton(props) { return ( @@ -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} resource + * @returns {JSX.Element} + * @constructor + */ +function PreExistingPhoto({resource}) { + return +} +/** + * + * @param {[{id: string, resource: ResourceReader, scheduledForRemoval: boolean}]} preExistingPhotos + * @returns {{preExistingPhotos: ([]|*), fileRejections: FileRejection[], isFileDialogActive: boolean, dropMessage: JSX.Element, inputRef: React.RefObject, isDragReject: boolean, isFocused: boolean, getInputProps(props?: DropzoneInputProps): DropzoneInputProps, acceptedFiles: File[], slideshowSection: JSX.Element, draggedFiles: File[], isDragAccept: boolean, rootRef: React.RefObject, 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 = send('PREVIOUS')} /> + const nextButton = send('NEXT')} /> + + let slideshowSection + if (state.matches('noPhotos')) { + slideshowSection = ( +
+ Artist looking at her art +
+ ) + } else if (state.matches('newPhotosPage')) { + // TODO: Make drop target accessible + slideshowSection = ( +
+ {previousButton} + +
+ ) + } else if (state.matches('newPhotos')) { + slideshowSection = ( +
+ {state.context.currentPage > 0 || state.context.preExistingPhotos.length > 0 ? previousButton : null} + + send('REMOVED_PHOTO')} + style={removeButtonStyles} + /> + {nextButton} +
+ ) + } else if (state.matches('preExistingPhotos')) { + const currentPhoto = state.context.preExistingPhotos[state.context.currentPage] + slideshowSection = ( +
+ {state.context.currentPage > 0 ? previousButton : null} + + {currentPhoto.scheduledForRemoval ? ( +
+

Scheduled for removal

+
+ ) : null} + {!currentPhoto.scheduledForRemoval ? ( + send('SCHEDULE_FOR_REMOVAL')} + style={removeButtonStyles} + /> + ) : ( + send('CANCEL_REMOVAL')} + style={removeButtonStyles} + /> + ) + } + {nextButton} +
+ ) + } + + 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 = ( + + Edit your character photos above. + + ) + else + dropMessage = ( + + {dropMessageContent} + + ) + + return { + ...dropzone, + dropID, + dropMessage, + slideshowSection, + files: state.context.files, + preExistingPhotos: state.context.preExistingPhotos, + } +} diff --git a/src/index.js b/src/index.js index 3f5947a..99246df 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,7 @@ import {CharacterPage} from './pages/character.js' import * as serviceWorker from './serviceWorker.js' import './styles/index.css' import {EditCharacterPage} from './pages/edit-character' +import {FadeLayout} from './components/page-transitions' loadTheme(theme) initializeIcons() @@ -32,7 +33,11 @@ function PrivateRoute({as, ...props}) { const user = useUser() if (user) return - return + return ( + + + + ) } /** @@ -44,7 +49,11 @@ function UnauthenticatedRoute({as, ...props}) { const user = useUser() if (!user) return - return + return ( + + + + ) } ReactDOM.render( diff --git a/src/pages/authentication.js b/src/pages/authentication.js index c0f2ba2..6b08320 100644 --- a/src/pages/authentication.js +++ b/src/pages/authentication.js @@ -8,6 +8,7 @@ import {Center} from '../components/center.js' import {transitions} from '../shared/theme.js' import {Notifications} from '../components/notifications.js' import {firestore, auth, provider as googleProvider} from '../shared/firebase.js' +import {FadeLayout} from '../components/page-transitions' const initialStatus = {type: 'idle', data: null} @@ -112,70 +113,80 @@ function AuthenticationPage({type}) { } return ( -
- - - - - {pageData.title[type]} - - - - - {type === 'register' && } - - - - +
+ + + - - - {pageData.mainButton[type]} - - {pageData.googleButton[type]} - - + + {pageData.title[type]} + + + + + {type === 'register' && } + + + + + + + {pageData.mainButton[type]} + + {pageData.googleButton[type]} + + + + + + {pageData.switchMessage[type]} + - - {pageData.switchMessage[type]} - - - - - {status.type === 'auth-error' && ( - - {pageData.errorMessage[type]}: -
- {status.data} -
- )} -
- -
+ + {status.type === 'auth-error' && ( + + {pageData.errorMessage[type]}: +
+ {status.data} +
+ )} +
+
+
+ ) } diff --git a/src/pages/character.js b/src/pages/character.js index 71e85a4..6be6153 100644 --- a/src/pages/character.js +++ b/src/pages/character.js @@ -77,7 +77,7 @@ export function CharacterPage() { .ref() .child(`${uid}/${fileID}`) .delete() - .catch(error => console.warn(`Failed to delete art ${uid}/${fileID}:`, error)) + .catch(error => console.warn(`Failed to delete art ${uid}/${fileID}:`, error)), ) } @@ -86,7 +86,7 @@ export function CharacterPage() { promises.push( ref .update({characters: firebase.firestore.FieldValue.arrayRemove(character)}) - .catch(error => console.warn(`Failed to delete character ${id}:`, error)) + .catch(error => console.warn(`Failed to delete character ${id}:`, error)), ) Promise.all(promises).finally(() => { @@ -98,7 +98,7 @@ export function CharacterPage() { return (
- +
) diff --git a/src/pages/edit-character.js b/src/pages/edit-character.js index 29a6e84..2564f29 100644 --- a/src/pages/edit-character.js +++ b/src/pages/edit-character.js @@ -1,31 +1,62 @@ +import {useEffect, useMemo, useState} from 'react' + +import {useId} from '@uifabric/react-hooks' import {useHistory, useParams} from 'react-router-dom' -import {CharacterLayout} from '../components/character-parts.js' -import {useCharacterWithImages, useUser} from '../shared/firebase.js' -import {ActionButton} from '../components/action-button' -function clearStorage() {} +import { + CharacterLayout, + CharacterNameInput, + CharacterStoryInput, + clearStorageKeys, +} from '../components/character-parts.js' +import {useCharacterWithImages, useUser} from '../shared/firebase.js' +import {ActionButton} from '../components/action-button.js' +import {useSlideshow} from '../components/slideshow-parts' export function EditCharacterPage() { const {characterID: id} = useParams() const {character, imageResources} = useCharacterWithImages(id) + const initialPreExistingPhotoData = useMemo( + () => + imageResources.map((resource, index) => ({ + id: character.files[index], + resource, + scheduledForRemoval: false, + })), + [character?.files, imageResources], + ) + const {getInputProps, slideshowSection, dropMessage, dropID, files, preExistingPhotos} = useSlideshow( + initialPreExistingPhotoData, + ) + + const [name, setName] = useState(localStorage.getItem('edit-character-name') || character.name) + const [story, setStory] = useState(localStorage.getItem('edit-character-story') || character.story) const history = useHistory() function cancel() { - clearStorage() + clearStorageKeys('edit-character-name', 'edit-character-story') history.replace(`/character/${id}`) } + const {uid} = useUser() function saveChanges(event) { event.preventDefault() + // send('SAVE', {name, story, files, uid, preExistingPhotos}) } return ( + {slideshowSection} + + {dropMessage} +
+ } + name={} + story={} actions={ <> diff --git a/src/pages/new-character.js b/src/pages/new-character.js index 386c82d..82d273a 100644 --- a/src/pages/new-character.js +++ b/src/pages/new-character.js @@ -1,200 +1,30 @@ import {useEffect, useState} from 'react' - -import {motion} from 'framer-motion' -import {debounce} from 'mini-debounce' import {useMachine} from '@xstate/react' import {useHistory} from 'react-router-dom' -import {useId} from '@uifabric/react-hooks' -import {Text, Label, FontIcon} from '@fluentui/react' -import {useDropzone} from 'react-dropzone' import {ActionButton} from '../components/action-button.js' import {SavingDialog} from '../components/saving-dialog.js' import {useUser} from '../shared/firebase.js' -import {newCharacterMachine, uploadSlideshowMachine} from '../shared/machines.js' -import {AutoExpandingTextarea} from '../components/auto-expanding-textarea.js' -import {NextButton, artworkStyles, artworkWrapperStyles, PreviousButton} from '../components/slideshow-parts.js' -import {CharacterLayout} from '../components/character-parts.js' - -import {colors} from '../shared/theme.js' +import {newCharacterMachine} from '../shared/machines.js' +import {useSlideshow} from '../components/slideshow-parts.js' +import { + CharacterLayout, + CharacterNameInput, + CharacterStoryInput, + clearStorageKeys, +} from '../components/character-parts.js' import '../styles/new-character.css' -const artistSVGStyles = { - width: 298, - height: 220, - marginBottom: 5, -} -const removeButtonStyles = { - position: 'absolute', - top: 0, - right: 0, - width: 46, - height: 46, - borderRadius: 0, - borderBottomLeftRadius: 8, -} - -function NewCharacterInput({className, multiline, label, ...props}) { - const [status, setStatus] = useState('idle') - const focusHandlers = { - onFocus() { - setStatus('focus') - }, - onBlur() { - setStatus('idle') - }, - } - return ( -
- {label && ( - - )} - {multiline ? ( - - ) : ( - - )} -
- ) -} - -function clearStorage() { - localStorage.setItem('character-name', '') - localStorage.setItem('character-story', '') -} - -const debouncedSetItem = debounce((key, value) => localStorage.setItem(key, value), 100) -function createValueStorer(key, setter) { - return event => { - const {value} = event.target - - setter(value) - debouncedSetItem(key, value) - } -} - -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.', -} - -function useSlideshow() { - const [state, send] = useMachine(uploadSlideshowMachine) - 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 = send('PREVIOUS')} /> - const nextButton = send('NEXT')} /> - - let slideshowSection - if (state.matches('noPhotos')) - slideshowSection = ( -
- Artist looking at her art -
- ) - else if (state.matches('newPhoto')) - // TODO: Make drop target accessible - slideshowSection = ( -
- {previousButton} - -
- ) - else if (state.matches('photos')) - slideshowSection = ( -
- {state.context.currentPage > 0 && previousButton} - - send('REMOVED_PHOTO')} - style={removeButtonStyles} - /> - {nextButton} -
- ) - - 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 = ( - - Edit your character photos above. - - ) - else - dropMessage = ( - - {dropMessageContent} - - ) - - return { - ...dropzone, - dropID, - dropMessage, - slideshowSection, - files: state.context.files, - } -} - export function NewCharacter() { - const nameFieldID = useId('name') - const storyFieldID = useId('character-story') - - const user = useUser() const [name, setName] = useState(localStorage.getItem('character-name') ?? '') const [story, setStory] = useState(localStorage.getItem('character-story') ?? '') + + const user = useUser() const {getInputProps, slideshowSection, dropMessage, dropID, files} = useSlideshow() const history = useHistory() function cancel() { - clearStorage() + clearStorageKeys('character-name', 'character-story') history.replace('/') } @@ -206,7 +36,7 @@ export function NewCharacter() { useEffect(() => { if (saveState.matches({finished: 'success'})) { - clearStorage() + clearStorageKeys('character-name', 'character-story') history.push(`/character/${saveState.context.characterID}`) } }, [history, saveState]) @@ -222,28 +52,8 @@ export function NewCharacter() { {dropMessage} } - name={ - - } - story={ - - } + name={} + story={} actions={ <> diff --git a/src/shared/empty.js b/src/shared/empty.js new file mode 100644 index 0000000..6868a70 --- /dev/null +++ b/src/shared/empty.js @@ -0,0 +1,2 @@ +export const emptyObject = {} +export const emptyArray = [] diff --git a/src/shared/firebase.js b/src/shared/firebase.js index 774e4cc..f0adf29 100644 --- a/src/shared/firebase.js +++ b/src/shared/firebase.js @@ -44,7 +44,7 @@ export const provider = new firebase.auth.GoogleAuthProvider() const FirebaseContext = createContext({user: null, characters: []}) function FirebaseCharactersResource({user, resource, children}) { - const {characters} = useDocumentResource(firestore.collection('users').doc(user.uid), resource) + const {characters} = useDocumentResource(firestore.collection('users').doc(user?.uid), resource) const value = useMemo(() => ({user, characters}), [user, characters]) return {children} } @@ -65,8 +65,12 @@ function FirebaseUserResource({resource, children}) { }, []) const userDocumentResource = useMemo(() => { - return createDocumentResource(firestore.collection('users').doc(user.uid)) - }, [user.uid]) + if (user?.uid) return createDocumentResource(firestore.collection('users').doc(user.uid)) + }, [user?.uid]) + + if (!user) { + return {children} + } return ( @@ -138,7 +142,7 @@ export function useCharacterWithImages(id) { const character = characters.find(character => character.id === id) const imageResources = character?.files?.map(id => createResource(fetchImageURL(user.uid, id))) ?? [] return {character, imageResources} - }, [characters, user.uid, id]) + }, [characters, user?.uid, id]) } export const corsAnywhere = ky.create({prefixUrl: '//cors-anywhere.herokuapp.com/'}) diff --git a/src/shared/helpers.js b/src/shared/helpers.js index 4820054..0793022 100644 --- a/src/shared/helpers.js +++ b/src/shared/helpers.js @@ -50,3 +50,10 @@ export function forEachNonDescendantTree(element, callback) { currentElement = currentElement.parentElement } } + +export function removeFromArray(array, index) { + return array.slice(0, index).concat(array.slice(index + 1)) +} +export function replaceInArray(array, index, callback) { + return array.slice(0, index).concat(callback(array[index]), array.slice(index + 1)) +} diff --git a/src/shared/machines.js b/src/shared/machines.js index 2bd9fc7..b624757 100644 --- a/src/shared/machines.js +++ b/src/shared/machines.js @@ -5,23 +5,75 @@ import {v1 as uuidv1} from 'uuid' import * as firebase from 'firebase' import {firestore, storage, corsAnywhere} from './firebase.js' import {MissingGravatarProfileError, UnreachableGravatarPhotoError, UnreachableGravatarProfileError} from './errors' +import {removeFromArray, replaceInArray} from './helpers' export const uploadSlideshowMachine = createMachine( { id: 'upload-slideshow', - initial: 'noPhotos', - context: {currentPage: 0, files: []}, + initial: 'idle', + context: {currentPage: 0, files: [], preExistingPhotos: []}, states: { - photos: { + idle: { + always: [ + { + cond: 'atLeastOnePreExistingPhotoLeft', + target: 'preExistingPhotos', + }, + { + // shouldn't happen but makes it future-proof + cond: 'atLeastOneNewPhotoLeft', + target: 'newPhotos', + }, + { + target: 'noPhotos', + }, + ], + }, + preExistingPhotos: { on: { PREVIOUS: { - cond: 'notAtBeginningOfPhotos', + cond: 'notAtBeginning', actions: ['decrementPage'], }, NEXT: [ { - cond: 'atEndOfPhotos', - target: 'newPhoto', + cond: 'atEndOfPreExistingPhotosButNewPhotosRemain', + target: 'newPhotos', + actions: ['goToFirstPage'], + }, + { + cond: 'atEndOfPreExistingPhotos', + target: 'newPhotosPage', + }, + { + actions: ['incrementPage'], + }, + ], + SCHEDULE_FOR_REMOVAL: { + actions: ['scheduleForRemoval'], + }, + CANCEL_REMOVAL: { + actions: ['cancelRemoval'], + }, + }, + }, + newPhotos: { + on: { + PREVIOUS: [ + { + cond: 'notAtBeginning', + actions: ['decrementPage'], + }, + { + cond: 'atLeastOnePreExistingPhotoLeft', + target: 'preExistingPhotos', + actions: ['goToLastPageOfPreExistingPhotos'], + }, + ], + NEXT: [ + { + cond: 'atEndOfNewPhotos', + target: 'newPhotosPage', }, { actions: ['incrementPage'], @@ -29,30 +81,47 @@ export const uploadSlideshowMachine = createMachine( ], REMOVED_PHOTO: [ { - cond: 'atLeastOnePhotoLeft', - actions: ['removePhoto', 'decrementPageOrZero'], + cond: 'atLeastOneNewPhotoLeftAfterRemoval', + actions: ['removeNewPhoto', 'decrementPageOrZero'], + }, + { + cond: 'atLeastOnePreExistingPhotoLeft', + actions: ['removeNewPhoto', 'goToLastPageOfPreExistingPhotos'], }, { - actions: ['removePhoto'], + actions: ['removeNewPhoto'], target: 'noPhotos', }, ], }, }, - newPhoto: { + newPhotosPage: { on: { - PREVIOUS: 'photos', + PREVIOUS: [ + { + cond: 'atLeastOneNewPhotoLeft', + target: 'newPhotos', + }, + { + cond: 'atLeastOnePreExistingPhotoLeft', + target: 'preExistingPhotos', + }, + { + // should be impossible to get here, this is just in case + target: 'noPhotos', + }, + ], ADDED_PHOTOS: { - actions: ['incrementPage', 'addPhotos'], - target: 'photos', + actions: ['addNewPhotos', 'goToLastPageOfNewPhotos'], + target: 'newPhotos', }, }, }, noPhotos: { on: { ADDED_PHOTOS: { - actions: ['addPhotos'], - target: 'photos', + actions: ['addNewPhotos'], + target: 'newPhotos', }, }, }, @@ -63,21 +132,34 @@ export const uploadSlideshowMachine = createMachine( decrementPage: assign({currentPage: ctx => ctx.currentPage - 1}), decrementPageOrZero: assign({currentPage: ctx => Math.max(ctx.currentPage - 1, 0)}), incrementPage: assign({currentPage: ctx => ctx.currentPage + 1}), - removePhoto: assign({ - files(ctx) { - return [...ctx.files.slice(0, ctx.currentPage), ...ctx.files.slice(ctx.currentPage + 1)] - }, + goToFirstPage: assign({currentPage: 0}), + goToLastPageOfPreExistingPhotos: assign({currentPage: ctx => ctx.preExistingPhotos.length - 1}), + goToLastPageOfNewPhotos: assign({currentPage: ctx => ctx.files.length - 1}), + removeNewPhoto: assign({ + files: ({files, currentPage}) => removeFromArray(files, currentPage), }), - addPhotos: assign({ - files(ctx, event) { - return ctx.files.concat(event.data) - }, + addNewPhotos: assign({ + files: (ctx, event) => ctx.files.concat(event.data), + }), + scheduleForRemoval: assign({ + preExistingPhotos: ({preExistingPhotos, currentPage}) => + replaceInArray(preExistingPhotos, currentPage, photo => ({...photo, scheduledForRemoval: true})), + }), + cancelRemoval: assign({ + preExistingPhotos: ({preExistingPhotos, currentPage}) => + replaceInArray(preExistingPhotos, currentPage, photo => ({...photo, scheduledForRemoval: false})), }), }, guards: { - notAtBeginningOfPhotos: ctx => ctx.currentPage > 0, - atEndOfPhotos: ctx => ctx.currentPage >= ctx.files.length - 1, - atLeastOnePhotoLeft: ctx => ctx.files.length > 1, + atEndOfPreExistingPhotos: ctx => ctx.currentPage >= ctx.preExistingPhotos.length - 1, + atEndOfPreExistingPhotosButNewPhotosRemain: ctx => + ctx.currentPage >= ctx.preExistingPhotos.length - 1 && ctx.files.length > 0, + noPreExistingPhotos: ctx => ctx.preExistingPhotos.length === 0, + notAtBeginning: ctx => ctx.currentPage > 0, + atEndOfNewPhotos: ctx => ctx.currentPage >= ctx.files.length - 1, + atLeastOneNewPhotoLeft: ctx => ctx.files.length > 0, + atLeastOneNewPhotoLeftAfterRemoval: ctx => ctx.files.length > 1, + atLeastOnePreExistingPhotoLeft: ctx => ctx.preExistingPhotos.length > 0, }, }, ) diff --git a/src/styles/new-character.css b/src/styles/new-character.css index 89c439b..eee1203 100644 --- a/src/styles/new-character.css +++ b/src/styles/new-character.css @@ -1,7 +1,7 @@ -.NewCharacterInput input, -.NewCharacterInput input::placeholder, -.NewCharacterInput textarea, -.NewCharacterInput textarea::placeholder { +.CharacterInput input, +.CharacterInput input::placeholder, +.CharacterInput textarea, +.CharacterInput textarea::placeholder { width: 100%; padding: 0; color: var(--dark); @@ -14,27 +14,27 @@ box-shadow: none; } -.NewCharacterInput input:focus, -.NewCharacterInput textarea:focus { +.CharacterInput input:focus, +.CharacterInput textarea:focus { outline: none; } -.NewCharacterInput textarea { +.CharacterInput textarea { margin-top: 10px; } -.NewCharacterInput input::placeholder, -.NewCharacterInput textarea::placeholder { +.CharacterInput input::placeholder, +.CharacterInput textarea::placeholder { color: var(--not-as-dark); } -.NewCharacterInput label { +.CharacterInput label { font-family: Inter, sans-serif; font-weight: 600; font-size: 20px; } -.NewCharacterInput:not(:last-of-type) { +.CharacterInput:not(:last-of-type) { margin-bottom: 20px; }