<commit_before>import { useContext, useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';

import CategoryLegend from './CategoryLegend';
import FullnessLegend from './FullnessLegend';
import ScheduleGrid from './ScheduleGrid';
import { getConfig } from './ScheduleGridConfig';
import { ScheduleGridProvider } from './ScheduleGridContext';
import { RATING_OPTIONS } from '../EventList/EventListMyRatingSelector';
import AppRootContext from '../../AppRootContext';
import ChoiceSet from '../../BuiltInFormControls/ChoiceSet';
import { parseIntOrNull } from '../../ValueUtils';
import useReactRouterReactTable from '../../Tables/useReactRouterReactTable';
import { FilterCodecs, buildFieldFilterCodecs } from '../../Tables/FilterUtils';
import ErrorDisplay from '../../ErrorDisplay';
import { formatLCM, useAppDateTimeFormat } from '../../TimeUtils';

const filterCodecs = buildFieldFilterCodecs({
  my_rating: FilterCodecs.integerArray,
  hide_conflicts: FilterCodecs.boolean,
});

const DEFAULT_PERSONAL_FILTERS = [
  { id: 'my_rating', value: [1, 0] },
  { id: 'hide_conflicts', value: false },
];

export type ScheduleGridAppProps = {
  configKey: string;
};

function ScheduleGridApp({ configKey }: ScheduleGridAppProps) {
  const { t } = useTranslation();
  const location = useLocation();
  const { myProfile, timezoneName, language } = useContext(AppRootContext);
  const { filters, updateSearch } = useReactRouterReactTable({ ...filterCodecs });
  const config = getConfig(configKey);
  const storageKey = `schedule:${configKey}:personalFilters`;
  const format = useAppDateTimeFormat();

  const loadPersonalFilters = useCallback(() => {
    const storedValue = window.localStorage.getItem(storageKey);
    if (storedValue) {
      try {
        return JSON.parse(storedValue);
      } catch (e) {
        return DEFAULT_PERSONAL_FILTERS;
      }
    }

    return DEFAULT_PERSONAL_FILTERS;
  }, [storageKey]);

  useEffect(() => {
    if (config?.showPersonalFilters && myProfile && !location.search) {
      updateSearch({ filters: loadPersonalFilters() });
    }
  }, [config?.showPersonalFilters, loadPersonalFilters, location, myProfile, updateSearch]);

  const ratingFilter = (filters.find((f) => f.id === 'my_rating') || {}).value;
  const hideConflicts = (filters.find((f) => f.id === 'hide_conflicts') || {}).value;

  const choiceSetValue = [
    ...(ratingFilter || []).map((integer: number) => integer.toString()),
    ...(hideConflicts ? [] : ['conflicts']),
  ];

  const choiceSetChanged = (newValue: string[]) => {
    const integerArray = newValue.filter((choice) => choice !== 'conflicts').map(parseIntOrNull);

    const newFilters = [
      { id: 'my_rating', value: integerArray },
      { id: 'hide_conflicts', value: !newValue.includes('conflicts') },
    ];

    updateSearch({ filters: newFilters });
    window.localStorage.setItem(storageKey, JSON.stringify(newFilters));
  };

  if (!config) {
    return <ErrorDisplay stringError={`Schedule grid configuration "${configKey}" not found`} />;
  }

  return (
    <>
      <nav aria-label="breadcrumb">
        <ol className="breadcrumb">
          <li className="breadcrumb-item active" aria-current="page">
            {t(config.titlei18nKey, config.title)}
          </li>
        </ol>
      </nav>
      <ScheduleGridProvider
        config={config}
        myRatingFilter={myProfile ? ratingFilter : null}
        hideConflicts={myProfile ? hideConflicts : false}
      >
        {(timespan) => (
          <div className="mb-4">
            {config.showPersonalFilters && myProfile && (
              <div className="d-flex flex-column flex-md-row bg-light">
                <div className="d-flex btn">
                  <span className="mr-2">Show:</span>
                  <ChoiceSet
                    choices={[...RATING_OPTIONS, { label: 'Conflicts', value: 'conflicts' }]}
                    choiceClassName="form-check-inline mr-md-4"
                    containerClassName="d-flex flex-wrap"
                    value={choiceSetValue}
                    onChange={choiceSetChanged}
                    multiple
                  />
                </div>
              </div>
            )}
            <h3 className="m-0 p-2 border-bottom">{format(timespan.start, 'longDate')}</h3>
            <ScheduleGrid timespan={timespan} />
            <div className="font-italic">
              {t('schedule.timezoneMessage', 'All times displayed in {{ offsetName }}.', {
                offsetName: timespan.start
                  .reconfigure({
                    locale: language,
                  })
                  .setZone(timezoneName).offsetNameLong,
              })}
            </div>
          </div>
        )}
      </ScheduleGridProvider>
      {(config.legends || []).map((legend, i) => {
        if (legend.type === 'text') {
          return (<commit_msg>Reorganize a little<commit_after>import { useContext, useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';

import CategoryLegend from './CategoryLegend';
import FullnessLegend from './FullnessLegend';
import ScheduleGrid from './ScheduleGrid';
import { getConfig } from './ScheduleGridConfig';
import { ScheduleGridProvider } from './ScheduleGridContext';
import { RATING_OPTIONS } from '../EventList/EventListMyRatingSelector';
import AppRootContext from '../../AppRootContext';
import ChoiceSet from '../../BuiltInFormControls/ChoiceSet';
import { parseIntOrNull } from '../../ValueUtils';
import useReactRouterReactTable from '../../Tables/useReactRouterReactTable';
import { FilterCodecs, buildFieldFilterCodecs } from '../../Tables/FilterUtils';
import ErrorDisplay from '../../ErrorDisplay';
import { useAppDateTimeFormat } from '../../TimeUtils';

const filterCodecs = buildFieldFilterCodecs({
  my_rating: FilterCodecs.integerArray,
  hide_conflicts: FilterCodecs.boolean,
});

const DEFAULT_PERSONAL_FILTERS = [
  { id: 'my_rating', value: [1, 0] },
  { id: 'hide_conflicts', value: false },
];

export type ScheduleGridAppProps = {
  configKey: string;
};

function ScheduleGridApp({ configKey }: ScheduleGridAppProps) {
  const { t } = useTranslation();
  const location = useLocation();
  const { myProfile, timezoneName, language } = useContext(AppRootContext);
  const { filters, updateSearch } = useReactRouterReactTable({ ...filterCodecs });
  const config = getConfig(configKey);
  const storageKey = `schedule:${configKey}:personalFilters`;
  const format = useAppDateTimeFormat();

  const loadPersonalFilters = useCallback(() => {
    const storedValue = window.localStorage.getItem(storageKey);
    if (storedValue) {
      try {
        return JSON.parse(storedValue);
      } catch (e) {
        return DEFAULT_PERSONAL_FILTERS;
      }
    }

    return DEFAULT_PERSONAL_FILTERS;
  }, [storageKey]);

  useEffect(() => {
    if (config?.showPersonalFilters && myProfile && !location.search) {
      updateSearch({ filters: loadPersonalFilters() });
    }
  }, [config?.showPersonalFilters, loadPersonalFilters, location, myProfile, updateSearch]);

  const ratingFilter = (filters.find((f) => f.id === 'my_rating') || {}).value;
  const hideConflicts = (filters.find((f) => f.id === 'hide_conflicts') || {}).value;

  const choiceSetValue = [
    ...(ratingFilter || []).map((integer: number) => integer.toString()),
    ...(hideConflicts ? [] : ['conflicts']),
  ];

  const choiceSetChanged = (newValue: string[]) => {
    const integerArray = newValue.filter((choice) => choice !== 'conflicts').map(parseIntOrNull);

    const newFilters = [
      { id: 'my_rating', value: integerArray },
      { id: 'hide_conflicts', value: !newValue.includes('conflicts') },
    ];

    updateSearch({ filters: newFilters });
    window.localStorage.setItem(storageKey, JSON.stringify(newFilters));
  };

  if (!config) {
    return <ErrorDisplay stringError={`Schedule grid configuration "${configKey}" not found`} />;
  }

  return (
    <>
      <nav aria-label="breadcrumb">
        <ol className="breadcrumb">
          <li className="breadcrumb-item active" aria-current="page">
            {t(config.titlei18nKey, config.title)}
          </li>
        </ol>
      </nav>
      <ScheduleGridProvider
        config={config}
        myRatingFilter={myProfile ? ratingFilter : null}
        hideConflicts={myProfile ? hideConflicts : false}
      >
        {(timespan) => (
          <div className="mb-4">
            {config.showPersonalFilters && myProfile && (
              <div className="d-flex flex-column flex-md-row bg-light">
                <div className="d-flex btn">
                  <span className="mr-2">Show:</span>
                  <ChoiceSet
                    choices={[...RATING_OPTIONS, { label: 'Conflicts', value: 'conflicts' }]}
                    choiceClassName="form-check-inline mr-md-4"
                    containerClassName="d-flex flex-wrap"
                    value={choiceSetValue}
                    onChange={choiceSetChanged}
                    multiple
                  />
                </div>
              </div>
            )}
            <div className="m-0 p-2 border-bottom">
              <h3 className="p-0 m-0">{format(timespan.start, 'longWeekdayDate')}</h3>
              <div className="font-italic">
                {t('schedule.timezoneMessage', 'All times displayed in {{ offsetName }}.', {
                  offsetName: timespan.start
                    .reconfigure({
                      locale: language,
                    })
                    .setZone(timezoneName).offsetNameLong,
                })}
              </div>
            </div>
            <ScheduleGrid timespan={timespan} />
          </div>
        )}
      </ScheduleGridProvider>
      {(config.legends || []).map((legend, i) => {
        if (legend.type === 'text') {
          return (
{'fraction_non_alphanumeric': 0.14503052747472725, 'fraction_numerical': 0.0028025222700430387, 'mean_word_length': 2.7076066790352504, 'pattern_counts': {'":': 0, '<': 53, '<?xml version=': 0, '>': 71, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7581500', 'n_tokens_mistral': 2829, 'n_tokens_neox': 2761, 'n_words': 581}