├── CONTRIBUTING.md ├── .gitignore ├── src ├── types │ ├── fonts.d.ts │ └── images.d.ts ├── shared │ └── readme.txt ├── renderer │ ├── resources │ │ └── images │ │ │ └── clock-64x64.png │ ├── views │ │ ├── HomeView.tsx │ │ ├── ExampleView1.tsx │ │ └── ExampleView2.tsx │ ├── renderer-process.tsx │ ├── index.html │ ├── styles │ │ ├── GlobalStyle.tsx │ │ ├── extensions.d.ts │ │ ├── defaultTheme.tsx │ │ └── fontFaces.ts │ ├── core │ │ └── ExampleCoreLogic.ts │ └── MainLayout.tsx ├── __tests__ │ ├── setup.ts │ ├── teardown.ts │ └── example.spec.ts └── main │ └── main-process.ts ├── .github ├── project-logo-400.jpg └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── tsconfig.json ├── jest.config.ts ├── LICENSE ├── readme.md ├── package.json ├── CODE_OF_CONDUCT.md └── webpack.config.ts /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | . 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | pack 4 | .vscode -------------------------------------------------------------------------------- /src/types/fonts.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.eot"; 2 | declare module "*.ttf"; 3 | declare module "*.woff"; 4 | declare module "*.woff2"; -------------------------------------------------------------------------------- /.github/project-logo-400.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marceloaugusto80/electron-react-typescript-boilerplate/HEAD/.github/project-logo-400.jpg -------------------------------------------------------------------------------- /src/shared/readme.txt: -------------------------------------------------------------------------------- 1 | put scripts and modules used by both renderer and main process here. 2 | import then like: import {something} from "@/shared/something"; -------------------------------------------------------------------------------- /src/types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.gif"; 2 | declare module "*.jpg"; 3 | declare module "*.jpeg"; 4 | declare module "*.png"; 5 | declare module "*.svg"; -------------------------------------------------------------------------------- /src/renderer/resources/images/clock-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marceloaugusto80/electron-react-typescript-boilerplate/HEAD/src/renderer/resources/images/clock-64x64.png -------------------------------------------------------------------------------- /src/__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defined in jest.config.ts, "globalSetup" property. 3 | */ 4 | export default function setup():Promise { 5 | console.log("\n\n## Global setup executed\n"); 6 | return Promise.resolve(); 7 | } -------------------------------------------------------------------------------- /src/__tests__/teardown.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defined in jest.config.ts, "globalTeardown" property. 3 | */ 4 | export default function teardown(): Promise { 5 | console.log("\n## Global teardown executed."); 6 | return Promise.resolve(); 7 | } -------------------------------------------------------------------------------- /src/renderer/views/HomeView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function HomeView() { 4 | return ( 5 |
6 |

Home

7 |

This is the initial view.

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/__tests__/example.spec.ts: -------------------------------------------------------------------------------- 1 | describe("Example", () => { 2 | 3 | test("2 + 2 returns 4", () => { 4 | const expected = 4; 5 | const actual = 2 + 2; 6 | expect(actual).toBe(expected); 7 | 8 | }); 9 | 10 | }); // end example 11 | -------------------------------------------------------------------------------- /src/renderer/renderer-process.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import React, { StrictMode } from "react"; 3 | import MainLayout from "./MainLayout"; 4 | 5 | const container = document.getElementById("root"); 6 | if(!container) throw Error("Root element not found."); 7 | 8 | const root = createRoot(container); 9 | 10 | root.render(); 11 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | My App 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "CommonJS", 5 | "jsx": "react", 6 | "resolveJsonModule": true, 7 | "sourceMap": true, 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "baseUrl": "./", 12 | "paths": { 13 | "@/shared/*" : ["src/shared/*"], 14 | "@/renderer/*" : ["src/renderer/*"], 15 | "@/main/*" : ["src/main/*"] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/renderer/styles/GlobalStyle.tsx: -------------------------------------------------------------------------------- 1 | import {createGlobalStyle } from "styled-components"; 2 | import { robotoFontFace } from "./fontFaces"; 3 | 4 | export const GlobalStyle = createGlobalStyle` 5 | 6 | ${robotoFontFace} 7 | 8 | body { 9 | margin: 0 !important; 10 | padding: 0 !important; 11 | font-family: Roboto; 12 | } 13 | 14 | a { 15 | color: unset; 16 | text-decoration: none; 17 | &:visited { 18 | text-decoration: none; 19 | } 20 | } 21 | `; 22 | 23 | -------------------------------------------------------------------------------- /src/renderer/styles/extensions.d.ts: -------------------------------------------------------------------------------- 1 | import "styled-components"; 2 | 3 | interface ThemeColor { 4 | main: string; 5 | contrast: string; 6 | } 7 | 8 | // allow intellisense and avoid tsc warnings or errors 9 | declare module "styled-components" { 10 | export interface DefaultTheme { 11 | colors: { 12 | background: string; 13 | foreground: string; 14 | brand1: ThemeColor; 15 | brand2: ThemeColor; 16 | primary: ThemeColor; 17 | danger: ThemeColor; 18 | warning: ThemeColor; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import {InitialOptionsTsJest, pathsToModuleNameMapper} from "ts-jest"; 2 | import {compilerOptions} from "./tsconfig.json"; 3 | 4 | const config: InitialOptionsTsJest = { 5 | 6 | clearMocks: true, 7 | 8 | coverageProvider: "v8", 9 | 10 | globalSetup: "./src/__tests__/setup.ts", 11 | 12 | globalTeardown: "./src/__tests__/teardown.ts", 13 | 14 | preset: "ts-jest", 15 | 16 | roots: [ 17 | "./src/__tests__" 18 | ], 19 | 20 | testEnvironment: "node", 21 | 22 | testMatch: [ 23 | "**/?(*.)+(spec|test).[tj]s?(x)" 24 | ], 25 | 26 | testPathIgnorePatterns: [ 27 | "\\\\node_modules\\\\" 28 | ], 29 | 30 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths) 31 | 32 | }; 33 | 34 | export default config; -------------------------------------------------------------------------------- /src/renderer/core/ExampleCoreLogic.ts: -------------------------------------------------------------------------------- 1 | type TimerCallback = (n: number) => void; 2 | 3 | /** 4 | * Just an example class. 5 | */ 6 | export default class ExampleCoreLogic { 7 | 8 | private readonly _onTickCallback: TimerCallback; 9 | private intervalRef: NodeJS.Timeout | null; 10 | n: number; 11 | 12 | constructor(onTickCallback: TimerCallback) { 13 | this._onTickCallback = onTickCallback; 14 | this.intervalRef = null; 15 | this.n = 0; 16 | 17 | } 18 | 19 | run(): void { 20 | if(!this.intervalRef) { 21 | this.intervalRef = setInterval(() => { 22 | this._onTickCallback(++this.n); 23 | }, 1000); 24 | } 25 | 26 | } 27 | 28 | stop() { 29 | if(this.intervalRef) 30 | clearInterval(this.intervalRef); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/renderer/styles/defaultTheme.tsx: -------------------------------------------------------------------------------- 1 | import {DefaultTheme} from "styled-components"; 2 | 3 | // The .tsx extension in this file is to allow color picker extension in visual code. Change to .ts if you want. 4 | 5 | const defaultTheme: DefaultTheme = { 6 | colors: { 7 | background: "#f1f1f1", 8 | foreground: "#111111", 9 | brand1: { 10 | main: "#116f8b", 11 | contrast: "#dae9ee" 12 | }, 13 | brand2: { 14 | main: "#460261", 15 | contrast: "#e3d6e9" 16 | }, 17 | primary: { 18 | main: "#045f7a", 19 | contrast: "#d7f0f8" 20 | }, 21 | danger: { 22 | main: "#610202", 23 | contrast: "#f5dcdc" 24 | }, 25 | warning: { 26 | main: "#c2c500", 27 | contrast: "#fdfde0" 28 | }, 29 | 30 | } 31 | 32 | }; 33 | 34 | export default defaultTheme; -------------------------------------------------------------------------------- /src/renderer/views/ExampleView1.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import ExampleCoreLogic from "@/renderer/core/ExampleCoreLogic"; 3 | import clock from "@/renderer/resources/images/clock-64x64.png"; 4 | 5 | export function ExampleView1() { 6 | 7 | const objRef = useRef(); 8 | const [theNumber, setTheNumber] = useState(0); 9 | 10 | useEffect(() => { 11 | objRef.current = new ExampleCoreLogic((n: number) => setTheNumber(n)); 12 | objRef.current.run(); 13 | return () => { 14 | if(objRef.current) objRef.current.stop(); 15 | } 16 | }, []); 17 | 18 | return ( 19 |
20 |

Example view 1

21 |

Just a example view

22 |

An image:

23 | 24 |

A counter:

25 |

{theNumber}

26 |
27 | ); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Marcelo Augusto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/renderer/styles/fontFaces.ts: -------------------------------------------------------------------------------- 1 | import { css } from "styled-components" 2 | import RobotoBoldItalic from "@fontsource/roboto/files/roboto-latin-ext-700-italic.woff" 3 | import RobotoBoldNormal from "@fontsource/roboto/files/roboto-latin-ext-700-normal.woff" 4 | import RobotoNormalItalic from "@fontsource/roboto/files/roboto-latin-ext-300-italic.woff" 5 | import RobotoNormalNormal from "@fontsource/roboto/files/roboto-latin-ext-300-normal.woff" 6 | 7 | export const robotoFontFace = css` 8 | @font-face { 9 | font-family: 'Roboto'; 10 | font-weight: normal; 11 | font-style: normal; 12 | src: url(${RobotoNormalNormal}) format('woff'); 13 | } 14 | @font-face { 15 | font-family: 'Roboto'; 16 | font-weight: normal; 17 | font-style: italic; 18 | src: url(${RobotoNormalItalic}) format('woff'); 19 | } 20 | @font-face { 21 | font-family: 'Roboto'; 22 | font-weight: bold; 23 | font-style: normal; 24 | src: url(${RobotoBoldNormal}) format('woff'); 25 | } 26 | @font-face { 27 | font-family: 'Roboto'; 28 | font-weight: bold; 29 | font-style: italic; 30 | src: url(${RobotoBoldItalic}) format('woff'); 31 | } 32 | `; -------------------------------------------------------------------------------- /src/renderer/views/ExampleView2.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from 'react'; 2 | import {dialog} from "@electron/remote"; 3 | import fs from "fs"; 4 | 5 | export function ExampleView2() { 6 | 7 | const [text, setText] = useState(null); 8 | 9 | const handleOpenFileClick = async () => { 10 | try { 11 | 12 | const result = await dialog.showOpenDialog({ properties: ["openFile"] }); 13 | const { filePaths } = result; 14 | if (filePaths.length != 1) return; 15 | 16 | const response = await fs.promises.readFile(filePaths[0], { encoding: "utf-8" }); 17 | setText(response); 18 | 19 | } catch (error) { 20 | 21 | setText((error as Error).message); 22 | 23 | } 24 | } 25 | 26 | return ( 27 |
28 |

Example view 2

29 |

Another example view.

30 |

Try to open a file to test electron dialog and file system:

31 | 32 | { 33 | text && 34 | 35 |

File content:

36 |

{text}

37 | 38 |
39 | } 40 |
41 | ); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Electron / React / Typescript boilerplate project 2 | ![](./.github/project-logo-400.jpg) 3 | 4 | 5 | #### Slim template for desktop apps using: 6 | - [Electron](https://electronjs.org/) 7 | - [Typescript](https://www.typescriptlang.org/) 8 | - [React](https://reactjs.org/) 9 | - [Styled Components](https://styled-components.com/) 10 | - [Webpack](https://webpack.js.org/) 11 | - [React Fast Refresh](https://github.com/pmmmwh/react-refresh-webpack-plugin) 12 | 13 |
14 |
15 | 16 | # Before anything 17 | As always, install packages. 18 | Open a terminal in the project root folder and run: 19 | ```cmd 20 | $ npm install 21 | ``` 22 | 23 | 24 | # During development 25 | 26 | ### Start the app in **development** mode (**with** hot reload). 27 | Run: 28 | ```cmd 29 | $ npm start 30 | ``` 31 | It will launch webpack dev server and electron [concurrently](https://www.npmjs.com/package/concurrently). 32 | It has [fast refresh](https://www.npmjs.com/package/react-refresh-webpack-plugin) (AKA hot-reload) enabled by default. 33 |
34 |
35 |
36 | ### Start app in **production** mode (**without** hot reload). 37 | Run: 38 | ```cmd 39 | $ npm run start:prod 40 | ``` 41 | 42 | # Testing 43 | Run: 44 | ```cmd 45 | $ npm test 46 | ``` 47 | This templated uses [Jest](https://jestjs.io/) (along with [Ts-Jest](https://www.npmjs.com/package/ts-jest)) as testing framework. 48 | 49 | # Deploy 50 | Just run: 51 | ```cmd 52 | $ npm run pack 53 | ``` 54 | and the output will available in the ```./pack``` folder. 55 | 56 | -------------------------------------------------------------------------------- /src/main/main-process.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from "electron"; 2 | import { initialize, enable } from "@electron/remote/main"; 3 | 4 | declare const ENVIRONMENT: String; 5 | 6 | const IS_DEV = (ENVIRONMENT == "development"); // const injected via webpack define plugin. 7 | const DEV_SERVER_URL = "http://localhost:9000"; // must match webpack dev server port. 8 | const HTML_FILE_PATH = "renderer/index.html"; 9 | 10 | function createWindow(): BrowserWindow | null { 11 | 12 | let win: BrowserWindow | null = new BrowserWindow({ 13 | width: 800, 14 | height: 600, 15 | webPreferences: { 16 | nodeIntegration: true, 17 | contextIsolation: false 18 | } 19 | }); 20 | 21 | if (IS_DEV) { 22 | win.webContents.openDevTools(); 23 | win.loadURL(DEV_SERVER_URL); 24 | } 25 | else { 26 | win.loadFile(HTML_FILE_PATH); 27 | win.removeMenu(); 28 | } 29 | 30 | return win; 31 | } 32 | 33 | app.whenReady() 34 | .then(() => { 35 | 36 | let win = createWindow(); 37 | if (!win) throw Error("BrowserWindow is null. Check main process initialization!"); 38 | initialize(); 39 | 40 | win.maximize(); 41 | enable(win.webContents); 42 | 43 | win.on("closed", () => { 44 | win = null; 45 | }); 46 | 47 | app.on('window-all-closed', () => { 48 | if (process.platform != "darwin") { 49 | app.quit() 50 | } 51 | }) 52 | 53 | app.on('activate', () => { 54 | if (win === null) { 55 | createWindow() 56 | } 57 | }) 58 | 59 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-react-typescript-boilerplate", 3 | "version": "3.0.0", 4 | "description": "Template project for desktop apps using Electron, React and Typescript", 5 | "author": "Marcelo Augusto", 6 | "license": "MIT", 7 | "keywords": [ 8 | "electron", 9 | "react", 10 | "typescript", 11 | "hot", 12 | "reload" 13 | ], 14 | "homepage": "https://github.com/marceloaugusto80/electron-react-typescript-boilerplate", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/marceloaugusto80/electron-react-typescript-boilerplate.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/marceloaugusto80/electron-react-typescript-boilerplate/issues" 21 | }, 22 | "main": "main-process.js", 23 | "scripts": { 24 | "test": "npx jest", 25 | "build": "npx webpack", 26 | "build:prod": "npx webpack --env production", 27 | "start": "npx concurrently \"npx webpack serve --config-name renderer --env hot-reload\" \"npx webpack --config-name main && npx electron ./dist/main-process.js\" --kill-others", 28 | "start:prod": "npm run build:prod && npx electron ./dist/main-process.js", 29 | "pack": "npx rimraf ./pack && npm run build:prod && npx electron-packager ./dist --out ./pack --overwrite" 30 | }, 31 | "dependencies": { 32 | "@electron/remote": "^2.0.8", 33 | "@fontsource/roboto": "^4.5.8", 34 | "electron": "^21.1.1", 35 | "react": "^18.2.0", 36 | "react-dom": "^18.2.0", 37 | "react-router-dom": "^6.4.2", 38 | "styled-components": "^5.3.6" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "^7.19.3", 42 | "@babel/plugin-transform-runtime": "^7.19.1", 43 | "@babel/preset-react": "^7.18.6", 44 | "@babel/preset-typescript": "^7.18.6", 45 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.8", 46 | "@types/jest": "^29.1.2", 47 | "@types/node": "^18.11.0", 48 | "@types/react": "^18.0.21", 49 | "@types/react-dom": "^18.0.6", 50 | "@types/react-router-dom": "^5.3.3", 51 | "@types/styled-components": "^5.1.26", 52 | "@types/webpack": "^5.28.0", 53 | "@types/webpack-dev-server": "^4.7.2", 54 | "@types/webpack-env": "^1.18.0", 55 | "babel-loader": "^8.2.5", 56 | "clean-webpack-plugin": "^4.0.0", 57 | "concurrently": "^7.4.0", 58 | "copy-webpack-plugin": "^11.0.0", 59 | "electron-packager": "^16.0.0", 60 | "file-loader": "^6.2.0", 61 | "html-webpack-plugin": "^5.5.0", 62 | "jest": "^29.2.0", 63 | "react-refresh": "^0.14.0", 64 | "rimraf": "^3.0.2", 65 | "ts-jest": "^29.0.3", 66 | "ts-node": "^10.9.1", 67 | "tsconfig-paths-webpack-plugin": "^4.0.0", 68 | "typescript": "^4.8.4", 69 | "webpack": "^5.74.0", 70 | "webpack-cli": "^4.10.0", 71 | "webpack-dev-middleware": "^5.3.3", 72 | "webpack-dev-server": "^4.11.1", 73 | "webpack-merge": "^5.8.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/renderer/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { HashRouter, Link, Route, Routes } from 'react-router-dom'; 3 | import styled, { ThemeProvider } from 'styled-components'; 4 | import defaultTheme from '@/renderer/styles/defaultTheme'; 5 | import { GlobalStyle } from "@/renderer/styles/GlobalStyle"; 6 | import { ExampleView1 } from '@/renderer/views/ExampleView1'; 7 | import { ExampleView2 } from '@/renderer/views/ExampleView2'; 8 | import HomeView from '@/renderer/views/HomeView'; 9 | 10 | export default function MainLayout() { 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | Home 20 | Example 1 21 | Example 2 22 | 23 |
24 | 25 |

The header

26 | 27 |
28 | 29 | 30 | } /> 31 | } /> 32 | } /> 33 | 34 | 35 | 38 |
39 |
40 |
41 |
42 | ); 43 | } 44 | 45 | const Content = styled.div` 46 | min-height: 100vh; 47 | overflow-x: hidden; 48 | display: grid; 49 | grid-template-areas: 50 | "header header" 51 | "panel body" 52 | "footer footer"; 53 | grid-template-columns: auto 1fr; 54 | grid-template-rows: auto 1fr auto; 55 | `; 56 | 57 | const SidePanel = styled.div` 58 | grid-area: panel; 59 | padding: 16px; 60 | background-color: ${props => props.theme.colors.brand2.main}; 61 | color: ${props => props.theme.colors.brand2.contrast}; 62 | a { 63 | display: block; 64 | margin-top: 32px; 65 | } 66 | `; 67 | 68 | const Header = styled.div` 69 | grid-area: header; 70 | background-color: ${props => props.theme.colors.brand1.main}; 71 | color: ${props => props.theme.colors.brand1.contrast}; 72 | padding: 16px; 73 | `; 74 | 75 | const Body = styled.div` 76 | background-color: ${props => props.theme.colors.background}; 77 | color: ${props => props.theme.colors.foreground}; 78 | grid-area: body; 79 | padding: 16px; 80 | `; 81 | 82 | const Footer = styled.div` 83 | grid-area: footer; 84 | padding: 2px; 85 | background-color: ${props => props.theme.colors.brand1.main}; 86 | color: ${props => props.theme.colors.brand1.contrast}; 87 | `; -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { Configuration, DefinePlugin, WebpackPluginInstance } from "webpack"; 3 | import "webpack-dev-server"; 4 | import HtmlWebpackPlugin from "html-webpack-plugin"; 5 | import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin"; 6 | import { TsconfigPathsPlugin } from "tsconfig-paths-webpack-plugin"; 7 | import CopyWebpackPlugin from "copy-webpack-plugin"; 8 | import { merge } from "webpack-merge"; 9 | 10 | // wraps env vars injected by webpack cli 11 | interface Env { 12 | development: boolean; 13 | hotReload: boolean; 14 | } 15 | 16 | function baseConfiguration(env: Env): Configuration { 17 | return { 18 | 19 | resolve: { 20 | plugins: [new TsconfigPathsPlugin()] 21 | }, 22 | 23 | mode: env.development ? "development" : "production", 24 | 25 | devtool: env.development ? "source-map" : undefined, 26 | 27 | plugins: [ 28 | new DefinePlugin({ 29 | "ENVIRONMENT": JSON.stringify(env.development ? "development" : "production") 30 | }), 31 | ] 32 | 33 | }; 34 | 35 | } // end base configuration 36 | 37 | function mainConfiguration(env: Env): Configuration { 38 | 39 | return { 40 | 41 | name: "main", 42 | 43 | context: path.join(__dirname, "src/main"), 44 | 45 | target: "electron-main", 46 | 47 | mode: env.development ? "development" : "production", 48 | 49 | externalsPresets: { 50 | electronMain: true 51 | }, 52 | 53 | entry: { 54 | "main-process": "./main-process.ts" 55 | }, 56 | 57 | output: { 58 | filename: "[name].js", 59 | path: path.join(__dirname, "dist"), 60 | clean: { 61 | keep: /renderer\// 62 | } 63 | }, 64 | 65 | module: { 66 | rules: [ 67 | { 68 | test: /\.ts$/, 69 | exclude: /node_modules/, 70 | use: { 71 | loader: "babel-loader", 72 | options: { 73 | presets: [ 74 | "@babel/preset-typescript", 75 | ], 76 | plugins: [ 77 | "@babel/plugin-transform-runtime" 78 | ] 79 | } 80 | } 81 | } 82 | ] 83 | }, 84 | 85 | plugins: [ 86 | new CopyWebpackPlugin({ 87 | patterns: ["../../package.json"] // electron packager need this file to pack the application. not needed during development. 88 | }) 89 | ] 90 | 91 | }; 92 | } // end main configuration 93 | 94 | function RendererConfiguration(env: Env): Configuration { 95 | 96 | const babelConfig = { 97 | presets: [ 98 | "@babel/preset-react", 99 | "@babel/preset-typescript", 100 | ], 101 | plugins: [ 102 | "@babel/plugin-transform-runtime", 103 | env.hotReload && require.resolve("react-refresh/babel") 104 | ].filter(Boolean) 105 | }; 106 | 107 | return { 108 | 109 | name: "renderer", 110 | 111 | context: path.join(__dirname, "src/renderer"), 112 | 113 | target: "electron-renderer", 114 | 115 | resolve: { 116 | extensions: [".js", ".jsx", ".ts", ".tsx", ".json"] 117 | }, 118 | 119 | externalsPresets: { 120 | electronRenderer: true 121 | }, 122 | 123 | entry: { 124 | "renderer-process": "./renderer-process.tsx" 125 | }, 126 | 127 | output: { 128 | filename: "scripts/[name].js", 129 | path: path.join(__dirname, "dist", "renderer"), 130 | clean: true, 131 | globalObject: env.hotReload ? "self" : undefined, // Hot Module Replacement needs this to work. See: // https://stackoverflow.com/questions/51000346/uncaught-typeerror-cannot-read-property-webpackhotupdate-of-undefined 132 | }, 133 | 134 | module: { 135 | rules: [ 136 | { 137 | test: /\.(js|jsx|ts|tsx)$/, 138 | exclude: /node_modules/, 139 | use: { 140 | loader: "babel-loader", 141 | options: babelConfig 142 | } 143 | }, 144 | { 145 | test: /\.(png|jpe?g|gif|svg)$/, 146 | use: { 147 | loader: "file-loader", 148 | options: { 149 | outputPath: "images", 150 | name: "[name].[ext]" 151 | } 152 | } 153 | }, 154 | { 155 | test: /\.(ttf|otf|woff2?)$/, 156 | use: { 157 | loader: "file-loader", 158 | options: { 159 | outputPath: "fonts", 160 | name: "[name].[ext]" 161 | } 162 | } 163 | }, 164 | 165 | ] 166 | }, 167 | 168 | plugins: [ 169 | 170 | new HtmlWebpackPlugin({ 171 | template: "index.html", 172 | }), 173 | 174 | (env.hotReload && new ReactRefreshWebpackPlugin()) as WebpackPluginInstance, 175 | 176 | ].filter(Boolean), 177 | 178 | devServer: { 179 | compress: true, 180 | hot: env.hotReload, 181 | port: 9000, 182 | historyApiFallback: true, 183 | devMiddleware: { 184 | writeToDisk: true 185 | } 186 | } 187 | }; 188 | 189 | } // end renderer configuration 190 | 191 | export default function (e: any) { 192 | 193 | const env: Env = { 194 | development: !e["production"], 195 | hotReload: !!e["hot-reload"] && !e["production"], 196 | }; 197 | 198 | const baseConfig = baseConfiguration(env); 199 | const mainConfig = merge(baseConfig, mainConfiguration(env)); 200 | const rendererConfig = merge(baseConfig, RendererConfiguration(env)); 201 | 202 | return [mainConfig, rendererConfig]; 203 | } --------------------------------------------------------------------------------