Загрузка данных


export type SettingValue = string | number | boolean;
export type SettingsValues = Record<string, SettingValue>;

interface BaseSettingField {
  key: string;
  label: string;
}

export interface NumberSettingField extends BaseSettingField {
  type: 'number';
  defaultValue: number;
  min?: number;
  max?: number;
}

export interface SelectSettingField extends BaseSettingField {
  type: 'select';
  defaultValue: string | number;
  options: { label: string; value: string | number }[];
}

export interface ColorSettingField extends BaseSettingField {
  type: 'color';
  defaultValue: string;
}

export interface TextSettingField extends BaseSettingField {
  type: 'text';
  defaultValue: string;
  placeholder?: string;
}

export interface TextAreaSettingField extends BaseSettingField {
  type: 'textarea';
  defaultValue: string;
  placeholder?: string;
}

export interface BooleanSettingField extends BaseSettingField {
  type: 'boolean';
  defaultValue: boolean;
}

export type SettingField =
  | NumberSettingField
  | SelectSettingField
  | ColorSettingField
  | TextSettingField
  | TextAreaSettingField
  | BooleanSettingField;

export interface SettingsTab<TField extends SettingField = SettingField> {
  key: string;
  label: string;
  fields: TField[];
}



import classNames from 'classnames';
import { Button } from 'exchange-elements/v2';
import { useEffect, useMemo, useState } from 'react';

import {
  CheckboxField,
  ColorField,
  NumberField,
  SelectField,
  TextAreaField,
  TextField,
} from '@src/components/FormFields';
import { SettingField, SettingsTab, SettingsValues, SettingValue } from '@src/types';

import styles from './index.module.scss';

interface EntitySettingsModalProps<TValues extends SettingsValues, TField extends SettingField = SettingField> {
  tabs: SettingsTab<TField>[];
  values: TValues;
  onChange: (settings: TValues) => void;
  initialTabKey?: string;
}

export const EntitySettingsModal = <TValues extends SettingsValues, TField extends SettingField = SettingField>({
  tabs,
  values,
  onChange,
  initialTabKey,
}: EntitySettingsModalProps<TValues, TField>) => {
  const availableTabs = useMemo(() => tabs.filter((tab) => tab.fields.length > 0), [tabs]);

  const [activeTabKey, setActiveTabKey] = useState(initialTabKey ?? availableTabs[0]?.key ?? '');
  const [form, setForm] = useState<TValues>(values);

  useEffect(() => {
    setForm(values);
  }, [values]);

  useEffect(() => {
    if (!availableTabs.find((tab) => tab.key === activeTabKey)) {
      setActiveTabKey(availableTabs[0]?.key ?? '');
    }
  }, [availableTabs, activeTabKey]);

  const activeTab = availableTabs.find((tab) => tab.key === activeTabKey) ?? availableTabs[0];

  const changeValue = (key: string, value: SettingValue) => {
    setForm((current) => {
      const next = {
        ...current,
        [key]: value,
      } as TValues;

      onChange(next);

      return next;
    });
  };

  const renderField = (field: TField) => {
    const value = form[field.key] ?? field.defaultValue;

    if (field.type === 'select') {
      return (
        <SelectField
          key={field.key}
          label={field.label}
          value={String(value)}
          options={field.options}
          onValueChange={(nextValue) => {
            changeValue(field.key, nextValue);
          }}
        />
      );
    }

    if (field.type === 'color') {
      return (
        <ColorField
          key={field.key}
          label={field.label}
          value={String(value)}
          onChange={(nextValue) => {
            changeValue(field.key, nextValue);
          }}
        />
      );
    }

    if (field.type === 'text') {
      return (
        <TextField
          key={field.key}
          label={field.label}
          value={String(value)}
          placeholder={field.placeholder}
          onValueChange={(nextValue) => {
            changeValue(field.key, nextValue);
          }}
        />
      );
    }

    if (field.type === 'textarea') {
      return (
        <TextAreaField
          key={field.key}
          label={field.label}
          value={String(value)}
          placeholder={field.placeholder}
          onValueChange={(nextValue) => {
            changeValue(field.key, nextValue);
          }}
        />
      );
    }

    if (field.type === 'boolean') {
      return (
        <CheckboxField
          key={field.key}
          label={field.label}
          checked={Boolean(value)}
          onValueChange={(nextValue) => {
            changeValue(field.key, nextValue);
          }}
        />
      );
    }

    return (
      <NumberField
        key={field.key}
        label={field.label}
        value={typeof value === 'number' ? value : field.defaultValue}
        min={field.min}
        max={field.max}
        onValueChange={(nextValue) => {
          changeValue(field.key, nextValue);
        }}
      />
    );
  };

  return (
    <div className={styles.wrapper}>
      {availableTabs.length > 1 && (
        <div className={styles.tabs}>
          {availableTabs.map(({ key, label }) => (
            <Button
              key={key}
              className={classNames(styles.tab, { [styles.tab_active]: key === activeTabKey })}
              onClick={() => setActiveTabKey(key)}
            >
              {label}
            </Button>
          ))}
        </div>
      )}

      {activeTab?.fields.map(renderField)}
    </div>
  );
};


import { ChangeEvent, useId } from 'react';

import styles from './index.module.scss';

interface RangeFieldProps {
  label?: string;
  value: number;
  min?: number;
  max?: number;
  step?: number;
  color?: string;
  suffix?: string;
  onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}

export const RangeField = ({
  label,
  value,
  min = 0,
  max = 100,
  step = 1,
  color = 'var(--blue-1)',
  suffix = '%',
  onChange,
}: RangeFieldProps) => {
  const inputId = useId();

  return (
    <div className={styles.range}>
      {label && (
        <label
          htmlFor={inputId}
          className={styles.range_label}
        >
          {label}
        </label>
      )}

      <div className={styles.range_wrapper}>
        <input
          id={inputId}
          type="range"
          min={min}
          max={max}
          step={step}
          value={value}
          className={styles.range_input}
          style={{ ['--range-color' as string]: color }}
          onChange={onChange}
        />

        <span className={styles.range_value}>
          {value}
          {suffix}
        </span>
      </div>
    </div>
  );
};