Updated to V2

Signed-off-by: Florian Bouillon <florian.bouillon@delta-wings.net>
This commit is contained in:
2020-09-12 23:11:43 +02:00
parent de9f57906f
commit abad642e10
77 changed files with 6380 additions and 3093 deletions

View File

@ -1,191 +0,0 @@
module.exports = {
env: {
browser: true,
es6: true,
node: true
},
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
],
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly"
},
parser: "@typescript-eslint/parser",
parserOptions: {
project: "tsconfig.json",
ecmaFeatures: {
jsx: true
},
ecmaVersion: 2018,
sourceType: "module"
},
settings: {
react: {
version: "detect"
}
},
plugins: [
"react",
"@typescript-eslint"
],
rules: {
indent: [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
quotes: "off",
"@typescript-eslint/quotes": [
"error",
"single",
{ avoidEscape: true }
],
semi: "off",
"@typescript-eslint/semi": [
"error",
"never"
],
"no-unused-expressions": "off",
"@typescript-eslint/no-unused-expressions": ["error", { "allowTernary": true }],
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": [
"error",
{
default: 'generic'
}
],
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
accessibility: "explicit"
}
],
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/member-delimiter-style": [
"error",
{
multiline: {
delimiter: "none",
requireLast: true
},
singleline: {
delimiter: "semi",
requireLast: false
}
}
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
"arrow-parens": [
"error",
"always"
],
complexity: "off",
"constructor-super": "error",
curly: "error",
"dot-notation": "error",
"eol-last": "error",
eqeqeq: [
"error",
"smart"
],
"guard-for-in": "error",
"id-blacklist": [
"error",
"any",
"Number",
"number",
"String",
"string",
"Boolean",
"boolean",
"Undefined"
],
"id-match": "error",
"max-classes-per-file": [
"error",
1
],
"max-len": [
"error",
{
code: 120
}
],
"new-parens": "error",
"no-bitwise": "error",
"no-caller": "error",
"no-cond-assign": "error",
"no-debugger": "error",
"no-empty": "error",
"no-eval": "error",
"no-fallthrough": "off",
"no-invalid-this": "off",
"no-multiple-empty-lines": "error",
"no-new-wrappers": "error",
"no-shadow": [
"error",
{
hoist: "all"
}
],
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-underscore-dangle": "error",
"no-unsafe-finally": "error",
"no-unused-labels": "error",
"no-unused-vars": "off",
"no-var": "error",
"object-shorthand": "error",
"one-var": [
"error",
"never"
],
"prefer-const": "error",
"quote-props": [
"error",
"consistent-as-needed"
],
"radix": "error",
"space-before-function-paren": "off",
"@typescript-eslint/space-before-function-paren": ["error", {
asyncArrow: "always",
anonymous: "never",
named: "never"
}],
"spaced-comment": "error",
"use-isnan": "error",
"valid-typeof": "off",
}
};

72
.eslintrc.json Normal file
View File

@ -0,0 +1,72 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"root": true,
"ignorePatterns": [
"src/shared",
"out",
".next",
"node_modules",
"*.js"
],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"google",
"plugin:@typescript-eslint/recommended",
"plugin:sonarjs/recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"settings": {
"react": {
"version": "detect"
},
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
},
"import/resolver": {
"typescript": {
"alwaysTryTypes": true // always try to resolve types under `<root>@types` directory even it doesn't contain any source code, like `@types/unist`
}
}
},
"plugins": [
"react",
"@typescript-eslint",
"sonarjs",
"import"
],
"rules": {
"indent": ["error", "tab"],
"@typescript-eslint/quotes": ["error", "single", { "avoidEscape": true }],
"semi": ["error", "never"],
"no-tabs": "off",
"require-jsdoc": "off",
"no-invalid-this": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"max-len": ["warn", { "code": 120 }],
"object-curly-spacing": ["error", "always"],
"comma-dangle": ["error", "never"],
"padded-blocks": ["error", { "classes": "always" }],
"import/order": ["error"],
"import/no-named-as-default-member": "off",
"import/no-named-as-default": "off"
}
}

3
.gitattributes vendored
View File

@ -1,3 +1,6 @@
*.ttf filter=lfs diff=lfs merge=lfs -text
*.eot filter=lfs diff=lfs merge=lfs -text
*.woff filter=lfs diff=lfs merge=lfs -text
*.woff2 filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"stylelint.customSyntax": "stylelint-plugin-stylus/custom-syntax",
"stylelint.validate": [
"stylus"
]
}

View File

@ -1,22 +1,23 @@
# Next Template
_if a line contains the following `<cs>` it means that it is only used on a Custom Server and that it can safely removed if it is not used_
## Folders
- \__tests__: Test files to test webpage or single components
- .vscode: VSCode settings for Stylelint
- docker: Docker files to launch in a Container
- public: Static files
- assets: Generally used for Static assets like Pictures
- uploads: used for user uploaded files
- scripts: Contains scripts to quicky make actions (take look in each file to see what it does)
- scripts: Contains Addons script (like Favicon generation or sitemap generation)
- src: Source folder
- client: Client-side Elements
- components: Components used in pages
- libs: Code used by the Client
- styl: Stylus file location
- modules: Stylus module location
- common: Elements used by both Client-side and Server-side code
- pages: Contains your NextJS pages
- server: Custom server folder (if you are doing a basic NextJS app you can freely delete this folder)
- server: Custom server folder `<cs>`
## TODO list
@ -45,24 +46,27 @@ If you want to have a Custom server you simply have to start editing `src/server
### Dependencies
- @zeit/next-stylus: Stylus support in Nextjs
- express: Server `<cs>`
- glob: Sitemap Generation
- next: Nextjs
- next-compose-plugins: Better plugin formatting in config
- next-purgecss: PurgeCSS Plugin in config
- next-seo: SEO for NextJS
- react: React
- react-dom: React DOM (React Dependency)
- react-feather: Icon library
- serve: Server for static website
- styled-jsx-plugin-stylus: Styled-JSX plugin of Stylus
- stylus: Stylus
- typescript: Typescript
- webpack: Webpack
- webpack-cli: Webpack dependency
### Dev Dependencies
- @babel/core: Tests Dependency
- @babel/preset-env: Tests Dependency
- @babel/preset-react: Tests Dependency
- @types/express: Express typing for Typescript `<cs>`
- @types/jest: Testing Typing
- @types/node: Typescript Typing
- @types/react: Typescript Typing
@ -71,7 +75,16 @@ If you want to have a Custom server you simply have to start editing `src/server
- @typescript-eslint/parser: ESLint Typescript parser
- babel-jest: Compile files for jest use
- eslint: ESLint
- eslint-config-google: ESLint base configuration
- eslint-import-resolver-typescript:
- eslint-plugin-import: eslint support for imports
- eslint-plugin-react: ESLint React Plugin
- eslint-plugin-sonarjs: eslint support for code quality
- favicons: Favicon generator
- jest: Jest tessting framework
- react-test-renderer: Test React components
- ts-node-dev: Start the developpement server and restart it on changes (remove this dev-deps if you're not using a custom server)
- sitemap: Sitemap generator
- stylelint: Stylesheet linting
- stylelint-config-recommended: Stylelint recommended settings
- stylelint-plugin-stylus: Stylelint plugin for stylus
- ts-node-dev: Start the developpement server and restart it on changes `<cs>`

15
clientConfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"seo": {
"openGraph": {
"type": "website",
"locale": "en_IE",
"url": "https://www.example.com/",
"site_name": "SiteName"
},
"twitter": {
"handle": "@handle",
"site": "@site",
"cardType": "summary_large_image"
}
}
}

View File

@ -1,3 +0,0 @@
const config = {}
export default config

View File

