How to make simple overlay container in React

Issue #699

Use term ZStack like in SwiftUI, we declare container as relative position. For now it uses only 2 items from props.children but can be tweaked to support mutiple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class App extends React.Component {
render() {
return (
<ZStack>
<Header />
<div css={css`
padding-top: 50px;
`}>
<Showcase factory={factory} />
<Footer />
</div>
</ZStack>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** @jsx jsx */
import React from 'react';
import { css, jsx } from '@emotion/core'

export default function ZStack(props) {
return (
<div css={css`
position: relative;
`}>
<div >
{props.children[0]}
</div>
<div css={css`
position: absolute;
top: 0;
left: 0;
width: 100%;
`}>
{props.children[1]}
</div>
</div>
)
}

How to use useEffect in React hook

Issue #669

Specify params as array [year, id], not object {year, id}

1
2
3
4
5
6
7
8
9
10
11
12
13
const { year, id } = props
useEffect(() => {
const loadData = async () => {
try {

} catch (error) {
console.log(error)
}
}

listenChats()
}, [year, id])
}

Hooks effect

By default, it runs both after the first render and after every update.

This requirement is common enough that it is built into the useEffect Hook API. You can tell React to skip applying an effect if certain values haven’t changed between re-renders. To do so, pass an array as an optional second argument to useEffect:

1
2
3
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

How to handle events for input with React

Issue #661

Use Bulma css

1
2
3
4
5
6
7
8
9
10
11
12
<input
class="input is-rounded"
type="text"
placeholder="Say something"
value={value}
onChange={(e) => { onValueChange(e.target.value) }}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onSend(e)
}
}}
/>

How to handle evens in put with React

Issue #661

Use Bulma css

1
2
3
4
5
6
7
8
9
10
11
12
<input
class="input is-rounded"
type="text"
placeholder="Say something"
value={value}
onChange={(e) => { onValueChange(e.target.value) }}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onSend(e)
}
}}
/>

How to auto scroll to top in react router 5

Issue #659

Use useLocation https://reacttraining.com/react-router/web/guides/scroll-restoration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, { useEffect } from 'react';
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useLocation,
withRouter
} from 'react-router-dom'

function _ScrollToTop(props) {
const { pathname } = useLocation();

useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);

return props.children
}

const ScrollToTop = withRouter(_ScrollToTop)

function App() {
return (
<div>
<Router>
<ScrollToTop>
<Header />
<Content />
<Footer />
</ScrollToTop>
</Router>
</div>
)
}

How to use dynamic route in react router 5

Issue #658

Declare routes, use exact to match exact path as finding route is from top to bottom. For dynamic route, I find that we need to use render and pass the props manually.

Declare Router as the rooter with Header, Content and Footer. Inside Content there is the Switch, so header and footer stay the same across pages

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from 'react-router-dom'

function Content() {
return (
<Switch>
<Route exact path="/about">
<About />
</Route>
<Route exact path="/">
<Home />
</Route>
<Route path='/book/:id' render={(props) => {
return ( <BookDetail {...props } /> )
}} />
<Route>
<NotFound />
</Route>
</Switch>
)
}

function App() {
return (
<div>
<Router>
<Header />
<Content />
<Footer />
</Router>
</div>
)
}

To trigger route request, use useHistory hook. Note that we need to declare variable, and not use useHistory().push directly

1
2
3
4
5
6
7
import { useHistory } from 'react-router-dom'

const history = useHistory()

<a onClick={() => {
history.push(`/book/${uuid}`)
}} >

To get parameters, use match

1
2
3
4
5
6
7
8
export default function BookDetail(props) {
const [ state, setState ] = useState({
book: null
})

const { match } = props
// match.params.id
}

How to use folder as local npm package

Issue #657

Add library folder src/library

1
2
3
4
5
6
7
src
library
res.js
screens
Home
index.js
package.json

Declare package.json in library folder

1
2
3
4
{
"name": "library",
"version": "0.0.1"
}

Declare library as dependency in root package.json

1
2
3
"dependencies": {
"library": "file:src/library"
},

Now import like normal, for example in src/screens/Home/index.js

1
import res from 'library/res'

How to scroll to element in React

