import Engine from 'publicodes'
import { Documentation, getDocumentationSiteMap } from 'publicodes-react'
import { invertObj, last } from 'ramda'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import MonacoEditor from 'react-monaco-editor'
import { useHistory, useLocation } from 'react-router-dom'
import styled from 'styled-components'
const EXAMPLE_CODE = `
# Bienvenue dans le bac à sable du langage publicodes !
# Pour en savoir plus sur le langage :
# => https://publi.codes/documentation/principes-de-base
prix:
prix . carottes: 2€/kg
prix . champignons: 5€/kg
prix . avocat: 2€/avocat
dépenses primeur:
formule:
somme:
- prix . carottes * 1.5 kg
- prix . champignons * 500g
- prix . avocat * 3 avocat
`
export default function Studio() {
const { search, pathname } = useLocation()
const initialValue = useMemo(() => {
const code = new URLSearchParams(search ?? '').get('code')
return code ? code : EXAMPLE_CODE
}, [search])
const [editorValue, setEditorValue] = useState(initialValue)
const debouncedEditorValue = useDebounce(editorValue, 1000)
const history = useHistory()
useEffect(() => {
history.replace({
pathname,
search: `?code=${encodeURIComponent(debouncedEditorValue)}`,
})
}, [debouncedEditorValue, history])
const handleShare = useCallback(() => {
navigator.clipboard.writeText(window.location.href)
}, [window.location.href])
return (
setEditorValue(newValue ?? '')}
options={{
minimap: { enabled: false },
}}
/>
{/* TODO: prévoir de changer la signature de EngineProvider */}
)
}
type ResultsProps = {
rules: string
onClickShare: React.MouseEventHandler
}
class Logger {
messages: string[] = []
warn(message: string) {
this.messages.push(message)
}
error(message: string) {
this.messages.push(message)
}
log(message: string) {
this.messages.push(message)
}
toJSX() {
return this.messages.map((m) => (
{nl2br(m)}
))
}
}
export const Results = ({ onClickShare, rules }: ResultsProps) => {
const logger = useMemo(() => new Logger(), [rules])
const engine = useMemo(() => new Engine(rules, { logger }), [rules, logger])
const targets = useMemo(() => Object.keys(engine.getParsedRules()), [engine])
const documentationPath = '/studio'
const pathToRules = useMemo(
() => getDocumentationSiteMap({ engine, documentationPath }),
[engine, documentationPath]
)
const ruleToPaths = useMemo(() => invertObj(pathToRules), [pathToRules])
const { search, pathname } = useLocation()
const history = useHistory()
const setCurrentTarget = useCallback(
(target) =>
history.replace({
pathname: ruleToPaths[target],
search,
}),
[ruleToPaths, history, search]
)
useEffect(() => {
if (!pathToRules[pathname]) {
setCurrentTarget(last(targets))
}
})
return (
<>
{logger.toJSX()}
Aller à{' '}
{
setCurrentTarget(e.target.value)
}}
css={`
font-size: inherit;
color: inherit;
font-family: inherit;
`}
>
{targets.map((target) => (
{target}
))}
🔗 Copier le lien
>
)
}
const newlineRegex = /(\r\n|\r|\n)/g
function nl2br(str: string) {
if (typeof str !== 'string') {
return str
}
return str.split(newlineRegex).map(function (line, index) {
if (line.match(newlineRegex)) {
return React.createElement('br', { key: index })
}
return line
})
}
const Layout = styled.div`
flex-grow: 1;
display: flex;
height: 100%;
> :first-child {
width: 55% !important;
}
@media (max-width: 960px) {
flex-direction: column;
padding: 20px;
> :first-child {
width: 100% !important;
}
}
`
class ErrorBoundary extends React.Component {
state: { error: false | { message: string; name: string } } = { error: false }
static getDerivedStateFromError(error: Error) {
console.error(error)
return { error: { message: error.message, name: error.name } }
}
render() {
if (this.state.error) {
return (
)
}
return this.props.children
}
}
function useDebounce(value: T, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler)
}
},
[value, delay] // Only re-call effect if value or delay changes
)
return debouncedValue
}