Update dependencies and add blog
This commit is contained in:
parent
477b6e4dc7
commit
7a8fb3449d
22 changed files with 948 additions and 332 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -5,4 +5,5 @@ public
|
||||||
*.jpg
|
*.jpg
|
||||||
*.svg
|
*.svg
|
||||||
*.log
|
*.log
|
||||||
*.lock
|
*.lock
|
||||||
|
*.zip
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
# Deep Learning Framework Benchmark
|
||||||
|
*Posted on October, 7 2021*
|
||||||
|
|
||||||
|
## Preambule
|
||||||
|
|
||||||
|
There are few frameworks to work on Deep Learning neural networks, I used to be very familiar with Tensorflow back in the days when the second version was not yet released. As someone with software engineering background the strictness and clarity of this first version of Tensorflow was a joy. Also the graph outputed by tensorboard were amazing to the point of getting the habits of debugging my networks from tensorboard most of the time.
|
||||||
|
|
||||||
|
<div class="image-row">
|
||||||
|
|
||||||
|
### Graph showed in tensorboard from Tensorflow version 1
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Those days are gone, now a new era of dynamic programming came to the Deep Learning field with PyTorch becoming increasingly popular (from what I have experienced), the second version of Tensorflow converging to the same API and Jax going a step further with near-python programming paradigm. The dynamic paradigm has some very nice points, especially if you do reinforcement learning it makes things way easier.
|
||||||
|
|
||||||
|
There are also other frameworks I haven't yet tested like [MXNet](https://mxnet.apache.org/versions/1.8.0/).
|
||||||
|
|
||||||
|
Now most of the frameworks I have experienced have nearly the same API and ONNX brings a very nice way to output the final result of trainings independently of the framework. Thus choosing which one to use is getting less clear than before.
|
||||||
|
|
||||||
|
Lately I have been trying out some RNN-like network with different modification to improve the infamous *long term memory* problem (Hopefully I will post something about that latter). Using PyTorch I feel very frustrated that the included [LSTM layer](https://pytorch.org/docs/stable/generated/torch.nn.LSTM.html) was running very well but **an equivalent code would run several times slower** (around 1/3), even following the [official documentation on GPU optimizations](https://pytorch.org/blog/optimizing-cuda-rnn-with-torchscript/) (which seems deprecated on few points), sometimes too the point of going at 10% of the initial speed. So if I want to do some research I might as well choose a framework that wouldn't work so slow I would need to wait for hours a training that could be performed in minutes. But would other frameworks really give me better performance?
|
||||||
|
|
||||||
|
I decided to see for myself how the different framework behave, starting from simple operations and hopefully testing up to whole network trainings.
|
||||||
|
|
||||||
|
I will use a naming convention for the frameworks (also called platform in my scripts) tested here:
|
||||||
|
|
||||||
|
* TF1 : first version of Tensorflow (verion 1.x), as of this writting the latest version is 1.15
|
||||||
|
* TF2 : second version of Tensorflow (verion 2.x), as of this writting the latest version is 2.6
|
||||||
|
* TF2_V1 : second version of Tensorflow but using the compatibility API to write as the first version, also disabling the dynamic behaviour (I suspected different performance)
|
||||||
|
* Torch : PyTorch
|
||||||
|
* Jax
|
||||||
|
|
||||||
|
|
||||||
|
## Benchmarking implementation
|
||||||
|
|
||||||
|
### No Gradient
|
||||||
|
|
||||||
|
This is obvious but PyTorch is very nice for the majority of the time were you need to compute gradients but not here as I started with the most simple operations first. The `requires_grad=False` argument on all tensors does the trick on PyTorch while Tensorflow and Jax don't need any additional care as far as I know.
|
||||||
|
|
||||||
|
### Warmup
|
||||||
|
|
||||||
|
I have experienced many time on all framework so far that the first run is always several time slower, this is obvious for dynamically allocated tensors of the modern frameworks but I strongly remember this happened too when I was using TF1. To avoid the first run to skew the benchmark each experiment has a small warmup loop:
|
||||||
|
|
||||||
|
```
|
||||||
|
# warmup
|
||||||
|
for _ in range(20):
|
||||||
|
self.experiment()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optimizations
|
||||||
|
|
||||||
|
I had to test with **random tensor at start and before each operation** to be sure that frameworks do not optimize out some already made operations (could be cache), especially since I disabled gradient computation. All my tests showed no difference so I stuck with tensors initially filled with ones.
|
||||||
|
|
||||||
|
### Benchmark time
|
||||||
|
|
||||||
|
Each operation is benchmarked during an "experiment", to get consistent benchmarking time a first loop is done to estimate the number of operation per second then the loop being benchmarked is run with a fixed number of step from the estimation. This allows to set in a configuration file the time per experiment for statistical stability and avoid unnecessary call to the system clock (CPython not being know for its speed I'd rather have a simple integer increment per loop as overhead).
|
||||||
|
|
||||||
|
Latter this could also make a progression bar with ETA possible as the benchmarks can be quiet exhaustive.
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
**The code is publicly available [here](https://gitlab.com/corentin-pro/dl_bench). It will output raw data as csv files and their plots. All the data and plot from my machine (NVIDIA GeForce RTX 2060 SUPER) can be downloaded [here](/blog/2021-10-07_Deep%20Learning%20Framework%20Benchmarks/gpu_NVIDIA%20GeForce%20RTX%202060%20SUPER.zip).**
|
||||||
|
|
||||||
|
<div class="image-row">
|
||||||
|
|
||||||
|
### Experiment benchmark samples
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
As expected the bigger the operations (experiments) the better [GFLOPS](https://en.wikipedia.org/wiki/FLOPS) (Giga floating point operations per second) the GPU can output. So far nothing unexpected.
|
||||||
|
|
||||||
|
### Comparisons
|
||||||
|
|
||||||
|
Comparison plots are also generated from the experiment data, for now the only comparison are done between 'platforms' (aka framework) but data type comparisons could be interesting in the future. Categories were made to plot subsets of comparisons in order to keep the scale of the y axis linear, the script will automatically switch to logarithmic scale if needed in the general case. The categories are ranges of Mop (Milions of operations) per experiment like `MEDIUM = [20, 1000]` (there is SMALL, MEDIUM, LARGE and VERY_LARGE) and can be changed in the configuration files.
|
||||||
|
|
||||||
|
<div class="image-row">
|
||||||
|
|
||||||
|
### Comparison samples
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
**NOTE** : all operations with the `nn` prefix means that it is run inside a 'module' (or equivalent), in Jax for instace I used `stax` and `jit` as intended by the library. JIT is not needed as far as I tested for Torch.
|
||||||
|
|
||||||
|
Torch seems the best for simple and small operation while tensorflow in general seems to have big overheads. Jax does very well once we use the JIT. All frameworks tends to converge more with bigger layers/operations, the XLA based Tensorflow and Jax seems to have slightly better performance there. But for small operations Torch can be orders of magnitude faster!
|
||||||
|
|
||||||
|
The result between float32 and float16 are very similar but float64 is different:
|
||||||
|
|
||||||
|
* For some reasons TF2 didn't accept matmul on float64 inside a module, I should fix that latter
|
||||||
|
* TF2 get better results relatively to other platforms
|
||||||
|
* Except for element-wise operations, Torch doesn't have its lead on small operations
|
||||||
|
* There is a weird behaviour for the matmul of 800x800 tensors both in Torch and TF2. After additional testing I couldn't figure out why the first runs (even after warmup) were way to fast.
|
||||||
|
|
||||||
|
The specific behaviour of the 800x800 matmul in the data (see `run times (s)`) looks like :
|
||||||
|
|
||||||
|
```
|
||||||
|
experiment run times (s) count ms/matmul Mop/matmul GFLOPS
|
||||||
|
300 800x800 @ 800x800 0.03258013725280762 60 0.5430022875467936 1022.72 1883.4543121733468
|
||||||
|
[...]
|
||||||
|
308 800x800 @ 800x800 0.032579898834228516 60 0.5429983139038086 1022.72 1883.4680952272229
|
||||||
|
309 800x800 @ 800x800 0.03258252143859863 60 0.5430420239766439 1022.72 1883.316492728723
|
||||||
|
310 800x800 @ 800x800 0.1323096752166748 60 2.2051612536112466 1022.72 463.7846771183555
|
||||||
|
311 800x800 @ 800x800 0.2970736026763916 60 4.951226711273193 1022.72 206.55891148579838
|
||||||
|
312 800x800 @ 800x800 0.29687929153442383 60 4.947988192240397 1022.72 206.6941068298959
|
||||||
|
[...]
|
||||||
|
329 800x800 @ 800x800 0.2968714237213135 60 4.947857062021892 1022.72 206.69958472528631
|
||||||
|
```
|
||||||
|
|
||||||
|
It is the only instance of such a behavior across all operations and even within the matmul benchamrk. Because of this the result plot doesn't look great :
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The results so far confort me into using Torch overall as I usually design small networks but Jax seems to be a very interesting contender. I am surprise the difference on small/medium operations could be that significant between Torch and TF2, I sometimes use my DL framework for GPU accelerated math in other context so it is interesting.
|
||||||
|
|
||||||
|
The code is not yet complete and in the future I would like to test for more:
|
||||||
|
|
||||||
|
* Convolutions : 1d, 2d, transpose
|
||||||
|
* Gradient
|
||||||
|
* Optimizer
|
||||||
|
* RNN : which was the trigger that started all of this
|
||||||
|
* Data transfert? (CPU->GPU and GPU->CPU)
|
||||||
|
|
||||||
|
If you have questions or remarks you can contact me or reply the [reddit post]() (TO BE INSERTED).
|
||||||
13
assets/theme/dark.css
Normal file
13
assets/theme/dark.css
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
:root
|
||||||
|
{
|
||||||
|
--main-bg-color: #21262b;
|
||||||
|
--main-fg-color: #b0b0b0;
|
||||||
|
--lighter-bg-color: #31363b;
|
||||||
|
--lighter-fg-color: #d0d0d0;
|
||||||
|
--highlight-bg-color: #41464b;
|
||||||
|
--highlight-fg-color: #e0e0e0;
|
||||||
|
--dim-bg-color: rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
--main-primary-color: hsl(213, 35%, 65%);
|
||||||
|
--light-primary-color: hsl(213, 35%, 45%);
|
||||||
|
}
|
||||||
13
assets/theme/light.css
Normal file
13
assets/theme/light.css
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
:root
|
||||||
|
{
|
||||||
|
--main-bg-color: #e0e0e0;
|
||||||
|
--main-fg-color: #41464b;
|
||||||
|
--lighter-bg-color: #c8c8c8;
|
||||||
|
--lighter-fg-color: #31363b;
|
||||||
|
--highlight-bg-color: #b0b0b0;
|
||||||
|
--highlight-fg-color: #21262b;
|
||||||
|
--dim-bg-color: rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
--main-primary-color: hsl(213, 35%, 45%);
|
||||||
|
--light-primary-color: hsl(213, 35%, 65%);
|
||||||
|
}
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
<title>AYO Inc. - AI solution for business</title>
|
<title>AYO Inc. - AI solution for business</title>
|
||||||
<meta name="description" content="AYO provides customized AI solution for comapnies to improve their process. We take inspiration in the latest research in machine learning : deep learning, probabilistic models, neuroscience and physics."/>
|
<meta name="description" content="AYO provides customized AI solution for comapnies to improve their process. We take inspiration in the latest research in machine learning : deep learning, probabilistic models, neuroscience and physics."/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="shortcut icon" href="/assets/favicon.svg" type="image/svg+xml">
|
<link rel="shortcut icon" href="/assets/icons/favicon.svg" type="image/svg+xml">
|
||||||
<link rel="shortcut icon" href="/assets/favicon.png">
|
<link rel="shortcut icon" href="/assets/icons/favicon.png">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
19
package.json
19
package.json
|
|
@ -10,19 +10,22 @@
|
||||||
"@babel/preset-env": "^7.7.1",
|
"@babel/preset-env": "^7.7.1",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-inferno": "^6.1.0",
|
"babel-plugin-inferno": "^6.1.0",
|
||||||
"compression-webpack-plugin": "^3.0.0",
|
"compression-webpack-plugin": "^9.0.0",
|
||||||
"inferno": "^7.3.2",
|
"inferno": "^7.3.2",
|
||||||
"inferno-redux": "^7.3.3",
|
"inferno-redux": "^7.3.3",
|
||||||
|
"inferno-router": "^7.4.10",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"webpack": "^4.41.2",
|
"webpack": "5.56.1",
|
||||||
"webpack-cli": "^3.3.10"
|
"webpack-cli": "^4.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^9.0.1",
|
||||||
"css-loader": "^3.4.0",
|
"css-loader": "^6.3.0",
|
||||||
"file-loader": "^5.0.2",
|
"marked": "^3.0.4",
|
||||||
"style-loader": "^1.1.2",
|
"sass": "^1.42.1",
|
||||||
"webpack-dev-server": "^3.9.0"
|
"sass-loader": "^12.1.0",
|
||||||
|
"style-loader": "^3.3.0",
|
||||||
|
"webpack-dev-server": "^4.3.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.prod.js",
|
"build": "webpack --config webpack.prod.js",
|
||||||
|
|
|
||||||
93
server.js
93
server.js
|
|
@ -1,9 +1,8 @@
|
||||||
const fs = require('fs');
|
const fs = require('fs')
|
||||||
const http = require('http');
|
const http = require('http')
|
||||||
const path = require('path');
|
const path = require('path')
|
||||||
const url = require('url');
|
|
||||||
|
|
||||||
const port = 8080; // Prefer port > 1024 to avoid super-user permission
|
const port = 8080 // Prefer port > 1024 to avoid super-user permission
|
||||||
|
|
||||||
const server = http.createServer((request, response) => {
|
const server = http.createServer((request, response) => {
|
||||||
const mimeType = {
|
const mimeType = {
|
||||||
|
|
@ -13,50 +12,58 @@ const server = http.createServer((request, response) => {
|
||||||
'.svg': 'image/svg+xml',
|
'.svg': 'image/svg+xml',
|
||||||
'.css': 'text/css',
|
'.css': 'text/css',
|
||||||
'.json': 'application/json'
|
'.json': 'application/json'
|
||||||
};
|
}
|
||||||
const mimeEncoding = {
|
const mimeEncoding = {
|
||||||
'.gz': 'gzip',
|
'.gz': 'gzip',
|
||||||
};
|
}
|
||||||
let pathname = 'public' + url.parse(request.url).pathname;
|
let pathname = 'public' + decodeURI(request.url)
|
||||||
fs.exists(pathname, function (exist) {
|
if(!fs.existsSync(pathname) || fs.statSync(pathname).isDirectory())
|
||||||
if(!exist)
|
{
|
||||||
|
// If the file is not found, return 404
|
||||||
|
// response.statusCode = 404
|
||||||
|
// response.end(`File ${pathname} not found!`)
|
||||||
|
// return
|
||||||
|
pathname = 'public/index.html'
|
||||||
|
}
|
||||||
|
// If is a directory, then look for index.html
|
||||||
|
// else if (fs.statSync(pathname).isDirectory()) {
|
||||||
|
// pathname += 'index.html'
|
||||||
|
// }
|
||||||
|
|
||||||
|
if(!fs.existsSync(pathname))
|
||||||
|
{
|
||||||
|
// If the file is not found, return 404
|
||||||
|
response.statusCode = 404
|
||||||
|
response.end(`File ${pathname} not found!`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file from file system
|
||||||
|
fs.readFile(pathname, function(error, data){
|
||||||
|
if(error)
|
||||||
{
|
{
|
||||||
// If the file is not found, return 404
|
response.statusCode = 500
|
||||||
response.statusCode = 404;
|
response.end(`Error getting the file: ${error}.`)
|
||||||
response.end(`File ${pathname} not found!`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// If is a directory, then look for index.html
|
else
|
||||||
if (fs.statSync(pathname).isDirectory()) {
|
{
|
||||||
pathname += '/index.html';
|
// Based on the URL path, extract the file extention. e.g. .js, .doc, ...
|
||||||
|
const extension = path.parse(pathname).ext
|
||||||
|
// Set Content-Type
|
||||||
|
response.setHeader('Content-Type', mimeType[extension] || 'text/plain' )
|
||||||
|
// If the file is found, set Content-Encoding
|
||||||
|
if(mimeEncoding[extension])
|
||||||
|
{
|
||||||
|
response.setHeader('Content-Encoding', mimeEncoding[extension])
|
||||||
|
}
|
||||||
|
response.end(data)
|
||||||
}
|
}
|
||||||
// Read file from file system
|
})
|
||||||
fs.readFile(pathname, function(error, data){
|
})
|
||||||
if(error)
|
|
||||||
{
|
|
||||||
response.statusCode = 500;
|
|
||||||
response.end(`Error getting the file: ${error}.`);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Based on the URL path, extract the file extention. e.g. .js, .doc, ...
|
|
||||||
const extension = path.parse(pathname).ext;
|
|
||||||
// Set Content-Type
|
|
||||||
response.setHeader('Content-Type', mimeType[extension] || 'text/plain' );
|
|
||||||
// If the file is found, set Content-Encoding
|
|
||||||
if(mimeEncoding[extension])
|
|
||||||
{
|
|
||||||
response.setHeader('Content-Encoding', mimeEncoding[extension]);
|
|
||||||
}
|
|
||||||
response.end(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(port, (error) => {
|
server.listen(port, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return console.log(`Server cannot listen port ${port} :`, error);
|
return console.log(`Server cannot listen port ${port} :`, error)
|
||||||
}
|
}
|
||||||
console.log(`Server is listening on port ${port}`);
|
console.log(`Server is listening on port ${port}`)
|
||||||
});
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
export const themeActions = {
|
export const themeActions = {
|
||||||
THEME_CHANGE: 'THEME_CHANGE'
|
CHANGE: 'THEME_CHANGE'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const changeTheme = (new_theme) => ({
|
export const changeTheme = (new_theme) => ({
|
||||||
type: themeActions.THEME_CHANGE,
|
type: themeActions.CHANGE,
|
||||||
theme: new_theme
|
theme: new_theme
|
||||||
})
|
})
|
||||||
|
|
||||||
export const langActions = {
|
export const langActions = {
|
||||||
LANG_CHANGE_REQUEST: 'LANG_CHANGE_REQUEST',
|
CHANGE_REQUEST: 'LANG_CHANGE_REQUEST',
|
||||||
LANG_CHANGE_RESPONSE: 'LANG_CHANGE_RESPONSE'
|
CHANGE_RESPONSE: 'LANG_CHANGE_RESPONSE'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const changeLang = (new_lang) => ({
|
export const changeLang = (new_lang) => ({
|
||||||
type: langActions.LANG_CHANGE_REQUEST,
|
type: langActions.CHANGE_REQUEST,
|
||||||
lang: new_lang
|
lang: new_lang
|
||||||
})
|
})
|
||||||
|
|
||||||
export const receiveLang = (new_lang, strings) => ({
|
export const receiveLang = (new_lang, strings) => ({
|
||||||
type: langActions.LANG_CHANGE_RESPONSE,
|
type: langActions.CHANGE_RESPONSE,
|
||||||
lang: new_lang,
|
lang: new_lang,
|
||||||
strings: strings
|
strings: strings
|
||||||
})
|
})
|
||||||
|
|
@ -47,4 +47,82 @@ export const fetchLang = (new_lang) => {
|
||||||
dispatch(changeLang(null))
|
dispatch(changeLang(null))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const blogActions = {
|
||||||
|
LIST_REQUEST: 'BLOG_LIST_REQUEST',
|
||||||
|
LIST_RESPONSE: 'BLOG_LIST_RESPONSE',
|
||||||
|
ENTRY_REQUEST: 'BLOG_ENTRY_REQUEST',
|
||||||
|
ENTRY_RESPONSE: 'BLOG_ENTRY_RESPONSE'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const blogListRequest = () => ({
|
||||||
|
type: blogActions.LIST_REQUEST,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const blogListResponse = (list = []) => ({
|
||||||
|
type: blogActions.LIST_RESPONSE,
|
||||||
|
list: list
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fetchBlogList = () => {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState()
|
||||||
|
if(state.blog.list_requesting)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dispatch(blogListRequest())
|
||||||
|
return fetch('/blog_list.json')
|
||||||
|
.then((response) => {
|
||||||
|
if(!response.ok)
|
||||||
|
{
|
||||||
|
console.log('Couldn\'t fetch blog_list.json')
|
||||||
|
throw Error(response.statusText)
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
.then(json => {
|
||||||
|
dispatch(blogListResponse(json));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(blogListResponse())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const blogEntryRequest = () => ({
|
||||||
|
type: blogActions.ENTRY_REQUEST,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const blogEntryResponse = (name= null, entry = null) => ({
|
||||||
|
type: blogActions.ENTRY_RESPONSE,
|
||||||
|
entry: entry,
|
||||||
|
name: name
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fetchBlogEntry = (entry_date, entry_name) => {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState()
|
||||||
|
if(state.blog.entry_requesting)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dispatch(blogEntryRequest())
|
||||||
|
return fetch('/blog/' + entry_date + '_' + entry_name + '/' + entry_name + '.html')
|
||||||
|
.then((response) => {
|
||||||
|
if(!response.ok)
|
||||||
|
{
|
||||||
|
console.log('Couldn\'t fetch blog entry : ' + entry_name)
|
||||||
|
throw Error(response.statusText)
|
||||||
|
}
|
||||||
|
return response.text()
|
||||||
|
})
|
||||||
|
.then(text => {
|
||||||
|
dispatch(blogEntryResponse(entry_name, text))
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(blogEntryResponse())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
28
src/blog.css
Normal file
28
src/blog.css
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
.blog
|
||||||
|
{
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog > a
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
text-decoration: none;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: var(--dim-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog a h2
|
||||||
|
{
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 20px 60px 20px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog a .date
|
||||||
|
{
|
||||||
|
font-size: 0.7em;
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
56
src/blog.jsx
Normal file
56
src/blog.jsx
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
`use strict`
|
||||||
|
import { Component } from 'inferno'
|
||||||
|
import { Link } from 'inferno-router'
|
||||||
|
import { connect } from 'inferno-redux'
|
||||||
|
|
||||||
|
import { fetchBlogList } from './actions'
|
||||||
|
|
||||||
|
import './blog.css'
|
||||||
|
|
||||||
|
|
||||||
|
class BlogComponent extends Component
|
||||||
|
{
|
||||||
|
componentDidMount()
|
||||||
|
{
|
||||||
|
if(this.props.blog_list.length === 0)
|
||||||
|
{
|
||||||
|
this.props.fetchBlogList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<div className="blog">
|
||||||
|
{this.props.blog_list.map((blog_info, index) => {
|
||||||
|
return (
|
||||||
|
<Link to={'/blog/' + blog_info.date + '/' + encodeURIComponent(blog_info.name)} key={index}>
|
||||||
|
<h2>
|
||||||
|
{blog_info.name}
|
||||||
|
</h2>
|
||||||
|
<span className="date">
|
||||||
|
{this.props.strings.publish_date
|
||||||
|
+ new Date(blog_info.date).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
strings: state.lang.strings,
|
||||||
|
blog_list: state.blog.list
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
fetchBlogList: () => dispatch(fetchBlogList())
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(BlogComponent)
|
||||||
57
src/blog_entry.jsx
Normal file
57
src/blog_entry.jsx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
`use strict`
|
||||||
|
import { Component } from 'inferno'
|
||||||
|
import { connect } from 'inferno-redux'
|
||||||
|
|
||||||
|
import { fetchBlogList, fetchBlogEntry } from './actions'
|
||||||
|
|
||||||
|
import './blog_entry.scss'
|
||||||
|
|
||||||
|
|
||||||
|
class BlogEntryComponent extends Component
|
||||||
|
{
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.entry_date = this.props.match.params.entry_date
|
||||||
|
this.entry_name = this.props.match.params.entry_name
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount()
|
||||||
|
{
|
||||||
|
if(this.props.blog_list.length === 0)
|
||||||
|
{
|
||||||
|
this.props.fetchBlogList()
|
||||||
|
}
|
||||||
|
if(this.props.blog_entry_name !== this.entry_name)
|
||||||
|
{
|
||||||
|
this.props.fetchBlogEntry(this.entry_date, this.entry_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<div className="blog-entry"
|
||||||
|
dangerouslySetInnerHTML={this.props.blog_entry_name === this.entry_name ?
|
||||||
|
{__html: this.props.blog_entry} : null}>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
strings: state.lang.strings,
|
||||||
|
blog_list: state.blog.list,
|
||||||
|
blog_entry: state.blog.entry,
|
||||||
|
blog_entry_name: state.blog.entry_name
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
fetchBlogList: () => dispatch(fetchBlogList()),
|
||||||
|
fetchBlogEntry: (entry_date, entry_name) => dispatch(fetchBlogEntry(entry_date, entry_name))
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(BlogEntryComponent)
|
||||||
81
src/blog_entry.scss
Normal file
81
src/blog_entry.scss
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
.blog-entry
|
||||||
|
{
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
line-height: 1.8rem;
|
||||||
|
font-family: "open sans", sans;
|
||||||
|
|
||||||
|
h1, h2, h3
|
||||||
|
{
|
||||||
|
font-weight: lighter;
|
||||||
|
margin: 8px 0;
|
||||||
|
color: var(--highlight-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1
|
||||||
|
{
|
||||||
|
padding: 1rem 0 1.2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2
|
||||||
|
{
|
||||||
|
padding: 0.5rem 0 1rem 0;
|
||||||
|
border-bottom: 1px solid var(--dim-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
code, pre
|
||||||
|
{
|
||||||
|
background-color: var(--lighter-bg-color);
|
||||||
|
color: var(--lighter-fg-color);
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre
|
||||||
|
{
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
em
|
||||||
|
{
|
||||||
|
color: var(--lighter-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
img
|
||||||
|
{
|
||||||
|
min-height: 160px;
|
||||||
|
max-height: 50vh;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong
|
||||||
|
{
|
||||||
|
color: var(--lighter-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-row
|
||||||
|
{
|
||||||
|
h3
|
||||||
|
{
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
p
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img
|
||||||
|
{
|
||||||
|
margin: 0 10px;
|
||||||
|
object-fit: contain;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/home.jsx
Normal file
50
src/home.jsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
`use strict`
|
||||||
|
import { Component } from 'inferno'
|
||||||
|
import { connect } from 'inferno-redux'
|
||||||
|
|
||||||
|
import { Svg } from './svg.jsx'
|
||||||
|
|
||||||
|
import ArrowSvg from '../assets/icons/arrow_forward.svg'
|
||||||
|
import Flow1Svg from '../assets/images/undraw_maintenance_cn7j.svg'
|
||||||
|
import Flow2Svg from '../assets/images/undraw_design_data_khdb.svg'
|
||||||
|
import Flow3Svg from '../assets/images/undraw_Artificial_intelligence_oyxx.svg'
|
||||||
|
|
||||||
|
class HomeComponent extends Component
|
||||||
|
{
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<div className="content">
|
||||||
|
<div className="process-flow">
|
||||||
|
<div>
|
||||||
|
<Svg src={Flow1Svg}/>
|
||||||
|
<h3>{this.props.strings.flow1}</h3>
|
||||||
|
</div>
|
||||||
|
<Svg src={ArrowSvg}/>
|
||||||
|
<div>
|
||||||
|
<Svg src={Flow2Svg}/>
|
||||||
|
<h3>{this.props.strings.flow2}</h3>
|
||||||
|
</div>
|
||||||
|
<Svg src={ArrowSvg}/>
|
||||||
|
<div>
|
||||||
|
<Svg src={Flow3Svg}/>
|
||||||
|
<h3>{this.props.strings.flow3}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
strings: state.lang.strings
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(HomeComponent)
|
||||||
|
|
@ -3,7 +3,7 @@ import { render } from 'inferno'
|
||||||
import { Provider } from 'inferno-redux';
|
import { Provider } from 'inferno-redux';
|
||||||
import { applyMiddleware, createStore, combineReducers } from 'redux';
|
import { applyMiddleware, createStore, combineReducers } from 'redux';
|
||||||
|
|
||||||
import { langReducer, themeReducer } from './reducers';
|
import { blogReducer, langReducer, themeReducer } from './reducers';
|
||||||
import { fetchLang } from './actions'
|
import { fetchLang } from './actions'
|
||||||
|
|
||||||
import Main from './main.jsx'
|
import Main from './main.jsx'
|
||||||
|
|
@ -33,7 +33,8 @@ export const init_app = () => {
|
||||||
|
|
||||||
const reducers = combineReducers({
|
const reducers = combineReducers({
|
||||||
theme: themeReducer,
|
theme: themeReducer,
|
||||||
lang: langReducer
|
lang: langReducer,
|
||||||
|
blog: blogReducer
|
||||||
})
|
})
|
||||||
|
|
||||||
const store = createStore(
|
const store = createStore(
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,10 @@
|
||||||
"inferno_credit": "Website built with ",
|
"inferno_credit": "Website built with ",
|
||||||
"flow1": "From repetitive tasks done by human",
|
"flow1": "From repetitive tasks done by human",
|
||||||
"flow2": "With the help of data gathered",
|
"flow2": "With the help of data gathered",
|
||||||
"flow3": "To automated systems"
|
"flow3": "To automated systems",
|
||||||
|
"section": {
|
||||||
|
"home": "Home",
|
||||||
|
"blog": "Blog"
|
||||||
|
},
|
||||||
|
"publish_date": "Publish date : "
|
||||||
}
|
}
|
||||||
|
|
@ -6,5 +6,10 @@
|
||||||
"inferno_credit": "Website built with ",
|
"inferno_credit": "Website built with ",
|
||||||
"flow1": "人によって\u200b繰り返される\u200b作業",
|
"flow1": "人によって\u200b繰り返される\u200b作業",
|
||||||
"flow2": "収集された\u200bデータ\u200b(の助け)\u200bによって",
|
"flow2": "収集された\u200bデータ\u200b(の助け)\u200bによって",
|
||||||
"flow3": "システム\u200bを自動化するため"
|
"flow3": "システム\u200bを自動化するため",
|
||||||
|
"section": {
|
||||||
|
"home": "ホーム",
|
||||||
|
"blog": "ブログ"
|
||||||
|
},
|
||||||
|
"publish_date": "Publish date : "
|
||||||
}
|
}
|
||||||
251
src/main.css
251
src/main.css
|
|
@ -1,216 +1,217 @@
|
||||||
*, ::after, ::before
|
*, ::after, ::before
|
||||||
{
|
{
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
a, a:visited
|
a, a:visited
|
||||||
{
|
{
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
body
|
body
|
||||||
{
|
{
|
||||||
background-color: var(--main-bg-color);
|
background-color: var(--main-bg-color);
|
||||||
color: var(--main-fg-color);
|
color: var(--main-fg-color);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: sans;
|
font-family: sans;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background-image: url(/assets/back.webp);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0.15;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 420px)
|
@media (max-width: 420px)
|
||||||
{
|
{
|
||||||
body
|
body
|
||||||
{
|
{
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
.process-flow
|
.process-flow
|
||||||
{
|
{
|
||||||
padding: 3px 3vw;
|
padding: 3px 3vw;
|
||||||
}
|
}
|
||||||
.process-flow .svg:nth-child(even) > svg
|
.process-flow .svg:nth-child(even) > svg
|
||||||
{
|
{
|
||||||
width: 18px;
|
width: 18px;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 420px) and (max-width: 600px)
|
@media (min-width: 420px) and (max-width: 600px)
|
||||||
{
|
{
|
||||||
body
|
body
|
||||||
{
|
{
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.process-flow
|
.process-flow
|
||||||
{
|
{
|
||||||
padding: 5px 5vw;
|
padding: 5px 5vw;
|
||||||
}
|
}
|
||||||
.process-flow .svg:nth-child(even) > svg
|
.process-flow .svg:nth-child(even) > svg
|
||||||
{
|
{
|
||||||
width: 5vw;
|
width: 5vw;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 600px)
|
@media (min-width: 600px)
|
||||||
{
|
{
|
||||||
body
|
body
|
||||||
{
|
{
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.process-flow
|
.process-flow
|
||||||
{
|
{
|
||||||
padding: 10px 10vw;
|
padding: 10px 10vw;
|
||||||
}
|
}
|
||||||
.process-flow .svg:nth-child(even) > svg
|
.process-flow .svg:nth-child(even) > svg
|
||||||
{
|
{
|
||||||
width: 5vw;
|
width: 5vw;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (orientation: portrait)
|
@media (orientation: portrait)
|
||||||
{
|
{
|
||||||
.process-flow
|
.process-flow
|
||||||
{
|
{
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.process-flow .svg
|
.process-flow .svg
|
||||||
{
|
{
|
||||||
height: 15vh;
|
height: 15vh;
|
||||||
}
|
}
|
||||||
.process-flow > .svg
|
.process-flow > .svg
|
||||||
{
|
{
|
||||||
transform: rotate(90deg)
|
transform: rotate(90deg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer > section
|
footer > section
|
||||||
{
|
{
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .legal div
|
footer .legal div
|
||||||
{
|
{
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
|
||||||
|
|
||||||
footer .credit
|
|
||||||
{
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer > section > div
|
footer > section > div
|
||||||
{
|
{
|
||||||
word-break: keep-all;
|
word-break: keep-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h5
|
h1, h2, h3, h4, h5, h5
|
||||||
{
|
{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
color: var(--lighter-fg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
main
|
main
|
||||||
{
|
{
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 375px;
|
align-items: center;
|
||||||
|
min-width: 375px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav
|
nav
|
||||||
{
|
{
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
width: 100%;
|
||||||
align-items: center;
|
justify-content: space-between;
|
||||||
padding: 10px;
|
align-items: center;
|
||||||
background-color: var(--lighterbg-color);
|
padding: 10px;
|
||||||
border-bottom: 1px solid var(--highlight-bg-color);
|
background-color: var(--lighterbg-color);
|
||||||
|
border-bottom: 1px solid var(--highlight-bg-color);
|
||||||
|
word-break: keep-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a
|
||||||
|
{
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav .actions
|
nav .actions
|
||||||
{
|
{
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1 1 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nav .main-logo
|
nav .main-logo
|
||||||
{
|
{
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .sections
|
||||||
|
{
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .sections a
|
||||||
|
{
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg
|
svg
|
||||||
{
|
{
|
||||||
fill: var(--main-fg-color);
|
fill: var(--main-fg-color);
|
||||||
width: auto;
|
height: auto;
|
||||||
height: auto;
|
max-width: 100%;
|
||||||
max-width: 100%;
|
max-height: 100%;
|
||||||
max-height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content
|
.content
|
||||||
{
|
{
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.process-flow
|
.process-flow
|
||||||
{
|
{
|
||||||
background-color: var(--lighter-bg-color);
|
background-color: var(--lighter-bg-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.process-flow h3
|
.process-flow h3
|
||||||
{
|
{
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.process-flow .svg
|
.process-flow .svg
|
||||||
{
|
{
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle
|
.toggle
|
||||||
{
|
{
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle:hover
|
.toggle:hover
|
||||||
{
|
{
|
||||||
background-color: var(--highlight-bg-color);
|
background-color: var(--highlight-bg-color);
|
||||||
color: var(--highlight-fg-color);
|
color: var(--highlight-fg-color);
|
||||||
fill: var(--highlight-fg-color);
|
fill: var(--highlight-fg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ayo_logo
|
#ayo_logo
|
||||||
{
|
{
|
||||||
height: 42px;
|
height: 42px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
103
src/main.jsx
103
src/main.jsx
|
|
@ -1,37 +1,26 @@
|
||||||
`use strict`
|
`use strict`
|
||||||
import { Component, version } from 'inferno'
|
import { Component } from 'inferno'
|
||||||
|
import { BrowserRouter, Link, Route, Switch } from 'inferno-router'
|
||||||
import { connect } from 'inferno-redux'
|
import { connect } from 'inferno-redux'
|
||||||
|
|
||||||
import { changeTheme, fetchLang } from './actions'
|
import { changeTheme, fetchLang } from './actions'
|
||||||
import { Svg } from './svg.jsx'
|
import { Svg } from './svg.jsx'
|
||||||
import { SvgToggle } from './svg_toggle.jsx'
|
import { SvgToggle } from './svg_toggle.jsx'
|
||||||
|
|
||||||
import './main.css'
|
import Blog from './blog.jsx'
|
||||||
import LogoSvg from '../assets/logo.svg'
|
import BlogEntry from './blog_entry.jsx'
|
||||||
import LangSvg from '../assets/translate.svg'
|
import Home from './home.jsx'
|
||||||
import LightSvg from '../assets/brightness_high.svg'
|
|
||||||
import DarkSvg from '../assets/brightness_medium.svg'
|
import './main.css'
|
||||||
|
|
||||||
|
import LogoSvg from '../assets/icons/logo.svg'
|
||||||
|
import LangSvg from '../assets/icons/translate.svg'
|
||||||
|
import LightSvg from '../assets/icons/brightness_high.svg'
|
||||||
|
import DarkSvg from '../assets/icons/brightness_medium.svg'
|
||||||
|
|
||||||
import ArrowSvg from '../assets/arrow_forward.svg'
|
|
||||||
import Flow1Svg from '../assets/undraw_maintenance_cn7j.svg'
|
|
||||||
import Flow2Svg from '../assets/undraw_design_data_khdb.svg'
|
|
||||||
import Flow3Svg from '../assets/undraw_Artificial_intelligence_oyxx.svg'
|
|
||||||
|
|
||||||
class MainComponent extends Component
|
class MainComponent extends Component
|
||||||
{
|
{
|
||||||
componentDidMount()
|
|
||||||
{
|
|
||||||
// Smooth color transitions (after rendering to avoid flash on reload)
|
|
||||||
// setTimeout(() => {
|
|
||||||
// const styleEl = document.head.getElementsByTagName('style')[0]
|
|
||||||
// document.head.appendChild(styleEl)
|
|
||||||
// const styleSheet = styleEl.sheet
|
|
||||||
// styleSheet.insertRule(
|
|
||||||
// '*, ::after, ::before { transition-property: background-color, color, fill; transition-duration: 1s;}')
|
|
||||||
// },
|
|
||||||
// 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTheme(checked)
|
toggleTheme(checked)
|
||||||
{
|
{
|
||||||
this.props.changeTheme(checked ? 'dark' : 'light')
|
this.props.changeTheme(checked ? 'dark' : 'light')
|
||||||
|
|
@ -47,51 +36,35 @@ class MainComponent extends Component
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<link rel="stylesheet" type="text/css" href={'/assets/theme/' + this.props.theme + '.css'} />
|
<link rel="stylesheet" type="text/css" href={'/assets/theme/' + this.props.theme + '.css'} />
|
||||||
<nav>
|
<BrowserRouter>
|
||||||
<div className="actions"> </div>
|
<nav>
|
||||||
<div className="main-logo">
|
<div className="main-logo">
|
||||||
<Svg src={LogoSvg}/>
|
<Svg src={LogoSvg}/>
|
||||||
</div>
|
|
||||||
<div className="actions">
|
|
||||||
<SvgToggle default={LangSvg} text={this.props.strings.lang}
|
|
||||||
toggle={(checked) => this.toggleLang(checked)} value={this.props.lang === 'en'}/>
|
|
||||||
<SvgToggle default={LightSvg} checked={DarkSvg}
|
|
||||||
toggle={(checked) => this.toggleTheme(checked)} value={this.props.theme === 'dark'}/>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<div className="content">
|
|
||||||
<div className="process-flow">
|
|
||||||
<div>
|
|
||||||
<Svg src={Flow1Svg}/>
|
|
||||||
<h3>{this.props.strings.flow1}</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<Svg src={ArrowSvg}/>
|
<div className="sections">
|
||||||
<div>
|
<Link to="/">{this.props.strings.section.home}</Link>
|
||||||
<Svg src={Flow2Svg}/>
|
<Link to="/blog">{this.props.strings.section.blog}</Link>
|
||||||
<h3>{this.props.strings.flow2}</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<Svg src={ArrowSvg}/>
|
<div className="actions">
|
||||||
<div>
|
<SvgToggle default={LangSvg} text={this.props.strings.lang}
|
||||||
<Svg src={Flow3Svg}/>
|
toggle={(checked) => this.toggleLang(checked)} value={this.props.lang === 'en'}/>
|
||||||
<h3>{this.props.strings.flow3}</h3>
|
<SvgToggle default={LightSvg} checked={DarkSvg}
|
||||||
|
toggle={(checked) => this.toggleTheme(checked)} value={this.props.theme === 'dark'}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</div>
|
<Switch>
|
||||||
<footer>
|
<Route path="/blog/:entry_date/:entry_name" component={BlogEntry}/>
|
||||||
<section className="legal">
|
<Route path="/blog" component={Blog}/>
|
||||||
<div>{this.props.strings.company_name}</div>
|
<Route path="/" component={Home}/>
|
||||||
<div>{this.props.strings.address_one_line}</div>
|
</Switch>
|
||||||
<div>{this.props.strings.email_contact}</div>
|
<footer>
|
||||||
</section>
|
<section className="legal">
|
||||||
<section className="credit">
|
<div>{this.props.strings.company_name}</div>
|
||||||
<div>
|
<div>{this.props.strings.address_one_line}</div>
|
||||||
{this.props.strings.inferno_credit}
|
<div>{this.props.strings.email_contact}</div>
|
||||||
<a target="_blank" rel="noopener noreferrer" href="https://infernojs.org/">
|
</section>
|
||||||
{`Inferno ` + version}
|
</footer>
|
||||||
</a>
|
</BrowserRouter>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</footer>
|
|
||||||
</main>)
|
</main>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { langActions, themeActions } from './actions.js'
|
import { blogActions, langActions, themeActions } from './actions.js'
|
||||||
|
|
||||||
export const themeReducer = (state = 'light', action) => {
|
export const themeReducer = (state = 'dark', action) => {
|
||||||
switch (action.type)
|
switch (action.type)
|
||||||
{
|
{
|
||||||
case themeActions.THEME_CHANGE:
|
case themeActions.CHANGE:
|
||||||
return action.theme
|
return action.theme
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
|
|
@ -13,12 +13,12 @@ export const themeReducer = (state = 'light', action) => {
|
||||||
export const langReducer = (state = {lang: 'jp', request: null, strings: null}, action) => {
|
export const langReducer = (state = {lang: 'jp', request: null, strings: null}, action) => {
|
||||||
switch (action.type)
|
switch (action.type)
|
||||||
{
|
{
|
||||||
case langActions.LANG_CHANGE_REQUEST:
|
case langActions.CHANGE_REQUEST:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
request: action.lang
|
request: action.lang
|
||||||
}
|
}
|
||||||
case langActions.LANG_CHANGE_RESPONSE:
|
case langActions.CHANGE_RESPONSE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
lang: action.lang,
|
lang: action.lang,
|
||||||
|
|
@ -28,4 +28,41 @@ export const langReducer = (state = {lang: 'jp', request: null, strings: null},
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const blogReducer = (state = {
|
||||||
|
list: [],
|
||||||
|
list_requesting: false,
|
||||||
|
entry: null,
|
||||||
|
entry_name: null,
|
||||||
|
entry_requesting: false
|
||||||
|
}, action) => {
|
||||||
|
switch (action.type)
|
||||||
|
{
|
||||||
|
case blogActions.LIST_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
list_requesting: true
|
||||||
|
}
|
||||||
|
case blogActions.LIST_RESPONSE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
list: action.list,
|
||||||
|
list_requesting: false
|
||||||
|
}
|
||||||
|
case blogActions.ENTRY_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
entry_requesting: true
|
||||||
|
}
|
||||||
|
case blogActions.ENTRY_RESPONSE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
entry: action.entry,
|
||||||
|
entry_name: action.name,
|
||||||
|
entry_requesting: false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
159
webpack.base.js
159
webpack.base.js
|
|
@ -1,4 +1,100 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const marked = require('marked');
|
||||||
|
|
||||||
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
// const CompressionPlugin = require("compression-webpack-plugin")
|
||||||
|
|
||||||
|
const renderer = new marked.Renderer()
|
||||||
|
renderer.link = (href, title, text) => {
|
||||||
|
if(href === null)
|
||||||
|
{
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
let out = '<a href="' + href + '"'
|
||||||
|
if(title)
|
||||||
|
{
|
||||||
|
out += ' title="' + title + '"'
|
||||||
|
}
|
||||||
|
if(href.startsWith('http'))
|
||||||
|
{
|
||||||
|
out += ' target="_blank" rel="noreferrer noopener nofollow"'
|
||||||
|
}
|
||||||
|
out += '>' + text + '</a>'
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
renderer.image = (href, title, text) => {
|
||||||
|
if (href === null) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = '<img src="' + href + '" alt="' + text + '"';
|
||||||
|
if (title) {
|
||||||
|
out += ' title="' + title + '"';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out += ' title="' + text + '"';
|
||||||
|
}
|
||||||
|
out += '/>';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
marked.setOptions({ renderer: renderer })
|
||||||
|
|
||||||
|
class BlogListingPlugin
|
||||||
|
{
|
||||||
|
static default_options = {
|
||||||
|
output_file: 'blog_list.json',
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options = {})
|
||||||
|
{
|
||||||
|
this.options = {...BlogListingPlugin.default_options, ...options}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(compiler)
|
||||||
|
{
|
||||||
|
const plugin_name = BlogListingPlugin.name
|
||||||
|
const { webpack } = compiler
|
||||||
|
compiler.hooks.thisCompilation.tap(
|
||||||
|
plugin_name,
|
||||||
|
(compilation) => {
|
||||||
|
compilation.hooks.processAssets.tapAsync(
|
||||||
|
{
|
||||||
|
name: plugin_name,
|
||||||
|
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
|
||||||
|
},
|
||||||
|
(compilationAssets, callback) => {
|
||||||
|
let content = []
|
||||||
|
Object.keys(compilationAssets).forEach((file_path) => {
|
||||||
|
if(file_path.startsWith('blog') && file_path.endsWith('.md'))
|
||||||
|
{
|
||||||
|
const file_name = path.basename(file_path).slice(0, -3)
|
||||||
|
const blog_info = path.basename(path.dirname(file_path)).split('_')
|
||||||
|
const blog_date = blog_info[0]
|
||||||
|
const blog_entry = blog_info.slice(1).join('_')
|
||||||
|
compilation.emitAsset(
|
||||||
|
'blog/' + blog_date + '_' + blog_entry + '/' + file_name + '.html',
|
||||||
|
new webpack.sources.RawSource(
|
||||||
|
marked(
|
||||||
|
new TextDecoder('utf-8').decode(compilationAssets[file_path].source()))))
|
||||||
|
content.push({
|
||||||
|
name: file_name,
|
||||||
|
date: blog_date
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
content.sort((x1, x2) => { x1.create_date < x2.create_date })
|
||||||
|
compilation.emitAsset(
|
||||||
|
this.options.output_file, new webpack.sources.RawSource(JSON.stringify(content)))
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: "none",
|
mode: "none",
|
||||||
|
|
@ -8,33 +104,60 @@ module.exports = {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.jsx$/,
|
test: /\.jsx$/,
|
||||||
loader: "babel-loader",
|
exclude: /node_modules/,
|
||||||
options:
|
use: [{
|
||||||
{
|
loader: "babel-loader",
|
||||||
presets: ["@babel/preset-env"],
|
options:
|
||||||
plugins: [
|
{
|
||||||
["babel-plugin-inferno",
|
presets: ["@babel/preset-env"],
|
||||||
{
|
plugins: [
|
||||||
"imports": true
|
["babel-plugin-inferno",
|
||||||
}]
|
{
|
||||||
]
|
"imports": true
|
||||||
}
|
}]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /src.*\.s[ac]ss$/,
|
||||||
|
use: ['style-loader', 'css-loader', 'sass-loader']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /src.*\.css$/,
|
test: /src.*\.css$/,
|
||||||
use: ['style-loader', 'css-loader']
|
use: ['style-loader', 'css-loader']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /assets.*\.(jpg|png|svg|css)$/,
|
test: /\.svg$/,
|
||||||
loader: 'file-loader',
|
type: "asset/inline"
|
||||||
options: {
|
}
|
||||||
name: '[path][name].[ext]',
|
]
|
||||||
},
|
|
||||||
}]
|
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
new BlogListingPlugin(),
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [
|
||||||
|
{from: 'index.html'},
|
||||||
|
{from: 'robot.txt'},
|
||||||
|
{from: 'src/lang', to: 'lang'},
|
||||||
|
{from: 'assets/blog', to: 'blog'},
|
||||||
|
{from: 'assets/icons', to: 'assets/icons'},
|
||||||
|
{from: 'assets/images', to: 'assets/images'},
|
||||||
|
{from: 'assets/theme', to: 'assets/theme'}
|
||||||
|
]}),
|
||||||
|
// new CompressionPlugin(
|
||||||
|
// {
|
||||||
|
// filename: "[path].gz[query]",
|
||||||
|
// algorithm: "gzip",
|
||||||
|
// test: [/\.js/, /\.svg/],
|
||||||
|
// threshold: 1000
|
||||||
|
// })
|
||||||
|
],
|
||||||
output:
|
output:
|
||||||
{
|
{
|
||||||
filename: 'bundle.js',
|
filename: 'bundle.js',
|
||||||
path: path.resolve(__dirname, 'public')
|
path: path.resolve(__dirname, 'public'),
|
||||||
|
assetModuleFilename: 'assets/[query].[ext]',
|
||||||
|
clean: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
const path = require('path');
|
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
|
||||||
const CompressionPlugin = require("compression-webpack-plugin")
|
|
||||||
const config = require('./webpack.base.js');
|
const config = require('./webpack.base.js');
|
||||||
|
|
||||||
module.exports = Object.assign(
|
module.exports = Object.assign(
|
||||||
|
|
@ -23,47 +20,14 @@ module.exports = Object.assign(
|
||||||
inferno: require.resolve('inferno/dist/index.dev.esm.js')
|
inferno: require.resolve('inferno/dist/index.dev.esm.js')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: config.plugins.concat([
|
||||||
new webpack.DefinePlugin(
|
new webpack.DefinePlugin(
|
||||||
{
|
{
|
||||||
'process.env':
|
'process.env':
|
||||||
{
|
{
|
||||||
'DEBUG': true
|
'DEBUG': true
|
||||||
} // set a DEBUG flag that can be used in the scripts
|
} // set a DEBUG flag that can be used in the scripts
|
||||||
}),
|
|
||||||
new CopyPlugin([
|
|
||||||
{from: 'index.html'},
|
|
||||||
{from: 'robot.txt'},
|
|
||||||
{from: 'src/lang', to: 'lang'},
|
|
||||||
{from: 'assets/public', to: 'assets'}
|
|
||||||
]),
|
|
||||||
new CompressionPlugin(
|
|
||||||
{
|
|
||||||
filename: "[path].gz[query]",
|
|
||||||
algorithm: "gzip",
|
|
||||||
test: [/\.js/, /\.svg/],
|
|
||||||
threshold: 1000
|
|
||||||
})
|
})
|
||||||
],
|
])
|
||||||
// webpack-dev-server configuration
|
|
||||||
devServer:
|
|
||||||
{
|
|
||||||
contentBase: path.join(__dirname),
|
|
||||||
compress: true,
|
|
||||||
port: 8080,
|
|
||||||
// Not elegant way to get rid of the gzip path in index.html
|
|
||||||
proxy:
|
|
||||||
{
|
|
||||||
'/$':
|
|
||||||
{
|
|
||||||
target: 'http://localhost:8080',
|
|
||||||
secure: false,
|
|
||||||
bypass: function(request, response, proxyOptions)
|
|
||||||
{
|
|
||||||
return '/index_dev.html';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
|
||||||
const CompressionPlugin = require("compression-webpack-plugin")
|
|
||||||
const config = require('./webpack.base.js');
|
const config = require('./webpack.base.js');
|
||||||
|
|
||||||
module.exports = Object.assign(
|
module.exports = Object.assign(
|
||||||
|
|
@ -11,27 +9,14 @@ module.exports = Object.assign(
|
||||||
{
|
{
|
||||||
minimize: true
|
minimize: true
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: config.plugins.concat([
|
||||||
new webpack.DefinePlugin(
|
new webpack.DefinePlugin(
|
||||||
{
|
{
|
||||||
'process.env':
|
'process.env':
|
||||||
{
|
{
|
||||||
'DEBUG': false
|
'DEBUG': false
|
||||||
} // set a DEBUG flag that can be used in the scripts (can be skipped)
|
} // set a DEBUG flag that can be used in the scripts (can be skipped)
|
||||||
}),
|
|
||||||
new CopyPlugin([
|
|
||||||
{from: 'index.html'},
|
|
||||||
{from: 'robot.txt'},
|
|
||||||
{from: 'src/lang', to: 'lang'},
|
|
||||||
{from: 'assets/public', to: 'assets'}
|
|
||||||
]),
|
|
||||||
new CompressionPlugin(
|
|
||||||
{
|
|
||||||
filename: "[path].gz[query]",
|
|
||||||
algorithm: "gzip",
|
|
||||||
test: [/\.js/, /\.svg/],
|
|
||||||
threshold: 1000
|
|
||||||
})
|
})
|
||||||
]
|
])
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue