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 ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
-
+ + + + + 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 ( +
+ + + + + Profile + My account + +
+ ); +} 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 }