feat: port stroy-bot-main as the hub page
Project picker, section cards and the period Excel export now live on the app's home page. The selected project is shared app state, so the sidebar hides sections with the same rules main used to disable its cards (archived, finished, no edit0 right). Queries are 1:1 with stroy-bot-main and covered by tests against a fake HTTP client. Behaviour differences are listed in the migration checklist. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import * as React from 'react';
|
||||
import { Icon } from '@tnsu/ui-kit';
|
||||
import { useProject } from '../../app/project/ProjectContext';
|
||||
import { ProjectStatusBadge } from '../../app/project/ProjectStatusBadge';
|
||||
import { IAccessibleProject } from '../../app/project/projectTypes';
|
||||
import { useOutsideClose } from '../../shared/hooks/useOutsideClose';
|
||||
import { ProjectList } from './ProjectList';
|
||||
import { HUB_TEXTS } from './texts';
|
||||
import styles from './ProjectPicker.module.scss';
|
||||
|
||||
const TEXTS = HUB_TEXTS.picker;
|
||||
|
||||
/** Поле выбора проекта с поиском — как в stroy-bot-main. */
|
||||
export const ProjectPicker: React.FC = () => {
|
||||
const { projects, loadState, currentProject, selectProject } = useProject();
|
||||
const selectedTitle = currentProject?.title ?? '';
|
||||
const [query, setQuery] = React.useState<string>(selectedTitle);
|
||||
const [isOpen, setIsOpen] = React.useState<boolean>(false);
|
||||
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
// Выбор мог поменяться извне (адрес, восстановление из сессии) — пока список закрыт, поле показывает его.
|
||||
React.useEffect(() => {
|
||||
if (!isOpen) setQuery(selectedTitle);
|
||||
}, [selectedTitle, isOpen]);
|
||||
|
||||
useOutsideClose(wrapperRef, isOpen, (reason) => {
|
||||
setIsOpen(false);
|
||||
setQuery(selectedTitle);
|
||||
if (reason === 'escape') inputRef.current?.blur();
|
||||
});
|
||||
|
||||
const onFocus = (event: React.FocusEvent<HTMLInputElement>): void => {
|
||||
event.target.select();
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setQuery(event.target.value);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
// mousedown, а не click: иначе поле теряет фокус раньше, чем срабатывает выбор.
|
||||
const onPick = (event: React.MouseEvent, project: IAccessibleProject): void => {
|
||||
event.preventDefault();
|
||||
selectProject(project);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.picker}>
|
||||
<label className={styles.label} htmlFor="stroybot-project-search">
|
||||
{TEXTS.label}
|
||||
</label>
|
||||
<div className={styles.field} ref={wrapperRef}>
|
||||
<span className={styles.searchIcon} aria-hidden="true">
|
||||
<Icon name="search" size="sm" />
|
||||
</span>
|
||||
<input
|
||||
id="stroybot-project-search"
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className={styles.input}
|
||||
placeholder={TEXTS.placeholder}
|
||||
value={query}
|
||||
autoComplete="off"
|
||||
onFocus={onFocus}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{currentProject?.status && (
|
||||
<span className={styles.selectedBadge}>
|
||||
<ProjectStatusBadge status={currentProject.status} />
|
||||
</span>
|
||||
)}
|
||||
{isOpen && (
|
||||
<div className={styles.dropdown}>
|
||||
<ProjectList loadState={loadState} projects={projects} query={query} onPick={onPick} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user