Issue #648

In a similar fashion to plain old javascript, note that href needs to have valid hash tag, like #features

1
2
3
4
5
6
7
8
9
10
11
<a
href='#features'
onClick={() => {
const options = {
behavior: 'smooth'
}
document.getElementById('features-section').scrollIntoView(options)
}}
>
Features
</a>

How to make simple filter menu in css

Issue #643

Use material icons

1
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
div#filter-container {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10%;
height: 60px;
}

div#filter-items {
display: inline-flex;

background-color: #fff;
box-shadow: 0 0 1px 0 rgba(52, 46, 173, 0.25), 0 15px 30px 0 rgba(52, 46, 173, 0.1);
border-radius: 12px;
overflow: hidden;
padding: 10px;
}

a.filter-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100px;
text-decoration: none;
padding: 10px;
}

a.filter-item:hover {
background-color: rgb(239, 240, 241);
border-radius: 10px;
}

a.filter-item:active {
transform: scale(0.9);
}

a.filter-item.selected {
background-color: rgb(239, 240, 241);
border-radius: 10px;
}

span.material-icons {
font-family: "Material Icons";
display: block;
margin-bottom: 4px;
font-size: 26px;
color: mix(#fff, #342ead, 60%);
transition: 0.25s ease;
}

span.name {
display: block;
font-size: 13px;
color: mix(#fff, #342ead, 70%);
transition: 0.25s ease;
font-family: 'Open Sans', sans-serif;
font-weight: 500;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
filters.forEach((filter) => {
const item = document.createElement('a')
item.href = '#'
item.className = 'filter-item'
container.appendChild(item)

const icon = document.createElement('span')
icon.className = 'material-icons'
icon.innerText = filter.icon
item.appendChild(icon)

const name = document.createElement('span')
name.className = 'name'
name.innerText = filter.name
item.appendChild(name)

item.onclick = () => {
handleFilterClick(item, filter)
}
})

How to add independent page in hexo

Issue #641

Create a new page

1
hexo new page mydemo

Remove index.md and create index.html, you can reference external css and js in this index.html. Hexo has hexo new page mydemo --slug but it does not support page hierarchy

Specify no layout so it is independent page.

1
2
3
---
layout: false
---

How to use async function as parameter in TypeScript

Issue #640

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function useCache(
collectionName: string,
key: string,
response: functions.Response<any>,
fetch: () => Promise<any>
): Promise<any> {
const existing = await db.collection(collectionName).doc(key).get()
if (existing.exists) {
response.send(existing.data())
return
}

const object = await fetch()
const json = Object.assign({}, object)
await db.collection(collectionName).doc(key).set(json)
response.send(object)
}
1
2
3
4
5
6
7
8
9
useCache(
"books",
key,
response,
async () => {
const service = new Service()
return await service.doSomething(key)
}
)

How to get ISO string from date in Javascript

Issue #552

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type Components = {
day: number,
month: number,
year: number
}

export default class DateFormatter {
// 2018-11-11T00:00:00
static ISOStringWithoutTimeZone = (date: Date): string => {
const components = DateFormatter.format(DateFormatter.components(date))
return `${components.year}-${components.month}-${components.day}T00:00:00`
}

static format = (components: Components) => {
return {
day: `${components.day}`.padStart(2, '0'),
month: `${components.month}`.padStart(2, '0'),
year: components.year
}
}

static components = (date: Date): Components => {
return {
day: date.getDate(),
month: date.getMonth() + 1,
year: date.getFullYear()
}
}
}

Read more

How to apply translations to Localizable.strings

Issue #492

Suppose we have a base Localizable.strings

1
2
"open" = "Open";
"closed" = "Closed";

After sending that file for translations, we get translated versions.

1
2
"open" = "Åpen";
"closed" = "Stengt";

Searching and copy pasting these to our Localizable.strings is tedious and time consuming. We can write a script to apply that.

Remember that we need to be aware of smart and dump quotes

1
2
.replace(/\"/g, '')
.replace(/\"/g, '')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const fs = require('fs')

const originalFile = 'MyApp/Resources/nb.lproj/Localizable.strings'
const translationString = `
"open" = "Åpen";
"closed" = "Stengt";
`

class Translation {
constructor(key, value) {
this.key = key
this.value = value
}
}

Translation.make = function make(line) {
if (!line.endsWith(';')) {
return new Translation('', '')
}

const parts = line
.replace(';')
.split(" = ")

const key = parts[0]
.replace(/\"/g, '')
.replace(/\”/g, '')
const value = parts[1]
.replace(/\"/g, '')
.replace(/\”/g, '')
.replace('undefined', '')
return new Translation(key, value)
}

function main() {
const translations = translationString
.split(/\r?\n/)
.map((line) => { return Translation.make(line) })

apply(translations, originalFile)
}

function apply(translations, originalFile) {
try {
const originalData = fs.readFileSync(originalFile, 'utf8')
const originalLines = originalData.split(/\r?\n/)
const parsedLine = originalLines.map((originalLine) => {
const originalTranslation = Translation.make(originalLine)
const find = translations.find((translation) => { return translation.key === originalTranslation.key })
if (originalLine !== "" && find !== undefined && find.key !== "") {
return `"${find.key}" = "${find.value}";`
} else {
return originalLine
}
})

const parsedData = parsedLine.join('\n')
fs.writeFileSync(originalFile, parsedData, { overwrite: true })
} catch (err) {
console.error(err)
}
}

main()

How to use react-native link and CocoaPods

How to notarize electron app

Issue #430

Use electron builder

1
npm install electron-builder@latest --save-dev

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"name": "icon_generator",
"version": "1.3.0",
"description": "A macOS app to generate app icons",
"main": "babel/main.js",
"repository": "https://github.com/onmyway133/IconGenerator",
"author": "Khoa Pham",
"license": "MIT",
"scripts": {
"start": "npm run babel && electron .",
"babel": "babel ./src --out-dir ./babel --copy-files",
"dist": "npm run babel && electron-builder"
},
"build": {
"appId": "com.onmyway133.IconGenerator",
"buildVersion": "20",
"productName": "Icon Generator",
"icon": "./Icon/Icon.icns",
"mac": {
"category": "public.app-category.productivity",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "./entitlements.plist",
"entitlementsInherit": "./entitlements.plist"
},
"win": {
"target": "msi"
},
"linux": {
"target": [
"AppImage",
"deb"
]
},
"afterSign": "./afterSignHook.js"
}
}

Declare entitlements

entitlements.plist

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>

Use electron-notarize

afterSignHook.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const fs = require('fs');
const path = require('path');
var electron_notarize = require('electron-notarize');

module.exports = async function (params) {
// Only notarize the app on Mac OS only.
if (process.platform !== 'darwin') {
return;
}
console.log('afterSign hook triggered', params);

// Same appId in electron-builder.
let appId = 'com.onmyway133.IconGenerator'

let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`);
if (!fs.existsSync(appPath)) {
throw new Error(`Cannot find application at: ${appPath}`);
}

console.log(`Notarizing ${appId} found at ${appPath}`);

try {
await electron_notarize.notarize({
appBundleId: appId,
appPath: appPath,
appleId: process.env.appleId,
appleIdPassword: process.env.appleIdPassword,
});
} catch (error) {
console.error(error);
}

console.log(`Done notarizing ${appId}`);
};

Run

Generate password for Apple Id because of 2FA

1
2
3
export appleId=onmyway133@gmail.com
export appleIdPassword=1234-abcd-efgh-7890
npm run dist

Check

1
spctl --assess --verbose Icon\ Generator.app

Troubleshooting

babel

  • Since electron-builder create dist folder for distribution, for example dist/mac/Icon Generator, I’ve renamed babel generated code to babel directory

babel 6 regeneratorRuntime is not defined

It is because of afterSignHook. Ignore in .babelrc not work

1
2
3
4
5
6
7
8
9
{
"plugins": [
"transform-react-jsx-source"
],
"presets": ["env", "react"],
"ignore": [
"afterSignHook.js"
]
}

Should use babel 7 with babel.config.js

1
2
npm install --save @babel/runtime 
npm install --save-dev @babel/plugin-transform-runtime

Use electron-forge

https://httptoolkit.tech/blog/notarizing-electron-apps-with-electron-forge/

Read more

How to use custom domain for GitHub pages

Issue #423

In DNS settings

Add 4 A records

1
2
3
4
A @ 185.199.110.153
A @ 185.199.111.153
A @ 185.199.108.153
A @ 185.199.109.153

and 1 CNAME record

1
CNAME www learntalks.github.io

In GitHub

  • Select custom domain and type learntalks.com

In source

public/CNAME

1
learntalks.com

How to read and write file using fs in node

Issue #419

1
2
3
4
5
6
7
8
9
10
11
12
function write(json) {
const data = JSON.stringify(json)
const year = json.date.getFullYear()
const directory = `collected/${slugify(className)}/${year}`

fs.mkdirSync(directory, { recursive: true })
fs.writeFileSync(
`${directory}/${slugify(studentName)}`,
data,
{ overwrite: true }
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
async function readAll() {
const classes = fs.readdirSync('classes')
classes.forEach((class) => {
const years = fs.readdirSync(`classes/${class}`)
years.forEach((year) => {
const students = fs.readdirSync(`classes/${class}/${year}`)
students.forEach((student) => {
const data = fs.readFileSync(`classes/${class}/${year}/${student})
const json = JSON.parse(data)
})
})
})
}

