diff --git a/remix/app/Providers/Chakra/theme.tsx b/remix/app/Providers/Chakra/theme.tsx index aeb6ee5..b44a762 100644 --- a/remix/app/Providers/Chakra/theme.tsx +++ b/remix/app/Providers/Chakra/theme.tsx @@ -4,6 +4,49 @@ import { colors } from './colors' export const theme = extendTheme({ colors, // edit Input defaultProps + styles: { + global: { + // make all elements padding and margin animate + '*': { + transition: 'padding 0.2s ease, margin 0.2s ease' + }, + // make all elements have a transparent focus border + 'input:focus': { + boxShadow: 'none !important', + borderColor: 'transparent !important' + }, + // make all elements have a transparent focus border + 'textarea:focus': { + boxShadow: 'none !important', + borderColor: 'transparent !important' + }, + // make all elements have a transparent focus border + 'select:focus': { + boxShadow: 'none !important', + borderColor: 'transparent !important' + }, + // make all elements have a transparent focus border + 'button:focus': { + boxShadow: 'none !important', + borderColor: 'transparent !important' + }, + // make all elements have a transparent focus border + 'div:focus': { + boxShadow: 'none !important', + borderColor: 'transparent !important' + }, + // make all elements have a transparent focus border + 'a:focus': { + boxShadow: 'none !important', + borderColor: 'transparent !important' + }, + // make all elements have a transparent focus border + 'span:focus': { + boxShadow: 'none !important', + borderColor: 'transparent !important' + } + } + }, components: { Input: { defaultProps: { diff --git a/remix/app/Providers/ThingtimeProvider.tsx b/remix/app/Providers/ThingtimeProvider.tsx index 8c02be4..8524a83 100644 --- a/remix/app/Providers/ThingtimeProvider.tsx +++ b/remix/app/Providers/ThingtimeProvider.tsx @@ -19,15 +19,63 @@ try { const initialThingtime = { nav: {}, + version: 4 +} + +const userData = { settings: { showCommander: true, clearCommanderOnToggle: true, clearCommanderContextOnToggle: true + }, + 'Bottom Content': { + Content: "Edit this to your heart's desire" } } export const ThingtimeProvider = (props: any): JSX.Element => { - const [thingtime, set] = React.useState(initialThingtime) + const [thingtime, set] = React.useState({ + ...initialThingtime, + ...userData + }) + + const thingtimeRef = React.useRef(thingtime) + + // get thingtime from localstorage + React.useEffect(() => { + try { + const thingtimeFromLocalStorage = window.localStorage.getItem('thingtime') + + if (thingtimeFromLocalStorage) { + const parsed = JSON.parse(thingtimeFromLocalStorage) + if (parsed) { + const invalidLocalStorage = + !parsed.version || parsed.version < initialThingtime.version + if (!invalidLocalStorage) { + set(parsed) + } else { + const newThingtime = { + ...parsed, + ...initialThingtime + } + set(newThingtime) + } + } + } + } catch (err) { + console.error('There was an error getting thingtime from localStorage') + } + }, []) + + React.useEffect(() => { + thingtimeRef.current = thingtime + + try { + window.localStorage.setItem('thingtime', JSON.stringify(thingtime)) + } catch (err) { + console.error('There was an error saving thingtime to localStorage') + } + }, [thingtime]) const setThingtime = React.useCallback( (path, value) => { @@ -41,12 +89,22 @@ export const ThingtimeProvider = (props: any): JSX.Element => { path = sanitise(path) - // log the path and value - console.log('nik ThingtimeProvider setThingtime path', path) - console.log('nik ThingtimeProvider setThingtime value', value) - smarts.setsmart(newThingtime, path, value) + // subtract last path part from dot delimitted path + // prop1.prop2.prop3 => prop1.prop2 + const pathParts = path.split('.') + pathParts.pop() + const parentPath = pathParts.join('.') + + if (parentPath?.length) { + const parent = smarts.getsmart(newThingtime, parentPath) + + const newParent = Array.isArray(parent) ? [...parent] : { ...parent } + + smarts.setsmart(newThingtime, parentPath, newParent) + } + set(newThingtime) }, [thingtime] @@ -55,7 +113,7 @@ export const ThingtimeProvider = (props: any): JSX.Element => { const getThingtime = React.useCallback( (...args) => { const path = args[0] - if (path === 'thingtime' || path === 'tt' || !path) { + if (path === 'thingtime' || path === 'tt' || path === '.' || !path) { return thingtime } return smarts.getsmart(thingtime, path) @@ -83,7 +141,8 @@ export const ThingtimeProvider = (props: any): JSX.Element => { const value = { thingtime, setThingtime, - getThingtime + getThingtime, + thingtimeRef } return ( diff --git a/remix/app/components/Commander/Commander.tsx b/remix/app/components/Commander/Commander.tsx index 8872090..4267297 100644 --- a/remix/app/components/Commander/Commander.tsx +++ b/remix/app/components/Commander/Commander.tsx @@ -5,97 +5,202 @@ import { Thingtime } from '../Thingtime/Thingtime' import { sanitise } from '~/functions/path' export const Commander = props => { - const { thingtime, setThingtime, getThingtime } = useThingtime() - - const [contextPath, setContextPath] = React.useState() - - const [showContext, setShowContext] = React.useState(false) - - const contextValue = React.useMemo(() => { - console.log('thingtime updated!') - const ret = getThingtime(contextPath) - console.log('nik ret', ret) - return ret - }, [contextPath, getThingtime]) - - const showCommander = React.useMemo(() => { - console.log( - 'nik thingtime?.settings?.showCommander', - thingtime?.settings?.showCommander - ) - return thingtime?.settings?.showCommander - }, [thingtime?.settings?.showCommander]) - - React.useEffect(() => { - if (thingtime?.settings?.clearCommanderOnToggle) { - setValue('') - } - if (showCommander) { - inputRef?.current?.focus?.() - } - }, [showCommander, thingtime]) + const { thingtime, setThingtime, getThingtime, thingtimeRef } = useThingtime() const inputRef = React.useRef() const [value, setValue] = React.useState('') + const [contextPath, setContextPath] = React.useState() + + const [showContext, setShowContextState] = React.useState(false) + + const setShowContext = React.useCallback( + (value, from) => { + setShowContextState(value) + }, + [setShowContextState] + ) + const [suggestions, setSuggestions] = React.useState([]) + + const contextValue = React.useMemo(() => { + console.log('thingtime updated!') + const ret = getThingtime(contextPath) + return ret + }, [contextPath, getThingtime]) + + const showCommander = React.useMemo(() => { + return thingtime?.settings?.showCommander + }, [thingtime?.settings?.showCommander]) + + // watch value + React.useEffect(() => { + if (!value?.length) { + setSuggestions([]) + } + }, [value]) + + // showCommander useEffect + React.useEffect(() => { + if (showCommander) { + inputRef?.current?.focus?.() + } else { + if (thingtimeRef?.current?.settings?.clearCommanderOnToggle) { + setValue('') + } + if (thingtimeRef?.current?.settings?.clearCommanderContextOnToggle) { + setShowContext(false, 'showCommander useEffect') + } + } + }, [showCommander, thingtimeRef, setShowContext]) + const onChange = React.useCallback(e => { setValue(e.target.value) }, []) + const validSetters = React.useMemo(() => { + return ['=', ' is '] + }, []) + + const command = React.useMemo(() => { + const sanitizedCommand = sanitise(value) + + if (sanitizedCommand?.includes(validSetters[0])) { + const indexOfSplitter = sanitizedCommand?.indexOf(validSetters[0]) + const [pathRaw, valRaw] = [ + sanitizedCommand?.slice(0, indexOfSplitter), + sanitizedCommand?.slice(indexOfSplitter + validSetters[0]?.length) + ] + return [pathRaw?.trim(), valRaw?.trim()] + } else if (sanitizedCommand?.includes(validSetters[1])) { + const indexOfSplitter = sanitizedCommand?.indexOf(validSetters[1]) + const [pathRaw, valRaw] = [ + sanitizedCommand?.slice(0, indexOfSplitter), + sanitizedCommand?.slice(indexOfSplitter + validSetters[1]?.length) + ] + return [pathRaw?.trim(), valRaw?.trim()] + } + return [sanitizedCommand] + }, [value, validSetters]) + + const commandContainsPath = React.useMemo(() => { + const suggestionsIncludesSubstring = suggestions?.find(suggestion => { + return command?.includes(suggestion) + }) + return suggestionsIncludesSubstring + }, [suggestions, command]) + + const commandPath = React.useMemo(() => { + return command?.[0] + }, [command]) + + const commandValue = React.useMemo(() => { + return command?.[1] + }, [command]) + + const validQuotations = React.useMemo(() => { + return ['"', "'"] + }, []) + + const escapedCommandValue = React.useMemo(() => { + // replace quotations with escaped quoations except for first and last quotation + const startingQuotation = commandValue?.[0] + const endingQuotation = commandValue?.[commandValue?.length - 1] + const isQuoted = + validQuotations?.includes(startingQuotation) && + validQuotations?.includes(endingQuotation) + const restOfCommandValue = isQuoted + ? commandValue?.slice(1, commandValue?.length - 1) + : commandValue + const escaped = restOfCommandValue + ?.replace(/"/g, '\\"') + ?.replace(/'/g, "\\'") + const ret = `"${escaped}"` + return ret + }, [commandValue, validQuotations]) + + const commandIsAction = React.useMemo(() => { + return commandPath && commandValue + }, [commandPath, commandValue]) + + // thingtime changes update suggestions + React.useEffect(() => { + // when thingtime changes, update suggestions + // with all flattened key path values recursively + + if (value?.length) { + const suggestions = ['tt', 'thingtime', '.'] + const recurse = (obj, path) => { + Object.keys(obj).forEach(key => { + const val = obj[key] + const newPath = path ? `${path}.${key}` : key + if (typeof val === 'object') { + suggestions.push(key) + recurse(val, newPath) + } else { + suggestions.push(newPath) + } + }) + } + recurse(thingtime, '') + + if (commandPath) { + const filteredSuggestions = suggestions.filter((suggestion, i) => { + return suggestion?.toLowerCase()?.includes(commandPath?.toLowerCase()) + }) + if (!filteredSuggestions?.includes(commandPath)) { + const adjustedSuggestions = [commandPath, ...filteredSuggestions] + setSuggestions(adjustedSuggestions) + } else { + setSuggestions(filteredSuggestions) + } + } else { + setSuggestions(suggestions) + } + + // if (value) { + // setShowContext(true, 'Thingtime changes update suggestions') + // } + } + }, [thingtime, value, commandPath, setShowContext]) + const onEnter = React.useCallback( props => { // if first characters of value equal tt. then run command // or if first character is a dot then run command try { - const isTT = value?.slice(0, 3) === 'tt.' - const isDot = value?.slice(0, 1) === '.' - const executeCommand = isTT || isDot - if (executeCommand) { - const command = isTT ? value?.slice(3) : value?.slice(1) - const sanitisedCommand = sanitise(command) - console.log('nik command', command) - - console.log('setting to thingtime', thingtime) - - const commandIsSetter = command?.includes('=') - - if (commandIsSetter) { - // nothing - const [pathRaw, valRaw] = sanitisedCommand?.split('=') - const path = pathRaw.trim() - const val = valRaw.trim() - console.log('nik path', path) - console.log('nik val', val) - try { - const realVal = eval(val) - console.log('nik realVal', realVal) - setThingtime(path, realVal) - } catch (err) { - console.log('setThingtime errored in Commander', err) - } - // setContextPath(path) - } else { - const val = getThingtime(sanitisedCommand) - - console.log('setting to val', val) - - setContextPath(sanitisedCommand) - setShowContext(true) + if (commandIsAction) { + // nothing + try { + const fn = `() => { return ${escapedCommandValue} }` + const evalFn = eval(fn) + const realVal = evalFn() + setThingtime(commandPath, realVal) + } catch (err) { + console.log('setThingtime errored in Commander', err) } + } else if (commandContainsPath) { + setContextPath(commandPath) + setShowContext(true, 'commandContainsPath check') } } catch (err) { console.error('Caught error on commander onEnter', err) } }, - [value, thingtime, getThingtime, setThingtime] + [ + setShowContext, + escapedCommandValue, + setThingtime, + commandIsAction, + commandPath, + commandContainsPath + ] ) // trigger on enter const onKeyDown = React.useCallback( e => { if (e.key === 'Enter') { - console.log('nik enter') e.preventDefault() e.stopPropagation() onEnter({ e }) @@ -105,7 +210,7 @@ export const Commander = props => { // ) } }, - [thingtime?.settings?.showCommander, onEnter] + [onEnter] ) const openCommander = React.useCallback(() => { @@ -116,7 +221,7 @@ export const Commander = props => { setThingtime('settings.showCommander', false) setShowContext(false) setContextPath(undefined) - }, [setThingtime]) + }, [setThingtime, setShowContext]) const toggleCommander = React.useCallback(() => { if (thingtime?.settings?.showCommander) { @@ -129,7 +234,6 @@ export const Commander = props => { React.useEffect(() => { const keyListener = (e: any) => { if (e?.metaKey && e?.code === 'KeyP') { - console.log('nik heard event') e.preventDefault() e.stopPropagation() toggleCommander() @@ -145,44 +249,114 @@ export const Commander = props => { return () => { window.removeEventListener('keydown', keyListener) } - }, [setThingtime, thingtime]) + }, [setThingtime, thingtime, toggleCommander, closeCommander]) + + const selectSuggestion = React.useCallback( + suggestion => { + setValue(suggestion) + setContextPath(suggestion) + setShowContext(true, 'Select suggestion') + }, + [setValue, setContextPath, setShowContext] + ) + + const excludedSuggestions = React.useMemo(() => { + return ['.'] + }, []) + + const renderedSuggestions = React.useMemo(() => { + return suggestions?.filter(suggestion => { + return !excludedSuggestions?.includes(suggestion) + }) + }, [suggestions, excludedSuggestions]) + + const mobileVW = React.useMemo(() => { + return 'calc(100vw - 45px)' + }, []) return ( -
-
- + + {renderedSuggestions.map((suggestion, i) => { + return ( + selectSuggestion(suggestion)} + > + {suggestion} + + ) + })} + + -
+
{ placeholder={"What's on your mind?"} >
-
+ ) } diff --git a/remix/app/components/Nav/Nav.tsx b/remix/app/components/Nav/Nav.tsx index 18eaabc..95f9055 100644 --- a/remix/app/components/Nav/Nav.tsx +++ b/remix/app/components/Nav/Nav.tsx @@ -13,10 +13,18 @@ export const Nav = props => { return ( <> - + { ml={'auto'} w='25px' h='25px' + cursor='pointer' onClick={toggleProfileDrawer} bg={'rgba(0,0,0,0.1)'} sx={{}} diff --git a/remix/app/components/Thingtime/Thingtime.tsx b/remix/app/components/Thingtime/Thingtime.tsx index 3e53dc4..65d47a1 100644 --- a/remix/app/components/Thingtime/Thingtime.tsx +++ b/remix/app/components/Thingtime/Thingtime.tsx @@ -10,6 +10,10 @@ export const Thingtime = props => { const [uuid, setUuid] = React.useState() + const pl = React.useMemo(() => { + return props?.pl || [4, 6] + }, [props?.pl]) + // will only run on the client React.useEffect(() => { setUuid(Math.random().toString(36).substring(7)) @@ -17,14 +21,6 @@ export const Thingtime = props => { const { thingtime } = useThingtime() - React.useEffect(() => { - // console.log('nik thingtime?.test changed', thingtime?.test) - }, [thingtime?.test]) - - React.useEffect(() => { - // console.log('nik thingtime changed', thingtime) - }, [thingtime]) - const depth = React.useMemo(() => { return props?.depth || 1 }, [props?.depth]) @@ -44,7 +40,6 @@ export const Thingtime = props => { const keys = React.useMemo(() => { if (validKeyTypes?.includes(typeof thing)) { const keysRet = Object.keys(thing) - console.log('nik keysRet', keysRet) return keysRet } else { return [] @@ -57,12 +52,18 @@ export const Thingtime = props => { const renderableValue = React.useMemo(() => { if (type === 'string') { + if (!thing) { + return 'Empty string' + } return thing } else if (type === 'number') { return thing } else if (type === 'boolean') { return thing ? 'true' : 'false' } else if (type === 'object') { + if (thing === null) { + return 'null' + } if (!keys?.length) { return 'Empty object' } @@ -78,7 +79,7 @@ export const Thingtime = props => { return 'Circular reference in object.' } } else { - return null + return 'Undefined' } }, [thing, type]) @@ -140,8 +141,7 @@ export const Thingtime = props => { // w={['200px', '500px']} maxW='100%' py={props?.path ? 3 : 0} - pl={[4, 6]} - // pr={[4, 6]} + pl={props?.valuePl} > {keysToUse?.length && keysToUse.map((key, idx) => { @@ -162,6 +162,7 @@ export const Thingtime = props => { path={key} thing={nextThing} // thing={{ infinite: { yes: true } }} + valuePl={pl} > ) })} @@ -175,6 +176,7 @@ export const Thingtime = props => { border='none' outline={'none'} py={2} + pl={pl} fontSize={'20px'} > {renderableValue} @@ -184,7 +186,7 @@ export const Thingtime = props => { } const contextMenu = ( - + Settings ) @@ -193,7 +195,7 @@ export const Thingtime = props => { const path = React.useMemo(() => { return ( - + {props?.path?.human} ) @@ -227,7 +229,7 @@ export const Thingtime = props => { > {/* {uuid?.current} */} {path} - {showContextMenu && contextMenu} + {/* {showContextMenu && contextMenu} */} {editableValue} {value} diff --git a/remix/app/components/Thingtime/ThingtimeDemo.tsx b/remix/app/components/Thingtime/ThingtimeDemo.tsx index 34ea768..135985e 100644 --- a/remix/app/components/Thingtime/ThingtimeDemo.tsx +++ b/remix/app/components/Thingtime/ThingtimeDemo.tsx @@ -4,19 +4,34 @@ import { Flex } from '@chakra-ui/react' export const ThingtimeDemo = props => { const thing = { - name: 'thing', - description: 'thing description', - image: 'thing image', - price: 100, - quantity: 1, - id: 1, - nested: { + Suggestion: 'Press ctrl/cmd + P', + Name: 'thingtime', + Description: ` + Thing Time is a versatile online platform that empowers users to organize, customize, and manage different types of data. By generalizing the concept of social media feeds, Thing Time allows users to switch between various data schemas, essentially providing them with personalized feeds and organizational tools. It's like a combination of Facebook, Twitter, and Evernote, but with the user in control of what kind of data they want to see and manage. Whether it's social posts, marketplace listings, or personal notes, Thing Time puts the power of data organization and customization in the user’s hands. + `, + 'Image URL': 'https://google.com/images', + Price: '$100', + QTY: 1, + ID: 123, + Nested: { data: { is: { fun: "Isn't it?" } } - } + }, + Array: ['one', 'two', 'three'], + 'Array of Objects': [ + { + name: 'one' + }, + { + name: 'two' + }, + { + name: 'three' + } + ] } const [demoThing, setDemoThing] = React.useState(thing) diff --git a/remix/app/components/Thingtime/useThingtime.tsx b/remix/app/components/Thingtime/useThingtime.tsx index 1602e1d..3612f72 100644 --- a/remix/app/components/Thingtime/useThingtime.tsx +++ b/remix/app/components/Thingtime/useThingtime.tsx @@ -13,7 +13,7 @@ const getGlobal = () => { export const useThingtime = (props?: any) => { const value = useContext(ThingtimeContext) - const { thingtime, setThingtime, getThingtime } = value + const { thingtime, setThingtime, getThingtime, thingtimeRef } = value return value } diff --git a/remix/app/functions/path.tsx b/remix/app/functions/path.tsx index ef5665c..e1cbeda 100644 --- a/remix/app/functions/path.tsx +++ b/remix/app/functions/path.tsx @@ -1,6 +1,7 @@ export const sanitise = str => { const isTT = str?.slice(0, 3) === 'tt.' const isThingtime = str?.slice(0, 9) === 'thingtime.' + const isDot = str?.slice(0, 1) === '.' if (isTT) { str = str?.slice(3) @@ -9,5 +10,9 @@ export const sanitise = str => { str = str?.slice(9) } + if (isDot) { + str = str?.slice(1) + } + return str } diff --git a/remix/app/routes/index.tsx b/remix/app/routes/index.tsx index 49d9657..48d72a0 100644 --- a/remix/app/routes/index.tsx +++ b/remix/app/routes/index.tsx @@ -1,9 +1,13 @@ import { Flex } from '@chakra-ui/react' import { ProfileDrawer } from '~/components/Nav/ProfileDrawer' import { Splash } from '~/components/Splash/Splash' +import { Thingtime } from '~/components/Thingtime/Thingtime' import { ThingtimeDemo } from '~/components/Thingtime/ThingtimeDemo' +import { useThingtime } from '~/components/Thingtime/useThingtime' export default function Index () { + const { thingtime } = useThingtime() + return ( + ) diff --git a/remix/app/smarts/index.tsx b/remix/app/smarts/index.tsx index 78382f0..f7a11c2 100644 --- a/remix/app/smarts/index.tsx +++ b/remix/app/smarts/index.tsx @@ -11,13 +11,13 @@ try { // nothing } -const babel = require('@babel/standalone') +// const babel = require('@babel/standalone') -babel.template = require('@babel/template').default -babel.t = require('@babel/types') -babel.generator = require('@babel/generator').default -babel.babylon = require('@babel/parser') -babel.prettier = require('prettier') +// babel.template = require('@babel/template').default +// babel.t = require('@babel/types') +// babel.generator = require('@babel/generator').default +// babel.babylon = require('@babel/parser') +// babel.prettier = require('prettier') const objList = [] const stringList = [] @@ -38,8 +38,10 @@ const stringList = [] // import prettier from 'prettier' // babel.prettier = prettier +export const babel = () => {} export const local = {} -export const t = babel?.t +export const t = () => {} +// export const t = babel?.t export const getBabel = () => { return babel