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 ( -