How to get videos from vimeo in node

Issue #418

Code

Path for user users/nsspain/videos
Path for showcase https://developer.vimeo.com/api/reference/albums#get_album
Path for Channels, Groups and Portfolios

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const Vimeo = require('vimeo').Vimeo
const vimeoClient = new Vimeo(vimeoClientId, vimeoClientSecret, vimeoAccessToken)

async getVideos(path) {
const options = {
path: `channels/staffpicks/videos`,
query: {
page: 1,
per_page: 100,
fields: 'uri,name,description,created_time,pictures'
}
}

return new Promise((resolve, reject) => {
try {
vimeoClient.request(options, (error, body, status_code, headers) => {
if (isValid(body)) {
resolve(body)
} else {
throw error
}
})
} catch (e) {
reject(e)
console.log(e)
}
})
}

Response look like

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
{
"total":13754,
"page":1,
"per_page":100,
"paging":{
"next":"/channels/staffpicks/videos?page=2&per_page=100&fields=uri%2Cname%2Cdescription%2Ccreated_time%2Cpictures",
"previous":null,
"first":"/channels/staffpicks/videos?page=1&per_page=100&fields=uri%2Cname%2Cdescription%2Ccreated_time%2Cpictures",
"last":"/channels/staffpicks/videos?page=138&per_page=100&fields=uri%2Cname%2Cdescription%2Ccreated_time%2Cpictures"
},
"data":[
{
"uri":"/videos/359281775",
"name":"Maestro",
"description":"A Bloom Pictures short film directed by Illogic.\n\n\"Maestro\" is this week's Staff Pick Premiere. Read more about it on the Vimeo Blog: https://vimeo.com/blog/post/staff-pick-premiere-maestro-from-illogic\n\nMaking of :\n https://vimeo.com/bloompictures/maestromakingof\n\nYou want to collaborate?\nSend us a message at : hello@bloompictures.tv\n\nFor festivals and screenings, please contact : \nfestival@miyu.fr\n\nPress/Media requests : \nbenoit@animationshowcase.com\n\nhttps://www.bloompictures.tv\n\n©Bloom Pictures 2019",
"created_time":"2019-09-11T12:31:33+00:00",
"pictures":{
"uri":"/videos/359281775/pictures/813130850",
"active":true,
"type":"custom",
"sizes":[
{
"width":100,
"height":75,
"link":"https://i.vimeocdn.com/video/813130850_100x75.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_100x75.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":200,
"height":150,
"link":"https://i.vimeocdn.com/video/813130850_200x150.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_200x150.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":295,
"height":166,
"link":"https://i.vimeocdn.com/video/813130850_295x166.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_295x166.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":640,
"height":360,
"link":"https://i.vimeocdn.com/video/813130850_640x360.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_640x360.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":1280,
"height":720,
"link":"https://i.vimeocdn.com/video/813130850_1280x720.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_1280x720.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":1920,
"height":1080,
"link":"https://i.vimeocdn.com/video/813130850_1920x1080.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_1920x1080.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":640,
"height":346,
"link":"https://i.vimeocdn.com/video/813130850_640x346.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_640x346.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":960,
"height":519,
"link":"https://i.vimeocdn.com/video/813130850_960x519.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_960x519.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":1280,
"height":692,
"link":"https://i.vimeocdn.com/video/813130850_1280x692.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_1280x692.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":1920,
"height":1038,
"link":"https://i.vimeocdn.com/video/813130850_1920x1038.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_1920x1038.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":1280,
"height":692,
"link":"https://i.vimeocdn.com/video/813130850_1280x692.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_1280x692.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
}
],
"resource_key":"b42ac645a67b3277cb2fe66d3894016842ceef72"
}
}
]
}