@ -29,7 +29,6 @@ module.exports = withPlugins([
// }
// }],
], {
exportTrailingSlash: true,
plugins: [
["styled-jsx/babel", {
optimizeForSpeed: true,

View File

@ -1,11 +1,11 @@
{
"name": "@avior/next-template",
"version": "1.0.1",
"version": "2.0.0",
"license": "MIT",
"scripts": {
"dev": "next dev",
"cs-dev": "ts-node --project tsconfig.server.json src/server/server.ts --respawn --transpileOnly --prefer-ts --watch next.config.js,.env --ignore-watch .next --ignore-watch src/pages --ignore-watch src/client --ignore-watch scripts src/server/server.ts",
"build": "next build && tsc --project tsconfig.server.json",
"build": "node scripts/generateFavicons && next build && tsc --project tsconfig.server.json",
"cs-server": "NODE_ENV=production node --experimental-modules dist/server/server.js",
"server": "next start",
"export": "next export && node scripts/generateSitemap",
@ -16,38 +16,47 @@
"test": "jest --config jext.config.js"
},
"dependencies": {
"@sentry/browser": "^5.15.5",
"@zeit/next-stylus": "^1.0.1",
"express": "^4.17.1",
"glob": "^7.1.6",
"next": "^9.3.3",
"next-compose-plugins": "^2.2.0",
"next-purgecss": "^4.0.0",
"next-seo": "^4.7.3",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-feather": "^2.0.8",
"serve": "^11.3.0",
"styled-jsx-plugin-stylus": "^0.0.4",
"stylus": "^0.54.7",
"typescript": "^3.7.4",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10"
"webpack": "^4.41.5"
},
"devDependencies": {
"@babel/core": "^7.8.7",
"@babel/preset-env": "^7.8.6",
"@babel/preset-react": "^7.8.3",
"@types/express": "^4.17.6",
"@types/jest": "^25.1.3",
"@types/jest": "^26.0.13",
"@types/node": "^14.0.0",
"@types/react": "^16.9.17",
"@types/react-test-renderer": "^16.9.2",
"@typescript-eslint/eslint-plugin": "^3.0.0",
"@typescript-eslint/parser": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
"babel-jest": "^26.0.0",
"eslint": "^7.1.0",
"eslint-config-google": "^0.14.0",
"eslint-import-resolver-typescript": "^2.3.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-react": "^7.18.3",
"eslint-plugin-sonarjs": "^0.5.0",
"favicons": "^6.2.0",
"jest": "^26.0.0",
"react-test-renderer": "^16.13.0",
"ts-node-dev": "^1.0.0-pre.44"
"sitemap": "^6.3.0",
"stylelint": "^13.7.1",
"stylelint-config-recommended": "^3.0.0",
"stylelint-plugin-stylus": "^0.9.0",
"ts-node-dev": "^1.0.0-pre.44",
"typescript": "^4.0.2"
}
}

62
public/.gitignore vendored Normal file
View File

@ -0,0 +1,62 @@
favicon-16x16.png
favicon-32x32.png
favicon-48x48.png
favicon.ico
android-chrome-36x36.png
android-chrome-48x48.png
android-chrome-72x72.png
android-chrome-96x96.png
android-chrome-144x144.png
android-chrome-192x192.png
android-chrome-256x256.png
android-chrome-384x384.png
android-chrome-512x512.png
apple-touch-icon-57x57.png
apple-touch-icon-60x60.png
apple-touch-icon-72x72.png
apple-touch-icon-76x76.png
apple-touch-icon-114x114.png
apple-touch-icon-120x120.png
apple-touch-icon-144x144.png
apple-touch-icon-152x152.png
apple-touch-icon-167x167.png
apple-touch-icon-180x180.png
apple-touch-icon-1024x1024.png
apple-touch-icon.png
apple-touch-icon-precomposed.png
apple-touch-startup-image-640x1136.png
apple-touch-startup-image-750x1334.png
apple-touch-startup-image-828x1792.png
apple-touch-startup-image-1125x2436.png
apple-touch-startup-image-1242x2208.png
apple-touch-startup-image-1242x2688.png
apple-touch-startup-image-1536x2048.png
apple-touch-startup-image-1668x2224.png
apple-touch-startup-image-1668x2388.png
apple-touch-startup-image-2048x2732.png
apple-touch-startup-image-1136x640.png
apple-touch-startup-image-2160x1620.png
apple-touch-startup-image-1620x2160.png
apple-touch-startup-image-1334x750.png
apple-touch-startup-image-1792x828.png
apple-touch-startup-image-2436x1125.png
apple-touch-startup-image-2208x1242.png
apple-touch-startup-image-2688x1242.png
apple-touch-startup-image-2048x1536.png
apple-touch-startup-image-2224x1668.png
apple-touch-startup-image-2388x1668.png
apple-touch-startup-image-2732x2048.png
coast-228x228.png
firefox_app_60x60.png
firefox_app_128x128.png
firefox_app_512x512.png
mstile-70x70.png
mstile-144x144.png
mstile-150x150.png
mstile-310x150.png
mstile-310x310.png
yandex-browser-50x50.png
manifest.json
manifest.webapp
browserconfig.xml
yandex-browser-manifest.json

3
public/assets/logo.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="150" height="150" viewBox="0 0 150 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M72.8109 101.066L25 128.807L44.2174 84.4441L72.8109 101.066ZM79.7067 24.0024L125 128.807L45.0697 82.3519L70.2925 24.0024C71.8679 22.4526 73.4308 21.8064 74.9806 22.0652C76.556 21.7808 78.1321 22.427 79.7067 24.0024Z" fill="#123456"/>
</svg>

After

Width:  |  Height:  |  Size: 390 B

View File

@ -1,7 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
":preserveSemverRanges"
]
}

View File

@ -0,0 +1,67 @@
const favicons = require('favicons')
const package = require('../package.json')
const config = require('../scriptsConfig.json').favicons
const fs = require('fs')
if (!config.enabled) {
return
}
// image source
const source = config.sourceFile
// Output folder
const outFolder = config.assetsOutput
// Typescript React outpout file
// NOTE: add the file to your .gitignore
const tsxOutput = config.tsxOutput
// Configuration
const configuration = config.config
if (configuration.version === 'package.json') {
configuration.version = package.version
}
console.log('Generating Favicons...')
favicons(source, configuration, (error, response) => {
if (error) {
console.log(error)
return
}
console.log('Saving Files')
let gitignore = ''
for (const file of response.images) {
fs.writeFileSync(`${outFolder}/${file.name}`, file.contents)
gitignore += `${file.name}\n`
}
for (const file of response.files) {
fs.writeFileSync(`${outFolder}/${file.name}`, file.contents)
gitignore += `${file.name}\n`
}
fs.writeFileSync(`${outFolder}/.gitignore`, gitignore)
if (!tsxOutput) {
return
}
var htmlList = response.html.map((el) => ` ${el.replace('>', '/>')}`).join('\n')
fs.writeFileSync(tsxOutput, `import React from 'react'
export default class Favicons extends React.Component {
public render = () => (
<>
${htmlList}
</>
)
}
`)
console.log('Saved!')
})

View File

@ -3,23 +3,44 @@
* for static exports
**/
const config = require('../scriptsConfig.json').sitemap
const glob = require('glob')
const fs = require('fs')
const fs = require('fs/promises')
const { SitemapStream , streamToPromise } = require('sitemap')
const { Readable } = require( 'stream' )
// DOMAIN NAME WITHOUT THE LAST /
const domain = "https://www.avior.me"
if (!config.enabled) {
return
}
console.log('Generating Sitemap...')
const files = glob.sync('./out/**/*.html')
let res = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`
const stream = new SitemapStream(config.config)
for (let file of files) {
file = file.replace("./out", "").replace("index.html", "")
res += `<url><loc>${domain}${file}</loc></url>`
;(async () => {
console.log('Fetching files')
const files = await new Promise((res, rej) => {
glob('./out/**/*.html', (err, results) => {
if (err) {
rej(err)
}
res(results.map(el => ({url: el.replace('./out', '').replace('index.html', '')})))
})
})
console.log('Merging with custom routes')
for (const item of config.customRoutes) {
const index = files.findIndex((val) => val.url === item.url)
if (index !== -1) {
files[index] = item
} else {
files.push(item)
}
}
res += `</urlset>`
fs.writeFileSync('./out/sitemap.xml', res)
console.log('Pasing to sitemap.xml')
const res = await streamToPromise(Readable.from(files).pipe(stream))
console.log('Writing file')
await fs.writeFile(config.outputPath, res)
console.log(res)
process.exit(0)
})()

60
scriptsConfig.json Normal file
View File

@ -0,0 +1,60 @@
{
"sitemap": {
"enabled": false,
"outputPath": "./out/sitemap.xml",
"config": {
"hostname": "https://www.example.com",
"lastmodDateOnly": false,
"xmlns": {
"news": false,
"xhtml": false,
"image": false,
"video": false
}
},
"customRoutes": [
{
"url": "/",
"changefreq": "weekly",
"priority": 1.0
}
]
},
"favicons": {
"enabled": false,
"sourceFile":"./public/assets/logo.svg",
"assetsOutput": "./public/",
"tsxOutput": "./src/client/components/Favicons.tsx",
"config": {
"path": "/",
"appName": "Next Template",
"appShortName": "Next Template",
"appDescription": "Next Template",
"developerName": null,
"developerURL": null,
"dir": "auto",
"lang": "en-US",
"background": "#fff",
"theme_color": "#123456",
"appleStatusBarStyle": "black-translucent",
"display": "standalone",
"orientation": "any",
"scope": "/",
"start_url": "/",
"version": "package.json",
"logging": false,
"pixel_art": false,
"loadManifestWithCredentials": false,
"icons": {
"android": true,
"appleIcon": true,
"appleStartup": true,
"coast": true,
"favicons": true,
"firefox": true,
"windows": true,
"yandex": true
}
}
}
}

View File

@ -0,0 +1,59 @@
import React from 'react'
export default class Favicons extends React.Component {
public render = () => (
<>
<link rel="shortcut icon" href="/favicon.ico"/>
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/>
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png"/>
<link rel="manifest" href="/manifest.json"/>
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="theme-color" content="#123456"/>
<meta name="application-name" content="Next Template"/>
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png"/>
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png"/>
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png"/>
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png"/>
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png"/>
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png"/>
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png"/>
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png"/>
<link rel="apple-touch-icon" sizes="167x167" href="/apple-touch-icon-167x167.png"/>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png"/>
<link rel="apple-touch-icon" sizes="1024x1024" href="/apple-touch-icon-1024x1024.png"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<meta name="apple-mobile-web-app-title" content="Next Template"/>
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/apple-touch-startup-image-640x1136.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/apple-touch-startup-image-750x1334.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/apple-touch-startup-image-828x1792.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/apple-touch-startup-image-1125x2436.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/apple-touch-startup-image-1242x2208.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/apple-touch-startup-image-1242x2688.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/apple-touch-startup-image-1536x2048.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/apple-touch-startup-image-1668x2224.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/apple-touch-startup-image-1668x2388.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/apple-touch-startup-image-2048x2732.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/apple-touch-startup-image-1620x2160.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/apple-touch-startup-image-1136x640.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/apple-touch-startup-image-1334x750.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/apple-touch-startup-image-1792x828.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/apple-touch-startup-image-2436x1125.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/apple-touch-startup-image-2208x1242.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/apple-touch-startup-image-2688x1242.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/apple-touch-startup-image-2048x1536.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/apple-touch-startup-image-2224x1668.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/apple-touch-startup-image-2388x1668.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/apple-touch-startup-image-2732x2048.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/apple-touch-startup-image-2160x1620.png"/>
<link rel="icon" type="image/png" sizes="228x228" href="/coast-228x228.png"/>
<meta name="msapplication-TileColor" content="#fff"/>
<meta name="msapplication-TileImage" content="/mstile-144x144.png"/>
<meta name="msapplication-config" content="/browserconfig.xml"/>
<link rel="yandex-tableau-widget" href="/yandex-browser-manifest.json"/>
</>
)
}

View File

@ -1,20 +0,0 @@
import React from 'react'
interface Props {
children: React.ReactNode
}
export default class HelloWorld extends React.Component<Props> {
public render() {
return (
<>
<h1>{this.props.children}</h1>
<style jsx>{`
h1
font-weight 700
`}</style>
</>
)
}
}

View File

@ -0,0 +1,2 @@
.body
padding 0 16px 16px

View File

@ -0,0 +1,18 @@
import React from 'react'
import css from './BoxBody.module.styl'
import { buildClassName } from '@cp/dzeio/Util'
interface Props {
noPadding?: boolean
}
export default class BoxBody extends React.Component<Props> {
public render = () => (
<div className={buildClassName([css.body, !this.props.noPadding])}>
{this.props.children}
</div>
)
}

View File

@ -0,0 +1,21 @@
@import "../../config.styl"
.header
padding 16px
.delimiter
border-bottom 2px solid grey
padding-bottom 2px
.img
border-top-left-radius 4px
border-top-right-radius 4px
.title
font-weight bold
font-size rem(20)
margin 0 0 8px
.subtitle
font-size rem(14)
margin 0

View File

@ -0,0 +1,55 @@
import React from 'react'
import { buildClassName } from '../../Util'
import css from './BoxHeader.module.styl'
import Row from '@cp/dzeio/Row'
import Col from '@cp/dzeio/Col'
export interface Props {
title?: string
titleColSize?: number
subtitle?: string
delimiter?: boolean
titleClassName?: string
// image?: ImageProps
}
export default class BoxHeader extends React.Component<Props> {
public render = () => (
<>
{/* {this.props.image && (
<Image {...this.props.image} />
)} */}
<div className={buildClassName(
[css.header],
[css.delimiter, this.props.delimiter]
)}>
<Row>
<Col size={this.props.titleColSize as 1 || 8}>
<p className={buildClassName(css.title, this.props.titleClassName)}>{this.props.title}</p>
<p className={css.subtitle}>{this.props.subtitle}</p>
</Col>
<Col>
<Row justify="flex-end">
{this.props.children}
</Row>
</Col>
</Row>
</div>
</>
)
}
/*
Header
delimiter?: boolean
picture?: string // url
category?: string // subtitle but above title
title string
subtitle string
center?: boolean // if Center children is not used
children?: content
*/

View File

@ -0,0 +1,22 @@
@import "../../config.styl"
.box
background white
border-radius 8px
box-shadow 0px 2px 4px 0px rgba(black, .33)
transition all $transition
color black
.outline
border 2px solid #E0E0E0
box-shadow none
transition border-color $transition
&:hover
border-color darken(@border[2], 20%)
@media (prefers-color-scheme dark)
.box
background black
.outline
border-color #1F1F1F

View File

@ -0,0 +1,32 @@
import React from 'react'
import css from './BoxWrapper.module.styl'
import { buildClassName } from '@cp/dzeio/Util'
interface Props extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
outline?: boolean
className?: string
}
export default class BoxWrapper extends React.Component<Props> {
public render = () => (
<div {...this.props}
className={buildClassName(
css.box,
this.props.className,
[css.outline, this.props.outline]
)}
>
{this.props.children}
</div>
)
}
/*
Wrapper extends div
Body
noPadding?: boolean
*/

View File

@ -0,0 +1,10 @@
import BoxWrapper from './BoxWrapper'
import BoxHeader from './BoxHeader'
import BoxBody from './BoxBody'
export {
BoxWrapper,
BoxHeader,
BoxBody
}

View File

@ -0,0 +1,201 @@
@import '../config'
.button
font-size rem(16)
position relative
transition all $transition
font-weight 600
line-height 1.5
display inline-flex
padding 10px 20px
margin 8px 8px 0 8px
cursor pointer
align-items center
text-align center
border-radius 4px
border none
justify-content center
color white
background-color $default
// Chrome Specific
outline none
// Link specific
text-decoration none
&.nomargintop
margin-top 0
&.outline
border 2px solid @background-color
padding 8px 18px // @padding - @border
background transparent
color @background-color
&:hover
&:active
&:focus
color @color
&:hover
background-color @background-color
transform translateY(-2px)
box-shadow 0 4px 4px rgba(@background-color,.2)
&:active
&:focus
background-color darken(@background-color, 30%)
&.large
padding 15px 30px
font-size rem(20)
&.outline
padding 13px 28px // @padding - @border
&.small
padding 5px 10px
font-size rem(14)
&.outline
padding 3px 8px // @padding - @border
&.block
display flex
width 100%
margin 0
margin-top 8px
&:disabled
background #E0E0E0 !important
color #B0B0B0 !important
transform none !important
box-shadow none !important
cursor initial
&.loading
color transparent !important
position relative
pointer-events none
&::after
content ""
display block
border white 2px solid
border-color transparent transparent white white
width 1em
position absolute
top calc(50% - (1em / 2))
left calc(50% - (1em / 2))
border-radius 100%
height 1em
box-sizing inherit
animation ButtonLoading 1s infinite linear
.textInner
margin-left 8px
.primary
background-color $primary
&.outline
color @background-color
border-color @background-color
&:hover
background-color @background-color
&:active
&:focus
background-color darken(@background-color, 30%)
&.loading::after
border-color transparent transparent @background-color @background-color
.secondary
background-color $secondary
color black
&.outline
color @color
border-color @background-color
&:hover
background-color @background-color
&:active
&:focus
background-color darken(@background-color, 30%)
&.loading::after
border-color transparent transparent @color @color
.info
background-color $info
&.outline
color @background-color
border-color @background-color
&:hover
background-color @background-color
&:active
&:focus
background-color darken(@background-color, 30%)
&.loading::after
border-color transparent transparent @background-color @background-color
.success
background-color $success
&.outline
color @background-color
border-color @background-color
&:hover
background-color @background-color
&:active
&:focus
background-color darken(@background-color, 30%)
&.loading::after
border-color transparent transparent @background-color @background-color
.danger
background-color $danger
&.outline
color @background-color
border-color @background-color
&:hover
background-color @background-color
&:active
&:focus
background-color darken(@background-color, 30%)
&.loading::after
border-color transparent transparent @background-color @background-color
.warning
background-color $warning
&.outline
color @background-color
border-color @background-color
&:hover
background-color @background-color
&:active
&:focus
background-color darken(@background-color, 30%)
&.loading::after
border-color transparent transparent @background-color @background-color
@keyframes ButtonLoading
0%
transform rotate(0)
100%
transform rotate(365deg)

View File

@ -0,0 +1,67 @@
import React, { FC } from 'react'
import Link from '../Link'
import { ColorType, IconProps } from '../interfaces'
import { buildClassName } from '../Util'
import Image from '../Image'
import css from './Button.module.styl'
interface Props {
outline?: boolean
nomargintop?: boolean
color?: ColorType
children?: React.ReactNode
icon?: FC<IconProps> | string
size?: 'large' | 'small' | 'block'
href?: string
as?: string
disabled?: boolean
loading?: boolean
onClick?: (event: React.MouseEvent<HTMLButtonElement|HTMLAnchorElement, MouseEvent>) => void
}
export default class Button extends React.Component<Props> {
public render = () => {
let inner: any = this.props.children
if (!this.props.loading && this.props.icon) {
const Icon = this.props.icon
inner = (
<>
{typeof Icon === 'string' ? (
<Image src={Icon} max={{ height: 16, width: 16 }} />
) : (
<Icon size={16} />
)}
{this.props.children && (
<span className={css.textInner}>{this.props.children}</span>
)}
</>
)
}
const classes = buildClassName(
[css.button],
[css[this.props.color as string], this.props.color],
[css.outline, this.props.outline],
[css[this.props.size as string], this.props.size],
[css.nomargintop, this.props.nomargintop],
[css.loading, this.props.loading]
)
if (this.props.href) {
return (
<Link href={this.props.href} as={this.props.as} className={buildClassName([classes], [css.disabled, this.props.disabled])}>
{inner}
</Link>
)
}
return (
<button onClick={this.props.onClick} disabled={this.props.disabled} className={classes}>{inner}</button>
)
}
}

View File

@ -0,0 +1,190 @@
@import "../config.styl"
$backColor = #757575
.label
position relative
display flex
padding-left 8px
margin 8px
user-select none
span
top 0
left 0
width 20px
height @width
position absolute
box-shadow inset 0 0 0 2px $backColor
border-radius 2px
transition all $transition
&::after
border-radius 20px
position absolute
transition all $transition
background $default
svg
transition $transition
transform scale(0)
color transparent
margin 2px
input
visibility hidden
width 20px
height @width
margin 0
&:checked + span
background rgba($default, .5)
box-shadow inset 0 0 0 2px $default
svg
color white
transform scale(1)
&:hover
span
box-shadow inset 0 0 0 2px black
.radio
margin-left 18px // Margin + margin for Circle
span
border-radius 20px
&::after
content " "
top 5px
left 5px
width 10px
height @width
transform scale(0)
input:checked + span::after
transform scale(1)
background white
.switch
padding 2px 0 2px 10px // 2px base padding 10px circle padding
&:hover span
box-shadow none
&::after
background black
span
width 28px
height 14px
border-radius 20px
top 50%
box-shadow none
background rgba($backColor, .5)
transform translateY(-50%)
&::after
content " "
top 50%
transform translate(-50%, -50%)
left 0
background $backColor
width 20px
height @width
input
margin 0 8px
width 20px
&:checked + span
box-shadow none
&::after
left 100%
transform translate(-50%, -50%)
background $default
.primary
$color = $primary
input:checked + span
background rgba($color, .5)
box-shadow inset 0 0 0 2px $color
&::after
background $color
&.switch
input:checked + span
box-shadow none
.secondary
$color = $secondary
input:checked + span
background rgba($color, .5)
box-shadow inset 0 0 0 2px $color
&::after
background $color
&.switch
input:checked + span
box-shadow none
.info
$color = $info
input:checked + span
background rgba($color, .5)
box-shadow inset 0 0 0 2px $color
&::after
background $color
&.switch
input:checked + span
box-shadow none
.success
$color = $success
input:checked + span
background rgba($color, .5)
box-shadow inset 0 0 0 2px $color
&::after
background $color
&.switch
input:checked + span
box-shadow none
.danger
$color = $danger
input:checked + span
background rgba($color, .5)
box-shadow inset 0 0 0 2px $color
&::after
background $color
&.switch
input:checked + span
box-shadow none
.warning
$color = $warning
input:checked + span
background rgba($color, .5)
box-shadow inset 0 0 0 2px $color
&::after
background $color
&.switch
input:checked + span
box-shadow none

View File

@ -0,0 +1,50 @@
import React from 'react'
import { Check } from 'react-feather'
import { buildClassName } from '../Util'
import { ColorType } from '../interfaces'
import css from './Checkbox.module.styl'
interface Props extends React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> {
label?: string
id: string
type?: undefined
radio?: boolean
switch?: boolean
color?: ColorType
}
export default class Checkbox extends React.Component<Props> {
public render() {
const props: Props = Object.assign({}, this.props)
delete props.label
delete props.type
delete props.color
delete props.switch
delete props.radio
const realType = this.props.radio ? 'radio' : 'checkbox'
return (
<label htmlFor={this.props.id} className={buildClassName(
[css.label],
[css.radio, realType === 'radio'],
[css.switch, this.props.switch],
[css[this.props.color as string], this.props.color]
)}>
<input {...props}
type={realType}
/>
<span>
{realType === 'checkbox' && ! this.props.switch && (
<Check strokeWidth={4} size={16}/>
)}
</span>
{this.props.label}
</label>
)
}
}

View File

@ -0,0 +1,5 @@
.code
font-family 'source code pro', monospace
background #E8EAF6
padding 4px 8px
border-radius 8px

View File

@ -0,0 +1,28 @@
import React from 'react'
import css from './Code.module.styl'
interface Props {
block?: boolean
}
export default class Code extends React.Component<Props> {
public render = () => {
const code = (
<code className={css.code}>
{this.props.children}
</code>
)
if (!this.props.block) {
return code
}
return (
<pre>
{code}
</pre>
)
}
}

View File

@ -0,0 +1,48 @@
@import "../config"
.col
max-width 100%
flex-basis 0
flex-grow 1
padding $gapSize 0 0 $gapSize
&.nogrow
max-width intial
flex-grow 0
flex-basis initial
for i in (1...$colCount+1)
.col-{i}
flex 0 0 ((i / 12) * 100)%
min-width ((i / 12) * 100)%
if i != $colCount
.offset-{i}
margin-left ((i / 12) * 100)% + (i * $gapSize + $gapSize)
@media (max-width $tablet)
.col.tabletGrow
flex-grow 1
for i in (1...$colCountTablet+1)
.col-tablet-{i}
flex 0 0 ((i / $colCountTablet) * 100)%
min-width ((i / $colCountTablet) * 100)%
if i != $colCountTablet
.offset-tablet-{i}
margin-left ((i / $colCountTablet) * 100)% + (i * $gapSize - $gapSize)
@media (max-width $mobile)
.col.mobileGrow
flex-grow 1
for i in (1...$colCountMobile+1)
.col-mobile-{i}
flex 0 0 ((i / $colCountMobile) * 100)%
min-width ((i / $colCountMobile) * 100)%
if i != $colCountMobile
.offset-tablet-{i}
margin-left ((i / $colCountMobile) * 100)% + (i * $gapSize - $gapSize)

View File

@ -0,0 +1,12 @@
.row
position fixed
top 0
left 0
height 100%
width 100%
z-index 10000
pointer-events none
.col > div
background rgba(red, .2)
height 100%

View File

@ -0,0 +1,72 @@
import React from 'react'
import Row from '../Row'
import css from './DebugCols.module.styl'
import Col from '.'
enum Breakpoint {
MOBILE,
TABLET,
COMPUT
}
interface States {
breakpoint: Breakpoint
}
export default class DebugCols extends React.Component<unknown, States> {
public state: States = {
breakpoint: Breakpoint.COMPUT
}
public componentDidMount() {
this.onResize()
window.addEventListener('resize', this.onResize)
}
public componentWillUnmount() {
window.removeEventListener('resize', this.onResize)
}
public render = () => (
<Row className={css.row}>
<Col size={1} tabletSize={1} mobileSize={1} className={css.col}><div></div></Col>
<Col size={1} tabletSize={1} mobileSize={1} className={css.col}><div></div></Col>
<Col size={1} tabletSize={1} mobileSize={1} className={css.col}><div></div></Col>
<Col size={1} tabletSize={1} mobileSize={1} className={css.col}><div></div></Col>
{this.state.breakpoint !== Breakpoint.MOBILE && (
<>
{this.state.breakpoint !== Breakpoint.TABLET && (
<>
<Col size={1} className={css.col}><div></div></Col>
<Col size={1} className={css.col}><div></div></Col>
<Col size={1} className={css.col}><div></div></Col>
<Col size={1} className={css.col}><div></div></Col>
</>
)}
<Col size={1} tabletSize={1} className={css.col}><div></div></Col>
<Col size={1} tabletSize={1} className={css.col}><div></div></Col>
<Col size={1} tabletSize={1} className={css.col}><div></div></Col>
<Col size={1} tabletSize={1} className={css.col}><div></div></Col>
</>
)}
</Row>
)
private onResize = async () => {
const currentBreakpoint =
window.innerWidth <= 768 ?
Breakpoint.MOBILE :
window.innerWidth <= 1200 ?
Breakpoint.TABLET :
Breakpoint.COMPUT
const hasChanged = currentBreakpoint !== this.state.breakpoint
if (hasChanged) {
this.setState({ breakpoint: currentBreakpoint })
}
}
}

View File

@ -0,0 +1,49 @@
import React from 'react'
import { buildClassName } from '../Util'
import css from './Col.module.styl'
interface Props {
size?: 1|2|3|4|5|6|7|8|9|10|11|12
offset?: 1|2|3|4|5|6|7|8|9|10|11
children?: React.ReactNode
className?: string
nogrow?: boolean
// Tablet related
tabletSize?: 1|2|3|4|5|6|7|8
tabletoffset?: 1|2|3|4|5|6|7
tabletGrow?: boolean
// Mobile related
mobileSize?: 1|2|3|4
mobileoffset?: 1|2|3
mobileGrow?: boolean
}
export default class Col extends React.Component<Props> {
public render = () => (
<div className={buildClassName(
css.col,
// Normal
[css[`col-${this.props.size}`], this.props.size],
[css[`offset-${this.props.offset}`], this.props.offset],
// Tablet
[css[`col-tablet-${this.props.tabletSize}`], this.props.tabletSize],
[css[`offset-tablet-${this.props.tabletoffset}`], this.props.tabletoffset],
// Mobile
[css[`col-mobile-${this.props.mobileSize}`], this.props.mobileSize],
[css[`offset-mobile-${this.props.mobileoffset}`], this.props.mobileoffset],
[css.nogrow, this.props.nogrow],
[css.mobileGrow, this.props.mobileGrow],
this.props.className
)}>
{this.props.children}
</div>
)
}

View File

@ -0,0 +1,2 @@
.container
padding 16px

View File

@ -0,0 +1,19 @@
import React from 'react'
import { buildClassName } from '../Util'
import css from './Container.module.styl'
interface Props {
children: React.ReactNode
className?: string
}
export default class Container extends React.Component<Props> {
public render = () => (
<div className={buildClassName(css.container, this.props.className)}>
{this.props.children}
</div>
)
}

View File

@ -0,0 +1,18 @@
@import '../config.styl'
.fieldset
border-radius 8px
border 2px solid grey
transition all $transition
margin 0
> legend
font-weight 600
transition all $transition
padding 0 4px
&:hover
border-color black
> legend
color $default

View File

@ -0,0 +1,21 @@
import React from 'react'
import css from './Fieldset.module.styl'
interface Props {
title?: string
children?: React.ReactNode
}
export default class Fieldset extends React.Component<Props> {
public render = () => (
<fieldset className={css.fieldset}>
{this.props.title && (
<legend>{this.props.title}</legend>
)}
{this.props.children}
</fieldset>
)
}

View File

@ -0,0 +1,32 @@
@import "../config"
$transparent = 75%
.back
transition all $transition
color white
background linear-gradient(to left, $default, transparentify($default, $transparent))
.primary
$color = $primary
background linear-gradient(to left, $color, transparentify($color, $transparent))
.secondary
$color = $secondary
background linear-gradient(to left, $color, transparentify($color, $transparent))
.info
$color = $info
background linear-gradient(to left, $color, transparentify($color, $transparent))
.success
$color = $success
background linear-gradient(to left, $color, transparentify($color, $transparent))
.danger
$color = $danger
background linear-gradient(to left, $color, transparentify($color, $transparent))
.warning
$color = $warning
background linear-gradient(to left, $color, transparentify($color, $transparent))

View File

@ -0,0 +1,21 @@
import React from 'react'
import { ColorType } from '../interfaces'
import { buildClassName } from '../Util'
import css from './GradientBackground.module.styl'
interface Props {
color?: ColorType
className?: string
children: React.ReactNode
}
export default class GradientBackground extends React.Component<Props> {
public render = () => (
<div className={buildClassName([css.back], [css[this.props.color as string], this.props.color], [this.props.className])}>
{this.props.children}
</div>
)
}

View File

@ -2,8 +2,7 @@
transition .3s
object-fit contain
z-index 2
transition .3s
max-width 100%
&.ph1
position fixed
@ -25,10 +24,10 @@
&.after
background #00000000
/* height 100% */
// height 100%
box-sizing border-box
position fixed
/* width 100% */
// width 100%
z-index 0
padding initial

View File

@ -1,7 +1,9 @@
import React, { SyntheticEvent } from 'react'
import css from '@smd/Image.module.styl'
import React from 'react'
import { buildClassName } from '../Util'
interface Props {
import css from './Image.module.styl'
export interface ImageProps {
defaultHeight?: number
src?: string
sources?: Array<string>
@ -18,6 +20,15 @@ interface Props {
width?: number|string
}
alt?: string
classes?: string
className?: string
onClick?: () => void
}
enum images {
JPEG = 'image/jpeg',
XICON = 'image/x-icon',
TIFF = 'image/tiff'
}
const mimeTypes = {
@ -25,22 +36,22 @@ const mimeTypes = {
bmp: 'image/bmp',
gif: 'image/gif',
ico: 'image/x-icon',
cur: 'image/x-icon',
ico: images.XICON,
cur: images.XICON,
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
jfif: 'image/jpeg',
pjpeg: 'image/jpeg',
pjp: 'image/jpeg',
jpg: images.JPEG,
jpeg: images.JPEG,
jfif: images.JPEG,
pjpeg: images.JPEG,
pjp: images.JPEG,
png: 'image/png',
svg: 'image/svg+xml',
tif: 'image/tiff',
tiff: 'image/tiff',
tif: images.TIFF,
tiff: images.TIFF,
webp: 'image/webp',
webp: 'image/webp'
}
const getMimeType = (img: string) => {
@ -48,9 +59,9 @@ const getMimeType = (img: string) => {
return mimeTypes[arr[arr.length-1] as 'apng'] || mimeTypes.png
}
type evType<T = HTMLImageElement> = SyntheticEvent<T, Event>
type evType<T = HTMLImageElement> = React.SyntheticEvent<T, Event>
export default class Image extends React.Component<Props> {
export default class Image extends React.Component<ImageProps> {
private ref: React.RefObject<HTMLImageElement> = React.createRef()
private plchldr: React.RefObject<HTMLDivElement> = React.createRef()
@ -78,7 +89,9 @@ export default class Image extends React.Component<Props> {
this.onScroll()
this.onResize()
}
if (this.isFullscreen) {this.onClick()}
if (this.isFullscreen) {
this.onClick()
}
}
public async componentWillUnmount() {
@ -90,33 +103,35 @@ export default class Image extends React.Component<Props> {
public render() {
const pic = (
<picture ref={this.pic}>
<picture ref={this.pic} className={this.props.classes}>
{this.props.sources && this.props.sources.map((el, index) => (
<source key={index} srcSet={el} type={getMimeType(el)}/>
))}
<img
className={css.image}
className={buildClassName([css.image], [this.props.className])}
ref={this.ref}
src={this.props.src}
onClick={this.props.canFullscreen && this.onClick || undefined}
onClick={this.props.canFullscreen && this.onClick || this.props.onClick}
onLoad={this.props.default && this.onLoad || undefined}
onError={this.props.deleteOnError && this.onError || undefined}
style={{
width: this.props.default?.width,
height: this.props.default?.height,
maxHeight: this.props.max?.height,
maxWidth: this.props.max?.width,
maxWidth: this.props.max?.width
}}
alt={this.props.alt}
/>
</picture>
)
if (this.props.canFullscreen) {return (
if (this.props.canFullscreen) {
return (
<div ref={this.parent}>
<div ref={this.plchldr} className={css.none}></div>
{pic}
</div>
)}
)
}
return pic
}
@ -146,6 +161,9 @@ export default class Image extends React.Component<Props> {
if (!this.ref.current || !this.props.canFullscreen || !this.plchldr.current) {
return
}
if (this.props.onClick) {
this.props.onClick()
}
const i = this.ref.current
const c = this.plchldr.current
@ -161,7 +179,9 @@ export default class Image extends React.Component<Props> {
i.classList.add(css.after)
setTimeout(() => {
if (i.classList.contains(css.ph2) || i.classList.contains(css.ph1) || this.isFullscreen) {return}
if (i.classList.contains(css.ph2) || i.classList.contains(css.ph1) || this.isFullscreen) {
return
}
const w = this.valToPixel(this.props.width)
const mh = this.valToPixel(this.props.max?.height)
const mw = this.valToPixel(this.props.max?.width)
@ -206,4 +226,5 @@ export default class Image extends React.Component<Props> {
private w(...messages: any) {
console.warn('[ Picture ]', ...messages)
}
}

View File

@ -0,0 +1,165 @@
@import '../config'
.parent
position relative
margin 16px
label
font-size 1rem
font-weight 600
color black
display inline-block
margin-bottom .5rem
transition all $transition
position absolute
top 16px
pointer-events none
left 16px
svg
position absolute
color #AAA
top 14px
left 16px // input padding-left
transition color $transition
~ label
left 16px + 24px + 16px
select
appearance none
input
select
padding 14px 16px
max-width 100%
height 56px
border 2px solid rgba(black, .3)
border-radius 4px
box-sizing border-box
font-size .875rem
outline none
background transparent
transition all $transition
color black
&:hover
border-color black
+ svg
color black
&:not(:placeholder-shown)
&:focus
&:not([placeholder=" "])
~ label
top -8px
left 16px - 4px // .input/padding-left label/padding-left
background white
padding 0 4px
&:invalid
border-color $danger
~ label
color @border-color
~ svg
color @border-color
&:focus
border-color $default
~ label
color @border-color
~ svg
color @border-color
&.primary
border-color $primary
~ label
color @border-color
~ svg
color @border-color
&.secondary
border-color $secondary
~ label
color @border-color
~ svg
color @border-color
&.info
border-color $info
~ label
color @border-color
~ svg
color @border-color
&.success
border-color $success
~ label
color @border-color
~ svg
color @border-color
&.danger
border-color $danger
~ label
color @border-color
~ svg
color @border-color
&.warning
border-color $warning
~ label
color @border-color
~ svg
color @border-color
&.hasIcon
padding-left 16px + 24px + 16px
&.filled
border none
background rgba(gray, .1)
border-radius @border-radius @border-radius 0 0
border-bottom 2px solid rgba(black,.4)
&:hover
background rgba(gray, .2)
&:focus
background rgba(gray, .3)
&:not(:placeholder-shown)
&:focus
&:not([placeholder=" "])
~ label
top 3px
left 16px - 4px // .input/padding-left label/padding-left
background transparent
padding 0
font-size .75rem
~ svg ~ label
left 16px + 24px + 16px // .input/padding-left label/padding-left
div
display flex
justify-content space-between
padding 0 16px
font-size .9em
&.block input
width 100%

View File

@ -0,0 +1,117 @@
import React, { FC } from 'react'
import { ChevronDown } from 'react-feather'
import { IconProps, ColorType } from '../interfaces'
import { buildClassName } from '../Util'
import css from './Input.module.styl'
interface Props extends React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> {
id?: string
label?: string
icon?: FC<IconProps>
helper?: string
characterCount?: boolean
inputRef?: React.RefObject<HTMLInputElement>
selectRef?: React.RefObject<HTMLSelectElement>
type?: 'color' | 'text' | 'date' | 'datetime-local' |
'email' | 'file' | 'month' | 'number' | 'password' |
'range' | 'search' | 'tel' | 'time' | 'url' | 'week' | 'select'
maxLength?: number | undefined
infinityText?: string
filled?: boolean
block?: boolean
color?: ColorType
children?: React.ReactNode
}
export default class Input extends React.Component<Props> {
private charCountRef: React.RefObject<HTMLSpanElement> = React.createRef()
public componentDidMount() {
this.updatecharCount()
}
public render() {
const props: Props = Object.assign({}, this.props)
const Icon = this.props.icon
delete props.label
delete props.icon
delete props.helper
delete props.infinityText
delete props.filled
delete props.inputRef
delete props.selectRef
delete props.block
delete props.color
delete props.characterCount
return (
<div
className={buildClassName(
[css.parent],
[css.block, this.props.block]
)}
onChangeCapture={this.props.characterCount ? this.updatecharCount : undefined}
>
{this.props.type === 'select' ? (
<select
ref={this.props.selectRef}
className={buildClassName(
css.hasIcon,
[css.filled, this.props.filled],
[css[this.props.color as string], this.props.color]
)}
>
{this.props.children}
</select>
) : (
<input
placeholder=" "
ref={this.props.inputRef}
{...props}
className={buildClassName(
[css.hasIcon, Icon],
[css.filled, this.props.filled],
[css[this.props.color as string], this.props.color]
)}
onInvalid={(ev) => ev.preventDefault()}
/>
)}
{this.props.type === 'select' && (
<ChevronDown />
)}
{Icon && (
<Icon />
)}
{this.props.label && (
<label className={css.label} htmlFor={this.props.id}>{this.props.label}</label>
)}
{(this.props.helper || this.props.characterCount) && (
<div>
<span>{this.props.helper}</span>
<span ref={this.charCountRef}></span>
</div>
)}
</div>
)
}
private updatecharCount = async (event?: React.FormEvent<HTMLDivElement>) => {
if (this.props.characterCount && this.charCountRef.current) {
const max = this.props.maxLength || this.props.infinityText || 'Infinity'
let currentCount = 0
if (event) {
currentCount = (event.target as HTMLInputElement).value.length
} else {
if (this.props.defaultValue) {
currentCount = this.props.defaultValue.toString().length
} else if (this.props.value) {
currentCount = this.props.value.toString().length
}
}
this.charCountRef.current.innerText = currentCount + ' / ' + max
}
}
}

View File

@ -0,0 +1,3 @@
.icon
vertical-align sub
margin 2px

View File

@ -0,0 +1,40 @@
import React from 'react'
import NextLink from 'next/link'
import { ExternalLink } from 'react-feather'
import css from './Link.module.styl'
interface Props {
href: string
as?: string
children?: React.ReactNode
className?: string
}
export default class Link extends React.Component<Props> {
public render() {
if (
this.props.href.includes('://') ||
this.props.href.startsWith('//')
) {
// external link
return (
<a
className={this.props.className}
href={this.props.href}
rel="noreferrer nofollow"
target="_blank"
>
{this.props.children}<ExternalLink size={16} className={css.icon} />
</a>
)
}
return (
<NextLink href={this.props.href} as={this.props.as}>
<a className={this.props.className}>{this.props.children}</a>
</NextLink>
)
}
}

View File

@ -0,0 +1,19 @@
@import '../config'
.menu
position absolute
opacity 0
background white
pointer-events none
border-radius 4px
box-shadow 0 2px 4px 2px rgba(black, 25%)
z-index 99
transition opacity $transition
a
display block
text-align center
padding 16px
&.shown
opacity 1
pointer-events initial

View File

@ -0,0 +1,25 @@
import React from 'react'
import Link from 'next/link'
import { buildClassName } from '../Util'
import css from './Menu.module.styl'
interface Props {
pos?: {top?: number, bottom?: number, left?: number, right?: number}
content: Array<{name: string, href: string, as?: string}>
show?: boolean
}
export default class Menu extends React.Component<Props> {
public render = () => (
<div className={buildClassName([css.menu], [css.shown, this.props.show])} style={this.props.pos}>
{this.props.content.map((item, index) => (
<Link key={index} href={item.href} as={item.as}>
<a>{item.name}</a>
</Link>
))}
</div>
)
}

View File

@ -0,0 +1,52 @@
@import "../config"
.navbar
width 100%
height 70px
padding 8px 0
position absolute
top 0
left 0
border-bottom 1px solid white
> div
height 100%
&.small
padding-left 216px
.alignRight
text-align right
.favicon img
height 38px
border-radius 8px
border 2px solid white
padding 2px
background white
width 38px
.userIcon img
padding 0
cursor pointer
.text
display inline-block
margin 0
height 38px
line-height 1
font-weight bold
font-size rem(20)
padding 7px 0 11px 16px
text-decoration none
color white
cursor pointer
.spacer
height 70px
.icon
padding 7px 16px
color white
box-sizing content-box
cursor pointer

View File

@ -0,0 +1,11 @@
import React from 'react'
import css from './Navbar.module.styl'
export default class NavbarSpace extends React.Component {
public render = () => (
<div className={css.spacer}></div>
)
}

View File

@ -0,0 +1,47 @@
import React from 'react'
import Link from 'next/link'
import Row from '../Row'
import Col from '../Col'
import Button from '../Button'
import Image from '../Image'
import { buildClassName } from '../Util'
import css from './Navbar.module.styl'
interface Props {
links: Array<{href: string, txt: string}>
}
export default class Navbar extends React.Component<Props> {
public render = () => (
<nav className={buildClassName(css.navbar)}>
<Row>
<Col size={3} mobileSize={4}>
<Row nomargin align="center">
<Link href="/">
<a aria-label="Homepage">
<Image
alt="Logo"
src="/assets/logo.svg"
max={{ height: 70-16 }}
/>
</a>
</Link>
</Row>
</Col>
<Col>
<Row nomargin justify="flex-end" align="center">
<>
{this.props.links.map((el, index) => (
<Button key={index} nomargintop href={el.href}>{el.txt}</Button>
))}
</>
</Row>
</Col>
</Row>
</nav>
)
}

View File

@ -0,0 +1,6 @@
$height = 45px
.top
height $height
.bottom
margin-top - $height

View File

@ -0,0 +1,20 @@
import React from 'react'
import { buildClassName } from '../Util'
import css from './Overflow.module.styl'
interface Props {
bottom?: boolean
top?: boolean
}
export default class Overflow extends React.Component<Props> {
public render = () => (
<div className={buildClassName(
[css.bottom, this.props.bottom],
[css.top, this.props.top]
)}></div>
)
}

View File

@ -0,0 +1,14 @@
.popup
position fixed
height 100%
width 100%
top 0
left 0
background rgba(black, .3)
cursor pointer
.popupChild
cursor initial
.exit
cursor pointer

View File

@ -0,0 +1,36 @@
import React from 'react'
import { X } from 'react-feather'
import { BoxWrapper, BoxHeader, BoxBody } from '../Box'
import { Props as HeaderProps } from '../Box/BoxHeader'
import Row from '../Row'
import css from './Popup.module.styl'
interface Props {
children: React.ReactNode
onClose?: () => void
header?: HeaderProps
}
export default class Popup extends React.Component<Props> {
public render = () => (
<Row onClick={this.parentClose} justify="center" align="center" className={css.popup}>
<BoxWrapper className={css.popupChild}>
<BoxHeader {...this.props.header}>
<X onClick={this.props.onClose} className={css.exit} />
</BoxHeader>
<BoxBody>
{this.props.children}
</BoxBody>
</BoxWrapper>
</Row>
)
private parentClose = (ev: React.MouseEvent<HTMLDivElement>) => {
if ((ev.target as HTMLElement).classList.contains(css.popup) && this.props.onClose) {
this.props.onClose()
}
}
}

View File

@ -0,0 +1,36 @@
@import "../config"
.row
display flex
flex-wrap wrap
&:not(.nomargin)
margin (0 - $gapSize) 0 0
padding 0 $gapSize * 2 0 $gapSize
.row:not(.nomargin)
padding 0
margin (0 - $gapSize) 0 0 (0 - $gapSize)
.nowrap
flex-wrap nowrap
.nogrow > *
flex-grow 0
for dir in 'row-reverse' 'column' 'column-reverse'
.direction-{dir}
flex-direction unquote(dir)
@media (max-width: $mobile)
for dir in 'row-reverse' 'column' 'column-reverse'
.direction-mobile-{dir}
flex-direction unquote(dir)
for just in 'flex-start' 'center' 'flex-end' 'space-between' 'space-around' 'space-evenly'
.justify-{just}
justify-content unquote(just)
for align in 'flex-start' 'center' 'flex-end' 'baseline'
.align-{align}
align-items unquote(align)

View File

@ -0,0 +1,40 @@
import React from 'react'
import { buildClassName } from '../Util'
import css from './Row.module.styl'
interface Props {
children?: React.ReactNode
direction?: 'row-reverse' | 'column' | 'column-reverse'
mobileDirection?: 'row-reverse' | 'column' | 'column-reverse'
justify?: 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly'
align?: 'flex-start' | 'center' | 'flex-end' | 'baseline'
nowrap?: boolean
nogrow?: boolean
className?: string
nomargin?: boolean
onClick?: (ev: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
}
export default class Row extends React.Component<Props> {
public render = () => (
<div
className={buildClassName(
css.row,
[css[`direction-${this.props.direction}`], this.props.direction],
[css[`direction-mobile-${this.props.mobileDirection}`], this.props.mobileDirection],
[css[`justify-${this.props.justify}`], this.props.justify],
[css[`align-${this.props.align}`], this.props.align],
[css.nowrap, this.props.nowrap],
[css.nogrow, this.props.nogrow],
this.props.className,
[css.nomargin, this.props.nomargin]
)}
onClick={this.props.onClick}
>
{this.props.children}
</div>
)
}

View File

@ -0,0 +1,13 @@
.table
border-spacing 0
border 2px solid #EEE
border-radius 4px
width 100%
td
border-top 1px solid #EEE
th
td
padding 8px
text-align left

View File

@ -0,0 +1,15 @@
import React from 'react'
import css from './Table.module.styl'
interface Props {
children: React.ReactNode
}
export default class Table extends React.Component<Props> {
public render = () => (
<table className={css.table}>{this.props.children}</table>
)
}

View File

@ -0,0 +1,137 @@
@import '../config'
.tag
padding 8px 12px
border-radius 8px
margin-left 8px
height 32px
line-height 1
background #EEE
outline none
&:hover
background darken(#EEE, 10%)
&:focus
background darken(#EEE, 20%)
.outline
border 2px solid #AAA
padding 6px 10px
background transparent
&:hover
background rgba(#AAA, .5)
&:focus
background rgba(#AAA, .7)
.primary
$color = $primary
color white
background $color
&:hover
background lighten($color, 30%)
&:focus
background lighten($color, 15%)
&.outline
color black
border 2px solid $color
background transparent
&:hover
color white
background rgba($color, .5)
&:focus
color white
background rgba($color, .7)
.secondary
$color = $secondary
background $color
&:hover
background lighten($color, 30%)
&:focus
background lighten($color, 15%)
&.outline
color white
border 2px solid $color
background transparent
&:hover
color black
background rgba($color, .5)
&:focus
color black
background rgba($color, .7)
.info
$color = $info
color white
background $color
&:hover
background lighten($color, 30%)
&:focus
background lighten($color, 15%)
&.outline
color black
border 2px solid $color
background transparent
&:hover
color white
background rgba($color, .5)
&:focus
color white
background rgba($color, .7)
.success
$color = $success
color white
background $color
&:hover
background lighten($color, 30%)
&:focus
background lighten($color, 15%)
&.outline
color black
border 2px solid $color
background transparent
&:hover
color white
background rgba($color, .5)
&:focus
color white
background rgba($color, .7)
.danger
$color = $danger
color white
background $color
&:hover
background lighten($color, 30%)
&:focus
background lighten($color, 15%)
&.outline
color black
border 2px solid $color
background transparent
&:hover
color white
background rgba($color, .5)
&:focus
color white
background rgba($color, .7)
.warning
$color = $warning
color white
background $color
&:hover
background lighten($color, 30%)
&:focus
background lighten($color, 15%)
&.outline
color black
border 2px solid $color
background transparent
&:hover
color white
background rgba($color, .5)
&:focus
color white
background rgba($color, .7)

View File

@ -0,0 +1,33 @@
import React from 'react'
import { ColorType } from '../interfaces'
import { buildClassName } from '../Util'
import Link from '../Link'
import css from './Tag.module.styl'
interface Props {
text: string
color?: ColorType
href: string
as?: string
outline?: boolean
}
export default class Tag extends React.Component<Props> {
public render = () => (
<Link
href={this.props.href}
as={this.props.as}
className={buildClassName(
css.tag,
[css[this.props.color as string], this.props.color],
[css.outline, this.props.outline]
)}
>
{this.props.text}
</Link>
)
}

View File

@ -0,0 +1,36 @@
export function buildClassName(...classes: Array<Array<string | boolean | undefined> | string | undefined>): string | undefined {
const classesFinal: Array<string> = []
root: for (const classe of classes) {
if (typeof classe === 'undefined') {
continue
}
if (typeof classe === 'string') {
classesFinal.push(classe)
continue
}
const classToPut = classe.shift()
if (typeof classToPut === 'undefined') {
continue
}
for (const iterator of classe) {
if (!iterator) {
continue root
}
}
classesFinal.push(classToPut + '')
}
if (classesFinal.length === 0) {
return undefined
}
return classesFinal.join(' ')
}
export const colors = {
default: '#3949AB', // This color should never appear
primary: '#3949AB',
secondary: '#FCFCFC',
info: '#03A9F4',
success: '#2DCE89',
danger: '#F5365C',
warning: '#FB6340'
}

View File

@ -0,0 +1,24 @@
$default = #3700B3 // This color should never appear
$primary = #3700B3
$secondary = #FFF
$info = #0000FF
$success = #00FF00
$danger = #FF0000
$warning = #FFFF00
$transitionTime = .15s
$transitionFunction = ease-in-out
$transition = $transitionTime $transitionFunction
$mobile = 768px
$tablet = 1200px
$totalGapSize = 10%
$colCount = 12
$colCountTablet = 8
$colCountMobile = 4
$gapSize = 16px //$totalGapSize / ($colCount+1)
rem($a)
($a / 16)rem

View File

@ -0,0 +1,13 @@
*
*::before
*::after
box-sizing border-box
html
body
body > div
margin 0
height 100%
a
color inherit
text-decoration none

View File

@ -0,0 +1,8 @@
import { SVGAttributes } from 'react'
export type ColorType = 'primary' | 'secondary' | 'info' | 'success' | 'danger' | 'warning'
export interface IconProps extends SVGAttributes<SVGElement> {
color?: string
size?: string | number
}

View File

@ -1,2 +1,14 @@
h1
font-weight 600
// General Styling
html
body
margin 0
a
color inherit
text-decoration none
*
*::before
*::after
box-sizing border-box

View File

@ -8,4 +8,5 @@ export default class E404 extends React.Component {
<Error statusCode={404} />
)
}
}

View File

@ -1,50 +1,24 @@
import App from 'next/app'
import Head from 'next/head'
import React from 'react'
import * as Sentry from '@sentry/browser'
import ErrorPage from './_error'
import { DefaultSeo } from 'next-seo'
import config from '../../clientConfig.json'
import '@styl/index.styl'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN
})
interface States {
errorId?: string
}
export default class CApp extends App<Record<string, unknown>, Record<string, unknown>, States> {
public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.log('catching error', error, errorInfo)
Sentry.withScope((scope) => {
Object.keys(errorInfo).forEach((key) => {
scope.setExtra(key, (errorInfo as any)[key])
})
const id = Sentry.captureException(error)
console.log(id)
this.setState({
errorId: id
})
})
}
export default class CApp extends App {
public render() {
const { Component, pageProps } = this.props
return (
<>
<Head>
<title>Next Template Hello World !</title>
</Head>
{this.state && this.state.errorId && (
<ErrorPage statusCode={500} eventId={this.state.errorId} />
) || (
<DefaultSeo
{...config.seo}
/>
<Component {...pageProps} />
)}
</>
)
}
}

View File

@ -1,20 +1,16 @@
import React from 'react'
import Document, { Html, Head, Main, NextScript } from 'next/document'
import * as Sentry from '@sentry/browser'
process.on('unhandledRejection', (err) => {
Sentry.captureException(err)
})
process.on('uncaughtException', (err) => {
Sentry.captureException(err)
})
import Favicons from '@cp/Favicons'
export default class CDocument extends Document {
public render() {
return (
<Html>
<Head />
<Head>
<Favicons />
</Head>
<body>
<Main />
<NextScript />
@ -22,4 +18,5 @@ export default class CDocument extends Document {
</Html>
)
}
}

View File

@ -1,11 +1,9 @@
import React from 'react'
import { NextPageContext } from 'next'
import * as Sentry from '@sentry/browser'
interface Props {
statusCode: number
eventId?: string
}
export default class Error extends React.Component<Props> {
@ -22,10 +20,8 @@ export default class Error extends React.Component<Props> {
return (
<>
<h1>An Error Occured ! {this.props.statusCode}</h1>
{this.props.eventId && (
<button onClick={() => Sentry.showReportDialog({ eventId: this.props.eventId })}>Report Error</button>
)}
</>
)
}
}

View File

@ -1,34 +1,53 @@
import React from 'react'
import HelloWorld from '@cp/HelloWorld'
import Image from '@cp/Image'
function isWindow() {
try {
window.isNaN(1)
return true
} catch {
return false
}
}
import Navbar from '@cp/dzeio/Navbar'
import GradientBackground from '@cp/dzeio/GradientBackground'
import NavbarSpace from '@cp/dzeio/Navbar/NavbarSpace'
import Container from '@cp/dzeio/Container'
import Overflow from '@cp/dzeio/Overflow'
import Row from '@cp/dzeio/Row'
import Col from '@cp/dzeio/Col'
import Input from '@cp/dzeio/Input'
import { BoxWrapper, BoxHeader, BoxBody } from '@cp/dzeio/Box'
import DebugCols from '@cp/dzeio/Col/DebugCols'
export default class Index extends React.Component {
public render() {
if (isWindow()) {
throw new Error('Test')
}
return (
<>
<HelloWorld>Hello World</HelloWorld>
<Image
max={{height:400,width:200}}
default={{width:200,height:100}}
deleteOnError={true}
src="https://source.unsplash.com/random/800x600"
/>
<HelloWorld>Hello World</HelloWorld>
{/* <DebugCols /> */}
<Navbar links={[]} />
<GradientBackground color="primary">
<NavbarSpace />
<Overflow top />
</GradientBackground>
<Row>
<Col size={9} tabletSize={6} mobileSize={4}>
<Overflow bottom />
<Row>
{[0, 1, 2, 3, 4, 5].map((el, index) => (
<Col size={4} tabletSize={4} mobileSize={4} key={index}>
<BoxWrapper outline>
<BoxHeader title="Post Title" subtitle="Post subtitle" />
<BoxBody>
<p>This Article is wonderfull !</p>
</BoxBody>
</BoxWrapper>
</Col>
))}
</Row>
</Col>
<Col size={3} tabletSize={2} mobileSize={4}>
<Input type="select" label="Sort">
<option>Most to less recent post</option>
<option>less to mort recent post</option>
</Input>
</Col>
</Row>
</>
)
}
}

View File

@ -1,22 +1,20 @@
// Base Server Config
// https://nextjs.org/docs/advanced-features/custom-server
import { createServer } from 'http'
import { parse } from 'url'
import next from 'next'
import express from 'express'
const port = parseInt(process.env.PORT || '3000', 10)
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
;(async () => {
await app.prepare()
createServer((req, res) => {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(req.url || '', true)
app.prepare().then(() => {
const server = express()
handle(req, res, parsedUrl)
}).listen(parseInt(process.env.PORT || '3000', 10), () => {
console.log('> Ready on http://localhost:3000')
server.all('*', (req, res) => {
return handle(req, res)
})
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`)
})
})
})()

6633
yarn.lock

File diff suppressed because it is too large Load Diff