diff --git a/config/config.go b/config/config.go
index 1f7ee9174..6989f4f2a 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,10 +1,11 @@
package config
import (
- "github.com/k0kubun/pp"
- "gopkg.in/yaml.v2"
"io/ioutil"
"log"
+
+ "github.com/k0kubun/pp"
+ "gopkg.in/yaml.v2"
)
type Maintainers struct {
@@ -54,6 +55,7 @@ type Commit0Config struct {
Maintainers []Maintainers `yaml:"maintainers"`
Network Network `yaml:"network"`
Services []Service `yaml:"services"`
+ React React `yaml:react`
}
func LoadConfig(filePath string) *Commit0Config {
diff --git a/config/react.go b/config/react.go
new file mode 100644
index 000000000..b1207b861
--- /dev/null
+++ b/config/react.go
@@ -0,0 +1,24 @@
+package config
+
+type reactApp struct {
+ Name string
+}
+
+type reactHeader struct {
+ Enabled bool
+}
+
+type reactSidenav struct {
+ Enabled bool
+}
+
+type reactAccount struct {
+ Enabled bool
+ Required bool
+}
+type React struct {
+ App reactApp
+ Account reactAccount
+ Header reactHeader
+ Sidenav reactSidenav
+}
diff --git a/templates/commit0/commit0.tmpl b/templates/commit0/commit0.tmpl
index e73129bc5..71a4a9ff0 100644
--- a/templates/commit0/commit0.tmpl
+++ b/templates/commit0/commit0.tmpl
@@ -1,10 +1,10 @@
organization: mycompany
name: {{.}}
-description:
+description:
git-repo: github.com/yourrepo
-docker-repo:
-maintainers:
-# - name: bob
+docker-repo:
+maintainers:
+# - name: bob
# email: bob@test.com
network:
@@ -18,5 +18,15 @@ network:
enabled: true
port: 8090
+react:
+ app:
+ name: {{.}}
+ header:
+ enabled: true
+ account:
+ enabled: true
+ required: false
+ sidenav:
+ enabled: true
services:
diff --git a/templates/react/.gitignore b/templates/react/.gitignore
index 4d29575de..6ec1a4213 100644
--- a/templates/react/.gitignore
+++ b/templates/react/.gitignore
@@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
+/package-lock.json
# testing
/coverage
diff --git a/templates/react/jsconfig.json b/templates/react/jsconfig.json
new file mode 100644
index 000000000..ec2332eb4
--- /dev/null
+++ b/templates/react/jsconfig.json
@@ -0,0 +1,5 @@
+{
+ "compilerOptions": {
+ "baseUrl": "src"
+ }
+}
diff --git a/templates/react/package.json b/templates/react/package.json
index e2bf084fb..ef3c20a7f 100644
--- a/templates/react/package.json
+++ b/templates/react/package.json
@@ -1,11 +1,17 @@
{
- "name": "{{ .Name }}",
+ "name": "commit0",
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@material-ui/core": "^4.5.1",
+ "@material-ui/icons": "^4.5.1",
"react": "^16.10.2",
"react-dom": "^16.10.2",
- "react-scripts": "3.2.0"
+ "react-redux": "^7.1.1",
+ "react-router": "^5.1.2",
+ "react-router-dom": "^5.1.2",
+ "react-scripts": "3.2.0",
+ "redux": "^4.0.4"
},
"scripts": {
"start": "react-scripts start",
diff --git a/templates/react/package.json.tmpl b/templates/react/package.json.tmpl
new file mode 100644
index 000000000..82a05ebc9
--- /dev/null
+++ b/templates/react/package.json.tmpl
@@ -0,0 +1,37 @@
+{
+ "name": "{{ .React.App.Name }}",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@material-ui/core": "^4.5.1",
+ "@material-ui/icons": "^4.5.1",
+ "react": "^16.10.2",
+ "react-dom": "^16.10.2",
+ "react-redux": "^7.1.1",
+ "react-router": "^5.1.2",
+ "react-router-dom": "^5.1.2",
+ "react-scripts": "3.2.0",
+ "redux": "^4.0.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": "react-app"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/templates/react/src/App.css b/templates/react/src/App.css
deleted file mode 100644
index afc388571..000000000
--- a/templates/react/src/App.css
+++ /dev/null
@@ -1,22 +0,0 @@
-.App {
- text-align: center;
-}
-
-.App-logo {
- height: 40vmin;
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #09d3ac;
-}
diff --git a/templates/react/src/App.js b/templates/react/src/App.js
index ce9cbd294..d734e7173 100644
--- a/templates/react/src/App.js
+++ b/templates/react/src/App.js
@@ -1,26 +1,23 @@
import React from 'react';
-import logo from './logo.svg';
-import './App.css';
+import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
+import Layout from 'components/layout';
-function App() {
+export default function App() {
return (
-
+
+
+
+
+ a
+
+
+ b
+
+
+ c
+
+
+
+
);
}
-
-export default App;
diff --git a/templates/react/src/App.test.js b/templates/react/src/App.test.js
deleted file mode 100644
index a754b201b..000000000
--- a/templates/react/src/App.test.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import App from './App';
-
-it('renders without crashing', () => {
- const div = document.createElement('div');
- ReactDOM.render(, div);
- ReactDOM.unmountComponentAtNode(div);
-});
diff --git a/templates/react/src/components/layout/header/account.js b/templates/react/src/components/layout/header/account.js
new file mode 100644
index 000000000..354c566ab
--- /dev/null
+++ b/templates/react/src/components/layout/header/account.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import IconButton from '@material-ui/core/IconButton';
+import AccountCircle from '@material-ui/icons/AccountCircle';
+import MenuItem from '@material-ui/core/MenuItem';
+import Menu from '@material-ui/core/Menu';
+
+export default function MenuAppBar() {
+ const [anchorEl, setAnchorEl] = React.useState(null);
+ const open = Boolean(anchorEl);
+
+ const handleMenu = event => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const anchorOriginProps = {
+ vertical: 'top',
+ horizontal: 'right',
+ };
+ const transformOriginProps = {
+ vertical: 'top',
+ horizontal: 'right',
+ };
+ return (
+
+ );
+}
diff --git a/templates/react/src/components/layout/header/index.js b/templates/react/src/components/layout/header/index.js
new file mode 100644
index 000000000..3b0ac11bb
--- /dev/null
+++ b/templates/react/src/components/layout/header/index.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import AppBar from '@material-ui/core/AppBar';
+import Toolbar from '@material-ui/core/Toolbar';
+import Typography from '@material-ui/core/Typography';
+import config from 'config';
+import Sidenav from 'components/layout/header/sidenav';
+import Account from 'components/layout/header/account';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ flexGrow: 1,
+ },
+ title: {
+ flexGrow: 1,
+ },
+}));
+
+export default function MenuAppBar() {
+ const classes = useStyles();
+ return (
+
+
+
+ { config && config.sidenav && config.sidenav.enabled && }
+
+ { config && config.app && config.app.name }
+
+ { config && config.account && config.account.enabled && }
+
+
+
+ );
+}
diff --git a/templates/react/src/components/layout/header/sidenav.js b/templates/react/src/components/layout/header/sidenav.js
new file mode 100644
index 000000000..d6b01d618
--- /dev/null
+++ b/templates/react/src/components/layout/header/sidenav.js
@@ -0,0 +1,26 @@
+import React, { Fragment } from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import IconButton from '@material-ui/core/IconButton';
+import MenuIcon from '@material-ui/icons/Menu';
+
+const useStyles = makeStyles(theme => ({
+ menuButton: {
+ marginRight: theme.spacing(2),
+ },
+}));
+
+export default function Sidenav() {
+ const classes = useStyles();
+ return (
+
+
+
+
+
+ );
+}
diff --git a/templates/react/src/components/layout/index.js b/templates/react/src/components/layout/index.js
new file mode 100644
index 000000000..4f18ad4d0
--- /dev/null
+++ b/templates/react/src/components/layout/index.js
@@ -0,0 +1,15 @@
+import React, { Fragment } from 'react';
+import Container from '@material-ui/core/Container';
+import Header from 'components/layout/header';
+import config from 'config';
+
+export default function App({children}) {
+ return (
+
+ { config && config.header && config.header.enabled && }
+
+ {children}
+
+
+ );
+}
diff --git a/templates/react/src/config/index.js b/templates/react/src/config/index.js
new file mode 100644
index 000000000..c4006e493
--- /dev/null
+++ b/templates/react/src/config/index.js
@@ -0,0 +1,15 @@
+export default {
+ app: {
+ name: 'Commit0',
+ },
+ account: {
+ enabled: true,
+ required: true,
+ },
+ header: {
+ enabled: true,
+ },
+ sidenav: {
+ enabled: true,
+ }
+}
diff --git a/templates/react/src/config/index.js.tmpl b/templates/react/src/config/index.js.tmpl
new file mode 100644
index 000000000..710a0a554
--- /dev/null
+++ b/templates/react/src/config/index.js.tmpl
@@ -0,0 +1,15 @@
+export default {
+ app: {
+ name: '{{ .React.App.Name }}',
+ },
+ account: {
+ enabled: {{ .React.Account.Enabled }},
+ required: {{ .React.Account.Required }},
+ },
+ header: {
+ enabled: {{ .React.Header.Enabled }},
+ },
+ sidenav: {
+ enabled: {{ .React.Sidenav.Enabled }},
+ }
+}
diff --git a/templates/react/src/index.css b/templates/react/src/index.css
deleted file mode 100644
index 4a1df4db7..000000000
--- a/templates/react/src/index.css
+++ /dev/null
@@ -1,13 +0,0 @@
-body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
- "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
- monospace;
-}
diff --git a/templates/react/src/index.js b/templates/react/src/index.js
index 87d1be551..f38f13b15 100644
--- a/templates/react/src/index.js
+++ b/templates/react/src/index.js
@@ -1,12 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom';
-import './index.css';
+import { Provider } from "react-redux";
+import CssBaseline from '@material-ui/core/CssBaseline';
+import { ThemeProvider } from '@material-ui/styles';
import App from './App';
-import * as serviceWorker from './serviceWorker';
+import theme from './theme';
+import store from "./redux/store";
-ReactDOM.render(, document.getElementById('root'));
-
-// If you want your app to work offline and load faster, you can change
-// unregister() to register() below. Note this comes with some pitfalls.
-// Learn more about service workers: https://bit.ly/CRA-PWA
-serviceWorker.unregister();
+ReactDOM.render(
+
+
+
+
+
+ ,
+ document.querySelector('#root'),
+);
diff --git a/templates/react/src/logo.svg b/templates/react/src/logo.svg
deleted file mode 100644
index 2e5df0d3a..000000000
--- a/templates/react/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/templates/react/src/redux/actions.js b/templates/react/src/redux/actions.js
new file mode 100644
index 000000000..b88d66584
--- /dev/null
+++ b/templates/react/src/redux/actions.js
@@ -0,0 +1,3 @@
+export const USER_LOADING = 'USER_LOADING';
+export const USER_ERROR = 'USER_ERROR';
+export const USER_LOADED = 'USER_LOADED';
diff --git a/templates/react/src/redux/reducers/index.js b/templates/react/src/redux/reducers/index.js
new file mode 100644
index 000000000..c164a700c
--- /dev/null
+++ b/templates/react/src/redux/reducers/index.js
@@ -0,0 +1,6 @@
+import { combineReducers } from "redux";
+import user from 'redux/reducers/user';
+
+export default combineReducers({
+ user,
+});
diff --git a/templates/react/src/redux/reducers/object-reducer.js b/templates/react/src/redux/reducers/object-reducer.js
new file mode 100644
index 000000000..05f976174
--- /dev/null
+++ b/templates/react/src/redux/reducers/object-reducer.js
@@ -0,0 +1,31 @@
+const initialState = {
+ loading: true,
+ error: null,
+ data: null,
+};
+
+export default (loading, loaded, error) => {
+ return (state = initialState, action) => {
+ switch (action.type) {
+ case loading:
+ return {
+ ...state,
+ loading: true,
+ }
+ case error:
+ return {
+ ...state,
+ loading: false,
+ error: action.error,
+ }
+ case loaded:
+ return {
+ loading: false,
+ error: null,
+ data: action.data
+ }
+ default:
+ return state;
+ }
+ }
+}
diff --git a/templates/react/src/redux/reducers/user.js b/templates/react/src/redux/reducers/user.js
new file mode 100644
index 000000000..54959b8f1
--- /dev/null
+++ b/templates/react/src/redux/reducers/user.js
@@ -0,0 +1,9 @@
+import objectReducer from 'redux/reducers/object-reducer';
+
+import {
+ USER_LOADING,
+ USER_LOADED,
+ USER_ERROR,
+} from 'redux/actions';
+
+export default objectReducer(USER_LOADING, USER_LOADED, USER_ERROR);
diff --git a/templates/react/src/redux/store.js b/templates/react/src/redux/store.js
new file mode 100644
index 000000000..8fc60501c
--- /dev/null
+++ b/templates/react/src/redux/store.js
@@ -0,0 +1,4 @@
+import { createStore } from "redux";
+import rootReducer from "redux/reducers";
+
+export default createStore(rootReducer);
diff --git a/templates/react/src/serviceWorker.js b/templates/react/src/serviceWorker.js
deleted file mode 100644
index f8c7e50c2..000000000
--- a/templates/react/src/serviceWorker.js
+++ /dev/null
@@ -1,135 +0,0 @@
-// This optional code is used to register a service worker.
-// register() is not called by default.
-
-// This lets the app load faster on subsequent visits in production, and gives
-// it offline capabilities. However, it also means that developers (and users)
-// will only see deployed updates on subsequent visits to a page, after all the
-// existing tabs open on the page have been closed, since previously cached
-// resources are updated in the background.
-
-// To learn more about the benefits of this model and instructions on how to
-// opt-in, read https://bit.ly/CRA-PWA
-
-const isLocalhost = Boolean(
- window.location.hostname === 'localhost' ||
- // [::1] is the IPv6 localhost address.
- window.location.hostname === '[::1]' ||
- // 127.0.0.1/8 is considered localhost for IPv4.
- window.location.hostname.match(
- /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
- )
-);
-
-export function register(config) {
- if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
- // The URL constructor is available in all browsers that support SW.
- const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
- if (publicUrl.origin !== window.location.origin) {
- // Our service worker won't work if PUBLIC_URL is on a different origin
- // from what our page is served on. This might happen if a CDN is used to
- // serve assets; see https://github.com/facebook/create-react-app/issues/2374
- return;
- }
-
- window.addEventListener('load', () => {
- const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
-
- if (isLocalhost) {
- // This is running on localhost. Let's check if a service worker still exists or not.
- checkValidServiceWorker(swUrl, config);
-
- // Add some additional logging to localhost, pointing developers to the
- // service worker/PWA documentation.
- navigator.serviceWorker.ready.then(() => {
- console.log(
- 'This web app is being served cache-first by a service ' +
- 'worker. To learn more, visit https://bit.ly/CRA-PWA'
- );
- });
- } else {
- // Is not localhost. Just register service worker
- registerValidSW(swUrl, config);
- }
- });
- }
-}
-
-function registerValidSW(swUrl, config) {
- navigator.serviceWorker
- .register(swUrl)
- .then(registration => {
- registration.onupdatefound = () => {
- const installingWorker = registration.installing;
- if (installingWorker == null) {
- return;
- }
- installingWorker.onstatechange = () => {
- if (installingWorker.state === 'installed') {
- if (navigator.serviceWorker.controller) {
- // At this point, the updated precached content has been fetched,
- // but the previous service worker will still serve the older
- // content until all client tabs are closed.
- console.log(
- 'New content is available and will be used when all ' +
- 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
- );
-
- // Execute callback
- if (config && config.onUpdate) {
- config.onUpdate(registration);
- }
- } else {
- // At this point, everything has been precached.
- // It's the perfect time to display a
- // "Content is cached for offline use." message.
- console.log('Content is cached for offline use.');
-
- // Execute callback
- if (config && config.onSuccess) {
- config.onSuccess(registration);
- }
- }
- }
- };
- };
- })
- .catch(error => {
- console.error('Error during service worker registration:', error);
- });
-}
-
-function checkValidServiceWorker(swUrl, config) {
- // Check if the service worker can be found. If it can't reload the page.
- fetch(swUrl)
- .then(response => {
- // Ensure service worker exists, and that we really are getting a JS file.
- const contentType = response.headers.get('content-type');
- if (
- response.status === 404 ||
- (contentType != null && contentType.indexOf('javascript') === -1)
- ) {
- // No service worker found. Probably a different app. Reload the page.
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister().then(() => {
- window.location.reload();
- });
- });
- } else {
- // Service worker found. Proceed as normal.
- registerValidSW(swUrl, config);
- }
- })
- .catch(() => {
- console.log(
- 'No internet connection found. App is running in offline mode.'
- );
- });
-}
-
-export function unregister() {
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister();
- });
- }
-}
diff --git a/templates/react/src/theme/index.js b/templates/react/src/theme/index.js
new file mode 100644
index 000000000..b9718a185
--- /dev/null
+++ b/templates/react/src/theme/index.js
@@ -0,0 +1,22 @@
+import { red } from '@material-ui/core/colors';
+import { createMuiTheme } from '@material-ui/core/styles';
+
+// A custom theme for this app
+const theme = createMuiTheme({
+ palette: {
+ primary: {
+ main: '#556cd6',
+ },
+ secondary: {
+ main: '#19857b',
+ },
+ error: {
+ main: red.A400,
+ },
+ background: {
+ default: '#fff',
+ },
+ },
+});
+
+export default theme;
diff --git a/templator/templator.go b/templator/templator.go
index 9c2050ed7..6260e5b65 100644
--- a/templator/templator.go
+++ b/templator/templator.go
@@ -2,6 +2,7 @@ package templator
import (
"path/filepath"
+ "strings"
"text/template"
"github.com/commitdev/commit0/config"
@@ -125,6 +126,9 @@ type DirectoryTemplator struct {
func (d *DirectoryTemplator) TemplateFiles(config *config.Commit0Config, overwrite bool) {
for _, template := range d.Templates {
d, f := filepath.Split(template.Name())
+ if strings.HasSuffix(f, ".tmpl") {
+ f = strings.Replace(f, ".tmpl", "", -1)
+ }
if overwrite {
util.TemplateFileAndOverwrite(d, f, template, config)
} else {
@@ -137,7 +141,10 @@ func NewDirectoryTemplator(box *packr.Box, dir string) *DirectoryTemplator {
templates := []*template.Template{}
for _, file := range getFileNames(box, dir) {
templateSource, _ := box.FindString(file)
- template, _ := template.New(file).Funcs(util.FuncMap).Parse(templateSource)
+ template, err := template.New(file).Funcs(util.FuncMap).Parse(templateSource)
+ if err != nil {
+ panic(err)
+ }
templates = append(templates, template)
}
return &DirectoryTemplator{
@@ -157,5 +164,24 @@ func getFileNames(box *packr.Box, dir string) []string {
}
return nil
})
- return keys
+ return removeTmplDuplicates(keys)
+}
+
+func removeTmplDuplicates(keys []string) []string {
+ filteredKeys := []string{}
+ for _, key := range keys {
+ if !containsStr(keys, key+".tmpl") {
+ filteredKeys = append(filteredKeys, key)
+ }
+ }
+ return filteredKeys
+}
+
+func containsStr(arr []string, key string) bool {
+ for _, val := range arr {
+ if val == key {
+ return true
+ }
+ }
+ return false
}