123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- import React, { FC, useContext, useCallback, useEffect, useState } from 'react';
- import { Button, Col, Collapse, Row, Slider, Space } from 'antd';
- import Paragraph from 'antd/lib/typography/Paragraph';
- import Title from 'antd/lib/typography/Title';
- import { EditCustomStyles } from '../../EditCustomStyles';
- import s from './appearance.module.scss';
- import { postConfigUpdateToAPI, RESET_TIMEOUT } from '../../../../utils/config-constants';
- import {
- createInputStatus,
- StatusState,
- STATUS_ERROR,
- STATUS_SUCCESS,
- } from '../../../../utils/input-statuses';
- import { ServerStatusContext } from '../../../../utils/server-status-context';
- import { FormStatusIndicator } from '../../FormStatusIndicator';
- const { Panel } = Collapse;
- const ENDPOINT = '/appearance';
- interface AppearanceVariable {
- value: string;
- description: string;
- }
- type ColorCollectionProps = {
- variables: { name; description; value }[];
- updateColor: (variable: string, color: string, description: string) => void;
- };
- const chatColorVariables = [
- { name: 'theme-color-users-0', description: '' },
- { name: 'theme-color-users-1', description: '' },
- { name: 'theme-color-users-2', description: '' },
- { name: 'theme-color-users-3', description: '' },
- { name: 'theme-color-users-4', description: '' },
- { name: 'theme-color-users-5', description: '' },
- { name: 'theme-color-users-6', description: '' },
- { name: 'theme-color-users-7', description: '' },
- ];
- const componentColorVariables = [
- { name: 'theme-color-background-main', description: 'Background' },
- { name: 'theme-color-action', description: 'Action' },
- { name: 'theme-color-action-hover', description: 'Action Hover' },
- { name: 'theme-color-components-primary-button-border', description: 'Primary Button Border' },
- { name: 'theme-color-components-primary-button-text', description: 'Primary Button Text' },
- { name: 'theme-color-components-chat-background', description: 'Chat Background' },
- { name: 'theme-color-components-chat-text', description: 'Text: Chat' },
- { name: 'theme-color-components-text-on-dark', description: 'Text: Light' },
- { name: 'theme-color-components-text-on-light', description: 'Text: Dark' },
- { name: 'theme-color-background-header', description: 'Header/Footer' },
- { name: 'theme-color-components-content-background', description: 'Page Content' },
- {
- name: 'theme-color-components-video-status-bar-background',
- description: 'Video Status Bar Background',
- },
- {
- name: 'theme-color-components-video-status-bar-foreground',
- description: 'Video Status Bar Foreground',
- },
- ];
- const others = [{ name: 'theme-rounded-corners', description: 'Corner radius' }];
- // Create an object so these vars can be indexed by name.
- const allAvailableValues = [...componentColorVariables, ...chatColorVariables, ...others].reduce(
- (obj, val) => {
- // eslint-disable-next-line no-param-reassign
- obj[val.name] = { name: val.name, description: val.description };
- return obj;
- },
- {},
- );
- // eslint-disable-next-line react/function-component-definition
- const ColorPicker = React.memo(
- ({
- value,
- name,
- description,
- onChange,
- }: {
- value: string;
- name: string;
- description: string;
- onChange: (name: string, value: string, description: string) => void;
- }) => (
- <Col span={3} key={name}>
- <input
- type="color"
- id={name}
- name={description}
- title={description}
- value={value}
- className={s.colorPicker}
- onChange={e => onChange(name, e.target.value, description)}
- />
- <div style={{ padding: '2px' }}>{description}</div>
- </Col>
- ),
- );
- const ColorCollection: FC<ColorCollectionProps> = ({ variables, updateColor }) => {
- const cc = variables.map(colorVar => {
- const { name, description, value } = colorVar;
- return (
- <ColorPicker
- key={name}
- value={value}
- name={name}
- description={description}
- onChange={updateColor}
- />
- );
- });
- // eslint-disable-next-line react/jsx-no-useless-fragment
- return <>{cc}</>;
- };
- // eslint-disable-next-line react/function-component-definition
- export default function Appearance() {
- const serverStatusData = useContext(ServerStatusContext);
- const { serverConfig, setFieldInConfigState } = serverStatusData;
- const { instanceDetails } = serverConfig;
- const { appearanceVariables } = instanceDetails;
- const [defaultValues, setDefaultValues] = useState<Record<string, AppearanceVariable>>();
- const [customValues, setCustomValues] = useState<Record<string, AppearanceVariable>>();
- const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
- let resetTimer = null;
- const resetStates = () => {
- setSubmitStatus(null);
- resetTimer = null;
- clearTimeout(resetTimer);
- };
- const setDefaults = () => {
- const c = {};
- [...componentColorVariables, ...chatColorVariables, ...others].forEach(color => {
- const resolvedColor = getComputedStyle(document.documentElement).getPropertyValue(
- `--${color.name}`,
- );
- c[color.name] = { value: resolvedColor.trim(), description: color.description };
- });
- setDefaultValues(c);
- };
- useEffect(() => {
- setDefaults();
- }, []);
- useEffect(() => {
- if (Object.keys(appearanceVariables).length === 0) return;
- const c = {};
- Object.keys(appearanceVariables).forEach(key => {
- c[key] = {
- value: appearanceVariables[key],
- description: allAvailableValues[key]?.description || '',
- };
- });
- setCustomValues(c);
- }, [appearanceVariables]);
- const updateColor = useCallback((variable: string, color: string, description: string) => {
- setCustomValues(oldCustomValues => ({
- ...oldCustomValues,
- [variable]: { value: color, description },
- }));
- }, []);
- const reset = async () => {
- await postConfigUpdateToAPI({
- apiPath: ENDPOINT,
- data: { value: {} },
- onSuccess: () => {
- setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
- resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
- setCustomValues({});
- },
- onError: (message: string) => {
- setSubmitStatus(createInputStatus(STATUS_ERROR, message));
- resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
- },
- });
- };
- const save = async () => {
- const c = {};
- Object.keys(customValues).forEach(color => {
- c[color] = customValues[color].value;
- });
- await postConfigUpdateToAPI({
- apiPath: ENDPOINT,
- data: { value: c },
- onSuccess: () => {
- setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
- resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
- setFieldInConfigState({
- fieldName: 'appearanceVariables',
- value: c,
- path: 'instanceDetails',
- });
- },
- onError: (message: string) => {
- setSubmitStatus(createInputStatus(STATUS_ERROR, message));
- resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
- },
- });
- };
- const onBorderRadiusChange = (value: string) => {
- const variableName = 'theme-rounded-corners';
- updateColor(variableName, `${value.toString()}px`, '');
- };
- if (!defaultValues) {
- return <div>Loading...</div>;
- }
- const transformToColorMap = variables =>
- variables.map(colorVar => {
- const source = customValues?.[colorVar.name] ? customValues : defaultValues;
- const { name, description } = colorVar;
- const { value } = source[name];
- return { name, description, value };
- });
- return (
- <>
- <Space direction="vertical">
- <Title>Customize Appearance</Title>
- <Paragraph>The following colors are used across the user interface.</Paragraph>
- <div>
- <Collapse defaultActiveKey={['1']}>
- <Panel header={<strong>Section Colors</strong>} key="1">
- <p>
- Certain sections of the interface can be customized by selecting new colors for
- them.
- </p>
- <Row gutter={[16, 16]}>
- <ColorCollection
- variables={transformToColorMap(componentColorVariables)}
- updateColor={updateColor}
- />
- </Row>
- </Panel>
- <Panel header={<strong>Chat User Colors</strong>} key="2">
- <Row gutter={[16, 16]}>
- <ColorCollection
- variables={transformToColorMap(chatColorVariables)}
- updateColor={updateColor}
- />
- </Row>
- </Panel>
- <Panel header={<strong>Other Settings</strong>} key="4">
- How rounded should corners be?
- <Row gutter={[16, 16]}>
- <Col span={12}>
- <Slider
- min={0}
- max={20}
- tooltip={{ formatter: null }}
- onChange={v => {
- onBorderRadiusChange(v);
- }}
- value={Number(
- customValues?.['theme-rounded-corners']?.value?.replace('px', '') ??
- defaultValues?.['theme-rounded-corners']?.value?.replace('px', '') ??
- 0,
- )}
- />
- </Col>
- <Col span={4}>
- <div
- style={{
- width: '100px',
- height: '30px',
- borderRadius: `${
- customValues?.['theme-rounded-corners']?.value ??
- defaultValues?.['theme-rounded-corners']?.value
- }`,
- backgroundColor: 'var(--theme-color-palette-7)',
- }}
- />
- </Col>
- </Row>
- </Panel>
- </Collapse>
- </div>
- <Space direction="horizontal">
- <Button type="primary" onClick={save}>
- Save Colors
- </Button>
- <Button type="ghost" onClick={reset}>
- Reset to Defaults
- </Button>
- </Space>
- <FormStatusIndicator status={submitStatus} />
- </Space>
- <div className="form-module page-content-module">
- <EditCustomStyles />
- </div>
- </>
- );
- }
|