├── .gitignore ├── README.md ├── _config.yml ├── package.json ├── pom.xml ├── src └── main │ ├── java │ └── com │ │ └── example │ │ └── about │ │ ├── AboutLoader.java │ │ ├── Application.java │ │ ├── domain │ │ ├── Address.java │ │ ├── AddressRepository.java │ │ ├── Company.java │ │ ├── CompanyRepository.java │ │ ├── Link.java │ │ ├── LinkRepository.java │ │ ├── Person.java │ │ ├── PersonRepository.java │ │ ├── Position.java │ │ ├── PositionRepository.java │ │ ├── Project.java │ │ ├── ProjectRepository.java │ │ ├── University.java │ │ └── UniversityRepository.java │ │ └── web │ │ ├── AboutController.java │ │ └── WebConfiguration.java │ ├── js │ ├── Address.js │ ├── App.js │ ├── BaseComponent.js │ ├── Company.js │ ├── Education.js │ ├── Employment.js │ ├── Person.js │ ├── Position.js │ ├── Positions.js │ ├── Project.js │ ├── Projects.js │ ├── University.js │ ├── Utils.js │ └── theme.js │ ├── resources │ ├── application.properties │ └── templates │ │ └── index.html │ └── styles │ ├── Address.less │ ├── App.less │ ├── Company.less │ ├── Education.less │ ├── Person.less │ ├── Position.less │ ├── Project.less │ ├── University.less │ └── images │ └── background.jpg └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Node Modules 4 | node_modules/ 5 | 6 | # IntelliJ 7 | *.iml 8 | *.idea 9 | 10 | # Maven Build 11 | /target 12 | 13 | # Build Resources (JS/CSS) 14 | src/main/resources/static/build* 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full stack development with Spring Boot, React, Material-UI and Webpack 2 | 3 | This project demonstrates a full-stack implementation using [Spring Boot](https://projects.spring.io/spring-boot/) 4 | to handle the backend wired together with [React](https://facebook.github.io/react/) and 5 | [Material-UI](https://github.com/callemall/material-ui) for the frontend rendering. 6 | While there are great resources out there for each of these components, more than 7 | I would ever attempt to document, I found it difficult to find them all put together 8 | in an non-trivial way. 9 | 10 | While this project is in no way what I would consider "production quality" it does 11 | try to demonstrate more than just the simplistic "hello world" examples. Hopefully you 12 | will find it of use in getting started with these great Web development platform/frameworks. 13 | 14 | ## Installation 15 | 16 | ### Dependencies 17 | 18 | 1. [Git](https://git-scm.com/downloads), of course, installed on your local machine. 19 | 2. [Maven](https://maven.apache.org/) to compile and run the project. 20 | 21 | Assuming you have Git and Maven on your local machine you will run the following commands. On the terminal of your 22 | choice change directories to where you want the cloned project files to download and run: 23 | 24 | ``` 25 | git clone https://github.com/kluman/about.git 26 | ``` 27 | Since we are using Maven to run/build this project you will execute the following at the project root. 28 | 29 | ``` 30 | mvn spring-boot:run 31 | ``` 32 | This will compile and startup a Tomcat server on your localhost. In your Web browser go to http://localhost:8080/ 33 | and check it out. 34 | 35 | 36 | ## Backend 37 | 38 | ### Spring Boot 39 | 40 | As mentioned [Spring Boot](https://projects.spring.io/spring-boot/) is the foundation 41 | for the server side. An H2 (in memory Java-based) SQL database is used as the data store. The 42 | choice of H2 was solely for simplicity of the demo, since no installation is required. 43 | Also included is the Spring implementation of the [Java Persistence API](http://projects.spring.io/spring-data-jpa/) 44 | (JPA). This allows for easy DB access using annotations and `*Repository` interfaces. 45 | Rounding off the backend is the REST [Spring Boot 46 | Starter](http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-starter) to 47 | support the RESTful APIs our React Javascript is going to call. You can read more about Spring 48 | Boot Starters and get a list of the many that are provided at the link provided. In short though, 49 | as stated: 50 | 51 | >Spring Boot Starters are a set of convenient dependency descriptors that you can include 52 | in your application. You get a one-stop-shop for all the Spring and related technology that you 53 | need without having to hunt through sample code and copy paste loads of dependency descriptors. 54 | 55 | Simply add the Starter you want as a dependency in your POM file and that's it, you're ready to roll. 56 | 57 | There are four used here. 58 | 59 | |Name | Description 60 | -------------------------------|---------------------------- 61 | spring-boot-starter-data-rest | Starter for exposing Spring Data repositories over REST using Spring Data REST. | 62 | spring-boot-starter-data-jpa | Starter for using Spring Data JPA with Hibernate. 63 | spring-boot-starter-web | Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container. 64 | spring-boot-starter-mustache | Starter for building MVC web applications using Mustache views. 65 | 66 | 67 | #### Structure 68 | 69 | ``` 70 | About Project Structure 71 | ------------------------------------------------------------------------ 72 | 73 | + 74 | - pom.xml 75 | - package.json 76 | - webpack.config.js 77 | 78 | + src/main/java/com.example.about 79 | - AboutLoader 80 | - Application 81 | 82 | + domain 83 | - Address 84 | - AddressRepository 85 | - Company 86 | - CompanyRepository 87 | - ... 88 | 89 | + web 90 | - AboutController 91 | - WebConfiguration 92 | 93 | + js 94 | - Address.js 95 | - Company.js 96 | - .... 97 | 98 | + resources/templates 99 | - index.html 100 | 101 | + styles 102 | - Address.less 103 | - Company.less 104 | - ... 105 | ``` 106 | 107 | 108 | The models and their corresponding `*Repository` interfaces are all contained within 109 | the "domain" package. Models are simple POJOs with various Spring JPA annotations. Basically, 110 | every field in your POJO is in turn tied to a column in the DB. It's that easy! 111 | 112 | Each model POJO also contains a `Builder` static nested class 113 | following a builder pattern. None of these `Builder` classes are required and 114 | would probably not exist in a real world example, but it made populating the H2 database 115 | a heck of a lot easier. More on that later. 116 | 117 | #### MVC 118 | 119 | We are following the typical layout specified in the [Spring Boot Docs](http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-locating-the-main-class) 120 | with an `Application` class on the root package using a `@SpringBootApplication` annotation to 121 | explicitly identify our main application class. This class does two things of interest. First it kicks off our Spring 122 | Boot app and second imports our `WebConfiguration` class. 123 | 124 | The `WebConfiguration` is also very simple doing one thing. By extending the `RepositoryRestConfigurerAdapter` 125 | class and overriding its `configureRepositoryRestConfiguration` method it routes all of the RESTful API 126 | endpoints through the "/api" path. 127 | 128 | ``` 129 | http://localhost:8080/api/persons 130 | ``` 131 | 132 | There is a single controller class in the project, `AboutController`, that routes requests to the root path to the 133 | "index.html" page. Which is actually our one Mustache-templated page. It also looks for the first `Person` in our 134 | database and puts it as the model for Mustache to use. 135 | 136 | #### Database Population 137 | 138 | With Spring Boot there is no need for reading and loading in a SQL file, we can do everything we need to populate 139 | or H2 database - right in Java. Spring has various events that you can hook into and the `AboutLoader` class takes 140 | advantage of `ContextRefreshedEvent` that is triggered when the `ApplicationContext` gets initialized or refreshed. 141 | 142 | This class does a couple things. First it used the `AutoWired` annotation to inject in instances of our `*Repository` JPA 143 | classes and set them to corresponding fields. The second is overrdiding the `onApplicationEvent` method and it is 144 | in this method that we use all of those `Builder` static nested classes to populate the database with content. 145 | 146 | ## Frontend 147 | 148 | The frontend is built using React and Material-UI, which is a set of React components following Google's Material 149 | Design. The React components call the JPA fed REST endpoints to populate themselves. CSS is generated at build time 150 | from the LESS files in the "styles" directory. 151 | 152 | [Webpack2](https://webpack.js.org/guides/get-started/) is used to build and bundle up our Javascript as well as 153 | compiling our LESS files into CSS and injecting into the HTML `head` element. The POM file also contains a 154 | plugin that will install Node, Node Package Manager (NPM) and execute the Webpack build. 155 | 156 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spring-boot-jpa-mustache-reactjs", 3 | "version": "0.1.0", 4 | "description": "Spring Boot using JPA, Mustache and ReactJS", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:kluman/about.git" 8 | }, 9 | "author": "Kevin Luman", 10 | "license": "JSON", 11 | "dependencies": { 12 | "material-ui": "^0.17.1", 13 | "react": "^15.4.2", 14 | "react-dom": "^15.4.2", 15 | "react-router": "^4.0.0", 16 | "rest": "^2.0.0", 17 | "react-tap-event-plugin": "^2.0.1" 18 | }, 19 | "devDependencies": { 20 | "babel-core": "^6.24.0", 21 | "babel-loader": "^6.4.1", 22 | "babel-polyfill": "^6.23.0", 23 | "babel-preset-es2015": "^6.24.0", 24 | "babel-preset-react": "^6.23.0", 25 | "css-loader": "^0.23.1", 26 | "url-loader": "^0.5.8", 27 | "less": "^2.7.2", 28 | "less-loader": "^4.0.2", 29 | "style-loader": "^0.16.1", 30 | "npm-install-webpack-plugin": "^4.0.4", 31 | "webpack": "^2.2.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | about 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 1.5.2.RELEASE 15 | 16 | 17 | 18 | UTF-8 19 | 1.8 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-rest 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-jpa 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-mustache 41 | 42 | 43 | 44 | com.h2database 45 | h2 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-devtools 51 | true 52 | 53 | 54 | 55 | org.springframework 56 | springloaded 57 | 1.2.6.RELEASE 58 | 59 | 60 | 61 | com.google.guava 62 | guava 63 | 21.0 64 | 65 | 66 | 67 | 68 | 69 | 70 | org.springframework.boot 71 | spring-boot-maven-plugin 72 | 73 | 74 | 75 | com.github.eirslett 76 | frontend-maven-plugin 77 | 1.2 78 | 79 | target 80 | 81 | 82 | 83 | install node and npm 84 | 85 | install-node-and-npm 86 | 87 | 88 | v4.4.5 89 | 3.9.2 90 | 91 | 92 | 93 | npm install 94 | 95 | npm 96 | 97 | 98 | install 99 | 100 | 101 | 102 | webpack build 103 | 104 | webpack 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | spring-snapshots 115 | http://repo.spring.io/snapshot 116 | 117 | 118 | spring-milestones 119 | http://repo.spring.io/milestone 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/AboutLoader.java: -------------------------------------------------------------------------------- 1 | package com.example.about; 2 | 3 | import java.time.Instant; 4 | import java.util.Date; 5 | 6 | import com.example.about.domain.Address; 7 | import com.example.about.domain.AddressRepository; 8 | import com.example.about.domain.Company; 9 | import com.example.about.domain.CompanyRepository; 10 | import com.example.about.domain.Person; 11 | import com.example.about.domain.PersonRepository; 12 | import com.example.about.domain.Position; 13 | import com.example.about.domain.PositionRepository; 14 | import com.example.about.domain.Project; 15 | import com.example.about.domain.ProjectRepository; 16 | import com.example.about.domain.University; 17 | import com.example.about.domain.UniversityRepository; 18 | import com.google.common.collect.ImmutableList; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.context.ApplicationListener; 21 | import org.springframework.context.event.ContextRefreshedEvent; 22 | import org.springframework.stereotype.Component; 23 | 24 | /** 25 | * Preload database with information. 26 | */ 27 | @Component 28 | public class AboutLoader implements ApplicationListener { 29 | 30 | private UniversityRepository universityRepository; 31 | private CompanyRepository companyRepository; 32 | private PersonRepository personRepository; 33 | private ProjectRepository projectRepository; 34 | private PositionRepository positionRepository; 35 | private AddressRepository addressRepository; 36 | 37 | @Autowired 38 | public void setUniversityRepository(UniversityRepository universityRepository) { 39 | this.universityRepository = universityRepository; 40 | } 41 | 42 | @Autowired 43 | public void setCompanyRepository(CompanyRepository companyRepository) { 44 | this.companyRepository = companyRepository; 45 | } 46 | 47 | @Autowired 48 | public void setPersonRepository(PersonRepository personRepository) { 49 | this.personRepository = personRepository; 50 | } 51 | 52 | @Autowired 53 | public void setProjectRepository(ProjectRepository projectRepository) { 54 | this.projectRepository = projectRepository; 55 | } 56 | 57 | @Autowired 58 | public void setPositionRepository(PositionRepository positionRepository) { 59 | this.positionRepository = positionRepository; 60 | } 61 | 62 | @Autowired 63 | public void setAddressRepository(AddressRepository addressRepository) { 64 | this.addressRepository = addressRepository; 65 | } 66 | 67 | @Override 68 | public void onApplicationEvent(ContextRefreshedEvent event) { 69 | 70 | new Person.Builder() 71 | .first("Kevin") 72 | .last("Luman") 73 | .middle("L.") 74 | .email("kevinleeluman@gmail.com") 75 | .address(new Address.Builder() 76 | .setAddress("1234 Main St.") 77 | .setCity("Ashburn") 78 | .setRegion("VA") 79 | .setPostalCode("20147") 80 | .setPhone("(703) 555-0000") 81 | .build(addressRepository)) 82 | .education(new ImmutableList.Builder() 83 | .add(new University.Builder() 84 | .degree("Government & Politics") 85 | .name("George Mason University") 86 | .notes("Graduated with High Honors.") 87 | .graduation(Date.from(Instant.ofEpochSecond(833635669))) 88 | .build(universityRepository)) 89 | .build()) 90 | .employment(new ImmutableList.Builder() 91 | .add(new Company.Builder() 92 | .name("Pefect Sense") 93 | .website("http://www.perfectsensedigital.com") 94 | .address(new Address.Builder() 95 | .setAddress("12120 Sunset Hills Rd") 96 | .setCity("Reston") 97 | .setRegion("VA") 98 | .setPostalCode("20190") 99 | .setPhone("(703) 956-5850") 100 | .build(addressRepository)) 101 | .positions(new ImmutableList.Builder() 102 | .add(new Position.Builder() 103 | .title("Principle Software Engineer") 104 | .start(Date.from(Instant.ofEpochSecond(1271725446))) 105 | .projects(new ImmutableList.Builder() 106 | .add(new Project.Builder() 107 | .title("Politico") 108 | .responsibilities("Backend Java & JSP development. Custom Adobe InDesign plugin to allow bi-directional updating between Brightspot CMS and Adobe InDesign.") 109 | .website("www.politico.com") 110 | .build(projectRepository)) 111 | .add(new Project.Builder() 112 | .title("Univision") 113 | .responsibilities("Backend Java (+Freemarker) and Frontend (CSS, Javascript, HTML) development.") 114 | .website("www.univision.com") 115 | .build(projectRepository)) 116 | .build()) 117 | .build(positionRepository)) 118 | .build()) 119 | .build(companyRepository)) 120 | .add(new Company.Builder() 121 | .name("Aol") 122 | .website("http://www.aol.com") 123 | .address(new Address.Builder() 124 | .setAddress("22000 Aol Way") 125 | .setCity("Dulles") 126 | .setRegion("VA") 127 | .setPostalCode("20166") 128 | .setPhone("(703) 265-2100") 129 | .build(addressRepository)) 130 | .positions(new ImmutableList.Builder() 131 | .add(new Position.Builder() 132 | .title("Technology Manager, Software Engineering") 133 | .start(Date.from(Instant.ofEpochSecond(1086120718))) 134 | .end(Date.from(Instant.ofEpochSecond(1270152718))) 135 | .responsibilities("Managed a team of up to 8 engineers working on AOL's main portal page, AOL.com.") 136 | .build(positionRepository)) 137 | .add(new Position.Builder() 138 | .title("Software Engineer") 139 | .start(Date.from(Instant.ofEpochSecond(959890318))) 140 | .end(Date.from(Instant.ofEpochSecond(1086120718))) 141 | .responsibilities("Software engineer working on various Web products such as Yellow Pages (yp.aol.com), My Locations, Netscape.com among others.") 142 | .build(positionRepository)) 143 | .build()) 144 | .build(companyRepository)) 145 | .build()) 146 | .build(personRepository); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/Application.java: -------------------------------------------------------------------------------- 1 | package com.example.about; 2 | 3 | import com.example.about.web.WebConfiguration; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Import; 10 | 11 | @Configuration 12 | @EnableAutoConfiguration 13 | @ComponentScan 14 | @SpringBootApplication 15 | @Import(WebConfiguration.class) 16 | public class Application { 17 | 18 | public static void main(String[] args) { 19 | SpringApplication.run(Application.class, args); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/Address.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.GenerationType; 6 | import javax.persistence.Id; 7 | 8 | @Entity 9 | public class Address { 10 | 11 | @Id 12 | @GeneratedValue(strategy = GenerationType.AUTO) 13 | private long id; 14 | 15 | private final String address; 16 | 17 | private final String city; 18 | 19 | private final String region; 20 | 21 | private final String postalCode; 22 | 23 | private final String phone; 24 | 25 | public Address(Builder builder) { 26 | this.address = builder.address; 27 | this.city = builder.city; 28 | this.region = builder.region; 29 | this.postalCode = builder.postalCode; 30 | this.phone = builder.phone; 31 | } 32 | 33 | // Spring Boot JPA needs the default constructor so stub out with null values. 34 | public Address() { 35 | this.address = null; 36 | this.city = null; 37 | this.region = null; 38 | this.postalCode = null; 39 | this.phone = null; 40 | } 41 | 42 | public String getAddress() { 43 | return address; 44 | } 45 | 46 | public String getCity() { 47 | return city; 48 | } 49 | 50 | public String getRegion() { 51 | return region; 52 | } 53 | 54 | public String getPhone() { 55 | return phone; 56 | } 57 | 58 | public String getPostalCode() { 59 | return postalCode; 60 | } 61 | 62 | public static class Builder { 63 | 64 | private String address; 65 | 66 | private String city; 67 | 68 | private String region; 69 | 70 | private String postalCode; 71 | 72 | private String phone; 73 | 74 | public Address build(AddressRepository repository) { 75 | Address address = new Address(this); 76 | 77 | if (repository != null) { 78 | repository.save(address); 79 | } 80 | 81 | return address; 82 | } 83 | 84 | public Builder setAddress(String address) { 85 | this.address = address; 86 | return this; 87 | } 88 | 89 | public Builder setCity(String city) { 90 | this.city = city; 91 | return this; 92 | } 93 | 94 | public Builder setRegion(String region) { 95 | this.region = region; 96 | return this; 97 | } 98 | 99 | public Builder setPostalCode(String postalCode) { 100 | this.postalCode = postalCode; 101 | return this; 102 | } 103 | 104 | public Builder setPhone(String phone) { 105 | this.phone = phone; 106 | return this; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/AddressRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | 6 | @RepositoryRestResource() 7 | public interface AddressRepository extends CrudRepository { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/Company.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.OneToMany; 10 | import javax.persistence.OneToOne; 11 | 12 | @Entity 13 | public class Company { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.AUTO) 17 | private long id; 18 | 19 | private final String name; 20 | 21 | @OneToOne 22 | private final Address address; 23 | 24 | private final String website; 25 | 26 | @OneToMany 27 | private final List positions; 28 | 29 | public Company(Builder builder) { 30 | this.name = builder.name; 31 | this.address = builder.address; 32 | this.website = builder.website; 33 | this.positions = builder.positions; 34 | } 35 | 36 | // Spring Boot JPA needs the default constructor so stub out with null values. 37 | public Company() { 38 | this.name = null; 39 | this.address = null; 40 | this.website = null; 41 | this.positions = null; 42 | } 43 | 44 | public String getName() { 45 | return name; 46 | } 47 | 48 | public Address getAddress() { 49 | return address; 50 | } 51 | 52 | public String getWebsite() { 53 | return website; 54 | } 55 | 56 | public List getPositions() { 57 | if (positions == null) { 58 | return new ArrayList<>(); 59 | } 60 | return positions; 61 | } 62 | 63 | public static class Builder { 64 | 65 | private String name; 66 | 67 | private Address address; 68 | 69 | private String website; 70 | 71 | private List positions; 72 | 73 | public Company build(CompanyRepository repository) { 74 | Company company = new Company(this); 75 | 76 | if (repository != null) { 77 | repository.save(company); 78 | } 79 | 80 | return company; 81 | } 82 | 83 | public Builder name(String name) { 84 | this.name = name; 85 | return this; 86 | } 87 | 88 | public Builder address(Address address) { 89 | this.address = address; 90 | return this; 91 | } 92 | 93 | public Builder website(String website) { 94 | this.website = website; 95 | return this; 96 | } 97 | 98 | public Builder positions(List positions) { 99 | this.positions = positions; 100 | return this; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/CompanyRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import com.example.about.domain.Company; 5 | 6 | public interface CompanyRepository extends CrudRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/Link.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.GenerationType; 6 | import javax.persistence.Id; 7 | 8 | @Entity 9 | public class Link { 10 | 11 | @Id 12 | @GeneratedValue(strategy = GenerationType.AUTO) 13 | private long id; 14 | 15 | private final String body; 16 | 17 | private final String href; 18 | 19 | private final Target target; 20 | 21 | public Link(Builder builder) { 22 | this.body = builder.body; 23 | this.href = builder.href; 24 | this.target = builder.target; 25 | } 26 | 27 | // Spring Boot JPA needs the default constructor so stub out with null values. 28 | public Link() { 29 | this.body = null; 30 | this.href = null; 31 | this.target = null; 32 | } 33 | 34 | public String getBody() { 35 | return body; 36 | } 37 | 38 | public String getHref() { 39 | return href; 40 | } 41 | 42 | public Target getTarget() { 43 | return target; 44 | } 45 | 46 | public static class Builder { 47 | 48 | private String body; 49 | 50 | private String href; 51 | 52 | private Target target; 53 | 54 | public Builder body(String body) { 55 | this.body = body; 56 | return this; 57 | } 58 | 59 | public Builder href(String href) { 60 | this.href = href; 61 | return this; 62 | } 63 | 64 | public Builder target(Target target) { 65 | this.target = target; 66 | return this; 67 | } 68 | } 69 | 70 | public enum Target { 71 | TOP("_top"), 72 | BLANK("_blank"), 73 | SELF("_self"); 74 | 75 | private String attribute; 76 | 77 | Target(String attribute) { 78 | this.attribute = attribute; 79 | } 80 | 81 | public String attribute() { 82 | return attribute; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/LinkRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | public interface LinkRepository extends CrudRepository { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/Person.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.OneToMany; 10 | import javax.persistence.OneToOne; 11 | 12 | @Entity 13 | public class Person { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.AUTO) 17 | private long id; 18 | 19 | private final String first; 20 | 21 | private final String middle; 22 | 23 | private final String last; 24 | 25 | @OneToOne 26 | private final Address address; 27 | 28 | private final String email; 29 | 30 | @OneToMany 31 | private final List links; 32 | 33 | @OneToMany 34 | private final List employment; 35 | 36 | @OneToMany 37 | private final List education; 38 | 39 | public Person(Builder builder) { 40 | this.first = builder.first; 41 | this.middle = builder.middle; 42 | this.last = builder.last; 43 | this.address = builder.address; 44 | this.email = builder.email; 45 | this.links = builder.links; 46 | this.employment = builder.employment; 47 | this.education = builder.education; 48 | } 49 | 50 | // Spring Boot JPA needs the default constructor so stub out with null values. 51 | public Person() { 52 | this.first = null; 53 | this.middle = null; 54 | this.last = null; 55 | this.address = null; 56 | this.email = null; 57 | this.links = null; 58 | this.employment = null; 59 | this.education = null; 60 | } 61 | 62 | public String getFirst() { 63 | return first; 64 | } 65 | 66 | public String getMiddle() { 67 | return middle; 68 | } 69 | 70 | public String getLast() { 71 | return last; 72 | } 73 | 74 | public Address getAddress() { 75 | return address; 76 | } 77 | 78 | public String getEmail() { 79 | return email; 80 | } 81 | 82 | public List getLinks() { 83 | if (links == null) { 84 | return new ArrayList<>(); 85 | } 86 | return links; 87 | } 88 | 89 | public List getEmployment() { 90 | if (employment == null) { 91 | return new ArrayList<>(); 92 | } 93 | return employment; 94 | } 95 | 96 | public List getEducation() { 97 | if (education == null) { 98 | new ArrayList<>(); 99 | } 100 | return education; 101 | } 102 | 103 | public String getFullName() { 104 | StringBuilder fullName = new StringBuilder(); 105 | fullName.append(first).append(" "); 106 | if (middle != null) { 107 | fullName.append(middle).append(" "); 108 | } 109 | fullName.append(last); 110 | 111 | return fullName.toString(); 112 | } 113 | 114 | public static class Builder { 115 | 116 | private String first; 117 | 118 | private String middle; 119 | 120 | private String last; 121 | 122 | private Address address; 123 | 124 | private String homePhone; 125 | 126 | private String mobilePhone; 127 | 128 | private String email; 129 | 130 | private List links; 131 | 132 | private List employment; 133 | 134 | private List education; 135 | 136 | public Person build(PersonRepository repository) { 137 | Person person = new Person(this); 138 | 139 | if (repository != null) { 140 | repository.save(person); 141 | } 142 | 143 | return person; 144 | } 145 | 146 | public Builder first(String first) { 147 | this.first = first; 148 | return this; 149 | } 150 | 151 | public Builder middle(String middle) { 152 | this.middle = middle; 153 | return this; 154 | } 155 | 156 | public Builder last(String last) { 157 | this.last = last; 158 | return this; 159 | } 160 | 161 | public Builder address(Address address) { 162 | this.address = address; 163 | return this; 164 | } 165 | 166 | public Builder homePhone(String homePhone) { 167 | this.homePhone = homePhone; 168 | return this; 169 | } 170 | 171 | public Builder mobilePhone(String mobilePhone) { 172 | this.mobilePhone = mobilePhone; 173 | return this; 174 | } 175 | 176 | public Builder email(String email) { 177 | this.email = email; 178 | return this; 179 | } 180 | 181 | public Builder links(List links) { 182 | this.links = links; 183 | return this; 184 | } 185 | 186 | public Builder employment(List employment) { 187 | this.employment = employment; 188 | return this; 189 | } 190 | 191 | public Builder education(List education) { 192 | this.education = education; 193 | return this; 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/PersonRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | public interface PersonRepository extends CrudRepository { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/Position.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Date; 5 | import java.util.List; 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.OneToMany; 11 | 12 | @Entity 13 | public class Position { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.AUTO) 17 | private long id; 18 | 19 | private final String title; 20 | 21 | private final Date start; 22 | 23 | private final Date end; 24 | 25 | private final String responsibilities; 26 | 27 | @OneToMany 28 | private final List projects; 29 | 30 | public Position(Builder builder) { 31 | this.title = builder.title; 32 | this.start = builder.start; 33 | this.end = builder.end; 34 | this.responsibilities = builder.responsibilities; 35 | this.projects = builder.projects; 36 | } 37 | 38 | // Spring Boot JPA needs the default constructor so stub out with null values. 39 | public Position() { 40 | this.title = null; 41 | this.start = null; 42 | this.end = null; 43 | this.responsibilities = null; 44 | this.projects = null; 45 | } 46 | 47 | public String getTitle() { 48 | return title; 49 | } 50 | 51 | public Date getStart() { 52 | return start; 53 | } 54 | 55 | public Date getEnd() { 56 | return end; 57 | } 58 | 59 | public String getResponsibilities() { 60 | return responsibilities; 61 | } 62 | 63 | public List getProjects() { 64 | if (projects == null) { 65 | return new ArrayList<>(); 66 | } 67 | return projects; 68 | } 69 | 70 | public static class Builder { 71 | 72 | private String title; 73 | 74 | private Date start; 75 | 76 | private Date end; 77 | 78 | private String responsibilities; 79 | 80 | private List projects; 81 | 82 | public Position build(PositionRepository repository) { 83 | Position position = new Position(this); 84 | 85 | if (repository != null) { 86 | repository.save(position); 87 | } 88 | 89 | return position; 90 | } 91 | 92 | public Builder title(String title) { 93 | this.title = title; 94 | return this; 95 | } 96 | 97 | public Builder start(Date start) { 98 | this.start = start; 99 | return this; 100 | } 101 | 102 | public Builder end(Date end) { 103 | this.end = end; 104 | return this; 105 | } 106 | 107 | public Builder responsibilities(String responsibilities) { 108 | this.responsibilities = responsibilities; 109 | return this; 110 | } 111 | 112 | public Builder projects(List projects) { 113 | this.projects = projects; 114 | return this; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/PositionRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import com.example.about.domain.Position; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | public interface PositionRepository extends CrudRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/Project.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Date; 5 | import java.util.List; 6 | import javax.persistence.ElementCollection; 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | 12 | @Entity 13 | public class Project { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.AUTO) 17 | private long id; 18 | 19 | private final String title; 20 | 21 | private final String website; 22 | 23 | private final Date start; 24 | 25 | private final Date end; 26 | 27 | private final String responsibilities; 28 | 29 | @ElementCollection 30 | private final List code; 31 | 32 | public Project(Builder builder) { 33 | this.title = builder.title; 34 | this.website = builder.website; 35 | this.start = builder.start; 36 | this.end = builder.end; 37 | this.responsibilities = builder.responsibilities; 38 | this.code = builder.code; 39 | } 40 | 41 | // Spring Boot JPA needs the default constructor so stub out with null values. 42 | public Project() { 43 | this.title = null; 44 | this.website = null; 45 | this.start = null; 46 | this.end = null; 47 | this.responsibilities = null; 48 | this.code = null; 49 | } 50 | 51 | public String getTitle() { 52 | return title; 53 | } 54 | 55 | public String getWebsite() { 56 | return website; 57 | } 58 | 59 | public Date getStart() { 60 | return start; 61 | } 62 | 63 | public Date getEnd() { 64 | return end; 65 | } 66 | 67 | public String getResponsibilities() { 68 | return responsibilities; 69 | } 70 | 71 | public List getCode() { 72 | if (code == null) { 73 | return new ArrayList<>(); 74 | } 75 | return code; 76 | } 77 | 78 | public static class Builder { 79 | 80 | private String title; 81 | 82 | private String website; 83 | 84 | private Date start; 85 | 86 | private Date end; 87 | 88 | private String responsibilities; 89 | 90 | private List code; 91 | 92 | public Project build(ProjectRepository repository) { 93 | Project project = new Project(this); 94 | 95 | if (repository != null) { 96 | repository.save(project); 97 | } 98 | 99 | return project; 100 | } 101 | 102 | public Builder title(String title) { 103 | this.title = title; 104 | return this; 105 | } 106 | 107 | public Builder website(String website) { 108 | this.website = website; 109 | return this; 110 | } 111 | 112 | public Builder start(Date start) { 113 | this.start = start; 114 | return this; 115 | } 116 | 117 | public Builder end(Date end) { 118 | this.end = end; 119 | return this; 120 | } 121 | 122 | public Builder responsibilities(String responsibilities) { 123 | this.responsibilities = responsibilities; 124 | return this; 125 | } 126 | 127 | public Builder code(List code) { 128 | this.code = code; 129 | return this; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/ProjectRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import com.example.about.domain.Project; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | public interface ProjectRepository extends CrudRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/University.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import java.util.Date; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | 9 | @Entity 10 | public class University { 11 | 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.AUTO) 14 | private long id; 15 | 16 | private final String name; 17 | 18 | private final Date graduation; 19 | 20 | private final String degree; 21 | 22 | private final String notes; 23 | 24 | public University(Builder builder) { 25 | this.name = builder.name; 26 | this.graduation = builder.graduation; 27 | this.degree = builder.degree; 28 | this.notes = builder.notes; 29 | } 30 | 31 | // Spring Boot JPA needs the default constructor so stub out with null values. 32 | public University() { 33 | this.name = null; 34 | this.graduation = null; 35 | this.degree = null; 36 | this.notes = null; 37 | } 38 | 39 | public String getName() { 40 | return name; 41 | } 42 | 43 | public Date getGraduation() { 44 | return graduation; 45 | } 46 | 47 | public String getDegree() { 48 | return degree; 49 | } 50 | 51 | public String getNotes() { 52 | return notes; 53 | } 54 | 55 | public static class Builder { 56 | 57 | private String name; 58 | 59 | private Date graduation; 60 | 61 | private String degree; 62 | 63 | private String notes; 64 | 65 | public University build(UniversityRepository repository) { 66 | University university = new University(this); 67 | 68 | if (repository != null) { 69 | repository.save(university); 70 | } 71 | 72 | return university; 73 | } 74 | 75 | public Builder name(String name) { 76 | this.name = name; 77 | return this; 78 | } 79 | 80 | public Builder graduation(Date graduation) { 81 | this.graduation = graduation; 82 | return this; 83 | } 84 | 85 | public Builder degree(String degree) { 86 | this.degree = degree; 87 | return this; 88 | } 89 | 90 | public Builder notes(String notes) { 91 | this.notes = notes; 92 | return this; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/domain/UniversityRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.about.domain; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | 6 | @RepositoryRestResource 7 | public interface UniversityRepository extends CrudRepository { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/example/about/web/AboutController.java: -------------------------------------------------------------------------------- 1 | package com.example.about.web; 2 | 3 | import java.util.Map; 4 | 5 | import com.example.about.domain.Person; 6 | import com.example.about.domain.PersonRepository; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | 11 | @Controller 12 | public class AboutController { 13 | 14 | private PersonRepository personRepository; 15 | 16 | @Autowired 17 | public void setPersonRepository(PersonRepository personRepository) { 18 | this.personRepository = personRepository; 19 | } 20 | 21 | @GetMapping("/") 22 | public String about(Map 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 |
57 | 59 |
60 | 61 |
62 | 63 | 64 |
65 |

Spring Boot, React and Material-UI

66 |
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 |
14 |

{this.props.name}

15 | {this.props.website} 16 |
17 | 18 |
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 | --------------------------------------------------------------------------------