Read more

How to get videos from youtube in node

Issue #417

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Youtube {
async getVideos(playlistId, pageToken) {
const options = {
key: clientKey,
part: 'id,contentDetails,snippet',
playlistId: playlistId,
maxResult: 100,
pageToken
}

return new Promise((resolve, reject) => {
try {
youtube.playlistItems.list(options, (error, result) => {
if (isValid(result)) {
resolve(result)
} else {
throw error
}
})
} catch (e) {
reject(e)
}
})
}
}

Response look like

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
{
"kind": "youtube#playlistItemListResponse",
"etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ZNTrH71d3sV6gR6BWPeamXI1HhE\"",
"nextPageToken": "CAUQAA",
"pageInfo": {
"totalResults": 32,
"resultsPerPage": 5
},
"items": [
{
"kind": "youtube#playlistItem",
"etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/pt-bElhU3f7Q6c1Wc0URk9GJN-w\"",
"id": "UExDbDVOTTRxRDN1X0w4ZEpyV1liTEI4RmNVYW9BSERGdC4yODlGNEE0NkRGMEEzMEQy",
"snippet": {
"publishedAt": "2019-04-11T06:09:26.000Z",
"channelId": "UCuPue-GLK4nVX8klxQITIOw",
"title": "abc",
"description": "abc",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/ZefmzgLabCA/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/ZefmzgLabCA/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/ZefmzgLabCA/hqdefault.jpg",
"width": 480,
"height": 360
},
"standard": {
"url": "https://i.ytimg.com/vi/ZefmzgLabCA/sddefault.jpg",
"width": 640,
"height": 480
},
"maxres": {
"url": "https://i.ytimg.com/vi/ZefmzgLabCA/maxresdefault.jpg",
"width": 1280,
"height": 720
}
},
"channelTitle": "try! Swift Conference",
"playlistId": "PLCl5NM4qD3u_L8dJrWYbLB8FcUaoAHDFt",
"position": 0,
"resourceId": {
"kind": "youtube#video",
"videoId": "ZefmzgLabCA"
}
}
}
]
}

