initial commit
@@ -3,17 +3,43 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^16.9.0",
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-scripts": "3.4.0",
|
||||
"typescript": "~3.7.2"
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^10.0.4",
|
||||
"@testing-library/user-event": "^10.3.1",
|
||||
"@types/async-lock": "^1.1.2",
|
||||
"@types/jest": "^25.2.3",
|
||||
"@types/lokijs": "^1.5.3",
|
||||
"@types/node": "^14.0.1",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-helmet": "^6.0.0",
|
||||
"@types/react-html-parser": "^2.0.1",
|
||||
"@types/react-overlays": "^1.1.3",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/react-router-hash-link": "^1.2.1",
|
||||
"@types/xregexp": "^4.3.0",
|
||||
"async-lock": "^1.2.4",
|
||||
"axios": "^0.19.2",
|
||||
"lokijs": "^1.5.8",
|
||||
"moment": "2.25.3",
|
||||
"rc-tree": "^3.2.2",
|
||||
"react": "^16.13.1",
|
||||
"react-app-polyfill": "^1.0.6",
|
||||
"react-cookie": "^4.0.3",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-ga": "^2.7.0",
|
||||
"react-helmet": "^6.0.0",
|
||||
"react-html-parser": "^2.0.2",
|
||||
"react-image-lightbox": "^5.1.1",
|
||||
"react-overlays": "^3.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-hash-link": "^1.2.2",
|
||||
"react-scripts": "3.4.1",
|
||||
"react-spinner-material": "^1.3.1",
|
||||
"rss-parser": "^3.8.0",
|
||||
"split-string": "^6.1.0",
|
||||
"typescript": "~3.9.2",
|
||||
"xregexp": "^4.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@@ -28,12 +54,14 @@
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
"not op_mini all",
|
||||
"ie 11"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
"last 1 safari version",
|
||||
"ie 11"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 894 B |
@@ -7,37 +7,26 @@
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
content="The Japan Node of the INCF coordinates neuroinformatics activities within Japan and represents Japanese efforts in INCF. The Neuroinformatics Japan Center (NIJC) represents the Japan Node. Together with the Japan Node Committee and the Platform Subcommittees, we promotes domestic activities of neuroinformatics. Platform Subcommittee members collaborate to develop databases that are available for use on the website."
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<meta property='og:locale' content='en_US' />
|
||||
<meta property='og:locale:alternate' content='ja_JP' />
|
||||
<meta property='fb:admins' content='1073741825' />
|
||||
<meta property='article:publisher' content='https://www.facebook.com/INCFJapanNode' />
|
||||
<meta property='og:type' content='article'>
|
||||
<meta property='og:title' content='INCF Japan Node' />
|
||||
<meta property='og:url' content='https://www.neuroinf.jp/' />
|
||||
<meta property='og:description' content='The Japan Node of the INCF coordinates neuroinformatics activities within Japan and represents Japanese efforts in INCF. The Neuroinformatics Japan Center (NIJC) represents the Japan Node. Together with the Japan Node Committee and the Platform Subcommittees, we promotes domestic activities of neuroinformatics. Platform Subcommittee members collaborate to develop databases that are available for use on the website.' />
|
||||
<meta property='og:site_name' content='INCF Japan Node' />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<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" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>Top - INCF Japan Node</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,21 +1,11 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"short_name": "INCF Japan Node",
|
||||
"name": "INCF Japan Node Portal Site",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"sizes": "16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
|
||||
@@ -1,38 +1,2 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@import url("./common/assets/xoops.css");
|
||||
@import url("./custom/assets/style.css");
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import { CookiesProvider } from 'react-cookie';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './App.css';
|
||||
import AppRoot from './common/AppRoot';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<CookiesProvider>
|
||||
<BrowserRouter>
|
||||
<AppRoot />
|
||||
</BrowserRouter >
|
||||
</CookiesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
import Loading from '../common/lib/Loading';
|
||||
import PageNotFound from '../common/lib/PageNotFound';
|
||||
import { MultiLang } from '../config';
|
||||
import BulletinArchive from './lib/BulletinArchive';
|
||||
import BulletinContext from './lib/BulletinContext';
|
||||
import BulletinStories from './lib/BulletinStories';
|
||||
import BulletinStory from './lib/BulletinStory';
|
||||
import BulletinUtils from './lib/BulletinUtils';
|
||||
|
||||
interface Props {
|
||||
lang: MultiLang;
|
||||
mydirname: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
lang: MultiLang;
|
||||
mydirname: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
loading: boolean;
|
||||
context: BulletinContext | null;
|
||||
}
|
||||
|
||||
class Bulletin extends React.Component<Props, State> {
|
||||
|
||||
private isActive: boolean;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
context: null,
|
||||
};
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.isActive = true;
|
||||
this.updateContext();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
const { mydirname } = this.props;
|
||||
const prevMydirname = prevProps.mydirname;
|
||||
if (mydirname !== prevMydirname) {
|
||||
this.setState({ loading: true, context: null });
|
||||
this.updateContext();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
async updateContext() {
|
||||
const { mydirname } = this.props;
|
||||
const context = await BulletinUtils.getContext(mydirname);
|
||||
if (this.isActive) {
|
||||
this.setState({ loading: false, context });
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
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>;
|
||||
}
|
||||
}
|
||||
|
||||
export default Bulletin;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React 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 BulletinContext from '../lib/BulletinContext';
|
||||
import BulletinStoryItem from '../lib/BulletinStoryItem';
|
||||
import BulletinUtils from '../lib/BulletinUtils';
|
||||
|
||||
interface Props {
|
||||
lang: MultiLang;
|
||||
mydirname: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
loading: boolean;
|
||||
context: BulletinContext | null;
|
||||
}
|
||||
|
||||
class BulletinRecentNews extends React.Component<Props, State> {
|
||||
|
||||
private isActive: boolean;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
context: null,
|
||||
};
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.isActive = true;
|
||||
this.updateContext();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
const { mydirname } = this.props;
|
||||
const prevMydirname = prevProps.mydirname;
|
||||
if (mydirname !== prevMydirname) {
|
||||
this.setState({ loading: true, context: null });
|
||||
this.updateContext();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
async updateContext() {
|
||||
const { mydirname } = this.props;
|
||||
const context = await BulletinUtils.getContext(mydirname);
|
||||
if (this.isActive) {
|
||||
this.setState({ loading: false, context });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { lang } = this.props;
|
||||
const { loading, context } = this.state;
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (context === null) {
|
||||
return <PageNotFound lang={lang} />;
|
||||
}
|
||||
const module = context.getModule();
|
||||
if (module === null) {
|
||||
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>;
|
||||
}
|
||||
}
|
||||
|
||||
export default BulletinRecentNews;
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PageNotFound from '../../common/lib/PageNotFound';
|
||||
import { MultiLang } from '../../config';
|
||||
import Functions from '../../functions';
|
||||
import BulletinContext from './BulletinContext';
|
||||
import BulletinUtils from './BulletinUtils';
|
||||
|
||||
interface Props {
|
||||
lang: MultiLang;
|
||||
context: BulletinContext;
|
||||
year: number;
|
||||
month: number;
|
||||
}
|
||||
|
||||
const BulletinArchive: React.FC<Props> = (props: Props) => {
|
||||
const { lang, context, year, month } = props;
|
||||
const years = context.getArchiveYearRange();
|
||||
if (years.min === 0 || years.max === 0) {
|
||||
return <PageNotFound lang={lang} />;
|
||||
}
|
||||
const module = context.getModule();
|
||||
if (module === null) {
|
||||
return <PageNotFound lang={lang} />;
|
||||
}
|
||||
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' }}>| <Link to={context.getArchiveUrl()}>{Functions.mlang('[en]Archive[/en][ja]アーカイブ[/ja]', lang)}</Link> |</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>
|
||||
:
|
||||
<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;
|
||||
@@ -0,0 +1,191 @@
|
||||
import axios from 'axios';
|
||||
import loki from 'lokijs';
|
||||
|
||||
export interface BulletinModuleData {
|
||||
name: string;
|
||||
dirname: string;
|
||||
storyhome: number;
|
||||
displaynav: number;
|
||||
disp_list_of_cat: number;
|
||||
stories_of_cat: number;
|
||||
use_pankuzu: number;
|
||||
}
|
||||
|
||||
export interface BulletinTopicData {
|
||||
id: number;
|
||||
pid: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface BulletinStoryData {
|
||||
id: number;
|
||||
posttime: string;
|
||||
topic_id: number;
|
||||
title: string;
|
||||
text: {
|
||||
en: string;
|
||||
ja: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface BulletinIndexData {
|
||||
module: BulletinModuleData;
|
||||
topics: BulletinTopicData[];
|
||||
stories: BulletinStoryData[];
|
||||
}
|
||||
|
||||
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;
|
||||
private module: BulletinModuleData | null;
|
||||
private topics: Collection<BulletinTopicData>;
|
||||
private stories: Collection<BulletinStoryData>;
|
||||
private counter: BulletinCounterData;
|
||||
|
||||
constructor(dirname: string) {
|
||||
this.isInitialized = false;
|
||||
this.dirname = dirname;
|
||||
this.database = new loki(`bulletin_${dirname}`);
|
||||
this.module = null;
|
||||
this.topics = this.database.addCollection('topics');
|
||||
this.stories = this.database.addCollection('stories');
|
||||
this.counter = { count: {}, year: { min: 0, max: 0 } };
|
||||
}
|
||||
|
||||
getUrl(path: string): string {
|
||||
return `/${this.dirname}/${path}`;
|
||||
}
|
||||
|
||||
getStoryUrl(id: number, page: number = 0): string {
|
||||
const params = new URLSearchParams([
|
||||
['page', 'article'],
|
||||
['storyid', id.toString()]
|
||||
]);
|
||||
if (page !== 0) {
|
||||
params.set('storypage', page.toString());
|
||||
}
|
||||
return this.getUrl('index.php?' + params.toString());
|
||||
}
|
||||
|
||||
getTopicUrl(id: number, limit: number, start: number): string {
|
||||
const params = new URLSearchParams();
|
||||
if (id !== 0) {
|
||||
params.set('storytopic', id.toString());
|
||||
}
|
||||
if (limit !== 5) {
|
||||
params.set('storynum', limit.toString());
|
||||
}
|
||||
if (start !== 0) {
|
||||
params.set('start', start.toString());
|
||||
}
|
||||
return this.getUrl('index.php?' + params.toString());
|
||||
}
|
||||
|
||||
getArchiveUrl(year: number = 0, month: number = 0): string {
|
||||
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());
|
||||
}
|
||||
|
||||
getModule(): BulletinModuleData | null {
|
||||
return this.module;
|
||||
}
|
||||
|
||||
getTopic(topicId: number): BulletinTopicData | null {
|
||||
return this.topics.findOne({ id: topicId });
|
||||
}
|
||||
|
||||
getStory(storyId: number): BulletinStoryData | null {
|
||||
return this.stories.findOne({ id: storyId });
|
||||
}
|
||||
|
||||
getTopics(): BulletinTopicData[] {
|
||||
return this.topics.chain().find().simplesort('title').data();
|
||||
}
|
||||
|
||||
getStories(topicId: number, limit: number, start: number): BulletinStoryData[] {
|
||||
const filter = topicId === 0 ? undefined : { topic_id: topicId };
|
||||
return this.stories.chain().find(filter).simplesort('posttime', true).offset(start).limit(limit).data();
|
||||
}
|
||||
|
||||
getStoriesByMonth(year: number, month: number) {
|
||||
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();
|
||||
}
|
||||
|
||||
countStories(topicId: number): number {
|
||||
const filter = topicId === 0 ? undefined : { topic_id: topicId };
|
||||
return this.stories.count(filter);
|
||||
}
|
||||
|
||||
getArchiveYearRange(): { min: number, max: number } {
|
||||
return this.counter.year;
|
||||
}
|
||||
|
||||
getArchiveCounter(year: number, month: number) {
|
||||
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;
|
||||
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);
|
||||
if (index === 0) {
|
||||
this.counter.year.min = year;
|
||||
this.counter.year.max = year;
|
||||
} else {
|
||||
if (this.counter.year.min > year) {
|
||||
this.counter.year.min = year;
|
||||
}
|
||||
if (this.counter.year.max < year) {
|
||||
this.counter.year.max = year;
|
||||
}
|
||||
}
|
||||
if (key in this.counter.count) {
|
||||
this.counter.count[key]++;
|
||||
} else {
|
||||
this.counter.count[key] = 1;
|
||||
}
|
||||
this.stories.insert(value);
|
||||
});
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
this.isInitialized = true;
|
||||
}
|
||||
return this.isInitialized;
|
||||
}
|
||||
}
|
||||
|
||||
export default BulletinContext;
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PageNotFound from '../../common/lib/PageNotFound';
|
||||
import { MultiLang } from '../../config';
|
||||
import Functions from '../../functions';
|
||||
import BulletinContext from './BulletinContext';
|
||||
import BulletinStoryItem from './BulletinStoryItem';
|
||||
import BulletinStoriesSelect from './BulletinStoriesSelect';
|
||||
import BulletinStoriesPageNavi from './BulletinStoriesPageNavi';
|
||||
|
||||
interface Props {
|
||||
lang: MultiLang;
|
||||
context: BulletinContext;
|
||||
topicId: number;
|
||||
storyNum: number;
|
||||
start: number;
|
||||
}
|
||||
|
||||
const BulletinStories: React.FC<Props> = (props: Props) => {
|
||||
const { lang, context, topicId, storyNum, start } = props;
|
||||
const topic = context.getTopic(topicId);
|
||||
if (topicId !== 0 && topic === null) {
|
||||
return <PageNotFound lang={lang} />;
|
||||
}
|
||||
const module = context.getModule();
|
||||
if (module === null) {
|
||||
return <PageNotFound lang={lang} />;
|
||||
}
|
||||
const titles = [Functions.siteTitle(lang), Functions.mlang(module.name, lang)];
|
||||
if (topicId !== 0 && topic !== null) {
|
||||
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' }}>| <Link to={context.getArchiveUrl()}>{Functions.mlang('[en]Archive[/en][ja]アーカイブ[/ja]', lang)}</Link> |</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;
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { MultiLang } from '../../config';
|
||||
import Functions from '../../functions';
|
||||
import BulletinContext from './BulletinContext';
|
||||
|
||||
interface Props {
|
||||
lang: MultiLang;
|
||||
context: BulletinContext;
|
||||
topicId: number;
|
||||
limit: number;
|
||||
start: number;
|
||||
}
|
||||
|
||||
const BulletinStoriesPageNavi: React.FC<Props> = (props: Props) => {
|
||||
const { lang, context, topicId, limit, start } = props;
|
||||
const total = context.countStories(topicId);
|
||||
if (start > total) {
|
||||
return null;
|
||||
}
|
||||
const now = Math.ceil((start + 1) / limit);
|
||||
const min = 1;
|
||||
const max = Math.ceil(total / limit);
|
||||
const prev = now === 1 ? 0 : now - 1;
|
||||
const next = now === max ? 0 : now + 1;
|
||||
let rangeMin = now - 3;
|
||||
if (rangeMin < 2) {
|
||||
rangeMin = 2;
|
||||
}
|
||||
let rangeMax = now + 3;
|
||||
if (rangeMax > max - 1) {
|
||||
rangeMax = max - 1;
|
||||
}
|
||||
const link = (num: number, el?: JSX.Element) => {
|
||||
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[] = [];
|
||||
if (prev !== 0) {
|
||||
items.push(link(prev, <span className="prev">«</span>));
|
||||
}
|
||||
items.push(link(min));
|
||||
if (now - min > 4) {
|
||||
items.push(<>...</>);
|
||||
}
|
||||
for (let range = rangeMin; range <= rangeMax; range++) {
|
||||
items.push(link(range));
|
||||
}
|
||||
if (max - now > 4) {
|
||||
items.push(<>...</>);
|
||||
}
|
||||
items.push(link(max));
|
||||
if (next !== 0) {
|
||||
items.push(link(next, <span className="next">»</span>));
|
||||
}
|
||||
const rangeS = start + 1;
|
||||
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 && <> </>}{el}</Fragment>)}</span>}
|
||||
</nav>;
|
||||
};
|
||||
|
||||
export default BulletinStoriesPageNavi;
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { MultiLang } from '../../config';
|
||||
import Functions from '../../functions';
|
||||
import BulletinContext from './BulletinContext';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
lang: MultiLang;
|
||||
context: BulletinContext;
|
||||
topicId: number;
|
||||
num: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
path: string;
|
||||
topicId: number;
|
||||
num: number;
|
||||
}
|
||||
|
||||
class BulletinStoriesSelect extends React.Component<Props, State> {
|
||||
|
||||
private STORY_NUMS: ReadonlyArray<number> = [5, 10, 15, 20, 30];
|
||||
|
||||
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;
|
||||
event.preventDefault();
|
||||
if (context !== null) {
|
||||
const url = context.getTopicUrl(this.state.topicId, this.state.num, 0);
|
||||
history.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
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}>
|
||||
<option value="0">----</option>
|
||||
{topics.map(topic =>
|
||||
<option key={topic.id} value={topic.id}>{Functions.mlang(topic.title, lang)}</option>
|
||||
)}
|
||||
</select>
|
||||
|
||||
<select value={this.state.num} onChange={this.handleSelectNum}>
|
||||
{this.STORY_NUMS.map((num) => <option key={num} value={num}>{num}</option>)}
|
||||
</select>
|
||||
|
||||
<input type="submit" value={Functions.mlang('[en]Go![/en][ja]送信[/ja]', lang)} />
|
||||
</form>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(BulletinStoriesSelect);
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PageNotFound from '../../common/lib/PageNotFound';
|
||||
import { MultiLang } from '../../config';
|
||||
import Functions from '../../functions';
|
||||
import BulletinContext from './BulletinContext';
|
||||
import BulletinStoryItem from './BulletinStoryItem';
|
||||
|
||||
interface Props {
|
||||
lang: MultiLang;
|
||||
context: BulletinContext;
|
||||
id: number;
|
||||
}
|
||||
|
||||
const BulletinStory: React.FC<Props> = (props: Props) => {
|
||||
const { lang, context, id } = props;
|
||||
const story = context.getStory(id);
|
||||
if (story === null) {
|
||||
return <PageNotFound lang={lang} />;
|
||||
}
|
||||
const topic = context.getTopic(story.topic_id);
|
||||
if (topic === null) {
|
||||
return <PageNotFound lang={lang} />;
|
||||
}
|
||||
const module = context.getModule();
|
||||
if (module === null) {
|
||||
return <PageNotFound lang={lang} />;
|
||||
}
|
||||
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' }}>| <Link to={context.getArchiveUrl()}>{Functions.mlang('[en]Archive[/en][ja]アーカイブ[/ja]', lang)}</Link> |</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default BulletinStory;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PageNotFound from '../../common/lib/PageNotFound';
|
||||
import XoopsCode from '../../common/lib/XoopsCode';
|
||||
import { MultiLang } from '../../config';
|
||||
import Functions from '../../functions';
|
||||
import BulletinContext, { BulletinStoryData } from './BulletinContext';
|
||||
import BulletinUtils from './BulletinUtils';
|
||||
|
||||
interface Props {
|
||||
lang: MultiLang;
|
||||
context: BulletinContext;
|
||||
story: BulletinStoryData;
|
||||
isList: boolean;
|
||||
}
|
||||
|
||||
const BulletinStoryItem: React.FC<Props> = (props: Props) => {
|
||||
const { lang, context, story, isList } = props;
|
||||
const topic = context.getTopic(story.topic_id);
|
||||
const urlTopic = context.getTopicUrl(story.topic_id, 5, 0);
|
||||
const urlStory = context.getStoryUrl(story.id);
|
||||
if (topic === null) {
|
||||
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>
|
||||
:
|
||||
{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"> {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;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { MultiLang } from '../../config';
|
||||
import BulletinContext, { BulletinStoryData } from './BulletinContext';
|
||||
|
||||
class BulletinUtils {
|
||||
|
||||
private static contexts: Map<string, BulletinContext> = new Map<string, BulletinContext>();
|
||||
|
||||
private constructor() {
|
||||
// blcok to access from outside of this class
|
||||
}
|
||||
|
||||
public static async getContext(mydirname: string): Promise<BulletinContext> {
|
||||
let context = this.contexts.get(mydirname);
|
||||
if (typeof context === 'undefined') {
|
||||
context = new BulletinContext(mydirname);
|
||||
await context.initialize();
|
||||
this.contexts.set(mydirname, context);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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月']
|
||||
}
|
||||
return label[lang][month - 1];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BulletinUtils;
|
||||
@@ -0,0 +1,67 @@
|
||||
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 Page from '../custom/Page';
|
||||
|
||||
interface Props extends RouteComponentProps, ReactCookieProps { }
|
||||
interface State {
|
||||
lang: MultiLang;
|
||||
}
|
||||
|
||||
class AppRoot extends Component<Props, State> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { lang: 'en' };
|
||||
if (Config.GOOGLE_ANALYTICS_TRACKING_ID !== '') {
|
||||
ReactGA.initialize(Config.GOOGLE_ANALYTICS_TRACKING_ID);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { pathname } = this.props.location;
|
||||
if (Config.GOOGLE_ANALYTICS_TRACKING_ID !== '') {
|
||||
ReactGA.set({ page: pathname });
|
||||
ReactGA.pageview(pathname);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { pathname, search } = this.props.location;
|
||||
if (Config.GOOGLE_ANALYTICS_TRACKING_ID !== '') {
|
||||
ReactGA.set({ page: pathname });
|
||||
ReactGA.pageview(pathname);
|
||||
}
|
||||
if (pathname !== prevProps.location.pathname || search !== prevProps.location.search) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { lang } = this.state;
|
||||
return <Page lang={lang} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default withCookies(withRouter(AppRoot));
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, RouteComponentProps } from 'react-router';
|
||||
import { MultiLang } from '../config';
|
||||
import PageNotFound from './lib/PageNotFound';
|
||||
|
||||
interface Params {
|
||||
module: string;
|
||||
pathname: string;
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<Params> {
|
||||
lang: MultiLang;
|
||||
}
|
||||
|
||||
class XoopsPathRedirect extends Component<Props> {
|
||||
|
||||
getRedirectUrl() {
|
||||
const { pathname } = this.props.location;
|
||||
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} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default XoopsPathRedirect;
|
||||
|
After Width: | Height: | Size: 175 B |
|
After Width: | Height: | Size: 126 B |
|
After Width: | Height: | Size: 985 B |
|
After Width: | Height: | Size: 121 B |
|
After Width: | Height: | Size: 121 B |
|
After Width: | Height: | Size: 121 B |
|
After Width: | Height: | Size: 905 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 894 B |
|
After Width: | Height: | Size: 856 B |
|
After Width: | Height: | Size: 789 B |
|
After Width: | Height: | Size: 779 B |
|
After Width: | Height: | Size: 475 B |
|
After Width: | Height: | Size: 210 B |
|
After Width: | Height: | Size: 204 B |
|
After Width: | Height: | Size: 174 B |
|
After Width: | Height: | Size: 204 B |
|
After Width: | Height: | Size: 176 B |
|
After Width: | Height: | Size: 207 B |
|
After Width: | Height: | Size: 170 B |
|
After Width: | Height: | Size: 210 B |
|
After Width: | Height: | Size: 278 B |
|
After Width: | Height: | Size: 202 B |
|
After Width: | Height: | Size: 678 B |
|
After Width: | Height: | Size: 263 B |
|
After Width: | Height: | Size: 209 B |
|
After Width: | Height: | Size: 492 B |
|
After Width: | Height: | Size: 205 B |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 736 B |
|
After Width: | Height: | Size: 273 B |
@@ -0,0 +1,17 @@
|
||||
img {border: 0;}
|
||||
|
||||
#xoopsHiddenText {visibility: hidden; color: #000000; font-weight: normal; font-style: normal; text-decoration: none;}
|
||||
|
||||
.pagneutral {font-size: 10px; width: 16px; height: 19px;text-align: center; background-image: url(./images/pagneutral.gif);}
|
||||
.pagact {font-size: 10px; width: 16px; height: 19px;text-align: center; background-image: url(./images/pagact.gif);}
|
||||
.paginact {font-size: 10px; width: 16px; height: 19px;text-align: center; background-image: url(./images/paginact.gif);}
|
||||
|
||||
|
||||
#mainmenu a {text-align:left; display: block; margin: 0; padding: 4px;}
|
||||
#mainmenu a.menuTop {padding-left: 3px;}
|
||||
#mainmenu a.menuMain {padding-left: 3px;}
|
||||
#mainmenu a.menuSub {padding-left: 9px;}
|
||||
|
||||
#usermenu a {text-align:left; display: block; margin: 0; padding: 4px;}
|
||||
#usermenu a.menuTop {}
|
||||
#usermenu a.highlight {color: #0000ff; background-color: #fcc;}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { MultiLang } from '../../config';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
lang: MultiLang;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const langResources = {
|
||||
en: 'English',
|
||||
ja: 'Japanese',
|
||||
};
|
||||
|
||||
const LangLink = (props: Props) => {
|
||||
const { lang, className } = props;
|
||||
const params = new URLSearchParams(props.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>;
|
||||
}
|
||||
|
||||
|
||||
export default withRouter(LangLink);
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import Spinner from 'react-spinner-material';
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignContent: 'center', margin: '100px 0' }}>
|
||||
<Spinner radius={60} color={'#cccccc'} stroke={8} visible={true} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Loading;
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { MultiLang } from '../../config';
|
||||
import Functions from '../../functions';
|
||||
|
||||
interface Props {
|
||||
lang: MultiLang;
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
export default NoticeSiteHasBeenArchived;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { RouteComponentProps, withRouter } from 'react-router';
|
||||
import { MultiLang } from '../../config';
|
||||
import Functions from '../../functions';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
lang: MultiLang;
|
||||
}
|
||||
|
||||
class PageNotFound extends Component<Props> {
|
||||
|
||||
private url = '/';
|
||||
private timer: NodeJS.Timer | null = null;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -0,0 +1,140 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import ReactHtmlParser, { convertNodeToElement } from 'react-html-parser';
|
||||
import { HashLink } from 'react-router-hash-link';
|
||||
import { MultiLang } from '../../config';
|
||||
import Functions from '../../functions';
|
||||
|
||||
interface Props {
|
||||
lang: MultiLang;
|
||||
text: string;
|
||||
dohtml?: boolean;
|
||||
dosmiley?: boolean;
|
||||
doxcode?: boolean;
|
||||
doimage?: boolean;
|
||||
dobr?: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const postConvertXCode = (text: string, doxcode: boolean, doimage: boolean): string => {
|
||||
if (doxcode) {
|
||||
return text.replace(/\[code\](.*)\[\/code\]/sg, (m0, m1) => {
|
||||
const text = convertXCode(Functions.htmlspecialchars(Functions.base64Decode(m1)), doimage);
|
||||
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>';
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
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 = {};
|
||||
text.split(';').forEach((line) => {
|
||||
const line_ = line.trim();
|
||||
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 (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);
|
||||
}
|
||||
}
|
||||
|
||||
const XoopsCode = (props: Props) => {
|
||||
const { lang } = props;
|
||||
let text = props.text;
|
||||
const dohtml = !!props.dohtml;
|
||||
const dosmiley = !!props.dosmiley;
|
||||
const doxcode = !!props.doxcode;
|
||||
const doimage = !!props.doimage;
|
||||
const dobr = !!props.dobr;
|
||||
text = preConvertXCode(text, doxcode);
|
||||
if (!dohtml) {
|
||||
text = Functions.htmlspecialchars(text);
|
||||
text = convertClickable(text);
|
||||
}
|
||||
if (dosmiley) {
|
||||
text = convertSmiley(text);
|
||||
}
|
||||
if (doxcode) {
|
||||
text = convertXCode(text, doimage);
|
||||
}
|
||||
if (dobr) {
|
||||
text = convertBr(text);
|
||||
}
|
||||
text = postConvertXCode(text, doxcode, doimage);
|
||||
text = Functions.mlang(text, lang);
|
||||
return <>{ReactHtmlParser(text, { transform })}</>;
|
||||
}
|
||||
|
||||
export default XoopsCode;
|
||||
@@ -0,0 +1,18 @@
|
||||
const SITE_TITLE = 'INCF Japan Node';
|
||||
const SITE_SLOGAN = 'Top';
|
||||
const GOOGLE_ANALYTICS_TRACKING_ID = 'UA-2365647-1';
|
||||
const XOONIPS_ITEMTYPES = ['journal', 'article', 'generaldata'];
|
||||
const PICO_MODULES = ['aboutus', 'aini', 'hackathon', 'joinus', 'link', 'nbni', 'program', 'publication', 'resource', 'tutorial'];
|
||||
const BULLETIN_MODULES = ['news'];
|
||||
export type MultiLang = 'en' | 'ja';
|
||||
|
||||
const Config = {
|
||||
SITE_TITLE,
|
||||
SITE_SLOGAN,
|
||||
GOOGLE_ANALYTICS_TRACKING_ID,
|
||||
XOONIPS_ITEMTYPES,
|
||||
PICO_MODULES,
|
||||
BULLETIN_MODULES,
|
||||
};
|
||||
|
||||
export default Config;
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import NoticeSiteHasBeenArchived from '../common/lib/NoticeSiteHasBeenArchived';
|
||||
import Config, { MultiLang } from '../config';
|
||||
import Functions from '../functions';
|
||||
|
||||
interface Props {
|
||||
lang: MultiLang;
|
||||
}
|
||||
|
||||
const message = {
|
||||
ja: <p>
|
||||
INCF日本ノードの事務局は2005年に理研BSI神経情報基盤センターとして設置され、2018年4月より理研CBS神経情報基盤開発ユニットに引き継がれ、国内外の多くの皆様のご協力を得て、運営して参りました。
|
||||
CBSにおけるニューロインフォマティクスの新たな方向性を探る中で、神経情報基盤開発ユニットは日本ノード事務局の任を2019年3月末日にて終了することとなりました。
|
||||
</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 { 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>;
|
||||
}
|
||||
|
||||
export default ContactUs;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { MultiLang } from '../config';
|
||||
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 { 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>
|
||||
</ul>
|
||||
<span className="copyright">Copyright (C) 2018 Neuroinformatics Unit, RIKEN Center for Brain Science</span>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,130 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import LangLink from '../common/lib/LangLink';
|
||||
import Config, { 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 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 {
|
||||
lang: MultiLang;
|
||||
}
|
||||
|
||||
interface State {
|
||||
keyword: string;
|
||||
menuid: number;
|
||||
}
|
||||
|
||||
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>) {
|
||||
event.preventDefault();
|
||||
const keyword = this.state.keyword.trim();
|
||||
if (keyword !== '') {
|
||||
const url = ItemUtil.getSearchByKeywordUrl('all', this.state.keyword);
|
||||
this.props.history.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(Header);
|
||||
@@ -0,0 +1,174 @@
|
||||
import React from 'react';
|
||||
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
import Bulletin from '../bulletin/Bulletin';
|
||||
import XoopsPathRedirect from '../common/XoopsPathRedirect';
|
||||
import Config, { MultiLang } from '../config';
|
||||
import Functions from '../functions';
|
||||
import Pico from '../pico/Pico';
|
||||
import XooNIpsIndexTreeBlock from '../xoonips/blocks/IndexTree';
|
||||
import XooNIpsSearchBlock from '../xoonips/blocks/Search';
|
||||
import Xoonips from '../xoonips/Xoonips';
|
||||
import RssBlock from './blocks/RssBlock';
|
||||
import ContactUs from './ContactUs';
|
||||
import Footer from './Footer';
|
||||
import Header from './Header';
|
||||
import RssFeedSites from './RssFeedSites';
|
||||
import TopPage from './TopPage';
|
||||
import Xsearch from './Xsearch';
|
||||
|
||||
interface Props {
|
||||
lang: MultiLang;
|
||||
}
|
||||
|
||||
const hackathonPathes = ['hackathon2018', 'brainhack-global-2017', 'hackathon2016', 'bah2015'];
|
||||
const ainiPathes = ['2014', '2015', '2016', '2017', '2018'];
|
||||
|
||||
const Page: React.FC<Props> = (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="/modules/xsearch" render={() => <Xsearch lang={lang} />} />
|
||||
{ainiPathes.map((name) =>
|
||||
<Route key={name} path={`/aini${name}`} render={() => <Pico lang={lang} dirname="aini" virtualpath={`/aini`} 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>
|
||||
</div>
|
||||
</div>
|
||||
<Footer lang={lang} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,103 @@
|
||||
import imageRssBmi from './assets/images/rss-bmi.png';
|
||||
import imageRssBsiNi from './assets/images/rss-bsi-ni.png';
|
||||
import imageRssCbsn from './assets/images/rss-cbsn.png';
|
||||
import imageRssCdtdb from './assets/images/rss-cdtdb.png';
|
||||
import imageRssCerebellum from './assets/images/rss-cerebellum.png';
|
||||
import imageRssDynamicbrain from './assets/images/rss-dynamicbrain.png';
|
||||
import imageRssIncf from './assets/images/rss-incf.png';
|
||||
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';
|
||||
|
||||
const RssFeedSites: { [key: string]: RssFeedSite } = {
|
||||
'incf': {
|
||||
id: 'incf',
|
||||
name: 'INCF Neuroinformatics Portal',
|
||||
logo: imageRssIncf,
|
||||
url: 'https://www.incf.org/',
|
||||
rss: 'https://www.incf.org/news/feed'
|
||||
},
|
||||
'visiome':
|
||||
{
|
||||
id: 'visiome',
|
||||
name: 'Visiome Platform',
|
||||
logo: imageRssVisiome,
|
||||
url: 'https://visiome.neuroinf.jp/',
|
||||
rss: 'https://visiome.neuroinf.jp/rss.xml'
|
||||
},
|
||||
'bmi':
|
||||
{
|
||||
id: 'bmi',
|
||||
name: 'Brain Machine Interface Platform',
|
||||
logo: imageRssBmi,
|
||||
url: 'https://bmi.neuroinf.jp/',
|
||||
rss: 'https://bmi.neuroinf.jp/researchers/rss.xml'
|
||||
},
|
||||
'invbrain':
|
||||
{
|
||||
id: 'invbrain',
|
||||
name: 'Invertebrate Brain Platform',
|
||||
logo: imageRssInvbrain,
|
||||
url: 'https://invbrain.neuroinf.jp/',
|
||||
rss: 'https://invbrain.neuroinf.jp/rss.xml'
|
||||
},
|
||||
'cbsn':
|
||||
{
|
||||
id: 'cbsn',
|
||||
name: 'Comprehensive Brain Science Network Platform',
|
||||
logo: imageRssCbsn,
|
||||
url: 'https://cbsn.neuroinf.jp/',
|
||||
rss: 'https://cbsn.neuroinf.jp/rss.xml'
|
||||
},
|
||||
'cdtdb':
|
||||
{
|
||||
id: 'cdtdb',
|
||||
name: 'Brain Transcriptome Database',
|
||||
logo: imageRssCdtdb,
|
||||
url: 'http://www.cdtdb.neuroinf.jp/',
|
||||
rss: 'http://www.cdtdb.neuroinf.jp/CDT/cdtdb.xml'
|
||||
},
|
||||
'cerebellum':
|
||||
{
|
||||
id: 'cerebellum',
|
||||
name: 'Cerebellar Platform',
|
||||
logo: imageRssCerebellum,
|
||||
url: 'https://cerebellum.neuroinf.jp/',
|
||||
rss: 'https://cerebellum.neuroinf.jp/rss.xml'
|
||||
},
|
||||
'nimg': {
|
||||
id: 'nimg',
|
||||
name: 'Neuro-Imaging Platform',
|
||||
logo: imageRssNimg,
|
||||
url: 'https://nimg.neuroinf.jp/',
|
||||
rss: 'https://nimg.neuroinf.jp/rss.xml'
|
||||
},
|
||||
'dynamicbrain':
|
||||
{
|
||||
id: 'dynamicbrain',
|
||||
name: 'Dynamic Brain Platform',
|
||||
logo: imageRssDynamicbrain,
|
||||
url: 'https://dynamicbrain.neuroinf.jp/',
|
||||
rss: 'https://dynamicbrain.neuroinf.jp/rss.xml'
|
||||
},
|
||||
'mpdb':
|
||||
{
|
||||
id: 'mpdb',
|
||||
name: 'Mouse Phenotype Database',
|
||||
logo: imageRssMpdb,
|
||||
url: 'http://www.mouse-phenotype.org/',
|
||||
rss: 'http://www.mouse-phenotype.org/index.xml'
|
||||
},
|
||||
'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'
|
||||
}
|
||||
};
|
||||
|
||||
export default RssFeedSites;
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import BulletinRecentNews from '../bulletin/blocks/BulletinRecentNews';
|
||||
import Config, { MultiLang } from '../config';
|
||||
import Functions from '../functions';
|
||||
import imageMainBanner from './assets/images/main-banner.png';
|
||||
import LinkBannersBlock from './blocks/LinkBannersBlock';
|
||||
import RssBlock from './blocks/RssBlock';
|
||||
import { 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 TopPage: React.FC<Props> = (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>
|
||||
</div>
|
||||
<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 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>
|
||||
<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>;
|
||||
}
|
||||
|
||||
export default TopPage;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import NoticeSiteHasBeenArchived from '../common/lib/NoticeSiteHasBeenArchived';
|
||||
import Config, { 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 research search service 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 { 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>;
|
||||
}
|
||||
|
||||
export default Xsearch;
|
||||
|
After Width: | Height: | Size: 475 B |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 439 B |
|
After Width: | Height: | Size: 419 B |
|
After Width: | Height: | Size: 149 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 973 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 150 B |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 955 B |