| 1 | // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt |
| 2 | |
| 3 | import { createElement as h, Fragment, useEffect, useMemo, useState } from 'react'; |
| 4 | import { Field, SelectField } from '@hfs/mui-grid-form' |
| 5 | import { apiCall, useApiEx } from './api' |
| 6 | import { Alert, Box } from '@mui/material' |
| 7 | import { Dict, HTTP_MESSAGES, prefix, md, isNumeric, CFG } from './misc' |
| 8 | import { hTooltip, IconBtn, reloadBtn, useCtrlShortcutButton, wikiLink } from './mui' |
| 9 | import { Save } from '@mui/icons-material' |
| 10 | import _ from 'lodash' |
| 11 | import { useDebounce } from 'usehooks-ts' |
| 12 | import { TextEditor } from './TextEditor'; |
| 13 | import { state, useSnapState } from './state' |
| 14 | import { PageProps } from './App' |
| 15 | import { switchBtn } from './VerticalSwitch'; |
| 16 | import { adminApis } from '../../src/adminApis' |
| 17 | |
| 18 | const names: any = { |
| 19 | top: "Top of HTML Body", |
| 20 | bottom: "Bottom of HTML Body", |
| 21 | } |
| 22 | |
| 23 | export default function CustomHtmlPage({ setTitleSide }: PageProps) { |
| 24 | const { data, reload } = useApiEx<typeof adminApis.get_custom_html>('get_custom_html') |
| 25 | const { customHtmlSection: section } = useSnapState() |
| 26 | const [all, setAll] = useState<Dict<string>>({}) |
| 27 | const [saved, setSaved] = useState({}) |
| 28 | useEffect(() => { |
| 29 | if (!data) return |
| 30 | setSaved(data.sections) |
| 31 | setEnabled(data.enabled) |
| 32 | }, [data]) |
| 33 | useEffect(() => setAll(saved), [saved]) |
| 34 | const options = useMemo(() => { |
| 35 | const keys = _.sortBy(Object.keys(all), isNumeric) // http codes at the bottom |
| 36 | if (keys.length && !keys.includes(section)) |
| 37 | state.customHtmlSection = _.findKey(all, Boolean) || keys?.[0] || '' // prefer any key with content |
| 38 | return keys.map(x => ({ |
| 39 | value: x, |
| 40 | label: (names[x] || prefix('HTTP ', HTTP_MESSAGES[x as any]) || _.startCase(x)) + (all[x]?.trim() ? ' *' : '') |
| 41 | })) |
| 42 | }, [useDebounce(all, 500)]) |
| 43 | const anyChange = useMemo(() => !_.isEqualWith(saved, all, (a,b) => !a && !b || undefined), |
| 44 | [saved, all]) |
| 45 | const [enabled, setEnabled] = useState<boolean>() |
| 46 | setTitleSide(useMemo(() => h(Box, { sx: { display: { xs: 'none', md: 'block' } } }, |
| 47 | h(Alert, { severity: 'info' }, |
| 48 | md("Add HTML code to some parts of the Front-end. It's saved to file `custom.html`, that you can edit directly with your editor of choice. "), |
| 49 | wikiLink('customization', "More help") |
| 50 | ), |
| 51 | h(Alert, { severity: 'info' }, "To customize icons ", wikiLink('customization#icons', "read documentation") ), |
| 52 | ), [])) |
| 53 | return h(Fragment, {}, |
| 54 | h(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1, mb: 1 } }, |
| 55 | h(SelectField as Field<string>, { |
| 56 | label: "Section", |
| 57 | value: section, |
| 58 | options, |
| 59 | onChange: v => state.customHtmlSection = v |
| 60 | }), |
| 61 | reloadBtn(reload), |
| 62 | h(IconBtn, { |
| 63 | ref: useCtrlShortcutButton(['s', 'Enter']).ref, |
| 64 | icon: Save, |
| 65 | title: "Save\n(ctrl+s)", |
| 66 | modified: anyChange, |
| 67 | doneAnimation: true, |
| 68 | onClick: save, |
| 69 | }), |
| 70 | hTooltip("Enable all sections", undefined, switchBtn(enabled, async v => { |
| 71 | await apiCall('set_config', { values: { [CFG.disable_custom_html]: !v } }) |
| 72 | setEnabled(v) |
| 73 | })), |
| 74 | ), |
| 75 | h(TextEditor, { |
| 76 | lang: section === 'style' ? 'css' : section === 'script' ? 'js' : 'html', |
| 77 | value: all?.[section] || '', |
| 78 | style: { background: '#8881' }, |
| 79 | onValueChange(v: string) { |
| 80 | setAll(all => ({ ...all, [section]: v })) |
| 81 | }, |
| 82 | }), |
| 83 | ) |
| 84 | |
| 85 | function save() { |
| 86 | return apiCall('set_custom_html', { sections: all }).then(() => setSaved(all)) |
| 87 | } |
| 88 | } |