: '€'\n\n return (\n \u003CFormControl id={label.toUpperCase()}\u003E\n \u003CFormLabel\u003E{label.toUpperCase()}\u003C\u002FFormLabel\u003E\n \u003CNumberInput\n value={`${symbol} ${amount}`}\n onChange={(value) =\u003E {\n const withoutSymbol = value.split(' ')[0]\n onChange?.(parseFloat(withoutSymbol || '0'))\n }}\n \u003E\n \u003CNumberInputField \u002F\u003E\n \u003C\u002FNumberInput\u003E\n \u003C\u002FFormControl\u003E\n )\n}\n\nconst Commission = () =\u003E {\n const [enabled, setEnabled] = useRecoilState(commissionEnabledAtom)\n const [commission, setCommission] = useRecoilState(commissionAtom)\n\n return (\n \u003CBox width=\"300px\"\u003E\n \u003CFormControl display=\"flex\" alignItems=\"center\" mb={2}\u003E\n \u003CFormLabel htmlFor=\"includeCommission\" mb=\"0\"\u003E\n Include forex commission?\n \u003C\u002FFormLabel\u003E\n \u003CSwitch\n id=\"includeCommission\"\n isChecked={enabled}\n onChange={(event) =\u003E setEnabled(event.currentTarget.checked)}\n \u002F\u003E\n \u003C\u002FFormControl\u003E\n \u003CNumberInput\n isDisabled={!enabled}\n value={commission}\n onChange={(value) =\u003E setCommission(parseFloat(value || '0'))}\n \u003E\n \u003CNumberInputField \u002F\u003E\n \u003C\u002FNumberInput\u003E\n \u003C\u002FBox\u003E\n )\n}\n\nconst addCommission = (amount: number, commission: number) =\u003E {\n return amount \u002F (1 - commission \u002F 100)\n}\n\nconst removeCommission = (amount: number, commission: number) =\u003E {\n return amount * (1 - commission \u002F 100)\n}\n","id":"83a44256-331b-44cb-8fcd-56e6a78bce3c","is_binary":false,"title":"Selectors.tsx","sha":null,"inserted_at":"2022-05-14T18:05:25","updated_at":"2022-05-14T18:09:30","upload_id":null,"shortid":"6z4qO","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"JKNlo"},{"code":"import {Suspense} from 'react'\nimport {atom, atomFamily, useRecoilState} from 'recoil'\nimport {Drag} from '..\u002FDrag'\nimport {Resize} from '..\u002FResize'\nimport {RectangleContainer} from '.\u002FRectangleContainer'\nimport {RectangleInner} from '.\u002FRectangleInner'\nimport {RectangleLoading} from '.\u002FRectangleLoading'\n\nexport type ElementStyle = {\n position: {top: number; left: number}\n size: {width: number; height: number}\n}\n\nexport type Element = {\n style: ElementStyle\n image?: {src: string; id: number}\n}\n\nexport const defaultStyle = {\n position: {top: 0, left: 0},\n size: {width: 200, height: 200},\n}\n\nexport const elementState = atomFamily\u003CElement, number\u003E({\n key: 'element',\n default: {\n style: defaultStyle,\n },\n})\n\nexport const selectedElementState = atom\u003Cnumber | null\u003E({\n key: 'selectedElement',\n default: null,\n})\n\nexport const Rectangle = ({id}: {id: number}) =\u003E {\n const [element, setElement] = useRecoilState(elementState(id))\n const [selectedElement, setSelectedElement] = useRecoilState(selectedElementState)\n\n const selected = selectedElement === id\n\n return (\n \u003CRectangleContainer\n position={element.style.position}\n size={element.style.size}\n onSelect={() =\u003E {\n setSelectedElement(id)\n }}\n \u003E\n \u003CResize\n selected={selected}\n position={element.style.position}\n size={element.style.size}\n onResize={(style) =\u003E setElement({...element, style})}\n lockAspectRatio={element.image !== undefined}\n \u003E\n \u003CDrag\n position={element.style.position}\n onDrag={(position) =\u003E {\n setElement({\n ...element,\n style: {\n ...element.style,\n position,\n },\n })\n }}\n \u003E\n \u003Cdiv\u003E\n \u003CSuspense fallback={\u003CRectangleLoading selected={selected} \u002F\u003E}\u003E\n \u003CRectangleInner selected={selected} id={id} \u002F\u003E\n \u003C\u002FSuspense\u003E\n \u003C\u002Fdiv\u003E\n \u003C\u002FDrag\u003E\n \u003C\u002FResize\u003E\n \u003C\u002FRectangleContainer\u003E\n )\n}\n","id":"ab93173b-49be-4e5a-8b9c-77be1e1bd089","is_binary":false,"title":"Rectangle.tsx","sha":null,"inserted_at":"2022-05-14T17:59:31","updated_at":"2022-05-15T06:58:00","upload_id":null,"shortid":"BykeY9xeL-d","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"SyMYqxgUZ_"},{"code":"import {Button} from '@chakra-ui\u002Fbutton'\nimport {Container, Heading, Text} from '@chakra-ui\u002Flayout'\nimport {Select} from '@chakra-ui\u002Fselect'\nimport {Suspense, useState} from 'react'\nimport {ErrorBoundary, FallbackProps} from 'react-error-boundary'\nimport {atomFamily, selectorFamily, useRecoilValue, useSetRecoilState} from 'recoil'\nimport {getWeather} from '.\u002FfakeAPI'\n\nconst userState = selectorFamily({\n key: 'user',\n get: (userId: number) =\u003E async () =\u003E {\n const userData = await fetch(`https:\u002F\u002Fjsonplaceholder.typicode.com\u002Fusers\u002F${userId}`).then((res) =\u003E res.json())\n if (userId === 4) throw new Error('User does not exist')\n return userData\n },\n})\n\nconst UserWeather = ({userId}: {userId: number}) =\u003E {\n const user = useRecoilValue(userState(userId))\n const weather = useRecoilValue(weatherState(userId))\n const refresh = useRefreshWeather(userId)\n\n return (\n \u003Cdiv\u003E\n \u003CText\u003E\n \u003Cb\u003EWeather in {user.address.city}:\u003C\u002Fb\u003E {weather}ΒΊC\n \u003C\u002FText\u003E\n \u003CText onClick={refresh}\u003E(refresh weather)\u003C\u002FText\u003E\n \u003C\u002Fdiv\u003E\n )\n}\nconst UserData = ({userId}: {userId: number}) =\u003E {\n const user = useRecoilValue(userState(userId))\n if (!user) return null\n\n return (\n \u003Cdiv\u003E\n \u003CHeading as=\"h2\" size=\"md\" mb={1}\u003E\n User data:\n \u003C\u002FHeading\u003E\n \u003CText\u003E\n \u003Cb\u003EName:\u003C\u002Fb\u003E {user.name}\n \u003C\u002FText\u003E\n \u003CText\u003E\n \u003Cb\u003EPhone:\u003C\u002Fb\u003E {user.phone}\n \u003C\u002FText\u003E\n \u003CSuspense fallback={\u003Cdiv\u003ELoading weather...\u003C\u002Fdiv\u003E}\u003E\n \u003CUserWeather userId={userId} \u002F\u003E\n \u003C\u002FSuspense\u003E\n \u003C\u002Fdiv\u003E\n )\n}\n\nconst weatherFetchIdState = atomFamily({\n key: 'weatherFetchId',\n default: 0,\n})\n\nconst useRefreshWeather = (userId: number) =\u003E {\n const setFetchId = useSetRecoilState(weatherFetchIdState(userId))\n return () =\u003E setFetchId((id) =\u003E id + 1)\n}\n\nconst weatherState = selectorFamily({\n key: 'weather',\n get: (userId: number) =\u003E async ({get}) =\u003E {\n get(weatherFetchIdState(userId))\n\n const user = get(userState(userId))\n const weather = await getWeather(user.address.city)\n return weather\n },\n})\n\nconst ErrorFallback = ({error, resetErrorBoundary}: FallbackProps) =\u003E {\n return (\n \u003Cdiv\u003E\n \u003CHeading as=\"h2\" size=\"md\" mb={1}\u003E\n Something went wrong\n \u003C\u002FHeading\u003E\n \u003CText mb={1}\u003E{error.message}\u003C\u002FText\u003E\n \u003CButton onClick={resetErrorBoundary}\u003EOk\u003C\u002FButton\u003E\n \u003C\u002Fdiv\u003E\n )\n}\n\nconst Async = () =\u003E {\n const [userId, setUserId] = useState\u003Cundefined | number\u003E(undefined)\n\n console.log('userId', userId)\n\n return (\n \u003CContainer py={10}\u003E\n \u003CHeading as=\"h1\" mb={4}\u003E\n View Profile\n \u003C\u002FHeading\u003E\n \u003CHeading as=\"h2\" size=\"md\" mb={1}\u003E\n Choose a user:\n \u003C\u002FHeading\u003E\n \u003CSelect\n placeholder=\"Choose a user\"\n mb={4}\n value={userId}\n onChange={(event) =\u003E {\n const value = event.target.value\n setUserId(value ? parseInt(value) : undefined)\n }}\n \u003E\n \u003Coption value=\"1\"\u003EUser 1\u003C\u002Foption\u003E\n \u003Coption value=\"2\"\u003EUser 2\u003C\u002Foption\u003E\n \u003Coption value=\"3\"\u003EUser 3\u003C\u002Foption\u003E\n \u003Coption value=\"4\"\u003EUser 4 (Throws)\u003C\u002Foption\u003E\n \u003C\u002FSelect\u003E\n \u003CErrorBoundary\n FallbackComponent={ErrorFallback}\n resetKeys={[userId]}\n onReset={() =\u003E {\n setUserId(undefined)\n }}\n \u003E\n {userId !== undefined && (\n \u003CSuspense fallback={\u003Cdiv\u003ELoading...\u003C\u002Fdiv\u003E}\u003E\n \u003CUserData userId={userId} \u002F\u003E\n \u003C\u002FSuspense\u003E\n )}\n \u003C\u002FErrorBoundary\u003E\n \u003C\u002FContainer\u003E\n )\n}\n\nexport default Async\n","id":"139f7be1-4f96-4aeb-a9af-8d67baf12e11","is_binary":false,"title":"Async.tsx","sha":null,"inserted_at":"2022-05-15T04:42:37","updated_at":"2022-05-15T05:26:01","upload_id":null,"shortid":"4GOW6","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"JKNlo"},{"code":"{\n \"name\": \"learning-recoil\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"dependencies\": {\n \"@chakra-ui\u002Freact\": \"^1.1.4\",\n \"@emotion\u002Freact\": \"^11.1.4\",\n \"@emotion\u002Fstyled\": \"^11.0.0\",\n \"@testing-library\u002Fjest-dom\": \"^5.11.4\",\n \"@testing-library\u002Freact\": \"^11.1.0\",\n \"@testing-library\u002Fuser-event\": \"^12.1.10\",\n \"@types\u002Fjest\": \"^26.0.15\",\n \"@types\u002Fnode\": \"^12.0.0\",\n \"@types\u002Freact\": \"^16.9.53\",\n \"@types\u002Freact-dom\": \"^16.9.8\",\n \"@types\u002Freact-resizable\": \"^1.7.2\",\n \"@types\u002Freact-router-dom\": \"^5.1.7\",\n \"browser-nativefs\": \"^0.12.0\",\n \"framer-motion\": \"^3.2.1\",\n \"immer\": \"9.0.14\",\n \"lodash\": \"4.17.21\",\n \"nanoevents\": \"^5.1.10\",\n \"react\": \"^17.0.1\",\n \"react-dom\": \"^17.0.1\",\n \"react-draggable\": \"^4.4.3\",\n \"react-error-boundary\": \"3.1.4\",\n \"react-feather\": \"2.0.9\",\n \"react-resizable\": \"^1.11.0\",\n \"react-router-dom\": \"^5.2.0\",\n \"react-scripts\": \"4.0.1\",\n \"recoil\": \"0.7.3-alpha.2\",\n \"typescript\": \"^4.0.3\",\n \"web-vitals\": \"^0.2.4\"\n },\n \"scripts\": {\n \"start\": \"react-scripts start\",\n \"build\": \"react-scripts build\",\n \"test\": \"react-scripts test\",\n \"eject\": \"react-scripts eject\"\n },\n \"eslintConfig\": {\n \"extends\": [\n \"react-app\",\n \"react-app\u002Fjest\"\n ]\n },\n \"browserslist\": {\n \"production\": [\n \"\u003E0.2%\",\n \"not dead\",\n \"not op_mini all\"\n ],\n \"development\": [\n \"last 1 chrome version\",\n \"last 1 firefox version\",\n \"last 1 safari version\"\n ]\n },\n \"keywords\": [],\n \"description\": \"\"\n}","id":"8aa0e280-32bb-4946-ad16-7d8b39886d0c","is_binary":false,"title":"package.json","sha":null,"inserted_at":"2022-05-14T17:59:31","updated_at":"2022-05-15T06:10:27","upload_id":null,"shortid":"SJ8F5elI-u","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":null},{"code":"import {Box, Spinner} from '@chakra-ui\u002Freact'\nimport {getBorderColor} from '..\u002F..\u002Futil'\n\nexport const RectangleLoading = ({selected}: {selected: boolean}) =\u003E {\n return (\n \u003CBox\n position=\"absolute\"\n border={`1px solid ${getBorderColor(selected)}`}\n transition=\"0.1s border-color ease-in-out\"\n width=\"100%\"\n height=\"100%\"\n display=\"flex\"\n padding=\"2px\"\n \u003E\n \u003CBox\n flex=\"1\"\n border=\"3px dashed #101010\"\n borderRadius=\"255px 15px 225px 15px\u002F15px 225px 15px 255px\"\n backgroundColor=\"white\"\n display=\"flex\"\n alignItems=\"center\"\n justifyContent=\"center\"\n \u003E\n \u003CSpinner size=\"lg\" thickness=\"3px\" \u002F\u003E\n \u003C\u002FBox\u003E\n \u003C\u002FBox\u003E\n )\n}\n","id":"b439926c-7311-47c5-b3aa-94ef07269c83","is_binary":false,"title":"RectangleLoading.tsx","sha":null,"inserted_at":"2022-05-15T06:13:25","updated_at":"2022-05-15T06:13:34","upload_id":null,"shortid":"p28Ar","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"SyMYqxgUZ_"},{"code":"import queryString, {ParsedUrlQueryInput} from 'querystring'\n\ntype RequestOptions = {\n queryParams?: ParsedUrlQueryInput\n method?: 'GET' | 'POST'\n body?: object | string\n}\n\nexport const apiUrl = (lambda: string, queryParams?: ParsedUrlQueryInput) =\u003E {\n let url = `https:\u002F\u002Ff10adraov8.execute-api.us-east-1.amazonaws.com\u002Fdev\u002F${lambda}`\n if (queryParams) url += '?' + queryString.stringify(queryParams)\n\n return url\n}\n\nexport const callApi = (lambda: string, options?: RequestOptions) =\u003E {\n const {queryParams, body, method} = options || {}\n const url = apiUrl(lambda, queryParams)\n\n let bodyString = body\n if (typeof bodyString === 'object') {\n bodyString = JSON.stringify(body)\n }\n\n return fetch(url, {body: bodyString, method}).then((res) =\u003E res.json())\n}\n","id":"650de6bd-417e-47ff-aa11-baac501ed3bb","is_binary":false,"title":"api.tsx","sha":null,"inserted_at":"2022-05-15T06:12:41","updated_at":"2022-05-15T06:12:52","upload_id":null,"shortid":"AD5k9","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"HkgYqxg8-d"},{"code":"import {apiUrl} from '.\u002Fapi'\n\nexport const getBorderColor = (visible: boolean) =\u003E {\n return visible ? '#CCC' : 'transparent'\n}\n\n\u002F**\n * Returns the width and height for the specified image.\n *\u002F\nexport const getImageDimensions = (\n src: string,\n): Promise\u003C{\n width: number\n height: number\n}\u003E =\u003E {\n return new Promise\u003C{width: number; height: number}\u003E((resolve, reject) =\u003E {\n const image = new Image()\n image.onload = () =\u003E {\n resolve({width: image.width, height: image.height})\n }\n image.onerror = (error) =\u003E {\n reject(error)\n }\n image.src = src\n })\n}\n\n\u002F**\n * A function that returns a random image URL and that image's\n * id, which can be used to refer back to that image in API requests.\n *\u002F\nexport const getRandomImage = (): {\n src: string\n id: number\n} =\u003E {\n const id = Date.now()\n return {src: apiUrl('random-image', {seed: id}), id}\n}\n","id":"dcf2c95a-192a-4ff0-8d06-40a74ee001f3","is_binary":false,"title":"util.tsx","sha":null,"inserted_at":"2022-05-14T17:59:31","updated_at":"2022-05-15T06:14:55","upload_id":null,"shortid":"HJdxKqlxUZd","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"HkgYqxg8-d"},{"code":"import {Icon, IconButton, VStack} from '@chakra-ui\u002Freact'\nimport {Image, Square} from 'react-feather'\nimport {atom, useRecoilCallback, useRecoilValue} from 'recoil'\nimport {defaultStyle, elementState} from '.\u002Fcomponents\u002FRectangle\u002FRectangle'\nimport {getRandomImage} from '.\u002Futil'\n\nexport const elementsState = atom\u003Cnumber[]\u003E({\n key: 'elements',\n default: [],\n})\n\nexport const Toolbar = () =\u003E {\n const elements = useRecoilValue(elementsState)\n const newId = elements.length\n\n const insertElement = useRecoilCallback(\n ({set}) =\u003E (type: 'rectangle' | 'image') =\u003E {\n set(elementsState, (e) =\u003E [...e, e.length])\n\n if (type === 'image') {\n set(elementState(newId), {\n style: defaultStyle,\n image: getRandomImage(),\n })\n }\n },\n [newId],\n )\n\n return (\n \u003CVStack\n position=\"absolute\"\n top=\"20px\"\n left=\"20px\"\n backgroundColor=\"white\"\n padding={2}\n boxShadow=\"md\"\n borderRadius=\"md\"\n spacing={2}\n \u003E\n \u003CIconButton\n onClick={() =\u003E insertElement('rectangle')}\n aria-label=\"Add rectangle\"\n icon={\u003CIcon style={{width: 24, height: 24}} as={Square} \u002F\u003E}\n \u002F\u003E\n \u003CIconButton\n onClick={() =\u003E insertElement('image')}\n aria-label=\"Add image\"\n icon={\u003CIcon style={{width: 24, height: 24}} as={Image} \u002F\u003E}\n \u002F\u003E\n \u003C\u002FVStack\u003E\n )\n}\n","id":"02ce8fa4-def9-4a8b-87df-34d42cbc67bc","is_binary":false,"title":"Toolbar.tsx","sha":null,"inserted_at":"2022-05-14T17:59:31","updated_at":"2022-05-15T06:58:35","upload_id":null,"shortid":"rknFceeIZu","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"HkgYqxg8-d"},{"code":"import {Box} from '@chakra-ui\u002Freact'\nimport {useEffect} from 'react'\nimport {selectorFamily, useRecoilValue, useSetRecoilState} from 'recoil'\nimport {editProperty} from '..\u002F..\u002FEditProperties'\nimport {getBorderColor, getImageDimensions} from '..\u002F..\u002Futil'\nimport {Element, elementState} from '.\u002FRectangle'\n\nconst imageSizeState = selectorFamily({\n key: 'imageSize',\n get: (src?: string) =\u003E () =\u003E {\n if (!src) return\n return getImageDimensions(src)\n },\n})\n\nexport const RectangleInner = ({selected, id}: {selected: boolean; id: number}) =\u003E {\n const element = useRecoilValue(elementState(id))\n const imageSize = useRecoilValue(imageSizeState(element.image?.src))\n const setSize = useSetRecoilState\u003CElement['style']['size']\u003E(editProperty({id, path: 'style.size'}))\n\n useEffect(() =\u003E {\n if (imageSize) setSize(imageSize)\n }, [imageSize, setSize])\n\n return (\n \u003CBox\n position=\"absolute\"\n border={`1px solid ${getBorderColor(selected)}`}\n transition=\"0.1s border-color ease-in-out\"\n width=\"100%\"\n height=\"100%\"\n display=\"flex\"\n padding=\"2px\"\n \u003E\n \u003CBox\n flex=\"1\"\n border=\"3px dashed #101010\"\n borderRadius=\"255px 15px 225px 15px\u002F15px 225px 15px 255px\"\n backgroundColor=\"white\"\n backgroundImage={`url('${element.image?.src}')`}\n backgroundSize=\"cover\"\n \u002F\u003E\n \u003C\u002FBox\u003E\n )\n}\n","id":"00fc408e-d237-44bd-8270-03dc56377183","is_binary":false,"title":"RectangleInner.tsx","sha":null,"inserted_at":"2022-05-14T17:59:31","updated_at":"2022-05-15T07:01:17","upload_id":null,"shortid":"B1-xt9xlIZO","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"SyMYqxgUZ_"},{"code":"import {InputGroup, InputRightElement, NumberInput, NumberInputField, Text, VStack} from '@chakra-ui\u002Freact'\nimport produce from 'immer'\nimport _ from 'lodash'\nimport {selector, selectorFamily, useRecoilState, useRecoilValue} from 'recoil'\nimport {elementState, selectedElementState} from '.\u002Fcomponents\u002FRectangle\u002FRectangle'\nimport {ImageInfo, ImageInfoFallback} from '.\u002Fcomponents\u002FImageInfo'\nimport {Suspense} from 'react'\n\nexport const editProperty = selectorFamily\u003Cany, {path: string; id: number}\u003E({\n key: 'editProperty',\n\n get: ({path, id}) =\u003E ({get}) =\u003E {\n const element = get(elementState(id))\n return _.get(element, path)\n },\n\n set: ({path, id}) =\u003E ({get, set}, newValue) =\u003E {\n const element = get(elementState(id))\n const newElement = produce(element, (draft) =\u003E {\n _.set(draft, path, newValue)\n })\n set(elementState(id), newElement)\n },\n})\n\nconst editSize = selectorFamily\u003Cany, {dimension: 'width' | 'height'; id: number}\u003E({\n key: 'editSize',\n\n get: ({dimension, id}) =\u003E ({get}) =\u003E {\n return get(editProperty({path: `style.size.${dimension}`, id}))\n },\n\n set: ({dimension, id}) =\u003E ({get, set}, newValue) =\u003E {\n const hasImage = get(editProperty({path: 'image', id})) !== undefined\n if (!hasImage) {\n set(editProperty({path: `style.size.${dimension}`, id}), newValue)\n return\n }\n\n const {width, height} = get(editProperty({path: 'style.size', id}))\n const aspectRatio = width \u002F height\n\n if (dimension === 'width') {\n set(editProperty({path: 'style.size', id}), {\n width: newValue,\n height: Math.round(newValue \u002F aspectRatio),\n })\n } else {\n set(editProperty({path: 'style.size', id}), {\n height: newValue,\n width: Math.round(newValue * aspectRatio),\n })\n }\n },\n})\n\nexport const hasImageState = selector({\n key: 'hasImage',\n get: ({get}) =\u003E {\n const id = get(selectedElementState)\n if (id == null) return false\n return get(elementState(id)).image !== undefined\n },\n})\n\nexport const EditProperties = () =\u003E {\n const selectedElement = useRecoilValue(selectedElementState)\n const hasImage = useRecoilValue(hasImageState)\n if (selectedElement == null) return null\n\n return (\n \u003CCard\u003E\n \u003CSection heading=\"Position\"\u003E\n \u003CProperty label=\"Top\" path=\"style.position.top\" id={selectedElement} \u002F\u003E\n \u003CProperty label=\"Left\" path=\"style.position.left\" id={selectedElement} \u002F\u003E\n \u003C\u002FSection\u003E\n \u003CSection heading=\"Size\"\u003E\n \u003CSizeProperty label=\"Width\" dimension=\"width\" id={selectedElement} \u002F\u003E\n \u003CSizeProperty label=\"Height\" dimension=\"height\" id={selectedElement} \u002F\u003E\n \u003C\u002FSection\u003E\n {hasImage && (\n \u003CSection heading=\"Image\"\u003E\n \u003CSuspense fallback={\u003CImageInfoFallback \u002F\u003E}\u003E\n \u003CImageInfo \u002F\u003E\n \u003C\u002FSuspense\u003E\n \u003C\u002FSection\u003E\n )}\n \u003C\u002FCard\u003E\n )\n}\n\nconst Section: React.FC\u003C{heading: string}\u003E = ({heading, children}) =\u003E {\n return (\n \u003CVStack spacing={2} align=\"flex-start\"\u003E\n \u003CText fontWeight=\"500\"\u003E{heading}\u003C\u002FText\u003E\n {children}\n \u003C\u002FVStack\u003E\n )\n}\n\nconst SizeProperty = ({label, dimension, id}: {label: string; dimension: 'width' | 'height'; id: number}) =\u003E {\n const [value, setValue] = useRecoilState\u003Cnumber\u003E(editSize({dimension, id}))\n return \u003CPropertyInput label={label} value={value} onChange={setValue} \u002F\u003E\n}\n\nconst Property = ({label, path, id}: {label: string; path: string; id: number}) =\u003E {\n const [value, setValue] = useRecoilState\u003Cnumber\u003E(editProperty({path, id}))\n return \u003CPropertyInput label={label} value={value} onChange={setValue} \u002F\u003E\n}\n\nconst PropertyInput = ({label, value, onChange}: {label: string; value: number; onChange: (value: number) =\u003E void}) =\u003E {\n return (\n \u003Cdiv\u003E\n \u003CText fontSize=\"14px\" fontWeight=\"500\" mb=\"2px\"\u003E\n {label}\n \u003C\u002FText\u003E\n \u003CInputGroup size=\"sm\" variant=\"filled\"\u003E\n \u003CNumberInput value={value} onChange={(_, value) =\u003E onChange(value)}\u003E\n \u003CNumberInputField borderRadius=\"md\" \u002F\u003E\n \u003CInputRightElement pointerEvents=\"none\" children=\"px\" lineHeight=\"1\" fontSize=\"12px\" \u002F\u003E\n \u003C\u002FNumberInput\u003E\n \u003C\u002FInputGroup\u003E\n \u003C\u002Fdiv\u003E\n )\n}\n\nconst Card: React.FC = ({children}) =\u003E (\n \u003CVStack\n position=\"absolute\"\n top=\"20px\"\n right=\"20px\"\n backgroundColor=\"white\"\n padding={2}\n boxShadow=\"md\"\n borderRadius=\"md\"\n spacing={3}\n align=\"flex-start\"\n onClick={(e) =\u003E e.stopPropagation()}\n \u003E\n {children}\n \u003C\u002FVStack\u003E\n)\n","id":"78497343-f2f5-4ba4-b34e-a9b1ec5884f5","is_binary":false,"title":"EditProperties.tsx","sha":null,"inserted_at":"2022-05-14T18:54:38","updated_at":"2022-05-15T07:01:48","upload_id":null,"shortid":"l5LW5","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"HkgYqxg8-d"},{"code":"import {Box, Text, VStack} from '@chakra-ui\u002Flayout'\nimport {Skeleton} from '@chakra-ui\u002Fskeleton'\nimport {selector, useRecoilValue} from 'recoil'\nimport {callApi} from '..\u002Fapi'\nimport {elementState, selectedElementState} from '.\u002FRectangle\u002FRectangle'\n\nconst imageIdState = selector({\n key: 'imageId',\n get: ({get}) =\u003E {\n const id = get(selectedElementState)\n if (id == null) return\n return get(elementState(id)).image?.id\n },\n})\n\nconst imageInfoState = selector({\n key: 'imageInfo',\n get: ({get}) =\u003E {\n const imageId = get(imageIdState)\n if (imageId == null) return\n return callApi('image-details', {queryParams: {seed: imageId}})\n },\n})\n\nexport const ImageInfo = () =\u003E {\n const imageDetails = useRecoilValue(imageInfoState)\n\n return (\n \u003CVStack spacing={2} alignItems=\"flex-start\" width=\"100%\"\u003E\n \u003CInfo label=\"Author\" value={imageDetails.author} \u002F\u003E\n \u003CInfo label=\"Image URL\" value={imageDetails.url} \u002F\u003E\n \u003C\u002FVStack\u003E\n )\n}\n\nexport const ImageInfoFallback = () =\u003E {\n return (\n \u003CVStack spacing={2} alignItems=\"flex-start\" width=\"100%\"\u003E\n \u003CInfo label=\"Author\" \u002F\u003E\n \u003CInfo label=\"Image URL\" \u002F\u003E\n \u003C\u002FVStack\u003E\n )\n}\n\nexport const Info = ({label, value}: {label: string; value?: string}) =\u003E {\n return (\n \u003CBox width=\"175px\"\u003E\n \u003CText fontSize=\"14px\" fontWeight=\"500\" mb=\"2px\"\u003E\n {label}\n \u003C\u002FText\u003E\n {value === undefined ? \u003CSkeleton width=\"100%\" height=\"21px\" \u002F\u003E : \u003CText fontSize=\"14px\"\u003E{value}\u003C\u002FText\u003E}\n \u003C\u002FBox\u003E\n )\n}\n","id":"aedba9e2-44f1-4102-82c3-8fc8ed2b8224","is_binary":false,"title":"ImageInfo.tsx","sha":null,"inserted_at":"2022-05-15T07:01:58","updated_at":"2022-05-15T07:02:07","upload_id":null,"shortid":"P5PR1","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"ryWYcex8Zu"},{"code":"import {Button} from '@chakra-ui\u002Fbutton'\nimport {Input} from '@chakra-ui\u002Finput'\nimport {Box, Divider, Heading, VStack} from '@chakra-ui\u002Flayout'\nimport produce from 'immer'\nimport React, {useState} from 'react'\nimport {atom, DefaultValue, useRecoilState, useResetRecoilState} from 'recoil'\n\ntype ItemType = {\n label: string\n checked: boolean\n}\n\nconst shoppingListState = atom\u003CItemType[]\u003E({\n key: 'shoppingList',\n default: [],\n effects: [\n ({onSet, setSelf}) =\u003E {\n const storedItems = localStorage.getItem('shoppingList')\n if (storedItems != null) {\n setSelf(JSON.parse(storedItems))\n }\n\n onSet((newItems) =\u003E {\n if (newItems instanceof DefaultValue) {\n localStorage.removeItem('shoppingList')\n } else {\n localStorage.setItem('shoppingList', JSON.stringify(newItems))\n }\n })\n },\n ],\n})\n\nconst AtomEffects = () =\u003E {\n const [items, setItems] = useRecoilState(shoppingListState)\n const resetList = useResetRecoilState(shoppingListState)\n\n const toggleItem = (index: number) =\u003E {\n setItems(\n produce(items, (draftItems) =\u003E {\n draftItems[index].checked = !draftItems[index].checked\n }),\n )\n }\n\n const insertItem = (label: string) =\u003E {\n setItems([...items, {label, checked: false}])\n }\n\n return (\n \u003CContainer onClear={() =\u003E resetList()}\u003E\n {items.map((item, index) =\u003E (\n \u003CItem\n key={item.label}\n label={item.label}\n checked={item.checked}\n onClick={() =\u003E {\n toggleItem(index)\n }}\n \u002F\u003E\n ))}\n \u003CNewItemInput\n onInsert={(label) =\u003E {\n insertItem(label)\n }}\n \u002F\u003E\n \u003C\u002FContainer\u003E\n )\n}\nexport default AtomEffects\n\nconst Container: React.FC\u003C{onClear: () =\u003E void}\u003E = ({children, onClear}) =\u003E {\n return (\n \u003CBox display=\"flex\" flexDir=\"column\" alignItems=\"center\" pt={10}\u003E\n \u003CBox width=\"400px\" backgroundColor=\"yellow.100\" p={5} borderRadius=\"lg\"\u003E\n \u003CHeading size=\"lg\" mb={4}\u003E\n Shopping List\n \u003C\u002FHeading\u003E\n \u003CVStack spacing={3} divider={\u003CDivider borderColor=\"rgba(86, 0, 0, 0.48)\" \u002F\u003E}\u003E\n {children}\n \u003C\u002FVStack\u003E\n \u003C\u002FBox\u003E\n \u003CButton variant=\"link\" mt={3} onClick={onClear}\u003E\n Clear list\n \u003C\u002FButton\u003E\n \u003C\u002FBox\u003E\n )\n}\n\ntype ItemProps = {\n label: string\n checked: boolean\n onClick: () =\u003E void\n}\n\nconst Item = ({label, checked, onClick}: ItemProps) =\u003E {\n return (\n \u003CBox\n rounded=\"md\"\n textDecoration={checked ? 'line-through' : ''}\n opacity={checked ? 0.5 : 1}\n _hover={{textDecoration: 'line-through'}}\n cursor=\"pointer\"\n width=\"100%\"\n onClick={onClick}\n \u003E\n {label}\n \u003C\u002FBox\u003E\n )\n}\n\nconst NewItemInput = ({onInsert}: {onInsert: (label: string) =\u003E void}) =\u003E {\n const [value, setValue] = useState('')\n\n return (\n \u003CInput\n value={value}\n placeholder=\"New item\"\n padding={0}\n height=\"auto\"\n border=\"none\"\n _focus={{border: 'none'}}\n _placeholder={{color: 'rgba(86, 0, 0, 0.48)'}}\n onChange={(e) =\u003E {\n setValue(e.currentTarget.value)\n }}\n onKeyPress={({key}) =\u003E {\n if (key === 'Enter') {\n onInsert(value)\n setValue('')\n }\n }}\n \u002F\u003E\n )\n}\n","id":"db78d9d4-f7bd-47f0-8ba0-054473072602","is_binary":false,"title":"AtomEffects.tsx","sha":null,"inserted_at":"2022-05-15T11:14:11","updated_at":"2022-05-15T13:00:00","upload_id":null,"shortid":"2klL1","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"JKNlo"},{"code":"const randomDelay = () =\u003E {\n return new Promise\u003Cvoid\u003E((resolve) =\u003E {\n setTimeout(resolve, randomIntBetween(500, 3000))\n })\n}\n\nexport const getWeather = async (zipCode: string) =\u003E {\n await randomDelay()\n\n if (!getWeatherCache[zipCode]) {\n getWeatherCache[zipCode] = randomIntBetween(5, 35)\n } else {\n getWeatherCache[zipCode] += randomIntBetween(-1, 2)\n }\n\n return getWeatherCache[zipCode]\n}\n\nconst getWeatherCache: Record\u003Cstring, number\u003E = {}\n\nfunction randomIntBetween(min: number, max: number) {\n return Math.floor(Math.random() * (max - min + 1) + min)\n}\n\ntype ItemType = {\n label: string\n checked: boolean\n}\n\nclass ShoppingListAPI {\n items: Record\u003Cnumber, ItemType\u003E\n\n constructor() {\n const persisted = localStorage.getItem('ShoppingListAPI')\n if (persisted == null) {\n this.items = {}\n } else {\n this.items = JSON.parse(persisted)\n }\n }\n\n async getItems() {\n await this.randomDelay('getItems')\n return this.items\n }\n\n async getItem(id: number): Promise\u003CItemType | undefined\u003E {\n await this.randomDelay('getItem', id)\n return this.items[id]\n }\n\n async createOrUpdateItem(id: number, item: ItemType) {\n await this.randomDelay('createOrUpdateItem', id)\n this.items[id] = item\n this.persist()\n }\n\n async deleteItem(id: number) {\n await this.randomDelay('deleteItem', id)\n delete this.items[id]\n this.persist()\n }\n\n private async randomDelay(name: string, param?: number) {\n let label = `Fake Request: ${name}.`\n if (param != null) label += ` id: ${param}`\n\n await randomDelay()\n console.log(`End ${label}`)\n }\n\n private persist() {\n localStorage.setItem('ShoppingListAPI', JSON.stringify(this.items))\n }\n}\n\nexport const shoppingListAPI = new ShoppingListAPI()\n","id":"226f54de-54ac-44fe-b3be-561accac5f17","is_binary":false,"title":"fakeAPI.ts","sha":null,"inserted_at":"2022-05-15T05:24:06","updated_at":"2022-05-15T13:14:31","upload_id":null,"shortid":"2gj4A","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"JKNlo"},{"code":"import {ChakraProvider} from '@chakra-ui\u002Freact'\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport {BrowserRouter as Router, Route, Switch} from 'react-router-dom'\nimport {RecoilRoot} from 'recoil'\nimport Canvas from '.\u002FCanvas'\nimport {Atoms} from '.\u002Fexamples\u002FAtoms'\nimport {Selectors} from '.\u002Fexamples\u002FSelectors'\nimport Async from '.\u002Fexamples\u002FAsync'\nimport AtomEffects from '.\u002Fexamples\u002FAtomEffects'\nimport AtomEffectsWithFamilies from '.\u002Fexamples\u002FAtomEffectsWithFamilies'\nimport '.\u002Findex.css'\n\u002F\u002F AtomEffects\nReactDOM.render(\n \u003CReact.StrictMode\u003E\n \u003CRecoilRoot\u003E\n \u003CChakraProvider\u003E\n \u003CRouter\u003E\n \u003CSwitch\u003E\n \u003CRoute path=\"\u002Fexamples\u002Fatoms\"\u003E\n \u003CAtoms \u002F\u003E\n \u003C\u002FRoute\u003E\n \u003CRoute path=\"\u002Fexamples\u002Fselectors\"\u003E\n \u003CSelectors \u002F\u003E\n \u003C\u002FRoute\u003E\n \u003CRoute path=\"\u002Fexamples\u002Fasync\"\u003E\n \u003CAsync \u002F\u003E\n \u003C\u002FRoute\u003E\n \u003CRoute path=\"\u002Fexamples\u002Fatomeffects\"\u003E\n \u003CAtomEffects \u002F\u003E\n \u003C\u002FRoute\u003E\n \u003CRoute path=\"\u002Fexamples\u002Fatomeffectswithfamilies\"\u003E\n \u003CReact.Suspense fallback={'loading....'}\u003E\n \u003CAtomEffectsWithFamilies \u002F\u003E\n \u003C\u002FReact.Suspense\u003E\n \u003C\u002FRoute\u003E\n \u003CRoute\u003E\n \u003CCanvas \u002F\u003E\n \u003C\u002FRoute\u003E\n \u003C\u002FSwitch\u003E\n \u003C\u002FRouter\u003E\n \u003C\u002FChakraProvider\u003E\n \u003C\u002FRecoilRoot\u003E\n \u003C\u002FReact.StrictMode\u003E,\n document.getElementById('root'),\n)\n","id":"2d5dd5fe-0cb7-4e6b-a2a8-92449d36e80f","is_binary":false,"title":"index.tsx","sha":null,"inserted_at":"2022-05-14T17:59:31","updated_at":"2022-05-15T13:16:14","upload_id":null,"shortid":"SJEetcggI-u","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"HkgYqxg8-d"},{"code":"import {Button} from '@chakra-ui\u002Fbutton'\nimport {Input} from '@chakra-ui\u002Finput'\nimport {Box, Divider, Heading, VStack} from '@chakra-ui\u002Flayout'\nimport React, {useState} from 'react'\nimport {\n atom,\n atomFamily,\n DefaultValue,\n useRecoilCallback,\n useRecoilState,\n useRecoilValue,\n useResetRecoilState,\n} from 'recoil'\nimport {shoppingListAPI} from '.\u002FfakeAPI'\n\ntype ItemType = {\n label: string\n checked: boolean\n}\n\u002F\u002F We look at how to avoid making an API call for each atom initialised in an atom family.\nclass CachedAPI {\n cachedItems: Record\u003Cstring, ItemType\u003E | undefined\n\n private async getItems() {\n if (this.cachedItems) return this.cachedItems\n\n this.cachedItems = await shoppingListAPI.getItems()\n return this.cachedItems\n }\n\n async getIds() {\n const items = await this.getItems()\n return Object.keys(items).map((id) =\u003E parseInt(id))\n }\n\n async getItem(id: number) {\n const items = await this.getItems()\n return items[id]\n }\n}\n\nconst cachedAPI = new CachedAPI()\n\nconst idsState = atom\u003Cnumber[]\u003E({\n key: 'ids',\n default: [],\n effects_UNSTABLE: [\n ({setSelf}) =\u003E {\n setSelf(cachedAPI.getIds())\n },\n ],\n})\n\nconst itemState = atomFamily\u003CItemType, number\u003E({\n key: 'item',\n default: {label: '', checked: false},\n effects_UNSTABLE: (id) =\u003E [\n ({onSet, setSelf, trigger}) =\u003E {\n setSelf(cachedAPI.getItem(id))\n\n onSet((item, oldItem) =\u003E {\n \u002F\u002F We look at how to prevent onSet from being called when the atom is first initialised.\n if (oldItem instanceof DefaultValue && trigger === 'get') return\n\n if (item instanceof DefaultValue) {\n shoppingListAPI.deleteItem(id)\n } else {\n shoppingListAPI.createOrUpdateItem(id, item)\n }\n })\n },\n ],\n})\n\nconst AtomEffects = () =\u003E {\n const ids = useRecoilValue(idsState)\n const resetList = useResetRecoilState(idsState)\n const nextId = ids.length\n\n const insertItem = useRecoilCallback(({set}) =\u003E (label: string) =\u003E {\n set(idsState, [...ids, nextId])\n set(itemState(nextId), {label, checked: false})\n })\n\n return (\n \u003CContainer onClear={() =\u003E resetList()}\u003E\n {ids.map((id) =\u003E (\n \u003CItem key={id} id={id} \u002F\u003E\n ))}\n \u003CNewItemInput\n onInsert={(label) =\u003E {\n insertItem(label)\n }}\n \u002F\u003E\n \u003C\u002FContainer\u003E\n )\n}\nexport default AtomEffects\nconst Container: React.FC\u003C{onClear: () =\u003E void}\u003E = ({children, onClear}) =\u003E {\n return (\n \u003CBox display=\"flex\" flexDir=\"column\" alignItems=\"center\" pt={10}\u003E\n \u003CBox width=\"400px\" backgroundColor=\"yellow.100\" p={5} borderRadius=\"lg\"\u003E\n \u003CHeading size=\"lg\" mb={4}\u003E\n Shopping List\n \u003C\u002FHeading\u003E\n \u003CVStack spacing={3} divider={\u003CDivider borderColor=\"rgba(86, 0, 0, 0.48)\" \u002F\u003E}\u003E\n {children}\n \u003C\u002FVStack\u003E\n \u003C\u002FBox\u003E\n \u003CButton variant=\"link\" mt={3} onClick={onClear}\u003E\n Clear list\n \u003C\u002FButton\u003E\n \u003C\u002FBox\u003E\n )\n}\n\ntype ItemProps = {\n id: number\n}\n\nconst Item = ({id}: ItemProps) =\u003E {\n const [item, setItem] = useRecoilState(itemState(id))\n\n return (\n \u003CBox\n rounded=\"md\"\n textDecoration={item.checked ? 'line-through' : ''}\n opacity={item.checked ? 0.5 : 1}\n _hover={{textDecoration: 'line-through'}}\n cursor=\"pointer\"\n width=\"100%\"\n onClick={() =\u003E setItem({...item, checked: !item.checked})}\n \u003E\n {item.label}\n \u003C\u002FBox\u003E\n )\n}\n\nconst NewItemInput = ({onInsert}: {onInsert: (label: string) =\u003E void}) =\u003E {\n const [value, setValue] = useState('')\n\n return (\n \u003CInput\n value={value}\n placeholder=\"New item\"\n padding={0}\n height=\"auto\"\n border=\"none\"\n _focus={{border: 'none'}}\n _placeholder={{color: 'rgba(86, 0, 0, 0.48)'}}\n onChange={(e) =\u003E {\n setValue(e.currentTarget.value)\n }}\n onKeyPress={({key}) =\u003E {\n if (key === 'Enter') {\n onInsert(value)\n setValue('')\n }\n }}\n \u002F\u003E\n )\n}\n","id":"f1c79b8e-5bc3-4d43-9d28-368e1a7c717d","is_binary":false,"title":"AtomEffectsWithFamilies.tsx","sha":null,"inserted_at":"2022-05-15T13:01:27","updated_at":"2022-05-15T16:32:11","upload_id":null,"shortid":"VJKx5","source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","directory_shortid":"JKNlo"}],"is_frozen":false,"inserted_at":"2022-05-14T17:59:31","fork_count":5,"settings":{"ai_consent":null},"updated_at":"2022-05-15T16:32:11","entry":"src\u002Findex.js","ai_consent":false,"original_git_commit_sha":"6164ac11b9b8f0216fba4e323891edc19be395e2","permissions":{"prevent_sandbox_export":false,"prevent_sandbox_leaving":false},"always_on":false,"custom_template":null,"alias":"learning-recoil-2phlq6","is_sse":false,"forked_from_sandbox":null,"source_id":"458f74c4-1a72-4a40-9afa-a4fb72720bd7","restricted":false,"author":{"id":"7cc24e88-e8f3-4ff7-82aa-f263911f26ca","name":"hyeoki","username":"devtaehyeok","avatar_url":"https:\u002F\u002Fuploads.codesandbox.io\u002Fuploads\u002Favatars\u002FkbLD-ddd.png","personal_workspace_id":"e8de257d-2075-4a14-b9f3-2fba548defcc","subscription_plan":null,"subscription_since":null},"free_plan_editing_restricted":false,"pr_number":null,"view_count":34491,"npm_dependencies":{},"restrictions":{"free_plan_editing_restricted":false,"live_sessions_restricted":true},"screenshot_url":"https:\u002F\u002Fscreenshots.codesandbox.io\u002F2phlq6\u002F123.png","id":"2phlq6","team":{"id":"e8de257d-2075-4a14-b9f3-2fba548defcc","name":"devtaehyeok","settings":{"ai_consent":{"public_sandboxes":false,"private_sandboxes":false}},"subscription_type":null,"avatar_url":"https:\u002F\u002Fuploads.codesandbox.io\u002Fuploads\u002Favatars\u002FkbLD-ddd.png"},"forked_template_sandbox":{"alias":"recoil-course-y1p97","id":"y1p97","title":"recoil-course","template":"create-react-app","inserted_at":"2021-02-14T00:36:32","updated_at":"2021-02-14T00:36:32","git":{"path":"","branch":"main","repo":"recoil-course","username":"jacques-blom","commit_sha":"6164ac11b9b8f0216fba4e323891edc19be395e2"},"privacy":0,"custom_template":{"id":"cce58a2a-45e3-4e70-a471-6efba10f1241","title":"recoil-course","color":"#61DAFB","v2":false,"url":null,"published":false,"icon_url":null,"official":false}},"room_id":null,"tags":[],"template":"create-react-app","collection":false,"like_count":0};