To handle pagination

1
2
3
4
5
6
7
8
9
async getVideosLoop(playlistId, nextPageToken, items, count) {
const response = await this.getVideos(playlistId, nextPageToken)
const newItems = items.concat(response.data.items)
if (isValid(response.data.nextPageToken) && count < 10) {
return this.getVideosLoop(playlistId, response.data.nextPageToken, newItems, count + 1)
} else {
return newItems
}
}

To get playlist title, use playlists.list

Read more

How to convert callback to Promise in Javascript

Issue #416

1
2
3
4
5
6
7
8
9
10
11
12
13
// @flow

const toPromise = (f: (any) => void) => {
return new Promise<any>((resolve, reject) => {
try {
f((result) => {
resolve(result)
})
} catch (e) {
reject(e)
}
})
}
1
const videos = await toPromise(callback)

If a function accepts many parameters, we need to curry https://onmyway133.github.io/blog/Curry-in-Swift-and-Javascript/

1
2
3
4
5
6
7
8
9
10
function curry2(f) {
return (p1) => {
return (p2) => {
return f(p1, p2)
}
}
}

const callback = curry2(aFunctionThatAcceptsOptionsAndCallback)(options)
const items = await toPromise(callback)

How to fix electron issues

Issue #415

Electron require() is not defined

