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:14:39 +09:00
parent 3f4f7bd580
commit aae43e9421
58 changed files with 4207 additions and 9714 deletions
+38
View File
@@ -0,0 +1,38 @@
---
description: Analyze repository changes, gather contextual intent from the current codebase status, and generate structurally compliant Git commit messages.
metadata:
github-path: skills/git-commit
github-ref: refs/heads/main
github-repo: https://github.com/orrisroot/agent-skills
github-tree-sha: 661413ef1afaece6071d57b938ef050d4862e4d4
name: git-commit
---
# Git Commit Operator
## 1. Purpose
Analyze repository changes, gather contextual intent from the current codebase status, and generate structurally compliant Git commit messages.
## 2. Goals
* **Atomic Evaluation:** Review diffs to ensure changes represent a single, focused utility. Suggest breaking up large, mixed changes into distinct, atomic commits.
* **Contextual Analysis:** Cross-reference physical changes with the recent discussion history to understand not just *what* changed, but *why* it changed.
* **Executable Output:** Provide ready-to-run terminal commands that apply the correct formatting flags, paragraph separators, and line-wrapping behaviors.
## 3. Operational Workflow for Commits
You must execute the following sequential workflow whenever a commit task is initiated or modifications are targeted for staging:
### Step 1: Diff and Intent Inspection
1. Run `git diff --cached` to evaluate the staged modifications.
2. Cross-reference the structural changes against recent code review comments, commit messages, or the active chat context to confirm the primary engineering objective.
3. If those sources do not clearly explain the change, ask one concise clarifying question before generating the draft.
### Step 2: Message Draft Formulation
1. Construct the message draft strictly using the type classifications and syntax limits specified in `references/conventional_commits.md`.
2. Present the drafted structure to the user for validation, highlighting the assigned commit type and scope.
### Step 3: Terminal Command Delivery
1. Upon user confirmation, output the explicit, single-line terminal command using at most three `-m` flags as specified in the formatting policy.
* **CRITICAL:** Use at most one `-m` flag per structural part (Subject, Body, and Footer).
* **NEVER** use multiple `-m` flags for individual lines or bullet points within the body. Doing so causes Git to insert unwanted blank lines.
* **CRITICAL:** For multi-line body or footer content, you must use a single double-quoted string containing **literal newlines (actual line breaks)** within the command. Do NOT use escape sequences like `\n` (which shell command execution will commit literally) or additional `-m` flags.
Process each step in order and complete the current step before moving to the next.
@@ -0,0 +1,60 @@
# Reference: Conventional Commits Guidelines
When drafting, validating, or executing Git commits, you must strictly adhere to the specification and specific formatting rules defined below.
## Message Format
```plain
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
```
## Allowed Types
* **feat**: A new feature for the user.
* **fix**: A bug fix for the user.
* **docs**: Documentation only changes.
* **style**: Changes that do not affect the meaning of the code (formatting, missing semi-colons, etc).
* **refactor**: A code change that neither fixes a bug nor adds a feature.
* **perf**: A code change that improves performance.
* **test**: Adding missing tests or correcting existing tests.
* **build**: Changes that affect the build system or external dependencies.
* **ci**: Changes to CI configuration files and scripts.
* **chore**: Other changes that do not modify src or test files.
* **revert**: Reverts a previous commit.
## Strict Constraints
1. **Default Message Language**:
* **The commit message must be written exclusively in English** unless the user provides explicit, specific instructions to use another language for that particular commit.
2. **Line Length Limits**:
* The subject line (first line) must not exceed 72 characters, ideally 50 characters or less.
* **Every single line within the commit message body must also be wrapped at 72 characters or less.** If an explanation runs longer, you must manually insert line breaks to keep each line under the 72-character threshold.
3. **Part Separation via Multiple `-m` Flags (At Most Three)**:
* Use at most one `-m` flag per structural part: the subject line, the body, and the footer. Use at most three `-m` flags in total.
* **NEVER split body lines or bullet lists across multiple `-m` flags.** Doing so inserts unwanted blank lines because Git automatically treats each `-m` flag as a separate paragraph.
* **DO NOT use escape sequences like `\n` in double quotes.** Shells like bash will treat `\n` as literal backslash-n characters and commit them as-is.
* **Use literal newlines (actual line breaks)** inside double quotes to write multi-line bodies or footers.
* *Example:*
```bash
git commit -m "feat(auth): add jwt authentication" -m "Validate tokens on every incoming request.
Secure endpoints by rejecting expired credentials.
- Added middleware for token validation
- Removed legacy session handling" -m "BREAKING CHANGE: The old session-based cookie auth is deprecated."
```
4. **Co-authored-by Restriction**:
* **Do not include any `Co-authored-by:` trailers** in the commit message or footer unless the user explicitly requests you to add credit for a co-author.
5. **Case and Punctuation**:
* The description line must be written in lowercase and must not end with a period.
6. **Imperative Mood**:
* Always use the imperative mood in the description (e.g., use "add", "fix", "change" instead of "added", "fixes", "changed").
+26 -13
View File
@@ -1,24 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# dependencies
/node_modules
node_modules
/.pnp
.pnp.js
# production
dist
dist-ssr
# testing
/coverage
# production
/build
# Environment files
*.local
# misc
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.eslintcache
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Misc
.eslintcache
+36
View File
@@ -0,0 +1,36 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 120
},
"linter": {
"enabled": true,
"domains": {
"react": "recommended"
},
"rules": {
"recommended": true
}
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"files": {
"includes": ["**", "!**/dist", "!**/node_modules"]
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "all"
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import js from '@eslint/js';
import { defineConfig, globalIgnores } from 'eslint/config';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
},
},
]);
+3 -2
View File
@@ -2,18 +2,19 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="CelLoc-3D is a database of the 3D arrangement of neocortical cells (gultamatargic/excitatory, GABAergic/inhibitory and/or astrocytes) identified in vivo in layer 2/3 of the primary visual cortex of the mouse by two-photon imaging."
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="manifest" href="/manifest.json" />
<title>CelLoc3D Server</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+3266
View File
File diff suppressed because it is too large Load Diff
+35 -49
View File
@@ -1,57 +1,43 @@
{
"name": "celloc3d",
"version": "2.0.0",
"version": "3.0.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^1.8.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.0",
"@types/node": "^17.0.40",
"@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5",
"@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3",
"@types/three": "^0.141.0",
"axios": "^0.27.2",
"react": "^18.1.0",
"react-app-polyfill": "^3.0.0",
"react-dom": "^18.1.0",
"react-ga4": "^1.4.1",
"react-helmet-async": "^1.3.0",
"react-nl2br": "^1.0.4",
"react-redux": "^8.0.2",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"redux": "^4.2.0",
"three": "^0.141.0",
"typescript": "^4.7.3",
"web-vitals": "^2.1.4"
},
"resolutions": {
"@svgr/webpack": "^6.2.1"
},
"type": "module",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"format": "biome check --write .",
"format:check": "biome check .",
"preview": "vite preview"
},
"eslintConfig": {
"extends": "react-app"
"dependencies": {
"@reduxjs/toolkit": "^2.12.0",
"ky": "^2.0.2",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-ga4": "^3.0.1",
"react-helmet-async": "^3.0.0",
"react-redux": "^9.3.0",
"react-router-dom": "^7.15.1",
"redux": "^5.0.1",
"three": "^0.184.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all",
"ie 11"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version",
"ie 11"
]
"devDependencies": {
"@biomejs/biome": "^2.4.15",
"@eslint/js": "^10.0.1",
"@types/node": "^24.12.4",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@types/react-redux": "^7.1.34",
"@types/three": "^0.184.1",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^10.4.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"typescript": "~6.0.3",
"typescript-eslint": "^8.59.4",
"vite": "^8.0.14"
}
}
+1 -5
View File
@@ -9,11 +9,7 @@
"view": "\/data\/TE0001\/viewer",
"mouseLine": "VGAT-Venus transgenic mouse",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"Glutamatergic\/Excitatory neurons",
"GABAergic\/Inhibitory neurons",
"Astrocytes"
],
"cellType": ["Glutamatergic\/Excitatory neurons", "GABAergic\/Inhibitory neurons", "Astrocytes"],
"postnatalDays": 67,
"imagingVolumeSize": "317 x 317 x 90 um",
"imagingDepth": "from 120 to 210 um",
+1 -5
View File
@@ -9,11 +9,7 @@
"view": "\/data\/TE0002\/viewer",
"mouseLine": "VGAT-Venus transgenic mouse",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"Glutamatergic\/Excitatory neurons",
"GABAergic\/Inhibitory neurons",
"Astrocytes"
],
"cellType": ["Glutamatergic\/Excitatory neurons", "GABAergic\/Inhibitory neurons", "Astrocytes"],
"postnatalDays": 78,
"imagingVolumeSize": "317 x 317 x 90 um",
"imagingDepth": "from 130 to 220 um",
+1 -5
View File
@@ -9,11 +9,7 @@
"view": "\/data\/TE0003\/viewer",
"mouseLine": "VGAT-Venus transgenic mouse",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"Glutamatergic\/Excitatory neurons",
"GABAergic\/Inhibitory neurons",
"Astrocytes"
],
"cellType": ["Glutamatergic\/Excitatory neurons", "GABAergic\/Inhibitory neurons", "Astrocytes"],
"postnatalDays": 70,
"imagingVolumeSize": "317 x 317 x 90 um",
"imagingDepth": "from 120 to 225 um",
+1 -5
View File
@@ -9,11 +9,7 @@
"view": "\/data\/TE0004\/viewer",
"mouseLine": "VGAT-Venus transgenic mouse",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"Glutamatergic\/Excitatory neurons",
"GABAergic\/Inhibitory neurons",
"Astrocytes"
],
"cellType": ["Glutamatergic\/Excitatory neurons", "GABAergic\/Inhibitory neurons", "Astrocytes"],
"postnatalDays": 90,
"imagingVolumeSize": "317 x 317 x 90 um",
"imagingDepth": "from 110 to 200 um",
+1 -5
View File
@@ -9,11 +9,7 @@
"view": "\/data\/TE0005\/viewer",
"mouseLine": "VGAT-Venus transgenic mouse",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"Glutamatergic\/Excitatory neurons",
"GABAergic\/Inhibitory neurons",
"Astrocytes"
],
"cellType": ["Glutamatergic\/Excitatory neurons", "GABAergic\/Inhibitory neurons", "Astrocytes"],
"postnatalDays": 75,
"imagingVolumeSize": "317 x 317 x 90 um",
"imagingDepth": "from 125 to 230 um",
+1 -6
View File
@@ -9,12 +9,7 @@
"view": "\/data\/TE0006\/viewer",
"mouseLine": "PV\/myrGFP-LDLRct transgenic mouse",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"PV-positive neurons",
"SOM-positive neurons",
"Putative excitatory neurons",
"Astrocytes"
],
"cellType": ["PV-positive neurons", "SOM-positive neurons", "Putative excitatory neurons", "Astrocytes"],
"postnatalDays": 77,
"imagingVolumeSize": "317 x 317 x 105 um",
"imagingDepth": "from 110 to 215 um",
+1 -6
View File
@@ -9,12 +9,7 @@
"view": "\/data\/TE0007\/viewer",
"mouseLine": "PV/myrGFP-LDLRct transgenic mouse",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"PV-positive neurons",
"SOM-positive neurons",
"Putative excitatory neurons",
"Astrocytes"
],
"cellType": ["PV-positive neurons", "SOM-positive neurons", "Putative excitatory neurons", "Astrocytes"],
"postnatalDays": 73,
"imagingVolumeSize": "317 x 317 x 105 um",
"imagingDepth": "from 110 to 215 um",
+1 -6
View File
@@ -9,12 +9,7 @@
"view": "\/data\/TE0008\/viewer",
"mouseLine": "PV/myrGFP-LDLRct transgenic mouse",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"PV-positive neurons",
"SOM-positive neurons",
"Putative excitatory neurons",
"Astrocytes"
],
"cellType": ["PV-positive neurons", "SOM-positive neurons", "Putative excitatory neurons", "Astrocytes"],
"postnatalDays": 70,
"imagingVolumeSize": "317 x 317 x 105 um",
"imagingDepth": "from 110 to 215 um",
+1 -3
View File
@@ -9,9 +9,7 @@
"view": "\/data\/TE0009\/viewer",
"mouseLine": "Dlx5\/6-GCaMP3 mouse (Crossing Dlx5\/6-Flpe and R26-CAG-FRT-GCaMP3 transgenic mice)",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"GABAergic\/inhibitory neurons"
],
"cellType": ["GABAergic\/inhibitory neurons"],
"postnatalDays": 53,
"imagingVolumeSize": "317 x 317 x 110 um",
"imagingDepth": "from 100 to 210 um",
+1 -3
View File
@@ -9,9 +9,7 @@
"view": "\/data\/TE0010\/viewer",
"mouseLine": "Dlx5\/6-GCaMP3 mouse (Crossing Dlx5\/6-Flpe and R26-CAG-FRT-GCaMP3 transgenic mice)",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"GABAergic/inhibitory neurons"
],
"cellType": ["GABAergic/inhibitory neurons"],
"postnatalDays": 100,
"imagingVolumeSize": "512 x 512 x 110 um",
"imagingDepth": "from 90 to 200 um",
+1 -3
View File
@@ -9,9 +9,7 @@
"view": "\/data\/TE0011\/viewer",
"mouseLine": "Dlx5\/6-GCaMP3 mouse (Crossing Dlx5\/6-Flpe and R26-CAG-FRT-GCaMP3 transgenic mice)",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"GABAergic\/inhibitory neurons"
],
"cellType": ["GABAergic\/inhibitory neurons"],
"postnatalDays": 70,
"imagingVolumeSize": "512 x 512 x 110 um",
"imagingDepth": "from 90 to 200 um",
+1 -3
View File
@@ -9,9 +9,7 @@
"view": "\/data\/TE0012\/viewer",
"mouseLine": "Dlx5\/6-GCaMP3 mouse (Crossing Dlx5\/6-Flpe and R26-CAG-FRT-GCaMP3 transgenic mice)",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"GABAergic\/inhibitory neurons"
],
"cellType": ["GABAergic\/inhibitory neurons"],
"postnatalDays": 78,
"imagingVolumeSize": "512 x 512 x 110 um",
"imagingDepth": "from 90 to 200 um",
+1 -3
View File
@@ -9,9 +9,7 @@
"view": "\/data\/TE0013\/viewer",
"mouseLine": "Dlx5\/6-GCaMP3 mouse (Crossing Dlx5\/6-Flpe and R26-CAG-FRT-GCaMP3 transgenic mice)",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"GABAergic\/inhibitory neurons"
],
"cellType": ["GABAergic\/inhibitory neurons"],
"postnatalDays": 69,
"imagingVolumeSize": "512 x 512 x 110 um",
"imagingDepth": "from 90 to 200 um",
+1 -3
View File
@@ -9,9 +9,7 @@
"view": "\/data\/TE0014\/viewer",
"mouseLine": "Dlx5\/6-GCaMP3 mouse (Crossing Dlx5\/6-Flpe and R26-CAG-FRT-GCaMP3 transgenic mice)",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"GABAergic\/inhibitory neurons"
],
"cellType": ["GABAergic\/inhibitory neurons"],
"postnatalDays": 44,
"imagingVolumeSize": "317 x 317 x 110 um",
"imagingDepth": "from 90 to 200 um",
+1 -3
View File
@@ -9,9 +9,7 @@
"view": "\/data\/TE0015\/viewer",
"mouseLine": "Dlx5\/6-GCaMP3 mouse (Crossing Dlx5\/6-Flpe and R26-CAG-FRT-GCaMP3 transgenic mice)",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"GABAergic\/inhibitory neurons"
],
"cellType": ["GABAergic\/inhibitory neurons"],
"postnatalDays": 92,
"imagingVolumeSize": "512 x 512 x 110 um",
"imagingDepth": "from 90 to 200 um",
+1 -3
View File
@@ -9,9 +9,7 @@
"view": "\/data\/TE0016\/viewer",
"mouseLine": "Dlx5\/6-GCaMP3 mouse (Crossing Dlx5\/6-Flpe and R26-CAG-FRT-GCaMP3 transgenic mice)",
"imagingArea": "Layer 2\/3, Primary Visual Cortex",
"cellType": [
"GABAergic\/inhibitory neurons"
],
"cellType": ["GABAergic\/inhibitory neurons"],
"postnatalDays": 47,
"imagingVolumeSize": "512 x 512 x 110 um",
"imagingDepth": "from 90 to 200 um",
+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;
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client", "node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Project-specific settings */
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"strict": true,
/* Linting */
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+2 -24
View File
@@ -1,26 +1,4 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
+25
View File
@@ -0,0 +1,25 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [react()],
build: {
target: 'es2023',
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react') || id.includes('react-dom')) {
return 'vendor';
}
if (id.includes('three')) {
return 'three';
}
return 'vendor-lib';
}
},
},
},
},
});
-8924
View File
File diff suppressed because it is too large Load Diff