refactor(build): migrate from Create React App to Vite

Major build system migration replacing react-scripts with Vite.
Upgrades React to v19, Redux Toolkit to v2, Three.js to 0.184,
and replaces axios with ky. Removes IE11 support, test
infrastructure, and polyfills. Updates TypeScript config to
project references and bumps version to 3.0.0.
This commit is contained in:
2026-05-24 20:16:04 +09:00
parent 3f4f7bd580
commit aae43e9421
58 changed files with 4207 additions and 9714 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
.main {
width: 100%;
min-height: 330px;
height: auto !important;
height: auto;
border: thin solid #bb3300;
text-align: left;
border-radius: 20px;
-15
View File
@@ -1,15 +0,0 @@
import { render, screen } from "@testing-library/react";
import React from "react";
import { Provider } from "react-redux";
import App from "./App";
import { store } from "./app/store";
test("renders learn react link", () => {
render(
<Provider store={store}>
<App />
</Provider>
);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
+15 -14
View File
@@ -1,20 +1,19 @@
import React from "react";
import ReactGA from "react-ga4";
import { BrowserRouter, NavLink, Route, Routes, useLocation } from "react-router-dom";
import styles from "./App.module.css";
import CompatRedirect from "./features/compat-redirect/CompatRedirect";
import Contact from "./features/contact/Contact";
import Data from "./features/data/Data";
import Help from "./features/help/Help";
import News from "./features/news/News";
import PageTitle from "./features/page-title/PageTitle";
import Search from "./features/search/Search";
import React from 'react';
import ReactGA from 'react-ga4';
import { BrowserRouter, NavLink, Route, Routes, useLocation } from 'react-router-dom';
import styles from './App.module.css';
import CompatRedirect from './features/compat-redirect/CompatRedirect';
import Contact from './features/contact/Contact';
import Data from './features/data/Data';
import Help from './features/help/Help';
import News from './features/news/News';
import PageTitle from './features/page-title/PageTitle';
import Search from './features/search/Search';
const AppMain: React.FC = () => {
const location = useLocation();
React.useEffect(() => {
ReactGA.send("pageview");
ReactGA.send('pageview');
}, [location]);
return (
@@ -52,7 +51,9 @@ const AppMain: React.FC = () => {
};
const App: React.FC = () => {
ReactGA.initialize("G-T4L3SDCN6P");
React.useEffect(() => {
ReactGA.initialize('G-T4L3SDCN6P');
}, []);
return (
<BrowserRouter>
<AppMain />
+2 -2
View File
@@ -1,5 +1,5 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { AppDispatch, RootState } from "./store";
import { type TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from './store';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
+3 -3
View File
@@ -1,6 +1,6 @@
import { Action, configureStore, ThunkAction } from "@reduxjs/toolkit";
import pageTitleReducer from "../features/page-title/pageTitleSlice";
import searchResultsReducer from "../features/search-results/searchResultsSlice";
import { type Action, configureStore, type ThunkAction } from '@reduxjs/toolkit';
import pageTitleReducer from '../features/page-title/pageTitleSlice';
import searchResultsReducer from '../features/search-results/searchResultsSlice';
export const store = configureStore({
reducer: {
+1 -1
View File
@@ -99,7 +99,7 @@
"recordingVolume": {
"width": 317,
"height": 317,
"depth": 105
"depth": 105
},
"imagingDepth": {
"from": 110,
+18 -17
View File
@@ -1,41 +1,42 @@
import React from "react";
import { Navigate, useLocation } from "react-router-dom";
import NotFound from "../not-found/NotFound";
import type React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import NotFound from '../not-found/NotFound';
const CompatRedirect: React.FC = () => {
const { pathname } = useLocation();
switch (pathname) {
case "/index.html":
case '/index.html':
return <Navigate to="/" />;
case "/Search.html":
case '/Search.html':
return <Navigate to="/search" />;
case "/Help.html":
case '/Help.html':
return <Navigate to="/help" />;
case "/Help_S1.html":
case '/Help_S1.html':
return <Navigate to="/help/search/1" />;
case "/Help_S2.html":
case '/Help_S2.html':
return <Navigate to="/help/search/2" />;
case "/Help_S3.html":
case '/Help_S3.html':
return <Navigate to="/help/search/3" />;
case "/Help_S4.html":
case '/Help_S4.html':
return <Navigate to="/help/search/4" />;
case "/Help_S5.html":
case '/Help_S5.html':
return <Navigate to="/help/search/5" />;
case "/Help_Dataset.html":
case '/Help_Dataset.html':
return <Navigate to="/help/dataset" />;
case "/Help_Viewer.html":
case '/Help_Viewer.html':
return <Navigate to="/help/viewer" />;
case "/Contact.html":
case '/Contact.html':
return <Navigate to="/help/contact" />;
case "/cgi-bin/SearchResult.cgi":
case '/cgi-bin/SearchResult.cgi':
return <Navigate to="/search/results" />;
default:
default: {
const match = pathname.match(/^\/DB\/TE\/(3D-)?(.+)\.html$/);
if (match !== null) {
const url = "/data/" + match[2] + (typeof match[1] !== "undefined" ? "/viewer" : "");
const url = `/data/${match[2]}${typeof match[1] !== 'undefined' ? '/viewer' : ''}`;
return <Navigate to={url} />;
}
break;
}
}
return <NotFound />;
};
+9 -6
View File
@@ -1,16 +1,19 @@
import React, { useEffect } from "react";
import { useAppDispatch } from "../../app/hooks";
import { setTitle } from "../page-title/pageTitleSlice";
import styles from "./Contact.module.css";
import type React from 'react';
import { useEffect } from 'react';
import { useAppDispatch } from '../../app/hooks';
import { setTitle } from '../page-title/pageTitleSlice';
import styles from './Contact.module.css';
const Contact: React.FC = () => {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setTitle("Contact Information"));
dispatch(setTitle('Contact Information'));
}, [dispatch]);
return (
<div className={styles.contact}>
<section className={styles.notice}>Notice: This site has been archived since May 2019 and is no longer updated.</section>
<section className={styles.notice}>
Notice: This site has been archived since May 2019 and is no longer updated.
</section>
<h4>CelLoc-3D is managed by Teppei Ebina and Tadaharu Tsumoto</h4>
<p>
Laboratory for cortical circuit plasticity
+100 -119
View File
@@ -1,12 +1,12 @@
import axios from "axios";
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { useAppDispatch } from "../../app/hooks";
import DataUtils from "../data/DataUtils";
import Loading from "../loading/Loading";
import NotFound from "../not-found/NotFound";
import { setTitle } from "../page-title/pageTitleSlice";
import styles from "./DataDetail.module.css";
import ky from 'ky';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useAppDispatch } from '../../app/hooks';
import DataUtils from '../data/DataUtils';
import Loading from '../loading/Loading';
import NotFound from '../not-found/NotFound';
import { setTitle } from '../page-title/pageTitleSlice';
import styles from './DataDetail.module.css';
interface DetailDataJson {
title: string;
@@ -28,129 +28,110 @@ interface DetailDataJson {
releaseDate: string;
}
interface PropsFC {
interface Props {
name: string;
}
interface Props extends PropsFC {
dispatch: any;
}
const DataDetail: React.FC<Props> = ({ name }) => {
const dispatch = useAppDispatch();
const [data, setData] = useState<DetailDataJson | null>(null);
const [error, setError] = useState(false);
interface State {
name: string;
data: DetailDataJson | null;
}
useEffect(() => {
let isActive = true;
class DataDetail extends Component<Props, State> {
isActive = false;
constructor(props: Props) {
super(props);
this.state = {
name: props.name,
data: null,
};
}
componentDidMount() {
const { name } = this.state;
this.isActive = true;
if (!DataUtils.exists(name)) {
if (name !== "") {
this.setState({ name: "" });
}
return;
}
axios
.get(`/data/${name}.json`, { responseType: "json" })
.then((response) => {
if (this.isActive) {
this.props.dispatch(setTitle(`DS: ${name}`));
this.setState({ data: response.data as DetailDataJson });
setData(null);
setError(false);
ky.get(`/data/${name}.json`)
.json<DetailDataJson>()
.then((result) => {
if (isActive) {
dispatch(setTitle(`DS: ${name}`));
setData(result);
}
})
.catch((error) => {
if (this.isActive) {
this.setState({ name: "" });
.catch(() => {
if (isActive) {
setError(true);
}
});
return () => {
isActive = false;
};
}, [name, dispatch]);
if (name === '' || error) {
return <NotFound />;
}
if (!data) {
return <Loading />;
}
componentWillUnmount() {
this.isActive = false;
}
render() {
const { name, data } = this.state;
if (name === "") {
return <NotFound />;
}
if (!data) {
return <Loading />;
}
return (
<section className={styles.detail}>
<h4 className={styles.title}>{data.title}</h4>
<div className={styles.author}>{data.author}</div>
<figure>
<img src={data.figure.file} alt={data.figure.caption} />
<figcaption>{data.figure.caption}</figcaption>
</figure>
<dl>
<dt>Download &amp; View</dt>
<dd>
<a href={data.download}>Text download</a>
</dd>
<dd>
<Link to={data.view}>View 3D structure</Link>
</dd>
</dl>
<dl>
<dt>Mouse line</dt>
<dd>{data.mouseLine}</dd>
</dl>
<dl>
<dt>Imaging area</dt>
<dd>{data.imagingArea}</dd>
</dl>
<dl>
<dt>Cell type</dt>
{data.cellType.map((value, key) => {
return <dd key={key}>{value}</dd>;
})}
</dl>
<dl>
<dt>Postnatal days</dt>
<dd>{data.postnatalDays}</dd>
</dl>
<dl>
<dt>Imaging volume size</dt>
<dd>{data.imagingVolumeSize}</dd>
</dl>
<dl>
<dt>Imaging depth</dt>
<dd>{data.imagingDepth}</dd>
</dl>
<dl>
<dt>Journal</dt>
<dd>{data.journal}</dd>
</dl>
<dl>
<dt>Summary</dt>
<dd dangerouslySetInnerHTML={{ __html: data.summary }}></dd>
</dl>
<dl>
<dt>Release date</dt>
<dd>{data.releaseDate}</dd>
</dl>
</section>
);
}
}
const DataDetailFC: React.FC<PropsFC> = (props) => {
const dispatch = useAppDispatch();
return <DataDetail dispatch={dispatch} name={props.name} />;
return (
<section className={styles.detail}>
<h4 className={styles.title}>{data.title}</h4>
<div className={styles.author}>{data.author}</div>
<figure>
<img src={data.figure.file} alt={data.figure.caption} />
<figcaption>{data.figure.caption}</figcaption>
</figure>
<dl>
<dt>Download &amp; View</dt>
<dd>
<a href={data.download}>Text download</a>
</dd>
<dd>
<Link to={data.view}>View 3D structure</Link>
</dd>
</dl>
<dl>
<dt>Mouse line</dt>
<dd>{data.mouseLine}</dd>
</dl>
<dl>
<dt>Imaging area</dt>
<dd>{data.imagingArea}</dd>
</dl>
<dl>
<dt>Cell type</dt>
{data.cellType.map((value, key) => {
// biome-ignore lint/suspicious/noArrayIndexKey: cell type values may duplicate
return <dd key={key}>{value}</dd>;
})}
</dl>
<dl>
<dt>Postnatal days</dt>
<dd>{data.postnatalDays}</dd>
</dl>
<dl>
<dt>Imaging volume size</dt>
<dd>{data.imagingVolumeSize}</dd>
</dl>
<dl>
<dt>Imaging depth</dt>
<dd>{data.imagingDepth}</dd>
</dl>
<dl>
<dt>Journal</dt>
<dd>{data.journal}</dd>
</dl>
<dl>
<dt>Summary</dt>
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: summary is trusted content from our own JSON */}
<dd dangerouslySetInnerHTML={{ __html: data.summary }}></dd>
</dl>
<dl>
<dt>Release date</dt>
<dd>{data.releaseDate}</dd>
</dl>
</section>
);
};
export default DataDetailFC;
export default DataDetail;
+92 -107
View File
@@ -1,14 +1,15 @@
import axios from "axios";
import React, { Component } from "react";
import { Link } from "react-router-dom";
import * as THREE from "three";
import { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
import { useAppDispatch } from "../../app/hooks";
import DataUtils from "../data/DataUtils";
import Loading from "../loading/Loading";
import NotFound from "../not-found/NotFound";
import { setTitle } from "../page-title/pageTitleSlice";
import styles from "./DataViewer.module.css";
import ky from 'ky';
import type React from 'react';
import { useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import * as THREE from 'three';
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls';
import { useAppDispatch } from '../../app/hooks';
import DataUtils from '../data/DataUtils';
import Loading from '../loading/Loading';
import NotFound from '../not-found/NotFound';
import { setTitle } from '../page-title/pageTitleSlice';
import styles from './DataViewer.module.css';
class Viewer3D {
private mount: HTMLDivElement;
@@ -26,15 +27,15 @@ class Viewer3D {
}
init(data: string) {
const lines = data.split("\n");
const lines = data.split('\n');
const head = lines
.shift()
?.trim()
.split("\t")
.map((line, k) => {
return parseInt(line.replace(/^#/g, ""), 10) || 0;
.split('\t')
.map((line) => {
return parseInt(line.replace(/^#/g, ''), 10) || 0;
});
if (typeof head === "undefined" || head.length !== 3 || head.includes(0)) {
if (typeof head === 'undefined' || head.length !== 3 || head.includes(0)) {
return false;
}
const width = this.mount.clientWidth;
@@ -65,9 +66,9 @@ class Viewer3D {
};
const geometory = new THREE.SphereGeometry(4, 10, 10);
lines.forEach((line) => {
const point = line.trim().split("\t");
if (point.length === 4 && material.hasOwnProperty(point[0])) {
const particle = new THREE.Mesh(geometory, material[point[0] as "e" | "i" | "g" | "p" | "s" | "ps"]);
const point = line.trim().split('\t');
if (point.length === 4 && Object.hasOwn(material, point[0])) {
const particle = new THREE.Mesh(geometory, material[point[0] as 'e' | 'i' | 'g' | 'p' | 's' | 'ps']);
particle.position.x = parseFloat(point[1]) - head[0] / 2.0;
particle.position.y = parseFloat(point[2]) - head[1] / 2.0;
particle.position.z = head[2] - parseFloat(point[3]) * 2.0;
@@ -79,7 +80,7 @@ class Viewer3D {
this.renderer.setSize(width, height);
this.renderer.setClearColor(0xffffff);
this.mount.appendChild(this.renderer.domElement);
} catch (error) {
} catch {
return false;
}
return true;
@@ -90,118 +91,102 @@ class Viewer3D {
}
stop() {
this.frameId && cancelAnimationFrame(this.frameId);
this.renderer && this.mount.removeChild(this.renderer.domElement);
if (this.frameId) cancelAnimationFrame(this.frameId);
if (this.renderer && this.mount) this.mount.removeChild(this.renderer.domElement);
}
animate() {
this.controls && this.controls.update();
this.renderer && this.renderer.clear();
this.renderer && this.scene && this.camera && this.renderer.render(this.scene, this.camera);
if (this.controls) this.controls.update();
if (this.renderer) this.renderer.clear();
if (this.renderer && this.scene && this.camera) this.renderer.render(this.scene, this.camera);
this.frameId = requestAnimationFrame(this.animate);
}
}
interface PropsFC {
interface DataViewerProps {
name: string;
}
interface Props extends PropsFC {
dispatch: any;
}
const DataViewer: React.FC<DataViewerProps> = ({ name: initialName }) => {
const dispatch = useAppDispatch();
const [name, setName] = useState(initialName);
const [data, setData] = useState('');
const mountRef = useRef<HTMLDivElement>(null);
const viewerRef = useRef<Viewer3D | null>(null);
const isActiveRef = useRef(true);
interface State {
name: string;
data: string;
}
class DataViewer extends Component<Props, State> {
private isActive = false;
private mount: HTMLDivElement | null = null;
private viewer: Viewer3D | null = null;
constructor(props: Props) {
super(props);
this.state = {
name: this.props.name,
data: "",
};
}
componentDidMount() {
const { name } = this.state;
this.isActive = true;
useEffect(() => {
isActiveRef.current = true;
if (!DataUtils.exists(name)) {
if (name !== "") {
this.setState({ name: "" });
if (name !== '') {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional validation reset
setName('');
}
return;
}
axios
.get(`/data/${name}.dat`, { responseType: "text" })
.then((response) => {
if (this.isActive) {
this.props.dispatch(setTitle(`View 3D: ${name}`));
this.setState({ data: response.data });
const controller = new AbortController();
ky.get(`/data/${name}.dat`, { signal: controller.signal })
.text()
.then((data) => {
if (isActiveRef.current) {
dispatch(setTitle(`View 3D: ${name}`));
setData(data);
}
})
.catch((error) => {
if (this.isActive) {
this.setState({ name: "" });
.catch(() => {
if (isActiveRef.current && !controller.signal.aborted) {
setName('');
}
});
}
return () => {
controller.abort();
};
}, [name, dispatch]);
componentDidUpdate(prevProps: Props, prevState: State) {
if (this.mount && this.state.data !== "" && !this.viewer) {
const viewer = new Viewer3D(this.mount);
if (viewer.init(this.state.data)) {
this.viewer = viewer;
this.viewer.start();
useEffect(() => {
if (mountRef.current && data !== '') {
// Stop and clean up the old viewer if switching datasets
if (viewerRef.current) {
viewerRef.current.stop();
viewerRef.current = null;
}
const viewer = new Viewer3D(mountRef.current);
if (viewer.init(data)) {
viewerRef.current = viewer;
viewerRef.current.start();
} else {
this.setState({ name: "@@NOWEBGL@@" });
setName('@@NOWEBGL@@');
}
}
}
}, [data]);
componentWillUnmount() {
this.isActive = false;
if (this.viewer) {
this.viewer.stop();
this.viewer = null;
}
}
useEffect(() => {
return () => {
isActiveRef.current = false;
if (viewerRef.current) {
viewerRef.current.stop();
viewerRef.current = null;
}
};
}, []);
render() {
const { name, data } = this.state;
switch (name) {
case "":
return <NotFound />;
case "@@NOWEBGL@@":
return <div className={styles.no_webgl}>It does not appear your computer supports WebGL.</div>;
}
if (data === "") {
return <Loading />;
}
return (
<section>
<div className={styles.back}>
<Link to={`/data/${name}`}>Back to {name}</Link>
</div>
<div
className={styles.viewer}
ref={(mount) => {
this.mount = mount;
}}
/>
</section>
);
switch (name) {
case '':
return <NotFound />;
case '@@NOWEBGL@@':
return <div className={styles.no_webgl}>It does not appear your computer supports WebGL.</div>;
}
}
const DataViewerFC: React.FC<PropsFC> = (props) => {
const dispatch = useAppDispatch();
return <DataViewer dispatch={dispatch} name={props.name} />;
if (data === '') {
return <Loading />;
}
return (
<section>
<div className={styles.back}>
<Link to={`/data/${name}`}>Back to {name}</Link>
</div>
<div className={styles.viewer} ref={mountRef} />
</section>
);
};
export default DataViewerFC;
export default DataViewer;
+9 -9
View File
@@ -1,14 +1,14 @@
import React from "react";
import { Route, Routes, useParams } from "react-router-dom";
import DataDetail from "../data-detail/DataDetail";
import DataViewer from "../data-viewer/DataViewer";
import NotFound from "../not-found/NotFound";
import styles from "./Data.module.css";
import DataUtils from "./DataUtils";
import type React from 'react';
import { Route, Routes, useParams } from 'react-router-dom';
import DataDetail from '../data-detail/DataDetail';
import DataViewer from '../data-viewer/DataViewer';
import NotFound from '../not-found/NotFound';
import styles from './Data.module.css';
import DataUtils from './DataUtils';
const Data: React.FC = () => {
const { name } = useParams<"name">();
if (typeof name === "undefined" || !DataUtils.exists(name)) {
const { name } = useParams<'name'>();
if (typeof name === 'undefined' || !DataUtils.exists(name)) {
return <NotFound />;
}
return (
+37 -39
View File
@@ -1,7 +1,6 @@
import JsonData_ from "../../assets/data.json";
import JsonData_ from '../../assets/data.json';
const cellTypes = ["E", "I", "G", "P", "S"] as const;
type TCellType = typeof cellTypes[number];
type TCellType = 'E' | 'I' | 'G' | 'P' | 'S';
interface JsonDataItem {
pd: number;
@@ -48,42 +47,41 @@ export type SearchResults = SearchResult[];
const jsonData: JsonData = JsonData_;
class DataUtils {
static exists(name: string): boolean {
return jsonData.hasOwnProperty(name);
}
static search(params: SearchParams): SearchResults {
const results: SearchResults = [];
for (let name in jsonData) {
const item = jsonData[name];
if (item.pd < params.pdStart || params.pdEnd < item.pd) {
continue;
}
if (item.recordingVolume.width < params.widthMin || params.widthMax < item.recordingVolume.width) {
continue;
}
if (item.imagingDepth.from < params.depthMin || params.depthMax < item.imagingDepth.to) {
continue;
}
if (params.cellType !== "any") {
if (!item.cellType.includes(params.cellType as TCellType)) {
continue;
}
}
if (params.area !== "all" && item.area !== params.area) {
continue;
}
if (params.geneType !== "all" && item.geneType !== params.geneType) {
continue;
}
if (params.keyword.length !== 0 && !item.remark.includes(params.keyword)) {
continue;
}
results.push({ name, item });
}
return results;
}
export function exists(name: string): boolean {
return Object.hasOwn(jsonData, name);
}
export function search(params: SearchParams): SearchResults {
const results: SearchResults = [];
for (const name in jsonData) {
const item = jsonData[name];
if (item.pd < params.pdStart || params.pdEnd < item.pd) {
continue;
}
if (item.recordingVolume.width < params.widthMin || params.widthMax < item.recordingVolume.width) {
continue;
}
if (item.imagingDepth.from < params.depthMin || params.depthMax < item.imagingDepth.to) {
continue;
}
if (params.cellType !== 'any') {
if (!item.cellType.includes(params.cellType as TCellType)) {
continue;
}
}
if (params.area !== 'all' && item.area !== params.area) {
continue;
}
if (params.geneType !== 'all' && item.geneType !== params.geneType) {
continue;
}
if (params.keyword.length !== 0 && !item.remark.includes(params.keyword)) {
continue;
}
results.push({ name, item });
}
return results;
}
const DataUtils = { exists, search };
export default DataUtils;
+30 -25
View File
@@ -1,16 +1,17 @@
import React, { useEffect } from "react";
import { Link, Route, Routes } from "react-router-dom";
import { useAppDispatch } from "../../app/hooks";
import NotFound from "../not-found/NotFound";
import { setTitle } from "../page-title/pageTitleSlice";
import styles from "./Help.module.css";
import type React from 'react';
import { useEffect } from 'react';
import { Link, Route, Routes } from 'react-router-dom';
import { useAppDispatch } from '../../app/hooks';
import NotFound from '../not-found/NotFound';
import { setTitle } from '../page-title/pageTitleSlice';
import styles from './Help.module.css';
const HelpIndex = () => {
const title = "Help";
const title = 'Help';
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setTitle(title));
}, [dispatch, title]);
}, [dispatch]);
return (
<ul className={styles.index}>
<li>
@@ -49,11 +50,11 @@ const HelpSearchLinks = (
);
const HelpSearch1 = () => {
const title = "Help - Search dataset - Step 1";
const title = 'Help - Search dataset - Step 1';
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setTitle(title));
}, [dispatch, title]);
}, [dispatch]);
return (
<div>
{HelpSearchLinks}
@@ -67,11 +68,11 @@ const HelpSearch1 = () => {
};
const HelpSearch2 = () => {
const title = "Help - Search dataset - Step 2";
const title = 'Help - Search dataset - Step 2';
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setTitle(title));
}, [dispatch, title]);
}, [dispatch]);
return (
<div>
{HelpSearchLinks}
@@ -85,11 +86,11 @@ const HelpSearch2 = () => {
};
const HelpSearch3 = () => {
const title = "Help - Search dataset - Step 3";
const title = 'Help - Search dataset - Step 3';
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setTitle(title));
}, [dispatch, title]);
}, [dispatch]);
return (
<div>
{HelpSearchLinks}
@@ -103,11 +104,11 @@ const HelpSearch3 = () => {
};
const HelpSearch4 = () => {
const title = "Help - Search dataset - Step 4";
const title = 'Help - Search dataset - Step 4';
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setTitle(title));
}, [dispatch, title]);
}, [dispatch]);
return (
<div>
{HelpSearchLinks}
@@ -121,11 +122,11 @@ const HelpSearch4 = () => {
};
const HelpSearch5 = () => {
const title = "Help - Search dataset - Step 5";
const title = 'Help - Search dataset - Step 5';
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setTitle(title));
}, [dispatch, title]);
}, [dispatch]);
return (
<div>
{HelpSearchLinks}
@@ -133,7 +134,8 @@ const HelpSearch5 = () => {
<p>
The result page consists of details of the dataset.
<br />
Click the links in the "Download &amp; View" section to download the dataset and/or view 3D model on your browser.{" "}
Click the links in the "Download &amp; View" section to download the dataset and/or view 3D model on your
browser.{' '}
</p>
<div className={styles.figure}>
<img src="/images/help-search5.jpg" alt={title} />
@@ -143,11 +145,11 @@ const HelpSearch5 = () => {
};
const HelpDataset = () => {
const title = "Help - Downloadable dataset format";
const title = 'Help - Downloadable dataset format';
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setTitle(title));
}, [dispatch, title]);
}, [dispatch]);
return (
<section>
<div className={styles.title}>Dataset Format</div>
@@ -155,21 +157,24 @@ const HelpDataset = () => {
<img src="/images/help-dataset.jpg" alt={title} />
</div>
<p>
<span className={styles.legend}>First row:</span> The first row of the data file indicates the size of the imaging volume. The first, second and third column represent the width, height and depth of the volume, respectively.
<span className={styles.legend}>First row:</span> The first row of the data file indicates the size of the
imaging volume. The first, second and third column represent the width, height and depth of the volume,
respectively.
</p>
<p>
<span className={styles.legend}>Other rows:</span> Each row consists of the type (first column) and the position (second to fourth columns for x, y and z position) of the cell.
<span className={styles.legend}>Other rows:</span> Each row consists of the type (first column) and the position
(second to fourth columns for x, y and z position) of the cell.
</p>
</section>
);
};
const HelpViewer = () => {
const title = "Help - 3D Viewer Manual";
const title = 'Help - 3D Viewer Manual';
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setTitle(title));
}, [dispatch, title]);
}, [dispatch]);
return (
<section>
<div className={styles.title}>3D Viewer Manual</div>
+6 -5
View File
@@ -1,12 +1,13 @@
import React, { useEffect } from "react";
import { useAppDispatch } from "../../app/hooks";
import { setTitle } from "../page-title/pageTitleSlice";
import styles from "./Loading.module.css";
import type React from 'react';
import { useEffect } from 'react';
import { useAppDispatch } from '../../app/hooks';
import { setTitle } from '../page-title/pageTitleSlice';
import styles from './Loading.module.css';
const Loading: React.FC = () => {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setTitle("Loading now..."));
dispatch(setTitle('Loading now...'));
}, [dispatch]);
return <div className={styles.loading}>Loading now...</div>;
+8 -6
View File
@@ -1,9 +1,10 @@
import React, { useEffect } from "react";
import { useAppDispatch } from "../../app/hooks";
import JsonNews_ from "../../assets/news.json";
import { setTitle } from "../page-title/pageTitleSlice";
import styles from "./News.module.css";
const nl2br = require("react-nl2br");
import type React from 'react';
import { useEffect } from 'react';
import { useAppDispatch } from '../../app/hooks';
import JsonNews_ from '../../assets/news.json';
import nl2br from '../../utils/nl2br';
import { setTitle } from '../page-title/pageTitleSlice';
import styles from './News.module.css';
interface IJsonNewsItem {
date: string;
@@ -24,6 +25,7 @@ const News: React.FC = () => {
<div className={styles.news}>
{JsonNews.map((item, key) => {
return (
// biome-ignore lint/suspicious/noArrayIndexKey: static JSON data
<div className={styles.item} key={key}>
<div className={styles.date}>{item.date}</div>
<div className={styles.desc}>{nl2br(item.desc)}</div>
+6 -5
View File
@@ -1,12 +1,13 @@
import React, { useEffect } from "react";
import { useAppDispatch } from "../../app/hooks";
import { setTitle } from "../page-title/pageTitleSlice";
import styles from "./NotFound.module.css";
import type React from 'react';
import { useEffect } from 'react';
import { useAppDispatch } from '../../app/hooks';
import { setTitle } from '../page-title/pageTitleSlice';
import styles from './NotFound.module.css';
const NotFound: React.FC = () => {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setTitle("Page Not Found"));
dispatch(setTitle('Page Not Found'));
}, [dispatch]);
return <div className={styles.notFound}>Page not found.</div>;
+5 -5
View File
@@ -1,8 +1,8 @@
import React from "react";
import { Helmet } from "react-helmet-async";
import { useAppSelector } from "../../app/hooks";
import styles from "./PageTitle.module.css";
import { selectPageTitle } from "./pageTitleSlice";
import type React from 'react';
import { Helmet } from 'react-helmet-async';
import { useAppSelector } from '../../app/hooks';
import styles from './PageTitle.module.css';
import { selectPageTitle } from './pageTitleSlice';
const PageTitle: React.FC = () => {
const title = useAppSelector(selectPageTitle);
+4 -4
View File
@@ -1,16 +1,16 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../../app/store';
export interface PageTitleState {
value: string;
}
const initialState: PageTitleState = {
value: "",
value: '',
};
const pageTitleSlice = createSlice({
name: "pageTitle",
name: 'pageTitle',
initialState,
reducers: {
setTitle: (state, action: PayloadAction<string>) => {
+198 -193
View File
@@ -1,198 +1,203 @@
import React, { Component } from "react";
import { useNavigate } from "react-router-dom";
import { useAppDispatch } from "../../app/hooks";
import DataUtils, { SearchResults } from "../data/DataUtils";
import { setTitle } from "../page-title/pageTitleSlice";
import { setResults } from "../search-results/searchResultsSlice";
import styles from "./SearchForm.module.css";
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAppDispatch } from '../../app/hooks';
import DataUtils, { type SearchResults } from '../data/DataUtils';
import { setTitle } from '../page-title/pageTitleSlice';
import { setResults } from '../search-results/searchResultsSlice';
import styles from './SearchForm.module.css';
interface FCProps {}
interface Props extends FCProps {
navigate: any;
dispatch: any;
}
interface State {
pdStart: string;
pdEnd: string;
widthMin: string;
widthMax: string;
depthMin: string;
depthMax: string;
cellType: string;
area: string;
geneType: string;
keyword: string;
}
class SearchForm extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
pdStart: "44",
pdEnd: "100",
widthMin: "317",
widthMax: "512",
depthMin: "90",
depthMax: "230",
cellType: "any",
area: "all",
geneType: "all",
keyword: "",
};
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSelectChange = this.handleSelectChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
this.props.dispatch(setTitle("Database Search"));
}
doSearch(): SearchResults {
const toNumber = (v: string): number => parseInt(v.trim(), 10);
const params = {
pdStart: toNumber(this.state.pdStart),
pdEnd: toNumber(this.state.pdEnd),
widthMin: toNumber(this.state.widthMin),
widthMax: toNumber(this.state.widthMax),
depthMin: toNumber(this.state.depthMin),
depthMax: toNumber(this.state.depthMax),
cellType: this.state.cellType.trim(),
area: this.state.area.trim(),
geneType: this.state.geneType.trim(),
keyword: this.state.keyword.trim(),
};
return DataUtils.search(params);
}
handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (event: React.ChangeEvent<HTMLInputElement>) => {
const target = event.target;
const value = target.value;
const name = target.name;
switch (name) {
case "pdStart":
this.setState({ pdStart: value });
break;
case "pdEnd":
this.setState({ pdStart: value });
break;
case "widthMin":
this.setState({ widthMin: value });
break;
case "widthMax":
this.setState({ widthMax: value });
break;
case "depthMin":
this.setState({ depthMin: value });
break;
case "depthMax":
this.setState({ depthMax: value });
break;
case "cellType":
this.setState({ cellType: value });
break;
case "keyword":
this.setState({ keyword: value });
break;
}
};
handleSelectChange: React.ChangeEventHandler<HTMLSelectElement> = (event: React.ChangeEvent<HTMLSelectElement>) => {
const target = event.target;
const value = target.value;
const name = target.name;
switch (name) {
case "area":
this.setState({ area: value });
break;
case "geneType":
this.setState({ geneType: value });
break;
}
};
handleSubmit: React.FormEventHandler<HTMLFormElement> = (event: React.FormEvent) => {
event.preventDefault();
const results = this.doSearch();
this.props.dispatch(setResults(results));
this.props.navigate("/search/results");
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<div className={styles.formGroup}>
<label htmlFor="pdStart">Postnatal Days</label>
<div className={styles.value}>
<input type="number" name="pdStart" className={styles.txtParam} value={this.state.pdStart} onChange={this.handleInputChange} />
<span className={styles.range}>-</span>
<input type="number" name="pdEnd" className={styles.txtParam} value={this.state.pdEnd} onChange={this.handleInputChange} />
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="widthMin">Recording volume width (height)</label>
<div className={styles.value}>
<input type="number" name="widthMin" className={styles.txtParam} value={this.state.widthMin} onChange={this.handleInputChange} />
<span className={styles.range}>-</span>
<input type="number" name="widthMax" className={styles.txtParam} value={this.state.widthMax} onChange={this.handleInputChange} />
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="depthMin">Recording volume depth</label>
<div className={styles.value}>
<input type="number" name="depthMin" className={styles.txtParam} value={this.state.depthMin} onChange={this.handleInputChange} />
<span className={styles.range}>-</span>
<input type="number" name="depthMax" className={styles.txtParam} value={this.state.depthMax} onChange={this.handleInputChange} />
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="cellType">Cell type (E, I, G, P, S, or "any")</label>
<div className={styles.value}>
<input type="text" name="cellType" className={styles.txtParamStr} value={this.state.cellType} onChange={this.handleInputChange} />
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="area">Target area</label>
<div className={styles.value}>
<select name="area" className={styles.selParam} value={this.state.area} onChange={this.handleSelectChange}>
<option value="all">All</option>
<option value="L2/3, V1">L2/3 in the visual cortex</option>
</select>
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="geneType">Mouse line</label>
<div className={styles.value}>
<select name="geneType" className={styles.selParam} value={this.state.geneType} onChange={this.handleSelectChange}>
<option value="all">All</option>
<option value="VGAT-Venus">VGAT-Venus</option>
<option value="PV/myrGFP-LDLRct">PV/myrGFP-LDLRct</option>
<option value="Dlx5/6-GCaMP3">Dlx5/6-GCaMP3</option>
</select>
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="keyword">Keyword Search</label>
<div className={styles.value}>
<input type="text" name="keyword" className={styles.txtKeyword} value={this.state.keyword} onChange={this.handleInputChange} />
</div>
</div>
<div className={styles.formGroup}>
<div className={styles.submit}>
<input type="submit" value="Search" />
</div>
</div>
</form>
);
}
}
const SearchFormFC: React.FC<FCProps> = (props: FCProps) => {
const SearchForm: React.FC = () => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
return <SearchForm navigate={navigate} dispatch={dispatch} />;
const [pdStart, setPdStart] = useState('44');
const [pdEnd, setPdEnd] = useState('100');
const [widthMin, setWidthMin] = useState('317');
const [widthMax, setWidthMax] = useState('512');
const [depthMin, setDepthMin] = useState('90');
const [depthMax, setDepthMax] = useState('230');
const [cellType, setCellType] = useState('any');
const [area, setArea] = useState('all');
const [geneType, setGeneType] = useState('all');
const [keyword, setKeyword] = useState('');
useEffect(() => {
dispatch(setTitle('Database Search'));
}, [dispatch]);
const doSearch = (): SearchResults => {
const toNumber = (v: string): number => parseInt(v.trim(), 10);
const params = {
pdStart: toNumber(pdStart),
pdEnd: toNumber(pdEnd),
widthMin: toNumber(widthMin),
widthMax: toNumber(widthMax),
depthMin: toNumber(depthMin),
depthMax: toNumber(depthMax),
cellType: cellType.trim(),
area: area.trim(),
geneType: geneType.trim(),
keyword: keyword.trim(),
};
return DataUtils.search(params);
};
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
const { name, value } = event.target;
switch (name) {
case 'pdStart':
setPdStart(value);
break;
case 'pdEnd':
setPdEnd(value);
break;
case 'widthMin':
setWidthMin(value);
break;
case 'widthMax':
setWidthMax(value);
break;
case 'depthMin':
setDepthMin(value);
break;
case 'depthMax':
setDepthMax(value);
break;
case 'cellType':
setCellType(value);
break;
case 'keyword':
setKeyword(value);
break;
}
};
const handleSelectChange: React.ChangeEventHandler<HTMLSelectElement> = (event) => {
const { name, value } = event.target;
switch (name) {
case 'area':
setArea(value);
break;
case 'geneType':
setGeneType(value);
break;
}
};
const handleSubmit: React.FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
const results = doSearch();
dispatch(setResults(results));
navigate('/search/results');
};
return (
<form onSubmit={handleSubmit}>
<div className={styles.formGroup}>
<label htmlFor="pdStart">Postnatal Days</label>
<div className={styles.value}>
<input
type="number"
name="pdStart"
className={styles.txtParam}
value={pdStart}
onChange={handleInputChange}
/>
<span className={styles.range}>-</span>
<input type="number" name="pdEnd" className={styles.txtParam} value={pdEnd} onChange={handleInputChange} />
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="widthMin">Recording volume width (height)</label>
<div className={styles.value}>
<input
type="number"
name="widthMin"
className={styles.txtParam}
value={widthMin}
onChange={handleInputChange}
/>
<span className={styles.range}>-</span>
<input
type="number"
name="widthMax"
className={styles.txtParam}
value={widthMax}
onChange={handleInputChange}
/>
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="depthMin">Recording volume depth</label>
<div className={styles.value}>
<input
type="number"
name="depthMin"
className={styles.txtParam}
value={depthMin}
onChange={handleInputChange}
/>
<span className={styles.range}>-</span>
<input
type="number"
name="depthMax"
className={styles.txtParam}
value={depthMax}
onChange={handleInputChange}
/>
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="cellType">Cell type (E, I, G, P, S, or "any")</label>
<div className={styles.value}>
<input
type="text"
name="cellType"
className={styles.txtParamStr}
value={cellType}
onChange={handleInputChange}
/>
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="area">Target area</label>
<div className={styles.value}>
<select name="area" className={styles.selParam} value={area} onChange={handleSelectChange}>
<option value="all">All</option>
<option value="L2/3, V1">L2/3 in the visual cortex</option>
</select>
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="geneType">Mouse line</label>
<div className={styles.value}>
<select name="geneType" className={styles.selParam} value={geneType} onChange={handleSelectChange}>
<option value="all">All</option>
<option value="VGAT-Venus">VGAT-Venus</option>
<option value="PV/myrGFP-LDLRct">PV/myrGFP-LDLRct</option>
<option value="Dlx5/6-GCaMP3">Dlx5/6-GCaMP3</option>
</select>
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="keyword">Keyword Search</label>
<div className={styles.value}>
<input
type="text"
name="keyword"
className={styles.txtKeyword}
value={keyword}
onChange={handleInputChange}
/>
</div>
</div>
<div className={styles.formGroup}>
<div className={styles.submit}>
<input type="submit" value="Search" />
</div>
</div>
</form>
);
};
export default SearchFormFC;
export default SearchForm;
@@ -1,15 +1,16 @@
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { setTitle } from "../page-title/pageTitleSlice";
import styles from "./SearchResults.module.css";
import { selectSearchResults } from "./searchResultsSlice";
import type React from 'react';
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useAppDispatch, useAppSelector } from '../../app/hooks';
import { setTitle } from '../page-title/pageTitleSlice';
import styles from './SearchResults.module.css';
import { selectSearchResults } from './searchResultsSlice';
const SearchResults: React.FC = () => {
const results = useAppSelector(selectSearchResults);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setTitle("Search Results"));
dispatch(setTitle('Search Results'));
}, [dispatch]);
return (
@@ -19,7 +20,7 @@ const SearchResults: React.FC = () => {
) : (
results.map((result, key) => {
return (
<div className={styles.result} key={key}>
<div className={styles.result} key={result.name}>
<div className={styles.title}>
<Link to={`/data/${result.name}`}>
{key + 1}. {result.name}
@@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import { SearchResults } from "../data/DataUtils";
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../../app/store';
import type { SearchResults } from '../data/DataUtils';
export interface SearchResultsState {
value: SearchResults;
@@ -11,7 +11,7 @@ const initialState: SearchResultsState = {
};
const searchResultsSlice = createSlice({
name: "searchResults",
name: 'searchResults',
initialState,
reducers: {
setResults: (state, action: PayloadAction<SearchResults>) => {
+6 -6
View File
@@ -1,9 +1,9 @@
import React from "react";
import { Route, Routes } from "react-router-dom";
import NotFound from "../not-found/NotFound";
import SearchForm from "../search-form/SearchForm";
import SearchResults from "../search-results/SearchResults";
import styles from "./Search.module.css";
import type React from 'react';
import { Route, Routes } from 'react-router-dom';
import NotFound from '../not-found/NotFound';
import SearchForm from '../search-form/SearchForm';
import SearchResults from '../search-results/SearchResults';
import styles from './Search.module.css';
const Search: React.FC = () => {
return (
+3 -3
View File
@@ -1,9 +1,9 @@
@import-normalize;
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+9 -17
View File
@@ -1,15 +1,12 @@
import React from "react";
import "react-app-polyfill/ie11";
import "react-app-polyfill/stable";
import ReactDOM from "react-dom/client";
import { HelmetProvider } from "react-helmet-async";
import { Provider } from "react-redux";
import App from "./App";
import { store } from "./app/store";
import "./index.css";
import reportWebVitals from "./reportWebVitals";
import React from 'react';
import ReactDOM from 'react-dom/client';
import { HelmetProvider } from 'react-helmet-async';
import { Provider } from 'react-redux';
import App from './App';
import { store } from './app/store';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<Provider store={store}>
@@ -17,10 +14,5 @@ root.render(
<App />
</HelmetProvider>
</Provider>
</React.StrictMode>
</React.StrictMode>,
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
-1
View File
@@ -1 +0,0 @@
/// <reference types="react-scripts" />
-15
View File
@@ -1,15 +0,0 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
-5
View File
@@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
const newlineRegex = /(\r\n|\r|\n)/g;
export default function nl2br(str: string): (string | React.ReactElement)[] {
if (typeof str !== 'string') {
return [str];
}
let counter = 0;
return str.split(newlineRegex).map((line) => {
if (line.match(newlineRegex)) {
return <br key={`br-${counter++}`} />;
}
return <React.Fragment key={`line-${counter++}`}>{line}</React.Fragment>;
});
}
+36
View File
@@ -0,0 +1,36 @@
/// <reference types="vite/client" />
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
// Workaround: @types/three's exports map uses .js extensions that bundler resolution
// cannot resolve. Declare TrackballControls directly.
declare module 'three/examples/jsm/controls/TrackballControls' {
import type { Camera, Vector3 } from 'three';
export class TrackballControls {
constructor(camera: Camera, domElement?: HTMLElement);
object: Camera;
domElement: HTMLElement;
enabled: boolean;
screen: { left: number; top: number; right: number; bottom: number };
rotateSpeed: number;
zoomSpeed: number;
panSpeed: number;
staticMoving: boolean;
dynamicDampingFactor: number;
minDistance: number;
maxDistance: number;
minZoom: number;
maxZoom: number;
noRotate: boolean;
noZoom: boolean;
noPan: boolean;
noRoll: boolean;
target: Vector3;
update(deltaTime?: number): boolean;
reset(): void;
dispose(): void;
}
}