7 Commits
Author SHA1 Message Date
orrisroot 6093941c21 feat: show the archive notice on the top page
The other archived sites carry the archive notice on their top page,
while www.neuroinf.jp showed it only on inner pages. Put it where
visitors land, just below the menu and above the site banner.

- add an ArchiveNotice bar with a red accent line and icon on a light
  red background
- share the notice text with NoticeSiteHasBeenArchived so the two
  cannot drift apart
2026-09-15 20:17:57 +09:00
orrisroot 6737d11010 fix: keep text dark on form controls given a white background
sanitize.css forms.css makes form controls inherit the text color, so
the rules that put them back on white left white text wherever the
surrounding panel sets it, as in the nimgsearch search panel and the
open.neuroinf.jp advanced search.

- set the text to black next to each white control background,
  matching how the browser drew the controls before sanitize.css
2026-09-15 20:02:12 +09:00
orrisroot b3ec4709de fix: draw the index tree lines at the right depth
The index tree has a single root, so its first indent unit is always
marked as an end and every line was shifted one column to the right,
doubling the vertical line under the root. Hide that unit and draw the
node's own branch (├ or └) after the indent from the leaf-last class,
as nimg.neuroinf.jp already does.
2026-09-15 18:27:19 +09:00
orrisroot a795d16f0f docs: tighten the comment on the static-contents path decoding 2026-09-14 18:33:37 +09:00
orrisroot f6468a6b48 fix: serve data files with reserved characters in dev and preview
sirv decodes request paths with decodeURI, which leaves %26 (&) and
%23 (#) encoded, so files such as CFP&Reg.png were not found by the
dev and preview servers. Hand sirv the fully decoded path, the way
Apache resolves it in production.
2026-09-14 18:24:40 +09:00
orrisroot 3f63587756 feat: migrate from Create React App to Vite
Rebuild the site on the stack the other converted sites share: Vite 8,
React 19, TypeScript 7, react-router 8, Biome and sanitize.css, with the
site data served from static-contents/ instead of the bundle.

- base the XooNIps pages on the other converted sites, keeping the
  XooNIps 4 item types and the original module addresses
- port the pico, bulletin and credits modules to react-router 8 and ky
- read the RSS feeds with DOMParser instead of rss-parser
- load the index tree from modules/xoonips/ at start up
- fix the download.php redirect target
- restore the theme's list, table and image layout over sanitize.css
  and keep form controls on white
- track page views with GA4
2026-09-14 18:17:05 +09:00
orrisroot bb6e1f2871 chore: move XOOPS conversion tools to xoops-static-exporter
The dump scripts are site-specific but belong with the other sites'
tools rather than inside the front-end repository. They now live in
xoops-static-exporter under sites/www.neuroinf.jp/.
2026-09-14 18:17:04 +09:00
155 changed files with 6918 additions and 20389 deletions
+21 -24
View File
@@ -1,28 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.eslintcache
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# database contents
/dl-limit-items.csv
/public/modules
node_modules
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+6
View File
@@ -0,0 +1,6 @@
// Keep @types/node on the 24.x line to match the deployment target. Its
// "latest" dist-tag points at the 22.x line, so the default target would
// report no updates at all instead of offering 24.x patches.
export default {
target: (name) => (name === '@types/node' ? 'minor' : 'latest'),
};
+97 -25
View File
@@ -1,44 +1,116 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
# INCF Japan Node
## Available Scripts
Static front-end for the archived [INCF Japan Node](https://www.neuroinf.jp/)
portal, built with [Vite](https://vite.dev/), React and TypeScript.
In the project directory, you can run:
## Requirements
### `yarn start`
- Node.js 20.19+ / 22.12+ (Vite 8)
- npm
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
## Setup
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
```sh
npm install
```
### `yarn test`
## Site data
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
The pages are rendered from JSON dumped out of the original XOOPS installation
by [xoops-static-exporter](https://git.ni.riken.jp/niu/xoops-static-exporter)
(`sites/www.neuroinf.jp/`). All of it lives in `static-contents/`, which sits
next to the repository rather than inside it:
### `yarn build`
```
www.neuroinf.jp/
├── www.neuroinf.jp/ this repository
└── static-contents/
├── modules/ one directory per source XOOPS module
│ ├── aboutus/ ... pico: index.json, <content id>.json (ten modules)
│ ├── credits/ index.json, <page id>.json
│ ├── fmanager/ xelFinder files: index.php/view/<id>/, cache/
│ ├── news/ bulletin: index.json
│ └── xoonips/ tree.json, items.json, file/<id>/
├── flags/ static files, served as they are
├── report/
├── schema/
└── workshop/
```
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The `RewriteBase` in every `modules/xoonips/file/<id>/.htaccess` and
`modules/fmanager/index.php/*/<id>/.htaccess` carries the absolute path of that
directory and has to move with the layout.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
The dev and preview servers mount that directory at the site root. It is kept
out of `dist/`, and it is not tracked in git.
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
The XooNIps pages use the XooNIps 4 item types of this site (journal, article
and general data) and keep the addresses of the original module
(`/modules/xoonips/detail.php?item_id=N`, `list.php?index_id=N`,
`search.php?op=...`). The pico pages are served under their module directory
(`/aboutus/incf.html`), and the AINI, NBNI and hackathon pages also under their
event paths (`/aini2018/`, `/hackathon2018/...`).
### `yarn eject`
The top page and the platform page show the news of INCF and the other
platforms, read from their RSS feeds in the browser through
`https://archive.ni.brain.riken.jp/cors/`.
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
## Scripts
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
| Command | Description |
| --- | --- |
| `npm run dev` | Start the dev server on http://localhost:5173 |
| `npm run build` | Type-check and build into `dist/` |
| `npm run preview` | Serve the production build on http://localhost:4173 |
| `npm run lint` | Lint with [Biome](https://biomejs.dev/) |
| `npm run format` | Format with Biome |
| `npm run check` | Lint, format and organize imports, applying the safe fixes |
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
`@types/node` is pinned to the major that matches the Node runtime in use (24);
`.ncurc.js` keeps `npm-check-updates` from bumping it past that.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
There is currently no test suite.
## Learn More
## Styles
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
The base stylesheet is `sanitize.css` with its `forms` and `typography` sheets,
with its `box-sizing: border-box` kept. The site theme is
`src/custom/assets/style.css`. Where a sanitize.css default does not suit the
design, the rule for that place sets its own value rather than overriding the
element everywhere.
To learn React, check out the [React documentation](https://reactjs.org/).
## Deployment
Build locally and copy the result up; the server does not need a toolchain.
```sh
npm ci
npm run check
npm run build
../deploy.sh
```
`deploy.sh`, next to `static-contents/`, sends the data to
`archive:/data/www/www.neuroinf.jp/static-contents/`, then `dist/` to
`.../html/`, and finally links every top-level entry of `static-contents/` into
the document root over ssh (`ln -sfn ../static-contents/* .`). The data has to
go first: the document root's `rsync --delete` removes those links, and they
are only put back at the end.
Apache needs `Options FollowSymLinks` and `AllowOverride All` on both
directories. Use symlinks rather than `Alias`: an `Alias` maps the URL outside
the document root, so `.htaccess` no longer applies and the SPA fallback and
the redirects stop working.
### Redirects
`.htaccess` redirects `/modules/xoonips/download.php?file_id=N` to
`/modules/xoonips/file/N` with a 301.
XOOPS-style addresses (`/modules/<pico module>/...`, `/modules/news/...`,
`/index.php`) are redirected inside the app.
## Google Analytics
`GOOGLE_ANALYTICS_TRACKING_ID` in `src/config.ts` carries the GA4 measurement
ID. Emptying it disables tracking. Only production builds send page views.
+39
View File
@@ -0,0 +1,39 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.13/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!**/dist", "!src/common/assets", "!src/custom/assets", "!src/d3forum/assets"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 4,
"lineWidth": 120
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double",
"semicolons": "always",
"trailingCommas": "es5",
"arrowParentheses": "always"
}
},
"linter": {
"enabled": true,
"rules": {
"preset": "recommended"
}
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
-318
View File
@@ -1,318 +0,0 @@
<?php
require_once __DIR__.'/config.inc.php';
define('PROTECTOR_SKIP_DOS_CHECK', 1);
define('PROTECTOR_SKIP_FILESCHECKER', 1);
// disable query logger
define('XOOPS_LOGGER_ADDQUERY_DISABLED', true);
// ----
if (!file_exists($mainfile)) {
echo 'ERROR : mainfile.php not found'.PHP_EOL;
exit(1);
}
$xoops_path = '';
$xoops_url = '';
foreach (file($mainfile) as $line) {
if (preg_match('/^\s*define\s*\(\s*[\'"]XOOPS_ROOT_PATH[\'"]\s*,\s*[\'"](.+)[\'"]\)\s*;\s*$/', $line, $matches)) {
$xoops_path = $matches[1];
}
if (preg_match('/^\s*define\s*\(\s*[\'"]XOOPS_URL[\'"]\s*,\s*[\'"](.+)[\'"]\)\s*;\s*$/', $line, $matches)) {
$xoops_url = $matches[1];
}
}
if (isset($method) && 'POST' == strtoupper($method)) {
$method = 'POST';
} else {
$method = 'GET';
}
$_SERVER['HTTP_USER_AGENT'] = 'php-cli';
$_SERVER['REQUEST_METHOD'] = $method;
$_ENV['HTTP_REFERER'] = $xoops_url.'/index.php';
$_SERVER['QUERY_STRING'] = '/index.php';
$_SERVER['REMOTE_ADDR'] = '192.168.0.1';
require_once $xoops_path.'/mainfile.php';
if (defined('XOOPS_DB_PROXY') && XOOPS_DB_PROXY == 1 && 'POST' == $method) {
die('Error: not accept POST request'."\n");
}
error_reporting(E_ALL);
// remove ob fileters
while (ob_get_level()) {
ob_end_clean();
}
define('MYDUMPTOOL_OUTPUTDIR', __DIR__.'/data');
MyDumpTool::makeDirectory('');
MyDumpTool::makeDirectory('src');
MyDumpTool::makeDirectory('public');
class MyDumpTool
{
public static function decode(&$data)
{
foreach ($data as $k => $v) {
if (is_string($v)) {
$v = html_entity_decode($v, ENT_QUOTES | ENT_HTML5);
$v = mb_decode_numericentity($v, [0x0, 0x10000, 0, 0xfffff], 'UTF-8');
$data[$k] = \Normalizer::normalize(trim($v), \Normalizer::FORM_C);
} elseif (null === $v) {
$data[$k] = '';
}
}
}
public static function convertToInt(&$data, $keys, $zerofill = false)
{
foreach ($keys as $key) {
if (isset($data[$key])) {
$data[$key] = '' === $data[$key] ? ($zerofill ? 0 : null) : (int) $data[$key];
}
}
}
public static function dropColumn(&$data, $keys)
{
foreach ($keys as $key) {
if (isset($data[$key])) {
unset($data[$key]);
}
}
}
public static function fileCopy($from, $to)
{
system('rsync -aq '.escapeshellarg($from).' '.escapeshellarg($to), $ret);
return 0 === $ret;
}
public static function fixHtml($text)
{
static $config = [
//'clean' => true,
'hide-comments' => true,
'indent' => true,
'output-xhtml' => true,
'preserve-entities' => true,
'show-body-only' => true,
'drop-proprietary-attributes' => true,
'logical-emphasis' => true,
'wrap' => 0,
'input-encoding' => 'utf8',
'output-encoding' => 'utf8',
'output-bom' => false,
];
$text = tidy_repair_string($text, $config);
$text = preg_replace('/<table([^>]*)>((?:\s*<caption[^>]*>.*<\/caption>)?\s*)<tr([^>]*)>/Us', '<table\1>\2<tbody><tr\3>', $text);
$text = preg_replace('/<\/tr>(\s*)<\/table>/s', '</tr></tbody>\1</table>', $text);
$text = tidy_repair_string($text, $config);
return $text;
}
public static function tableExists($name)
{
global $xoopsDB;
$prefix = $xoopsDB->prefix();
$sql = <<< SQL
SELECT 1
FROM `${prefix}_${name}`
LIMIT 1
SQL;
if (!($res = $xoopsDB->query($sql))) {
return false;
}
$xoopsDB->freeRecordSet($res);
return true;
}
public static function makeDirectory($dirname)
{
$path = MYDUMPTOOL_OUTPUTDIR.('' !== $dirname ? '/'.$dirname : '');
if (!is_dir($path)) {
if (!@mkdir($path, 0755, true)) {
exit('Failed to create directory: '.$path.PHP_EOL);
}
}
}
public static function saveJson($fname, $data)
{
$path = MYDUMPTOOL_OUTPUTDIR.'/'.$fname;
$data = json_encode($data, JSON_UNESCAPED_UNICODE);
if (false === $data) {
echo json_last_error_msg().PHP_EOL;
exit('Failed to encode json data: '.$path.PHP_EOL);
}
if (false === file_put_contents($path, $data)) {
exit('Failed to save json file: '.$path.PHP_EOL);
}
}
public static function fileDownload($url, $fpath)
{
$fp = fopen($fpath, 'w');
if (false === $fp) {
exit('Failed to open file: '.$fpath.PHP_EOL);
}
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_AUTOREFERER, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FAILONERROR, true);
curl_setopt($curl, CURLOPT_FILE, $fp);
curl_setopt($curl, CURLOPT_TIMEOUT, 10);
$ret = curl_exec($curl);
fclose($fp);
if (false === $ret) {
unlink($fpath);
echo 'Failed to download file, unexpected error: '.$url.PHP_EOL;
return false;
}
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if (200 !== $code) {
unlink($fpath);
echo 'Failed to download file, invalid status code('.$code.'): '.$url.PHP_EOL;
return false;
}
curl_close($curl);
return true;
}
public static function getMimeType($fpath)
{
$finfo = finfo_open(FILEINFO_MIME);
$mime = finfo_file($finfo, $fpath);
finfo_close($finfo);
$mime = preg_replace('/(;.*)$/', '', $mime);
return $mime;
}
public static function getModule($dirname)
{
$moduleHandler = xoops_gethandler('module');
$moduleObj = $moduleHandler->getByDirname($dirname);
return [
'id' => (int) $moduleObj->get('mid'),
'name' => $moduleObj->get('name'),
'dirname' => $moduleObj->get('dirname'),
'version' => (int) $moduleObj->get('version'),
'lastupdate' => (int) $moduleObj->get('last_update'),
];
}
public static function canModuleRead($dirname, $uid)
{
$moduleHandler = xoops_gethandler('module');
$moduleObj = $moduleHandler->getByDirname($dirname);
$moduleId = (int) $moduleObj->get('mid');
$memberHandler = xoops_gethandler('member');
$groupIds = 0 === $uid ? [XOOPS_GROUP_ANONYMOUS] : $memberHandler->getGroupsByUser($uid);
$groupPermHandler = xoops_gethandler('groupperm');
$ret = $groupPermHandler->checkRight('module_read', $moduleId, $groupIds);
return $ret;
}
public static function mlang($s, $url, $lang)
{
if (!defined('CUBE_UTILS_ML_LANGS')) {
exit('Error: cubeUtils is not loaded'.PHP_EOL);
}
$mLanguage = $lang;
$mLanguages = explode(',', CUBE_UTILS_ML_LANGS);
$mLanguageNames = explode(',', CUBE_UTILS_ML_LANGNAMES);
// escape brackets inside of <input type="text" value="...">
$s = preg_replace_callback('/(\<input)([^>]*)(\>)/isU', 'self::mlang_escapeBracketTextBox', $s);
// escape brackets inside of <textarea></textarea>
$s = preg_replace_callback('/(\<textarea[^>]*\>)(.*)(<\/textarea\>)/isU', 'self::mlang_escapeBracket', $s);
// multilanguage image tag
$langimages = explode(',', CUBE_UTILS_ML_LANGIMAGES);
$langnames = explode(',', CUBE_UTILS_ML_LANGNAMES);
@list($url, $query) = explode('?', $url);
if (empty($query)) {
$link_base = '?'.CUBE_UTILS_ML_PARAM_NAME.'=';
} elseif (false === ($pos = strpos($query, CUBE_UTILS_ML_PARAM_NAME.'='))) {
$link_base = '?'.htmlspecialchars($query, ENT_QUOTES).'&amp;'.CUBE_UTILS_ML_PARAM_NAME.'=';
} elseif ($pos < 2) {
$link_base = '?'.CUBE_UTILS_ML_PARAM_NAME.'=';
} else {
$link_base = '?'.htmlspecialchars(substr($query, 0, $pos - 1), ENT_QUOTES).'&amp;'.CUBE_UTILS_ML_PARAM_NAME.'=';
}
$link_base = $url.$link_base;
$langimage_html = '';
foreach ($mLanguages as $l => $lang) {
$langimage_html .= '<a rel="nofollow" href="'.$link_base.$lang.'"><img src="'.XOOPS_URL.'/'.$langimages[$l].'" alt="flag" title="'.$langnames[$l].'" /></a>';
}
$s = preg_replace('/\['.CUBE_UTILS_ML_IMAGETAG.'\]/', $langimage_html, $s);
$s = preg_replace('/\['.CUBE_UTILS_ML_URLTAG.':([^\]]*?)\]/', $link_base.'$1', $s);
// simple pattern to strip selected lang_tags
$s = preg_replace('/\[(\/)?([^\]]+\|)?'.preg_quote($mLanguage).'(\|[^\]]+)?\](\<br \/\>)?/i', '', $s);
// eliminate description between the other language tags.
foreach ($mLanguages as $lang) {
if ($mLanguage == $lang) {
continue;
}
$s = preg_replace_callback('/\[(?:^\/[^\]]+\|)?'.preg_quote($lang).'(?:\|[^\]]+)?\].*\[\/(?:^\/[^\]]+\|)?'.preg_quote($lang).'(?:\|[^\]]+)?(?:\]\<br \/\>|\])/isU', 'self::mlang_checkNeverCross', $s);
}
// escape brackets inside of <input type="text" value="...">
$s = preg_replace_callback('/(\<input)([^>]*)(\>)/isU', 'self::mlang_unEscapeBracketTextBox', $s);
// escape brackets inside of <textarea></textarea>
$s = preg_replace_callback('/(\<textarea[^>]*\>)(.*)(<\/textarea\>)/isU', 'self::mlang_unEscapeBracket', $s);
return $s;
}
public static function mlang_escapeBracketTextBox($matches)
{
if (preg_match('/type=["\']?(?=text|hidden)["\']?/i', $matches[2])) {
return $matches[1].str_replace('[', '__ml[ml__', $matches[2]).$matches[3];
} else {
return $matches[1].$matches[2].$matches[3];
}
}
public static function mlang_escapeBracket($matches)
{
return $matches[1].str_replace('[', '__ml[ml__', $matches[2]).$matches[3];
}
public static function mlang_unEscapeBracketTextBox($matches)
{
if (preg_match('/type=["\']?(?=text|hidden)["\']?/i', $matches[2])) {
return $matches[1].str_replace('__ml[ml__', '[', $matches[2]).$matches[3];
} else {
return $matches[1].$matches[2].$matches[3];
}
}
public static function mlang_unEscapeBracket($matches)
{
return $matches[1].str_replace('__ml[ml__', '[', $matches[2]).$matches[3];
}
public static function mlang_checkNeverCross($matches)
{
return preg_match(CUBE_UTILS_ML_NEVERCROSSREGEX, $matches[0]) ? $matches[0] : '';
}
}
-7
View File
@@ -1,7 +0,0 @@
<?php
$mainfile = '/data/www/www.neuroinf.jp/html/mainfile.php';
$XOOPS_MODULE_PICO = ['aboutus', 'aini', 'hackathon', 'joinus', 'link', 'nbni', 'program', 'publication', 'resource', 'tutorial'];
$XOOPS_MODULE_BULLETIN = ['news'];
$XOOPS_MODULE_XELFINDER = ['fmanager'];
-8
View File
@@ -1,8 +0,0 @@
#!/bin/sh
cd $(dirname $0)
rsync -av --delete ../static/flags/ data/public/flags/
rsync -av --delete ../static/report/ data/public/report/
rsync -av --delete ../static/schema/ data/public/schema/
rsync -av --delete ../static/workshop/ data/public/workshop/
-98
View File
@@ -1,98 +0,0 @@
<?php
require_once __DIR__.'/common.inc.php';
require_once __DIR__.'/extra.inc.php';
if (!isset($XOOPS_MODULE_BULLETIN) || empty($XOOPS_MODULE_BULLETIN)) {
exit('Not configured for bulletin module'.PHP_EOL);
}
$mytrustdirname = 'bulletin';
$mytrustdirpath = XOOPS_TRUST_PATH.'/modules/'.$mytrustdirname;
$langmanpath = XOOPS_TRUST_PATH.'/libs/altsys/class/D3LanguageManager.class.php';
if (!file_exists($langmanpath)) {
die('install the latest altsys');
}
require_once $langmanpath;
$langman = &D3LanguageManager::getInstance();
$root = XCube_Root::getSingleton();
$ts = MyTextSanitizer::sGetInstance();
$configHandler = xoops_gethandler('config');
foreach ($XOOPS_MODULE_BULLETIN as $mydirname) {
$data = [];
// get module info
$module = MyDumpTool::getModule($mydirname);
$moduleConfig = $configHandler->getConfigsByDirname($mydirname);
$data['module'] = [];
foreach (['name', 'dirname'] as $key) {
$data['module'][$key] = $module[$key];
}
foreach (['storyhome', 'displaynav', 'disp_list_of_cat', 'stories_of_cat', 'use_pankuzu'] as $key) {
$data['module'][$key] = intval($moduleConfig[$key]);
}
$langman->read('main.php', $mydirname, $mytrustdirname);
$mydirpath = XOOPS_ROOT_PATH.'/modules/'.$mydirname;
$mydirurl = XOOPS_URL.'/modules/'.$mydirname;
include "$mytrustdirpath/main/header.php";
$gperm = BulletinGP::getInstance($mydirname);
$can_read_topic_ids = $gperm->makeOnTopics('can_read');
// get topics
$table = $xoopsDB->prefix($mydirname.'_topics');
$topic_ids_sql = implode(', ', $can_read_topic_ids);
$sql = <<< SQL
SELECT
`topic_id`, `topic_pid`, `topic_title`
FROM `${table}`
WHERE `topic_id` IN (${topic_ids_sql})
ORDER BY `topic_id` ASC
SQL;
if (!($res = $xoopsDB->query($sql))) {
var_dump($xoopsDB);
exit();
}
$data['topics'] = [];
while ($row = $xoopsDB->fetchArray($res)) {
MyDumpTool::decode($row);
MyDumpTool::convertToInt($row, ['topic_id', 'topic_pid']);
$data['topics'][] = [
'id' => $row['topic_id'],
'pid' => $row['topic_pid'],
'title' => $row['topic_title'],
];
}
$xoopsDB->freeRecordSet($res);
// get all stories
$articles = Bulletin::getAllPublished($mydirname, 0, 0, 0, 1, true, true, true);
foreach ($articles as $article) {
$article->unifyHomeTextAndBodyText();
$story = [];
$story['id'] = intval($article->getVar('storyid', 'n'));
$url = $mydirurl.'/index.php?page=article&storyid='.$story['id'];
$story['posttime'] = formatTimestamp($article->getVar('published'), $bulletin_date_format);
$story['topic_id'] = intval($article->getVar('topicid', 'n'));
$story['title'] = $article->getVar('title', 'n');
$text = $article->getVar('text', 'n');
$doHtml = $article->getVar('html', 'n');
$doSmiley = $article->getVar('smiley', 'n');
$doBr = $article->getVar('br', 'n');
$doXcode = $article->getVar('xcode', 'n');
$story['text'] = [
'en' => normalizeHtml($ts->displayTarea(MyDumpTool::mlang($text, $url, 'en'), $doHtml, $doSmiley, $doXcode, 0, $doBr), $url),
'ja' => normalizeHtml($ts->displayTarea(MyDumpTool::mlang($text, $url, 'ja'), $doHtml, $doSmiley, $doXcode, 0, $doBr), $url),
];
$type = $article->getVar('type', 'n');
if (1 != $type) {
echo 'Error: other type is unsupported'.PHP_EOL;
exit();
}
if (!empty($article->getRelated())) {
echo 'Error: related article feature is unsupported'.PHP_EOL;
exit();
}
$data['stories'][] = $story;
}
MyDumpTool::makeDirectory('public/modules/'.$mydirname);
MyDumpTool::saveJson('public/modules/'.$mydirname.'/index.json', $data);
}
-156
View File
@@ -1,156 +0,0 @@
<?php
require_once __DIR__.'/common.inc.php';
require_once __DIR__.'/extra.inc.php';
$prefix = $xoopsDB->prefix();
$table = 'nipfcredits_organization';
if (!MyDumpTool::tableExists($table)) {
exit('credits module not found'.PHP_EOL);
}
MyDumpTool::makeDirectory('public/modules/credits');
// module
$module = MyDumpTool::getModule('credits');
// organization
$organization = [];
$sql = <<< SQL
SELECT
`o`.*
FROM `${prefix}_nipfcredits_organization` AS `o`
SQL;
if (!($res = $xoopsDB->query($sql))) {
var_dump($xoopsDB);
exit();
}
while ($row = $xoopsDB->fetchArray($res)) {
MyDumpTool::decode($row);
$organization = $row;
}
$xoopsDB->freeRecordSet($res);
$organization['com_email'] = str_replace('@', ' (at) ', $organization['com_email']);
$organization['com_url'] = XOOPS_URL;
$organization['institute'] = '[en]Neuroinformatics Unit, RIKEN Center for Brain Science[/en][ja]理化学研究所 脳神経科学研究センター 神経情報基盤ユニット[/ja]';
$organization['url'] = 'https://www.ni.riken.jp/';
$organization['email'] = 'office (at) ni.riken.jp';
$organization['address'] = '[en]Hirosawa 2-1, Wako, Saitama, 351-0198 Japan[/en][ja]埼玉県和光市広沢 2-1[/ja]';
$organization['tel'] = '+81 48 (462) 1111';
// member
$member = [];
$sql = <<< SQL
SELECT
`m`.*
FROM `${prefix}_nipfcredits_member` AS `m`
ORDER BY `m`.`weight`
SQL;
if (!($res = $xoopsDB->query($sql))) {
var_dump($xoopsDB);
exit();
}
while ($row = $xoopsDB->fetchArray($res)) {
MyDumpTool::decode($row);
$member[] = $row;
}
$xoopsDB->freeRecordSet($res);
$opage = getOrganizationPage($organization, $member, $module);
MyDumpTool::saveJson('public/modules/credits/0.json', $opage);
// pages
$pages = [];
$sql = <<< SQL
SELECT
`p`.*
FROM `${prefix}_nipfcredits_page` AS `p`
ORDER BY `p`.`weight`
SQL;
if (!($res = $xoopsDB->query($sql))) {
var_dump($xoopsDB);
exit();
}
while ($row = $xoopsDB->fetchArray($res)) {
MyDumpTool::decode($row);
myDumpTool::convertToInt($row, ['pid', 'weight', 'lastupdate']);
$content = replaceOrganization($row['content'], $organization);
$content = normalizeHtml($content, '/modules/credits/index.php');
$content = preg_replace('/<a (?:[^>]*)href="(?:'.preg_quote(XOOPS_URL, '/').')?\/modules\/xoonips\/(?:register|edit|user)[^"]*"(?:[^>]*)>(.*)<\/a>/Usi', '<s>\1</s>(closed)', $content);
$row['content'] = $content;
MyDumpTool::dropColumn($row, ['weight']);
$pages[] = [
'id' => $row['pid'],
'title' => $row['title'],
];
MyDumpTool::saveJson('public/modules/credits/'.$row['pid'].'.json', $row);
}
$xoopsDB->freeRecordSet($res);
/*
$pages[] = [
'id' => $opage['id'],
'title' => $opage['title'],
];
*/
$data = [
'name' => $module['name'],
'pages' => $pages,
];
MyDumpTool::saveJson('public/modules/credits/index.json', $data);
function replaceOrganization($text, $org)
{
$map = [
'{COMMITTEE_NAME}' => htmlspecialchars($org['committee']),
//'{COMMITTEE_EMAIL}' => '<s>'.htmlspecialchars($org['com_email'], ENT_QUOTES).'</s>(committee email has been closed)',
'{COMMITTEE_EMAIL}' => htmlspecialchars($org['com_email'], ENT_QUOTES),
'{INSTITUTE_NAME}' => htmlspecialchars($org['institute'], ENT_QUOTES),
'{INSTITUTE_URL}' => htmlspecialchars($org['url'], ENT_QUOTES),
'{INSTITUTE_EMAIL}' => htmlspecialchars($org['email'], ENT_QUOTES),
'{INSTITUTE_ADDRESS}' => htmlspecialchars($org['address'], ENT_QUOTES),
'{INSTITUTE_TEL}' => htmlspecialchars($org['tel'], ENT_QUOTES),
'{PLATFORM_URL}' => XOOPS_URL,
];
return str_replace(array_keys($map), $map, $text);
}
function getOrganizationPage($org, $member, $module)
{
$members = [];
if (!empty($member)) {
$members[] = ' <li>[en]Members[/en][ja]委員一覧[/ja]';
$members[] = ' <ul>';
foreach ($member as $m) {
$members[] = ' <li>'.htmlspecialchars($m['name'].' : '.$m['division'], ENT_QUOTES).'</li>';
}
$members[] = ' </ul>';
$members[] = ' </li>';
}
$members = implode("\n", $members);
$committee = htmlspecialchars($org['committee'], ENT_QUOTES);
$institute = htmlspecialchars($org['institute'], ENT_QUOTES);
$address = htmlspecialchars($org['address'], ENT_QUOTES);
$url = htmlspecialchars($org['url'], ENT_QUOTES);
$email = htmlspecialchars($org['com_email'], ENT_QUOTES);
$html = <<< HTML
<h4>${committee}</h4>
<ul>
<li>E-Mail: <strike>${email}</strike></li>
${members}
</ul>
<h4>${institute}</h4>
<ul>
<li>[en]Address[/en][ja]所在地[/ja]: ${address}</li>
<li>URL: <a href="${url}" target="_blank" rel=”noopener noreferrer”>${url}</a></li>
</ul>
HTML;
return [
'id' => 0,
'title' => '[en]About Us[/en][ja]運用母体[/ja]',
'content' => $html,
'lastupdate' => 0,
];
}
-102
View File
@@ -1,102 +0,0 @@
<?php
require_once __DIR__.'/common.inc.php';
require_once __DIR__.'/extra.inc.php';
if (!isset($XOOPS_MODULE_PICO) || empty($XOOPS_MODULE_PICO)) {
exit('Not configured for pico module'.PHP_EOL);
}
error_reporting(0);
require_once XOOPS_TRUST_PATH.'/modules/pico/include/main_functions.php';
require_once XOOPS_TRUST_PATH.'/modules/pico/include/common_functions.php';
require_once XOOPS_TRUST_PATH.'/modules/pico/class/pico.textsanitizer.php';
require_once XOOPS_TRUST_PATH.'/modules/pico/class/PicoUriMapper.class.php';
require_once XOOPS_TRUST_PATH.'/modules/pico/class/PicoPermission.class.php';
require_once XOOPS_TRUST_PATH.'/modules/pico/class/PicoModelCategory.class.php';
require_once XOOPS_TRUST_PATH.'/modules/pico/class/PicoModelContent.class.php';
require_once XOOPS_TRUST_PATH.'/libs/altsys/class/AltsysBreadcrumbs.class.php';
error_reporting(E_ALL);
foreach ($XOOPS_MODULE_PICO as $pico) {
dumpPico($pico);
}
function dumpPico($pico)
{
global $xoopsDB;
$prefix = $xoopsDB->prefix();
$table = $pico.'_contents';
if (!MyDumpTool::tableExists($table)) {
exit('pico('.$pico.') module not found'.PHP_EOL);
}
MyDumpTool::makeDirectory('public/modules/'.$pico);
$moduleHandler = xoops_gethandler('module');
$module = $moduleHandler->getByDirname($pico);
$moduleConfigHandler = xoops_gethandler('config');
$moduleConfig = $moduleConfigHandler->getConfigsByDirname($pico);
$permObj = PicoPermission::getInstance();
$perms = $permObj->getPermissions($pico);
$categoryHandler = new PicoCategoryHandler($pico, $perms);
$contentHandler = new PicoContentHandler($pico);
$cats = $categoryHandler->getAllCategories();
$confContents = [];
$confCategories = [];
$dataMap = [];
$dataSeq = 1;
foreach ($cats as $cat) {
$catData = $cat->getData();
$modConfig = $cat->getOverriddenModConfig();
$link = pico_common_make_category_link4html($modConfig, $catData, $pico);
MyDumpTool::decode($catData);
$confCategories[] = [
'id' => $catData['id'],
'title' => $catData['cat_title'],
'desc' => (string) $catData['cat_desc'],
'pid' => (int) $catData['pid'],
'weight' => (int) $catData['cat_weight'],
'link' => $link,
];
$contents = $contentHandler->getCategoryContents($cat);
foreach ($contents as $content) {
$data = $content->getData();
$link = pico_common_make_content_link4html($modConfig, $data);
MyDumpTool::decode($data);
$url = XOOPS_URL.'/modules/'.$pico.'/'.$link;
$content_en = normalizeHtml(MyDumpTool::mlang($data['body_cached'], $url, 'en'), $url);
$content_ja = normalizeHtml(MyDumpTool::mlang($data['body_cached'], $url, 'ja'), $url);
$item = [
'id' => $data['id'],
'title' => $data['subject_raw'],
'content' => [
'en' => $content_en,
'ja' => $content_ja,
],
];
MyDumpTool::saveJson('public/modules/'.$pico.'/'.$data['id'].'.json', $item);
$confContents[] = [
'id' => $item['id'],
'title' => $data['subject_raw'],
'cat_id' => (int) $data['cat_id'],
'weight' => (int) $data['weight'],
'link' => $link,
];
}
}
$moduleinfo = [
'name' => $module->get('name'),
'dirname' => $pico,
'message' => $moduleConfig['top_message'],
'show_menuinmoduletop' => (int) $moduleConfig['show_menuinmoduletop'],
'show_listasindex' => (int) $moduleConfig['show_listasindex'],
'show_breadcrumbs' => (int) $moduleConfig['show_breadcrumbs'],
'show_pagenavi' => (int) $moduleConfig['show_pagenavi'],
];
MyDumpTool::decode($moduleinfo);
$config = [
'module' => $moduleinfo,
'categories' => $confCategories,
'contents' => $confContents,
];
MyDumpTool::saveJson('public/modules/'.$pico.'/index.json', $config);
}
-90
View File
@@ -1,90 +0,0 @@
<?php
require_once __DIR__.'/common.inc.php';
if (!isset($XOOPS_MODULE_XELFINDER) || empty($XOOPS_MODULE_XELFINDER)) {
exit('Not configured for xelfinder module'.PHP_EOL);
}
foreach ($XOOPS_MODULE_XELFINDER as $mydirname) {
dumpXelfinder($mydirname);
}
function dumpXelfinder($mydirname)
{
global $xoopsDB;
$prefix = $xoopsDB->prefix();
$table = $mydirname.'_file';
if (!MyDumpTool::tableExists($table)) {
exit($mydirname.' module not found'.PHP_EOL);
}
$hostname = rawurlencode(substr(XOOPS_URL, strpos(XOOPS_URL, '://') + 3));
// all registered files
$sql = <<< SQL
SELECT
*
FROM `${prefix}_${table}`
WHERE `mime` != 'directory'
SQL;
if (!($res = $xoopsDB->query($sql))) {
var_dump($xoopsDB);
exit();
}
while ($row = $xoopsDB->fetchArray($res)) {
MyDumpTool::decode($row);
MyDumpTool::convertToInt($row, ['file_id', 'parent_id', 'size', 'ctime', 'mtime', 'uid', 'gid', 'width', 'height']);
$row['gids'] = array_map('intval', explode(',', $row['gids']));
$operm = intval(hexdec(substr($row['perm'], 2, 1)));
if ($operm & 0x04) {
// bit:0100 - read permission
$src = XOOPS_TRUST_PATH.'/uploads/xelfinder/'.$hostname.'_'.$mydirname.'_'.$row['file_id'];
$basedir = '/modules/'.$mydirname.'/index.php/view/'.$row['file_id'];
MyDumpTool::makeDirectory('public'.$basedir);
$dest = MYDUMPTOOL_OUTPUTDIR.'/public'.$basedir.'/'.$row['name'];
if (!MyDumpTool::fileCopy($src, $dest)) {
exit('Failed to copy file : '.$src.PHP_EOL);
}
$htaccess = MYDUMPTOOL_OUTPUTDIR.'/public'.$basedir.'/.htaccess';
if (!file_exists($htaccess)) {
$htfname = str_replace(' ', '\\ ', $row['name']);
$data = "RewriteEngine On\nRewriteBase $basedir\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteRule .* $htfname [R=301,L]\n";
file_put_contents($htaccess, $data);
}
$thumbs = glob($src.'_*.tmb');
if (false !== $thumbs && !empty($thumbs)) {
foreach ($thumbs as $thumb) {
list($twidth, $theight) = getimagesize($thumb);
$tsize = max((int) $twidth, (int) $theight);
$tsrc = $thumb;
$tbasedir = '/modules/'.$mydirname.'/index.php/tmb/'.$tsize.'/'.$row['file_id'];
MyDumpTool::makeDirectory('public'.$tbasedir);
$tdest = MYDUMPTOOL_OUTPUTDIR.'/public'.$tbasedir.'/'.$row['name'];
if (!MyDumpTool::fileCopy($tsrc, $tdest)) {
exit('Failed to copy file : '.$tsrc.PHP_EOL);
}
$htaccess = MYDUMPTOOL_OUTPUTDIR.'/public'.$tbasedir.'/.htaccess';
if (!file_exists($htaccess)) {
$htfname = str_replace(' ', '\\ ', $row['name']);
$data = "RewriteEngine On\nRewriteBase $tbasedir\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteRule .* $htfname [R=301,L]\n";
file_put_contents($htaccess, $data);
}
}
}
}
}
$xoopsDB->freeRecordSet($res);
// cache files
$src = XOOPS_ROOT_PATH.'/modules/'.$mydirname.'/cache/tmb/*.png';
$thumbs = glob($src);
if (false !== $thumbs && !empty($thumbs)) {
$tbasedir = '/modules/'.$mydirname.'/cache/tmb';
MyDumpTool::makeDirectory('public'.$tbasedir);
foreach ($thumbs as $thumb) {
$tsrc = $thumb;
$tdest = MYDUMPTOOL_OUTPUTDIR.'/public'.$tbasedir.'/'.basename($tsrc);
if (!MyDumpTool::fileCopy($tsrc, $tdest)) {
exit('Failed to copy file : '.$tsrc.PHP_EOL);
}
}
}
}
-1062
View File
File diff suppressed because it is too large Load Diff
-178
View File
@@ -1,178 +0,0 @@
<?php
function normalizeUrl($url, $base, $type)
{
$localpath = function ($url, $type) {
$purl1 = parse_url($url);
$purl2 = parse_url(XOOPS_URL);
if (isset($purl1['host']) && isset($purl2['host']) && $purl1['host'] === $purl2['host']) {
$url = preg_replace('/^http:/', 'https:', $url);
}
$url = preg_replace('/^https?:\/\/www\.neuroinf\.jp\/?/', '/', $url);
$url = preg_replace('/^\/fmanager\/(view|tmb)\/(.*)(:?\?_t=.*)?$/U', '/modules/fmanager/index.php/\1/\2', $url);
$url = preg_replace('/^\/modules\/xoonips\/detail\.php\?((:?item_)?id=.+)&download=.*$/U', '/modules/xoonips/detail.php?\1', $url);
$url = preg_replace('/^\/modules\/xoonips\/image\.php\/thumbnail\/(\d+)\/(.*)$/U', '/modules/xoonips/file/\1.png', $url);
$url = preg_replace('/^\/modules\/xoonips\/preview\.php\/\d+\/(\d+)\/(.*)$/U', '/modules/xoonips/file/\1.png', $url);
$url = preg_replace('/^\/modules\/xoonips\/download\.php\/\d+\/(\d+)\/(.*)$/U', '/modules/xoonips/file/\1/\2', $url);
$url = preg_replace('/^\/modules\/xoonips\/download\.php\/([^\/]+)\?file_id=(\d+)$/U', '/modules/xoonips/file/\2/\1', $url);
if (preg_match('/^\/modules\/xoonips\/download\.php\?file_id=(\d+)$/U', $url, $matches)) {
$fpath = MYDUMPTOOL_OUTPUTDIR.'/public/modules/xoonips/file/'.$matches[1];
$found = false;
foreach (glob($fpath.'/*') as $fname) {
if ('.htaccess' === $fname) {
continue;
} else {
$url = '/modules/xoonips/file/'.$matches[1].'/'.urlencode(basename($fname));
$found = true;
}
}
if (false === $found) {
echo 'Failed to replace xoonips download url:'.$url.PHP_EOL;
return false;
}
}
// append
if ('videoSrc' === $type) {
$url = preg_replace('/^\/modules\/xoonips\/file\/1459.png$/', '/modules/xoonips/file/1459/benham\'s_top.mp4', $url);
$url = preg_replace('/^\/modules\/xoonips\/file\/1469.png$/', '/modules/xoonips/file/1469/music_notes.mp4', $url);
}
return $url;
};
if ('' === $url) {
return $localpath($base, $type);
}
$purl = parse_url($url);
if (isset($purl['scheme'])) {
return $localpath($url, $type);
}
$pburl = parse_url($base);
$path = $pburl['path'];
if ('/' == $url[0]) {
$path = '';
} elseif ('#' !== $url[0] && '?' !== $url[0]) {
$path = preg_replace('#/[^/]*$#', '', $path).'/';
}
$path .= $url;
$pattern = ['#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#'];
do {
$path = preg_replace($pattern, '/', $path, -1, $count);
} while (0 < $count);
return $localpath($pburl['scheme'].'://'.$pburl['host'].$path, $type);
}
function checkLocalFile($url, $tag)
{
$url2 = preg_replace('/'.preg_quote(XOOPS_URL, '/').'/', '', $url);
if (preg_match('/^\//', $url2)) {
if (!preg_match('/\/([^\/]+\.(php|html))?(\?.+)?(#.+)?$/i', $url2, $matches)) {
$fpath = MYDUMPTOOL_OUTPUTDIR.'/public'.urldecode($url);
if (!file_exists($fpath)) {
echo 'Error: File not found: '.$fpath.PHP_EOL;
return false;
}
}
return $url2;
}
return $url;
}
function normalizeHtml($text, $path)
{
$text = MyDumpTool::fixHtml($text);
$text = preg_replace_callback('/<img ([^>]*)src="(.*)"([^>]*)\/>/Us', function ($matches) use ($path) {
$url = htmlspecialchars_decode($matches[2], ENT_QUOTES);
$url = normalizeUrl($url, $path, 'imgSrc');
$url = checkLocalFile($url, 'imgSrc');
if (false === $url) {
exit($path.PHP_EOL);
}
return '<img '.$matches[1].'src="'.htmlspecialchars($url, ENT_QUOTES).'"'.$matches[3].'/>';
}, $text);
$text = preg_replace_callback('/<a ([^>]*)href="([^"]+)"([^>]*)>/Us', function ($matches) use ($path) {
$url = htmlspecialchars_decode($matches[2], ENT_QUOTES);
if (!preg_match('/^#/', $url)) {
$url = normalizeUrl($url, $path, 'aHref');
if (false === $url) {
exit($path.PHP_EOL);
}
$url = checkLocalFile($url, 'aHref');
if (false === $url) {
exit($path.PHP_EOL);
}
}
return '<a '.$matches[1].'href="'.htmlspecialchars($url, ENT_QUOTES).'"'.$matches[3].'>';
}, $text);
$text = preg_replace_callback('/<embed ([^>]*)src="(.*)"([^>]*)\/>/Us', function ($matches) use ($path) {
$url = htmlspecialchars_decode($matches[2], ENT_QUOTES);
$url = normalizeUrl($url, $path, 'embedSrc');
if (false === $url) {
exit($path.PHP_EOL);
}
$url = checkLocalFile($url, 'embedSrc');
if (false === $url) {
exit($path.PHP_EOL);
}
return '<embed '.$matches[1].'src="'.htmlspecialchars($url, ENT_QUOTES).'"'.$matches[3].'/>';
}, $text);
$text = preg_replace_callback('/<video ([^>]*)src="(.*)"([^>]*)>/Us', function ($matches) use ($path) {
$url = htmlspecialchars_decode($matches[2], ENT_QUOTES);
$url = normalizeUrl($url, $path, 'videoSrc');
if (false === $url) {
exit($path.PHP_EOL);
}
$url = checkLocalFile($url, 'videoSrc');
if (false === $url) {
exit($path.PHP_EOL);
}
return '<video '.$matches[1].'src="'.htmlspecialchars($url, ENT_QUOTES).'"'.$matches[3].'>';
}, $text);
$text = preg_replace('/ lang="([^"]+)"/Us', '', $text);
$text = preg_replace('/ xml:lang="([^"]+)"/Us', '', $text);
// remove font-family attributes
$text = preg_replace_callback('/<span([^>]*?)>((?!<span).*)<\/span>/Us', function ($matches) use ($path) {
if ('' === trim($matches[1]) || '' === trim($matches[2]) || '&nbsp;' === trim($matches[2])) {
return $matches[2];
}
$m1 = $matches[1];
$m1 = preg_replace_callback('/style="(.*)"/Us', function ($matches2) {
$styles1 = array_map('trim', explode(';', htmlspecialchars_decode($matches2[1], ENT_QUOTES)));
$styles2 = [];
foreach ($styles1 as $style1) {
if ('' === $style1) {
continue;
}
$style = explode(':', $style1);
list($key, $value) = array_map('trim', explode(':', $style1));
if ('font-family' === $key || 'line-height' === $key || '' === $value || 'color' === $key && 'null' === $value) {
// skip
} else {
$styles2[] = $key.':'.$value;
}
}
return 0 === count($styles2) ? '' : ('style="'.htmlspecialchars(implode(';', $styles2).';', ENT_QUOTES).'"');
}, $m1);
if ('' === trim($m1)) {
return $matches[2];
}
if (strpos($m1, 'font-family') > 0) {
echo 'trimed span:'.$path.PHP_EOL;
}
return '<span'.$m1.'>'.$matches[2].'</span>';
}, $text);
$text = MyDumpTool::fixHtml($text);
return $text;
}
-2206
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -1,8 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
@@ -22,11 +22,12 @@
<meta name="twitter:site" content="@neuroinf_jp" />
<meta name="twitter:title" content='INCF Japan Node' />
<meta property="og:image" content="/modules/fmanager/index.php/view/565/J-Node%20Logo%20Mark%20250x250.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="manifest" href="/manifest.json" />
<title>Top - INCF Japan Node</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+2104
View File
File diff suppressed because it is too large Load Diff
+41 -63
View File
@@ -1,65 +1,43 @@
{
"name": "portal",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^13.1.9",
"@types/async-lock": "^1.1.2",
"@types/jest": "^26.0.23",
"@types/lokijs": "^1.5.4",
"@types/node": "^15.6.1",
"@types/react": "^17.0.8",
"@types/react-dom": "^17.0.5",
"@types/react-helmet": "^6.1.1",
"@types/react-html-parser": "^2.0.1",
"@types/react-router-dom": "^5.1.7",
"@types/react-router-hash-link": "^2.4.0",
"async-lock": "^1.3.0",
"axios": "^0.21.1",
"lokijs": "^1.5.12",
"moment": "2.29.1",
"rc-tree": "^4.1.5",
"react": "^17.0.2",
"react-app-polyfill": "^2.0.0",
"react-cookie": "^4.0.3",
"react-dom": "^17.0.2",
"react-ga": "^3.3.0",
"react-helmet": "^6.1.0",
"react-html-parser": "^2.0.2",
"react-image-lightbox": "^5.1.1",
"react-overlays": "^5.0.1",
"react-router-dom": "^5.2.0",
"react-router-hash-link": "^2.4.3",
"react-scripts": "4.0.3",
"react-spinner-material": "^1.3.1",
"rss-parser": "^3.12.0",
"split-string": "^6.1.0",
"typescript": "~4.3.2",
"xregexp": "^5.0.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"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"
]
}
"name": "portal",
"version": "2.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "biome lint",
"format": "biome format --write",
"check": "biome check --write"
},
"dependencies": {
"@dr.pogodin/react-helmet": "^3.2.3",
"@orrisroot/react-html-parser": "^3.0.1",
"async-lock": "^1.4.1",
"date-fns": "^4.4.0",
"ky": "^2.1.0",
"lokijs": "^1.5.12",
"rc-tree": "^5.13.1",
"react": "^19.3.0",
"react-cookie": "^8.1.2",
"react-dom": "^19.3.0",
"react-ga4": "^3.0.1",
"react-overlays": "^5.2.1",
"react-router": "^8.3.1",
"sanitize.css": "^13.0.0",
"yet-another-react-lightbox": "^3.32.2"
},
"devDependencies": {
"@biomejs/biome": "^2.5.13",
"@types/async-lock": "^1.4.2",
"@types/lokijs": "^1.5.14",
"@types/node": "^24.13.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"sirv": "^3.0.2",
"typescript": "~7.0.2",
"vite": "^8.3.0"
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ RewriteEngine on
RewriteBase /
RewriteCond %{QUERY_STRING} (^|&)file_id=([0-9]+)($|&)
RewriteRule ^modules/xoonips/download.php /module/xoonips/file/%2? [R=301,L]
RewriteRule ^modules/xoonips/download.php /modules/xoonips/file/%2? [R=301,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d [OR]
+13 -13
View File
@@ -1,15 +1,15 @@
{
"short_name": "INCF Japan Node",
"name": "INCF Japan Node Portal Site",
"icons": [
{
"src": "favicon.ico",
"sizes": "16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
"short_name": "INCF Japan Node",
"name": "INCF Japan Node Portal Site",
"icons": [
{
"src": "favicon.ico",
"sizes": "16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
-1
View File
@@ -1,2 +1 @@
@import url("./common/assets/xoops.css");
@import url("./custom/assets/style.css");
-9
View File
@@ -1,9 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
+8 -8
View File
@@ -1,17 +1,17 @@
import React from 'react';
import { HelmetProvider } from '@dr.pogodin/react-helmet';
import { CookiesProvider } from 'react-cookie';
import { BrowserRouter } from 'react-router-dom';
import './App.css';
import { BrowserRouter } from 'react-router';
import AppRoot from './common/AppRoot';
import './App.css';
const App: React.FC = () => {
return (
const App = () => (
<HelmetProvider>
<CookiesProvider>
<BrowserRouter>
<AppRoot />
</BrowserRouter >
</BrowserRouter>
</CookiesProvider>
);
}
</HelmetProvider>
);
export default App;
+56 -54
View File
@@ -1,10 +1,10 @@
import React from 'react';
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { Component } from 'react';
import { Navigate, useLocation } from 'react-router';
import Loading from '../common/lib/Loading';
import PageNotFound from '../common/lib/PageNotFound';
import { MultiLang } from '../config';
import type { MultiLang } from '../config';
import BulletinArchive from './lib/BulletinArchive';
import BulletinContext from './lib/BulletinContext';
import type BulletinContext from './lib/BulletinContext';
import BulletinStories from './lib/BulletinStories';
import BulletinStory from './lib/BulletinStory';
import BulletinUtils from './lib/BulletinUtils';
@@ -14,18 +14,57 @@ interface Props {
mydirname: string;
}
interface Props {
lang: MultiLang;
mydirname: string;
}
interface State {
loading: boolean;
context: BulletinContext | null;
}
class Bulletin extends React.Component<Props, State> {
const str2int = (str: string | null): number | null => {
if (str === null || str.match(/^\d+$/) === null) {
return null;
}
return parseInt(str, 10);
};
interface BulletinPageProps {
lang: MultiLang;
context: BulletinContext;
}
const BulletinPage = (props: BulletinPageProps) => {
const { lang, context } = props;
const { pathname, search, hash } = useLocation();
if (pathname.replace(/\/$/, '') === context.getUrl('').replace(/\/$/, '')) {
return <Navigate to={context.getUrl(`index.php${search}${hash}`)} replace />;
}
if (pathname !== context.getUrl('index.php')) {
return <PageNotFound lang={lang} />;
}
const params = new URLSearchParams(search);
const page = params.get('page') || 'main';
switch (page) {
case 'main': {
const topicId = str2int(params.get('storytopic')) || 0;
const storyNum = str2int(params.get('storynum')) || 5;
const start = str2int(params.get('start')) || 0;
return (
<BulletinStories lang={lang} context={context} topicId={topicId} storyNum={storyNum} start={start} />
);
}
case 'article': {
const storyId = str2int(params.get('storyid')) || 0;
return <BulletinStory lang={lang} context={context} id={storyId} />;
}
case 'archive': {
const year = str2int(params.get('year')) || 0;
const month = str2int(params.get('month')) || 0;
return <BulletinArchive lang={lang} context={context} year={year} month={month} />;
}
}
return <PageNotFound lang={lang} />;
};
class Bulletin extends Component<Props, State> {
private isActive: boolean;
constructor(props: Props) {
@@ -42,7 +81,7 @@ class Bulletin extends React.Component<Props, State> {
this.updateContext();
}
componentDidUpdate(prevProps: Props, prevState: State) {
componentDidUpdate(prevProps: Props) {
const { mydirname } = this.props;
const prevMydirname = prevProps.mydirname;
if (mydirname !== prevMydirname) {
@@ -63,13 +102,6 @@ class Bulletin extends React.Component<Props, State> {
}
}
private str2int(str: string | null): number | null {
if (str === null || str.match(/^\d+$/) === null) {
return null;
}
return parseInt(str, 10);
}
render() {
const { lang, mydirname } = this.props;
const { loading, context } = this.state;
@@ -79,42 +111,12 @@ class Bulletin extends React.Component<Props, State> {
if (context === null) {
return <PageNotFound lang={lang} />;
}
return <div className={`bulletin ${mydirname}`}>
<Switch>
<Route exact path={context.getUrl('')} render={(props: RouteComponentProps) => {
const { location } = props;
const url = context.getUrl('index.php' + location.search + location.hash);
return <Redirect to={url} />
}} />
<Route exact path={context.getUrl('index.php')} render={(props: RouteComponentProps) => {
const { location } = props;
const params = new URLSearchParams(location.search);
const page = params.get('page') || 'main';
switch (page) {
case 'main': {
const topicId = this.str2int(params.get('storytopic')) || 0;
const storyNum = this.str2int(params.get('storynum')) || 5;
const start = this.str2int(params.get('start')) || 0;
return <BulletinStories lang={lang} context={context} topicId={topicId} storyNum={storyNum} start={start} />
}
case 'article': {
const storyId = this.str2int(params.get('storyid')) || 0;
return <BulletinStory lang={lang} context={context} id={storyId} />
}
case 'archive': {
const year = this.str2int(params.get('year')) || 0;
const month = this.str2int(params.get('month')) || 0;
return <BulletinArchive lang={lang} context={context} year={year} month={month} />
}
}
return <PageNotFound lang={lang} />
}} />
<Route render={(props: RouteComponentProps) => {
return <PageNotFound lang={lang} />;
}} />
</Switch>
</div>;
return (
<div className={`bulletin ${mydirname}`}>
<BulletinPage lang={lang} context={context} />
</div>
);
}
}
export default Bulletin;
export default Bulletin;
+38 -18
View File
@@ -1,10 +1,10 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Component } from 'react';
import { Link } from 'react-router';
import Loading from '../../common/lib/Loading';
import PageNotFound from '../../common/lib/PageNotFound';
import { MultiLang } from '../../config';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
import BulletinContext from '../lib/BulletinContext';
import type BulletinContext from '../lib/BulletinContext';
import BulletinStoryItem from '../lib/BulletinStoryItem';
import BulletinUtils from '../lib/BulletinUtils';
@@ -18,8 +18,7 @@ interface State {
context: BulletinContext | null;
}
class BulletinRecentNews extends React.Component<Props, State> {
class BulletinRecentNews extends Component<Props, State> {
private isActive: boolean;
constructor(props: Props) {
@@ -36,7 +35,7 @@ class BulletinRecentNews extends React.Component<Props, State> {
this.updateContext();
}
componentDidUpdate(prevProps: Props, prevState: State) {
componentDidUpdate(prevProps: Props) {
const { mydirname } = this.props;
const prevMydirname = prevProps.mydirname;
if (mydirname !== prevMydirname) {
@@ -71,17 +70,38 @@ class BulletinRecentNews extends React.Component<Props, State> {
return <PageNotFound lang={lang} />;
}
const stories = context.getStories(0, 10, 0);
return <div className={`bulletin ${module.dirname} recentnews-block`}>
{stories.filter((story, index) => index < 3).map((story, index) => {
return <BulletinStoryItem key={story.id} lang={lang} context={context} story={story} isList={true} />
})}
<ul>
{stories.filter((story, index) => index >= 3).map((story, index) => {
return <li key={story.id}><Link to={context.getStoryUrl(story.id)}>{Functions.mlang(story.title, lang)}</Link> ({story.posttime})</li>
})}
</ul>
<div style={{ textAlign: 'right' }}><Link to={context.getTopicUrl(0, 10, 10)}>[more]</Link></div>
</div>;
return (
<div className={`bulletin ${module.dirname} recentnews-block`}>
{stories
.filter((_story, index) => index < 3)
.map((story) => {
return (
<BulletinStoryItem
key={story.id}
lang={lang}
context={context}
story={story}
isList={true}
/>
);
})}
<ul>
{stories
.filter((_story, index) => index >= 3)
.map((story) => {
return (
<li key={story.id}>
<Link to={context.getStoryUrl(story.id)}>{Functions.mlang(story.title, lang)}</Link>{' '}
({story.posttime})
</li>
);
})}
</ul>
<div style={{ textAlign: 'right' }}>
<Link to={context.getTopicUrl(0, 10, 10)}>[more]</Link>
</div>
</div>
);
}
}
+103 -66
View File
@@ -1,10 +1,10 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { Helmet } from '@dr.pogodin/react-helmet';
import type { CSSProperties } from 'react';
import { Link } from 'react-router';
import PageNotFound from '../../common/lib/PageNotFound';
import { MultiLang } from '../../config';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
import BulletinContext from './BulletinContext';
import type BulletinContext from './BulletinContext';
import BulletinUtils from './BulletinUtils';
interface Props {
@@ -14,7 +14,7 @@ interface Props {
month: number;
}
const BulletinArchive: React.FC<Props> = (props: Props) => {
const BulletinArchive = (props: Props) => {
const { lang, context, year, month } = props;
const years = context.getArchiveYearRange();
if (years.min === 0 || years.max === 0) {
@@ -27,66 +27,103 @@ const BulletinArchive: React.FC<Props> = (props: Props) => {
const title = Functions.mlang('[en]Archive[/en][ja]アーカイブ[/ja]', lang);
const titles = [Functions.siteTitle(lang), Functions.mlang(module.name, lang), title];
const stories = year !== 0 ? context.getStoriesByMonth(year, month) : [];
return <div className="arcive">
<Helmet>
<title>{titles.reverse().join(' - ')}</title>
</Helmet>
<div style={{ float: 'right' }}>|&nbsp;<Link to={context.getArchiveUrl()}>{Functions.mlang('[en]Archive[/en][ja]アーカイブ[/ja]', lang)}</Link>&nbsp;|</div>
<div style={{ clear: 'both', height: '0px', visibility: 'hidden' }}></div>
<div>
<table>
<tbody>
<tr>
<th colSpan={13}>{Functions.mlang('[en]News archive[/en][ja]ニュースアーカイブ[/ja]', lang)}</th>
</tr>
{[...Array(years.max - years.min + 1)].map((v, index) => {
const _year = index + years.min;
const evenodd = index % 2 === 0 ? 'even' : 'edd';
return <tr key={_year}>
<th>{_year}{lang === 'ja' && '年'}</th>
{[...Array(12)].map((v, index) => {
const _month = index + 1;
const label = BulletinUtils.getMonthLabel(_month, lang);
const style: React.CSSProperties = { textAlign: 'center' };
const num = context.getArchiveCounter(_year, _month);
const xmonth = (num === 0 || (_year === year && _month === month)) ? label : <Link to={context.getArchiveUrl(_year, _month)} style={{textDecoration: 'underline'}}>{label}</Link>;
if (_year === year && _month === month) {
style['backgroundColor'] = '#ffcccc';
style['color'] = '#0000ff';
style['fontWeight'] = 'bold';
}
return <td key={index} className={evenodd} style={style}>{xmonth}</td>;
})}
</tr>
})}
</tbody>
</table>
</div>
{stories.length > 0 &&
<div className="item">
<div className="itemHead">
<span className="itemTitle">
{Functions.mlang('[en]News[/en][ja]ニュース[/ja]', lang)}
</span>
</div>
<div className="itemInfo">
<ul style={{ textAlign: 'left' }}>
{stories.map((story, index) => {
const topic = context.getTopic(story.topic_id);
return <li key={story.id}>
<Link to={context.getTopicUrl(story.topic_id, 5, 0)}>{Functions.mlang(topic === null ? '' : topic.title, lang)}</Link>
&nbsp;:&nbsp;
<Link to={context.getStoryUrl(story.id)}>{Functions.mlang(story.title, lang)}</Link> ({story.posttime})
</li>
})}
</ul>
</div>
<div>
{Functions.mlang('[en]There are[/en][ja]計[/ja] ' + stories.length + ' [en]story(ies) in total.[/en][ja]件のニュース記事があります[/ja]', lang)}
</div>
return (
<div className="arcive">
<Helmet>
<title>{titles.reverse().join(' - ')}</title>
</Helmet>
<div style={{ float: 'right' }}>
|&nbsp;
<Link to={context.getArchiveUrl()}>{Functions.mlang('[en]Archive[/en][ja]アーカイブ[/ja]', lang)}</Link>
&nbsp;|
</div>
}
</div>;
<div style={{ clear: 'both', height: '0px', visibility: 'hidden' }}></div>
<div>
<table>
<tbody>
<tr>
<th colSpan={13}>
{Functions.mlang('[en]News archive[/en][ja]ニュースアーカイブ[/ja]', lang)}
</th>
</tr>
{[...Array(years.max - years.min + 1)].map((_v, index) => {
const _year = index + years.min;
const evenodd = index % 2 === 0 ? 'even' : 'edd';
return (
<tr key={_year}>
<th>
{_year}
{lang === 'ja' && '年'}
</th>
{[...Array(12)].map((_v, index) => {
const _month = index + 1;
const label = BulletinUtils.getMonthLabel(_month, lang);
const style: CSSProperties = { textAlign: 'center' };
const num = context.getArchiveCounter(_year, _month);
const xmonth =
num === 0 || (_year === year && _month === month) ? (
label
) : (
<Link
to={context.getArchiveUrl(_year, _month)}
style={{ textDecoration: 'underline' }}
>
{label}
</Link>
);
if (_year === year && _month === month) {
style.backgroundColor = '#ffcccc';
style.color = '#0000ff';
style.fontWeight = 'bold';
}
return (
<td key={_month} className={evenodd} style={style}>
{xmonth}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
{stories.length > 0 && (
<div className="item">
<div className="itemHead">
<span className="itemTitle">{Functions.mlang('[en]News[/en][ja]ニュース[/ja]', lang)}</span>
</div>
<div className="itemInfo">
<ul style={{ textAlign: 'left' }}>
{stories.map((story) => {
const topic = context.getTopic(story.topic_id);
return (
<li key={story.id}>
<Link to={context.getTopicUrl(story.topic_id, 5, 0)}>
{Functions.mlang(topic === null ? '' : topic.title, lang)}
</Link>
&nbsp;:&nbsp;
<Link to={context.getStoryUrl(story.id)}>
{Functions.mlang(story.title, lang)}
</Link>{' '}
({story.posttime})
</li>
);
})}
</ul>
</div>
<div>
{Functions.mlang(
'[en]There are[/en][ja]計[/ja] ' +
stories.length +
' [en]story(ies) in total.[/en][ja]件のニュース記事があります[/ja]',
lang
)}
</div>
</div>
)}
</div>
);
};
export default BulletinArchive;
export default BulletinArchive;
+21 -22
View File
@@ -1,5 +1,6 @@
import axios from 'axios';
import ky from 'ky';
import loki from 'lokijs';
import Config from '../../config';
export interface BulletinModuleData {
name: string;
@@ -25,7 +26,7 @@ export interface BulletinStoryData {
text: {
en: string;
ja: string;
}
};
}
interface BulletinIndexData {
@@ -37,17 +38,16 @@ interface BulletinIndexData {
interface BulletinCounterData {
count: {
[date: string]: number;
}
};
year: {
min: number;
max: number;
}
};
}
export const BULLETIN_TOPIC_ID_ROOT = 0;
class BulletinContext {
private isInitialized: boolean;
private dirname: string;
private database: loki;
@@ -73,12 +73,12 @@ class BulletinContext {
getStoryUrl(id: number, page: number = 0): string {
const params = new URLSearchParams([
['page', 'article'],
['storyid', id.toString()]
['storyid', id.toString()],
]);
if (page !== 0) {
params.set('storypage', page.toString());
}
return this.getUrl('index.php?' + params.toString());
return this.getUrl(`index.php?${params.toString()}`);
}
getTopicUrl(id: number, limit: number, start: number): string {
@@ -92,20 +92,18 @@ class BulletinContext {
if (start !== 0) {
params.set('start', start.toString());
}
return this.getUrl('index.php?' + params.toString());
return this.getUrl(`index.php?${params.toString()}`);
}
getArchiveUrl(year: number = 0, month: number = 0): string {
const params = new URLSearchParams([
['page', 'archive']
]);
const params = new URLSearchParams([['page', 'archive']]);
if (year !== 0) {
params.set('year', year.toString());
}
if (month !== 0) {
params.set('month', month.toString());
}
return this.getUrl('index.php?' + params.toString());
return this.getUrl(`index.php?${params.toString()}`);
}
getModule(): BulletinModuleData | null {
@@ -130,8 +128,8 @@ class BulletinContext {
}
getStoriesByMonth(year: number, month: number) {
const key = ('0000' + year).substr(-4) + '-' + ('00' + month).substr(-2);
const filter = { posttime: { '$regex': ['^' + key] } };
const key = `${(`0000${year}`).substr(-4)}-${(`00${month}`).substr(-2)}`;
const filter = { posttime: { $regex: [`^${key}`] } };
return this.stories.chain().find(filter).simplesort('posttime', true).data();
}
@@ -140,27 +138,28 @@ class BulletinContext {
return this.stories.count(filter);
}
getArchiveYearRange(): { min: number, max: number } {
getArchiveYearRange(): { min: number; max: number } {
return this.counter.year;
}
getArchiveCounter(year: number, month: number) {
const key = ('0000' + year).substr(-4) + '-' + ('00' + month).substr(-2);
const key = `${(`0000${year}`).substr(-4)}-${(`00${month}`).substr(-2)}`;
return key in this.counter.count ? this.counter.count[key] : 0;
}
async initialize(): Promise<boolean> {
if (!this.isInitialized) {
try {
const response = await axios.get('/modules' + this.getUrl('index.json'));
const index = response.data as BulletinIndexData;
const index = await ky
.get(new URL(`${Config.BASE_URL}/modules${this.getUrl('index.json')}`, window.location.href))
.json<BulletinIndexData>();
this.module = index.module;
index.topics.forEach((value) => {
this.topics.insert(value);
});
index.stories.forEach((value, index) => {
const key = value.posttime.replace(/^(\d{4}-\d{2}).*$/, "$1");
const year = parseInt(key.replace(/^(\d{4}).*/, "$1"), 10);
const key = value.posttime.replace(/^(\d{4}-\d{2}).*$/, '$1');
const year = parseInt(key.replace(/^(\d{4}).*/, '$1'), 10);
if (index === 0) {
this.counter.year.min = year;
this.counter.year.max = year;
@@ -179,7 +178,7 @@ class BulletinContext {
}
this.stories.insert(value);
});
} catch (err) {
} catch (_err) {
// ignore
}
this.isInitialized = true;
@@ -188,4 +187,4 @@ class BulletinContext {
}
}
export default BulletinContext;
export default BulletinContext;
+26 -21
View File
@@ -1,13 +1,12 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { Helmet } from '@dr.pogodin/react-helmet';
import { Link } from 'react-router';
import PageNotFound from '../../common/lib/PageNotFound';
import { MultiLang } from '../../config';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
import BulletinContext from './BulletinContext';
import BulletinStoryItem from './BulletinStoryItem';
import BulletinStoriesSelect from './BulletinStoriesSelect';
import type BulletinContext from './BulletinContext';
import BulletinStoriesPageNavi from './BulletinStoriesPageNavi';
import BulletinStoriesSelect from './BulletinStoriesSelect';
import BulletinStoryItem from './BulletinStoryItem';
interface Props {
lang: MultiLang;
@@ -17,7 +16,7 @@ interface Props {
start: number;
}
const BulletinStories: React.FC<Props> = (props: Props) => {
const BulletinStories = (props: Props) => {
const { lang, context, topicId, storyNum, start } = props;
const topic = context.getTopic(topicId);
if (topicId !== 0 && topic === null) {
@@ -32,18 +31,24 @@ const BulletinStories: React.FC<Props> = (props: Props) => {
titles.push(Functions.mlang(topic.title, lang));
}
const stories = context.getStories(topicId, storyNum, start);
return <div className="stories">
<Helmet>
<title>{titles.reverse().join(' - ')}</title>
</Helmet>
<div style={{ float: 'right' }}>|&nbsp;<Link to={context.getArchiveUrl()}>{Functions.mlang('[en]Archive[/en][ja]アーカイブ[/ja]', lang)}</Link>&nbsp;|</div>
<div style={{ clear: 'both', height: '0px', visibility: 'hidden' }}></div>
<BulletinStoriesSelect lang={lang} context={context} topicId={topicId} num={storyNum} />
{stories.map((story) => {
return <BulletinStoryItem key={story.id} lang={lang} context={context} story={story} isList={true} />
})}
<BulletinStoriesPageNavi lang={lang} context={context} topicId={topicId} limit={storyNum} start={start} />
</div>;
return (
<div className="stories">
<Helmet>
<title>{titles.reverse().join(' - ')}</title>
</Helmet>
<div style={{ float: 'right' }}>
|&nbsp;
<Link to={context.getArchiveUrl()}>{Functions.mlang('[en]Archive[/en][ja]アーカイブ[/ja]', lang)}</Link>
&nbsp;|
</div>
<div style={{ clear: 'both', height: '0px', visibility: 'hidden' }}></div>
<BulletinStoriesSelect lang={lang} context={context} topicId={topicId} num={storyNum} />
{stories.map((story) => {
return <BulletinStoryItem key={story.id} lang={lang} context={context} story={story} isList={true} />;
})}
<BulletinStoriesPageNavi lang={lang} context={context} topicId={topicId} limit={storyNum} start={start} />
</div>
);
};
export default BulletinStories;
export default BulletinStories;
+32 -15
View File
@@ -1,8 +1,8 @@
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import { MultiLang } from '../../config';
import { Fragment, type ReactElement } from 'react';
import { Link } from 'react-router';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
import BulletinContext from './BulletinContext';
import type BulletinContext from './BulletinContext';
interface Props {
lang: MultiLang;
@@ -12,7 +12,7 @@ interface Props {
start: number;
}
const BulletinStoriesPageNavi: React.FC<Props> = (props: Props) => {
const BulletinStoriesPageNavi = (props: Props) => {
const { lang, context, topicId, limit, start } = props;
const total = context.countStories(topicId);
if (start > total) {
@@ -31,12 +31,12 @@ const BulletinStoriesPageNavi: React.FC<Props> = (props: Props) => {
if (rangeMax > max - 1) {
rangeMax = max - 1;
}
const link = (num: number, el?: JSX.Element) => {
const link = (num: number, el?: ReactElement) => {
const label = el || num;
const url = context.getTopicUrl(topicId, limit, (num - 1) * limit);
return num === now ? <span className="current">{label}</span> : <Link to={url}>{label}</Link>
}
const items: JSX.Element[] = [];
return num === now ? <span className="current">{label}</span> : <Link to={url}>{label}</Link>;
};
const items: ReactElement[] = [];
if (prev !== 0) {
items.push(link(prev, <span className="prev">&laquo;</span>));
}
@@ -55,12 +55,29 @@ const BulletinStoriesPageNavi: React.FC<Props> = (props: Props) => {
items.push(link(next, <span className="next">&raquo;</span>));
}
const rangeS = start + 1;
const rangeE = (start + limit) < total ? start + limit : total;
const rangeE = start + limit < total ? start + limit : total;
return <nav className="pagenavi">
<span className="label">{Functions.mlang(`[en]${total} Hits (${rangeS}-${rangeE})[/en][ja]全${total}件 (${rangeS}-${rangeE}件目)[/ja]`, lang)}</span>
{total > limit && <span className="pages">{items.map((el, index) => <Fragment key={index}>{index !== 0 && <>&nbsp;</>}{el}</Fragment>)}</span>}
</nav>;
return (
<nav className="pagenavi">
<span className="label">
{Functions.mlang(
`[en]${total} Hits (${rangeS}-${rangeE})[/en][ja]全${total}件 (${rangeS}-${rangeE}件目)[/ja]`,
lang
)}
</span>
{total > limit && (
<span className="pages">
{items.map((el, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: the page links are rebuilt in order on every render
<Fragment key={index}>
{index !== 0 && <>&nbsp;</>}
{el}
</Fragment>
))}
</span>
)}
</nav>
);
};
export default BulletinStoriesPageNavi;
export default BulletinStoriesPageNavi;
+47 -70
View File
@@ -1,91 +1,68 @@
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { MultiLang } from '../../config';
import { type ChangeEvent, type FormEvent, useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
import BulletinContext from './BulletinContext';
import type BulletinContext from './BulletinContext';
interface Props extends RouteComponentProps {
interface Props {
lang: MultiLang;
context: BulletinContext;
topicId: number;
num: number;
}
interface State {
path: string;
topicId: number;
num: number;
}
const STORY_NUMS: ReadonlyArray<number> = [5, 10, 15, 20, 30];
class BulletinStoriesSelect extends React.Component<Props, State> {
const BulletinStoriesSelect = (props: Props) => {
const { lang, context } = props;
const location = useLocation();
const navigate = useNavigate();
const [topicId, setTopicId] = useState(props.topicId);
const [num, setNum] = useState(props.num);
private STORY_NUMS: ReadonlyArray<number> = [5, 10, 15, 20, 30];
// Moving to another page resets the selection to what that page shows.
// biome-ignore lint/correctness/useExhaustiveDependencies: reset only when the address changes
useEffect(() => {
setTopicId(props.topicId);
setNum(props.num);
}, [location.pathname, location.search, location.hash]);
constructor(props: Props) {
super(props);
const {topicId, num, location} = props;
this.state = {
path: location.pathname + location.search + location.hash,
topicId,
num,
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleSelectTopic = this.handleSelectTopic.bind(this);
this.handleSelectNum = this.handleSelectNum.bind(this);
}
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
const { topicId, num, location } = nextProps;
const path = location.pathname + location.search + location.hash;
if (prevState.path !== path) {
return {
path,
topicId,
num
};
}
return null;
}
handleSelectTopic(event: React.ChangeEvent<HTMLSelectElement>) {
const topicId = parseInt(event.target.value, 10);
this.setState({ topicId })
}
handleSelectNum(event: React.ChangeEvent<HTMLSelectElement>) {
const num = parseInt(event.target.value, 10);
this.setState({ num })
}
handleSubmit(event: React.FormEvent<HTMLFormElement>) {
const { context, history } = this.props;
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (context !== null) {
const url = context.getTopicUrl(this.state.topicId, this.state.num, 0);
history.push(url);
}
}
void navigate(context.getTopicUrl(topicId, num, 0));
};
render() {
const { lang, context } = this.props;
const topics = context.getTopics();
return <div style={{ textAlign: 'center' }}>
<form onSubmit={this.handleSubmit}>
<select value={this.state.topicId} onChange={this.handleSelectTopic}>
const topics = context.getTopics();
return (
<div style={{ textAlign: 'center' }}>
<form onSubmit={handleSubmit}>
<select
value={topicId}
onChange={(event: ChangeEvent<HTMLSelectElement>) => setTopicId(parseInt(event.target.value, 10))}
>
<option value="0">----</option>
{topics.map(topic =>
<option key={topic.id} value={topic.id}>{Functions.mlang(topic.title, lang)}</option>
)}
{topics.map((topic) => (
<option key={topic.id} value={topic.id}>
{Functions.mlang(topic.title, lang)}
</option>
))}
</select>
&nbsp;
<select value={this.state.num} onChange={this.handleSelectNum}>
{this.STORY_NUMS.map((num) => <option key={num} value={num}>{num}</option>)}
<select
value={num}
onChange={(event: ChangeEvent<HTMLSelectElement>) => setNum(parseInt(event.target.value, 10))}
>
{STORY_NUMS.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
&nbsp;
<input type="submit" value={Functions.mlang('[en]Go![/en][ja]送信[/ja]', lang)} />
</form>
</div>;
}
}
</div>
);
};
export default withRouter(BulletinStoriesSelect);
export default BulletinStoriesSelect;
+38 -28
View File
@@ -1,10 +1,9 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { Helmet } from '@dr.pogodin/react-helmet';
import { Link } from 'react-router';
import PageNotFound from '../../common/lib/PageNotFound';
import { MultiLang } from '../../config';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
import BulletinContext from './BulletinContext';
import type BulletinContext from './BulletinContext';
import BulletinStoryItem from './BulletinStoryItem';
interface Props {
@@ -13,7 +12,7 @@ interface Props {
id: number;
}
const BulletinStory: React.FC<Props> = (props: Props) => {
const BulletinStory = (props: Props) => {
const { lang, context, id } = props;
const story = context.getStory(id);
if (story === null) {
@@ -29,30 +28,41 @@ const BulletinStory: React.FC<Props> = (props: Props) => {
}
const titles = [Functions.siteTitle(lang), Functions.mlang(module.name, lang), Functions.mlang(story.title, lang)];
const stories = context.getStories(story.topic_id, module.stories_of_cat, 0);
return <div className="story">
<Helmet>
<title>{titles.reverse().join(' - ')}</title>
</Helmet>
<div style={{ float: 'right' }}>|&nbsp;<Link to={context.getArchiveUrl()}>{Functions.mlang('[en]Archive[/en][ja]アーカイブ[/ja]', lang)}</Link>&nbsp;|</div>
<div style={{ clear: 'both', height: '0px', visibility: 'hidden' }}></div>
<div>
<BulletinStoryItem key={story.id} lang={lang} context={context} story={story} isList={false} />
</div>
<div className="item">
<div className="itemHead">
<span className="itemTitle">
<Link to={context.getTopicUrl(story.topic_id, 5, 0)}>{Functions.mlang(topic.title, lang)}</Link>
</span>
return (
<div className="story">
<Helmet>
<title>{titles.reverse().join(' - ')}</title>
</Helmet>
<div style={{ float: 'right' }}>
|&nbsp;
<Link to={context.getArchiveUrl()}>{Functions.mlang('[en]Archive[/en][ja]アーカイブ[/ja]', lang)}</Link>
&nbsp;|
</div>
<div className="itemInfo">
<ul style={{ textAlign: 'left' }}>
{stories.map((story, index) => {
return <li key={story.id}><Link to={context.getStoryUrl(story.id)}>{Functions.mlang(story.title, lang)}</Link> ({story.posttime})</li>
})}
</ul>
<div style={{ clear: 'both', height: '0px', visibility: 'hidden' }}></div>
<div>
<BulletinStoryItem key={story.id} lang={lang} context={context} story={story} isList={false} />
</div>
<div className="item">
<div className="itemHead">
<span className="itemTitle">
<Link to={context.getTopicUrl(story.topic_id, 5, 0)}>{Functions.mlang(topic.title, lang)}</Link>
</span>
</div>
<div className="itemInfo">
<ul style={{ textAlign: 'left' }}>
{stories.map((story) => {
return (
<li key={story.id}>
<Link to={context.getStoryUrl(story.id)}>{Functions.mlang(story.title, lang)}</Link>{' '}
({story.posttime})
</li>
);
})}
</ul>
</div>
</div>
</div>
</div>;
);
};
export default BulletinStory;
export default BulletinStory;
+43 -28
View File
@@ -1,10 +1,10 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Link } from 'react-router';
import PageNotFound from '../../common/lib/PageNotFound';
import XoopsCode from '../../common/lib/XoopsCode';
import { MultiLang } from '../../config';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
import BulletinContext, { BulletinStoryData } from './BulletinContext';
import type BulletinContext from './BulletinContext';
import type { BulletinStoryData } from './BulletinContext';
import BulletinUtils from './BulletinUtils';
interface Props {
@@ -14,7 +14,7 @@ interface Props {
isList: boolean;
}
const BulletinStoryItem: React.FC<Props> = (props: Props) => {
const BulletinStoryItem = (props: Props) => {
const { lang, context, story, isList } = props;
const topic = context.getTopic(story.topic_id);
const urlTopic = context.getTopicUrl(story.topic_id, 5, 0);
@@ -23,29 +23,44 @@ const BulletinStoryItem: React.FC<Props> = (props: Props) => {
return <PageNotFound lang={lang} />;
}
const { text, rchars } = BulletinUtils.getStoryText(story, isList, lang);
const remaining = lang === 'en' ? rchars + ' characters more' : '残り' + rchars + '字';
return <div className="item">
<div className="itemHead">
<span className="itemTitle">
<Link to={urlTopic}>{Functions.mlang(topic.title, lang)}</Link>
&nbsp;:&nbsp;
{isList ? <Link to={urlStory}>{Functions.mlang(story.title, lang)}</Link> : Functions.mlang(story.title, lang)}
</span>
const remaining = lang === 'en' ? `${rchars} characters more` : `残り${rchars}`;
return (
<div className="item">
<div className="itemHead">
<span className="itemTitle">
<Link to={urlTopic}>{Functions.mlang(topic.title, lang)}</Link>
&nbsp;:&nbsp;
{isList ? (
<Link to={urlStory}>{Functions.mlang(story.title, lang)}</Link>
) : (
Functions.mlang(story.title, lang)
)}
</span>
</div>
<div className="itemBody">
<div className="itemText">
<XoopsCode lang={lang} text={text} dohtml={true} />
</div>
<div className="itemPermaLink">
{rchars > 0 && (
<>
<Link to={urlStory}>
{Functions.mlang('[en]Read more..[/en][ja]続きを読む[/ja]', lang)}
</Link>{' '}
| {remaining}
</>
)}
</div>
<div className="itemInfo">
<span className="itemPostDate">
&nbsp;{Functions.mlang('[en]on[/en][ja]投稿日時:[/ja]', lang)} {story.posttime}
</span>
</div>
</div>
<div style={{ clear: 'both' }}></div>
<div className="itemFoot"></div>
</div>
<div className="itemBody">
<div className="itemText">
<XoopsCode lang={lang} text={text} dohtml={true} />
</div>
<div className="itemPermaLink">
{rchars > 0 && <><Link to={urlStory}>{Functions.mlang('[en]Read more..[/en][ja]続きを読む[/ja]', lang)}</Link> | {remaining}</>}
</div>
<div className="itemInfo">
<span className="itemPostDate">&nbsp;{Functions.mlang('[en]on[/en][ja]投稿日時:[/ja]', lang)} {story.posttime}</span>
</div>
</div>
<div style={{ clear: 'both' }}></div>
<div className="itemFoot"></div>
</div>;
);
};
export default BulletinStoryItem;
export default BulletinStoryItem;
+35 -18
View File
@@ -1,8 +1,7 @@
import { MultiLang } from '../../config';
import BulletinContext, { BulletinStoryData } from './BulletinContext';
import type { MultiLang } from '../../config';
import BulletinContext, { type BulletinStoryData } from './BulletinContext';
class BulletinUtils {
private static contexts: Map<string, BulletinContext> = new Map<string, BulletinContext>();
private constructor() {
@@ -10,34 +9,52 @@ class BulletinUtils {
}
public static async getContext(mydirname: string): Promise<BulletinContext> {
let context = this.contexts.get(mydirname);
let context = BulletinUtils.contexts.get(mydirname);
if (typeof context === 'undefined') {
context = new BulletinContext(mydirname);
await context.initialize();
this.contexts.set(mydirname, context);
BulletinUtils.contexts.set(mydirname, context);
}
return context;
}
public static getStoryText(story: BulletinStoryData, isList: boolean, lang: MultiLang): { text: string, rchars: number } {
public static getStoryText(
story: BulletinStoryData,
isList: boolean,
lang: MultiLang
): { text: string; rchars: number } {
const pages = story.text[lang].split('[pagebreak]');
return isList ? {
text: pages[0],
rchars: pages.filter((value, index) => index !== 0).join('<br /><br />').length
} : {
text: pages.join('<br /><br />'),
rchars: 0
};
return isList
? {
text: pages[0],
rchars: pages.filter((_value, index) => index !== 0).join('<br /><br />').length,
}
: {
text: pages.join('<br /><br />'),
rchars: 0,
};
}
public static getMonthLabel(month: number, lang: MultiLang): string {
const label = {
en: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
ja: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
}
en: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
ja: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
};
return label[lang][month - 1];
}
}
export default BulletinUtils;
export default BulletinUtils;
+33 -53
View File
@@ -1,67 +1,47 @@
import React, { Component } from 'react';
import { ReactCookieProps, withCookies } from 'react-cookie';
import ReactGA from 'react-ga';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import Config, { MultiLang } from '../config';
import { useEffect, useRef } from 'react';
import { useCookies } from 'react-cookie';
import ReactGA from 'react-ga4';
import { useLocation } from 'react-router';
import Config, { type MultiLang } from '../config';
import Page from '../custom/Page';
interface Props extends RouteComponentProps, ReactCookieProps { }
interface State {
lang: MultiLang;
const isAnalyticsEnabled = import.meta.env.PROD && Config.GOOGLE_ANALYTICS_TRACKING_ID !== '';
if (isAnalyticsEnabled) {
// Page views are sent from the location effect below, the first one included;
// gtag's own page_view on config would count the landing page twice.
ReactGA.initialize(Config.GOOGLE_ANALYTICS_TRACKING_ID, { gtagOptions: { send_page_view: false } });
}
class AppRoot extends Component<Props, State> {
const toMultiLang = (value: unknown): MultiLang | null => (value === 'en' || value === 'ja' ? value : null);
constructor(props: Props) {
super(props);
this.state = { lang: 'en' };
if (Config.GOOGLE_ANALYTICS_TRACKING_ID !== '') {
ReactGA.initialize(Config.GOOGLE_ANALYTICS_TRACKING_ID);
}
}
const AppRoot = () => {
const location = useLocation();
const [cookies, setCookie] = useCookies(['ml_lang']);
const previousLocation = useRef<string | null>(null);
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
const params = new URLSearchParams(nextProps.location.search);
const param_lang = params.get('ml_lang');
const cookie_lang = typeof nextProps.cookies !== 'undefined' ? nextProps.cookies.get('ml_lang') : null;
let lang = param_lang || (typeof cookie_lang === 'string' ? cookie_lang : null) || prevState.lang;
if (lang !== 'en' && lang !== 'ja') {
lang = 'en';
}
if (cookie_lang !== lang) {
if (typeof nextProps.cookies !== 'undefined') {
nextProps.cookies.set('ml_lang', lang, { path: '/' });
}
}
if (prevState.lang !== lang) {
return { lang };
}
return null;
}
const paramLang = toMultiLang(new URLSearchParams(location.search).get('ml_lang'));
const cookieLang = toMultiLang(cookies.ml_lang);
const lang: MultiLang = paramLang ?? cookieLang ?? 'en';
componentDidMount() {
const { pathname } = this.props.location;
if (Config.GOOGLE_ANALYTICS_TRACKING_ID !== '') {
ReactGA.set({ page: pathname });
ReactGA.pageview(pathname);
useEffect(() => {
if (cookies.ml_lang !== lang) {
setCookie('ml_lang', lang, { path: '/' });
}
}
}, [cookies.ml_lang, lang, setCookie]);
componentDidUpdate(prevProps: Props) {
const { pathname, search } = this.props.location;
if (Config.GOOGLE_ANALYTICS_TRACKING_ID !== '') {
ReactGA.set({ page: pathname });
ReactGA.pageview(pathname);
useEffect(() => {
if (isAnalyticsEnabled) {
ReactGA.send({ hitType: 'pageview', page: location.pathname + location.search });
}
if (pathname !== prevProps.location.pathname || search !== prevProps.location.search) {
const current = location.pathname + location.search;
if (previousLocation.current !== null && previousLocation.current !== current) {
window.scrollTo(0, 0);
}
}
previousLocation.current = current;
}, [location.pathname, location.search]);
render() {
const { lang } = this.state;
return <Page lang={lang} />;
}
}
return <Page lang={lang} />;
};
export default withCookies(withRouter(AppRoot));
export default AppRoot;
+20 -30
View File
@@ -1,40 +1,30 @@
import React, { Component } from 'react';
import { Redirect, RouteComponentProps } from 'react-router';
import { MultiLang } from '../config';
import { Navigate, useLocation } from 'react-router';
import type { MultiLang } from '../config';
import PageNotFound from './lib/PageNotFound';
interface Params {
module: string;
pathname: string;
}
interface Props extends RouteComponentProps<Params> {
interface Props {
lang: MultiLang;
}
class XoopsPathRedirect extends Component<Props> {
getRedirectUrl() {
const { pathname } = this.props.location;
switch (pathname || '') {
case '/index.php': {
return '/';
}
}
return '';
const getRedirectUrl = (pathname: string): string => {
switch (pathname || '') {
case '/index.php':
return '/';
}
return '';
};
render() {
const { lang } = this.props;
if (this.props.location.pathname === '/') {
return null;
}
const url = this.getRedirectUrl();
if (url === '') {
return <PageNotFound lang={lang} />;
}
return <Redirect to={url} />;
const XoopsPathRedirect = (props: Props) => {
const { lang } = props;
const location = useLocation();
if (location.pathname === '/') {
return null;
}
}
const url = getRedirectUrl(location.pathname);
if (url === '') {
return <PageNotFound lang={lang} />;
}
return <Navigate to={url} replace />;
};
export default XoopsPathRedirect;
+53
View File
@@ -0,0 +1,53 @@
import type { ComponentProps, MouseEvent } from 'react';
import { Link, type To } from 'react-router';
type LinkProps = ComponentProps<typeof Link>;
const RETRY_INTERVAL_MS = 16;
const RETRY_COUNT = 20;
const hashOf = (to: To): string => {
if (typeof to === 'string') {
const idx = to.indexOf('#');
return idx === -1 ? '' : to.slice(idx + 1);
}
return (to.hash ?? '').replace(/^#/, '');
};
const scrollToHash = (hash: string, remaining = RETRY_COUNT) => {
if (hash === '') {
return;
}
const id = decodeURIComponent(hash);
const target = document.getElementById(id) ?? document.querySelector(`[name="${CSS.escape(id)}"]`);
if (target !== null) {
target.scrollIntoView();
return;
}
// The target may not be in the DOM yet (pico pages fetch their content).
if (remaining > 0) {
setTimeout(() => {
scrollToHash(hash, remaining - 1);
}, RETRY_INTERVAL_MS);
}
};
/**
* A `<Link>` that also scrolls to the element named by the URL fragment.
* Replaces `react-router-hash-link`, which is unmaintained and still targets react-router v5.
*/
const HashLink = ({ onClick, to, ...rest }: LinkProps) => {
const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
onClick?.(e);
if (e.defaultPrevented) {
return;
}
// Let react-router commit the navigation before looking for the target.
setTimeout(() => {
scrollToHash(hashOf(to));
}, 0);
};
return <Link {...rest} to={to} onClick={handleClick} />;
};
export default HashLink;
+13 -11
View File
@@ -1,9 +1,7 @@
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
import { Link } from 'react-router-dom';
import { MultiLang } from '../../config';
import { Link, useLocation } from 'react-router';
import type { MultiLang } from '../../config';
interface Props extends RouteComponentProps {
interface Props {
lang: MultiLang;
className?: string;
}
@@ -15,12 +13,16 @@ const langResources = {
const LangLink = (props: Props) => {
const { lang, className } = props;
const params = new URLSearchParams(props.location.search);
const location = useLocation();
const params = new URLSearchParams(location.search);
const flagLang = lang === 'en' ? 'ja' : 'en';
params.set('ml_lang', flagLang);
const url = props.location.pathname + '?' + params.toString();
return <Link className={className} to={url}><span>{langResources[flagLang]}</span></Link>;
}
const url = `${location.pathname}?${params.toString()}`;
return (
<Link className={className} to={url}>
<span>{langResources[flagLang]}</span>
</Link>
);
};
export default withRouter(LangLink);
export default LangLink;
+27
View File
@@ -0,0 +1,27 @@
.container {
display: flex;
justify-content: center;
align-content: center;
margin: 100px 0;
}
.spinner {
width: 120px;
height: 120px;
border: 8px solid #eeeeee;
border-top-color: #cccccc;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.spinner {
animation-duration: 4s;
}
}
+7 -10
View File
@@ -1,12 +1,9 @@
import React from 'react';
import Spinner from 'react-spinner-material';
import styles from './Loading.module.css';
const Loading = () => {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignContent: 'center', margin: '100px 0' }}>
<Spinner radius={60} color={'#cccccc'} stroke={8} visible={true} />
</div>
);
}
const Loading = () => (
<div className={styles.container}>
<div className={styles.spinner} role="status" aria-label="Loading" />
</div>
);
export default Loading;
export default Loading;
+7 -6
View File
@@ -1,15 +1,16 @@
import React from 'react';
import { MultiLang } from '../../config';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
interface Props {
lang: MultiLang;
}
export const ARCHIVED_NOTICE =
'[en]This site has been archived since FY2019 and is no longer updated.[/en][ja]このサイトは、2019年度よりアーカイブサイトとして運用されています。[/ja]';
const NoticeSiteHasBeenArchived = (props: Props) => {
const { lang } = props;
const notice = '[en]This site has been archived since FY2019 and is no longer updated.[/en][ja]このサイトは、2019年度よりアーカイブサイトとして運用されています。[/ja]';
return <p style={{ color: 'red' }}>{Functions.mlang(notice, lang)}</p>;
}
return <p style={{ color: 'red' }}>{Functions.mlang(ARCHIVED_NOTICE, lang)}</p>;
};
export default NoticeSiteHasBeenArchived;
export default NoticeSiteHasBeenArchived;
+34 -44
View File
@@ -1,54 +1,44 @@
import React, { Component } from 'react';
import { Helmet } from 'react-helmet';
import { RouteComponentProps, withRouter } from 'react-router';
import { MultiLang } from '../../config';
import { Helmet } from '@dr.pogodin/react-helmet';
import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
interface Props extends RouteComponentProps {
interface Props {
lang: MultiLang;
}
class PageNotFound extends Component<Props> {
const TOP_URL = '/';
private url = '/';
private timer: NodeJS.Timer | null = null;
const PageNotFound = (props: Props) => {
const { lang } = props;
const location = useLocation();
const navigate = useNavigate();
goToTopPage() {
if (this.props.location.pathname !== '/') {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this.timer = null;
this.props.history.push(this.url);
}, 5000);
useEffect(() => {
if (location.pathname === TOP_URL) {
return;
}
}
const timer = setTimeout(() => {
void navigate(TOP_URL);
}, 5000);
return () => clearTimeout(timer);
}, [location.pathname, navigate]);
componentWillUnmount() {
if (this.timer) {
clearTimeout(this.timer);
}
}
return (
<div>
<Helmet>
<title>Page Not Found - {Functions.siteTitle(lang)}</title>
</Helmet>
<h1>Page Not Found</h1>
<section>
<p>The page you were trying to access doesn't exist.</p>
<p>
If the page does not automatically reload, please go to the <a href={TOP_URL}>top page</a>
</p>
</section>
</div>
);
};
render() {
const { lang } = this.props;
if (process.env.NODE_ENV === 'production') {
this.goToTopPage();
}
return (
<div>
<Helmet>
<title>Page Not Found - {Functions.siteTitle(lang)}</title>
</Helmet>
<h1>Page Not Found</h1>
<section>
<p>The page you were trying to access doesn't exist.</p>
<p>If the page does not automatically reload, please click <a href={this.url}>here</a></p>
</section>
</div>
);
}
}
export default withRouter(PageNotFound);
export default PageNotFound;
+100 -70
View File
@@ -1,8 +1,14 @@
import React, { ReactElement } from 'react';
import ReactHtmlParser, { convertNodeToElement } from 'react-html-parser';
import { HashLink } from 'react-router-hash-link';
import { MultiLang } from '../../config';
import ReactHtmlParser, {
convertNodeToElement,
type DomElement,
type DomNode,
type Transform,
} from '@orrisroot/react-html-parser';
import type { CSSProperties } from 'react';
import { useLocation } from 'react-router';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
import HashLink from './HashLink';
interface Props {
lang: MultiLang;
@@ -16,102 +22,126 @@ interface Props {
const preConvertXCode = (text: string, doxcode: boolean): string => {
if (doxcode) {
return text.replace(/\[code\](.*)\[\/code\]/sg, (m0, m1) => {
return '[code]' + Functions.base64Encode(m1) + '[/code]';
return text.replace(/\[code\](.*)\[\/code\]/gs, (_m0, m1) => {
return `[code]${Functions.base64Encode(m1)}[/code]`;
});
}
return text;
}
};
const postConvertXCode = (text: string, doxcode: boolean, doimage: boolean): string => {
if (doxcode) {
return text.replace(/\[code\](.*)\[\/code\]/sg, (m0, m1) => {
return text.replace(/\[code\](.*)\[\/code\]/gs, (_m0, m1) => {
const text = convertXCode(Functions.htmlspecialchars(Functions.base64Decode(m1)), doimage);
return '<div class="xoopsCode"><pre><code>' + text + '</code></pre></div>';
return `<div class="xoopsCode"><pre><code>${text}</code></pre></div>`;
});
}
return text;
}
};
const convertClickable = (text: string) => {
text = text.replace(/(^|[^\]_a-zA-Z0-9-="'/]+)((?:https?|ftp)(?::\/\/[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+[a-zA-Z0-9=]))/g, (...matches) => {
return matches[1] + '<a href="' + matches[2] + '" target="_blank" rel="external noopener noreferrer">' + matches[2] + '</a>';
});
text = text.replace(/(^|[^\]_a-zA-Z0-9-="'/:.]+)([a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+)/g, (...matches) => {
return matches[1] + '<a href="mailto:' + matches[2] + '">' + matches[2] + '</a>';
});
text = text.replace(
/(^|[^\]_a-zA-Z0-9-="'/]+)((?:https?|ftp)(?::\/\/[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+[a-zA-Z0-9=]))/g,
(...matches) => {
return (
matches[1] +
'<a href="' +
matches[2] +
'" target="_blank" rel="external noopener noreferrer">' +
matches[2] +
'</a>'
);
}
);
text = text.replace(
/(^|[^\]_a-zA-Z0-9-="'/:.]+)([a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+)/g,
(...matches) => {
return `${matches[1]}<a href="mailto:${matches[2]}">${matches[2]}</a>`;
}
);
return text;
}
};
const convertXCode = (text: string, doimage: boolean): string => {
const convertXCode = (text: string, _doimage: boolean): string => {
// TODO: implement
return text;
}
};
const convertSmiley = (text: string) => {
// TODO: implement
return text;
}
};
const convertBr = (text: string): string => {
return text.replace(/(\r?\n|\r)/g, '<br />');
}
};
interface TransformParsedNode {
type: string;
next: object | null;
prev: object | null;
parent: object | null;
name: string;
attribs: any;
children: object[];
data: string;
}
const cssConvert = (text: string): object => {
const ret: any = {};
const cssConvert = (text: string): CSSProperties => {
const ret: Record<string, string> = {};
text.split(';').forEach((line) => {
const line_ = line.trim();
if (line.length === 0) {
if (line_.length === 0) {
return;
}
const kv = line_.split(':');
const key = Functions.camelCase(kv[0].trim());
const value = kv[1].trim();
ret[key] = value;
})
return ret;
}
const transform = (node: object, idx: number): ReactElement | null | void => {
const node_ = node as TransformParsedNode;
if (node_.type === 'tag' && node_.name === 'a') {
const url = (node_.attribs && node_.attribs['href']) || '/';
const download = (node_.attribs && node_.attribs['download']) || '';
const rel = (node_.attribs && node_.attribs['rel']) || '';
const klass = (node_.attribs && node_.attribs['class']) || '';
const target = (node_.attribs && node_.attribs['target']) || '';
const isFile = download !== '' || /\.(zip|pdf|png|gif|jpg|gz|bz2|xz|mpg|mp3|mp4)$/i.test(url);
const isExternal = /^(_blank|_top|_parent)$/.test(target) || /external/.test(rel) || /external/.test(klass) || /^(mailto|https?:?\/\/)/.test(url);
if (!isFile && !isExternal) {
const style = (node_.attribs && node_.attribs['style']) || '';
const title = (node_.attribs && node_.attribs['title']) || null;
return <HashLink key={idx} to={url} style={cssConvert(style)} title={title}>
{node_.children.map((value: object, index: number) => {
return convertNodeToElement(value, index, transform);
})}
</HashLink>;
if (kv.length < 2) {
return;
}
}
if (node_.type === 'tag' && node_.name === 'img') {
const src = (node_.attribs && node_.attribs['src']) || '';
node_.attribs['src'] = src.replace('`XOOPS_URL`', process.env.PUBLIC_URL);
return convertNodeToElement(node_ as object, idx, transform);
}
}
ret[Functions.camelCase(kv[0].trim())] = kv.slice(1).join(':').trim();
});
return ret as CSSProperties;
};
/**
* Stored content links are written relative to the page that contains them
* (`node4.html`, `manual.html#SECTION1`, ...), so they have to be resolved the
* way a browser resolves an `href`. react-router would instead resolve them
* against the whole current pathname, turning `/manual_ja/node3.html` +
* `node4.html` into `/manual_ja/node3.html/node4.html`.
*/
const resolveUrl = (href: string, basePath: string): string => {
const url = new URL(href, window.location.origin + basePath);
return url.pathname + url.search + url.hash;
};
const makeTransform = (basePath: string): Transform => {
const transform: Transform = (node: DomNode, idx: number | string) => {
if (node.type !== 'tag') {
return undefined;
}
const element = node as DomElement;
if (element.name !== 'a') {
return undefined;
}
const url = element.attribs?.href || '/';
const download = element.attribs?.download || '';
const rel = element.attribs?.rel || '';
const klass = element.attribs?.class || '';
const target = element.attribs?.target || '';
// Files, links opening another window and links marked external leave the app.
const isFile = download !== '' || /\.(zip|pdf|png|gif|jpg|gz|bz2|xz|mpg|mp3|mp4)$/i.test(url);
const isExternal =
/^(_blank|_top|_parent)$/.test(target) ||
/external/.test(rel) ||
/external/.test(klass) ||
/^(mailto|https?:?\/\/)/.test(url);
if (isFile || isExternal) {
return undefined;
}
const style = element.attribs?.style || '';
const title = element.attribs?.title || '';
return (
<HashLink key={idx} to={resolveUrl(url, basePath)} style={cssConvert(style)} title={title}>
{element.children.map((child, index) => convertNodeToElement(child, index, transform))}
</HashLink>
);
};
return transform;
};
const XoopsCode = (props: Props) => {
const { lang } = props;
const { pathname } = useLocation();
let text = props.text;
const dohtml = !!props.dohtml;
const dosmiley = !!props.dosmiley;
@@ -134,7 +164,7 @@ const XoopsCode = (props: Props) => {
}
text = postConvertXCode(text, doxcode, doimage);
text = Functions.mlang(text, lang);
return <>{ReactHtmlParser(text, { transform })}</>;
}
return <>{ReactHtmlParser(text, { transform: makeTransform(pathname) })}</>;
};
export default XoopsCode;
export default XoopsCode;
+22 -3
View File
@@ -1,9 +1,27 @@
const SITE_TITLE = 'INCF Japan Node';
const SITE_SLOGAN = 'Top';
const GOOGLE_ANALYTICS_TRACKING_ID = 'UA-2365647-1';
// Replaces the Universal Analytics property (UA-2365647-1), shut down in 2023.
const GOOGLE_ANALYTICS_TRACKING_ID = 'G-FQK4EVM4WN';
const XOONIPS_ITEMTYPES = ['journal', 'article', 'generaldata'];
const PICO_MODULES = ['aboutus', 'aini', 'hackathon', 'joinus', 'link', 'nbni', 'program', 'publication', 'resource', 'tutorial'];
const PICO_MODULES = [
'aboutus',
'aini',
'hackathon',
'joinus',
'link',
'nbni',
'program',
'publication',
'resource',
'tutorial',
];
const BULLETIN_MODULES = ['news'];
// Vite's BASE_URL always carries a trailing slash ('/' when the app is served
// from the document root). Strip it so it can be concatenated the same way
// CRA's Config.BASE_URL was.
const BASE_URL = import.meta.env.BASE_URL.replace(/\/$/, '');
export type MultiLang = 'en' | 'ja';
const Config = {
@@ -13,6 +31,7 @@ const Config = {
XOONIPS_ITEMTYPES,
PICO_MODULES,
BULLETIN_MODULES,
BASE_URL,
};
export default Config;
export default Config;
+23 -29
View File
@@ -1,7 +1,6 @@
import React from 'react';
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { Navigate, Route, Routes, useLocation } from 'react-router';
import PageNotFound from '../common/lib/PageNotFound';
import { MultiLang } from '../config';
import type { MultiLang } from '../config';
import CreditsIndex from './CreditsIndex';
import CreditsPage from './CreditsPage';
@@ -9,35 +8,30 @@ interface Props {
lang: MultiLang;
}
const CreditsIndexPage = (props: Props) => {
const { lang } = props;
const { search } = useLocation();
const paramId = new URLSearchParams(search).get('id');
if (paramId === null) {
return <CreditsIndex lang={lang} />;
}
if (/^[1-9][0-9]*$/.test(paramId)) {
return <CreditsPage lang={lang} id={parseInt(paramId, 10)} />;
}
return <PageNotFound lang={lang} />;
};
const Credits = (props: Props) => {
const { lang } = props;
const pathname = '/modules/credits';
const { search, hash } = useLocation();
return (
<>
<Switch>
<Route exact path={`${pathname}/index.php`} render={(props: RouteComponentProps) => {
const { location } = props;
const params = new URLSearchParams(location.search);
const paramId = params.get('id');
if (null === paramId) {
return <CreditsIndex lang={lang} />;
}
if (/^[1-9][0-9]*$/.test(paramId)) {
const id = parseInt(paramId, 10);
return <CreditsPage lang={lang} id={id} />;
}
return <PageNotFound lang={lang} />;
}} />
<Route exact path={`${pathname}/aboutus.php`} render={() => <CreditsPage lang={lang} id={0} />} />
<Route exact path={pathname} render={(props: RouteComponentProps) => {
const { location } = props;
const url = pathname + '/index.php' + location.search + location.hash;
return <Redirect to={url} />
}} />
<Route component={PageNotFound} />
</Switch>
</>
<Routes>
<Route path="index.php" element={<CreditsIndexPage lang={lang} />} />
<Route path="aboutus.php" element={<CreditsPage lang={lang} id={0} />} />
<Route index element={<Navigate to={`/modules/credits/index.php${search}${hash}`} replace />} />
<Route path="*" element={<PageNotFound lang={lang} />} />
</Routes>
);
}
};
export default Credits;
+14 -10
View File
@@ -1,12 +1,12 @@
import React, { Component } from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { Helmet } from '@dr.pogodin/react-helmet';
import { Component } from 'react';
import { Link } from 'react-router';
import Loading from '../common/lib/Loading';
import NoticeSiteHasBeenArchived from '../common/lib/NoticeSiteHasBeenArchived';
import PageNotFound from '../common/lib/PageNotFound';
import { MultiLang } from '../config';
import type { MultiLang } from '../config';
import Functions from '../functions';
import CreditsUtils, { CreditsIndexData } from './lib/CreditsUtils';
import CreditsUtils, { type CreditsIndexData } from './lib/CreditsUtils';
interface Props {
lang: MultiLang;
@@ -18,7 +18,6 @@ interface State {
}
class CreditsIndex extends Component<Props, State> {
private isActive: boolean;
constructor(props: Props) {
@@ -35,7 +34,7 @@ class CreditsIndex extends Component<Props, State> {
this.updatePage();
}
componentDidUpdate(prevProps: Props, prevState: State) {
componentDidUpdate(prevProps: Props) {
const { lang } = this.props;
if (prevProps.lang !== lang) {
this.updatePage();
@@ -65,7 +64,9 @@ class CreditsIndex extends Component<Props, State> {
return (
<>
<Helmet>
<title>{mtitle} - {Functions.siteTitle(lang)}</title>
<title>
{mtitle} - {Functions.siteTitle(lang)}
</title>
</Helmet>
<h3>{mtitle}</h3>
<NoticeSiteHasBeenArchived lang={lang} />
@@ -73,8 +74,11 @@ class CreditsIndex extends Component<Props, State> {
{this.state.index.pages.map((page) => {
const title = Functions.mlang(page.title, lang);
const url = CreditsUtils.getPageUrl(page.id);
return <li key={page.id}><Link to={url}>{title}</Link></li>;
return (
<li key={page.id}>
<Link to={url}>{title}</Link>
</li>
);
})}
</ul>
</>
+17 -11
View File
@@ -1,12 +1,12 @@
import React, { Component } from 'react';
import { Helmet } from 'react-helmet';
import { Helmet } from '@dr.pogodin/react-helmet';
import { Component } from 'react';
import Loading from '../common/lib/Loading';
import NoticeSiteHasBeenArchived from '../common/lib/NoticeSiteHasBeenArchived';
import PageNotFound from '../common/lib/PageNotFound';
import XoopsCode from '../common/lib/XoopsCode';
import { MultiLang } from '../config';
import type { MultiLang } from '../config';
import Functions from '../functions';
import CreditsUtils, { CreditsIndexData, CreditsPageDetailData } from './lib/CreditsUtils';
import CreditsUtils, { type CreditsIndexData, type CreditsPageDetailData } from './lib/CreditsUtils';
interface Props {
lang: MultiLang;
@@ -20,7 +20,6 @@ interface State {
}
class CreditsPage extends Component<Props, State> {
private isActive: boolean;
constructor(props: Props) {
@@ -29,7 +28,7 @@ class CreditsPage extends Component<Props, State> {
this.state = {
loading: true,
index: null,
page: null
page: null,
};
}
@@ -38,7 +37,7 @@ class CreditsPage extends Component<Props, State> {
this.updatePage();
}
componentDidUpdate(prevProps: Props, prevState: State) {
componentDidUpdate(prevProps: Props) {
const { lang, id } = this.props;
if (prevProps.lang !== lang || prevProps.id !== id) {
this.updatePage();
@@ -68,19 +67,26 @@ class CreditsPage extends Component<Props, State> {
}
const mtitle = Functions.mlang(this.state.index.name, lang);
const title = Functions.mlang(this.state.page.title, lang);
const lastupdate = this.state.page.lastupdate === 0 ? '' : Functions.formatDate(this.state.page.lastupdate, 'MMMM Do, YYYY');
const lastupdate =
this.state.page.lastupdate === 0 ? '' : Functions.formatDate(this.state.page.lastupdate, 'MMMM do, yyyy');
return (
<>
<Helmet>
<title>{title} - {mtitle} - {Functions.siteTitle(lang)}</title>
<title>
{title} - {mtitle} - {Functions.siteTitle(lang)}
</title>
</Helmet>
<h3>{title}</h3>
<NoticeSiteHasBeenArchived lang={lang} />
{'' !== lastupdate && <div style={{ textAlign: 'right' }}>{Functions.mlang('[en]Last Update[/en][ja]最終更新日[/ja]', lang)} : {lastupdate}</div>}
{'' !== lastupdate && (
<div style={{ textAlign: 'right' }}>
{Functions.mlang('[en]Last Update[/en][ja]最終更新日[/ja]', lang)} : {lastupdate}
</div>
)}
<hr />
<XoopsCode lang={lang} text={this.state.page.content} dohtml={true} />
</>
);
}
}
export default CreditsPage;
export default CreditsPage;
-75
View File
@@ -1,75 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import Loading from '../../common/lib/Loading';
import PageNotFound from '../../common/lib/PageNotFound';
import { MultiLang } from '../../config';
import Functions from '../../functions';
import CreditsUtils, { CreditsMenuData } from '../lib/CreditsUtils';
interface Props {
lang: MultiLang;
}
interface State {
loading: boolean;
menu: CreditsMenuData[] | null;
}
class CreditsMenu extends Component<Props, State> {
private isActive: boolean;
constructor(props: Props) {
super(props);
this.isActive = false;
this.state = {
loading: true,
menu: null
};
}
componentDidMount() {
this.isActive = true;
this.updatePage();
}
componentDidUpdate(prevProps: Props, prevState: State) {
const { lang } = this.props;
if (prevProps.lang !== lang) {
this.updatePage();
}
}
componentWillUnmount() {
this.isActive = false;
}
async updatePage() {
const menu = await CreditsUtils.getMenu();
if (this.isActive) {
this.setState({ loading: false, menu });
}
}
render() {
const { lang } = this.props;
if (this.state.loading) {
return <Loading />;
}
if (null === this.state.menu || this.state.menu.length === 0) {
return <PageNotFound lang={lang} />;
}
const links = this.state.menu.map((item, idx) => {
const title = Functions.mlang(item.title, lang);
const style = idx === 0 ? 'menuTop' : 'menuMain';
return <li key={idx}><Link className={style} to={item.link}>{title}</Link></li>
});
return (
<ul className="mainmenu">
{links}
</ul>
);
}
}
export default CreditsMenu;
+12 -12
View File
@@ -1,4 +1,5 @@
import axios from 'axios';
import ky from 'ky';
import Config from '../../config';
export interface CreditsPageData {
id: number;
@@ -21,7 +22,6 @@ export interface CreditsMenuData {
}
class CreditsUtils {
private pathname: string;
private index?: CreditsIndexData | null;
@@ -30,21 +30,22 @@ class CreditsUtils {
}
getIndexUrl(): string {
return this.pathname + '/';
return `${this.pathname}/`;
}
getPageUrl(id: number): string {
return this.getIndexUrl() + (0 === id ? 'aboutus.php' : ('?id=' + id));
return this.getIndexUrl() + (0 === id ? 'aboutus.php' : `?id=${id}`);
}
async getIndex(): Promise<CreditsIndexData | null> {
if (typeof this.index === 'undefined') {
try {
const url = this.getIndexUrl() + 'index.json';
const response = await axios.get(url);
const index = response.data as CreditsIndexData;
const url = `${this.getIndexUrl()}index.json`;
const index = await ky
.get(new URL(Config.BASE_URL + url, window.location.href))
.json<CreditsIndexData>();
this.index = index;
} catch (err) {
} catch (_err) {
this.index = null;
}
}
@@ -65,10 +66,9 @@ class CreditsUtils {
async getPage(id: number): Promise<CreditsPageDetailData | null> {
let page = null;
try {
const url = this.getIndexUrl() + id + '.json';
const response = await axios.get(url);
page = response.data as CreditsPageDetailData;
} catch (err) {
const url = `${this.getIndexUrl() + id}.json`;
page = await ky.get(new URL(Config.BASE_URL + url, window.location.href)).json<CreditsPageDetailData>();
} catch (_err) {
// ignore
}
return page;
+46 -29
View File
@@ -1,8 +1,7 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { Helmet } from '@dr.pogodin/react-helmet';
import { Link } from 'react-router';
import NoticeSiteHasBeenArchived from '../common/lib/NoticeSiteHasBeenArchived';
import Config, { MultiLang } from '../config';
import Config, { type MultiLang } from '../config';
import Functions from '../functions';
interface Props {
@@ -10,34 +9,52 @@ interface Props {
}
const message = {
ja: <p>
INCF日本ノードの事務局は2005年に理研BSI神経情報基盤センターとして設置され20184CBS神経情報基盤開発ユニットに引き継がれ
CBSにおけるニューロインフォマティクスの新たな方向性を探る中で20193
</p>,
en: <p>
Secretariat of the INCF Japan Node (J-Node) was launched in 2005 as the starting of Neuroinformatics Japan Center, RIKEN Brain Science Institute (BSI).
In April 2018, J-Node secretariat was inherited to Neuroinformatics Unit, RIKEN Center for Brain Science (CBS).
However, based on reconsideration at CBS, closure of J-Node secretariat at the end of FY 2018 has been decided.
Accordingly, the collaborative development of neuroinformatics platforms will finish.
Each J-Node related site is going to be transferred to a new managing form.
</p>
ja: (
<p>
INCF日本ノードの事務局は2005年に理研BSI神経情報基盤センターとして設置され20184CBS神経情報基盤開発ユニットに引き継がれ
CBSにおけるニューロインフォマティクスの新たな方向性を探る中で20193
</p>
),
en: (
<p>
Secretariat of the INCF Japan Node (J-Node) was launched in 2005 as the starting of Neuroinformatics Japan
Center, RIKEN Brain Science Institute (BSI). In April 2018, J-Node secretariat was inherited to
Neuroinformatics Unit, RIKEN Center for Brain Science (CBS). However, based on reconsideration at CBS,
closure of J-Node secretariat at the end of FY 2018 has been decided. Accordingly, the collaborative
development of neuroinformatics platforms will finish. Each J-Node related site is going to be transferred
to a new managing form.
</p>
),
};
const ContactUs: React.FC<Props> = (props: Props) => {
const ContactUs = (props: Props) => {
const { lang } = props;
const title = Functions.mlang('[en]Contact Us[/en][ja]お問い合わせ[/ja]', lang);
return <div>
<Helmet><title>{title + ' - ' + Functions.mlang(Config.SITE_TITLE, lang)}</title></Helmet>
<h1>{title}</h1>
<NoticeSiteHasBeenArchived lang={lang} />
{message[lang]}
<p>
{Functions.mlang('[en]Please see the details[/en][ja]詳細についてはこちらをご覧ください[/ja]', lang)}: <Link to="/modules/news/index.php?page=article&storyid=363">{Functions.mlang('[en]Closure of Secretariat of the INCF Japan Node[/en][ja]INCF日本ノード事務局終了のお知らせ[/ja]', lang)}</Link>
</p>
<p>
{Functions.mlang('[en]Thank you for your understanding.[/en][ja]皆様よりの長年の多大なご支援、深く感謝申し上げます。[/ja]', lang)}
</p>
</div>;
}
return (
<div>
<Helmet>
<title>{`${title} - ${Functions.mlang(Config.SITE_TITLE, lang)}`}</title>
</Helmet>
<h1>{title}</h1>
<NoticeSiteHasBeenArchived lang={lang} />
{message[lang]}
<p>
{Functions.mlang('[en]Please see the details[/en][ja]詳細についてはこちらをご覧ください[/ja]', lang)}:{' '}
<Link to="/modules/news/index.php?page=article&storyid=363">
{Functions.mlang(
'[en]Closure of Secretariat of the INCF Japan Node[/en][ja]INCF日本ノード事務局終了のお知らせ[/ja]',
lang
)}
</Link>
</p>
<p>
{Functions.mlang(
'[en]Thank you for your understanding.[/en][ja]皆様よりの長年の多大なご支援、深く感謝申し上げます。[/ja]',
lang
)}
</p>
</div>
);
};
export default ContactUs;
+35 -12
View File
@@ -1,32 +1,55 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { MultiLang } from '../config';
import { Link } from 'react-router';
import type { MultiLang } from '../config';
import Functions from '../functions';
import imageCbsBanner from './assets/images/cbs-banner.png';
import imageIncfBanner from './assets/images/incf-banner.png';
import imageNiuBanner from './assets/images/niu-banner.png';
import imageRikenBanner from './assets/images/riken-banner.png';
import imageXoonisBanner from './assets/images/xoonips-banner.png';
import Functions from '../functions';
interface Props {
lang: MultiLang;
}
const Footer: React.FC<Props> = (props: Props) => {
const Footer = (props: Props) => {
const { lang } = props;
return (
<footer id="footer">
<ul className="banners">
<li><a href="http://www.riken.jp/" target="_blank" rel="noopener noreferrer"><img src={imageRikenBanner} alt="RIKEN" /></a></li>
<li><a href="https://cbs.riken.jp/" target="_blank" rel="noopener noreferrer"><img src={imageCbsBanner} alt="RIKEN Center for Brain Science" /></a></li>
<li><a href="https://www.ni.riken.jp/" target="_blank" rel="noopener noreferrer"><img src={imageNiuBanner} alt="Neuroinformatics Unit, RIKEN Center for Brain Science" /></a></li>
<li><a href="https://www.incf.org" target="_blank" rel="noopener noreferrer"><img src={imageIncfBanner} alt="International Neuroinformatics Coordinating Facility" /></a></li>
<li><a href="https://xoonips.osdn.jp/" target="_blank" rel="noopener noreferrer"><img src={imageXoonisBanner} alt="XooNIps" /></a></li>
<li><Link className="credits" to="/modules/credits/">{Functions.mlang('[en]Site Information[/en][ja]このサイトについて[/ja]', lang)}</Link></li>
<li>
<a href="http://www.riken.jp/" target="_blank" rel="noopener noreferrer">
<img src={imageRikenBanner} alt="RIKEN" />
</a>
</li>
<li>
<a href="https://cbs.riken.jp/" target="_blank" rel="noopener noreferrer">
<img src={imageCbsBanner} alt="RIKEN Center for Brain Science" />
</a>
</li>
<li>
<a href="https://www.ni.riken.jp/" target="_blank" rel="noopener noreferrer">
<img src={imageNiuBanner} alt="Neuroinformatics Unit, RIKEN Center for Brain Science" />
</a>
</li>
<li>
<a href="https://www.incf.org" target="_blank" rel="noopener noreferrer">
<img src={imageIncfBanner} alt="International Neuroinformatics Coordinating Facility" />
</a>
</li>
<li>
<a href="https://xoonips.osdn.jp/" target="_blank" rel="noopener noreferrer">
<img src={imageXoonisBanner} alt="XooNIps" />
</a>
</li>
<li>
<Link className="credits" to="/modules/credits/">
{Functions.mlang('[en]Site Information[/en][ja]このサイトについて[/ja]', lang)}
</Link>
</li>
</ul>
<span className="copyright">Copyright (C) 2018 Neuroinformatics Unit, RIKEN Center for Brain Science</span>
</footer>
);
}
};
export default Footer;
+174 -111
View File
@@ -1,130 +1,193 @@
import React, { Component } from 'react';
import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
import { type MouseEvent, useState } from 'react';
import { Link, useNavigate } from 'react-router';
import HashLink from '../common/lib/HashLink';
import LangLink from '../common/lib/LangLink';
import Config, { MultiLang } from '../config';
import Config, { type MultiLang } from '../config';
import Functions from '../functions';
import ItemUtil from '../xoonips/lib/ItemUtil';
import imageFacebookIcon from './assets/images/facebook-icon.png';
import imageFlickrIcon from './assets/images/flickr-icon.png';
import imageIncfLogo from './assets/images/incf-logo.png';
import imageSearchButton from './assets/images/search.png';
import imageSitemapIcon from './assets/images/sitemap.png';
import imageTwitterIcon from './assets/images/twitter-icon.png';
import imageSearchButton from './assets/images/search.png';
import { HashLink } from 'react-router-hash-link';
interface Props extends RouteComponentProps {
interface Props {
lang: MultiLang;
}
interface State {
keyword: string;
menuid: number;
}
const Header = (props: Props) => {
const { lang } = props;
const navigate = useNavigate();
const [keyword, setKeyword] = useState('');
const [menuid, setMenuid] = useState(0);
class Header extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
keyword: '',
menuid: 0
};
this.handleChangeKeyword = this.handleChangeKeyword.bind(this);
this.handleOnClick = this.handleOnClick.bind(this);
}
handleChangeKeyword(event: React.ChangeEvent<HTMLInputElement>) {
const keyword = event.target.value;
this.setState({ keyword });
}
handleOnClick(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
const handleOnClick = (event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
const keyword = this.state.keyword.trim();
if (keyword !== '') {
const url = ItemUtil.getSearchByKeywordUrl('all', this.state.keyword);
this.props.history.push(url);
const trimmed = keyword.trim();
if (trimmed !== '') {
void navigate(ItemUtil.getSearchByKeywordUrl('all', keyword));
}
}
};
setMenuId(menuid: number) {
this.setState({ menuid });
}
render() {
const { lang } = this.props;
return (
<header id="header">
<div className="top">
<Link className="logo" to="/"><span className="hidden">{Config.SITE_TITLE}</span></Link>
<div className="utility">
<div className="socialmedia">
<a href="https://twitter.com/neuroinf_jp" target="_blank" rel="noopener noreferrer"><img src={imageTwitterIcon} alt="Twitter" title="Twitter" /></a>
<a href="https://www.facebook.com/INCFJapanNode" target="_blank" rel="noopener noreferrer"><img src={imageFacebookIcon} alt="Facebook" title="Fackbook" /></a>
<a href="https://www.flickr.com/photos/137465753@N08/albums" target="_blank" rel="noopener noreferrer"><img src={imageFlickrIcon} alt="Flickr" title="Flickr" /></a>
</div>
<Link className="sitemap" to="/sitemap"><img src={imageSitemapIcon} alt="Sitemap" title="Sitemap" /></Link>
<span className="language"><LangLink lang={lang} /></span>
<a className="incf" href="https://www.incf.org/" target="_blank" rel="noopener noreferrer"><img src={imageIncfLogo} alt="INCF - International Neuroinformatics Coordinating Facility" title="INCF - International Neuroinformaitcs Coordinating Facility" /></a>
return (
<header id="header">
<div className="top">
<Link className="logo" to="/">
<span className="hidden">{Config.SITE_TITLE}</span>
</Link>
<div className="utility">
<div className="socialmedia">
<a href="https://twitter.com/neuroinf_jp" target="_blank" rel="noopener noreferrer">
<img src={imageTwitterIcon} alt="Twitter" title="Twitter" />
</a>
<a href="https://www.facebook.com/INCFJapanNode" target="_blank" rel="noopener noreferrer">
<img src={imageFacebookIcon} alt="Facebook" title="Fackbook" />
</a>
<a
href="https://www.flickr.com/photos/137465753@N08/albums"
target="_blank"
rel="noopener noreferrer"
>
<img src={imageFlickrIcon} alt="Flickr" title="Flickr" />
</a>
</div>
<Link className="sitemap" to="/sitemap">
<img src={imageSitemapIcon} alt="Sitemap" title="Sitemap" />
</Link>
<span className="language">
<LangLink lang={lang} />
</span>
<a className="incf" href="https://www.incf.org/" target="_blank" rel="noopener noreferrer">
<img
src={imageIncfLogo}
alt="INCF - International Neuroinformatics Coordinating Facility"
title="INCF - International Neuroinformaitcs Coordinating Facility"
/>
</a>
</div>
<nav className="menubar">
<ul className="mainmenu">
<li><Link to="/">{Functions.mlang('[en]Home[/en][ja]ホーム[/ja]', lang)}</Link></li>
<li onMouseEnter={() => this.setMenuId(1)} onMouseLeave={() => this.setMenuId(0)}><span>{Functions.mlang('[en]About us[/en][ja]J-Node について[/ja]', lang)}</span>
{1 === this.state.menuid &&
<div className="submenu">
<Link className="item" to="/aboutus/incf.html">{Functions.mlang('[en]INCF[/en][ja]ニューロインフォマティクス国際統合機構 INCF[/ja]', lang)}</Link>
<Link className="item" to="/aboutus/j-node.html">{Functions.mlang('[en]INCF Jpan Node[/en][ja]INCF 日本ノード[/ja]', lang)}</Link>
<Link className="item" to="/joinus/index.html">{Functions.mlang('[en]Join Us[/en][ja]参加方法[/ja]', lang)}</Link>
<Link className="item" to="/aboutus/history.html">{Functions.mlang('[en]History[/en][ja]沿革[/ja]', lang)}</Link>
</div>
}
</li>
<li onMouseEnter={() => this.setMenuId(2)} onMouseLeave={() => this.setMenuId(0)}><span>{Functions.mlang('[en]News and Topics[/en][ja]ニュース[/ja]', lang)}</span>
{2 === this.state.menuid &&
<div className="submenu">
<Link className="item" to="/news/index.php?storytopic=2">{Functions.mlang('[en]Topics/Articles[/en][ja]トピックス[/ja]', lang)}</Link>
<Link className="item" to="/news/index.php?storytopic=3">{Functions.mlang('[en]Meeting/Event[/en][ja]会議/イベント[/ja]', lang)}</Link>
<Link className="item" to="/news/index.php?storytopic=7">{Functions.mlang('[en]From J-Node Secretariat[/en][ja]事務局からのお知らせ[/ja]', lang)}</Link>
<Link className="item" to="/news/index.php?storytopic=4">{Functions.mlang('[en]Newsletters[/en][ja]ニュースレター[/ja]', lang)}</Link>
<Link className="item" to="/news/index.php?storytopic=5">{Functions.mlang('[en]J-Node Update[/en][ja]J-Nodeサイトの更新情報[/ja]', lang)}</Link>
</div>
}
</li>
<li onMouseEnter={() => this.setMenuId(3)} onMouseLeave={() => this.setMenuId(0)}><span>{Functions.mlang('[en]Resources[/en][ja]リソース[/ja]', lang)}</span>
{3 === this.state.menuid &&
<div className="submenu">
<Link className="item" to="/resource/platform.html">{Functions.mlang('[en]J-Node Platform[/en][ja]日本ノードプラットフォーム[/ja]', lang)}</Link>
<HashLink className="item" to="/resource/platform.html#link-pf-update">{Functions.mlang('[en]Platform Updates[/en][ja]プラットフォーム更新情報[/ja]', lang)}</HashLink>
<Link className="item" to="/resource/software.html">{Functions.mlang('[en]Infrastructure Software[/en][ja]基盤ソフトウェア[/ja]', lang)}</Link>
<Link className="item" to="/publication/index.html">{Functions.mlang('[en]Publication[/en][ja]出版物・刊行物[/ja]', lang)}</Link>
<Link className="item" to="/tutorial/index.html">{Functions.mlang('[en]Tutorial[/en][ja]チュートリアル[/ja]', lang)}</Link>
<Link className="item" to="/link/index.html">{Functions.mlang('[en]Related Link[/en][ja]関連リンク[/ja]', lang)}</Link>
</div>
}
</li>
<li onMouseEnter={() => this.setMenuId(4)} onMouseLeave={() => this.setMenuId(0)}><span>{Functions.mlang('[en]Program[/en][ja]プログラム[/ja]', lang)}</span>
{4 === this.state.menuid &&
<div className="submenu">
<Link className="item" to="/program/nijc-program.html">{Functions.mlang('[en]Overview[/en][ja]プログラム概要[/ja]', lang)}</Link>
<Link className="item" to="/program/platforms.html">{Functions.mlang('[en]Neuroinformatics Platforms[/en][ja]プラットフォーム開発運用[/ja]', lang)}</Link>
<Link className="item" to="/program/j-node-hackathon.html">{Functions.mlang('[en]Hackathon[/en][ja]ハッカソン[/ja]', lang)}</Link>
<Link className="item" to="/program/workshop-aini.html">{Functions.mlang('[en]Workshop AINI[/en][ja]国債ワークショップAINI[/ja]', lang)}</Link>
<Link className="item" to="/program/doi.html">{Functions.mlang('[en]DOI[/en][ja]DOI[/ja]', lang)}</Link>
</div>
}
</li>
<li><Link to="/contactus">{Functions.mlang('[en]Contact Us[/en][ja]お問い合わせ[/ja]', lang)}</Link></li>
</ul>
<div className="search_box">
<input type="text" value={this.state.keyword} onChange={this.handleChangeKeyword} />
<a href="/" onClick={this.handleOnClick}><img src={imageSearchButton} alt="Search" /></a>
</div>
</nav>
</header>
);
</div>
<nav className="menubar">
<ul className="mainmenu">
<li>
<Link to="/">{Functions.mlang('[en]Home[/en][ja]ホーム[/ja]', lang)}</Link>
</li>
<li onMouseEnter={() => setMenuid(1)} onMouseLeave={() => setMenuid(0)}>
<span>{Functions.mlang('[en]About us[/en][ja]J-Node について[/ja]', lang)}</span>
{1 === menuid && (
<div className="submenu">
<Link className="item" to="/aboutus/incf.html">
{Functions.mlang(
'[en]INCF[/en][ja]ニューロインフォマティクス国際統合機構 INCF[/ja]',
lang
)}
</Link>
<Link className="item" to="/aboutus/j-node.html">
{Functions.mlang('[en]INCF Jpan Node[/en][ja]INCF 日本ノード[/ja]', lang)}
</Link>
<Link className="item" to="/joinus/index.html">
{Functions.mlang('[en]Join Us[/en][ja]参加方法[/ja]', lang)}
</Link>
<Link className="item" to="/aboutus/history.html">
{Functions.mlang('[en]History[/en][ja]沿革[/ja]', lang)}
</Link>
</div>
)}
</li>
<li onMouseEnter={() => setMenuid(2)} onMouseLeave={() => setMenuid(0)}>
<span>{Functions.mlang('[en]News and Topics[/en][ja]ニュース[/ja]', lang)}</span>
{2 === menuid && (
<div className="submenu">
<Link className="item" to="/news/index.php?storytopic=2">
{Functions.mlang('[en]Topics/Articles[/en][ja]トピックス[/ja]', lang)}
</Link>
<Link className="item" to="/news/index.php?storytopic=3">
{Functions.mlang('[en]Meeting/Event[/en][ja]会議/イベント[/ja]', lang)}
</Link>
<Link className="item" to="/news/index.php?storytopic=7">
{Functions.mlang(
'[en]From J-Node Secretariat[/en][ja]事務局からのお知らせ[/ja]',
lang
)}
</Link>
<Link className="item" to="/news/index.php?storytopic=4">
{Functions.mlang('[en]Newsletters[/en][ja]ニュースレター[/ja]', lang)}
</Link>
<Link className="item" to="/news/index.php?storytopic=5">
{Functions.mlang('[en]J-Node Update[/en][ja]J-Nodeサイトの更新情報[/ja]', lang)}
</Link>
</div>
)}
</li>
<li onMouseEnter={() => setMenuid(3)} onMouseLeave={() => setMenuid(0)}>
<span>{Functions.mlang('[en]Resources[/en][ja]リソース[/ja]', lang)}</span>
{3 === menuid && (
<div className="submenu">
<Link className="item" to="/resource/platform.html">
{Functions.mlang(
'[en]J-Node Platform[/en][ja]日本ノードプラットフォーム[/ja]',
lang
)}
</Link>
<HashLink className="item" to="/resource/platform.html#link-pf-update">
{Functions.mlang(
'[en]Platform Updates[/en][ja]プラットフォーム更新情報[/ja]',
lang
)}
</HashLink>
<Link className="item" to="/resource/software.html">
{Functions.mlang('[en]Infrastructure Software[/en][ja]基盤ソフトウェア[/ja]', lang)}
</Link>
<Link className="item" to="/publication/index.html">
{Functions.mlang('[en]Publication[/en][ja]出版物・刊行物[/ja]', lang)}
</Link>
<Link className="item" to="/tutorial/index.html">
{Functions.mlang('[en]Tutorial[/en][ja]チュートリアル[/ja]', lang)}
</Link>
<Link className="item" to="/link/index.html">
{Functions.mlang('[en]Related Link[/en][ja]関連リンク[/ja]', lang)}
</Link>
</div>
)}
</li>
<li onMouseEnter={() => setMenuid(4)} onMouseLeave={() => setMenuid(0)}>
<span>{Functions.mlang('[en]Program[/en][ja]プログラム[/ja]', lang)}</span>
{4 === menuid && (
<div className="submenu">
<Link className="item" to="/program/nijc-program.html">
{Functions.mlang('[en]Overview[/en][ja]プログラム概要[/ja]', lang)}
</Link>
<Link className="item" to="/program/platforms.html">
{Functions.mlang(
'[en]Neuroinformatics Platforms[/en][ja]プラットフォーム開発運用[/ja]',
lang
)}
</Link>
<Link className="item" to="/program/j-node-hackathon.html">
{Functions.mlang('[en]Hackathon[/en][ja]ハッカソン[/ja]', lang)}
</Link>
<Link className="item" to="/program/workshop-aini.html">
{Functions.mlang('[en]Workshop AINI[/en][ja]国債ワークショップAINI[/ja]', lang)}
</Link>
<Link className="item" to="/program/doi.html">
{Functions.mlang('[en]DOI[/en][ja]DOI[/ja]', lang)}
</Link>
</div>
)}
</li>
<li>
<Link to="/contactus">{Functions.mlang('[en]Contact Us[/en][ja]お問い合わせ[/ja]', lang)}</Link>
</li>
</ul>
<div className="search_box">
<input type="text" value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<a href="/" onClick={handleOnClick}>
<img src={imageSearchButton} alt="Search" />
</a>
</div>
</nav>
</header>
);
};
}
}
export default withRouter(Header);
export default Header;
+168 -152
View File
@@ -1,8 +1,8 @@
import React from 'react';
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom';
import type { MouseEvent } from 'react';
import { Navigate, Route, Routes, useLocation } from 'react-router';
import Bulletin from '../bulletin/Bulletin';
import XoopsPathRedirect from '../common/XoopsPathRedirect';
import Config, { MultiLang } from '../config';
import Config, { type MultiLang } from '../config';
import Credits from '../credits/Credits';
import Functions from '../functions';
import Pico from '../pico/Pico';
@@ -26,157 +26,173 @@ const hackathonPathes = ['hackathon2018', 'brainhack-global-2017', 'hackathon201
const ainiPathes = ['2014', '2015', '2016', '2017', '2018'];
const nbniPathes = ['2014', '2017'];
const Page: React.FC<Props> = (props: Props) => {
// `/modules/<dirname>/...` addresses of the pico and bulletin modules are served without the prefix.
const ModulePathRedirect = () => {
const { pathname, search, hash } = useLocation();
return <Navigate to={pathname.replace(/^\/modules/, '') + search + hash} replace />;
};
const XoonipsPage = (props: Props) => {
const { lang } = props;
const goPageTop = Functions.mlang('[en]Go Page Top[/en][ja]ページの先頭へ[/ja]', lang);
return <div id="page">
<Header lang={lang} />
<div id="main">
<Switch>
<Route exact path="/" render={() => <TopPage lang={lang} />} />
<Route exact path="/contactus" render={() => <ContactUs lang={lang} />} />
<Route exact path="/sitemap" render={() => <Sitemap lang={lang} />} />
<Route exact path="/modules/xsearch" render={() => <Xsearch lang={lang} />} />
<Route path="/modules/credits" render={() => <Credits lang={lang} />} />
{ainiPathes.map((name) =>
<Route key={name} path={`/aini${name}`} render={() => <Pico lang={lang} dirname="aini" virtualpath={`/aini`} showTitle={false} />} />
)}
{nbniPathes.map((name) =>
<Route key={name} path={`/nbni${name}`} render={() => <Pico lang={lang} dirname="nbni" virtualpath={`/nbni`} showTitle={false} />} />
)}
{hackathonPathes.map((name) =>
<Route key={name} path={`/${name}`} render={() => <Pico lang={lang} dirname="hackathon" virtualpath="" showTitle={false} />} />
)}
{Config.PICO_MODULES.map((name) =>
<Route key={name} path={`/${name}`} render={() => <Pico lang={lang} dirname={name} virtualpath={`/${name}/`} showTitle={false} />} />
)}
{Config.PICO_MODULES.map((name) =>
<Route key={name} path={'/modules/' + name} render={(props: RouteComponentProps) => {
const { pathname, search, hash } = props.history.location;
const url = pathname.replace(/^\/modules/, '') + search + hash;
return <Redirect to={url} />;
}} />
)}
{Config.BULLETIN_MODULES.map((name) =>
<Route key={name} path={`/${name}`} render={() => <Bulletin lang={lang} mydirname={name} />} />
)}
{Config.BULLETIN_MODULES.map((name) =>
<Route key={name} path={'/modules/' + name} render={(props: RouteComponentProps) => {
const { pathname, search, hash } = props.history.location;
const url = pathname.replace(/^\/modules/, '') + search + hash;
return <Redirect to={url} />;
}} />
)}
<Route path="/modules/xoonips" render={() => {
return <div className="col2">
<div className="leftcolumn">
<div className="block">
<div className="blockTitle">
Item Search
</div>
<div className="blockContent">
<XooNIpsSearchBlock lang={lang} />
</div>
</div>
<div className="block">
<div className="blockTitle">
Index Tree
</div>
<div className="blockContent">
<XooNIpsIndexTreeBlock lang={lang} />
</div>
</div>
</div>
<div className="centercolumn"><Xoonips lang={lang} /> </div>
</div>;
}} />
<Route component={XoopsPathRedirect} />
</Switch>
<Switch>
<Route exact path="/resource/platform.html" render={() =>
<>
<h2 id="link-pf-update">{Functions.mlang('[en]Platform Updates[/en][ja]プラットフォーム更新情報[/ja]', lang)}</h2>
<div className="centercolumn">
<div className="centerLcolumn">
<div className="block">
<div className="blockTitle">
{Functions.mlang('Neuro-Imaging Platform', lang)}
</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.nimg]} num={2} />
</div>
</div>
<div className="block">
<div className="blockTitle">
{Functions.mlang('Dynamic Brain Platform', lang)}
</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.dynamicbrain]} num={2} />
</div>
</div>
<div className="block">
<div className="blockTitle">
{Functions.mlang('Brain Transcriptome Database', lang)}
</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.cdtdb]} num={2} />
</div>
</div>
<div className="block">
<div className="blockTitle">
{Functions.mlang('Invertebrate Brain Platform', lang)}
</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.invbrain]} num={2} />
</div>
</div>
</div>
<div className="centerRcolumn">
<div className="block">
<div className="blockTitle">
{Functions.mlang('Visiome Platform', lang)}
</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.visiome]} num={2} />
</div>
</div>
<div className="block">
<div className="blockTitle">
{Functions.mlang('Cerebellar Platform', lang)}
</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.cerebellum]} num={2} />
</div>
</div>
<div className="block">
<div className="blockTitle">
{Functions.mlang('Mouse Phenotype Database', lang)}
</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.mpdb]} num={2} />
</div>
</div>
<div className="block">
<div className="blockTitle">
{Functions.mlang('Brain Machine Interface Platform', lang)}
</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.bmi]} num={2} />
</div>
</div>
</div>
</div>
</>
} />
</Switch>
<div className="goPageTop">
<a href="/" onClick={(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { e.preventDefault(); window.scrollTo(0, 0); }}>{goPageTop}</a>
return (
<div className="col2">
<div className="leftcolumn">
<div className="block">
<div className="blockTitle">Item Search</div>
<div className="blockContent">
<XooNIpsSearchBlock lang={lang} />
</div>
</div>
<div className="block">
<div className="blockTitle">Index Tree</div>
<div className="blockContent">
<XooNIpsIndexTreeBlock lang={lang} />
</div>
</div>
</div>
<div className="centercolumn">
<Xoonips lang={lang} />{' '}
</div>
</div>
<Footer lang={lang} />
</div>;
}
);
};
const PlatformUpdates = (props: Props) => {
const { lang } = props;
return (
<>
<h2 id="link-pf-update">
{Functions.mlang('[en]Platform Updates[/en][ja]プラットフォーム更新情報[/ja]', lang)}
</h2>
<div className="centercolumn">
<div className="centerLcolumn">
<div className="block">
<div className="blockTitle">{Functions.mlang('Neuro-Imaging Platform', lang)}</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.nimg]} num={2} />
</div>
</div>
<div className="block">
<div className="blockTitle">{Functions.mlang('Dynamic Brain Platform', lang)}</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.dynamicbrain]} num={2} />
</div>
</div>
<div className="block">
<div className="blockTitle">{Functions.mlang('Brain Transcriptome Database', lang)}</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.cdtdb]} num={2} />
</div>
</div>
<div className="block">
<div className="blockTitle">{Functions.mlang('Invertebrate Brain Platform', lang)}</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.invbrain]} num={2} />
</div>
</div>
</div>
<div className="centerRcolumn">
<div className="block">
<div className="blockTitle">{Functions.mlang('Visiome Platform', lang)}</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.visiome]} num={2} />
</div>
</div>
<div className="block">
<div className="blockTitle">{Functions.mlang('Cerebellar Platform', lang)}</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.cerebellum]} num={2} />
</div>
</div>
<div className="block">
<div className="blockTitle">{Functions.mlang('Mouse Phenotype Database', lang)}</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.mpdb]} num={2} />
</div>
</div>
<div className="block">
<div className="blockTitle">{Functions.mlang('Brain Machine Interface Platform', lang)}</div>
<div className="blockContent">
<RssBlock lang={lang} sites={[RssFeedSites.bmi]} num={2} />
</div>
</div>
</div>
</div>
</>
);
};
const Page = (props: Props) => {
const { lang } = props;
const { pathname } = useLocation();
const goPageTop = Functions.mlang('[en]Go Page Top[/en][ja]ページの先頭へ[/ja]', lang);
return (
<div id="page">
<Header lang={lang} />
<div id="main">
<Routes>
<Route path="/" element={<TopPage lang={lang} />} />
<Route path="/contactus" element={<ContactUs lang={lang} />} />
<Route path="/sitemap" element={<Sitemap lang={lang} />} />
<Route path="/modules/xsearch" element={<Xsearch lang={lang} />} />
<Route path="/modules/credits/*" element={<Credits lang={lang} />} />
{ainiPathes.map((name) => (
<Route
key={`aini${name}`}
path={`/aini${name}/*`}
element={<Pico lang={lang} dirname="aini" virtualpath="/aini" showTitle={false} />}
/>
))}
{nbniPathes.map((name) => (
<Route
key={`nbni${name}`}
path={`/nbni${name}/*`}
element={<Pico lang={lang} dirname="nbni" virtualpath="/nbni" showTitle={false} />}
/>
))}
{hackathonPathes.map((name) => (
<Route
key={name}
path={`/${name}/*`}
element={<Pico lang={lang} dirname="hackathon" virtualpath="" showTitle={false} />}
/>
))}
{Config.PICO_MODULES.map((name) => (
<Route
key={name}
path={`/${name}/*`}
element={<Pico lang={lang} dirname={name} virtualpath={`/${name}/`} showTitle={false} />}
/>
))}
{Config.PICO_MODULES.map((name) => (
<Route key={`modules-${name}`} path={`/modules/${name}/*`} element={<ModulePathRedirect />} />
))}
{Config.BULLETIN_MODULES.map((name) => (
<Route key={name} path={`/${name}/*`} element={<Bulletin lang={lang} mydirname={name} />} />
))}
{Config.BULLETIN_MODULES.map((name) => (
<Route key={`modules-${name}`} path={`/modules/${name}/*`} element={<ModulePathRedirect />} />
))}
<Route path="/modules/xoonips/*" element={<XoonipsPage lang={lang} />} />
<Route path="*" element={<XoopsPathRedirect lang={lang} />} />
</Routes>
{pathname === '/resource/platform.html' && <PlatformUpdates lang={lang} />}
<div className="goPageTop">
<a
href="/"
onClick={(e: MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
window.scrollTo(0, 0);
}}
>
{goPageTop}
</a>
</div>
</div>
<Footer lang={lang} />
</div>
);
};
export default Page;
+24 -33
View File
@@ -9,95 +9,86 @@ import imageRssInvbrain from './assets/images/rss-invbrain.png';
import imageRssMpdb from './assets/images/rss-mpdb.png';
import imageRssNimg from './assets/images/rss-nimg.png';
import imageRssVisiome from './assets/images/rss-visiome.png';
import { RssFeedSite } from './lib/RssFeed';
import type { RssFeedSite } from './lib/RssFeed';
const RssFeedSites: { [key: string]: RssFeedSite } = {
'incf': {
incf: {
id: 'incf',
name: 'INCF Neuroinformatics Portal',
logo: imageRssIncf,
url: 'https://www.incf.org/',
rss: 'https://www.incf.org/news/feed'
rss: 'https://www.incf.org/news/feed',
},
'visiome':
{
visiome: {
id: 'visiome',
name: 'Visiome Platform',
logo: imageRssVisiome,
url: 'https://visiome.neuroinf.jp/',
rss: 'https://visiome.neuroinf.jp/rss.xml'
rss: 'https://visiome.neuroinf.jp/rss.xml',
},
'bmi':
{
bmi: {
id: 'bmi',
name: 'Brain Machine Interface Platform',
logo: imageRssBmi,
url: 'https://bmi.neuroinf.jp/',
rss: 'https://bmi.neuroinf.jp/researchers/rss.xml'
rss: 'https://bmi.neuroinf.jp/researchers/rss.xml',
},
'invbrain':
{
invbrain: {
id: 'invbrain',
name: 'Invertebrate Brain Platform',
logo: imageRssInvbrain,
url: 'https://invbrain.neuroinf.jp/',
rss: 'https://invbrain.neuroinf.jp/rss.xml'
rss: 'https://invbrain.neuroinf.jp/rss.xml',
},
'cbsn':
{
cbsn: {
id: 'cbsn',
name: 'Comprehensive Brain Science Network Platform',
logo: imageRssCbsn,
url: 'https://cbsn.neuroinf.jp/',
rss: 'https://cbsn.neuroinf.jp/rss.xml'
rss: 'https://cbsn.neuroinf.jp/rss.xml',
},
'cdtdb':
{
cdtdb: {
id: 'cdtdb',
name: 'Brain Transcriptome Database',
logo: imageRssCdtdb,
url: 'http://www.cdtdb.neuroinf.jp/',
rss: 'http://www.cdtdb.neuroinf.jp/CDT/cdtdb.xml'
rss: 'http://www.cdtdb.neuroinf.jp/CDT/cdtdb.xml',
},
'cerebellum':
{
cerebellum: {
id: 'cerebellum',
name: 'Cerebellar Platform',
logo: imageRssCerebellum,
url: 'https://cerebellum.neuroinf.jp/',
rss: 'https://cerebellum.neuroinf.jp/rss.xml'
rss: 'https://cerebellum.neuroinf.jp/rss.xml',
},
'nimg': {
nimg: {
id: 'nimg',
name: 'Neuro-Imaging Platform',
logo: imageRssNimg,
url: 'https://nimg.neuroinf.jp/',
rss: 'https://nimg.neuroinf.jp/rss.xml'
rss: 'https://nimg.neuroinf.jp/rss.xml',
},
'dynamicbrain':
{
dynamicbrain: {
id: 'dynamicbrain',
name: 'Dynamic Brain Platform',
logo: imageRssDynamicbrain,
url: 'https://dynamicbrain.neuroinf.jp/',
rss: 'https://dynamicbrain.neuroinf.jp/rss.xml'
rss: 'https://dynamicbrain.neuroinf.jp/rss.xml',
},
'mpdb':
{
mpdb: {
id: 'mpdb',
name: 'Mouse Phenotype Database',
logo: imageRssMpdb,
url: 'http://www.mouse-phenotype.org/',
rss: 'http://www.mouse-phenotype.org/index.xml'
rss: 'http://www.mouse-phenotype.org/index.xml',
},
'bsi-ni':
{
'bsi-ni': {
id: 'bsi-ni',
name: 'BSI-Neuroinfomatics',
logo: imageRssBsiNi,
url: 'https://bsi-ni.brain.riken.jp/',
rss: 'https://bsi-ni.brain.riken.jp/index.xml'
}
rss: 'https://bsi-ni.brain.riken.jp/index.xml',
},
};
export default RssFeedSites;
+41 -23
View File
@@ -1,8 +1,8 @@
import React, { ReactNode } from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { Helmet } from '@dr.pogodin/react-helmet';
import type { ReactNode } from 'react';
import { Link } from 'react-router';
import NoticeSiteHasBeenArchived from '../common/lib/NoticeSiteHasBeenArchived';
import Config, { MultiLang } from '../config';
import Config, { type MultiLang } from '../config';
import Functions from '../functions';
import imageSitemapHome from './assets/images/sitemap-home.png';
import imageSitemapNode from './assets/images/sitemap-node.png';
@@ -25,31 +25,49 @@ interface Props {
lang: MultiLang;
}
const Sitemap: React.FC<Props> = (props: Props) => {
const Sitemap = (props: Props) => {
const { lang } = props;
const title = Functions.mlang('[en]Sitemap[/en][ja]サイトマップ[/ja]', lang);
const sitemap = sitemapData as SitemapData;
const makeLink = (item: SitemapNodeData): ReactNode => {
const label = Functions.mlang(item.label, lang);
return (item.link === '') ? label : <Link to={item.link}>{label}</Link>
return item.link === '' ? label : <Link to={item.link}>{label}</Link>;
};
return <div>
<Helmet><title>{title + ' - ' + Functions.mlang(Config.SITE_TITLE, lang)}</title></Helmet>
<h1>{title}</h1>
<NoticeSiteHasBeenArchived lang={lang} />
<div className="sitemap">
<div style={{ textAlign: 'right' }}><img src={imageSitemapHome} alt="Sitemap Home" /> <Link to="/">{Functions.mlang('[en]Home[/en][ja]ホーム[/ja]', lang)}</Link></div>
{sitemap.map((item, i) => {
return <div key={'r' + i} style={{ width: '310px', margin: '10px', float: 'left', whiteSpace: 'nowrap' }}>
<div><img src={imageSitemapRoot} alt="Sitemap Root" /> {makeLink(item.root)}</div>
{item.nodes.map((node, j) => {
return <div key={'n' + j}><img src={imageSitemapNode} alt="Sitemap Node" /> {makeLink(node)}</div>;
})}
</div>;
})}
<div style={{ clear: 'both' }}>&nbsp;</div>
return (
<div>
<Helmet>
<title>{`${title} - ${Functions.mlang(Config.SITE_TITLE, lang)}`}</title>
</Helmet>
<h1>{title}</h1>
<NoticeSiteHasBeenArchived lang={lang} />
<div className="sitemap">
<div style={{ textAlign: 'right' }}>
<img src={imageSitemapHome} alt="Sitemap Home" />{' '}
<Link to="/">{Functions.mlang('[en]Home[/en][ja]ホーム[/ja]', lang)}</Link>
</div>
{sitemap.map((item) => {
return (
<div
key={item.root.label}
style={{ width: '310px', margin: '10px', float: 'left', whiteSpace: 'nowrap' }}
>
<div>
<img src={imageSitemapRoot} alt="Sitemap Root" /> {makeLink(item.root)}
</div>
{item.nodes.map((node) => {
return (
<div key={`${node.label}${node.link}`}>
<img src={imageSitemapNode} alt="Sitemap Node" /> {makeLink(node)}
</div>
);
})}
</div>
);
})}
<div style={{ clear: 'both' }}>&nbsp;</div>
</div>
</div>
</div>;
}
);
};
export default Sitemap;
+47 -46
View File
@@ -1,75 +1,76 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { Helmet } from '@dr.pogodin/react-helmet';
import BulletinRecentNews from '../bulletin/blocks/BulletinRecentNews';
import Config, { MultiLang } from '../config';
import Config, { type MultiLang } from '../config';
import Functions from '../functions';
import imageMainBanner from './assets/images/main-banner.png';
import ArchiveNotice from './blocks/ArchiveNotice';
import LinkBannersBlock from './blocks/LinkBannersBlock';
import RssBlock from './blocks/RssBlock';
import { RssFeedSite } from './lib/RssFeed';
import type { RssFeedSite } from './lib/RssFeed';
import RssFeedSites from './RssFeedSites';
interface Props {
lang: MultiLang;
}
const RSS_UPDATES_INCF: RssFeedSite[] = [
RssFeedSites.incf
];
const RSS_UPDATES_PLATFORMS: RssFeedSite[] = [
RssFeedSites.mpdb,
RssFeedSites.cdtdb,
RssFeedSites.nimg
];
const RSS_UPDATES_INCF: RssFeedSite[] = [RssFeedSites.incf];
const RSS_UPDATES_PLATFORMS: RssFeedSite[] = [RssFeedSites.mpdb, RssFeedSites.cdtdb, RssFeedSites.nimg];
const TopPage: React.FC<Props> = (props: Props) => {
const TopPage = (props: Props) => {
const { lang } = props;
const title = Functions.mlang(Config.SITE_SLOGAN, lang) + ' - ' + Functions.mlang(Config.SITE_TITLE, lang);
return <div>
<Helmet><title>{title}</title></Helmet>
<div><img src={imageMainBanner} alt="main banner" /></div>
<div className="col2">
<div className="leftcolumn">
<div className="block">
<div className="blockContent">
<LinkBannersBlock lang={lang} />
</div>
</div>
const title = `${Functions.mlang(Config.SITE_SLOGAN, lang)} - ${Functions.mlang(Config.SITE_TITLE, lang)}`;
return (
<div>
<Helmet>
<title>{title}</title>
</Helmet>
<ArchiveNotice lang={lang} />
<div>
<img src={imageMainBanner} alt="main banner" />
</div>
<div className="centercolumn">
<div className="centerCcolumn">
<div className="col2">
<div className="leftcolumn">
<div className="block">
<div className="blockTitle">
{Functions.mlang('[en]Latest News and Topics[/en][ja]最新ニュース[/ja]', lang)}
</div>
<div className="blockContent">
<BulletinRecentNews lang={lang} mydirname="news" />
<LinkBannersBlock lang={lang} />
</div>
</div>
</div>
<div className="centerLcolumn">
<div className="block">
<div className="blockTitle">
{Functions.mlang('[en]INCF Portal Updates[/en][ja]INCF Portal 更新情報[/ja]', lang)}
</div>
<div className="blockContent">
<RssBlock lang={lang} sites={RSS_UPDATES_INCF} num={5} />
<div className="centercolumn">
<div className="centerCcolumn">
<div className="block">
<div className="blockTitle">
{Functions.mlang('[en]Latest News and Topics[/en][ja]最新ニュース[/ja]', lang)}
</div>
<div className="blockContent">
<BulletinRecentNews lang={lang} mydirname="news" />
</div>
</div>
</div>
</div>
<div className="centerRcolumn">
<div className="block">
<div className="blockTitle">
{Functions.mlang('[en]Platform Updates[/en][ja]プラットフォーム更新情報[/ja]', lang)}
<div className="centerLcolumn">
<div className="block">
<div className="blockTitle">
{Functions.mlang('[en]INCF Portal Updates[/en][ja]INCF Portal 更新情報[/ja]', lang)}
</div>
<div className="blockContent">
<RssBlock lang={lang} sites={RSS_UPDATES_INCF} num={5} />
</div>
</div>
<div className="blockContent">
<RssBlock lang={lang} sites={RSS_UPDATES_PLATFORMS} num={5} />
</div>
<div className="centerRcolumn">
<div className="block">
<div className="blockTitle">
{Functions.mlang('[en]Platform Updates[/en][ja]プラットフォーム更新情報[/ja]', lang)}
</div>
<div className="blockContent">
<RssBlock lang={lang} sites={RSS_UPDATES_PLATFORMS} num={5} />
</div>
</div>
</div>
</div>
</div>
</div>
</div>;
}
);
};
export default TopPage;
+35 -21
View File
@@ -1,34 +1,48 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { Helmet } from '@dr.pogodin/react-helmet';
import { Link } from 'react-router';
import NoticeSiteHasBeenArchived from '../common/lib/NoticeSiteHasBeenArchived';
import Config, { MultiLang } from '../config';
import Config, { type MultiLang } from '../config';
import Functions from '../functions';
import { Link } from 'react-router-dom';
interface Props {
lang: MultiLang;
}
const message = {
ja: <div>
<p></p>
<p><Link to="/modules/xoonips"></Link></p>
</div>,
en: <div>
<p>Due to site archiving, the across platform resource search has been discontinued.</p>
<p>You can search the data only this site <Link to="/modules/xoonips">here</Link>.</p>
</div>
ja: (
<div>
<p></p>
<p>
<Link to="/modules/xoonips"></Link>
</p>
</div>
),
en: (
<div>
<p>Due to site archiving, the across platform resource search has been discontinued.</p>
<p>
You can search the data only this site <Link to="/modules/xoonips">here</Link>.
</p>
</div>
),
};
const Xsearch: React.FC<Props> = (props: Props) => {
const Xsearch = (props: Props) => {
const { lang } = props;
const title = Functions.mlang('[en]Across Platform Resource Search[/en][ja]日本ノードプラットフォーム横断検索[/ja]', lang);
return <div>
<Helmet><title>{title + ' - ' + Functions.mlang(Config.SITE_TITLE, lang)}</title></Helmet>
<h1>{title}</h1>
<NoticeSiteHasBeenArchived lang={lang} />
{message[lang]}
</div>;
}
const title = Functions.mlang(
'[en]Across Platform Resource Search[/en][ja]日本ノードプラットフォーム横断検索[/ja]',
lang
);
return (
<div>
<Helmet>
<title>{`${title} - ${Functions.mlang(Config.SITE_TITLE, lang)}`}</title>
</Helmet>
<h1>{title}</h1>
<NoticeSiteHasBeenArchived lang={lang} />
{message[lang]}
</div>
);
};
export default Xsearch;
+232 -232
View File
@@ -1,233 +1,233 @@
[
{
"root": {
"label": "[en]About Us[/en][ja]J-Node について[/ja]",
"link": ""
},
"nodes": [
{
"label": "[en]INCF[/en][ja]ニューロインフォマティクス国際統合機構 INCF[/ja]",
"link": "\/aboutus\/incf.html"
},
{
"label": "[en]INCF Japan Node[/en][ja]INCF 日本ノード[/ja]",
"link": "\/aboutus\/incf.html"
},
{
"label": "[en]Join Us[/en][ja]参加方法[/ja]",
"link": "\/joinus\/"
},
{
"label": "[en]History[/en][ja]沿革[/ja]",
"link": "\/aboutus\/history.html"
}
]
},
{
"root": {
"label": "[en]News[/en][ja]ニュース[/ja]",
"link": "\/news\/"
},
"nodes": [
{
"label": "[en]J-Node Update[/en][ja]J-Nodeサイトの更新情報[/ja]",
"link": "\/news\/index.php?storytopic=5"
},
{
"label": "[en]Topics/Article[/en][ja]トピックス[/ja]",
"link": "\/news\/index.php?storytopic=2"
},
{
"label": "[en]Newsletters[/en][ja]ニュースレター[/ja]",
"link": "\/news\/index.php?storytopic=4"
},
{
"label": "[en]From J-Node Secretariat[/en][ja]事務局からのお知らせ[/ja]",
"link": "\/news\/index.php?storytopic=7"
},
{
"label": "[en]Meeting/Event[/en][ja]会議/イベント[/ja]",
"link": "\/news\/index.php?storytopic=3"
}
]
},
{
"root": {
"label": "[en]Resources[/en][ja]リソース[/ja]",
"link": ""
},
"nodes": [
{
"label": "[en]J-Node Platform[/en][ja]日本ノードプラットフォーム[/ja]",
"link": "\/resource\/platform.html"
},
{
"label": "[en]Across Platform Resource Search[/en][ja]日本ノードプラットフォーム横断検索[/ja]",
"link": "\/modules\/xsearch\/"
},
{
"label": "[en]Platform Updates[/en][ja]プラットフォーム更新情報[/ja]",
"link": "\/resource\/platform.html#link-pf-update"
},
{
"label": "[en]Infrastructure Software[/en][ja]基盤ソフトウェア[/ja]",
"link": "\/resource\/software.html"
},
{
"label": "[en]Related Link[/en][ja]関連リンク[/ja]",
"link": "\/link\/"
}
]
},
{
"root": {
"label": "[en]Publication[/en][ja]出版物・刊行物[/ja]",
"link": "\/publication\/"
},
"nodes": [
{
"label": "Advances in Neuroinformatics",
"link": "\/publication\/#aini"
},
{
"label": "[en]J-Node Guidance[/en][ja]日本ノード 紹介資料[/ja]",
"link": "\/publication\/#brochure"
},
{
"label": "[en]Platform Posters[/en][ja]プラットフォーム ポスター[/ja]",
"link": "\/publication\/#pfposter"
}
]
},
{
"root": {
"label": "[en]Tutorial[/en][ja]チュートリアル[/ja]",
"link": "\/tutorial\/"
},
"nodes": [
{
"label": "[en]J-Node PFs Tutorial Catalogue[/en][ja]日本ノードPFチュートリアルカタログ[/ja]",
"link": "\/tutorial\/pfs-catalogue\/"
},
{
"label": "[en]Benham's Top[/en][ja]ベンハムの独楽[/ja]",
"link": "/tutorial/benham/"
},
{
"label": "[en]Semantic description of EEG[/en][ja]脳波データのSemantic Description[/ja]",
"link": "\/tutorial\/semantic-eeg\/"
}
]
},
{
"root": {
"label": "[en]Program[/en][ja]プログラム[/ja]",
"link": "\/program\/nijc-program.html"
},
"nodes": [
{
"label": "[en]Neuroinformatics Platforms[/en][ja]プラットフォームの開発運営[/ja]",
"link": "\/program\/platforms.html"
},
{
"label": "[en]Research Data management with DOI[/en][ja]DOI によるデジタル研究データの管理[/ja]",
"link": "\/program\/doi.html"
}
]
},
{
"root": {
"label": "[en]Hackathon[/en][ja]ハッカソン[/ja]",
"link": "\/program\/j-node-hackathon.html"
},
"nodes": [
{
"label": "J-Node Hackathon 2018",
"link": "\/hackathon2018\/"
},
{
"label": "Brainhack Global 2017",
"link": "\/brainhack-global-2017\/"
},
{
"label": "J-Node Hackathon 2016",
"link": "\/hackathon2016\/"
},
{
"label": "Brain Atlas Ideathon/Hackathon 2015",
"link": "\/bah2015\/"
}
]
},
{
"root": {
"label": "[en]Workshop AINI[/en][ja]国際ワークショップ AINI[/ja]",
"link": "\/program\/workshop-aini.html"
},
"nodes": [
{
"label": "Advances in Neuroinformatics 2018",
"link": "\/aini2018\/"
},
{
"label": "Advances in Neuroinformatics 2017",
"link": "\/aini2017\/"
},
{
"label": "Advances in Neuroinformatics 2016",
"link": "\/aini2016\/"
},
{
"label": "Advances in Neuroinformatics 2015",
"link": "\/aini2015\/"
},
{
"label": "Advances in Neuroinformatics 2014",
"link": "\/aini2014\/"
}
]
},
{
"root": {
"label": "[en]Workshop NBNI[/en][ja]日中韓合同ワークショップ NBNI[/ja]",
"link": ""
},
"nodes": [
{
"label": "NBNI 2017",
"link": "\/nbni2017\/"
},
{
"label": "NBNI 2014",
"link": "\/nbni2014\/"
}
]
},
{
"root": {
"label": "[en]Contact Us[/en][ja]問い合わせ[/ja]",
"link": "\/contactus"
},
"nodes": []
},
{
"root": {
"label": "[en]Site Information[/en][ja]このサイトについて[/ja]",
"link": "\/modules\/credits\/index.php"
},
"nodes": [
{
"label": "[en]Instructions for Use[/en][ja]ご利用上の注意[/ja]",
"link": "\/modules\/credits\/index.php?id=1"
},
{
"label": "[en]Copyrights[/en][ja]著作権について[/ja]",
"link": "\/modules\/credits\/index.php?id=2"
},
{
"label": "[en]Privacy Policy[/en][ja]個人情報の取扱いについて[/ja]",
"link": "\/modules\/credits\/index.php?id=3"
}
]
}
]
{
"root": {
"label": "[en]About Us[/en][ja]J-Node について[/ja]",
"link": ""
},
"nodes": [
{
"label": "[en]INCF[/en][ja]ニューロインフォマティクス国際統合機構 INCF[/ja]",
"link": "\/aboutus\/incf.html"
},
{
"label": "[en]INCF Japan Node[/en][ja]INCF 日本ノード[/ja]",
"link": "\/aboutus\/incf.html"
},
{
"label": "[en]Join Us[/en][ja]参加方法[/ja]",
"link": "\/joinus\/"
},
{
"label": "[en]History[/en][ja]沿革[/ja]",
"link": "\/aboutus\/history.html"
}
]
},
{
"root": {
"label": "[en]News[/en][ja]ニュース[/ja]",
"link": "\/news\/"
},
"nodes": [
{
"label": "[en]J-Node Update[/en][ja]J-Nodeサイトの更新情報[/ja]",
"link": "\/news\/index.php?storytopic=5"
},
{
"label": "[en]Topics/Article[/en][ja]トピックス[/ja]",
"link": "\/news\/index.php?storytopic=2"
},
{
"label": "[en]Newsletters[/en][ja]ニュースレター[/ja]",
"link": "\/news\/index.php?storytopic=4"
},
{
"label": "[en]From J-Node Secretariat[/en][ja]事務局からのお知らせ[/ja]",
"link": "\/news\/index.php?storytopic=7"
},
{
"label": "[en]Meeting/Event[/en][ja]会議/イベント[/ja]",
"link": "\/news\/index.php?storytopic=3"
}
]
},
{
"root": {
"label": "[en]Resources[/en][ja]リソース[/ja]",
"link": ""
},
"nodes": [
{
"label": "[en]J-Node Platform[/en][ja]日本ノードプラットフォーム[/ja]",
"link": "\/resource\/platform.html"
},
{
"label": "[en]Across Platform Resource Search[/en][ja]日本ノードプラットフォーム横断検索[/ja]",
"link": "\/modules\/xsearch\/"
},
{
"label": "[en]Platform Updates[/en][ja]プラットフォーム更新情報[/ja]",
"link": "\/resource\/platform.html#link-pf-update"
},
{
"label": "[en]Infrastructure Software[/en][ja]基盤ソフトウェア[/ja]",
"link": "\/resource\/software.html"
},
{
"label": "[en]Related Link[/en][ja]関連リンク[/ja]",
"link": "\/link\/"
}
]
},
{
"root": {
"label": "[en]Publication[/en][ja]出版物・刊行物[/ja]",
"link": "\/publication\/"
},
"nodes": [
{
"label": "Advances in Neuroinformatics",
"link": "\/publication\/#aini"
},
{
"label": "[en]J-Node Guidance[/en][ja]日本ノード 紹介資料[/ja]",
"link": "\/publication\/#brochure"
},
{
"label": "[en]Platform Posters[/en][ja]プラットフォーム ポスター[/ja]",
"link": "\/publication\/#pfposter"
}
]
},
{
"root": {
"label": "[en]Tutorial[/en][ja]チュートリアル[/ja]",
"link": "\/tutorial\/"
},
"nodes": [
{
"label": "[en]J-Node PFs Tutorial Catalogue[/en][ja]日本ノードPFチュートリアルカタログ[/ja]",
"link": "\/tutorial\/pfs-catalogue\/"
},
{
"label": "[en]Benham's Top[/en][ja]ベンハムの独楽[/ja]",
"link": "/tutorial/benham/"
},
{
"label": "[en]Semantic description of EEG[/en][ja]脳波データのSemantic Description[/ja]",
"link": "\/tutorial\/semantic-eeg\/"
}
]
},
{
"root": {
"label": "[en]Program[/en][ja]プログラム[/ja]",
"link": "\/program\/nijc-program.html"
},
"nodes": [
{
"label": "[en]Neuroinformatics Platforms[/en][ja]プラットフォームの開発運営[/ja]",
"link": "\/program\/platforms.html"
},
{
"label": "[en]Research Data management with DOI[/en][ja]DOI によるデジタル研究データの管理[/ja]",
"link": "\/program\/doi.html"
}
]
},
{
"root": {
"label": "[en]Hackathon[/en][ja]ハッカソン[/ja]",
"link": "\/program\/j-node-hackathon.html"
},
"nodes": [
{
"label": "J-Node Hackathon 2018",
"link": "\/hackathon2018\/"
},
{
"label": "Brainhack Global 2017",
"link": "\/brainhack-global-2017\/"
},
{
"label": "J-Node Hackathon 2016",
"link": "\/hackathon2016\/"
},
{
"label": "Brain Atlas Ideathon/Hackathon 2015",
"link": "\/bah2015\/"
}
]
},
{
"root": {
"label": "[en]Workshop AINI[/en][ja]国際ワークショップ AINI[/ja]",
"link": "\/program\/workshop-aini.html"
},
"nodes": [
{
"label": "Advances in Neuroinformatics 2018",
"link": "\/aini2018\/"
},
{
"label": "Advances in Neuroinformatics 2017",
"link": "\/aini2017\/"
},
{
"label": "Advances in Neuroinformatics 2016",
"link": "\/aini2016\/"
},
{
"label": "Advances in Neuroinformatics 2015",
"link": "\/aini2015\/"
},
{
"label": "Advances in Neuroinformatics 2014",
"link": "\/aini2014\/"
}
]
},
{
"root": {
"label": "[en]Workshop NBNI[/en][ja]日中韓合同ワークショップ NBNI[/ja]",
"link": ""
},
"nodes": [
{
"label": "NBNI 2017",
"link": "\/nbni2017\/"
},
{
"label": "NBNI 2014",
"link": "\/nbni2014\/"
}
]
},
{
"root": {
"label": "[en]Contact Us[/en][ja]問い合わせ[/ja]",
"link": "\/contactus"
},
"nodes": []
},
{
"root": {
"label": "[en]Site Information[/en][ja]このサイトについて[/ja]",
"link": "\/modules\/credits\/index.php"
},
"nodes": [
{
"label": "[en]Instructions for Use[/en][ja]ご利用上の注意[/ja]",
"link": "\/modules\/credits\/index.php?id=1"
},
{
"label": "[en]Copyrights[/en][ja]著作権について[/ja]",
"link": "\/modules\/credits\/index.php?id=2"
},
{
"label": "[en]Privacy Policy[/en][ja]個人情報の取扱いについて[/ja]",
"link": "\/modules\/credits\/index.php?id=3"
}
]
}
]
+332 -293
View File
@@ -1,27 +1,27 @@
*,
*::before,
*::after {
box-sizing: border-box;
box-sizing: border-box;
}
html {
line-height: 1.15;
line-height: 1.15;
}
html,
body,
#root {
width: 100%;
min-width: 990px;
width: 100%;
min-width: 990px;
}
body {
font-family: Arial, Helvetica, sans-serif;
color: #333333;
background-color: #ffffff;
margin: 0;
font-size: 14px;
border-top: 3px solid #2c6bab;
font-family: Arial, Helvetica, sans-serif;
color: #333333;
background-color: #ffffff;
margin: 0;
font-size: 14px;
border-top: 3px solid #2c6bab;
}
h1,
@@ -30,488 +30,517 @@ h3,
h4,
h5,
h6 {
margin-bottom: 0.5rem;
font-weight: bold;
line-height: 1.2;
margin-bottom: 0.5rem;
font-weight: bold;
line-height: 1.2;
}
h1 {
font-size: 1.383rem;
font-size: 1.383rem;
}
h2 {
font-size: 1.296rem;
font-size: 1.296rem;
}
h3 {
font-size: 1.215rem;
font-size: 1.215rem;
}
h4 {
font-size: 1.138rem;
font-size: 1.138rem;
}
h5 {
font-size: 1.067rem;
font-size: 1.067rem;
}
h6 {
font-size: 1rem;
font-size: 1rem;
}
p {
margin: 0 0 1rem;
margin: 0 0 1rem;
}
a {
color: #16315c;
color: #16315c;
}
a:visited {
color: #5c165b;
color: #5c165b;
}
ul, ol {
margin: 0 0 1rem;
padding-left: 1rem;
ul,
ol {
margin: 0 0 1rem;
padding-left: 1rem;
}
li {
margin: 0 0 0.5rem;
padding: 0 0 0 0.5rem;
margin: 0 0 0.5rem;
padding: 0 0 0 0.5rem;
}
/* Nested lists sit flush with their parent item, as the old reset had them. */
#main :is(dl, ol, ul) :is(dl, ol, ul) {
margin: 0;
}
table {
margin: 0 0 1rem;
width: 100%;
margin: 0 0 1rem;
width: 100%;
}
/* The pages were laid out with the browser's separated table borders. */
#main table {
border-collapse: separate;
}
td,
th {
padding: 0.5rem;
padding: 0.5rem;
}
td {
vertical-align: top;
vertical-align: top;
}
.foot {
vertical-align: middle;
vertical-align: middle;
}
th,
.head,
.foot {
font-weight: bold;
color: #666666;
background-color: #eeeeee;
vertical-align: middle;
font-weight: bold;
color: #666666;
background-color: #eeeeee;
vertical-align: middle;
}
.even {
background-color: #f7f7f7;
background-color: #f7f7f7;
}
.odd {
background-color: #fafafa;
background-color: #fafafa;
}
.col2::after {
content: "";
display: block;
clear: both;
content: "";
display: block;
clear: both;
}
.col2 .leftcolumn {
float: left;
width: 260px;
margin: 10px 0;
float: left;
width: 260px;
margin: 10px 0;
}
.col2 .centercolumn {
float: right;
width: 720px;
float: right;
width: 720px;
}
.centerCcolumn {
margin: 10px 0;
margin: 10px 0;
}
.centerLcolumn {
display: inline-block;
vertical-align: top;
margin: 10px 10px 10px 0;
width: calc(50% - 10px);
display: inline-block;
vertical-align: top;
margin: 10px 10px 10px 0;
width: calc(50% - 10px);
}
.centerRcolumn {
display: inline-block;
vertical-align: top;
margin: 10px 0 10px 10px;
width: calc(50% - 10px);
display: inline-block;
vertical-align: top;
margin: 10px 0 10px 10px;
width: calc(50% - 10px);
}
.centerLcolumn .block:not(:first-child),
.centerRcolumn .block:not(:first-child) {
margin-top: 20px;
margin-top: 20px;
}
.hidden {
display: none;
display: none;
}
.block .blockTitle {
font-weight: bold;
color: #ffffff;
background: url(images/blockTitleBack.png) no-repeat left center;
text-shadow: 0px -1px 0 #000000;
font-weight: bold;
color: #ffffff;
background: url(images/blockTitleBack.png) no-repeat left center;
text-shadow: 0px -1px 0 #000000;
}
.centercolumn .block {
border: 1px solid#cccccc;
border: 1px solid #cccccc;
}
.leftcolumn .block:not(:first-child) {
margin: 10px 0;
margin: 10px 0;
}
.leftcolumn .blockTitle,
.centerCcolumn .blockTitle {
padding: 0.75rem 0.75rem 0.75rem 1.5rem;
padding: 0.75rem 0.75rem 0.75rem 1.5rem;
}
.centerRcolumn .blockTitle,
.centerLcolumn .blockTitle {
margin: 8px 10px 0;
padding: 0.5rem 0.5rem 0.5rem 1.5rem;
margin: 8px 10px 0;
padding: 0.5rem 0.5rem 0.5rem 1.5rem;
}
.centerCcolumn .blockContent,
.centerRcolumn .blockContent,
.centerLcolumn .blockContent {
margin: 0;
padding: 10px;
margin: 0;
padding: 10px;
}
#page {
margin: 0 auto;
width: 990px;
margin: 0 auto;
width: 990px;
}
#header {
margin: 0;
width: 990px;
min-width: 990px;
margin: 0;
width: 990px;
min-width: 990px;
}
#header a {
color: #333333;
color: #333333;
}
#header .top {
height: 90px;
height: 90px;
}
#header .top::after {
clear: both;
content: '';
display: block;
clear: both;
content: "";
display: block;
}
#header .logo {
float: left;
display: block;
margin: 10px 0;
height: 70px;
width: 400px;
background-image: url(images/logo.png);
float: left;
display: block;
margin: 10px 0;
height: 70px;
width: 400px;
background-image: url(images/logo.png);
}
#header .utility {
float: right;
display: block;
height: 90px;
width: 590px;
text-align: right;
float: right;
display: block;
height: 90px;
width: 590px;
text-align: right;
}
#header .utility .socialmedia {
display: inline-block;
position: relative;
width: 126px;
height: 35px;
top: 20px;
right: 50px;
display: inline-block;
position: relative;
width: 126px;
height: 35px;
top: 20px;
right: 50px;
}
#header .utility .socialmedia a {
padding: 0 5px;
padding: 0 5px;
}
/* The icons line up on the text baseline; sanitize.css centres images. */
#header .utility img {
vertical-align: baseline;
}
#header .utility .sitemap {
display: inline-block;
position: relative;
top: 20px;
left: 80px;
display: inline-block;
position: relative;
top: 20px;
left: 80px;
}
#header .utility .incf {
display: inline-block;
position: relative;
width: 120px;
height: 68px;
top: 5px;
display: inline-block;
position: relative;
width: 120px;
height: 68px;
top: 5px;
}
#header .utility .language {
display: inline-block;
position: relative;
width: 80px;
top: 15px;
right: -115px;
display: inline-block;
position: relative;
width: 80px;
top: 15px;
right: -115px;
}
#header .utility .language a {
display: inline-block;
font-size: 12px;
text-indent: 10px;
background: url(./images/indent.png) no-repeat left center;
display: inline-block;
font-size: 12px;
text-indent: 10px;
background: url(./images/indent.png) no-repeat left center;
}
#header .menubar {
height: 40px;
background: linear-gradient(#fff, #eee);
height: 40px;
background: linear-gradient(#fff, #eee);
}
#header .menubar::after {
content: "";
display: block;
clear: both;
content: "";
display: block;
clear: both;
}
#header .menubar .mainmenu {
float: left;
margin: 0;
padding: 0;
list-style-type: none;
font-size: 90%;
float: left;
margin: 0;
padding: 0;
list-style-type: none;
font-size: 90%;
}
#header .menubar .mainmenu>li {
display: inline-block;
border-right: 2px solid #bbcdd5;
text-align: center;
#header .menubar .mainmenu > li {
display: inline-block;
border-right: 2px solid #bbcdd5;
text-align: center;
}
#header .menubar .mainmenu li:hover {
background: linear-gradient(#fff, #d9e5fe);
cursor: pointer;
background: linear-gradient(#fff, #d9e5fe);
cursor: pointer;
}
#header .menubar .mainmenu li>a,
#header .menubar .mainmenu li>span {
display: block;
margin: 0 10px;
text-decoration: none;
color: #333333;
text-shadow: 0px 1px #FFF;
#header .menubar .mainmenu li > a,
#header .menubar .mainmenu li > span {
display: block;
margin: 0 10px;
text-decoration: none;
color: #333333;
text-shadow: 0px 1px #fff;
}
#header .menubar .mainmenu>li {
line-height: 39px;
height: 40px;
margin: 0;
padding: 0;
#header .menubar .mainmenu > li {
line-height: 39px;
height: 40px;
margin: 0;
padding: 0;
}
#header .menubar .mainmenu .submenu {
display: block;
position: absolute;
width: 200px;
text-align: left;
border: 1px solid #cccccc;
animation: fadeIn 1s ease 0s 1 normal;
font-size: 90%;
z-index: 1;
display: block;
position: absolute;
width: 200px;
text-align: left;
border: 1px solid #cccccc;
animation: fadeIn 1s ease 0s 1 normal;
font-size: 90%;
z-index: 1;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
0% {
opacity: 0;
}
100% {
opacity: 1;
}
100% {
opacity: 1;
}
}
#header .menubar .mainmenu .submenu .item {
display: block;
line-height: 1.5;
padding: 5px 10px;
text-decoration: none;
background: linear-gradient(#fff, #eee);
display: block;
line-height: 1.5;
padding: 5px 10px;
text-decoration: none;
background: linear-gradient(#fff, #eee);
}
#header .menubar .mainmenu .submenu .item:not(:first-child) {
border-top: 1px solid #cccccc;
border-top: 1px solid #cccccc;
}
#header .menubar .mainmenu .submenu .item:hover {
background: linear-gradient(#fff, #d9e5fe);
background: linear-gradient(#fff, #d9e5fe);
}
#header .menubar .search_box {
float: right;
margin: 10px 15px 0 0;
float: right;
margin: 10px 15px 0 0;
}
#header .menubar .search_box input {
margin: 0 1px;
padding: 2px;
height: 20px;
width: 150px;
border: 1px #ddd solid;
line-height: normal;
vertical-align: top;
margin: 0 1px;
padding: 2px;
height: 20px;
width: 150px;
border: 1px #ddd solid;
line-height: normal;
vertical-align: top;
background-color: #ffffff;
}
#header .menubar .search_box a {
line-height: 0;
font-size: 0;
line-height: 0;
font-size: 0;
}
#header .menubar .search_box img {
vertical-align: baseline;
}
#footer {
text-align: center;
text-align: center;
}
#footer .banners {
margin: 0;
padding: 0;
height: 60px;
list-style-type: none;
line-height: 60px;
margin: 0;
padding: 0;
height: 60px;
list-style-type: none;
line-height: 60px;
}
#footer .banners>li {
margin: 0 10px;
display: inline-block;
#footer .banners > li {
margin: 0 10px;
display: inline-block;
}
#footer .banners>li img {
vertical-align: middle;
#footer .banners > li img {
vertical-align: middle;
}
#footer .banners .credits {
color: white;
background: #0174DF;
font-weight: bold;
padding: 5px 10px;
border-radius: 20px;
text-decoration: none;
color: white;
background: #0174df;
font-weight: bold;
padding: 5px 10px;
border-radius: 20px;
text-decoration: none;
}
#footer .copyright {
line-height: 38px;
display: block;
font-size: 90%;
line-height: 38px;
display: block;
font-size: 90%;
}
#main {
margin: 10px 0;
background-color: #ffffff;
min-height: 500px;
font-size: 90%;
margin: 10px 0;
background-color: #ffffff;
min-height: 500px;
font-size: 90%;
}
#main .goPageTop {
text-align: right;
font-size: 80%;
text-align: right;
font-size: 80%;
}
#main .goPageTop a {
display: inline-block;
line-height: 2;
padding: 0 20px;
background: url(./images/go-page-top.png) no-repeat;
text-decoration: none;
color: #333333;
display: inline-block;
line-height: 2;
padding: 0 20px;
background: url(./images/go-page-top.png) no-repeat;
text-decoration: none;
color: #333333;
}
#main .goPageTop a:hover {
text-decoration: none;
color: #333333;
text-decoration: none;
color: #333333;
}
#main h1 {
font-weight: bold;
padding: .3rem .3rem .3rem 2.25rem;
background: url(images/h1-back.png) no-repeat left center;
border: 1px solid #cccccc;
color: white;
text-shadow: 0px -1px 0 #000;
font-weight: bold;
padding: 0.3rem 0.3rem 0.3rem 2.25rem;
background: url(images/h1-back.png) no-repeat left center;
border: 1px solid #cccccc;
color: white;
text-shadow: 0px -1px 0 #000;
}
#main h2 {
font-weight: bold;
padding: .3rem .3rem .3rem 2.25rem;
background: url(images/h2-back.png) no-repeat left center;
border: 1px solid #cccccc;
text-shadow: 0px -1px 0 #FFF;
font-weight: bold;
padding: 0.3rem 0.3rem 0.3rem 2.25rem;
background: url(images/h2-back.png) no-repeat left center;
border: 1px solid #cccccc;
text-shadow: 0px -1px 0 #fff;
}
#main h3 {
position: relative;
padding: .2rem .2rem .2rem .75rem;
border-left: 6px solid #ccc;
border-bottom: 1px solid #ccc;
position: relative;
padding: 0.2rem 0.2rem 0.2rem 0.75rem;
border-left: 6px solid #ccc;
border-bottom: 1px solid #ccc;
}
#main .pico_body {
margin: 0;
padding: 0;
margin: 0;
padding: 0;
}
/* Images inside the stored pages, the news and the sitemap sit on the text
baseline as they were written for; sanitize.css centres them. */
#main .pico_body img,
#main .bulletin .itemText img,
#main .sitemap img {
vertical-align: baseline;
}
#link-banners div {
margin-bottom: 8px;
font-size: 0;
margin-bottom: 8px;
font-size: 0;
}
.feed {
margin: 0 0 10px;
margin: 0 0 10px;
}
.feed .pubDate {
color: #797c80;
margin: 0 5px 0;
font-size: 90%;
font-weight: bold;
font-style: italic;
color: #797c80;
margin: 0 5px 0;
font-size: 90%;
font-weight: bold;
font-style: italic;
}
.feed .pubDate .new {
margin-left: 3px;
font-weight: bold;
color: #ff9999;
margin-left: 3px;
font-weight: bold;
color: #ff9999;
}
.feed .logo {
display: inline-block;
width: 60px !important;
overflow: hidden;
margin: 2px 10px;
display: inline-block;
width: 60px;
overflow: hidden;
margin: 2px 10px;
}
.feed .item {
display: inline-block;
vertical-align: top;
width: calc(100% - 80px);
display: inline-block;
vertical-align: top;
width: calc(100% - 80px);
}
.feed .item .site {
margin: 2px 0 0;
font-size: 85%;
font-style: italic;
margin: 2px 0 0;
font-size: 85%;
font-style: italic;
}
/*
@@ -534,130 +563,140 @@ th,
*/
.bulletin {
margin: 10px 5px;
margin: 10px 5px;
}
.bulletin .itemHead {
margin: 10px 0 5px;
padding: 2px 0;
font-weight: bold;
border-bottom: 1px dotted #CCC;
margin: 10px 0 5px;
padding: 2px 0;
font-weight: bold;
border-bottom: 1px dotted #ccc;
}
.bulletin .itemInfo {
text-align: right;
margin: 5px 0;
padding: 3px 0;
font-size: 90%;
text-align: right;
margin: 5px 0;
padding: 3px 0;
font-size: 90%;
}
.bulletin .itemTitle {
font-size: 120%;
font-weight: bold;
font-size: 120%;
font-weight: bold;
}
.bulletin .itemTitle a {
text-decoration: none;
text-decoration: none;
}
.bulletin .itemPoster {
font-size: 90%;
font-size: 90%;
}
.bulletin .itemPostDate {
font-size: 90%;
font-size: 90%;
}
.bulletin .itemStats {
font-size: 90%;
font-size: 90%;
}
.bulletin .itemBody {
padding: 0;
overflow: hidden;
padding: 0;
overflow: hidden;
}
.bulletin .itemText {
margin: 5px 0;
line-height: 1.47em;
letter-spacing: 0.04em;
font-size: 94%;
margin: 5px 0;
line-height: 1.47em;
letter-spacing: 0.04em;
font-size: 94%;
}
.bulletin .itemFoot {
text-align: right;
padding: 2px 0;
border-top: 2px dotted #999;
text-align: right;
padding: 2px 0;
border-top: 2px dotted #999;
}
.bulletin .itemPermaLink {
clear: both;
margin: 5px 0;
font-size: 90%;
clear: both;
margin: 5px 0;
font-size: 90%;
}
.bulletin .pagenavi {
margin: 5px 0;
text-align: right;
font-size: 95%;
margin: 5px 0;
text-align: right;
font-size: 95%;
}
.bulletin .pagenavi .pages {
margin-left: 10px;
font-weight: bold;
margin-left: 10px;
font-weight: bold;
}
.bulletin .pagenavi .pages .current:before {
content: '(';
content: "(";
}
.bulletin .pagenavi .pages .current:after {
content: ')';
content: ")";
}
.platform-committee th:first-child {
width: 120px;
width: 120px;
}
.platform-committee th:nth-child(2) {
width: 200px;
width: 200px;
}
.xoonips .listTable table {
margin: 0;
margin: 0;
}
.xoonips .listTable table > tbody > tr > td {
padding: 0;
padding: 0;
}
.xoonips .listTable .listBody div {
margin: 1px 0;
margin: 1px 0;
}
.xoonips .listTable .listBody .journal .journal_name {
font-style: italic;
font-style: italic;
}
.xoonips .itemDetail {
max-width: 720px;
max-width: 720px;
}
.xoonips .itemDetail > tbody > tr > td:first-child {
width: 200px;
width: 200px;
}
.xoonips .itemDetail > tbody > tr > td:nth-child(2) {
width: 520px;
max-width: 520px;
width: 520px;
max-width: 520px;
}
.xoonips .itemDetail table {
margin: 5px 0;
margin: 5px 0;
}
.xoonips .itemDetail table td,
.xoonips .itemDetail table th {
padding: 5px 0;
padding: 5px 0;
}
.xoonips .itemDetail table td:first-child {
white-space: nowrap;
white-space: nowrap;
}
/* sanitize.css forms.css leaves form controls transparent; keep them on white. */
.leftcolumn .blockContent input[type="text"],
.leftcolumn .blockContent select,
.leftcolumn .blockContent .formButton,
.bulletin form select,
.bulletin form input[type="submit"] {
background-color: #ffffff;
color: #000000;
}
@@ -0,0 +1,23 @@
.notice {
display: flex;
align-items: center;
gap: 10px;
margin: 10px 0;
padding: 10px 14px;
border-left: 5px solid #c62828;
background-color: #fdecec;
color: #7f1d1d;
font-size: 13px;
line-height: 1.5;
}
.icon {
flex: none;
width: 20px;
height: 20px;
color: #c62828;
}
.text {
margin: 0;
}
+25
View File
@@ -0,0 +1,25 @@
import { ARCHIVED_NOTICE } from '../../common/lib/NoticeSiteHasBeenArchived';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
import styles from './ArchiveNotice.module.css';
interface Props {
lang: MultiLang;
}
/** The archive notice shown prominently on the top page. */
const ArchiveNotice = (props: Props) => {
const { lang } = props;
return (
<div className={styles.notice} role="note">
<svg className={styles.icon} viewBox="0 0 20 20" aria-hidden="true">
<circle cx="10" cy="10" r="9" fill="none" stroke="currentColor" strokeWidth="2" />
<rect x="9" y="8.5" width="2" height="6" fill="currentColor" />
<rect x="9" y="5" width="2" height="2" fill="currentColor" />
</svg>
<p className={styles.text}>{Functions.mlang(ARCHIVED_NOTICE, lang)}</p>
</div>
);
};
export default ArchiveNotice;
+33 -16
View File
@@ -1,6 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { MultiLang } from '../../config';
import { Link } from 'react-router';
import type { MultiLang } from '../../config';
import imageLinksAini2018 from '../assets/images/links-aini2018.png';
import imageLinksHakcathon2018Sep from '../assets/images/links-hackathon2018sep.png';
import imageLinksJnodePf from '../assets/images/links-jnodepf.png';
@@ -13,23 +12,41 @@ interface Props {
lang: MultiLang;
}
const LinkBannersBlock: React.FC<Props> = (props: Props) => {
const LinkBannersBlock = (props: Props) => {
const { lang } = props;
const links = [
{ name: 'AINI 2018', image: imageLinksAini2018, link: '/aini2018/' },
{ name: 'J-Node Hackathon 2018 September', image: imageLinksHakcathon2018Sep, link: '/hackathon2018/september.html' },
{
name: 'J-Node Hackathon 2018 September',
image: imageLinksHakcathon2018Sep,
link: '/hackathon2018/september.html',
},
{ name: 'J-Node Platform', image: imageLinksJnodePf, link: '/resource/platform.html' },
{ name: 'J-Node Newsletter', image: lang === 'en' ? imageLinksNewsletterE : imageLinksNewsletterJ, link: '/modules/news/index.php?storytopic=4' },
{ name: 'J-Node Platform Report', image: lang === 'en' ? imageLinksPfReportE : imageLinksPfReportJ, link: '/publication' }
{
name: 'J-Node Newsletter',
image: lang === 'en' ? imageLinksNewsletterE : imageLinksNewsletterJ,
link: '/modules/news/index.php?storytopic=4',
},
{
name: 'J-Node Platform Report',
image: lang === 'en' ? imageLinksPfReportE : imageLinksPfReportJ,
link: '/publication',
},
];
return <div id="link-banners">
{links.map((value, index) => {
const { name, image, link } = value;
return <div key={index}>
<Link to={link}><img src={image} alt={name} /></Link>
</div>;
})}
</div>;
return (
<div id="link-banners">
{links.map((value) => {
const { name, image, link } = value;
return (
<div key={name}>
<Link to={link}>
<img src={image} alt={name} />
</Link>
</div>
);
})}
</div>
);
};
export default LinkBannersBlock;
export default LinkBannersBlock;
+54 -27
View File
@@ -1,8 +1,8 @@
import React from 'react';
import { Component } from 'react';
import Loading from '../../common/lib/Loading';
import { MultiLang } from '../../config';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
import RssFeed, { RssFeedItem, RssFeedSite } from '../lib/RssFeed';
import RssFeed, { type RssFeedItem, type RssFeedSite } from '../lib/RssFeed';
interface Props {
lang: MultiLang;
@@ -16,19 +16,27 @@ interface State {
}
const rssMerge = (a: RssFeedItem[], b: RssFeedItem[], maxlength: number) => {
return a.concat(b).sort((i, j) => { return i.pubDate === j.pubDate ? 0 : (i.pubDate > j.pubDate ? -1 : 1) }).slice(0, maxlength);
return a
.concat(b)
.sort((i, j) => {
return i.pubDate === j.pubDate ? 0 : i.pubDate > j.pubDate ? -1 : 1;
})
.slice(0, maxlength);
};
const arrayDiff = (a: string[], b: string[]): string[] => {
return a.filter((i: string) => {
return b.indexOf(i) < 0;
});
};
const arrayDiff = (a: string[], b: string[]): string[] => { return a.filter((i: string) => { return b.indexOf(i) < 0 }) };
class RssBlock extends React.Component<Props, State> {
class RssBlock extends Component<Props, State> {
private isActive: boolean;
constructor(props: Props) {
super(props);
this.state = {
loading: true,
feeds: null
feeds: null,
};
this.isActive = false;
}
@@ -38,13 +46,17 @@ class RssBlock extends React.Component<Props, State> {
this.updateFeed();
}
componentDidUpdate(prevProps: Props, prevState: State) {
componentDidUpdate(prevProps: Props) {
const { sites, num } = this.props;
const prevSites = prevProps.sites;
const prevNum = prevProps.num;
const siteIds = sites.map((site) => site.id);
const prevSiteIds = prevSites.map((site) => site.id);
if (num !== prevNum || arrayDiff(siteIds, prevSiteIds).length !== 0 || arrayDiff(prevSiteIds, siteIds).length !== 0) {
if (
num !== prevNum ||
arrayDiff(siteIds, prevSiteIds).length !== 0 ||
arrayDiff(prevSiteIds, siteIds).length !== 0
) {
this.setState({ loading: true, feeds: null });
this.updateFeed();
}
@@ -60,7 +72,7 @@ class RssBlock extends React.Component<Props, State> {
const infos = await Promise.all(sites.map(async (site) => RssFeed.getInfo(site)));
infos.forEach((info) => {
feeds = rssMerge(feeds, info.items, num);
})
});
if (this.isActive) {
this.setState({ loading: false, feeds });
}
@@ -74,22 +86,37 @@ class RssBlock extends React.Component<Props, State> {
if (feeds === null) {
return <div>Faild to get RSS feeds.</div>;
}
const newRange = new Date().getTime() / 1000 - 604800;
return <div className="feeds">
{feeds.map((feed, index) => {
const site = RssFeed.getSite(feed.id);
const isNew = feed.pubDate > newRange;
return <div className="feed" key={feed.id + index}>
<div className="pubDate">{Functions.formatDate(feed.pubDate, 'YYYY-M-D')}{isNew && <span className="new">New</span>}</div>
<div className="logo"><a href={site.url} target="_blank" rel="noopener noreferrer"><img src={site.logo} alt={site.name} /></a></div>
<div className="item">
<div className="title"><a href={feed.link} target="_blank" rel="noopener noreferrer">{feed.title}</a></div>
<div className="site">({site.name})</div>
</div>
</div>
})}
</div>;
const newRange = Date.now() / 1000 - 604800;
return (
<div className="feeds">
{feeds.map((feed) => {
const site = RssFeed.getSite(feed.id);
const isNew = feed.pubDate > newRange;
return (
<div className="feed" key={feed.id + feed.link}>
<div className="pubDate">
{Functions.formatDate(feed.pubDate, 'yyyy-M-d')}
{isNew && <span className="new">New</span>}
</div>
<div className="logo">
<a href={site.url} target="_blank" rel="noopener noreferrer">
<img src={site.logo} alt={site.name} />
</a>
</div>
<div className="item">
<div className="title">
<a href={feed.link} target="_blank" rel="noopener noreferrer">
{feed.title}
</a>
</div>
<div className="site">({site.name})</div>
</div>
</div>
);
})}
</div>
);
}
}
export default RssBlock;
export default RssBlock;
+27 -27
View File
@@ -1,5 +1,4 @@
import moment from 'moment';
import RSSParser from 'rss-parser';
import ky from 'ky';
export interface RssFeedSite {
id: string;
@@ -22,39 +21,40 @@ export interface RssFeedInfo {
loaded: boolean;
}
const CORS_PROXY = "https://archive.ni.brain.riken.jp/cors/";
const CORS_PROXY = 'https://archive.ni.brain.riken.jp/cors/';
const feedInfoCache = new Map<string, RssFeedInfo>();
// RSS 2.0 `<item>`s and Atom `<entry>`s, read with the browser's own XML parser.
const parseFeed = (xml: string, site: RssFeedSite): RssFeedItem[] => {
const doc = new DOMParser().parseFromString(xml, 'application/xml');
const items: RssFeedItem[] = [];
doc.querySelectorAll('item, entry').forEach((node) => {
const text = (selector: string) => node.querySelector(selector)?.textContent?.trim() ?? '';
const title = text('title');
const link = text('link') || node.querySelector('link')?.getAttribute('href') || '';
const dateString = text('pubDate') || text('updated') || text('published') || text('date');
const pubDate = Math.floor(new Date(dateString).getTime() / 1000);
if (title !== '' && link !== '' && !Number.isNaN(pubDate)) {
items.push({ id: site.id, title, link, pubDate });
}
});
return items;
};
const getInfo = async (site: RssFeedSite): Promise<RssFeedInfo> => {
const info = feedInfoCache.get(site.id) || { site, items: [], loaded: false };
if (!info.loaded) {
info.loaded = true;
const parser = new RSSParser();
try {
const output = await parser.parseURL(CORS_PROXY + site.rss);
if (typeof output.items !== 'undefined') {
output.items.forEach((item) => {
if (typeof item.title !== 'undefined' && typeof item.link !== 'undefined') {
const dateString = item.pubDate || item.isoDate;
const pubDate = moment(dateString).unix();
if (!isNaN(pubDate)) {
info.items.push({
id: site.id,
title: item.title,
link: item.link,
pubDate: pubDate,
});
}
}
});
}
} catch (err) {
const xml = await ky.get(CORS_PROXY + site.rss).text();
info.items.push(...parseFeed(xml, site));
} catch {
// ignore
}
feedInfoCache.set(site.id, info);
}
return info;
}
};
const getSite = (id: string) => {
const info = feedInfoCache.get(id);
@@ -62,11 +62,11 @@ const getSite = (id: string) => {
return { id: id, name: '', logo: '', url: '' };
}
return info.site;
}
};
const RssFeed = {
getInfo,
getSite
}
getSite,
};
export default RssFeed;
export default RssFeed;
+49 -42
View File
@@ -1,18 +1,17 @@
import moment from 'moment';
import XRegExp from 'xregexp';
import Config, { MultiLang } from './config';
import { format } from 'date-fns';
import Config, { type MultiLang } from './config';
const escape = (str: string): string => {
const escapeUrl = (str: string) => {
return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {
return '%' + c.charCodeAt(0).toString(16);
return `%${c.charCodeAt(0).toString(16)}`;
});
}
};
const unescape = (str: string): string => {
const unescapeUrl = (str: string) => {
return decodeURIComponent(str);
}
};
const htmlspecialchars = (str: string): string => {
const htmlspecialchars = (str: string) => {
return str.replace(/(<|>|&|'|")/g, (match) => {
switch (match) {
case '<':
@@ -21,46 +20,47 @@ const htmlspecialchars = (str: string): string => {
return '&gt;';
case '&':
return '&amp;';
case '\'':
case "'":
return '&#039;';
case '"':
return '&quot;';
}
return '';
});
}
};
const camelCase = (str: string): string => {
const camelCase = (str: string) => {
return str.replace(/[-_](.)/g, (...matches) => {
return matches[1].toUpperCase();
});
}
};
const snakeCase = (str: string): string => {
const snakeCase = (str: string) => {
var camel = camelCase(str);
return camel.replace(/[A-Z]/g, (...matches) => {
return "_" + matches[0].charAt(0).toLowerCase();
return `_${matches[0].charAt(0).toLowerCase()}`;
});
}
};
const pascalCase = (str: string): string => {
const pascalCase = (str: string) => {
var camel = camelCase(str);
return camel.charAt(0).toUpperCase() + camel.slice(1);
}
};
const base64Encode = (str: string): string => {
const base64Encode = (str: string) => {
return btoa(unescape(encodeURIComponent(str)));
}
};
const base64Decode = (str: string): string => {
const base64Decode = (str: string) => {
return decodeURIComponent(escape(atob(str)));
}
};
const formatDate = (timestamp: number, format: string): string => {
return moment(new Date(timestamp * 1000)).format(format);
}
// `pattern` uses date-fns tokens (`yyyy-M-d`, `MMMM do, yyyy`, ...).
const formatDate = (timestamp: number, pattern: string): string => {
return format(new Date(timestamp * 1000), pattern);
};
const ordinal = (num: number): string => {
const ordinal = (num: number) => {
const hundred = num % 100;
if (hundred >= 10 && hundred <= 20) {
return 'th';
@@ -68,58 +68,65 @@ const ordinal = (num: number): string => {
const ten = num % 10;
const suffix = ['st', 'nd', 'rd'];
return ten > 0 && ten < 4 ? suffix[ten - 1] : 'th';
}
};
const mlang = (str: string, lang: MultiLang): string => {
const mlang = (str: string, lang: MultiLang) => {
const mlLangs = ['en', 'ja'];
// escape brackets inside of <input type="text" value="...">
let text = str.replace(/(<input)([^>]*)(>)/isg, (whole, m1, m2, m3) => {
let text = str.replace(/(<input)([^>]*)(>)/gis, (whole, m1, m2, m3) => {
if (m2.match(/type=["']?(?:text|hidden)["']?/is)) {
return m1 + m2.replace(/\[/g, '__ml[ml__') + m3;
}
return whole;
});
// escape brackets inside of <textarea></textarea>
text = text.replace(/(<textarea[^>]*>)(.*)(<\/textarea>)/isg, (whole, m1, m2, m3) => {
text = text.replace(/(<textarea[^>]*>)(.*)(<\/textarea>)/gis, (_whole, m1, m2, m3) => {
return m1 + m2.replace(/\[/g, '__ml[ml__') + m3;
});
// simple pattern to strip selected lang_tags
const re = new RegExp('\\[/?(?:[^\\]]+\\|)?' + lang + '(?:\\|[^\\]]+)?\\](?:<br />)?', 'g');
const re = new RegExp(`\\[/?(?:[^\\]]+\\|)?${lang}(?:\\|[^\\]]+)?\\](?:<br />)?`, 'g');
text = text.replace(re, '');
// eliminate description between the other language tags.
mlLangs.forEach((mlLang) => {
if (mlLang !== lang) {
const re = XRegExp('\\[(?:[^/][^\\]]+\\|)?' + mlLang + '(?:\\|[^\\]]+)?\\].*?\\[/(?:[^\\]]+\\|)?' + mlLang + '(?:\\|[^\\]]+)?\\](?:<br />)?', 'isg');
const re = new RegExp(
'\\[(?:[^/][^\\]]+\\|)?' +
mlLang +
'(?:\\|[^\\]]+)?\\].*?\\[/(?:[^\\]]+\\|)?' +
mlLang +
'(?:\\|[^\\]]+)?\\](?:<br />)?',
'gis'
);
text = text.replace(re, (whole) => {
return whole.match(/<\/table>/) ? whole : '';
});
}
});
// unescape brackets inside of <textarea></textarea>
text = text.replace(/(<textarea[^>]*>)(.*)(<\/textarea>)/isg, (whole, m1, m2, m3) => {
text = text.replace(/(<textarea[^>]*>)(.*)(<\/textarea>)/gis, (_whole, m1, m2, m3) => {
return m1 + m2.replace(/__ml\[ml__/g, '[') + m3;
});
// unescape brackets inside of <input type="text" value="...">
text = text.replace(/(<input)([^>]*)(>)/isg, (whole, m1, m2, m3) => {
text = text.replace(/(<input)([^>]*)(>)/gis, (whole, m1, m2, m3) => {
if (m2.match(/type=["']?(?:text|hidden)["']?/is)) {
return m1 + m2.replace(/__ml\[ml__/g, '[') + m3;
}
return whole;
});
return text;
}
};
const siteTitle = (lang: MultiLang): string => {
const siteTitle = (lang: MultiLang) => {
return mlang(Config.SITE_TITLE, lang);
}
};
const siteSlogan = (lang: MultiLang): string => {
const siteSlogan = (lang: MultiLang) => {
return mlang(Config.SITE_SLOGAN, lang);
}
};
const Functions = {
escape,
unescape,
escape: escapeUrl,
unescape: unescapeUrl,
htmlspecialchars,
camelCase,
snakeCase,
@@ -133,4 +140,4 @@ const Functions = {
siteSlogan,
};
export default Functions;
export default Functions;
+1 -1
View File
@@ -1 +1 @@
@import-normalize;
@import url("./common/assets/xoops.css");
-14
View File
@@ -1,14 +0,0 @@
import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
// serviceWorker.unregister();
+30
View File
@@ -0,0 +1,30 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import 'sanitize.css';
import 'sanitize.css/forms.css';
import 'sanitize.css/typography.css';
import App from './App';
import './index.css';
import { loadSiteData } from './siteData';
import IndexUtil from './xoonips/lib/IndexUtil';
const container = document.getElementById('root');
if (container === null) {
throw new Error('Root container #root was not found in the document.');
}
// The converted XOOPS data is served with static-contents/, so it is fetched
// before the first render. Components can then read it synchronously.
loadSiteData()
.then((data) => {
IndexUtil.init(data.tree);
createRoot(container).render(
<StrictMode>
<App />
</StrictMode>
);
})
.catch((error: unknown) => {
console.error('failed to load the site data', error);
container.textContent = 'Failed to load the site data.';
});
+66 -64
View File
@@ -1,10 +1,10 @@
import React, { Component } from 'react';
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { Component } from 'react';
import { Navigate, useLocation } from 'react-router';
import Loading from '../common/lib/Loading';
import PageNotFound from '../common/lib/PageNotFound';
import { MultiLang } from '../config';
import type { MultiLang } from '../config';
import './assets/pico_main.css';
import PicoContext from './lib/PicoContext';
import type PicoContext from './lib/PicoContext';
import PicoUtils from './lib/PicoUtils';
import PicoCategory from './PicoCategory';
import PicoContent from './PicoContent';
@@ -22,8 +22,67 @@ interface State {
context: PicoContext | null;
}
class Pico extends Component<Props, State> {
interface PicoPageProps {
lang: MultiLang;
dirname: string;
virtualpath: string;
context: PicoContext;
}
const PicoPage = (props: PicoPageProps) => {
const { lang, dirname, virtualpath, context } = props;
const { pathname, search, hash } = useLocation();
const module = context.getModule();
if (module === null) {
return <PageNotFound lang={lang} />;
}
const modulepath = context.getUrl('');
if (pathname === modulepath) {
return <Navigate to={`${modulepath}/index.php${search}${hash}`} replace />;
}
const regexVirutal = new RegExp(`^${virtualpath}`);
const regexModule = new RegExp(`^${dirname}/?`);
let link = pathname.replace(regexVirutal, dirname).replace(regexModule, '');
if (`${link}/` === virtualpath) {
link = '';
}
if (link.match(/.html$/)) {
const content = context.getContentByPath(link);
if (content !== null) {
return <PicoContent lang={lang} context={context} contentId={content.id} />;
}
return <PageNotFound lang={lang} />;
}
const category = context.getCategoryByPath(link);
if (category !== null) {
if (module.show_listasindex !== 0) {
return <PicoCategory lang={lang} context={context} catId={category.id} />;
}
const content = context.getFirstContent(category.id);
if (content !== null) {
return <PicoContent lang={lang} context={context} contentId={content.id} />;
}
return <PageNotFound lang={lang} />;
}
if (link === '' || link === 'index.php') {
const params = new URLSearchParams(search);
const paramCatId = params.get('cat_id');
if (paramCatId?.match(/^\d+$/)) {
return <PicoCategory lang={lang} context={context} catId={parseInt(paramCatId, 10)} />;
}
const paramContentId = params.get('content_id');
if (paramContentId?.match(/^\d+$/)) {
return <PicoContent lang={lang} context={context} contentId={parseInt(paramContentId, 10)} />;
}
const paramPage = params.get('page');
if (paramPage === 'menu' || module.show_menuinmoduletop !== 0) {
return <PicoMenu lang={lang} context={context} />;
}
}
return <PageNotFound lang={lang} />;
};
class Pico extends Component<Props, State> {
private isActive: boolean;
constructor(props: Props) {
@@ -40,7 +99,7 @@ class Pico extends Component<Props, State> {
this.updatePage();
}
componentDidUpdate(prevProps: Props, prevState: State) {
componentDidUpdate(prevProps: Props) {
const { lang, dirname, virtualpath } = this.props;
if (prevProps.lang !== lang || prevProps.dirname !== dirname || prevProps.virtualpath !== virtualpath) {
this.updatePage();
@@ -69,64 +128,7 @@ class Pico extends Component<Props, State> {
if (context === null) {
return <PageNotFound lang={lang} />;
}
const module = context.getModule();
if (module === null) {
return <PageNotFound lang={lang} />;
}
const modulepath = context.getUrl('');
return <Switch>
<Route exact path={modulepath} render={(props: RouteComponentProps) => {
const { location } = props;
const url = modulepath + '/index.php' + location.search + location.hash;
return <Redirect to={url} />
}} />
<Route render={(props: RouteComponentProps) => {
const { location } = props;
const { pathname, search } = location;
const regexVirutal = new RegExp(`^${virtualpath}`);
const regexModule = new RegExp(`^${dirname}/?`);
let link = pathname.replace(regexVirutal, dirname).replace(regexModule, '');
if (link + '/' === virtualpath) {
link = '';
}
if (link.match(/.html$/)) {
const content = context.getContentByPath(link);
if (content !== null) {
return <PicoContent lang={lang} context={context} contentId={content.id} />;
}
return <PageNotFound lang={lang} />;
}
const category = context.getCategoryByPath(link);
if (category !== null) {
if (module.show_listasindex !== 0) {
return <PicoCategory lang={lang} context={context} catId={category.id} />
}
const content = context.getFirstContent(category.id);
if (content !== null) {
return <PicoContent lang={lang} context={context} contentId={content.id} />;
}
return <PageNotFound lang={lang} />;
}
if (link === '' || link === 'index.php') {
const params = new URLSearchParams(search);
const paramCatId = params.get('cat_id');
if (paramCatId !== null && paramCatId.match(/^\d+$/)) {
const catId = parseInt(paramCatId, 10);
return <PicoCategory lang={lang} context={context} catId={catId} />;
}
const paramContentId = params.get('content_id');
if (paramContentId !== null && paramContentId.match(/^\d+$/)) {
const contentId = parseInt(paramContentId, 10);
return <PicoContent lang={lang} context={context} contentId={contentId} />;
}
const paramPage = params.get('page');
if ((paramPage !== null && paramPage === 'menu') || module.show_menuinmoduletop !== 0) {
return <PicoMenu lang={lang} context={context} />;
}
}
return <PageNotFound lang={lang} />;
}} />
</Switch>;
return <PicoPage lang={lang} dirname={dirname} virtualpath={virtualpath} context={context} />;
}
}
+28 -16
View File
@@ -1,10 +1,11 @@
import React, { Fragment } from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { Helmet } from '@dr.pogodin/react-helmet';
import { Fragment } from 'react';
import { Link } from 'react-router';
import PageNotFound from '../common/lib/PageNotFound';
import { MultiLang } from '../config';
import type { MultiLang } from '../config';
import Functions from '../functions';
import PicoContext, { PicoCategoryData, PicoContentData } from './lib/PicoContext';
import type PicoContext from './lib/PicoContext';
import type { PicoCategoryData, PicoContentData } from './lib/PicoContext';
interface Props {
lang: MultiLang;
@@ -28,7 +29,10 @@ const PicoCategory = (props: Props) => {
return (
<div className="pico_container">
<Helmet>
<title>{Functions.mlang(category.title, lang)} - {Functions.mlang(pico.name, lang)} - {Functions.siteTitle(lang)}</title>
<title>
{Functions.mlang(category.title, lang)} - {Functions.mlang(pico.name, lang)} -{' '}
{Functions.siteTitle(lang)}
</title>
</Helmet>
{pico.show_breadcrumbs !== 0 && (
<div className="pico_breadcrumbs">
@@ -36,7 +40,9 @@ const PicoCategory = (props: Props) => {
return (
<Fragment key={parentCategory.id}>
{idx > 0 && <>&nbsp;&gt;&nbsp;</>}
<Link key={parentCategory.id} to={context.getUrl(parentCategory.link)}>{Functions.mlang(parentCategory.title, lang)}</Link>
<Link key={parentCategory.id} to={context.getUrl(parentCategory.link)}>
{Functions.mlang(parentCategory.title, lang)}
</Link>
</Fragment>
);
})}
@@ -44,20 +50,20 @@ const PicoCategory = (props: Props) => {
{Functions.mlang(category.title, lang)}
</div>
)}
{catId === 0 && pico.message !== '' && (
<p>{Functions.mlang(pico.message, lang)}</p>
)}
{catId === 0 && pico.message !== '' && <p>{Functions.mlang(pico.message, lang)}</p>}
<h1>{Functions.mlang(category.title, lang)}</h1>
{category.desc !== '' && (
<p>{Functions.mlang(category.desc, lang)}</p>
)}
{category.desc !== '' && <p>{Functions.mlang(category.desc, lang)}</p>}
{subCategories.length > 0 && (
<>
<h2>{Functions.mlang('[en]Subcageroies[/en][ja]サブカテゴリー[/ja]', lang)}</h2>
{subCategories.map((subCategory: PicoCategoryData) => {
return (
<dl key={subCategory.id} className="pico_subcategory">
<dt><Link to={context.getUrl(subCategory.link)}>{Functions.mlang(subCategory.title, lang)}</Link></dt>
<dt>
<Link to={context.getUrl(subCategory.link)}>
{Functions.mlang(subCategory.title, lang)}
</Link>
</dt>
<dd>{Functions.mlang(subCategory.desc, lang)}</dd>
</dl>
);
@@ -69,7 +75,13 @@ const PicoCategory = (props: Props) => {
<h2>{Functions.mlang('[en]Contents[/en][ja]コンテンツ[/ja]', lang)}</h2>
<ul className="pico_list_contents">
{contents.map((content: PicoContentData) => {
return <li key={content.id}><Link to={context.getUrl(content.link)}>{Functions.mlang(content.title, lang)}</Link></li>;
return (
<li key={content.id}>
<Link to={context.getUrl(content.link)}>
{Functions.mlang(content.title, lang)}
</Link>
</li>
);
})}
</ul>
</>
@@ -78,4 +90,4 @@ const PicoCategory = (props: Props) => {
);
};
export default PicoCategory;
export default PicoCategory;
+24 -15
View File
@@ -1,14 +1,15 @@
import React, { Component, Fragment } from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { HashLink } from 'react-router-hash-link';
import { Helmet } from '@dr.pogodin/react-helmet';
import { Component, Fragment } from 'react';
import { Link } from 'react-router';
import HashLink from '../common/lib/HashLink';
import Loading from '../common/lib/Loading';
import NoticeSiteHasBeenArchived from '../common/lib/NoticeSiteHasBeenArchived';
import PageNotFound from '../common/lib/PageNotFound';
import XoopsCode from '../common/lib/XoopsCode';
import { MultiLang } from '../config';
import type { MultiLang } from '../config';
import Functions from '../functions';
import PicoContext, { PicoCategoryData, PicoPageData } from './lib/PicoContext';
import type PicoContext from './lib/PicoContext';
import type { PicoCategoryData, PicoPageData } from './lib/PicoContext';
interface Props {
lang: MultiLang;
@@ -22,7 +23,6 @@ interface State {
}
class PicoContent extends Component<Props, State> {
private isActive: boolean;
constructor(props: Props) {
@@ -40,7 +40,7 @@ class PicoContent extends Component<Props, State> {
this.updatePage(contentId);
}
componentDidUpdate(prevProps: Props, prevState: State) {
componentDidUpdate(prevProps: Props) {
const { context, contentId } = this.props;
const prevContext = prevProps.context;
const prevContentId = prevProps.contentId;
@@ -79,19 +79,26 @@ class PicoContent extends Component<Props, State> {
return (
<div className="pico_container">
<Helmet>
<title>{Functions.mlang(page.title, lang)} - {Functions.mlang(pico.name, lang)} - {Functions.siteTitle(lang)}</title>
<title>
{Functions.mlang(page.title, lang)} - {Functions.mlang(pico.name, lang)} -{' '}
{Functions.siteTitle(lang)}
</title>
</Helmet>
{context.isShowTitle() && <>
<h2>{Functions.mlang(pico.name, lang)}</h2>
<NoticeSiteHasBeenArchived lang={lang} />
</>}
{context.isShowTitle() && (
<>
<h2>{Functions.mlang(pico.name, lang)}</h2>
<NoticeSiteHasBeenArchived lang={lang} />
</>
)}
{pico.show_breadcrumbs !== 0 && (
<div className="pico_breadcrumbs">
{categories.map((category: PicoCategoryData, idx: number) => {
return (
<Fragment key={category.id}>
{idx > 0 && <>&nbsp;&gt;&nbsp;</>}
<Link key={category.id} to={context.getUrl(category.link)}>{Functions.mlang(category.title, lang)}</Link>
<Link key={category.id} to={context.getUrl(category.link)}>
{Functions.mlang(category.title, lang)}
</Link>
</Fragment>
);
})}
@@ -102,7 +109,9 @@ class PicoContent extends Component<Props, State> {
<XoopsCode lang={lang} text={page.content[lang]} dohtml={true} />
</div>
<div className="bottom_of_content_body">
<HashLink to="#top_of_pico_body">{Functions.mlang('[en]Jump to the top[/en][ja]この記事の1行目に飛ぶ[/ja]', lang)}</HashLink>
<HashLink to="#top_of_pico_body">
{Functions.mlang('[en]Jump to the top[/en][ja]この記事の1行目に飛ぶ[/ja]', lang)}
</HashLink>
</div>
</div>
);
+32 -20
View File
@@ -1,10 +1,10 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { Helmet } from '@dr.pogodin/react-helmet';
import { Link } from 'react-router';
import PageNotFound from '../common/lib/PageNotFound';
import { MultiLang } from '../config';
import type { MultiLang } from '../config';
import Functions from '../functions';
import PicoContext, { PicoCategoryData, PicoContentData } from './lib/PicoContext';
import type PicoContext from './lib/PicoContext';
import type { PicoCategoryData, PicoContentData } from './lib/PicoContext';
interface Props {
lang: MultiLang;
@@ -22,7 +22,7 @@ const PicoMenuCategory = (props: PicoMenuCategoryProps) => {
const { lang, context, category, depth } = props;
const subCategories = context.getSubCategories(category.id);
const contents = context.getCategoryContents(category.id);
const level = 'level' + depth;
const level = `level${depth}`;
const link = <Link to={context.getUrl(category.link)}>{Functions.mlang(category.title, lang)}</Link>;
let title = <h6>{link}</h6>;
switch (depth) {
@@ -49,21 +49,32 @@ const PicoMenuCategory = (props: PicoMenuCategoryProps) => {
<div className={level}>
<ul className="pico_list_contents_in_menu">
{contents.map((content: PicoContentData) => {
return <li key={content.id}><Link to={context.getUrl(content.link)}>{Functions.mlang(content.title, lang)}</Link></li>;
return (
<li key={content.id}>
<Link to={context.getUrl(content.link)}>
{Functions.mlang(content.title, lang)}
</Link>
</li>
);
})}
</ul>
</div>
)}
{subCategories.length > 0 && (
<>
{subCategories.map((subCategory: PicoCategoryData) => {
return <PicoMenuCategory key={subCategory.id} lang={lang} context={context} category={subCategory} depth={depth + 1} />;
})}
</>
)}
{subCategories.length > 0 &&
subCategories.map((subCategory: PicoCategoryData) => {
return (
<PicoMenuCategory
key={subCategory.id}
lang={lang}
context={context}
category={subCategory}
depth={depth + 1}
/>
);
})}
</>
);
}
};
const PicoMenu = (props: Props) => {
const { lang, context } = props;
@@ -78,14 +89,15 @@ const PicoMenu = (props: Props) => {
return (
<div className="pico_container pico_menu">
<Helmet>
<title>{Functions.mlang('[en]Menu[/en][ja]メニュー[/ja]', lang)} - {Functions.mlang(pico.name, lang)} - {Functions.siteTitle(lang)}</title>
<title>
{Functions.mlang('[en]Menu[/en][ja]メニュー[/ja]', lang)} - {Functions.mlang(pico.name, lang)} -{' '}
{Functions.siteTitle(lang)}
</title>
</Helmet>
{pico.message !== '' && (
<p>{Functions.mlang(pico.message, lang)}</p>
)}
{pico.message !== '' && <p>{Functions.mlang(pico.message, lang)}</p>}
<PicoMenuCategory lang={lang} context={context} category={category} depth={1} />
</div>
);
};
export default PicoMenu;
export default PicoMenu;
+1 -1
View File
@@ -103,7 +103,7 @@ table.pico_form_table td.pico_waiting {
table.pico_form_table td,
table.pico_form_table th {
color: black;
vertical-align: top !important;
vertical-align: top;
text-align: left;
}
p.pico_submit {
+13 -11
View File
@@ -1,5 +1,6 @@
import axios from 'axios';
import ky from 'ky';
import loki from 'lokijs';
import Config from '../../config';
export interface PicoModuleData {
name: string;
@@ -40,13 +41,12 @@ export interface PicoPageData {
content: {
en: string;
ja: string;
}
};
}
export const PICO_CATEGORY_ID_ROOT = 65535;
class PicoContext {
private isInitialized: boolean;
private dirname: string;
private showTitle: boolean;
@@ -70,7 +70,7 @@ class PicoContext {
}
getUrl(path: string): string {
return `/modules/${this.dirname}` + (path !== '' ? `/${path}` : '');
return `/modules/${this.dirname}${path !== '' ? `/${path}` : ''}`;
}
getModule(): PicoModuleData | null {
@@ -129,9 +129,10 @@ class PicoContext {
async getPage(contentId: number): Promise<PicoPageData | null> {
let page: PicoPageData | null = null;
try {
const response = await axios.get(this.getUrl(`${contentId}.json`));
page = response.data as PicoPageData;
} catch (err) {
page = await ky
.get(new URL(Config.BASE_URL + this.getUrl(`${contentId}.json`), window.location.href))
.json<PicoPageData>();
} catch (_err) {
// ignore
}
return page;
@@ -140,8 +141,9 @@ class PicoContext {
async initialize(): Promise<boolean> {
if (!this.isInitialized) {
try {
const response = await axios.get(this.getUrl('index.json'));
const index = response.data as PicoIndexData;
const index = await ky
.get(new URL(Config.BASE_URL + this.getUrl('index.json'), window.location.href))
.json<PicoIndexData>();
this.module = index.module;
index.categories.forEach((value) => {
this.categories.insert(value);
@@ -149,7 +151,7 @@ class PicoContext {
index.contents.forEach((value) => {
this.contents.insert(value);
});
} catch (err) {
} catch (_err) {
// ignore
}
this.isInitialized = true;
@@ -158,4 +160,4 @@ class PicoContext {
}
}
export default PicoContext;
export default PicoContext;
+2 -4
View File
@@ -1,7 +1,6 @@
import PicoContext from './PicoContext';
class PicoUtils {
private static contexts: Map<string, PicoContext> = new Map<string, PicoContext>();
private constructor() {
@@ -9,14 +8,13 @@ class PicoUtils {
}
public static getContext(dirname: string, showTitle: boolean): PicoContext {
let context = this.contexts.get(dirname);
let context = PicoUtils.contexts.get(dirname);
if (typeof context === 'undefined') {
context = new PicoContext(dirname, showTitle);
this.contexts.set(dirname, context);
PicoUtils.contexts.set(dirname, context);
}
return context;
}
}
export default PicoUtils;
-1
View File
@@ -1 +0,0 @@
/// <reference types="react-scripts" />
-149
View File
@@ -1,149 +0,0 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL,
window.location.href
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}
-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/extend-expect';
+21
View File
@@ -0,0 +1,21 @@
import ky from 'ky';
import Config from './config';
import type { IndexesData } from './xoonips/lib/IndexUtil';
/**
* Data converted from the original XOOPS modules. It is served with the rest of
* static-contents/ rather than bundled, so the part every page needs is fetched
* once at start up and handed to the singletons before the app renders. The
* pico, bulletin and credits modules load their own data on demand.
*/
export interface SiteData {
tree: IndexesData;
}
// ky hands the URL to fetch()/Request, which rejects bare relative paths.
const resolve = (path: string): string => new URL(Config.BASE_URL + path, window.location.href).toString();
export const loadSiteData = async (): Promise<SiteData> => {
const tree = await ky.get(resolve('/modules/xoonips/tree.json')).json<IndexesData>();
return { tree };
};
+11
View File
@@ -76,3 +76,14 @@
.database :global(.advancedSearch .fieldDate input) {
margin: 0 5px 0 0;
}
/* sanitize.css forms.css leaves form controls transparent; keep them on white. */
.database :global(.formButton),
.database :global(.fieldInputText),
.database :global(.fieldSelect),
.database :global(.fieldDate select),
.database :global(.fieldDate input),
.database :global(.listTable select) {
background-color: #ffffff;
color: #000000;
}
+59 -47
View File
@@ -1,8 +1,7 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { Redirect, Route, RouteComponentProps, Switch, withRouter } from 'react-router-dom';
import { Helmet } from '@dr.pogodin/react-helmet';
import { Navigate, Route, Routes, useLocation } from 'react-router';
import PageNotFound from '../common/lib/PageNotFound';
import { MultiLang } from '../config';
import type { MultiLang } from '../config';
import Functions from '../functions';
import ItemUtil from './lib/ItemUtil';
import styles from './Xoonips.module.css';
@@ -14,58 +13,71 @@ import XoonipsSearchByItemType from './XoonipsSearchByItemType';
import XoonipsSearchByKeyword from './XoonipsSearchByKeyword';
import XoonipsTop from './XoonipsTop';
interface Props extends RouteComponentProps {
interface Props {
lang: MultiLang;
}
const Xoonips: React.FC<Props> = (props: Props) => {
const { lang, location } = props;
const toId = (value: string | null): number | null =>
value !== null && /^\d+$/.test(value) ? parseInt(value, 10) : null;
/**
* The pages keep the addresses of the original XooNIps 4 module
* (`detail.php?item_id=N`, `list.php?index_id=N`, `search.php?op=...`), so
* the page is chosen by the script name and its query string.
*/
const XoonipsSearch = (props: Props) => {
const { lang } = props;
const location = useLocation();
const query = new URLSearchParams(location.search);
query.delete('ml_lang');
const op = query.get('op') || '';
query.delete('op');
switch (op) {
case 'quick': {
const { type, keyword } = ItemUtil.getSearchKeywordByQuery(location.search);
return <XoonipsSearchByKeyword lang={lang} type={type} keyword={keyword} />;
}
case 'advanced': {
const searchItemType = query.get('search_itemtype');
if (searchItemType !== null) {
return <XoonipsSearchByItemType lang={lang} itemType={searchItemType} itemSubType="" />;
}
if (Array.from(query).length > 0) {
return <XoonipsSearchByAdvancedKeyword lang={lang} />;
}
return <XoonipsAdvancedSearch lang={lang} />;
}
}
return <PageNotFound lang={lang} />;
};
const Xoonips = (props: Props) => {
const { lang } = props;
const location = useLocation();
const query = new URLSearchParams(location.search);
return (
<div className={`xoonips ${styles.database}`}>
<Helmet>
<title>{Functions.mlang('[en]Database[/en][ja]データベース[/ja]', lang)} - {Functions.siteTitle(lang)}</title>
<title>
{Functions.mlang('[en]Database[/en][ja]データベース[/ja]', lang)} - {Functions.siteTitle(lang)}
</title>
</Helmet>
<Switch>
<Route exact path="/modules/xoonips" render={(props: RouteComponentProps) => <XoonipsTop lang={lang} {...props} />} />
<Route exact path="/modules/xoonips/index.php" render={() => <Redirect to="/modules/xoonips" />} />
<Route exact path="/modules/xoonips/detail.php" render={() => {
const idStr = query.get('item_id');
const doi = query.get('id');
const id = idStr !== null && /^\d+$/.test(idStr) ? parseInt(idStr, 10) : null;
return <XoonipsDetailItem lang={lang} id={id} doi={doi} />;
}} />
<Route exact path="/modules/xoonips/list.php" render={() => {
const idStr = query.get('index_id');
const id = idStr !== null && /^\d+$/.test(idStr) ? parseInt(idStr, 10) : null;
return <XoonipsSearchByIndexId lang={lang} id={id} />;
}} />
<Route exact path="/modules/xoonips/search.php" render={() => {
const op = query.get('op') || '';
query.delete('op');
switch (op) {
case 'quick': {
const { type, keyword } = ItemUtil.getSearchKeywordByQuery(location.search);
return <XoonipsSearchByKeyword lang={lang} type={type} keyword={keyword} />
}
case 'advanced': {
const search_itemtype = query.get('search_itemtype');
if (search_itemtype !== null) {
return <XoonipsSearchByItemType lang={lang} itemType={search_itemtype} itemSubType="" />
}
if (Array.from(query).length > 0) {
return <XoonipsSearchByAdvancedKeyword {...props} />
}
return <XoonipsAdvancedSearch {...props} />
}
}
return <PageNotFound lang={lang} />;
}} />
<Route component={PageNotFound} />
</Switch>
<Routes>
<Route index element={<XoonipsTop lang={lang} />} />
<Route path="index.php" element={<Navigate to="/modules/xoonips" replace />} />
<Route
path="detail.php"
element={<XoonipsDetailItem lang={lang} id={toId(query.get('item_id'))} doi={query.get('id')} />}
/>
<Route
path="list.php"
element={<XoonipsSearchByIndexId lang={lang} id={toId(query.get('index_id'))} />}
/>
<Route path="search.php" element={<XoonipsSearch lang={lang} />} />
<Route path="*" element={<PageNotFound lang={lang} />} />
</Routes>
</div>
);
}
};
export default withRouter(Xoonips);
export default Xoonips;
+32 -35
View File
@@ -1,48 +1,45 @@
import React, { Component } from 'react';
import { RouteComponentProps } from 'react-router';
import Config, { MultiLang } from '../config';
import { useState } from 'react';
import { useNavigate } from 'react-router';
import Config, { type MultiLang } from '../config';
import Functions from '../functions';
import ItemType from './item-type';
import AdvancedSearchQuery from './lib/AdvancedSearchQuery';
import ItemUtil from './lib/ItemUtil';
interface Props extends RouteComponentProps {
interface Props {
lang: MultiLang;
}
class XoonipsAdvancedSearch extends Component<Props> {
const XoonipsAdvancedSearch = (props: Props) => {
const { lang } = props;
const navigate = useNavigate();
// The child fields mutate this instance directly, so it must survive re-renders.
const [query] = useState(() => new AdvancedSearchQuery());
private query: AdvancedSearchQuery = new AdvancedSearchQuery();
constructor(props: Props) {
super(props);
this.handleClickSearchButton = this.handleClickSearchButton.bind(this);
}
handleClickSearchButton() {
if (!this.query.empty()) {
const url = ItemUtil.getSearchByAdvancedKeywordsUrl(this.query);
this.props.history.push(url);
const handleClickSearchButton = () => {
if (!query.empty()) {
void navigate(ItemUtil.getSearchByAdvancedKeywordsUrl(query));
}
}
};
render() {
const { lang } = this.props;
return (
<div className="advancedSearch">
<h3>{Functions.mlang('[en]Search Items[/en][ja]アイテム検索[/ja]', lang)}</h3>
<div className="search">
<button className="formButton" onClick={this.handleClickSearchButton}>Search</button>
</div>
{Config.XOONIPS_ITEMTYPES.map((type) => {
return <ItemType.AdvancedSearch key={type} type={type} lang={lang} query={this.query} />;
})}
<div className="search">
<button className="formButton" onClick={this.handleClickSearchButton}>Search</button>
</div>
return (
<div className="advancedSearch">
<h3>{Functions.mlang('[en]Search Items[/en][ja]アイテム検索[/ja]', lang)}</h3>
<div className="search">
<button type="button" className="formButton" onClick={handleClickSearchButton}>
Search
</button>
</div>
);
}
}
{Config.XOONIPS_ITEMTYPES.map((type) => {
return <ItemType.AdvancedSearch key={type} type={type} lang={lang} query={query} />;
})}
<div className="search">
<button type="button" className="formButton" onClick={handleClickSearchButton}>
Search
</button>
</div>
</div>
);
};
export default XoonipsAdvancedSearch;
export default XoonipsAdvancedSearch;
+44 -58
View File
@@ -1,76 +1,62 @@
import React, { Component } from 'react';
import { Helmet } from 'react-helmet';
import { Helmet } from '@dr.pogodin/react-helmet';
import { useEffect, useState } from 'react';
import Loading from '../common/lib/Loading';
import PageNotFound from '../common/lib/PageNotFound';
import { MultiLang } from '../config';
import type { MultiLang } from '../config';
import Functions from '../functions';
import ItemType from './item-type';
import ItemUtil, { Item } from './lib/ItemUtil';
import ItemUtil, { type Item } from './lib/ItemUtil';
interface Props {
lang: MultiLang;
id: number | null;
doi: string | null
doi: string | null;
}
interface State {
loading: boolean;
item: Item | null;
}
const XoonipsDetailItem = (props: Props) => {
const { lang, id, doi } = props;
const [loading, setLoading] = useState(true);
const [item, setItem] = useState<Item | null>(null);
class XoonipsDetailItem extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
loading: true,
item: null,
useEffect(() => {
let active = true;
const receive = (found: Item | null) => {
if (active) {
setItem(found);
setLoading(false);
}
};
}
componentDidMount() {
this.updateItem();
}
componentDidUpdate(prevProps: Props, prevState: State) {
const { id, doi } = this.props;
if (id !== prevProps.id || doi !== prevProps.doi) {
this.updateItem();
}
}
updateItem() {
const { id, doi } = this.props;
if (doi !== null) {
ItemUtil.getByDoi(doi, (item) => {
this.setState({ loading: false, item });
});
ItemUtil.getByDoi(doi, receive);
} else if (id !== null) {
ItemUtil.get(id, (item) => {
this.setState({ loading: false, item });
});
ItemUtil.get(id, receive);
} else {
receive(null);
}
}
return () => {
active = false;
};
}, [id, doi]);
render() {
const { lang } = this.props;
if (this.state.loading) {
return <Loading />;
}
if (this.state.item === null) {
return <PageNotFound lang={lang} />;
}
return (
<>
<Helmet>
<title>{Functions.mlang(this.state.item.title, lang)} - {Functions.mlang('[en]Database[/en][ja]データベース[/ja]', lang)} - {Functions.siteTitle(lang)}</title>
</Helmet>
<h3>{Functions.mlang('[en]Detail[/en][ja]詳細[/ja]', lang)}</h3>
<br />
<ItemType.Detail lang={lang} item={this.state.item} />
</>
);
if (loading) {
return <Loading />;
}
}
if (item === null) {
return <PageNotFound lang={lang} />;
}
return (
<>
<Helmet>
<title>
{Functions.mlang(item.title, lang)} -{' '}
{Functions.mlang('[en]Database[/en][ja]データベース[/ja]', lang)} - {Functions.siteTitle(lang)}
</title>
</Helmet>
<h3>{Functions.mlang('[en]Detail[/en][ja]詳細[/ja]', lang)}</h3>
<br />
<ItemType.Detail lang={lang} item={item} />
</>
);
};
export default XoonipsDetailItem;
export default XoonipsDetailItem;
+23 -48
View File
@@ -1,56 +1,31 @@
import React, { Component } from 'react';
import { RouteComponentProps } from 'react-router';
import { MultiLang } from '../config';
import AdvancedSearchQuery from './lib/AdvancedSearchQuery';
import ItemUtil, { SearchCallbackFunc, SortCondition } from './lib/ItemUtil';
import { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router';
import type { MultiLang } from '../config';
import ItemUtil, { type SearchCallbackFunc, type SortCondition } from './lib/ItemUtil';
import XoonipsListItem from './lib/XoonipsListItem';
interface Props extends RouteComponentProps {
lang: MultiLang
interface Props {
lang: MultiLang;
}
interface State {
search: string;
query: AdvancedSearchQuery;
}
const XoonipsSearchByAdvancedKeyword = (props: Props) => {
const { lang } = props;
const location = useLocation();
const query = useMemo(() => ItemUtil.getAdvancedSearchQueryByQuery(location.search), [location.search]);
class XoonipsSearchByAdvancedKeyword extends Component<Props, State> {
const searchFunc = useCallback(
(condition: SortCondition, func: SearchCallbackFunc) => {
ItemUtil.getListByAdvancedSearchQuery(query, condition, func);
},
[query]
);
constructor(props: Props) {
super(props);
const search = props.location.search;
const query = ItemUtil.getAdvancedSearchQueryByQuery(search);
this.state = { search, query };
this.searchFunc = this.searchFunc.bind(this);
}
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
const search = nextProps.location.search;
if (prevState.search !== search) {
const query = ItemUtil.getAdvancedSearchQueryByQuery(search);
return { search, query };
}
return null;
}
getUrl() {
return ItemUtil.getSearchByAdvancedKeywordsUrl(this.state.query);
}
searchFunc(condition: SortCondition, func: SearchCallbackFunc) {
ItemUtil.getListByAdvancedSearchQuery(this.state.query, condition, func);
}
render() {
const { lang } = this.props;
const baseUrl = this.getUrl();
return (
<div className="list">
<h3>Listing item</h3>
<XoonipsListItem lang={lang} url={baseUrl} search={this.searchFunc} />
</div>
);
}
}
return (
<div className="list">
<h3>Listing item</h3>
<XoonipsListItem lang={lang} url={ItemUtil.getSearchByAdvancedKeywordsUrl(query)} search={searchFunc} />
</div>
);
};
export default XoonipsSearchByAdvancedKeyword;
+46 -58
View File
@@ -1,72 +1,60 @@
import React, { Component, Fragment } from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { Helmet } from '@dr.pogodin/react-helmet';
import { Fragment, useCallback } from 'react';
import { Link } from 'react-router';
import PageNotFound from '../common/lib/PageNotFound';
import { MultiLang } from '../config';
import type { MultiLang } from '../config';
import Functions from '../functions';
import IndexUtil, { Index, INDEX_ID_PUBLIC } from './lib/IndexUtil';
import ItemUtil, { SearchCallbackFunc, SortCondition } from './lib/ItemUtil';
import IndexUtil, { INDEX_ID_PUBLIC, type Index } from './lib/IndexUtil';
import ItemUtil, { type SearchCallbackFunc, type SortCondition } from './lib/ItemUtil';
import XoonipsListIndex from './lib/XoonipsListIndex';
import XoonipsListItem from './lib/XoonipsListItem';
interface Props {
export interface Props {
lang: MultiLang;
id: number | null;
}
class XoonipsSearchByIndexId extends Component<Props> {
const XoonipsSearchByIndexId = (props: Props) => {
const { lang, id } = props;
const indexId = id ?? INDEX_ID_PUBLIC;
constructor(props: Props) {
super(props);
this.searchFunc = this.searchFunc.bind(this);
}
const searchFunc = useCallback(
(condition: SortCondition, func: SearchCallbackFunc) => {
if (indexId === 0) {
func({ total: 0, data: [] });
} else {
ItemUtil.getListByIndexId(indexId, condition, func);
}
},
[indexId]
);
getUrl() {
const { id } = this.props;
const indexId = id === null ? INDEX_ID_PUBLIC : id;
if (indexId === 0) {
return '/';
}
return IndexUtil.getUrl(indexId);
const index = IndexUtil.get(indexId);
if (index === null) {
return <PageNotFound lang={lang} />;
}
searchFunc(condition: SortCondition, func: SearchCallbackFunc) {
const { id } = this.props;
const indexId = id === null ? INDEX_ID_PUBLIC : id;
if (id === 0) {
const res = { total: 0, data: [] };
func(res);
} else {
ItemUtil.getListByIndexId(indexId, condition, func);
}
}
render() {
const { lang, id } = this.props;
const indexId = id === null ? INDEX_ID_PUBLIC : id;
const index = IndexUtil.get(indexId);
if (index === null) {
return <PageNotFound lang={lang} />;
}
const baseUrl = this.getUrl();
const pIndexes = IndexUtil.getParents(indexId);
const parents = pIndexes.map((value: Index) => {
const url: string = IndexUtil.getUrl(value.id);
return <Fragment key={value.id}>/ <Link to={url}>{value.title}</Link> </Fragment>;
});
const title = pIndexes.map((value) => { return '/' + value.title; }).join('');
return (
<div className="list">
<Helmet>
<title>{Functions.mlang(title, lang)} - {Functions.mlang('[en]Database[/en][ja]データベース[/ja]', lang)} - {Functions.siteTitle(lang)}</title>
</Helmet>
<h3>{Functions.mlang('[en]Listing item[/en][ja]アイテム一覧[/ja]', lang)}</h3>
<div>{parents}</div>
<XoonipsListIndex lang={lang} index={index} />
<XoonipsListItem lang={lang} url={baseUrl} search={this.searchFunc} />
</div>
);
}
}
const baseUrl = indexId === 0 ? '/' : IndexUtil.getUrl(indexId);
const pIndexes = IndexUtil.getParents(indexId);
const parents = pIndexes.map((value: Index) => (
<Fragment key={value.id}>
/ <Link to={IndexUtil.getUrl(value.id)}>{Functions.mlang(value.title, lang)}</Link>{' '}
</Fragment>
));
const title = pIndexes.map((value) => `/${Functions.mlang(value.title, lang)}`).join('');
return (
<div className="list">
<Helmet>
<title>
{Functions.mlang(title, lang)} - {Functions.mlang('[en]Database[/en][ja]データベース[/ja]', lang)} -{' '}
{Functions.siteTitle(lang)}
</title>
</Helmet>
<h3>{Functions.mlang('[en]Listing item[/en][ja]アイテム一覧[/ja]', lang)}</h3>
<div>{parents}</div>
<XoonipsListIndex lang={lang} index={index} />
<XoonipsListItem lang={lang} url={baseUrl} search={searchFunc} />
</div>
);
};
export default XoonipsSearchByIndexId;
+25 -36
View File
@@ -1,47 +1,36 @@
import React, { Component } from 'react';
import { MultiLang } from '../config';
import { useCallback } from 'react';
import type { MultiLang } from '../config';
import Functions from '../functions';
import ItemUtil, { SearchCallbackFunc, SortCondition } from './lib/ItemUtil';
import ItemUtil, { type SearchCallbackFunc, type SortCondition } from './lib/ItemUtil';
import XoonipsListItem from './lib/XoonipsListItem';
interface Props {
lang: MultiLang;
itemType: string | null;
itemSubType: string | null;
itemType: string;
itemSubType: string;
}
class XoonipsSearchByItemType extends Component<Props> {
const XoonipsSearchByItemType = (props: Props) => {
const { lang, itemType, itemSubType } = props;
constructor(props: Props) {
super(props);
this.searchFunc = this.searchFunc.bind(this);
}
const searchFunc = useCallback(
(condition: SortCondition, func: SearchCallbackFunc) => {
if (itemType === '') {
func({ total: 0, data: [] });
} else {
ItemUtil.getListByItemType(itemType, itemSubType, condition, func);
}
},
[itemType, itemSubType]
);
searchFunc(condition: SortCondition, func: SearchCallbackFunc) {
const { itemType, itemSubType } = this.props;
if (itemType === null || itemSubType === null) {
const res = { total: 0, data: [] };
func(res);
} else {
ItemUtil.getListByItemType(itemType, itemSubType, condition, func);
}
}
getUrl() {
const { itemType, itemSubType } = this.props;
return ItemUtil.getItemTypeSearchUrl(itemType, itemSubType);
}
render() {
const { lang } = this.props;
const baseUrl = this.getUrl();
return (
<div className="list">
<h3>{Functions.mlang('[en]Listing item[/en][ja]アイテム一覧[/ja]', lang)}</h3>
<XoonipsListItem lang={lang} url={baseUrl} search={this.searchFunc} />
</div>
);
}
}
const baseUrl = ItemUtil.getItemTypeSearchUrl(itemType, itemSubType);
return (
<div className="list">
<h3>{Functions.mlang('[en]Listing item[/en][ja]アイテム一覧[/ja]', lang)}</h3>
<XoonipsListItem lang={lang} url={baseUrl} search={searchFunc} />
</div>
);
};
export default XoonipsSearchByItemType;
+25 -35
View File
@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { MultiLang } from '../config';
import { useCallback } from 'react';
import type { MultiLang } from '../config';
import Functions from '../functions';
import ItemUtil, { SearchCallbackFunc, SortCondition } from './lib/ItemUtil';
import ItemUtil, { type SearchCallbackFunc, type SortCondition } from './lib/ItemUtil';
import XoonipsListItem from './lib/XoonipsListItem';
interface Props {
@@ -10,39 +10,29 @@ interface Props {
keyword: string;
}
class XoonipsSearchByKeyword extends Component<Props> {
const XoonipsSearchByKeyword = (props: Props) => {
const { lang, type, keyword } = props;
constructor(props: Props) {
super(props);
this.searchFunc = this.searchFunc.bind(this);
}
const searchFunc = useCallback(
(condition: SortCondition, func: SearchCallbackFunc) => {
if (keyword === '') {
func({ total: 0, data: [] });
} else {
ItemUtil.getListByKeyword(type, keyword, condition, func);
}
},
[type, keyword]
);
getUrl() {
const { type, keyword } = this.props;
return ItemUtil.getSearchByKeywordUrl(type, keyword);
}
searchFunc(condition: SortCondition, func: SearchCallbackFunc) {
const { type, keyword } = this.props;
if (keyword === '') {
const res = { total: 0, data: [] };
func(res);
} else {
ItemUtil.getListByKeyword(type, keyword, condition, func);
}
}
render() {
const { lang, keyword } = this.props;
const baseUrl = this.getUrl();
return (
<div className="list">
<h3>{Functions.mlang('[en]Listing item[/en][ja]アイテム一覧[/ja]', lang)}</h3>
<p>{Functions.mlang('[en]Search Keyword[/en][ja]検索キーワード[/ja]', lang)} : {keyword}</p>
<XoonipsListItem lang={lang} url={baseUrl} search={this.searchFunc} />
</div>
);
}
}
return (
<div className="list">
<h3>{Functions.mlang('[en]Listing item[/en][ja]アイテム一覧[/ja]', lang)}</h3>
<p>
{Functions.mlang('[en]Search Keyword[/en][ja]検索キーワード[/ja]', lang)} : {keyword}
</p>
<XoonipsListItem lang={lang} url={ItemUtil.getSearchByKeywordUrl(type, keyword)} search={searchFunc} />
</div>
);
};
export default XoonipsSearchByKeyword;
+1 -1
View File
@@ -16,4 +16,4 @@
padding: 5px;
vertical-align: middle;
font-size: large;
}
}
+7 -8
View File
@@ -1,5 +1,4 @@
import React from 'react';
import Config, { MultiLang } from '../config';
import Config, { type MultiLang } from '../config';
import ItemType from './item-type';
import styles from './XoonipsTop.module.css';
@@ -7,7 +6,7 @@ interface Props {
lang: MultiLang;
}
const XoonipsTop: React.FC<Props> = (props: Props) => {
const XoonipsTop = (props: Props) => {
const { lang } = props;
const types: string[][] = [];
const len = Config.XOONIPS_ITEMTYPES.length;
@@ -19,12 +18,12 @@ const XoonipsTop: React.FC<Props> = (props: Props) => {
return (
<table className={styles.itemTypes}>
<tbody>
{types.map((value, idx) => {
{types.map((value) => {
return (
<tr key={idx}>
{value.map((type, idx) => {
<tr key={value.join('-')}>
{value.map((type) => {
return (
<td key={idx} className={styles.itemType}>
<td key={type} className={styles.itemType}>
{type !== '' && <ItemType.Top lang={lang} type={type} />}
</td>
);
@@ -35,6 +34,6 @@ const XoonipsTop: React.FC<Props> = (props: Props) => {
</tbody>
</table>
);
}
};
export default XoonipsTop;
-42
View File
@@ -1,42 +0,0 @@
import React, { Component } from 'react';
import { Redirect, RouteComponentProps, withRouter } from 'react-router';
import PageNotFound from '../common/lib/PageNotFound';
import { MultiLang } from '../config';
import Functions from '../functions';
interface Props extends RouteComponentProps {
lang: MultiLang;
}
class XoonipsXoopsPathRedirect extends Component<Props> {
getRedirectUrl(): string {
const { location } = this.props;
const pathname = location.pathname || '';
const query = new URLSearchParams(location.search);
const search = new RegExp('^/modules/xoonips(?:/+(.*))?$');
const matches = pathname.match(search);
if (matches === null) {
return '';
}
const path = matches[1] || '';
switch (path) {
case '':
case 'index.php': {
return '/modules/xoonips';
}
}
return '';
}
render() {
const { lang } = this.props;
const url = this.getRedirectUrl();
if (url === '') {
return <PageNotFound lang={lang} />;
}
return <Redirect to={url} />;
}
}
export default withRouter(XoonipsXoopsPathRedirect);
-1
View File
@@ -1 +0,0 @@
[]
-1
View File
@@ -1 +0,0 @@
[{"id":2,"title":"Public","num_of_items":0,"children":[{"id":593,"title":"Publication","num_of_items":0,"children":[{"id":665,"title":"Advances in Neuroinformatics","num_of_items":0,"children":[{"id":692,"title":"Advances in Neuroinformatics VI","num_of_items":1,"children":[]},{"id":687,"title":"Advances in Neuroinformatics V","num_of_items":1,"children":[]},{"id":596,"title":"Advances in Neuroinformatics IV","num_of_items":80,"children":[]},{"id":595,"title":"Advances in Neuroinformatics III","num_of_items":1,"children":[]},{"id":594,"title":"Advances in Neuroinformatics II","num_of_items":1,"children":[]},{"id":668,"title":"Advances in Neuroinformatics I","num_of_items":1,"children":[]}]}]},{"id":618,"title":"News & Topics","num_of_items":54,"children":[]},{"id":23,"title":"Newsletter","num_of_items":91,"children":[]},{"id":406,"title":"AINI","num_of_items":0,"children":[{"id":532,"title":"2016","num_of_items":3,"children":[]},{"id":519,"title":"2015","num_of_items":63,"children":[]},{"id":407,"title":"2014","num_of_items":41,"children":[]},{"id":650,"title":"2012","num_of_items":0,"children":[]},{"id":671,"title":"2017","num_of_items":2,"children":[]},{"id":688,"title":"2018","num_of_items":2,"children":[{"id":690,"title":"Tutorial 2","num_of_items":15,"children":[]}]}]},{"id":449,"title":"NBNI","num_of_items":3,"children":[]},{"id":534,"title":"Hackathon","num_of_items":0,"children":[{"id":535,"title":"2016","num_of_items":8,"children":[]},{"id":679,"title":"2018","num_of_items":56,"children":[]}]},{"id":657,"title":"Tutorial","num_of_items":0,"children":[{"id":659,"title":"Benham's Top","num_of_items":6,"children":[]},{"id":676,"title":"Semantic description of EEG","num_of_items":8,"children":[]}]},{"id":661,"title":"Document","num_of_items":2,"children":[{"id":496,"title":"Poster","num_of_items":39,"children":[]},{"id":629,"title":"Brocher","num_of_items":2,"children":[]},{"id":664,"title":"PF report","num_of_items":4,"children":[]},{"id":681,"title":"[en]Program[\/en][ja]プログラム[\/ja]","num_of_items":1,"children":[]}]}]}]
+27 -9
View File
@@ -4,8 +4,10 @@
border-bottom: 1px solid #404040;
border-right: 1px solid #404040;
background-color: white;
height: 400px;
width: calc(100% - 6px);
/* border-box totals: 400 + 3px padding + 1px border per side, and a width
that keeps the 2px the original content-box rule overflowed by. */
height: 408px;
width: calc(100% + 2px);
overflow: auto;
margin: 0 auto;
padding: 3px;
@@ -14,6 +16,9 @@
.formButton {
margin: 3px 3px 10px;
/* sanitize.css forms.css leaves buttons transparent */
background-color: #ffffff;
color: #000000;
}
.indexTree div span {
@@ -39,24 +44,37 @@
display: inline-block;
height: 20px;
width: 9px;
background: url(../assets/images/tree_line.png);
background: url(../assets/images/tree_line.png) no-repeat -18px 0;
}
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-indent .rc-tree-indent-unit:not(:last-child)) {
/* treeData has a single root, so the first indent unit never carries a line. */
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-indent .rc-tree-indent-unit:first-child) {
display: none;
}
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-indent .rc-tree-indent-unit:not(.rc-tree-indent-unit-end)) {
background-position: -9px 0;
}
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-indent .rc-tree-indent-unit:not(:last-child).rc-tree-indent-unit-end) {
background-position: -18px 0;
/* The node's own branch (├ or └) is drawn after the indent units. */
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-indent)::after {
content: "";
display: inline-block;
vertical-align: bottom;
height: 20px;
width: 9px;
background: url(../assets/images/tree_line.png) no-repeat 0 0;
}
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-indent .rc-tree-indent-unit:last-child.rc-tree-indent-unit-end) {
.indexTree:global(.rc-tree .rc-tree-treenode.rc-tree-treenode-leaf-last .rc-tree-indent)::after {
background-position: -27px 0;
}
.indexTree:global(.rc-tree .rc-tree-treenode:first-child .rc-tree-indent)::after {
display: none;
}
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-switcher) {
display: inline-block;
height: 20px;
width: 16px;
margin-right: 2px;
background: url(../assets/images/tree_node.png);
background: url(../assets/images/tree_node.png) no-repeat;
cursor: pointer;
}
.indexTree:global(.rc-tree .rc-tree-treenode:first-child .rc-tree-switcher.rc-tree-switcher-noop) {
@@ -88,4 +106,4 @@
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-node-content-wrapper .rc-tree-title) {
vertical-align: middle;
line-height: 20px;
}
}
+78 -107
View File
@@ -1,124 +1,95 @@
import Tree from 'rc-tree';
import { DataNode, EventDataNode } from 'rc-tree/lib/interface';
import React, { Component, ReactText } from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
import { MultiLang } from '../../config';
import type { DataNode, EventDataNode } from 'rc-tree/lib/interface';
import { type Key, useMemo, useState } from 'react';
import { useNavigate } from 'react-router';
import type { MultiLang } from '../../config';
import Functions from '../../functions';
import IndexUtil, { Index, INDEX_ID_PUBLIC } from '../lib/IndexUtil';
import IndexUtil, { INDEX_ID_PUBLIC, type Index } from '../lib/IndexUtil';
import styles from './IndexTree.module.css';
interface Props extends RouteComponentProps {
interface Props {
lang: MultiLang;
}
interface State {
tree: DataNode[];
interface TreeData {
elements: DataNode[];
expandableKeys: string[];
expandedKeys: string[];
selectedKeys: number[];
defaultExpandedKeys: string[];
}
class XooNIpsIndexTreeBlock extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.handleClickOpenAll = this.handleClickOpenAll.bind(this);
this.handleClickCloseAll = this.handleClickCloseAll.bind(this);
this.handleExpand = this.handleExpand.bind(this);
this.handleSelect = this.handleSelect.bind(this);
const res = this.load();
this.state = {
tree: res.elements,
expandableKeys: res.keys,
expandedKeys: res.expandedKeys,
selectedKeys: [],
};
}
load() {
const { lang } = this.props;
let keys: string[] = [];
let eKeys: string[] = [];
const makeTreeNode = (index: Index, depth: number): DataNode => {
const title = Functions.mlang(index.title, lang) + (index.numOfItems > 0 ? ' (' + index.numOfItems + ')' : '');
const children = IndexUtil.getChildren(index.id);
if (children.length === 0) {
return { key: String(index.id), title: title };
}
const childTreeNodes = children.map((value: Index) => {
return makeTreeNode(value, depth + 1);
});
if (depth < 1) {
eKeys.push(String(index.id));
}
keys.push(String(index.id));
return { key: String(index.id), title: title, children: childTreeNodes };
};
let elements: DataNode[] = [];
const index = IndexUtil.get(INDEX_ID_PUBLIC);
if (index !== null) {
elements.push(makeTreeNode(index, 0));
const load = (lang: MultiLang): TreeData => {
const expandableKeys: string[] = [];
const defaultExpandedKeys: string[] = [];
const makeTreeNode = (index: Index, depth: number): DataNode => {
const title = Functions.mlang(index.title, lang) + (index.numOfItems > 0 ? ` (${index.numOfItems})` : '');
const children = IndexUtil.getChildren(index.id);
if (children.length === 0) {
return { key: String(index.id), title: title };
}
return {
elements: elements,
keys: keys,
expandedKeys: eKeys,
const childTreeNodes = children.map((value: Index) => makeTreeNode(value, depth + 1));
if (depth < 1) {
defaultExpandedKeys.push(String(index.id));
}
expandableKeys.push(String(index.id));
return { key: String(index.id), title: title, children: childTreeNodes };
};
const elements: DataNode[] = [];
const index = IndexUtil.get(INDEX_ID_PUBLIC);
if (index !== null) {
elements.push(makeTreeNode(index, 0));
}
return { elements, expandableKeys, defaultExpandedKeys };
};
componentDidUpdate(prevProps: Props, prevState: State) {
const { lang } = this.props;
if (prevProps.lang !== lang) {
const res = this.load();
this.setState({
tree: res.elements,
selectedKeys: [],
});
const toStringKeys = (keys: Key[]): string[] => keys.map((key) => (typeof key === 'string' ? key : String(key)));
const IndexTree = (props: Props) => {
const { lang } = props;
const data = useMemo(() => load(lang), [lang]);
const [expandedKeys, setExpandedKeys] = useState<string[]>(data.defaultExpandedKeys);
const navigate = useNavigate();
const handleExpand = (
keys: Key[],
_info: { node: EventDataNode<DataNode>; expanded: boolean; nativeEvent: MouseEvent }
) => {
setExpandedKeys(toStringKeys(keys));
};
const handleSelect = (
selectedKeys: Key[],
_info: {
event: 'select';
selected: boolean;
node: EventDataNode<DataNode>;
selectedNodes: DataNode[];
nativeEvent: MouseEvent;
}
}
) => {
const selectedKey = selectedKeys[0] ?? 0;
const key = typeof selectedKey === 'string' ? parseInt(selectedKey, 10) : Number(selectedKey);
void navigate(IndexUtil.getUrl(key));
};
handleClickOpenAll() {
this.setState({ expandedKeys: this.state.expandableKeys })
}
return (
<div className={styles.container}>
<button type="button" className={styles.formButton} onClick={() => setExpandedKeys(data.expandableKeys)}>
open all
</button>
<button type="button" className={styles.formButton} onClick={() => setExpandedKeys([])}>
close all
</button>
<Tree
className={styles.indexTree}
expandedKeys={expandedKeys}
selectedKeys={[]}
onExpand={handleExpand}
onSelect={handleSelect}
showIcon={false}
treeData={data.elements}
/>
</div>
);
};
handleClickCloseAll() {
this.setState({ expandedKeys: [] })
}
handleExpand(expandedKeys: ReactText[], info: {
node: EventDataNode;
expanded: boolean;
nativeEvent: MouseEvent;
}): void {
const keys: string[] = expandedKeys.map((key) => {
return typeof key === 'string' ? key : String(key);
});
this.setState({ expandedKeys: keys });
}
handleSelect(selectedKeys: ReactText[], info: {
event: 'select';
selected: boolean;
node: EventDataNode;
selectedNodes: DataNode[];
nativeEvent: MouseEvent;
}): void {
const selectedKey = selectedKeys.shift() || 0;
const key = typeof selectedKey === 'string' ? parseInt(selectedKey, 10) : selectedKey;
const url = IndexUtil.getUrl(key);
this.props.history.push(url);
this.setState({ selectedKeys: [] });
}
render() {
return (
<div className={styles.container}>
<button className={styles.formButton} onClick={this.handleClickOpenAll}>open all</button>
<button className={styles.formButton} onClick={this.handleClickCloseAll}>close all</button>
<Tree className={styles.indexTree} expandedKeys={this.state.expandedKeys} selectedKeys={this.state.selectedKeys} onExpand={this.handleExpand} onSelect={this.handleSelect} showIcon={false} treeData={this.state.tree} />
</div>
);
}
}
export default withRouter(XooNIpsIndexTreeBlock);
export default IndexTree;
-28
View File
@@ -1,28 +0,0 @@
.heading {
margin: 0 0 3px;
font-size: 100%;
font-weight: normal;
}
.item {
display: flex;
white-space: nowrap;
width: 100%;
}
.order {
margin-right: 5px;
}
.title {
overflow: hidden;
text-overflow: ellipsis;
}
.count {
margin-left: 10px;
}
.star {
margin-left: 5px;
}
-181
View File
@@ -1,181 +0,0 @@
import axios from 'axios';
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import Loading from '../../common/lib/Loading';
import { MultiLang } from '../../config';
import Functions from '../../functions';
import imageStar from '../assets/images/star.gif';
import ItemUtil, { ItemCore } from '../lib/ItemUtil';
import styles from './Rankings.module.css';
interface RankingsData {
accessed?: {
item_id: number;
doi: string;
title: string;
count: number;
}[],
downloaded?: {
item_id: number;
doi: string;
title: string;
count: number;
}[],
contributed?: {
uid: number;
uname: string;
name: string;
count: number;
}[],
searched?: {
keyword: string;
count: number;
}[];
};
interface Props {
lang: MultiLang;
}
interface State {
loading: boolean;
rankings: RankingsData | null;
}
const orderLabel = (order: number, lang: string) => {
return String(order) + (lang === 'en' ? Functions.ordinal(order) : '位')
}
class Rankings extends React.Component<Props, State> {
private isActive: boolean;
constructor(props: Props) {
super(props);
this.state = {
loading: true,
rankings: null
};
this.isActive = false;
}
componentDidMount() {
this.isActive = true;
this.updatePage();
}
componentWillUnmount() {
this.isActive = false;
}
async updatePage() {
try {
const response = await axios.get('/modules/xoonips/rankings.json');
const rankings = response.status === 200 ? response.data as RankingsData : null;
if (this.isActive) {
this.setState({ loading: false, rankings });
}
} catch (e) {
if (this.isActive) {
this.setState({ loading: false, rankings: null });
}
}
}
render() {
const { lang } = this.props;
const { loading, rankings } = this.state;
if (loading) {
return <Loading />;
}
if (rankings === null) {
return <div>Failed to get data resource.</div>;
}
const items = [];
if (rankings.accessed) {
items.push({
title: '[en]Frequently accessed items[/en][ja]頻繁に閲覧されるアイテム[/ja]',
list: rankings.accessed.map((item, idx) => {
const url = ItemUtil.getUrl(item as ItemCore);
const title = Functions.mlang(item.title, lang);
const order = orderLabel(idx + 1, lang);
return (
<div key={idx} className={styles.item}>
<div className={styles.order}>{order}</div>
<div className={styles.title}><Link to={url} title={title}>{title}</Link></div>
{idx === 0 && <div className={styles.star}><img src={imageStar} alt={order} /></div>}
</div>
);
}),
});
}
if (rankings.downloaded) {
items.push({
title: '[en]Frequently downloaded items[/en][ja]頻繁にダウンロードされるアイテム[/ja]',
list: rankings.downloaded.map((item, idx) => {
const url = ItemUtil.getUrl(item as ItemCore);
const title = Functions.mlang(item.title, lang);
const order = orderLabel(idx + 1, lang);
return (
<div key={idx} className={styles.item}>
<div className={styles.order}>{order}</div>
<div className={styles.title}><Link to={url} title={title}>{title}</Link></div>
{idx === 0 && <div className={styles.star}><img src={imageStar} alt={order} /></div>}
</div>
);
}),
});
}
if (rankings.contributed) {
items.push({
title: '[en]Most active contributers[/en][ja]最も多くアイテムを公開したユーザ[/ja]',
list: rankings.contributed.map((item, idx) => {
const name = item.name !== '' ? item.name + ' (' + item.uname + ')' : item.uname;
const title = Functions.mlang(name, lang);
const order = orderLabel(idx + 1, lang);
return (
<div key={idx} className={styles.item}>
<div className={styles.order}>{order}</div>
<div className={styles.uname}>{title}</div>
<div className={styles.count}>({item.count})</div>
{idx === 0 && <div className={styles.star}><img src={imageStar} alt={order} /></div>}
</div>
);
}),
});
}
if (rankings.searched) {
items.push({
title: '[en]Frequently searched keywords[/en][ja]頻繁に検索されるキーワード[/ja]',
list: rankings.searched.map((item, idx) => {
const url = ItemUtil.getSearchByKeywordUrl('all', item.keyword);
const title = 'Search by: ' + item.keyword;
const order = orderLabel(idx + 1, lang);
return (
<div key={idx} className={styles.item}>
<div className={styles.order}>{order}</div>
<div className={styles.title}><Link to={url} title={title}>{item.keyword}</Link></div>
{idx === 0 && <div className={styles.star}><img src={imageStar} alt={order} /></div>}
</div>
);
}),
});
}
return (
<div>
{items.map((item, idx) => {
return (
<Fragment key={idx}>
{idx !== 0 && <hr />}
<h3 className={styles.heading}>{Functions.mlang(item.title, lang)}</h3>
<div>{item.list}</div>
</Fragment>
);
})}
</div>
);
}
}
export default Rankings;
@@ -1,28 +0,0 @@
.heading {
margin: 0 0 3px;
font-size: 100%;
font-weight: normal;
}
.item {
display: flex;
white-space: nowrap;
width: 100%;
}
.order {
margin-right: 5px;
}
.title {
overflow: hidden;
text-overflow: ellipsis;
}
.date {
margin-left: 10px;
}
.star {
margin-left: 5px;
}
-98
View File
@@ -1,98 +0,0 @@
import axios from 'axios';
import moment from 'moment';
import React from 'react';
import { Link } from 'react-router-dom';
import Loading from '../../common/lib/Loading';
import { MultiLang } from '../../config';
import Functions from '../../functions';
import imageStar from '../assets/images/star.gif';
import ItemUtil, { ItemCore } from '../lib/ItemUtil';
import styles from './RecentContents.module.css';
interface RecentContentData {
item_id: number;
doi: string;
last_update_date: number;
title: string;
};
type RecentContentsData = RecentContentData[];
interface Props {
lang: MultiLang;
}
interface State {
loading: boolean;
contents: RecentContentsData | null;
}
class RecentContents extends React.Component<Props, State> {
private isActive: boolean;
constructor(props: Props) {
super(props);
this.state = {
loading: true,
contents: null
};
this.isActive = false;
}
componentDidMount() {
this.isActive = true;
this.updatePage();
}
componentWillUnmount() {
this.isActive = false;
}
async updatePage() {
try {
const response = await axios.get('/modules/xoonips/recent-contents.json');
const contents = response.status === 200 ? response.data as RecentContentsData : null;
if (this.isActive) {
this.setState({ loading: false, contents });
}
} catch (e) {
if (this.isActive) {
this.setState({ loading: false, contents: null });
}
}
}
render() {
const { lang } = this.props;
const { loading, contents } = this.state;
if (loading) {
return <Loading />;
}
if (contents === null) {
return <div>Failed to get data resource.</div>;
}
const list = contents.map((item, idx) => {
const url = ItemUtil.getUrl(item as ItemCore);
const title = Functions.mlang(item.title, lang);
const date = moment(new Date(item.last_update_date * 1000)).format('Y/M/D');
const order = String(idx + 1) + (lang === 'en' ? Functions.ordinal(idx + 1) : '位');
return (
<div key={idx} className={styles.item}>
<div className={styles.order}>{order}</div>
<div className={styles.title}><Link className={styles.title} to={url} title={title}>{title}</Link></div>
<div className={styles.date}> ({date})</div>
{idx === 0 && <div className={styles.star}><img src={imageStar} alt={order} /></div>}
</div>
);
});
return (
<div>
<h3 className={styles.heading}>{Functions.mlang('[en]Newly Arrived Items[/en][ja]新着アイテム[/ja]', lang)}</h3>
<div>{list}</div>
</div>
);
}
}
export default RecentContents;
+55 -73
View File
@@ -1,86 +1,68 @@
import React, { ChangeEvent, Component, FormEvent } from 'react';
import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
import Config, { MultiLang } from '../../config';
import { type ChangeEvent, type FormEvent, useEffect, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router';
import Config, { type MultiLang } from '../../config';
import Functions from '../../functions';
import ItemUtil from '../lib/ItemUtil';
interface Props extends RouteComponentProps {
interface Props {
lang: MultiLang;
}
interface State {
type: string;
keyword: string;
path: string;
}
const SEARCH_PATH = '/modules/xoonips/search.php';
class XooNIpsSearchBlock extends Component<Props, State> {
const Search = (props: Props) => {
const { lang } = props;
const location = useLocation();
const navigate = useNavigate();
const [type, setType] = useState('all');
const [keyword, setKeyword] = useState('');
constructor(props: Props) {
super(props);
const { location } = props;
this.state = {
type: 'all',
keyword: '',
path: location.pathname + location.search + location.hash,
};
this.handleChangeType = this.handleChangeType.bind(this);
this.handleChangeKeyword = this.handleChangeKeyword.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
const { location } = nextProps;
const path = location.pathname + location.search + location.hash
if (path !== prevState.path) {
const { type, keyword } = ItemUtil.getSearchKeywordByQuery(location.search);
return { type, keyword, path };
// Landing on the results page adopts the query it was called with; moving
// anywhere else leaves whatever the visitor has typed alone.
useEffect(() => {
if (location.pathname === SEARCH_PATH && new URLSearchParams(location.search).get('op') === 'quick') {
const parsed = ItemUtil.getSearchKeywordByQuery(location.search);
setType(parsed.type);
setKeyword(parsed.keyword);
}
return null;
}
}, [location.pathname, location.search]);
handleChangeKeyword(event: ChangeEvent<HTMLInputElement>) {
const keyword = event.target.value.trim();
this.setState({ keyword });
}
handleChangeType(event: ChangeEvent<HTMLSelectElement>) {
const type = event.target.value;
this.setState({ type });
}
handleSubmit(event: FormEvent<HTMLFormElement>) {
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const url = ItemUtil.getSearchByKeywordUrl(this.state.type, this.state.keyword);
this.props.history.push(url);
}
void navigate(ItemUtil.getSearchByKeywordUrl(type, keyword));
};
render() {
const { lang } = this.props;
const options = [
{ value: 'all', label: '[en]All[/en][ja]全て[/ja]' },
{ value: 'basic', label: '[en]Title & Keyword[/en][ja]タイトル&キーワード[/ja]' },
];
const options = [
{ value: 'all', label: '[en]All[/en][ja]全て[/ja]' },
{ value: 'basic', label: '[en]Title & Keyword[/en][ja]タイトル&キーワード[/ja]' },
];
Config.XOONIPS_ITEMTYPES.forEach((itemType) => {
options.push({ value: itemType, label: Functions.pascalCase(itemType) });
});
return (
<form onSubmit={handleSubmit}>
<input
style={{ width: '170px' }}
type="text"
value={keyword}
onChange={(event: ChangeEvent<HTMLInputElement>) => setKeyword(event.target.value.trim())}
/>
&nbsp;&nbsp;
<select value={type} onChange={(event: ChangeEvent<HTMLSelectElement>) => setType(event.target.value)}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{Functions.mlang(option.label, lang)}
</option>
))}
</select>
<br />
<input className="formButton" type="submit" value="Search" />
&nbsp;&nbsp;&nbsp;&nbsp;
<Link to="/modules/xoonips/search.php?op=advanced">
{Functions.mlang('[en]Advanced[/en][ja]詳細検索[/ja]', lang)}
</Link>
</form>
);
};
Config.XOONIPS_ITEMTYPES.forEach((type) => {
options.push({ value: type, label: Functions.pascalCase(type) })
});
return (
<form onSubmit={this.handleSubmit}>
<input style={{ width: '170px' }} type="text" value={this.state.keyword} onChange={this.handleChangeKeyword} />
&nbsp;&nbsp;
<select value={this.state.type} onChange={this.handleChangeType}>
{options.map((option) => {
return <option key={option.value} value={option.value}>{Functions.mlang(option.label, lang)}</option>;
})}
</select>
<br />
<input className="formButton" type="submit" value="Search" />
&nbsp;&nbsp;&nbsp;&nbsp;
<Link to="/modules/xoonips/search.php?op=advanced">{Functions.mlang('[en]Advanced[/en][ja]詳細検索[/ja]', lang)}</Link>
</form>
);
}
}
export default withRouter(XooNIpsSearchBlock);
export default Search;
@@ -1,11 +1,9 @@
import React from 'react';
import Functions from '../../../functions';
import { ItemBasicLangs } from '../../lib/ItemUtil';
import AdvancedSearchBase, { AdvancedSearchBaseProps } from '../lib/AdvancedSearchBase';
import AdvancedSearchBase, { type AdvancedSearchBaseProps } from '../lib/AdvancedSearchBase';
import ArticleUtil from './ArticleUtil';
class ArticleAdvancedSearch extends AdvancedSearchBase {
constructor(props: AdvancedSearchBaseProps) {
super(props);
this.type = ArticleUtil.type;
@@ -14,37 +12,37 @@ class ArticleAdvancedSearch extends AdvancedSearchBase {
const year = String(now.getFullYear());
const month = String(now.getMonth() + 1);
const mday = String(now.getDate());
this.state.values['doi'] = '';
this.state.values['title'] = '';
this.state.values['author'] = '';
this.state.values['affiliation'] = '';
this.state.values['journal_name'] = '';
this.state.values['journal_volume'] = '';
this.state.values['publication_year'] = '';
this.state.values['publication_month'] = '';
this.state.values['journal_page'] = '';
this.state.values['journal_publisher'] = '';
this.state.values['meeting_name'] = '';
this.state.values['meeting_place'] = '';
this.state.values['meeting_date_from_year'] = year;
this.state.values['meeting_date_from_month'] = month;
this.state.values['meeting_date_from_mday'] = mday;
this.state.values['meeting_date_to_year'] = year;
this.state.values['meeting_date_to_month'] = month;
this.state.values['meeting_date_to_mday'] = mday;
this.state.values['meeting_program_no'] = '';
this.state.values['journal_page'] = '';
this.state.values['journal_page'] = '';
this.state.values['journal_page'] = '';
this.state.values['lang'] = '';
this.state.values.doi = '';
this.state.values.title = '';
this.state.values.author = '';
this.state.values.affiliation = '';
this.state.values.journal_name = '';
this.state.values.journal_volume = '';
this.state.values.publication_year = '';
this.state.values.publication_month = '';
this.state.values.journal_page = '';
this.state.values.journal_publisher = '';
this.state.values.meeting_name = '';
this.state.values.meeting_place = '';
this.state.values.meeting_date_from_year = year;
this.state.values.meeting_date_from_month = month;
this.state.values.meeting_date_from_mday = mday;
this.state.values.meeting_date_to_year = year;
this.state.values.meeting_date_to_month = month;
this.state.values.meeting_date_to_mday = mday;
this.state.values.meeting_program_no = '';
this.state.values.journal_page = '';
this.state.values.journal_page = '';
this.state.values.journal_page = '';
this.state.values.lang = '';
this.state.values['file.article_fulltext_file.original_file_name'] = '';
this.state.values['date_from_year'] = year;
this.state.values['date_from_month'] = month;
this.state.values['date_from_mday'] = mday;
this.state.values['date_to_year'] = year;
this.state.values['date_to_month'] = month;
this.state.values['date_to_mday'] = mday;
this.state.values['keyword'] = '';
this.state.values.date_from_year = year;
this.state.values.date_from_month = month;
this.state.values.date_from_mday = mday;
this.state.values.date_to_year = year;
this.state.values.date_to_month = month;
this.state.values.date_to_mday = mday;
this.state.values.keyword = '';
this.setIgnoreKey('meeting_date_from_year');
this.setIgnoreKey('meeting_date_from_month');
this.setIgnoreKey('meeting_date_from_mday');
@@ -61,88 +59,104 @@ class ArticleAdvancedSearch extends AdvancedSearchBase {
renderJournal() {
const { lang } = this.props;
return <table>
<tbody>
<tr>
<td>{Functions.mlang('[en]Name[/en][ja]名称[/ja]', lang)}</td>
<td>{this.renderFieldInputText('journal_name', 50)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Volume[/en][ja]巻[/ja]', lang)}</td>
<td>{this.renderFieldInputText('journal_volume', 50)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Publication Year[/en][ja]発行年[/ja]', lang)}</td>
<td>{this.renderFieldInputText('publication_year', 10)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Publication Month[/en][ja]発行月[/ja]', lang)}</td>
<td>{this.renderFieldInputText('publication_month', 10)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Page[/en][ja]ページ[/ja]', lang)}</td>
<td>{this.renderFieldInputText('journal_page', 10)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Publisher[/en][ja]出版者[/ja]', lang)}</td>
<td>{this.renderFieldInputText('journal_publisher', 50)}</td>
</tr>
</tbody>
</table>;
return (
<table>
<tbody>
<tr>
<td>{Functions.mlang('[en]Name[/en][ja]名称[/ja]', lang)}</td>
<td>{this.renderFieldInputText('journal_name', 50)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Volume[/en][ja]巻[/ja]', lang)}</td>
<td>{this.renderFieldInputText('journal_volume', 50)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Publication Year[/en][ja]発行年[/ja]', lang)}</td>
<td>{this.renderFieldInputText('publication_year', 10)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Publication Month[/en][ja]発行月[/ja]', lang)}</td>
<td>{this.renderFieldInputText('publication_month', 10)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Page[/en][ja]ページ[/ja]', lang)}</td>
<td>{this.renderFieldInputText('journal_page', 10)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Publisher[/en][ja]出版者[/ja]', lang)}</td>
<td>{this.renderFieldInputText('journal_publisher', 50)}</td>
</tr>
</tbody>
</table>
);
}
renderMeeting() {
const { lang } = this.props;
return <table>
<tbody>
<tr>
<td>{Functions.mlang('[en]Name[/en][ja]名称[/ja]', lang)}</td>
<td>{this.renderFieldInputText('meeting_name', 50)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Place[/en][ja]場所[/ja]', lang)}</td>
<td>{this.renderFieldInputText('meeting_place', 50)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Date (From)[/en][ja]日付 (自)[/ja]', lang)}</td>
<td>{this.renderFieldDate('', 'meeting_date_from_year', 'meeting_date_from_month', 'meeting_date_from_mday')}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Date (To)[/en][ja]日付 (至)[/ja]', lang)}</td>
<td>{this.renderFieldDate('', 'meeting_date_to_year', 'meeting_date_to_month', 'meeting_date_to_mday')}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Program Num[/en][ja]発表番号[/ja]', lang)}</td>
<td>{this.renderFieldInputText('meeting_program_no', 50)}</td>
</tr>
</tbody>
</table>;
return (
<table>
<tbody>
<tr>
<td>{Functions.mlang('[en]Name[/en][ja]名称[/ja]', lang)}</td>
<td>{this.renderFieldInputText('meeting_name', 50)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Place[/en][ja]場所[/ja]', lang)}</td>
<td>{this.renderFieldInputText('meeting_place', 50)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Date (From)[/en][ja]日付 (自)[/ja]', lang)}</td>
<td>
{this.renderFieldDate(
'',
'meeting_date_from_year',
'meeting_date_from_month',
'meeting_date_from_mday'
)}
</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Date (To)[/en][ja]日付 (至)[/ja]', lang)}</td>
<td>
{this.renderFieldDate(
'',
'meeting_date_to_year',
'meeting_date_to_month',
'meeting_date_to_mday'
)}
</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Program Num[/en][ja]発表番号[/ja]', lang)}</td>
<td>{this.renderFieldInputText('meeting_program_no', 50)}</td>
</tr>
</tbody>
</table>
);
}
renderDate() {
return (
<>
<div>
{this.renderFieldDate('From', 'date_from_year', 'date_from_month', 'date_from_mday')}
</div>
<div>
{this.renderFieldDate('To', 'date_to_year', 'date_to_month', 'date_to_mday')}
</div>
<div>{this.renderFieldDate('From', 'date_from_year', 'date_from_month', 'date_from_mday')}</div>
<div>{this.renderFieldDate('To', 'date_to_year', 'date_to_month', 'date_to_mday')}</div>
</>
);
}
getRows() {
const rows = [
{ label: 'ID', value: this.renderFieldInputText('doi', 50) },
{ label: '[en]Title[/en][ja]タイトル[/ja]', value: this.renderFieldInputText('title', 50) },
{ label: '[en]Author[/en][ja]著者[/ja]', value: this.renderFieldInputText('author', 50) },
{ label: '[en]Affiliation[/en][ja]所属[/ja]', value: this.renderFieldInputText('affiliation', 50) },
{ label: '[en]Author[/en][ja]著者[/ja]', value: this.renderFieldInputText('author', 50) },
{ label: '[en]Affiliation[/en][ja]所属[/ja]', value: this.renderFieldInputText('affiliation', 50) },
{ label: '[en]Journal[/en][ja]ジャーナル[/ja]', value: this.renderJournal() },
{ label: '[en]Meeting[/en][ja]会議[/ja]', value: this.renderMeeting() },
{ label: '[en]Language[/en][ja]言語[/ja]', value: this.renderFieldSelect('lang', ItemBasicLangs) },
{ label: '[en]Full Text[/en][ja]本文ファイル[/ja]', value: this.renderFieldInputText('file.article_fulltext_file.original_file_name', 50) },
{
label: '[en]Full Text[/en][ja]本文ファイル[/ja]',
value: this.renderFieldInputText('file.article_fulltext_file.original_file_name', 50),
},
{ label: '[en]Publication Date[/en][ja]発行日[/ja]', value: this.renderDate() },
{ label: '[en]Keywords[/en][ja]キーワード[/ja]', value: this.renderFieldInputText('keyword', 50) },
];
@@ -150,4 +164,4 @@ class ArticleAdvancedSearch extends AdvancedSearchBase {
}
}
export default ArticleAdvancedSearch;
export default ArticleAdvancedSearch;
+176 -85
View File
@@ -1,96 +1,136 @@
import React from 'react';
import Functions from '../../../functions';
import { ItemArticle } from '../../lib/ItemUtil';
import type { ItemArticle } from '../../lib/ItemUtil';
import DetailBase from '../lib/DetailBase';
import ItemTypeField from '../lib/field';
import ArticleUtil from './ArticleUtil';
class ArticleDetail extends DetailBase {
renderAuthor(item: ItemArticle) {
const { lang } = this.props;
return <table>
<tbody>
{item.author.map((author) => {
const name = author.replace(/(?:\(\d+\))+$/g, '');
const num = author === name ? '' : author.replace(/^.+?((?:\(\d+\))+)$/, '$1').replace(/\)\(/g, ', ').replace(/[()]/g, '');
return <tr key={author}><td>{Functions.mlang(name, lang)}{num !== '' && <span style={{ verticalAlign: 'super', fontSize: '70%' }}> {num}</span>}</td></tr>;
})}
</tbody>
</table>;
return (
<table>
<tbody>
{item.author.map((author) => {
const name = author.replace(/(?:\(\d+\))+$/g, '');
const num =
author === name
? ''
: author
.replace(/^.+?((?:\(\d+\))+)$/, '$1')
.replace(/\)\(/g, ', ')
.replace(/[()]/g, '');
return (
<tr key={author}>
<td>
{Functions.mlang(name, lang)}
{num !== '' && (
<span style={{ verticalAlign: 'super', fontSize: '70%' }}> {num}</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
);
}
renderAffiliation(item: ItemArticle) {
const { lang } = this.props;
return <table>
<tbody>
{item.affiliation.map((affiliation) => {
const name = affiliation.replace(/^(?:\(\d+\))+/g, '');
const num = affiliation === name ? '' : affiliation.replace(/^((?:\(\d+\))+).+?$/, '$1').replace(/\)\(/g, ', ').replace(/[()]/g, '');
return <tr key={affiliation}><td>{num !== '' && <span style={{ verticalAlign: 'super', fontSize: '70%' }}>{num} </span>}{Functions.mlang(name, lang)}</td></tr>;
})}
</tbody>
</table>;
return (
<table>
<tbody>
{item.affiliation.map((affiliation) => {
const name = affiliation.replace(/^(?:\(\d+\))+/g, '');
const num =
affiliation === name
? ''
: affiliation
.replace(/^((?:\(\d+\))+).+?$/, '$1')
.replace(/\)\(/g, ', ')
.replace(/[()]/g, '');
return (
<tr key={affiliation}>
<td>
{num !== '' && (
<span style={{ verticalAlign: 'super', fontSize: '70%' }}>{num} </span>
)}
{Functions.mlang(name, lang)}
</td>
</tr>
);
})}
</tbody>
</table>
);
}
renderJournal(item: ItemArticle) {
const { lang } = this.props;
return <table>
<tbody>
<tr>
<td>{Functions.mlang('[en]Name[/en][ja]名称[/ja]', lang)}</td>
<td>: {Functions.mlang(item.journal_name, lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Volume[/en][ja]巻[/ja]', lang)}</td>
<td>: {Functions.mlang(item.journal_volume, lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Publication Year[/en][ja]発行年[/ja]', lang)}</td>
<td>: {Functions.mlang(item.publication_year.toString(), lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Publication Month[/en][ja]発行月[/ja]', lang)}</td>
<td>: {Functions.mlang(item.publication_month.toString(), lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Page[/en][ja]ページ[/ja]', lang)}</td>
<td>: {Functions.mlang(item.journal_page, lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Publisher[/en][ja]出版者[/ja]', lang)}</td>
<td>: {Functions.mlang(item.journal_publisher, lang)}</td>
</tr>
</tbody>
</table>;
return (
<table>
<tbody>
<tr>
<td>{Functions.mlang('[en]Name[/en][ja]名称[/ja]', lang)}</td>
<td>: {Functions.mlang(item.journal_name, lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Volume[/en][ja]巻[/ja]', lang)}</td>
<td>: {Functions.mlang(item.journal_volume, lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Publication Year[/en][ja]発行年[/ja]', lang)}</td>
<td>: {Functions.mlang(item.publication_year.toString(), lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Publication Month[/en][ja]発行月[/ja]', lang)}</td>
<td>: {Functions.mlang(item.publication_month.toString(), lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Page[/en][ja]ページ[/ja]', lang)}</td>
<td>: {Functions.mlang(item.journal_page, lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Publisher[/en][ja]出版者[/ja]', lang)}</td>
<td>: {Functions.mlang(item.journal_publisher, lang)}</td>
</tr>
</tbody>
</table>
);
}
renderMeeting(item: ItemArticle) {
const { lang } = this.props;
return <table>
<tbody>
<tr>
<td>{Functions.mlang('[en]Name[/en][ja]名称[/ja]', lang)}</td>
<td>: {Functions.mlang(item.meeting_name, lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Place[/en][ja]場所[/ja]', lang)}</td>
<td>: {Functions.mlang(item.meeting_place, lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Date (From)[/en][ja]日付 (自)[/ja]', lang)}</td>
<td>: <ItemTypeField.DateTime lang={lang} date={item.meeting_date_from} onlyDate={true} /></td>
</tr>
<tr>
<td>{Functions.mlang('[en]Date (To)[/en][ja]日付 (至)[/ja]', lang)}</td>
<td>: <ItemTypeField.DateTime lang={lang} date={item.meeting_date_to} onlyDate={true} /></td>
</tr>
<tr>
<td>{Functions.mlang('[en]Program Num[/en][ja]発表番号[/ja]', lang)}</td>
<td>: {Functions.mlang(item.meeting_program_no, lang)}</td>
</tr>
</tbody>
</table>;
return (
<table>
<tbody>
<tr>
<td>{Functions.mlang('[en]Name[/en][ja]名称[/ja]', lang)}</td>
<td>: {Functions.mlang(item.meeting_name, lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Place[/en][ja]場所[/ja]', lang)}</td>
<td>: {Functions.mlang(item.meeting_place, lang)}</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Date (From)[/en][ja]日付 (自)[/ja]', lang)}</td>
<td>
: <ItemTypeField.DateTime lang={lang} date={item.meeting_date_from} onlyDate={true} />
</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Date (To)[/en][ja]日付 (至)[/ja]', lang)}</td>
<td>
: <ItemTypeField.DateTime lang={lang} date={item.meeting_date_to} onlyDate={true} />
</td>
</tr>
<tr>
<td>{Functions.mlang('[en]Program Num[/en][ja]発表番号[/ja]', lang)}</td>
<td>: {Functions.mlang(item.meeting_program_no, lang)}</td>
</tr>
</tbody>
</table>
);
}
getFields() {
@@ -105,20 +145,71 @@ class ArticleDetail extends DetailBase {
{ label: '[en]Affiliation[/en][ja]所属[/ja]', value: this.renderAffiliation(item) },
{ label: '[en]Journal[/en][ja]ジャーナル[/ja]', value: this.renderJournal(item) },
{ label: '[en]Meeting[/en][ja]会議[/ja]', value: this.renderMeeting(item) },
{ label: '[en]Language[/en][ja]言語[/ja]', value: <ItemTypeField.Language lang={lang} itemLang={item.lang} /> },
{ label: '[en]Full Text[/en][ja]本文[/ja]', value: <ItemTypeField.ItemFile lang={lang} file={item.file} type="article_fulltext_file" rights={item.rights} useCc={item.use_cc} ccCommercialUse={item.cc_commercial_use} ccModification={item.cc_modification} /> },
{ label: 'Rights', value: <ItemTypeField.Rights lang={lang} rights={item.rights} useCc={item.use_cc} ccCommercialUse={item.cc_commercial_use} ccModification={item.cc_modification} /> },
{ label: '[en]Release Date[/en][ja]本公開日[/ja]', value: <ItemTypeField.DateTime lang={lang} date={item.date} onlyDate={true} /> },
{ label: '[en]Keyword[/en][ja]キーワード[/ja]', value: <ItemTypeField.Repeatable lang={lang} data={item.keyword} /> },
{ label: '[en]Related item[/en][ja]関連アイテム[/ja]', value: <ItemTypeField.RelatedTo lang={lang} relatedTo={item.related_to} /> },
{ label: '[en]Index[/en][ja]インデックス[/ja]', value: <ItemTypeField.ItemIndex lang={lang} index={item.index} /> },
{ label: '[en]Item Owner[/en][ja]アイテム所有者[/ja]', value: <ItemTypeField.Contributer lang={lang} uname={item.uname} name={item.name} /> },
{ label: '[en]Registered Date[/en][ja]登録日[/ja]', value: <ItemTypeField.DateTime lang={lang} date={item.creation_date} /> },
{ label: '[en]Last Update[/en][ja]最終更新日[/ja]', value: <ItemTypeField.DateTime lang={lang} date={item.last_update_date} /> },
{ label: '[en]Change Log[/en][ja]更新履歴[/ja]', value: <ItemTypeField.ChangeLog lang={lang} changelog={item.changelog} /> },
{
label: '[en]Language[/en][ja]言語[/ja]',
value: <ItemTypeField.Language lang={lang} itemLang={item.lang} />,
},
{
label: '[en]Full Text[/en][ja]本文[/ja]',
value: (
<ItemTypeField.ItemFile
lang={lang}
file={item.file}
type="article_fulltext_file"
rights={item.rights}
useCc={item.use_cc}
ccCommercialUse={item.cc_commercial_use}
ccModification={item.cc_modification}
/>
),
},
{
label: 'Rights',
value: (
<ItemTypeField.Rights
lang={lang}
rights={item.rights}
useCc={item.use_cc}
ccCommercialUse={item.cc_commercial_use}
ccModification={item.cc_modification}
/>
),
},
{
label: '[en]Release Date[/en][ja]本公開日[/ja]',
value: <ItemTypeField.DateTime lang={lang} date={item.date} onlyDate={true} />,
},
{
label: '[en]Keyword[/en][ja]キーワード[/ja]',
value: <ItemTypeField.Repeatable lang={lang} data={item.keyword} />,
},
{
label: '[en]Related item[/en][ja]関連アイテム[/ja]',
value: <ItemTypeField.RelatedTo lang={lang} relatedTo={item.related_to} />,
},
{
label: '[en]Index[/en][ja]インデックス[/ja]',
value: <ItemTypeField.ItemIndex lang={lang} index={item.index} />,
},
{
label: '[en]Item Owner[/en][ja]アイテム所有者[/ja]',
value: <ItemTypeField.Contributer lang={lang} uname={item.uname} name={item.name} />,
},
{
label: '[en]Registered Date[/en][ja]登録日[/ja]',
value: <ItemTypeField.DateTime lang={lang} date={item.creation_date} />,
},
{
label: '[en]Last Update[/en][ja]最終更新日[/ja]',
value: <ItemTypeField.DateTime lang={lang} date={item.last_update_date} />,
},
{
label: '[en]Change Log[/en][ja]更新履歴[/ja]',
value: <ItemTypeField.ChangeLog lang={lang} changelog={item.changelog} />,
},
];
return fields;
}
}
export default ArticleDetail;
export default ArticleDetail;
+22 -10
View File
@@ -1,12 +1,10 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Link } from 'react-router';
import Functions from '../../../functions';
import { ItemArticle } from '../../lib/ItemUtil';
import ListBase, { ListBaseProps } from '../lib/ListBase';
import type { ItemArticle } from '../../lib/ItemUtil';
import ListBase, { type ListBaseProps } from '../lib/ListBase';
import ArticleUtil from './ArticleUtil';
class ArticleList extends ListBase {
constructor(props: ListBaseProps) {
super(props);
this.label = ArticleUtil.label;
@@ -17,19 +15,33 @@ class ArticleList extends ListBase {
const { lang } = this.props;
const item = this.props.item as ItemArticle;
const authors = item.author.map((author, i) => {
return <span key={i} className="author">{i > 0 && ', '}{Functions.mlang(author.replace(/(?:\(\d+\))+$/, ''), lang)}</span>
return (
<span key={author} className="author">
{i > 0 && ', '}
{Functions.mlang(author.replace(/(?:\(\d+\))+$/, ''), lang)}
</span>
);
});
const doi = ArticleUtil.getDOI(item.doi);
return (
<div className="journal">
<div className="title"><Link to={this.url}>{Functions.mlang(item.title, lang)}</Link></div>
<div className="title">
<Link to={this.url}>{Functions.mlang(item.title, lang)}</Link>
</div>
<div className="authors">{authors}</div>
<div className="journal"><span className="journal_name">{Functions.mlang(item.journal_name, lang)}</span> ({item.publication_year})</div>
<div className="journal">
<span className="journal_name">{Functions.mlang(item.journal_name, lang)}</span> (
{item.publication_year})
</div>
<div className="publisher">{Functions.mlang(item.journal_publisher, lang)}</div>
{doi !== '' && <div className="doi"><a href={`https://doi.org/${doi}`}>doi:{doi}</a></div>}
{doi !== '' && (
<div className="doi">
<a href={`https://doi.org/${doi}`}>doi:{doi}</a>
</div>
)}
</div>
);
}
}
export default ArticleList;
export default ArticleList;

Some files were not shown because too many files have changed in this diff Show More