https://stackoverflow.com/questions/44391448/electron-require-is-not-defined

1
2
3
4
5
6
7
8
9
10
11
12
function createWindow () {
win = new BrowserWindow({
title: 'MyApp',
width: 600,
height: 500,
resizable: false,
icon: __dirname + '/Icon/Icon.icns',
webPreferences: {
nodeIntegration: true
}
})
}

DevTools was disconnected from the page

1
2
npm install babel-cli@latest --save-dev
npm install react@16.2.0
1
win.openDevTools()

This leads to Cannot find module 'react/lib/ReactComponentTreeHook'
If we’re using binary, then rebuild, it is the problem that cause devTools not work

1
npx electron-builder

This goes to Cannot read property injection of undefined at react-tap-event-plugin

https://github.com/zilverline/react-tap-event-plugin/issues/121

1
npm uninstall react-tap-event-plugin

This goes to Unknown event handler property onTouchTap in EnhancedButton in material-ui

Update material-ui

https://material-ui.com/
https://material-ui.com/guides/migration-v0x/#raised-button

1
npm install @material-ui/core

From

1
2
3
4
import RadioButtonGroup from '@material-ui/core/RadioButton/RadioButtonGroup'
import RadioButton from '@material-ui/core/RadioButton'
import RaisedButton from '@material-ui/core/RaisedButton'
import CardText from '@material-ui/core/Card/CardText'

to

1
2
3
4
import RadioButtonGroup from '@material-ui/core/RadioGroup'
import RadioButton from '@material-ui/core/Radio'
import RaisedButton from '@material-ui/core/Button'
import CardText from '@material-ui/core/DialogContentText'

Use FormControlLabel https://material-ui.com/components/radio-buttons/

1
2
3
4
5
6
7
<RadioGroup 
style={styles.group}
defaultselected={this.state.choice}
onChange={this.handleChoiceChange}
children={choiceElements} />
{this.makeGenerateButton()}
/>

How to make collaborative drawing canvas with socketio and node

Issue #399

Client

App.js

1
2
3
4
5
6
7
8
9
10
11
12
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import Main from './Main'

class App extends Component {
render() {
return <Main />
}
}

export default App;

Main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// @flow

import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';

import Manager from './Manager'

const styles = {
root: {
flexGrow: 1,
},
grow: {
flexGrow: 1,
},
menuButton: {
marginLeft: -12,
marginRight: 20,
},
};

class Main extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
}

render() {
const { classes } = this.props;

return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<IconButton className={classes.menuButton} color="inherit" aria-label="Menu">
<MenuIcon />
</IconButton>
<Typography variant="h6" color="inherit" className={classes.grow}>
Collaborate Canvas
</Typography>
<Button color="inherit" onClick={this.onImagePress} >Image</Button>
<Button color="inherit" onClick={this.onClearPress} >Clear</Button>
<input ref="fileInput" type="file" id="myFile" multiple accept="image/*" style={{display: 'none'}} onChange={this.handleFiles}></input>
</Toolbar>
</AppBar>
<canvas ref="canvas" with="1000" height="1000"></canvas>
</div>
)
}

componentDidMount() {
const canvas = this.refs.canvas
this.manager = new Manager(canvas)
this.manager.connect()
}

onImagePress = () => {
const fileInput = this.refs.fileInput
fileInput.click()
}

onClearPress = () => {
this.manager.clear()
}

handleFiles = (e) => {
e.persist()
const canvas = this.refs.canvas
const context = canvas.getContext('2d')

const file = e.target.files[0]
var image = new Image()
image.onload = function() {
context.drawImage(image, 0, 0, window.innerWidth, window.innerHeight)
}
image.src = URL.createObjectURL(file)
}
}

export default withStyles(styles)(Main);

Server

Use express and socket.io

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// @flow

const express = require('express')
const app = express()
const http = require('http')
const socketIO = require('socket.io')

const server = http.createServer(app)
const io = socketIO.listen(server)
server.listen(3001)
app.use(express.static(__dirname + '/public'))
console.log("Server running on 127.0.0.1:8080")

