model) {
23 | Person me = personRepository.findOne(1L);
24 | model.put("me", me);
25 |
26 | // Return the name of the HTML (file) view.
27 | return "index";
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/example/about/web/WebConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.example.about.web;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 | import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
5 | import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;
6 |
7 | @Configuration
8 | public class WebConfiguration extends RepositoryRestConfigurerAdapter {
9 |
10 | @Override
11 | public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
12 | // Have all RESTful requests route through the "api" path.
13 | config.setBasePath("/api");
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/js/Address.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import BaseComponent from './BaseComponent'
3 |
4 | export default class Address extends BaseComponent {
5 |
6 | render() {
7 | if (!this.state.success) {
8 | return null;
9 | }
10 |
11 | return (
12 |
13 | {this.state.address}
14 | {this.state.city},
15 | {this.state.region}
16 | {this.state.postalCode}
17 | {this.state.phone}
18 |
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/js/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import {Router, Route} from 'react-router'
4 |
5 | import {BottomNavigation, BottomNavigationItem} from 'material-ui/BottomNavigation'
6 | import CircularProgress from 'material-ui/CircularProgress'
7 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
8 | import {
9 | Toolbar,
10 | ToolbarGroup,
11 | ToolbarSeparator,
12 | ToolbarTitle
13 | } from 'material-ui/Toolbar'
14 |
15 | import Address from './Address'
16 | import Education from './Education'
17 | import Employment from './Employment'
18 | import Person from './Person'
19 |
20 | import * as Utils from './Utils'
21 | import {customTheme} from './theme'
22 |
23 | // Webpack will treat this like any other module and the style+less loaders will insert style tags with the compiled CSS.
24 | import '../styles/App.less'
25 |
26 | class App extends React.Component {
27 |
28 | constructor(props) {
29 | super(props);
30 |
31 | this.state = {loading: true};
32 | }
33 |
34 | componentDidMount() {
35 | Utils.api('/api/persons/1', this);
36 | }
37 |
38 | render() {
39 | if (this.state.loading) {
40 | return (
41 |
42 |
43 |
44 | )
45 |
46 | } else {
47 | return (
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
62 |
63 |
64 |
67 |
68 |
69 | )
70 | }
71 | }
72 | }
73 |
74 | ReactDOM.render(
75 | ,
76 | document.getElementById('root')
77 | );
78 |
--------------------------------------------------------------------------------
/src/main/js/BaseComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as Utils from './Utils'
3 |
4 | /**
5 | * The base class used for all Components that will be doing JPA-AJAX requests.
6 | */
7 | export default class extends React.Component {
8 |
9 | constructor(props) {
10 | super(props);
11 |
12 | this.state = {};
13 | }
14 |
15 | componentDidMount() {
16 | if (this.props.url) {
17 | Utils.api(this.props.url, this);
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/js/Company.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Address from './Address'
3 | import Positions from './Positions'
4 |
5 | export default class Company extends React.Component {
6 |
7 | render() {
8 | if (!this.props.name) {
9 | return null;
10 | }
11 |
12 | return(
13 |
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/js/Education.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import BaseComponent from './BaseComponent'
3 | import University from './University'
4 |
5 | export default class Education extends BaseComponent {
6 |
7 | render() {
8 | if (!this.state.success
9 | || !this.state._embedded
10 | || !this.state._embedded.universities
11 | || !(this.state._embedded.universities.length > 0)) {
12 | return null;
13 | }
14 |
15 | return(
16 |
17 | {this.state._embedded.universities.map((university) =>
18 |
24 | )}
25 |
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/js/Employment.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import BaseComponent from './BaseComponent'
3 | import Company from './Company'
4 |
5 | import {
6 | Step,
7 | Stepper,
8 | StepButton,
9 | StepContent,
10 | StepLabel
11 | } from 'material-ui/Stepper'
12 | import RaisedButton from 'material-ui/RaisedButton'
13 | import FlatButton from 'material-ui/FlatButton'
14 |
15 | export default class Employment extends BaseComponent {
16 |
17 | constructor(props) {
18 | super(props);
19 |
20 | this.state = {
21 | stepIndex: 0
22 | };
23 |
24 | // This binding is necessary to make `this` work in the callback.
25 | this.handleNext = this.handleNext.bind(this);
26 | this.handlePrev = this.handlePrev.bind(this);
27 | }
28 |
29 | handleNext(e) {
30 | const stepIndex = this.state.stepIndex;
31 |
32 | if (stepIndex < 2) {
33 | this.setState({stepIndex: stepIndex + 1});
34 | }
35 | };
36 |
37 | handlePrev(e) {
38 | const stepIndex = this.state.stepIndex;
39 |
40 | if (stepIndex > 0) {
41 | this.setState({stepIndex: stepIndex - 1});
42 | }
43 | };
44 |
45 | // Note: To pass an argument like 'index' you need to use the ES6 arrow function to bind the arguments. Also
46 | // passing along the event object like the defaults.
47 | handleCurrent(e, index) {
48 | this.setState({stepIndex: index});
49 | }
50 |
51 | render() {
52 | if (!this.state.success) {
53 | return null;
54 | }
55 |
56 | const stepIndex = this.state.stepIndex;
57 | const employment = this;
58 | const limit = this.state._embedded.companies.length - 1;
59 |
60 | return(
61 |
62 | {this.state._embedded.companies.map((company, index) =>
63 |
64 | this.handleCurrent(e, index)}>
65 | {company.name}
66 |
67 |
68 |
75 | {index < limit ? (
76 |
84 | ) : (
85 |
91 | )
92 | }
93 |
94 |
95 | )}
96 |
97 | )
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/main/js/Person.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import BaseComponent from './BaseComponent'
3 |
4 | // Needed for onTouchTap
5 | import injectTapEventPlugin from 'react-tap-event-plugin';
6 | injectTapEventPlugin();
7 |
8 | export default class Person extends BaseComponent {
9 |
10 | render() {
11 | const middle = (this.props.middle) ? {this.props.middle} : '';
12 |
13 | return(
14 |
15 |
16 | {this.props.first}
17 | {middle}
18 | {this.props.last}
19 |
20 |
21 |
{this.props.email}
22 |
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/js/Position.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as Utils from './Utils'
3 | import Projects from './Projects'
4 |
5 | export default class Position extends React.Component {
6 |
7 | render() {
8 | if (!this.props.title) {
9 | return null;
10 | }
11 |
12 | return(
13 |
14 | {this.props.title}
15 |
16 | {Utils.formatMonthYear(this.props.start)}
17 | {Utils.formatMonthYear(this.props.end)}
18 |
19 | {
20 | (this.props.website) ?
21 | {this.props.website}
22 | : ''
23 | }
24 | {this.props.responsibilities}
25 |
26 |
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/js/Positions.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import BaseComponent from './BaseComponent'
3 | import Position from './Position'
4 |
5 | export default class Positions extends BaseComponent {
6 |
7 | render() {
8 | if (!this.state.success
9 | || !this.state._embedded
10 | || !this.state._embedded.positions
11 | || !(this.state._embedded.positions.length > 0)) {
12 | return null;
13 | }
14 |
15 | return(
16 |
17 | {this.state._embedded.positions.map((position) =>
18 |
25 | )}
26 |
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/js/Project.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as Utils from './Utils'
3 |
4 | export default class Project extends React.Component {
5 |
6 | render() {
7 | if (!this.props.title) {
8 | return null;
9 | }
10 |
11 | return(
12 |
13 | {this.props.title}
14 |
15 | {Utils.formatMonthYear(this.props.start)}
16 | {Utils.formatMonthYear(this.props.end)}
17 |
18 | {
19 | (this.props.website) ?
20 | {this.props.website}
21 | : ''
22 | }
23 | {this.props.responsibilities}
24 | {this.props.code}
25 |
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/js/Projects.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import BaseComponent from './BaseComponent'
3 | import Project from './Project'
4 |
5 | export default class Projects extends BaseComponent {
6 |
7 | render() {
8 | if (!this.state.success
9 | || !this.state._embedded
10 | || !this.state._embedded.projects
11 | || !(this.state._embedded.projects.length > 0)) {
12 | return null;
13 | }
14 |
15 | return(
16 |
17 |
Projects
18 |
19 | {this.state._embedded.projects.map((project) =>
20 |
27 | )}
28 |
29 |
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/js/University.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default class University extends React.Component {
4 |
5 | render() {
6 | if (!this.props.name) {
7 | return null;
8 | }
9 |
10 | return(
11 |
12 | {this.props.name}
13 | {this.props.degree}
14 |
15 | {new Date(this.props.graduation).getFullYear()}
16 |
17 | {this.props.notes}
18 |
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/js/Utils.js:
--------------------------------------------------------------------------------
1 | import Rest from 'rest'
2 |
3 | /**
4 | * Makes AJAX request to JPA endpoint and sets the response to the state of the React component.
5 | *
6 | * @param url to JPA endpoint.
7 | * @param component to set state.
8 | */
9 | export function api(url, component) {
10 | let model = component,
11 | _url = (url.href) ? url.href : url;
12 |
13 | if (typeof _url !== "string") {
14 | console.warn("Invalid 'url' argument!");
15 | return;
16 | }
17 |
18 | Rest(_url).then(function(response) {
19 | let json = {success: false, loading: false};
20 |
21 | if (response.status.code === 200) {
22 | try {
23 | Object.assign(json, JSON.parse(response.entity), {success: true});
24 | model.setState(json);
25 |
26 | } catch (e) {
27 | console.warn("Failed API request.", e);
28 | console.debug(_url, response, model);
29 | }
30 |
31 | } else {
32 | model.setState(json);
33 | }
34 | });
35 | }
36 |
37 | /**
38 | * Returns a formatted date or empty string.
39 | *
40 | * @param date
41 | */
42 | export function formatMonthYear(date) {
43 | if (date) {
44 | let _date = new Date(date);
45 | return (_date.getMonth() + 1) + '/' + _date.getFullYear();
46 | }
47 | return "";
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/js/theme.js:
--------------------------------------------------------------------------------
1 | import getMuiTheme from 'material-ui/styles/getMuiTheme'
2 | import * as colors from 'material-ui/styles/colors';
3 |
4 | const colorPrimary = colors.lightGreen200,
5 | colorSecondary = colors.lightGreen500,
6 | colorHighlight = colors.lightGreen700,
7 | colorText = colors.grey800,
8 | colorTextAlternate = colors.grey400;
9 |
10 | /* --- MUI theme options ---
11 | * https://github.com/callemall/material-ui/blob/master/src/styles/getMuiTheme.js
12 | */
13 | const customTheme = getMuiTheme({
14 | palette: {
15 | textColor: colorText,
16 | alternateTextColor: colorTextAlternate
17 | },
18 |
19 | flatButton: {
20 | color: colors.grey100,
21 | textColor: colorHighlight
22 | },
23 |
24 | raisedButton: {
25 | primaryColor: colorPrimary,
26 | textColor: colors.white,
27 | primaryTextColor: colors.white
28 | },
29 |
30 | stepper: {
31 | iconColor: colorPrimary
32 | },
33 |
34 | toolbar: {
35 | color: colorSecondary,
36 | backgroundColor: colorPrimary
37 | }
38 | });
39 |
40 | export { customTheme };
41 |
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kluman/spring-boot-full-stack/0cf79d21bf38ab9cdfbc1cc5ff924dc7d249097c/src/main/resources/application.properties
--------------------------------------------------------------------------------
/src/main/resources/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | About {{me.first}} {{me.middle}} {{me.last}}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/main/styles/Address.less:
--------------------------------------------------------------------------------
1 | .Address {
2 | font-size: small;
3 | line-height: 20px;
4 |
5 | [itemprop="streetAddress"], [itemprop="telephone"] {
6 | display: block;
7 | }
8 |
9 | [itemprop="addressRegion"] {
10 | margin: 0 5px;
11 | }
12 | }
--------------------------------------------------------------------------------
/src/main/styles/App.less:
--------------------------------------------------------------------------------
1 |
2 | @color-primary: #C5E1A5;
3 | @color-secondary: #8BC34A;
4 | @color-link: #689F38;
5 | @color-link-visited: #33691E;
6 | @color-white: #fff;
7 | @color-grey: #424242;
8 | @color-light-grey: #cdcdcd;
9 |
10 | @space: 20px;
11 | @space-three-quarter: 15px;
12 | @space-half: 10px;
13 | @space-quarter: 5px;
14 |
15 | @zindex-base: 1;
16 | @zindex-overlay: 10;
17 | @zindex-modal: 20;
18 |
19 | html {
20 | background-color: #fff;
21 | font-family: Roboto, sans-serif;
22 |
23 | &:after {
24 | content: "";
25 | background: url('./images/background.jpg') no-repeat center center fixed; background-size: cover;
26 | opacity: 0.3;
27 | top: 0;
28 | left: 0;
29 | bottom: 0;
30 | right: 0;
31 | position: absolute;
32 | z-index: -1;
33 | }
34 |
35 | a {
36 | text-decoration: none;
37 | color: @color-link;
38 |
39 | &:visited {
40 | color: @color-link-visited;
41 | }
42 | }
43 |
44 | body {
45 | margin: 0;
46 | padding: 0;
47 | }
48 |
49 | footer {
50 | position: fixed;
51 | left: 0;
52 | bottom: 0;
53 | z-index: @zindex-overlay;
54 | width: 100%;
55 | margin-top: @space;
56 | padding: @space;
57 | color: @color-white;
58 | background-color: @color-secondary;
59 | box-shadow: 0 0 1em @color-grey;
60 | }
61 | }
62 |
63 | .App {
64 |
65 | &-info {
66 | padding: @space @space-half;
67 | text-align: center;
68 | }
69 | }
70 |
71 | /* Component Styles */
72 |
73 | @import "Address";
74 | @import "Company";
75 | @import "Education";
76 | @import "Person";
77 | @import "Position";
78 | @import "Project";
79 | @import "University";
--------------------------------------------------------------------------------
/src/main/styles/Company.less:
--------------------------------------------------------------------------------
1 | .Company {
2 | margin-bottom: @space;
3 |
4 | .name {
5 | display: none;
6 | }
7 |
8 | .website {
9 | display: block;
10 | margin: @space-half 0;
11 | font-size: small;
12 | }
13 |
14 | &-name {
15 | display: block;
16 | font-size: large;
17 | }
18 | }
--------------------------------------------------------------------------------
/src/main/styles/Education.less:
--------------------------------------------------------------------------------
1 | .Education {
2 | margin: 0;
3 | padding: 0;
4 | list-style: none;
5 |
6 | .name {
7 | margin: @space 0 @space-quarter 0;
8 | font-size: large;
9 | }
10 |
11 | .degree, .graduation, .notes {
12 | font-size: small;
13 | }
14 |
15 | .degree, .graduation {
16 | font-style: italic;
17 | }
18 |
19 | .degree {
20 | &:after {
21 | content: ', ';
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/main/styles/Person.less:
--------------------------------------------------------------------------------
1 | .Person {
2 | margin-bottom: @space;
3 |
4 | [itemtype="http://schema.org/Person"] {
5 | display: none;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/styles/Position.less:
--------------------------------------------------------------------------------
1 | .Positions {
2 | margin: 0 @space;
3 | padding: 0;
4 | }
5 |
6 | .Position {
7 | list-style-type: none;
8 |
9 | h3 {
10 | margin: @space-three-quarter 0 @space-quarter 0;
11 | font-size: medium;
12 | }
13 |
14 | .dates {
15 | margin-bottom: @space-half;
16 | font-size: small;
17 | font-style: italic;
18 |
19 | .end {
20 | margin-left: @space-half
21 | }
22 | }
23 |
24 | .responsibilities {
25 | font-size: small;
26 | }
27 | }
--------------------------------------------------------------------------------
/src/main/styles/Project.less:
--------------------------------------------------------------------------------
1 | .Projects {
2 | margin-bottom: @space;
3 |
4 | > h4 {
5 | font-size: small;
6 | text-transform: uppercase;
7 | }
8 |
9 | &-list {
10 | padding: 0 0 0 @space-half;
11 | list-style-type: none;
12 | }
13 | }
14 |
15 | .Project {
16 |
17 | h4 {
18 | margin: @space-half 0 @space-quarter 0;
19 | font-size: small;
20 | }
21 |
22 | .dates, .responsiblities {
23 | font-size: small;
24 | }
25 | }
--------------------------------------------------------------------------------
/src/main/styles/University.less:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kluman/spring-boot-full-stack/0cf79d21bf38ab9cdfbc1cc5ff924dc7d249097c/src/main/styles/University.less
--------------------------------------------------------------------------------
/src/main/styles/images/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kluman/spring-boot-full-stack/0cf79d21bf38ab9cdfbc1cc5ff924dc7d249097c/src/main/styles/images/background.jpg
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | // Webpack 2 config options: https://webpack.js.org/configuration/
5 | module.exports = {
6 | context: path.resolve(__dirname, "src/main"),
7 | entry: './js/App.js',
8 | output: {
9 | path: __dirname,
10 | filename: './src/main/resources/static/build/bundle.js'
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.(js|jsx)$/,
16 | exclude: /(node_modules|bower_components)/,
17 | use: [
18 | {
19 | loader: "babel-loader",
20 | options: {
21 | cacheDirectory: false,
22 | presets: ['es2015', 'react']
23 | }
24 | }
25 | ]
26 | },
27 | {
28 | test: /\.less$/,
29 | use: [
30 | {
31 | loader: "style-loader"
32 | },
33 | {
34 | loader: "css-loader"
35 | },
36 | {
37 | loader: "less-loader"
38 | }
39 | ]
40 | },
41 | {
42 | test: /\.(jpg|gif|png)$/,
43 | use: [
44 | {
45 | loader: "url-loader"
46 | }
47 | ]
48 | }
49 | ]
50 | }
51 | };
52 |
--------------------------------------------------------------------------------