let lines = []
io.on('connection', (socket) => {
lines.forEach((line) => {
const data = { line }
socket.emit('draw_line', data)
})

socket.on('draw_line', (data) => {
const { line } = data
lines.push(line)

io.emit('draw_line', data)
})

socket.on('clear', () => {
lines = []
io.emit('clear')
})

socket.on('draw_image', (data) => {
io.emit('draw_image', data)
})
})

How to generate changelog for GitHub releases with rxjs and node

Issue #398

How to

Technical

Dependencies

1
2
3
4
const Rx = require('rxjs/Rx')
const Fetch = require('node-fetch')
const Minimist = require('minimist')
const Fs = require('fs')

Use GraphQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
makeOptions(query, token) {
return {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `bearer ${token}`
},
body: JSON.stringify({
query: `
query {
repository(owner: "${this.owner}", name: "${this.repo}") {
${query}
}
}
`
})
}
}

Use orderBy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fetchPRsAndIssues(dates) {
const query = `
pullRequests(last: 100, orderBy: {field: UPDATED_AT, direction: ASC}) {
edges {
node {
title
merged
mergedAt
url
author {
login
url
}
}
}
}
issues(last: 100, orderBy: {field: UPDATED_AT, direction: ASC}) {
edges {
node {
title
closed
updatedAt
url
}
}
}
}
}

How to get all GitHub issues using GraphQL

Issue #393

https://developer.github.com/v4/explorer/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
query { 
repository(owner: "onmyway133", name: "blog") {
issues(orderBy: {field: UPDATED_AT, direction: ASC}, last: 100) {
edges {
cursor
node {
title
createdAt
updatedAt
labels(first: 10) {
edges {
node {
name
}
}
}
}
}
}
}
}

In node.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const GraphQL = require('graphql-request')
const GraphQLClient = GraphQL.GraphQLClient

const client = new GraphQLClient('https://api.github.com/graphql', {
headers: {
Authorization: 'Bearer 123456730712334152e6e1232c53987654312',
},
})

const query = `{
repository(owner: "onmyway133", name: "blog") {
issues(first: 100) {
edges {
node {
title
createdAt
updatedAt
labels(first: 10) {
edges {
node {
name
}
}
}
}
}
}
}
}`

client.request(query)
.then(data => {
const issues = data.repository.issues.edges.map((edge) => { return edge.node })
console.log(issues)
})

How to use Hexo to deploy static site

Issue #392

It’s been a long journey since https://github.com/onmyway133/blog/issues/1, next step is to keep GitHub issues as source, and mirror those to a static site.

Use 2 repos

1
2
3
4
npm install -g hexo-cli
echo $PATH=$PATH:/Users/khoa/.nodenv/versions/10.15.2/bin/hexo
hexo init blog
npm install hexo-deployer-git --save

Update _config.yml

1
2
3
4
deploy:
type: git
repo: https://github.com/onmyway133/onmyway133.github.io.git
branch: master
1
2
hexo clean
hexo deploy

Read more

How to use type coersion in Javascript

Issue #391

People who make fun of Javascript probably don’t understand implicit type coersion and when to use triple equal. Javascript is very unexpected, but when we work with this language, we need to be aware.

  1. Coercion–Automatically changing a value from one type to another.
  2. If x is Number and y is String, return x == ToNumber(y)
  3. If x is String or Number and y is Object, return x == ToPrimitive(y)
  4. Empty array becomes empty string

Read more

How to safely access deeply nested object in Javascript

Issue #390

An object ‘s property can be null or undefined.

Accessing step by step is tedious

1
2
3
4
props.user &&
props.user.posts &&
props.user.posts[0] &&
props.user.posts[0].comments

Dynamic parsing path is too clever and involves string in the end, which is a no no

1
2
3
4
const get = (p, o) =>
p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o)

const getUserComments = get(['user', 'posts', 0, 'comments'])

Instead let’s use function and catch errors explicitly, and defaults with a fallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const get: (f, defaultValue) => {
try {
const value = f()
if (isNotNullOrUndefined(value)) {
return value
} else {
return defaultValue
}
} catch {
return defaultValue
}
}

const comments = get(() => { .user.posts[0].comments }, [])

Read more