├── .gitignore ├── README.md ├── backend ├── api │ ├── build.gradle.kts │ ├── gradle.properties │ └── src │ │ └── main │ │ ├── kotlin │ │ └── com │ │ │ └── packtpub │ │ │ ├── Config.kt │ │ │ ├── Model.kt │ │ │ ├── handler │ │ │ ├── ApiHandler.kt │ │ │ ├── ExceptionHandler.kt │ │ │ └── ViewHandler.kt │ │ │ ├── route │ │ │ ├── ApiRoutes.kt │ │ │ └── ViewRoutes.kt │ │ │ ├── util │ │ │ ├── Extensions.kt │ │ │ └── Logging.kt │ │ │ └── views │ │ │ └── ViewProvider.kt │ │ └── resources │ │ └── static │ │ ├── css │ │ └── spinner.css │ │ └── js │ │ └── hello.js ├── build.gradle.kts ├── gradle.properties ├── project │ ├── build.gradle.kts │ ├── gradle.properties │ └── src │ │ └── main │ │ └── kotlin │ │ └── com │ │ └── packtpub │ │ ├── Model.kt │ │ ├── ProjectConfig.kt │ │ ├── ProjectRepository.kt │ │ └── ProjectService.kt └── user │ ├── build.gradle.kts │ └── src │ └── main │ └── kotlin │ └── com │ └── packtpub │ ├── Model.kt │ ├── SecurityService.kt │ ├── UserConfig.kt │ ├── UserRepository.kt │ └── UserService.kt ├── build.gradle.kts ├── config └── application.properties ├── docs └── Server-Setup.md ├── frontend ├── build.gradle.kts ├── gradle.properties ├── src │ └── main │ │ ├── kotlin │ │ ├── com │ │ │ └── packtpub │ │ │ │ ├── components │ │ │ │ ├── ProjectList.kt │ │ │ │ ├── RouterComponent.kt │ │ │ │ ├── Spinner.kt │ │ │ │ └── form │ │ │ │ │ ├── Form.kt │ │ │ │ │ └── FormComponents.kt │ │ │ │ ├── index.kt │ │ │ │ ├── model │ │ │ │ └── Model.kt │ │ │ │ ├── store │ │ │ │ ├── ActionCreators.kt │ │ │ │ ├── Actions.kt │ │ │ │ ├── MainReducer.kt │ │ │ │ └── ReduxStore.kt │ │ │ │ └── util │ │ │ │ ├── XHR.kt │ │ │ │ ├── coroutines.kt │ │ │ │ └── util.kt │ │ ├── react │ │ │ ├── Helpers.kt │ │ │ ├── Imports.kt │ │ │ ├── ReactBuilder.kt │ │ │ ├── ReactComponent.kt │ │ │ ├── ReactExtensions.kt │ │ │ ├── ReactExternalComponent.kt │ │ │ ├── ReactWrapper.kt │ │ │ └── dom │ │ │ │ ├── ReactDOM.kt │ │ │ │ ├── ReactDOMAttributes.kt │ │ │ │ ├── ReactDOMBuilder.kt │ │ │ │ └── ReactDOMComponent.kt │ │ └── redux │ │ │ ├── Provider.kt │ │ │ ├── ReactReduxWrappers.kt │ │ │ ├── ReduxAction.kt │ │ │ ├── ReduxWrappers.kt │ │ │ └── util.kt │ │ └── resources │ │ └── index.html └── webpack.config.d │ └── dce.js ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | out/ 3 | *.class 4 | *.log 5 | *.jar 6 | hs_err_pid* 7 | *.iml 8 | 9 | !/gradle/wrapper/gradle-wrapper.jar 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fullstack-kotlin 2 | Full Stack Kotlin Course: Kotlin React Frontend, Kotlin Spring Boot 2 Backend 3 | 4 | Publihed via Packt to elearning sites. 5 | -------------------------------------------------------------------------------- /backend/api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.plugin.spring") version "1.2.10" 3 | } 4 | apply { 5 | plugin("org.springframework.boot") 6 | } 7 | dependencies { 8 | val springBootVersion: String = parent!!.properties["springBootVersion"] as String 9 | val hibernateValidatorVersion: String = parent!!.properties["hibernateValidatorVersion"] as String 10 | val kotlinxHtmlVersion: String = properties["kotlinxHtmlVersion"] as String 11 | compile(project(":backend:user")) 12 | compile(project(":backend:project")) 13 | compile("org.springframework.boot:spring-boot-starter-webflux:$springBootVersion") 14 | compile("org.springframework.boot:spring-boot-devtools:$springBootVersion") 15 | compile("org.jetbrains.kotlinx:kotlinx-html-jvm:$kotlinxHtmlVersion") 16 | compile("org.hibernate:hibernate-validator:$hibernateValidatorVersion") 17 | } -------------------------------------------------------------------------------- /backend/api/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlinxHtmlVersion=0.6.4 -------------------------------------------------------------------------------- /backend/api/src/main/kotlin/com/packtpub/Config.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub 2 | 3 | import com.packtpub.handler.ApiHandler 4 | import com.packtpub.handler.ExceptionHandler 5 | import com.packtpub.handler.ViewHandler 6 | import com.packtpub.route.ApiRoutes 7 | import com.packtpub.route.ViewRoutes 8 | import org.springframework.boot.SpringApplication 9 | import org.springframework.boot.autoconfigure.SpringBootApplication 10 | import org.springframework.context.ApplicationContextInitializer 11 | import org.springframework.context.support.GenericApplicationContext 12 | import org.springframework.context.support.beans 13 | import org.springframework.http.HttpMethod 14 | import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity 15 | 16 | 17 | @SpringBootApplication 18 | @EnableWebFluxSecurity 19 | class Config 20 | 21 | fun main(args: Array) { 22 | val application = SpringApplication(Config::class.java) 23 | application.addInitializers(ApplicationContextInitializer { ctx -> 24 | beans { 25 | bean { ViewHandler(ref()) } 26 | bean { ViewRoutes(ref()) } 27 | bean { ApiHandler(ref(), ref()) } 28 | bean { ApiRoutes(ref()) } 29 | bean() 30 | securityBeans{securityService -> 31 | pathMatchers(HttpMethod.GET, "/api/projects/**").permitAll() 32 | .pathMatchers("/resources/**").permitAll() 33 | .pathMatchers(HttpMethod.GET, "/login").permitAll() 34 | .pathMatchers(HttpMethod.POST, "/login").permitAll() 35 | .pathMatchers(HttpMethod.POST, "/api/projects/**").access(securityService::isAdmin) 36 | } 37 | }.initialize(ctx) 38 | }) 39 | application.run(*args) 40 | } 41 | -------------------------------------------------------------------------------- /backend/api/src/main/kotlin/com/packtpub/Model.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude 4 | import org.hibernate.validator.constraints.URL 5 | import javax.validation.constraints.Size 6 | 7 | @JsonInclude(JsonInclude.Include.NON_NULL) 8 | data class ProjectDTO( 9 | @get:Size(min = 2) 10 | val name: String, 11 | 12 | @get:URL 13 | val url: String, 14 | 15 | @get:Size(min = 2) 16 | val owner: String, 17 | 18 | val language: Language, 19 | val id: Long? = null, 20 | val extraInfo: GithubApiDto? 21 | ) : Validatable() 22 | 23 | fun ProjectDTO.toProject() = Project(name, url, owner, language, id) 24 | fun Project.toDto() = ProjectDTO(name, url, owner, language, id, 25 | GithubApiDto(description, license, tags)) 26 | 27 | @JsonInclude(JsonInclude.Include.NON_NULL) 28 | open class Validatable( 29 | var fieldErrors: List? = null, 30 | var genericError: String? = null 31 | ) 32 | 33 | data class FieldErrorDTO(val field: String, val message: String) -------------------------------------------------------------------------------- /backend/api/src/main/kotlin/com/packtpub/handler/ApiHandler.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.handler 2 | 3 | import com.packtpub.* 4 | import org.springframework.web.reactive.function.server.ServerRequest 5 | import org.springframework.web.reactive.function.server.ServerResponse 6 | import org.springframework.web.reactive.function.server.body 7 | import org.springframework.web.reactive.function.server.bodyToMono 8 | import reactor.core.publisher.Mono 9 | import javax.validation.Validator 10 | 11 | 12 | class ApiHandler(private val validator: Validator, 13 | private val projectService: ProjectService) { 14 | 15 | fun handle(req: ServerRequest) = 16 | req.bodyToMono() 17 | .map { project -> 18 | val violations = validator.validate(project) 19 | if (violations.isNotEmpty()) { 20 | project.fieldErrors = violations.map { 21 | FieldErrorDTO(it.propertyPath.toString(), it.message) 22 | } 23 | } 24 | project 25 | } 26 | .flatMap { 27 | when (it.fieldErrors) { 28 | null -> ServerResponse.ok().body( 29 | projectService.saveProject(it.toProject()).map { it.toDto() } 30 | ) 31 | else -> ServerResponse.unprocessableEntity().body(Mono.just(it)) 32 | } 33 | } 34 | 35 | @Suppress("UNUSED_PARAMETER") 36 | fun getProjects(req: ServerRequest) = 37 | ServerResponse.ok().body( 38 | Mono.just(projectService.fetchProjects().map { it.toDto() }) 39 | ) 40 | 41 | fun getProject(req: ServerRequest): Mono { 42 | val id = req.pathVariable("id").toLong() 43 | val projectDTO: ProjectDTO? = projectService.fetchProject(id)?.toDto() 44 | return if (projectDTO != null) { 45 | ServerResponse.ok().body(Mono.just(projectDTO)) 46 | } else { 47 | ServerResponse.notFound().build() 48 | } 49 | } 50 | 51 | @Suppress("UNUSED_PARAMETER") 52 | fun getOwners(req: ServerRequest): Mono = 53 | ServerResponse.ok().body(Mono.just(projectService.fetchAllOwners())) 54 | 55 | fun getByOwner(req: ServerRequest): Mono { 56 | val name = req.pathVariable("name") 57 | return ServerResponse.ok().body(Mono.just(projectService.findByOwner(name).map { it.toDto() })) 58 | } 59 | } -------------------------------------------------------------------------------- /backend/api/src/main/kotlin/com/packtpub/handler/ExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.handler 2 | 3 | import com.fasterxml.jackson.core.JsonParseException 4 | import com.packtpub.Validatable 5 | import com.packtpub.util.WithLogging 6 | import org.springframework.core.codec.DecodingException 7 | import org.springframework.http.HttpStatus 8 | import org.springframework.web.reactive.function.server.HandlerStrategies 9 | import org.springframework.web.reactive.function.server.ServerResponse 10 | import org.springframework.web.server.ServerWebExchange 11 | import org.springframework.web.server.WebExceptionHandler 12 | import reactor.core.publisher.Mono 13 | 14 | 15 | class ExceptionHandler : WebExceptionHandler, WithLogging() { 16 | override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono { 17 | LOG.error("failed to handle request", ex) 18 | return handle(ex) 19 | .flatMap { 20 | it.populateBody(exchange) 21 | } 22 | .flatMap { 23 | Mono.empty() 24 | } 25 | } 26 | 27 | private fun handle(throwable: Throwable): 28 | Mono = when (throwable) { 29 | is JsonParseException, 30 | is DecodingException -> 31 | respond(HttpStatus.BAD_REQUEST, 32 | Validatable(genericError = throwable.localizedMessage)) 33 | else -> 34 | respond(HttpStatus.INTERNAL_SERVER_ERROR, 35 | Validatable(genericError = throwable.localizedMessage)) 36 | } 37 | 38 | private fun respond(httpStatus: HttpStatus, body: Any): 39 | Mono = 40 | ServerResponse 41 | .status(httpStatus) 42 | .syncBody(body) 43 | } 44 | 45 | fun ServerResponse.populateBody(exchange: ServerWebExchange): Mono = writeTo(exchange, HandlerStrategiesResponseContext(HandlerStrategies.withDefaults())) 46 | private class HandlerStrategiesResponseContext(val strategies: HandlerStrategies) : ServerResponse.Context { 47 | override fun messageWriters() = strategies.messageWriters() 48 | override fun viewResolvers() = strategies.viewResolvers() 49 | } -------------------------------------------------------------------------------- /backend/api/src/main/kotlin/com/packtpub/handler/ViewHandler.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.handler 2 | 3 | import com.packtpub.ProjectService 4 | import com.packtpub.util.htmlView 5 | import com.packtpub.views.index 6 | import org.springframework.web.reactive.function.server.ServerRequest 7 | import org.springframework.web.reactive.function.server.ServerResponse 8 | import reactor.core.publisher.Mono 9 | 10 | 11 | class ViewHandler(private val projectService: ProjectService) { 12 | 13 | fun handle(req: ServerRequest) = 14 | ServerResponse.ok() 15 | .htmlView(Mono.just( 16 | index("Hello ${req.queryParam("name").orElse("PacktUser")}", 17 | projectService.fetchProjectsForView()) 18 | )) 19 | 20 | } -------------------------------------------------------------------------------- /backend/api/src/main/kotlin/com/packtpub/route/ApiRoutes.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.route 2 | 3 | import com.packtpub.handler.ApiHandler 4 | import com.packtpub.util.WithLogging 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.http.MediaType 7 | import org.springframework.web.reactive.function.server.RouterFunction 8 | import org.springframework.web.reactive.function.server.ServerResponse 9 | import org.springframework.web.reactive.function.server.router 10 | 11 | 12 | class ApiRoutes(private val apiHandler: ApiHandler) : WithLogging() { 13 | 14 | @Bean 15 | fun apiRouter(): RouterFunction = 16 | router { 17 | ("/api" and accept(MediaType.APPLICATION_JSON_UTF8)).nest { 18 | "/projects".nest { 19 | POST("/", apiHandler::handle) 20 | GET("/", apiHandler::getProjects) 21 | GET("/owners", apiHandler::getOwners) 22 | GET("/byOwner/{name}", apiHandler::getByOwner) 23 | GET("/{id}", apiHandler::getProject) 24 | } 25 | } 26 | }.filter { request, next -> 27 | LOG.debug(request) 28 | next.handle(request) 29 | } 30 | } -------------------------------------------------------------------------------- /backend/api/src/main/kotlin/com/packtpub/route/ViewRoutes.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.route 2 | 3 | import com.packtpub.handler.ViewHandler 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.core.io.ClassPathResource 6 | import org.springframework.web.reactive.function.server.RouterFunction 7 | import org.springframework.web.reactive.function.server.ServerResponse 8 | import org.springframework.web.reactive.function.server.router 9 | 10 | 11 | class ViewRoutes(private val viewHandler: ViewHandler) { 12 | 13 | @Bean 14 | fun viewRouter(): RouterFunction = 15 | router { 16 | "/".nest { 17 | GET("/", viewHandler::handle) 18 | } 19 | resources("/**", ClassPathResource("/static")) 20 | } 21 | } -------------------------------------------------------------------------------- /backend/api/src/main/kotlin/com/packtpub/util/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.util 2 | 3 | import org.reactivestreams.Publisher 4 | import org.springframework.http.MediaType 5 | import org.springframework.web.reactive.function.server.ServerResponse 6 | import org.springframework.web.reactive.function.server.body 7 | 8 | 9 | inline fun ServerResponse.BodyBuilder.json( 10 | content: Publisher) = 11 | contentType(MediaType.APPLICATION_JSON_UTF8) 12 | .body(content) 13 | 14 | inline fun ServerResponse.BodyBuilder.htmlView( 15 | content: Publisher) = 16 | contentType(MediaType.TEXT_HTML) 17 | .body(content) 18 | -------------------------------------------------------------------------------- /backend/api/src/main/kotlin/com/packtpub/util/Logging.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.util 2 | 3 | import org.apache.logging.log4j.LogManager 4 | import org.apache.logging.log4j.Logger 5 | import kotlin.reflect.KClass 6 | import kotlin.reflect.full.companionObject 7 | 8 | 9 | // Return logger for Java class, if companion object fix the name 10 | fun logger(forClass: Class): Logger { 11 | return LogManager.getLogger(unwrapCompanionClass(forClass).name) 12 | } 13 | 14 | // unwrap companion class to enclosing class given a Java Class 15 | fun unwrapCompanionClass(ofClass: Class): Class<*> { 16 | return if (ofClass.enclosingClass != null && ofClass.enclosingClass.kotlin.companionObject?.java == ofClass) { 17 | ofClass.enclosingClass 18 | } else { 19 | ofClass 20 | } 21 | } 22 | 23 | // unwrap companion class to enclosing class given a Kotlin Class 24 | fun unwrapCompanionClass(ofClass: KClass): KClass<*> { 25 | return unwrapCompanionClass(ofClass.java).kotlin 26 | } 27 | 28 | // Return logger for Kotlin class 29 | fun logger(forClass: KClass): Logger { 30 | return logger(forClass.java) 31 | } 32 | 33 | // return logger from extended class (or the enclosing class) 34 | fun T.logger(): Logger { 35 | return logger(this.javaClass) 36 | } 37 | 38 | // return a lazy logger property delegate for enclosing class 39 | fun R.lazyLogger(): Lazy { 40 | return lazy { logger(this.javaClass) } 41 | } 42 | 43 | // return a logger property delegate for enclosing class 44 | fun R.injectLogger(): Lazy { 45 | return lazyOf(logger(this.javaClass)) 46 | } 47 | 48 | // marker interface and related extension (remove extension for Any.logger() in favour of this) 49 | interface Loggable {} 50 | 51 | fun Loggable.logger(): Logger = logger(this.javaClass) 52 | 53 | // abstract base class to provide logging, intended for companion objects more than classes but works for either 54 | abstract class WithLogging : Loggable { 55 | val LOG = logger() 56 | } -------------------------------------------------------------------------------- /backend/api/src/main/kotlin/com/packtpub/views/ViewProvider.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.views 2 | 3 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 4 | import com.packtpub.ProjectView 5 | import kotlinx.html.* 6 | import kotlinx.html.stream.createHTML 7 | 8 | 9 | fun index(@Suppress("UNUSED_PARAMETER") header: String, @Suppress("UNUSED_PARAMETER") projects: List): String { 10 | return createHTML(true).html { 11 | head { 12 | title = "Full Stack Kotlin" 13 | styleLink("https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css") 14 | styleLink("/static/css/spinner.css") 15 | } 16 | body { 17 | div { 18 | id = "container" 19 | } 20 | div { 21 | id = "contentHolder" 22 | script { 23 | val writeValueAsString = jacksonObjectMapper().writeValueAsString(projects) 24 | +"var __projects__ = '$writeValueAsString'" 25 | } 26 | } 27 | script(src = "/static/frontend.bundle.js") 28 | } 29 | } 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /backend/api/src/main/resources/static/css/spinner.css: -------------------------------------------------------------------------------- 1 | #loader { 2 | bottom: 0; 3 | height: 175px; 4 | left: 0; 5 | margin: auto; 6 | position: absolute; 7 | right: 0; 8 | top: 0; 9 | width: 175px; 10 | } 11 | 12 | #loader { 13 | bottom: 0; 14 | height: 175px; 15 | left: 0; 16 | margin: auto; 17 | position: absolute; 18 | right: 0; 19 | top: 0; 20 | width: 175px; 21 | } 22 | 23 | #loader .dot { 24 | bottom: 0; 25 | height: 100%; 26 | left: 0; 27 | margin: auto; 28 | position: absolute; 29 | right: 0; 30 | top: 0; 31 | width: 87.5px; 32 | } 33 | 34 | #loader .dot::before { 35 | border-radius: 100%; 36 | content: ""; 37 | height: 87.5px; 38 | left: 0; 39 | position: absolute; 40 | right: 0; 41 | top: 0; 42 | transform: scale(0); 43 | width: 87.5px; 44 | } 45 | 46 | #loader .dot:nth-child(7n+1) { 47 | transform: rotate(45deg); 48 | } 49 | 50 | #loader .dot:nth-child(7n+1)::before { 51 | animation: 0.8s linear 0.1s normal none infinite running load; 52 | background: #00ff80 none repeat scroll 0 0; 53 | } 54 | 55 | #loader .dot:nth-child(7n+2) { 56 | transform: rotate(90deg); 57 | } 58 | 59 | #loader .dot:nth-child(7n+2)::before { 60 | animation: 0.8s linear 0.2s normal none infinite running load; 61 | background: #00ffea none repeat scroll 0 0; 62 | } 63 | 64 | #loader .dot:nth-child(7n+3) { 65 | transform: rotate(135deg); 66 | } 67 | 68 | #loader .dot:nth-child(7n+3)::before { 69 | animation: 0.8s linear 0.3s normal none infinite running load; 70 | background: #00aaff none repeat scroll 0 0; 71 | } 72 | 73 | #loader .dot:nth-child(7n+4) { 74 | transform: rotate(180deg); 75 | } 76 | 77 | #loader .dot:nth-child(7n+4)::before { 78 | animation: 0.8s linear 0.4s normal none infinite running load; 79 | background: #0040ff none repeat scroll 0 0; 80 | } 81 | 82 | #loader .dot:nth-child(7n+5) { 83 | transform: rotate(225deg); 84 | } 85 | 86 | #loader .dot:nth-child(7n+5)::before { 87 | animation: 0.8s linear 0.5s normal none infinite running load; 88 | background: #2a00ff none repeat scroll 0 0; 89 | } 90 | 91 | #loader .dot:nth-child(7n+6) { 92 | transform: rotate(270deg); 93 | } 94 | 95 | #loader .dot:nth-child(7n+6)::before { 96 | animation: 0.8s linear 0.6s normal none infinite running load; 97 | background: #9500ff none repeat scroll 0 0; 98 | } 99 | 100 | #loader .dot:nth-child(7n+7) { 101 | transform: rotate(315deg); 102 | } 103 | 104 | #loader .dot:nth-child(7n+7)::before { 105 | animation: 0.8s linear 0.7s normal none infinite running load; 106 | background: magenta none repeat scroll 0 0; 107 | } 108 | 109 | #loader .dot:nth-child(7n+8) { 110 | transform: rotate(360deg); 111 | } 112 | 113 | #loader .dot:nth-child(7n+8)::before { 114 | animation: 0.8s linear 0.8s normal none infinite running load; 115 | background: #ff0095 none repeat scroll 0 0; 116 | } 117 | 118 | #loader .loading { 119 | bottom: -40px; 120 | height: 20px; 121 | left: 0; 122 | position: absolute; 123 | right: 0; 124 | width: 180px; 125 | } 126 | 127 | @keyframes load { 128 | 100% { 129 | opacity: 0; 130 | transform: scale(1); 131 | } 132 | } 133 | 134 | @keyframes load { 135 | 100% { 136 | opacity: 0; 137 | transform: scale(1); 138 | } 139 | } -------------------------------------------------------------------------------- /backend/api/src/main/resources/static/js/hello.js: -------------------------------------------------------------------------------- 1 | console.log('Hello World Script Loaded'); 2 | 3 | (function(){ 4 | function reqListener () { 5 | console.log('Our API responded: ' + this.responseText); 6 | } 7 | 8 | var oReq = new XMLHttpRequest(); 9 | oReq.addEventListener('load', reqListener); 10 | oReq.open('GET', 'api/hello?name=Kotlin Coder'); 11 | oReq.send(); 12 | })(); -------------------------------------------------------------------------------- /backend/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.* 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | 4 | 5 | plugins { 6 | kotlin("jvm") version "1.2.10" 7 | } 8 | 9 | buildscript { 10 | val springBootVersion: String = properties["springBootVersion"] as String 11 | repositories { 12 | maven { setUrl("https://repo.spring.io/milestone") } 13 | maven { setUrl("https://repo.spring.io/snapshot") } 14 | } 15 | 16 | dependencies { 17 | classpath("org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion") 18 | } 19 | } 20 | 21 | subprojects { 22 | val kotlinVersion = properties["kotlinVersion"] as String 23 | repositories { 24 | mavenCentral() 25 | maven { setUrl("https://repo.spring.io/milestone") } 26 | maven { setUrl("https://repo.spring.io/snapshot") } 27 | } 28 | 29 | plugins { 30 | kotlin("jvm") version kotlinVersion 31 | } 32 | 33 | apply { 34 | plugin("kotlin") 35 | } 36 | 37 | dependencies { 38 | compile(kotlin("stdlib-jre8", kotlinVersion)) 39 | compile(kotlin("reflect", kotlinVersion)) 40 | } 41 | 42 | tasks.withType { 43 | kotlinOptions { 44 | jvmTarget = "1.8" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /backend/gradle.properties: -------------------------------------------------------------------------------- 1 | springBootVersion=2.0.0.M7 2 | kotlinVersion=1.2.10 3 | hibernateValidatorVersion=6.0.2.Final 4 | postgresVersion=42.1.4 -------------------------------------------------------------------------------- /backend/project/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.plugin.spring") version "1.2.10" 3 | id("org.jetbrains.kotlin.plugin.jpa") version "1.2.10" 4 | } 5 | 6 | dependencies { 7 | val springBootVersion: String = parent!!.properties["springBootVersion"] as String 8 | val postgresVersion: String = parent!!.properties["postgresVersion"] as String 9 | val kotlinJacksonVersion: String = properties["kotlinJacksonVersion"] as String 10 | compile("org.springframework.boot:spring-boot-starter-webflux:$springBootVersion") 11 | compile("org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion") 12 | compile("org.postgresql:postgresql:$postgresVersion") 13 | compile("com.fasterxml.jackson.module:jackson-module-kotlin:$kotlinJacksonVersion") 14 | } -------------------------------------------------------------------------------- /backend/project/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlinJacksonVersion=2.9.0 2 | -------------------------------------------------------------------------------- /backend/project/src/main/kotlin/com/packtpub/Model.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 4 | import com.fasterxml.jackson.annotation.JsonProperty 5 | import javax.persistence.* 6 | 7 | @Entity 8 | @Table(name = "project") 9 | data class Project( 10 | val name: String, 11 | val url: String, 12 | val owner: String, 13 | val language: Language, 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.AUTO) 17 | val id: Long? = null, 18 | val description: String? = null, 19 | 20 | @ElementCollection(fetch = FetchType.EAGER) 21 | val tags: List = listOf(), 22 | val license: String? = null 23 | ) 24 | 25 | enum class Language { 26 | KOTLIN, JAVASCRIPT, JAVA 27 | } 28 | 29 | data class ProjectView(val name: String, val url: String, val owner: String, val language: Language) 30 | 31 | @JsonIgnoreProperties(ignoreUnknown = true) 32 | data class GithubApiDto( 33 | val description: String? = "", 34 | val license: String? = null, 35 | @JsonProperty("topics") 36 | val tags: List = listOf() 37 | ) -------------------------------------------------------------------------------- /backend/project/src/main/kotlin/com/packtpub/ProjectConfig.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.context.annotation.Primary 6 | import org.springframework.context.support.GenericApplicationContext 7 | 8 | @Configuration 9 | class ProjectConfig(private val applicationContext: GenericApplicationContext) { 10 | 11 | @Primary 12 | @Bean 13 | fun projectService(): ProjectService = 14 | ProjectServiceImpl(applicationContext.getBean(ProjectRepository::class.java)) 15 | } -------------------------------------------------------------------------------- /backend/project/src/main/kotlin/com/packtpub/ProjectRepository.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub 2 | 3 | import org.springframework.data.jpa.repository.Query 4 | import org.springframework.data.repository.CrudRepository 5 | import org.springframework.stereotype.Repository 6 | 7 | @Repository 8 | interface ProjectRepository : CrudRepository{ 9 | fun findByOwner(owner: String): List 10 | 11 | @Query("SELECT distinct owner FROM Project") 12 | fun retrieveAllOwners(): List 13 | 14 | @Query(""" 15 | SELECT 16 | new com.packtpub.ProjectView(name, url, owner, language) 17 | FROM Project 18 | """) 19 | fun retrieveAllProjectsForView(): List 20 | } -------------------------------------------------------------------------------- /backend/project/src/main/kotlin/com/packtpub/ProjectService.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub 2 | 3 | import org.springframework.beans.factory.annotation.Value 4 | import org.springframework.http.MediaType 5 | import org.springframework.web.reactive.function.client.WebClient 6 | import org.springframework.web.reactive.function.client.bodyToMono 7 | import reactor.core.publisher.Mono 8 | import reactor.core.publisher.toMono 9 | 10 | 11 | const val mercy: String = "application/vnd.github.mercy-preview+json" 12 | const val drax: String = "application/vnd.github.drax-preview+json" 13 | 14 | interface ProjectService { 15 | fun saveProject(project: Project): Mono 16 | fun fetchProjects(): List 17 | fun fetchProject(id: Long): Project? 18 | fun findByOwner(owner: String): List 19 | fun fetchAllOwners(): List 20 | fun fetchProjectsForView(): List 21 | } 22 | 23 | internal class ProjectServiceImpl 24 | (private val projectRepository: ProjectRepository) : ProjectService { 25 | 26 | @Value("\${api.endpoint.url}") 27 | lateinit var endpoint: String 28 | 29 | override fun fetchProjects(): List = 30 | projectRepository.findAll().toList() 31 | 32 | override fun saveProject(project: Project): Mono { 33 | return project.toMono() 34 | .zipWith(fetchProjects(project), { it, githubApiDto -> 35 | it.copy(description = githubApiDto.description, 36 | tags = githubApiDto.tags, 37 | license = githubApiDto.license) 38 | }).map { fullproject -> 39 | projectRepository.save(fullproject) 40 | } 41 | } 42 | 43 | override fun fetchProject(id: Long): Project? = 44 | projectRepository.findById(id).orElse(null) 45 | 46 | override fun findByOwner(owner: String): List = 47 | projectRepository.findByOwner(owner) 48 | 49 | override fun fetchAllOwners(): List = 50 | projectRepository.retrieveAllOwners() 51 | 52 | override fun fetchProjectsForView(): List = 53 | projectRepository.retrieveAllProjectsForView() 54 | 55 | private fun fetchProjects(project: Project): Mono { 56 | val webclient = WebClient.create(endpoint) 57 | return webclient.get() 58 | .uri("/repos/${project.owner}/${project.name}") 59 | .accept(MediaType.parseMediaType(mercy), 60 | MediaType.parseMediaType(drax)) 61 | .exchange() 62 | .flatMap { response -> 63 | response.bodyToMono() 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /backend/user/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | val kotlinVersion = "1.2.10" 3 | id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion 4 | id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion 5 | } 6 | 7 | dependencies { 8 | val springBootVersion: String = parent!!.properties["springBootVersion"] as String 9 | val postgresVersion: String = parent!!.properties["postgresVersion"] as String 10 | compile("org.springframework.boot:spring-boot-starter:$springBootVersion") 11 | compile("org.springframework.boot:spring-boot-starter-webflux:$springBootVersion") 12 | compile("org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion") 13 | compile("org.springframework.boot:spring-boot-starter-security:$springBootVersion") 14 | compile("org.postgresql:postgresql:$postgresVersion") 15 | } -------------------------------------------------------------------------------- /backend/user/src/main/kotlin/com/packtpub/Model.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub 2 | 3 | import org.springframework.security.core.authority.SimpleGrantedAuthority 4 | import org.springframework.security.core.userdetails.User 5 | import org.springframework.security.core.userdetails.UserDetails 6 | import javax.persistence.* 7 | 8 | 9 | @Entity 10 | @Table(name = "packtuser", 11 | uniqueConstraints = 12 | arrayOf(UniqueConstraint(columnNames = arrayOf("username")))) 13 | data class PacktUser( 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.AUTO) 16 | val id: Long? = null, 17 | 18 | val username: String, 19 | val password: String, 20 | val active: Boolean = true, 21 | 22 | @ElementCollection(fetch = FetchType.EAGER) 23 | val roles: List = listOf() 24 | ) { 25 | 26 | fun toUserDetails(): UserDetails = 27 | User.withUsername(username) 28 | .password(password) 29 | .accountExpired(!active) 30 | .accountLocked(!active) 31 | .credentialsExpired(!active) 32 | .disabled(!active) 33 | .authorities(roles.map(::SimpleGrantedAuthority).toList()) 34 | .build() 35 | } 36 | -------------------------------------------------------------------------------- /backend/user/src/main/kotlin/com/packtpub/SecurityService.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub 2 | 3 | import org.springframework.security.authorization.AuthorizationDecision 4 | import org.springframework.security.core.Authentication 5 | import org.springframework.security.web.server.authorization.AuthorizationContext 6 | import reactor.core.publisher.Mono 7 | import reactor.core.publisher.toMono 8 | 9 | interface SecurityService { 10 | fun isAdmin(authentication: Mono, _ac: AuthorizationContext) 11 | : Mono 12 | } 13 | internal class SecurityServiceImpl: SecurityService { 14 | 15 | override fun isAdmin(authentication: Mono, 16 | _ac: AuthorizationContext): 17 | Mono { 18 | return authentication.flatMap { 19 | AuthorizationDecision(it.authorities.map { it.authority } 20 | .contains("ADMIN")).toMono() 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /backend/user/src/main/kotlin/com/packtpub/UserConfig.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub 2 | 3 | import org.springframework.context.support.BeanDefinitionDsl 4 | import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager 5 | import org.springframework.security.config.web.server.ServerHttpSecurity 6 | import org.springframework.security.core.userdetails.ReactiveUserDetailsService 7 | import org.springframework.security.web.server.SecurityWebFilterChain 8 | import reactor.core.publisher.Mono 9 | import reactor.core.publisher.toMono 10 | 11 | 12 | fun BeanDefinitionDsl.securityBeans(paths: ServerHttpSecurity.AuthorizeExchangeSpec 13 | .(SecurityService) -> ServerHttpSecurity.AuthorizeExchangeSpec) { 14 | bean() 15 | bean { 16 | UserServiceImpl(ref()) 17 | } 18 | bean { 19 | SecurityServiceImpl() 20 | } 21 | bean { 22 | ReactiveUserDetailsService { username -> 23 | ref().getUserByName(username) 24 | ?.toUserDetails() 25 | ?.toMono() 26 | ?: 27 | Mono.empty() 28 | 29 | } 30 | } 31 | bean { 32 | ServerHttpSecurity.http().authorizeExchange() 33 | .paths(ref()) 34 | .anyExchange().authenticated() 35 | .and() 36 | .authenticationManager(UserDetailsRepositoryReactiveAuthenticationManager(ref())) 37 | .formLogin() 38 | .and() 39 | .build() 40 | } 41 | } -------------------------------------------------------------------------------- /backend/user/src/main/kotlin/com/packtpub/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub 2 | 3 | import org.springframework.data.repository.CrudRepository 4 | import java.util.* 5 | 6 | 7 | internal interface UserRepository : CrudRepository { 8 | fun findOneByUsername(username: String): Optional 9 | } -------------------------------------------------------------------------------- /backend/user/src/main/kotlin/com/packtpub/UserService.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub 2 | 3 | 4 | interface UserService { 5 | fun getUser(id: Long): PacktUser? 6 | fun getUserByName(name: String): PacktUser? 7 | } 8 | internal class UserServiceImpl 9 | (private val userRepository: UserRepository) : UserService { 10 | override fun getUserByName(name: String): PacktUser? = 11 | userRepository.findOneByUsername(name).orElse(null) 12 | 13 | override fun getUser(id: Long): PacktUser? = 14 | userRepository.findById(id).orElse(null) 15 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | group = "com.packtpub" 3 | version = "1.0" 4 | repositories { 5 | jcenter() 6 | } 7 | } 8 | 9 | plugins { 10 | base 11 | } 12 | 13 | dependencies { 14 | subprojects.forEach { 15 | archives(it) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /config/application.properties: -------------------------------------------------------------------------------- 1 | # LOGGING 2 | logging.level.com.packtpub=DEBUG 3 | 4 | # DATASOURCE 5 | spring.datasource.url=jdbc:postgresql://localhost:5432/fullstack_kotlin 6 | spring.datasource.username=kotlin 7 | spring.datasource.password=salasana 8 | spring.jpa.hibernate.ddl-auto=update 9 | spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQL9Dialect -------------------------------------------------------------------------------- /docs/Server-Setup.md: -------------------------------------------------------------------------------- 1 | # Installing needed dependencies to a pristine Ubuntu 16.04 box 2 | 3 | 4 | ## Install needed application from package manager 5 | 6 | 1. Certbot to handle our `let's encrypt` SSL certificates 7 | * `sudo add-apt-repository ppa:certbot/certbot` 8 | * `sudo apt-get update` 9 | * `sudo apt-get install certbot` 10 | 11 | 2. Nginx to serve as our web server / reverse proxy 12 | * `sudo apt install nginx` 13 | 14 | 3. JRE to run our Java application 15 | * `sudo apt install openjdk-8-jre` 16 | 17 | 18 | 19 | ## Install SSL certs using Let's Encrypt 20 | 21 | Run command `sudo certbot certonly` and follow instructions on the screen. Note that you need to know your domain name at this point and it needs to point to your publicly exposed IP address. You also need to open port 443 which Amazon doesn't expose by default 22 | 23 | 24 | ## Setup Nginx to act as a reverse proxy for our application 25 | 26 | * Generate dhparam.pem file: `sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048` 27 | 28 | * Create SSL configuration snippet for your domain: `sudo nano /etc/nginx/snippets/ssl-yourdomain.com.conf` 29 | * Contents: 30 | ``` 31 | ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; 32 | ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; 33 | ``` 34 | * Create SSL params snippet for your domain: `sudo nano /etc/nginx/snippets/ssl-params.conf` 35 | * Contents: 36 | ``` 37 | # from https://cipherli.st/ 38 | # and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html 39 | 40 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 41 | ssl_prefer_server_ciphers on; 42 | ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; 43 | ssl_ecdh_curve secp384r1; 44 | ssl_session_cache shared:SSL:10m; 45 | ssl_session_tickets off; 46 | ssl_stapling on; 47 | ssl_stapling_verify on; 48 | resolver 8.8.8.8 8.8.4.4 valid=300s; 49 | resolver_timeout 5s; 50 | # disable HSTS header for now 51 | #add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; 52 | add_header X-Frame-Options DENY; 53 | #add_header X-Content-Type-Options nosniff; 54 | 55 | ssl_dhparam /etc/ssl/certs/dhparam.pem; 56 | ``` 57 | 58 | * Create the actual Nginx configuration (backup first: `sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak`): `sudo nano /etc/nginx/sites-available/default` 59 | * Contents: 60 | ``` 61 | upstream netty { 62 | server localhost:8080; 63 | } 64 | server { 65 | listen 80 default_server; 66 | listen [::]:80 default_server; 67 | server_name yourdomain.com www.yourdomain.com localhost; 68 | return 301 https://$server_name$request_uri; 69 | } 70 | 71 | server { 72 | 73 | # SSL configuration 74 | 75 | listen 443 ssl http2 default_server; 76 | listen [::]:443 ssl http2 default_server; 77 | include snippets/ssl-yourdomain.com.conf; 78 | include snippets/ssl-params.conf; 79 | location / { 80 | proxy_pass http://netty$request_uri; 81 | proxy_set_header Host $host; 82 | proxy_set_header X-Real-IP $remote_addr; 83 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 84 | proxy_set_header X-Forwarded-Host $server_name; 85 | } 86 | } 87 | 88 | ``` 89 | 90 | * Test Nginx config and restart: `sudo nginx -t` -> `sudo systemctl restart nginx` 91 | 92 | ## Set up our application to be run as a service 93 | 94 | * Create a user: `sudo adduser kotliner` 95 | * Modify file owners: `sudo chown kotliner fullstack-kotlin.jar` & `sudo chown kotliner application.properties` 96 | * Create a service definition to start the application as a service: `sudo nano /etc/systemd/system/fullstack-kotlin.service` 97 | * Contents: 98 | ``` 99 | [Unit] 100 | Description=Fullstack-Kotlin 101 | 102 | [Service] 103 | User=kotliner 104 | # The configuration file application.properties should be here: 105 | WorkingDirectory=/home/ubuntu/app 106 | ExecStart=/usr/bin/java -Xmx256m -jar fullstack-kotlin.jar 107 | SuccessExitStatus=143 108 | TimeoutStopSec=10 109 | Restart=on-failure 110 | RestartSec=5 111 | 112 | [Install] 113 | WantedBy=multi-user.target 114 | ``` 115 | * Start our application as a service: `sudo systemctl start fullstack-kotlin` 116 | * Logs can be found from journactl: `sudo journalctl -f -u fullstack-kotlin.service` 117 | 118 | 119 | More info: 120 | 121 | https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-on-ubuntu-14-04 122 | -------------------------------------------------------------------------------- /frontend/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | import org.gradle.api.tasks.Copy 3 | import org.gradle.kotlin.dsl.* 4 | import org.jetbrains.kotlin.gradle.frontend.KotlinFrontendExtension 5 | import org.jetbrains.kotlin.gradle.frontend.npm.NpmExtension 6 | import org.jetbrains.kotlin.gradle.frontend.webpack.WebPackExtension 7 | import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile 8 | 9 | val prod: Boolean = (parent!!.properties["production"] as String ).toBoolean() 10 | 11 | buildscript { 12 | val kotlinVersion: String = properties["kotlinVersion"] as String 13 | val frontendPluginVersion: String = properties["frontendPluginVersion"] as String 14 | repositories { 15 | jcenter() 16 | maven { setUrl("https://dl.bintray.com/kotlin/kotlin-eap") } 17 | } 18 | 19 | dependencies { 20 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") 21 | classpath("org.jetbrains.kotlin:kotlin-frontend-plugin:$frontendPluginVersion") 22 | } 23 | } 24 | 25 | apply { 26 | plugin("kotlin2js") 27 | plugin("org.jetbrains.kotlin.frontend") 28 | } 29 | 30 | repositories { 31 | jcenter() 32 | maven { setUrl("https://dl.bintray.com/kotlin/kotlin-eap") } 33 | } 34 | val kotlinVersion: String = properties["kotlinVersion"] as String 35 | val kotlinxHtmlVersion: String = properties["kotlinxHtmlVersion"] as String 36 | dependencies { 37 | "compile"(kotlin("stdlib-js", kotlinVersion)) 38 | "compile"("org.jetbrains.kotlinx:kotlinx-html-js:$kotlinxHtmlVersion") 39 | } 40 | 41 | val compileKotlin2Js: Kotlin2JsCompile by tasks 42 | 43 | compileKotlin2Js.kotlinOptions { 44 | sourceMap = !prod 45 | metaInfo = true 46 | freeCompilerArgs = listOf("-Xcoroutines=enable") 47 | outputFile = "${project.buildDir.path}/js/index.js" 48 | main = "call" 49 | moduleKind = "commonjs" 50 | } 51 | 52 | configure{ 53 | sourceMaps = !prod 54 | 55 | configure { 56 | replaceVersion("kotlinx-html-js", "0.6.4") 57 | replaceVersion("kotlinx-html-shared", "0.6.4") 58 | replaceVersion("kotlin-js-library", "1.2.10") 59 | 60 | dependency("react", "16.0.0") 61 | dependency("react-dom", "16.0.0") 62 | dependency("redux", "3.7.2") 63 | dependency("react-redux", "5.0.6") 64 | dependency("redux-thunk", "2.2.0") 65 | dependency("redux-devtools-extension", "2.13.2") 66 | dependency("lodash", "4.17.4") 67 | devDependency("source-map-loader") 68 | } 69 | 70 | bundle("webpack", delegateClosureOf { 71 | publicPath = "/static/" 72 | port = 3000 73 | proxyUrl = "http://localhost:8080" 74 | stats = "verbose" 75 | }) 76 | } 77 | task("copyResources"){ 78 | doLast { 79 | copy { 80 | from("build/bundle") 81 | into("../backend/api/build/resources/main/static") 82 | } 83 | copy { 84 | from("build/bundle") 85 | into("../backend/api/out/production/resources/static") 86 | } 87 | } 88 | } 89 | 90 | val build by tasks 91 | build.dependsOn("bundle") 92 | build.dependsOn("copyResources") 93 | -------------------------------------------------------------------------------- /frontend/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlinVersion=1.2.10 2 | frontendPluginVersion=0.0.21 3 | kotlinxHtmlVersion=0.6.4 -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/components/ProjectList.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.components 2 | 3 | import com.packtpub.model.Project 4 | import com.packtpub.store.ReduxStore 5 | import com.packtpub.util.jsObject 6 | import kotlinx.html.* 7 | import kotlinx.html.js.onClickFunction 8 | import react.RProps 9 | import react.ReactComponentStatelessSpec 10 | import react.dom.ReactDOMBuilder 11 | import react.dom.ReactDOMStatelessComponent 12 | import redux.connect 13 | 14 | val projectList = 15 | connect( 16 | { state: ReduxStore, _ -> 17 | jsObject { 18 | items = state.projectList.asList() 19 | isSpinning = state.isSpinning 20 | } 21 | }) 22 | 23 | class ProjectList : ReactDOMStatelessComponent() { 24 | companion object : ReactComponentStatelessSpec 25 | 26 | override fun ReactDOMBuilder.render() { 27 | if (props.isSpinning) { 28 | Spinner {} 29 | } else { 30 | div(classes = "container") { 31 | div(classes = "row") { 32 | div(classes = "col-12") { 33 | h1 { 34 | +"Projects" 35 | } 36 | a(href = "#form", classes = "float-right") { 37 | +"Go to form view" 38 | } 39 | } 40 | } 41 | 42 | table(classes = "table table-inverse table-hover") { 43 | thead { 44 | tr { 45 | th { 46 | +"Name" 47 | } 48 | th { 49 | +"Owner" 50 | } 51 | th { 52 | +"Language" 53 | } 54 | th { 55 | +"Url" 56 | } 57 | } 58 | } 59 | tbody { 60 | props.items.map { project: Project -> 61 | tr { 62 | 63 | td { 64 | +project.name 65 | } 66 | td { 67 | +project.owner 68 | } 69 | td { 70 | +(project.language?.name ?: "") 71 | } 72 | td { 73 | a(project.url) { 74 | onClickFunction = { 75 | props.action(project) 76 | } 77 | +project.url 78 | } 79 | } 80 | 81 | 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | 90 | } 91 | 92 | class Props(var items: List = listOf(), 93 | var action: (Project) -> Unit = {}, 94 | var isSpinning: Boolean = false) : RProps() 95 | } 96 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/components/RouterComponent.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnsafeCastFromDynamic") 2 | 3 | package com.packtpub.components 4 | 5 | import com.packtpub.components.form.Form 6 | import com.packtpub.model.Project 7 | import com.packtpub.store.ActionType 8 | import com.packtpub.store.FormInput 9 | import com.packtpub.store.ReduxStore 10 | import com.packtpub.store.submitForm 11 | import com.packtpub.util.js 12 | import com.packtpub.util.jsObject 13 | import react.RProps 14 | import react.ReactComponentStatelessSpec 15 | import react.dom.ReactDOMBuilder 16 | import react.dom.ReactDOMStatelessComponent 17 | import redux.ReduxAction 18 | import redux.asConnectedComponent 19 | import redux.connect 20 | 21 | val routerComponent = 22 | connect( 23 | { state: ReduxStore, _ -> 24 | jsObject { 25 | hash = state.hash 26 | currentProject = state.currentProject 27 | projectList = state.projectList 28 | isSpinning = state.isSpinning 29 | } 30 | }, { dispatch, _ -> 31 | jsObject { 32 | updateAction = { target, value -> 33 | dispatch(ReduxAction(ActionType.FORM_INPUT, FormInput(target, value))()) 34 | } 35 | submitAction = { project -> 36 | js { 37 | dispatch(submitForm(project)) 38 | } 39 | } 40 | clearAction = { dispatch(ReduxAction(ActionType.FORM_CLEAR)()) } 41 | } 42 | }) 43 | 44 | 45 | class RouterComponent : ReactDOMStatelessComponent() { 46 | companion object : ReactComponentStatelessSpec 47 | 48 | 49 | override fun ReactDOMBuilder.render() { 50 | when (props.hash) { 51 | "form" -> 52 | Form { 53 | update = props.updateAction 54 | submit = { props.submitAction(props.currentProject) } 55 | clear = props.clearAction 56 | project = props.currentProject 57 | } 58 | else -> { 59 | ProjectList.asConnectedComponent(projectList).invoke() 60 | } 61 | } 62 | } 63 | 64 | class Props(var hash: String = "", 65 | var projectList: Array = arrayOf(), 66 | var isSpinning: Boolean = false, 67 | var currentProject: Project = Project.identity(), 68 | var updateAction: (Any, String) -> Unit, 69 | var clearAction: () -> Unit, 70 | var submitAction: (Project) -> Unit) : RProps() 71 | } 72 | 73 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/components/Spinner.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.components 2 | 3 | import kotlinx.html.div 4 | import kotlinx.html.id 5 | import react.ReactComponentStaticSpec 6 | import react.dom.ReactDOMBuilder 7 | import react.dom.ReactDOMStaticComponent 8 | 9 | 10 | class Spinner : ReactDOMStaticComponent() { 11 | companion object: ReactComponentStaticSpec 12 | 13 | override fun ReactDOMBuilder.render() { 14 | div { 15 | id = "loader" 16 | (0..7).map { 17 | div(classes = "dot") 18 | } 19 | div(classes = "loading") 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/components/form/Form.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.components.form 2 | 3 | import com.packtpub.model.Language 4 | import com.packtpub.model.Project 5 | import kotlinx.html.* 6 | import react.RProps 7 | import react.ReactComponentStatelessSpec 8 | import react.dom.ReactDOMBuilder 9 | import react.dom.ReactDOMStatelessComponent 10 | 11 | 12 | class Form : ReactDOMStatelessComponent() { 13 | companion object : ReactComponentStatelessSpec 14 | 15 | 16 | override fun ReactDOMBuilder.render() { 17 | div(classes = "container") { 18 | form { 19 | div(classes = "form-group") { 20 | label { 21 | attributes["htmlFor"] = "name" 22 | +"Name" 23 | } 24 | TextInput { 25 | id = "name" 26 | change = { 27 | props.update(it, "name") 28 | } 29 | value = props.project.name 30 | } 31 | } 32 | div(classes = "form-group") { 33 | label { 34 | attributes["htmlFor"] = "url" 35 | +"URL" 36 | } 37 | TextInput { 38 | id = "url" 39 | change = { 40 | props.update(it, "url") 41 | } 42 | value = props.project.url 43 | } 44 | } 45 | div(classes = "form-group") { 46 | label { 47 | attributes["htmlFor"] = "owner" 48 | +"Owner" 49 | } 50 | TextInput { 51 | id = "owner" 52 | change = { 53 | props.update(it, "owner") 54 | } 55 | value = props.project.owner 56 | } 57 | } 58 | div(classes = "form-group") { 59 | label { 60 | attributes["htmlFor"] = "language" 61 | +"Language" 62 | } 63 | DropDown { 64 | id = "language" 65 | action = { 66 | props.update(Language.valueOf(it), "language") 67 | } 68 | options = Language.values().map { it.name } 69 | value = props.project.language?.name 70 | } 71 | } 72 | } 73 | div { 74 | Button { 75 | action = props.submit 76 | text = "Submit" 77 | } 78 | Button { 79 | action = { 80 | clearState() 81 | } 82 | text = "Clear Form" 83 | } 84 | } 85 | 86 | a(href = "#list") { 87 | +"Go to list view" 88 | } 89 | } 90 | } 91 | 92 | private fun clearState() { 93 | props.clear() 94 | } 95 | 96 | class Props( 97 | var project: Project, 98 | var update: (Any, String) -> Unit, 99 | var submit: () -> Unit, 100 | var clear: () -> Unit 101 | ) : RProps() 102 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/components/form/FormComponents.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.components.form 2 | 3 | import kotlinx.html.* 4 | import kotlinx.html.js.onChangeFunction 5 | import kotlinx.html.js.onClickFunction 6 | import org.w3c.dom.events.Event 7 | import react.RProps 8 | import react.ReactComponentStatelessSpec 9 | import react.dom.ReactDOMBuilder 10 | import react.dom.ReactDOMStatelessComponent 11 | 12 | 13 | class NumberInput : ReactDOMStatelessComponent() { 14 | companion object : ReactComponentStatelessSpec 15 | 16 | override fun ReactDOMBuilder.render() { 17 | input(InputType.number, classes = "form-control") { 18 | onChangeFunction = { event: Event -> 19 | val value = event.currentTarget.asDynamic().value as String 20 | props.change(value.toInt()) 21 | } 22 | id = props.id 23 | value = props.value.toString() 24 | } 25 | } 26 | 27 | class Props(var id: String = "numberInput", 28 | var value: Int, 29 | var change: (Int) -> Unit = {}) : RProps() 30 | } 31 | 32 | 33 | class TextInput : ReactDOMStatelessComponent() { 34 | companion object : ReactComponentStatelessSpec 35 | 36 | override fun ReactDOMBuilder.render() { 37 | input(InputType.text, classes = "form-control") { 38 | onChangeFunction = { event: Event -> 39 | val value = event.currentTarget.asDynamic().value as String 40 | props.change(value) 41 | } 42 | id = props.id 43 | value = props.value 44 | } 45 | } 46 | 47 | class Props(var id: String = "numberInput", 48 | var value: String, 49 | var change: (String) -> Unit = {}) : RProps() 50 | } 51 | 52 | 53 | class Button : ReactDOMStatelessComponent() { 54 | companion object : ReactComponentStatelessSpec 55 | 56 | override fun ReactDOMBuilder.render() { 57 | button(classes = "btn btn-primary p-2 mr-2") { 58 | onClickFunction = { event: Event -> 59 | event.preventDefault() 60 | props.action() 61 | } 62 | +props.text 63 | } 64 | } 65 | 66 | class Props(var text: String, 67 | var action: () -> Unit = {}) : RProps() 68 | } 69 | 70 | class DropDown : ReactDOMStatelessComponent() { 71 | companion object : ReactComponentStatelessSpec 72 | 73 | override fun ReactDOMBuilder.render() { 74 | val defaultValue = "Select One" 75 | select(classes = "form-control") { 76 | option { 77 | +defaultValue 78 | } 79 | props.options.map { 80 | option { 81 | +it 82 | } 83 | } 84 | onChangeFunction = { event: Event -> 85 | val value = event.currentTarget.asDynamic().value as String 86 | props.action(value) 87 | } 88 | attributes["value"] = props.value ?: defaultValue 89 | } 90 | } 91 | 92 | class Props(var options: List, 93 | var value: String? = null, 94 | var action: (String) -> Unit = {}) : RProps() 95 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/index.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnsafeCastFromDynamic") 2 | 3 | package com.packtpub 4 | 5 | import com.packtpub.components.RouterComponent 6 | import com.packtpub.components.routerComponent 7 | import com.packtpub.store.* 8 | import org.w3c.dom.events.EventListener 9 | import react.dom.ReactDOM 10 | import react.dom.render 11 | import redux.* 12 | import kotlin.browser.document 13 | import kotlin.browser.window 14 | 15 | fun main(args: Array) { 16 | val reduxStore = Redux.createStore(::mainReducer, ReduxStore(), 17 | composeWithDevTools(Redux.applyMiddleware(ReduxThunk)) 18 | ) 19 | val container = document.getElementById("container") 20 | fun render() { 21 | ReactDOM.render(container) { 22 | ProviderComponent { 23 | store = reduxStore 24 | children = RouterComponent.asConnectedElement(routerComponent) 25 | } 26 | 27 | } 28 | } 29 | 30 | window.addEventListener("hashchange", EventListener { 31 | val hash = window.location.hash.substring(1) 32 | reduxStore.dispatch(ReduxAction(ActionType.HASH_CHANGE, HashChange(hash))) 33 | }) 34 | reduxStore.dispatch(ReduxAction(ActionType.HASH_CHANGE, HashChange(window.location.hash.substring(1)))) 35 | reduxStore.dispatch(ReduxAction(ActionType.POPULATE_PROJECTS, PopulateProject(grabData()))) 36 | reduxStore.doDispatch(fetchData()) 37 | render() 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/model/Model.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.model 2 | 3 | data class Project( 4 | val name: String = "", 5 | val url: String = "", 6 | val owner: String = "", 7 | val language: Language?, 8 | val id: Long? = null, 9 | val extraInfo: ExtraInfo? = ExtraInfo() 10 | ) { 11 | companion object { 12 | fun identity() = Project(language = null) 13 | } 14 | } 15 | 16 | data class ExtraInfo( 17 | val description: String? = "", 18 | val license: String? = null, 19 | val topics: List = listOf() 20 | ) 21 | 22 | enum class Language { KOTLIN, JAVASCRIPT, JAVA } 23 | 24 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/store/ActionCreators.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnsafeCastFromDynamic") 2 | 3 | package com.packtpub.store 4 | 5 | import com.packtpub.model.ExtraInfo 6 | import com.packtpub.model.Language 7 | import com.packtpub.model.Project 8 | import com.packtpub.util.* 9 | import redux.ReduxAction 10 | import kotlin.browser.document 11 | 12 | 13 | fun submitForm(proj: Project): ((dynamic) -> Unit, () -> ReduxStore) -> Unit { 14 | return { dispatch: (dynamic) -> Unit, _: () -> ReduxStore -> 15 | dispatch(ReduxAction(ActionType.SPINNING, BooleanAction(true))()) 16 | dispatch(ReduxAction(ActionType.HASH_CHANGE, HashChange("list"))()) 17 | async { 18 | val savedProject = postAndParseResult("api/projects/", jsObject { 19 | name = proj.name 20 | url = proj.url 21 | owner = proj.owner 22 | language = proj.language?.name ?: "KOTLIN" 23 | }, ::extractProject) 24 | dispatch(ReduxAction(ActionType.FORM_SUBMIT, FormSubmit(savedProject))()) 25 | dispatch(ReduxAction(ActionType.SPINNING, BooleanAction(false))()) 26 | } 27 | } 28 | } 29 | 30 | fun fetchData(): ((dynamic) -> Unit, () -> ReduxStore) -> Unit { 31 | return { dispatch: (dynamic) -> Unit, _: () -> ReduxStore -> 32 | dispatch(ReduxAction(ActionType.SPINNING, BooleanAction(true))()) 33 | async { 34 | val projects = getAndParseResult("api/projects/") { 35 | (it as Array).map { 36 | extractProject(it) 37 | }.toTypedArray() 38 | } 39 | dispatch(ReduxAction(ActionType.POPULATE_PROJECTS, PopulateProject(projects))()) 40 | dispatch(ReduxAction(ActionType.SPINNING, BooleanAction(false))()) 41 | } 42 | } 43 | } 44 | 45 | fun grabData(): Array { 46 | val unescape = require("lodash/unescape") 47 | val projectJson = js("__projects__") 48 | document.getElementById("contentHolder")?.remove() 49 | return JSON.parse>(unescape(projectJson)).map { 50 | extractProject(it) 51 | }.toTypedArray() 52 | } 53 | 54 | private fun extractProject(it: dynamic): Project { 55 | val project = Project( 56 | it.name, it.url, it.owner, Language.valueOf(it.language), it.id 57 | ) 58 | 59 | if (it.extrainfo != null) { 60 | val extraInfo = ExtraInfo(it.extraInfo.description, it.extraInfo.license, 61 | arrayOf(it.extraInfo.topics).asList()) 62 | project.copy(extraInfo = extraInfo) 63 | } 64 | return project 65 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/store/Actions.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("ArrayInDataClass") 2 | 3 | package com.packtpub.store 4 | 5 | import com.packtpub.model.Project 6 | import redux.ActionPayload 7 | 8 | enum class ActionType { 9 | HASH_CHANGE, 10 | FORM_SUBMIT, 11 | FORM_INPUT, 12 | FORM_CLEAR, 13 | SPINNING, 14 | POPULATE_PROJECTS 15 | } 16 | 17 | data class HashChange(val newHash: String) : ActionPayload 18 | data class FormInput(val value: Any, val target: Any) : ActionPayload 19 | data class FormSubmit(val project: Project) : ActionPayload 20 | data class BooleanAction(val flag: Boolean) : ActionPayload 21 | data class PopulateProject(val projects: Array) : ActionPayload 22 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/store/MainReducer.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.store 2 | 3 | import com.packtpub.model.Language 4 | import com.packtpub.model.Project 5 | import redux.ReduxState 6 | 7 | 8 | fun mainReducer(reduxState: ReduxStore, reduxAction: dynamic): ReduxState = 9 | if (reduxAction.type == "@@INIT") { 10 | reduxState 11 | } else { 12 | when (ActionType.valueOf(reduxAction.type)) { 13 | ActionType.HASH_CHANGE -> { 14 | val hashChange = reduxAction.payload as HashChange 15 | reduxState.copy(hash = hashChange.newHash) 16 | } 17 | 18 | ActionType.FORM_SUBMIT -> { 19 | val newProject = reduxAction.payload as FormSubmit 20 | val newList = reduxState.projectList + newProject.project 21 | reduxState.copy(projectList = newList, currentProject = Project.identity()) 22 | } 23 | ActionType.FORM_CLEAR -> reduxState.copy(currentProject = Project.identity()) 24 | ActionType.FORM_INPUT -> { 25 | val formInput = reduxAction.payload as FormInput 26 | val currentProject = reduxState.currentProject 27 | val updatedProject = when (formInput.target) { 28 | "name" -> currentProject.copy(name = formInput.value as String) 29 | "url" -> currentProject.copy(url = formInput.value as String) 30 | "owner" -> currentProject.copy(owner = formInput.value as String) 31 | "language" -> currentProject.copy(language = formInput.value as Language) 32 | else -> currentProject 33 | } 34 | reduxState.copy(currentProject = updatedProject) 35 | } 36 | ActionType.SPINNING -> { 37 | val booleanAction = reduxAction.payload as BooleanAction 38 | reduxState.copy(isSpinning = booleanAction.flag) 39 | } 40 | ActionType.POPULATE_PROJECTS -> { 41 | val populateProject = reduxAction.payload as PopulateProject 42 | reduxState.copy(projectList = populateProject.projects) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/store/ReduxStore.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("ArrayInDataClass") 2 | 3 | package com.packtpub.store 4 | 5 | import com.packtpub.model.Project 6 | import redux.ReduxState 7 | 8 | 9 | 10 | data class ReduxStore( 11 | val hash: String = "", 12 | val projectList: Array = arrayOf(), 13 | val currentProject: Project = Project.identity(), 14 | val isSpinning: Boolean = false 15 | ) : ReduxState 16 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/util/XHR.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.util 2 | 3 | import org.w3c.fetch.RequestCredentials 4 | import org.w3c.fetch.RequestInit 5 | import kotlin.browser.window 6 | import kotlin.js.json 7 | 8 | 9 | suspend fun postAndParseResult(url: String, body: dynamic, parse: (dynamic) -> T): T = 10 | requestAndParseResult("POST", url, body, parse) 11 | 12 | suspend fun getAndParseResult(url: String, parse: (dynamic) -> T): T = 13 | requestAndParseResult("GET", url, null, parse) 14 | 15 | @Suppress("UnsafeCastFromDynamic") 16 | suspend fun requestAndParseResult(method: String, url: String, 17 | body: dynamic, parse: (dynamic) -> T): T { 18 | val response = window.fetch(url, object : RequestInit { 19 | override var method: String? = method 20 | override var body: dynamic = if(body != null) JSON.stringify(body) else undefined 21 | override var credentials: RequestCredentials? = "same-origin".asDynamic() 22 | override var headers: dynamic = json( 23 | "Accept" to "application/json", 24 | "Content-Type" to "application/json") 25 | }).await() 26 | return parse(response.json().await()) 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/util/coroutines.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.util 2 | 3 | import kotlin.coroutines.experimental.* 4 | import kotlin.js.Promise 5 | 6 | 7 | suspend fun Promise.await() = suspendCoroutine { cont -> 8 | then({ value -> cont.resume(value) }, 9 | { exception -> cont.resumeWithException(exception) } 10 | ) 11 | } 12 | 13 | fun async(block: suspend () -> T): Promise = Promise { resolve, reject -> 14 | block.startCoroutine(object : Continuation { 15 | override val context: CoroutineContext get() = EmptyCoroutineContext 16 | override fun resume(value: T) { 17 | resolve(value) 18 | } 19 | 20 | override fun resumeWithException(exception: Throwable) { 21 | reject(exception) 22 | } 23 | }) 24 | } 25 | 26 | fun launch(block: suspend () -> Unit) { 27 | async(block).catch { exception -> console.log("Failed with $exception") } 28 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/com/packtpub/util/util.kt: -------------------------------------------------------------------------------- 1 | package com.packtpub.util 2 | 3 | import org.w3c.dom.HTMLInputElement 4 | import org.w3c.dom.HTMLTextAreaElement 5 | import org.w3c.dom.events.Event 6 | import kotlin.reflect.KClass 7 | 8 | external fun require(module: String): dynamic 9 | 10 | inline fun jsObject(builder: T.() -> Unit): T { 11 | val obj: T = js("({})") 12 | return obj.apply { 13 | builder() 14 | } 15 | } 16 | 17 | inline fun js(builder: dynamic.() -> Unit): dynamic = jsObject(builder) 18 | 19 | @Suppress("UnsafeCastFromDynamic") 20 | fun Any.getOwnPropertyNames(): Array { 21 | @Suppress("UNUSED_VARIABLE") 22 | val me = this 23 | return js("Object.getOwnPropertyNames(me)") 24 | } 25 | 26 | fun toPlainObjectStripNull(me: Any): dynamic { 27 | val obj = js("({})") 28 | for (p in me.getOwnPropertyNames().filterNot { it == "__proto__" || it == "constructor" }) { 29 | js("if (me[p] != null) { obj[p]=me[p] }") 30 | } 31 | return obj 32 | } 33 | 34 | fun jsstyle(builder: dynamic.() -> Unit): String = js(builder) 35 | 36 | internal val Event.inputValue: String 37 | get() = (target as? HTMLInputElement)?.value ?: (target as? HTMLTextAreaElement)?.value ?: "" 38 | 39 | fun KClass.createInstance(): T { 40 | @Suppress("UNUSED_VARIABLE") 41 | val ctor = this.js 42 | 43 | return js("new ctor()") 44 | } 45 | 46 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/react/Helpers.kt: -------------------------------------------------------------------------------- 1 | package react 2 | 3 | 4 | class ReactComponentNoState : RState 5 | 6 | class ReactComponentNoProps : RProps() 7 | 8 | class ReactComponentEmptyProps : RProps() 9 | 10 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/react/Imports.kt: -------------------------------------------------------------------------------- 1 | package react 2 | 3 | 4 | external interface ReactUpdater { 5 | fun enqueueSetState(dest: Any, state: Any?) 6 | fun enqueueReplaceState(dest: Any, state: Any?) 7 | fun enqueueCallback(dest: Any, callback: Any, method: String) 8 | } 9 | 10 | @JsModule("react") 11 | @JsNonModule 12 | external object React { 13 | fun createElement(type: Any, props: dynamic, vararg child: Any): ReactElement 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/react/ReactBuilder.kt: -------------------------------------------------------------------------------- 1 | package react 2 | 3 | import com.packtpub.util.createInstance 4 | 5 | 6 | @DslMarker 7 | annotation class ReactDsl 8 | 9 | open class ReactBuilder { 10 | open class Node( 11 | val type: Any, 12 | val props: P 13 | ) { 14 | var children: ArrayList = ArrayList() 15 | 16 | open val realType 17 | get() = type 18 | 19 | fun create() : ReactElement { 20 | return ReactWrapper.createRaw(realType, props, children) 21 | } 22 | } 23 | 24 | val path = mutableListOf>() 25 | private var lastLeaved: ReactElement? = null 26 | 27 | val children: ArrayList 28 | get() = currentNode().children 29 | 30 | fun currentNode(): Node<*> = path.last() 31 | inline fun > currentNodeOfType(): T = currentNode() as T 32 | 33 | fun > enterNode(node: T) { 34 | if (path.isEmpty() && lastLeaved != null) { 35 | console.error("React only allows single element be returned from render() function") 36 | } 37 | path.add(node) 38 | } 39 | 40 | fun exitCurrentNode() : ReactElement { 41 | val node = path.removeAt(path.lastIndex) 42 | val element = node.create() 43 | if (path.isNotEmpty()) { 44 | children.add(element) 45 | } 46 | lastLeaved = element 47 | return element 48 | } 49 | 50 | open fun createReactNode(type: Any, props: P) : Node = Node(type, props) 51 | 52 | fun enterReactNode(type: Any, props: P, handler: ReactBuilder.() -> Unit) : ReactElement { 53 | enterNode(createReactNode(type, props)) 54 | handler() 55 | return exitCurrentNode() 56 | } 57 | 58 | inline fun instantiateProps() : P { 59 | return P::class.createInstance() 60 | } 61 | 62 | internal inline operator fun , reified P : RProps, S: RState> ReactComponentSpec.invoke( 63 | noinline handler: P.() -> Unit = {} 64 | ) : ReactElement { 65 | val props = instantiateProps

() 66 | return node(props) { props.handler() } 67 | } 68 | 69 | internal inline operator fun , reified P : RProps, S: RState> ReactComponentSpec.invoke( 70 | props: P, 71 | noinline handler: P.() -> Unit = {} 72 | ) : ReactElement { 73 | return node(props) { props.handler() } 74 | } 75 | 76 | inline fun , reified P : RProps, S: RState> ReactComponentSpec.node( 77 | props: P, 78 | noinline handler: ReactBuilder.() -> Unit = {} 79 | ) = enterReactNode(ReactComponent.wrap(T::class), props, handler) 80 | 81 | internal inline operator fun ReactExternalComponentSpec

.invoke( 82 | noinline handler: P.() -> Unit = {} 83 | ) : ReactElement { 84 | val props = instantiateProps

() 85 | return node(props) { props.handler() } 86 | } 87 | 88 | inline fun ReactExternalComponentSpec

.node( 89 | props: P, 90 | noinline handler: ReactBuilder.() -> Unit = {} 91 | ) = enterReactNode(ref, props, handler) 92 | 93 | fun result(): ReactElement? { 94 | return lastLeaved 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/react/ReactComponent.kt: -------------------------------------------------------------------------------- 1 | package react 2 | 3 | import com.packtpub.util.createInstance 4 | import kotlin.reflect.KClass 5 | 6 | abstract class RProps { 7 | var key: String? = null 8 | var children: Any? = null 9 | } 10 | 11 | external interface RState 12 | 13 | class BoxedState(var state: T) : RState 14 | 15 | interface ReactComponentSpec, P : RProps, S : RState> 16 | interface ReactComponentStatelessSpec, P : RProps> : ReactComponentSpec 17 | interface ReactComponentPropslessSpec, S : RState> : ReactComponentSpec 18 | interface ReactComponentStaticSpec> : ReactComponentSpec 19 | 20 | 21 | private var initWrapper: ReactComponentWrapper<*, *, *>? = null 22 | 23 | @Suppress("UNCHECKED_CAST") 24 | abstract class ReactComponent

: ReactExtensionProvider { 25 | 26 | internal val wrapper = initWrapper as ReactComponentWrapper<*, *, S> 27 | internal lateinit var stateField: S 28 | internal var isSealed = false 29 | internal var hasState = false 30 | val props: P 31 | get() = wrapper.props.asDynamic() 32 | 33 | var state: S 34 | get() = stateField 35 | set(value) { 36 | if (!isSealed) { 37 | stateField = value 38 | hasState = true 39 | } else { 40 | throw RuntimeException("You can't set initial state not in constructor") 41 | } 42 | } 43 | 44 | fun setState(builder: S.() -> Unit) { 45 | if (!isSealed) { 46 | state.builder() 47 | } else { 48 | wrapper.setState(builder) 49 | } 50 | } 51 | 52 | fun replaceState(state: S) { 53 | if (!isSealed) { 54 | this.state = state 55 | } else { 56 | wrapper.replaceState(state) 57 | } 58 | } 59 | 60 | internal fun seal() { 61 | isSealed = true 62 | } 63 | 64 | internal fun setStateFromWrapper(state: S) { 65 | stateField = state 66 | hasState = true 67 | } 68 | 69 | companion object { 70 | 71 | private val wrappers = HashMap() 72 | 73 | inline fun wrap(): (P, Any, ReactUpdater) -> ReactComponentWrapper where K : ReactComponent { 74 | return wrap(K::class) 75 | } 76 | 77 | fun wrap(clazz: KClass): (P, Any, ReactUpdater) -> ReactComponentWrapper where K : ReactComponent { 78 | if (wrappers[clazz] == null) { 79 | wrappers[clazz] = { p: P, _: Any, updater: ReactUpdater -> ReactComponentWrapper(p, updater, clazz) } 80 | wrappers[clazz].asDynamic().displayName = clazz.js.name 81 | } 82 | return wrappers[clazz] as (P, Any, ReactUpdater) -> ReactComponentWrapper 83 | } 84 | } 85 | 86 | abstract fun render(): ReactElement? 87 | open fun componentDidCatch(error: dynamic, info: dynamic) { 88 | println(error) 89 | println(info) 90 | } 91 | 92 | open fun componentWillMount() { 93 | 94 | } 95 | 96 | open fun componentDidMount() { 97 | 98 | } 99 | 100 | open fun componentWillUnmount() { 101 | 102 | } 103 | 104 | open fun componentDidUpdate(prevProps: P, prevState: S) { 105 | 106 | } 107 | 108 | open fun shouldComponentUpdate(nextProps: P, nextState: S): Boolean { 109 | return true 110 | } 111 | 112 | open fun componentWillUpdate() { 113 | 114 | } 115 | 116 | open fun componentWillReceiveProps(nextProps: P) { 117 | 118 | } 119 | 120 | override fun subscribe(listener: ReactComponentLifecycleListener) { 121 | wrapper.subscribers.add(listener) 122 | } 123 | 124 | override fun unsubscribe(listener: ReactComponentLifecycleListener) { 125 | wrapper.subscribers.remove(listener) 126 | } 127 | } 128 | 129 | // 130 | // Wrapper Class 131 | // Passed directly to React and proxifies all method calls to a real one 132 | // Created for not mixing react and kotlin (overridable) functions and for having ability 133 | // to alter our component's behaviour with powerful kotlin black magic 134 | // 135 | 136 | class ReactComponentWrapper(var props: P, val updater: ReactUpdater, val klazz: KClass) where K : ReactComponent { 137 | 138 | private val delegate: K 139 | private var stateField: S 140 | var state: S 141 | get() = stateField 142 | set(value) { 143 | stateField = value 144 | delegate.setStateFromWrapper(value) 145 | } 146 | var subscribers = ArrayList() 147 | 148 | init { 149 | val oldGlobal = initWrapper 150 | initWrapper = this 151 | delegate = klazz.createInstance() 152 | delegate.seal() 153 | initWrapper = oldGlobal 154 | 155 | if (!delegate.hasState) { 156 | throw RuntimeException("You haven't set initial state in your constructor of ${klazz.simpleName}!") 157 | } 158 | this.stateField = delegate.state 159 | } 160 | 161 | fun setState(stateBuilder: S.() -> Unit) { 162 | val partialState: S = js("({})") 163 | partialState.stateBuilder() 164 | 165 | updater.enqueueSetState(this, partialState) 166 | } 167 | 168 | fun replaceState(state: S) { 169 | updater.enqueueReplaceState(this, state) 170 | } 171 | 172 | @JsName("render") 173 | fun render(): ReactElement? { 174 | return delegate.render() 175 | } 176 | 177 | @JsName("shouldComponentUpdate") 178 | fun shouldComponentUpdate(nextProps: P, nextState: S): Boolean { 179 | return delegate.shouldComponentUpdate(nextProps, nextState) 180 | } 181 | 182 | @JsName("componentWillReceiveProps") 183 | fun componentWillReceiveProps(nextProps: P) { 184 | delegate.componentWillReceiveProps(nextProps) 185 | } 186 | 187 | @JsName("componentWillUpdate") 188 | fun componentWillUpdate() { 189 | subscribers.forEach { 190 | it.reactComponentWillUpdate() 191 | } 192 | delegate.componentWillUpdate() 193 | } 194 | 195 | @JsName("componentDidUpdate") 196 | fun componentDidUpdate(prevProps: P, prevState: S) { 197 | delegate.componentDidUpdate(prevProps, prevState) 198 | } 199 | 200 | @JsName("componentWillUnmount") 201 | fun componentWillUnmount() { 202 | subscribers.forEach { 203 | it.reactComponentWillUnmount() 204 | } 205 | delegate.componentWillUnmount() 206 | } 207 | 208 | @JsName("componentWillMount") 209 | fun componentWillMount() { 210 | subscribers.forEach { 211 | it.reactComponentWillMount() 212 | } 213 | delegate.componentWillMount() 214 | } 215 | 216 | @JsName("componentDidMount") 217 | fun componentDidMount() { 218 | subscribers.forEach { 219 | it.reactComponentDidMount() 220 | } 221 | delegate.componentDidMount() 222 | } 223 | 224 | @JsName("componentDidCatch") 225 | fun componentDidCatch(error: dynamic, info: dynamic) { 226 | delegate.componentDidCatch(error, info) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/react/ReactExtensions.kt: -------------------------------------------------------------------------------- 1 | package react 2 | 3 | import kotlin.properties.ReadOnlyProperty 4 | import kotlin.properties.ReadWriteProperty 5 | 6 | interface ReactComponentLifecycleListener { 7 | fun reactComponentWillUpdate() 8 | 9 | fun reactComponentWillUnmount() 10 | 11 | fun reactComponentWillMount() 12 | 13 | fun reactComponentDidMount() 14 | } 15 | 16 | interface ReactExtensionProvider { 17 | fun subscribe(listener: ReactComponentLifecycleListener) 18 | fun unsubscribe(listener: ReactComponentLifecycleListener) 19 | } 20 | 21 | abstract class BaseReactExtension(val provider: ReactExtensionProvider) { 22 | 23 | private val listener = object : ReactComponentLifecycleListener { 24 | override fun reactComponentWillUpdate() { 25 | componentWillUpdate() 26 | } 27 | 28 | override fun reactComponentWillUnmount() { 29 | provider.unsubscribe(this) 30 | componentWillUnmount() 31 | } 32 | 33 | override fun reactComponentWillMount() { 34 | componentWillMount() 35 | } 36 | 37 | override fun reactComponentDidMount() { 38 | componentDidMount() 39 | } 40 | } 41 | 42 | init { 43 | provider.subscribe(listener) 44 | } 45 | 46 | open fun componentWillUpdate() {} 47 | 48 | open fun componentWillUnmount() {} 49 | 50 | open fun componentWillMount() {} 51 | 52 | open fun componentDidMount() {} 53 | } 54 | 55 | abstract class BaseReactExtensionReadWriteProperty(provider: ReactExtensionProvider) : BaseReactExtension(provider), ReadWriteProperty { 56 | 57 | } 58 | 59 | abstract class BaseReactExtensionReadOnlyProperty(provider: ReactExtensionProvider) : BaseReactExtension(provider), ReadOnlyProperty { 60 | 61 | } 62 | 63 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/react/ReactExternalComponent.kt: -------------------------------------------------------------------------------- 1 | package react 2 | 3 | open class ReactExternalComponentSpec

(val ref: dynamic) 4 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/react/ReactWrapper.kt: -------------------------------------------------------------------------------- 1 | package react 2 | 3 | import com.packtpub.util.toPlainObjectStripNull 4 | 5 | interface ReactElement 6 | 7 | internal object ReactWrapper { 8 | fun normalize(child: Any?) : List = when(child) { 9 | null -> listOf() 10 | is Iterable<*> -> child.filterNotNull() 11 | is Array<*> -> child.filterNotNull() 12 | else -> listOf(child) 13 | } 14 | 15 | fun createRaw(type: Any, props: dynamic, child: Any? = null): ReactElement = 16 | React.createElement(type, toPlainObjectStripNull(props), *normalize(child).toTypedArray()) 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/react/dom/ReactDOM.kt: -------------------------------------------------------------------------------- 1 | package react.dom 2 | 3 | import org.w3c.dom.Element 4 | import react.RProps 5 | import react.RState 6 | import react.ReactComponent 7 | import react.ReactElement 8 | 9 | @JsModule("react-dom") 10 | external object ReactDOM { 11 | fun render(element: ReactElement?, container: Element?) 12 | fun findDOMNode(component: ReactComponent): Element 13 | fun unmountComponentAtNode(domContainerNode: Element?) 14 | } 15 | 16 | fun ReactDOM.render(container: Element?, handler: ReactDOMBuilder.() -> Unit) = 17 | render(buildElement(handler), container) 18 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/react/dom/ReactDOMAttributes.kt: -------------------------------------------------------------------------------- 1 | package react.dom 2 | 3 | import kotlinx.html.INPUT 4 | import kotlinx.html.TEXTAREA 5 | import kotlinx.html.attributes.Attribute 6 | import kotlinx.html.attributes.StringAttribute 7 | 8 | private val events = listOf( 9 | "onCopy", 10 | "onCut", 11 | "onPaste", 12 | "onCompositionEnd", 13 | "onCompositionStart", 14 | "onCompositionUpdate", 15 | "onKeyDown", 16 | "onKeyPress", 17 | "onKeyUp", 18 | "onFocus", 19 | "onBlur", 20 | "onChange", 21 | "onInput", 22 | "onSubmit", 23 | "onClick", 24 | "onContextMenu", 25 | "onDoubleClick", 26 | "onDrag", 27 | "onDragEnd", 28 | "onDragEnter", 29 | "onDragExit", 30 | "onDragLeave", 31 | "onDragOver", 32 | "onDragStart", 33 | "onDrop", 34 | "onMouseDown", 35 | "onMouseEnter", 36 | "onMouseLeave", 37 | "onMouseMove", 38 | "onMouseOut", 39 | "onMouseOver", 40 | "onMouseUp", 41 | "onSelect", 42 | "onTouchCancel", 43 | "onTouchEnd", 44 | "onTouchMove", 45 | "onTouchStart", 46 | "onScroll", 47 | "onWheel", 48 | "onAbort", 49 | "onCanPlay", 50 | "onCanPlayThrough", 51 | "onDurationChange", 52 | "onEmptied", 53 | "onEncrypted", 54 | "onEnded", 55 | "onError", 56 | "onLoadedData", 57 | "onLoadedMetadata", 58 | "onLoadStart", 59 | "onPause", 60 | "onPlay", 61 | "onPlaying", 62 | "onProgress", 63 | "onRateChange", 64 | "onSeeked", 65 | "onSeeking", 66 | "onStalled", 67 | "onSuspend", 68 | "onTimeUpdate", 69 | "onVolumeChange", 70 | "onWaiting", 71 | "onLoad", 72 | "onError", 73 | "onAnimationStart", 74 | "onAnimationEnd", 75 | "onAnimationIteration", 76 | "onTransitionEnd", 77 | 78 | 79 | // HTML attributes 80 | "accept", 81 | "acceptCharset", 82 | "accessKey", 83 | "action", 84 | "allowFullScreen", 85 | "allowTransparency", 86 | "alt", 87 | "async", 88 | "autoComplete", 89 | "autoFocus", 90 | "autoPlay", 91 | "capture", 92 | "cellPadding", 93 | "cellSpacing", 94 | "challenge", 95 | "charSet", 96 | "checked", 97 | "cite", 98 | "classID", 99 | "className", 100 | "colSpan", 101 | "cols", 102 | "content", 103 | "contentEditable", 104 | "contextMenu", 105 | "controls", 106 | "coords", 107 | "crossOrigin", 108 | "data", 109 | "dateTime", 110 | "default", 111 | "defer", 112 | "dir", 113 | "disabled", 114 | "download", 115 | "draggable", 116 | "encType", 117 | "form", 118 | "formAction", 119 | "formEncType", 120 | "formMethod", 121 | "formNoValidate", 122 | "formTarget", 123 | "frameBorder", 124 | "headers", 125 | "height", 126 | "hidden", 127 | "high", 128 | "href", 129 | "hrefLang", 130 | "htmlFor", 131 | "httpEquiv", 132 | "icon", 133 | "id", 134 | "inputMode", 135 | "integrity", 136 | "is", 137 | "keyParams", 138 | "keyType", 139 | "kind", 140 | "label", 141 | "lang", 142 | "list", 143 | "loop", 144 | "low", 145 | "manifest", 146 | "marginHeight", 147 | "marginWidth", 148 | "max", 149 | "maxLength", 150 | "media", 151 | "mediaGroup", 152 | "method", 153 | "min", 154 | "minLength", 155 | "multiple", 156 | "muted", 157 | "name", 158 | "noValidate", 159 | "nonce", 160 | "open", 161 | "optimum", 162 | "pattern", 163 | "placeholder", 164 | "poster", 165 | "preload", 166 | "profile", 167 | "radioGroup", 168 | "readOnly", 169 | "rel", 170 | "required", 171 | "reversed", 172 | "role", 173 | "rowSpan", 174 | "rows", 175 | "sandbox", 176 | "scope", 177 | "scoped", 178 | "scrolling", 179 | "seamless", 180 | "selected", 181 | "shape", 182 | "size", 183 | "sizes", 184 | "span", 185 | "spellCheck", 186 | "src", 187 | "srcDoc", 188 | "srcLang", 189 | "srcSet", 190 | "start", 191 | "step", 192 | "style", 193 | "summary", 194 | "tabIndex", 195 | "target", 196 | "title", 197 | "type", 198 | "useMap", 199 | "value", 200 | "width", 201 | "wmode", 202 | "wrap") 203 | 204 | private val eventMap = events.map {it.toLowerCase() to it}.toMap() 205 | 206 | fun fixAttributeName(event: String): String = eventMap[event] ?: if (event == "class") "className" else event 207 | 208 | private val attributeStringString : Attribute = StringAttribute() 209 | 210 | // See https://facebook.github.io/react/docs/forms.html 211 | var INPUT.defaultValue : String 212 | get() = attributeStringString.get(this, "defaultValue") 213 | set(newValue) {attributeStringString.set(this, "defaultValue", newValue)} 214 | 215 | var TEXTAREA.defaultValue : String 216 | get() = attributeStringString.get(this, "defaultValue") 217 | set(newValue) {attributeStringString.set(this, "defaultValue", newValue)} 218 | 219 | var TEXTAREA.value : String 220 | get() = attributeStringString.get(this, "value") 221 | set(newValue) {attributeStringString.set(this, "value", newValue)} 222 | 223 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/react/dom/ReactDOMBuilder.kt: -------------------------------------------------------------------------------- 1 | package react.dom 2 | 3 | import kotlinx.html.* 4 | import org.w3c.dom.events.Event 5 | import react.RProps 6 | import react.ReactBuilder 7 | import react.ReactElement 8 | 9 | class InnerHTML ( 10 | val __html: String 11 | ) 12 | 13 | class DOMProps: RProps() { 14 | var dangerouslySetInnerHTML: InnerHTML? = null 15 | } 16 | 17 | class ReactDOMBuilder : ReactBuilder(), TagConsumer { 18 | 19 | override fun createReactNode(type: Any, props: P) = Node(type, props) 20 | 21 | class DOMNode(val tagName: String) : Node(tagName, DOMProps()) 22 | 23 | private fun currentDOMNode() = currentNodeOfType() 24 | 25 | var HTMLTag.key: String 26 | get() { 27 | return currentDOMNode().props.key ?: "" 28 | } 29 | set(value) { 30 | currentDOMNode().props.key = value 31 | } 32 | 33 | fun setProp(attribute: String, value: dynamic) { 34 | val node = currentNode() 35 | val key = fixAttributeName(attribute) 36 | if (value == null) { 37 | js("delete node.props[key]") 38 | } else { 39 | node.props.asDynamic()[key] = value 40 | } 41 | } 42 | 43 | override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) { 44 | setProp(attribute, value) 45 | } 46 | 47 | operator fun String.unaryPlus() { 48 | onTagContent(this) 49 | } 50 | 51 | override fun onTagContent(content: CharSequence): Unit { 52 | children.add(content) 53 | } 54 | 55 | override fun onTagContentEntity(entity: Entities): Unit { 56 | children.add(entity.text) 57 | } 58 | 59 | override fun onTagContentUnsafe(block: Unsafe.() -> Unit) { 60 | val sb = StringBuilder() 61 | object : Unsafe { 62 | override fun String.unaryPlus() { 63 | sb.append(this) 64 | } 65 | }.block() 66 | val node = currentDOMNode() 67 | node.props.dangerouslySetInnerHTML = InnerHTML(sb.toString()) 68 | } 69 | 70 | override fun onTagStart(tag: Tag) { 71 | enterNode(DOMNode(tag.tagName)) 72 | tag.attributesEntries.forEach { setProp(it.key, it.value) } 73 | } 74 | 75 | override fun onTagEnd(tag: Tag) { 76 | if (path.isEmpty() || currentDOMNode().tagName.toLowerCase() != tag.tagName.toLowerCase()) 77 | throw IllegalStateException("We haven't entered tag ${tag.tagName} but trying to leave") 78 | exitCurrentNode() 79 | } 80 | 81 | override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) { 82 | setProp(event, value) 83 | } 84 | 85 | override fun finalize(): ReactElement? { 86 | return result() 87 | } 88 | } 89 | 90 | fun buildElement(handler: ReactDOMBuilder.() -> Unit) = with(ReactDOMBuilder()) { 91 | handler() 92 | finalize() 93 | } 94 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/react/dom/ReactDOMComponent.kt: -------------------------------------------------------------------------------- 1 | package react.dom 2 | 3 | import org.w3c.dom.Element 4 | import react.* 5 | 6 | abstract class ReactDOMComponent

: ReactComponent() { 7 | abstract fun ReactDOMBuilder.render() 8 | 9 | open fun ReactBuilder.children() { 10 | children.addAll(ReactWrapper.normalize(props.children)) 11 | } 12 | 13 | val DOMNode: Element 14 | get() = ReactDOM.findDOMNode(this) 15 | 16 | override fun render() = buildElement { render() } 17 | } 18 | 19 | abstract class ReactDOMStatelessComponent

: ReactDOMComponent() { 20 | init { 21 | state = ReactComponentNoState() 22 | } 23 | } 24 | 25 | abstract class ReactDOMPropslessComponent : ReactDOMComponent() 26 | 27 | abstract class ReactDOMStaticComponent : ReactDOMComponent() { 28 | init { 29 | state = ReactComponentNoState() 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/redux/Provider.kt: -------------------------------------------------------------------------------- 1 | package redux 2 | 3 | import com.packtpub.util.require 4 | import react.RProps 5 | import react.ReactExternalComponentSpec 6 | 7 | 8 | val Provider: dynamic = require("react-redux").Provider 9 | 10 | class ReactProviderProps(var store: Any) : RProps() 11 | 12 | object ProviderComponent : ReactExternalComponentSpec(Provider) 13 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/redux/ReactReduxWrappers.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("react-redux") 2 | 3 | package redux 4 | 5 | import react.RProps 6 | import react.ReactElement 7 | 8 | 9 | @JsName("connect") 10 | external fun

connect( 11 | mapStateToProps: ((ST, P) -> P)? = definedExternally, 12 | mapDispatchToProps: (((dynamic) -> Unit, P) -> P)? = definedExternally 13 | ): (Any) -> ReactElement 14 | 15 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/redux/ReduxAction.kt: -------------------------------------------------------------------------------- 1 | package redux 2 | 3 | import com.packtpub.store.ActionType 4 | import com.packtpub.util.js 5 | 6 | interface ActionPayload 7 | class EmptyPayload: ActionPayload 8 | class ReduxAction(private val type: ActionType, private val payload: ActionPayload = EmptyPayload()) { 9 | operator fun invoke(): dynamic { 10 | return js { 11 | this.type = type.name 12 | this.payload = payload 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/redux/ReduxWrappers.kt: -------------------------------------------------------------------------------- 1 | package redux 2 | 3 | import com.packtpub.util.require 4 | 5 | external interface ReduxState 6 | external class Store { 7 | @JsName("getState") 8 | fun getState(): ReduxState 9 | 10 | @JsName("dispatch") 11 | fun doDispatch(action: dynamic) 12 | } 13 | 14 | @JsModule("redux") 15 | @JsNonModule 16 | external object Redux { 17 | @JsName("createStore") 18 | fun createStore(reducer: (ST, dynamic) -> ReduxState, 19 | initialState: ST, 20 | enhancer: (dynamic) -> ST = definedExternally) 21 | : Store 22 | 23 | @JsName("applyMiddleware") 24 | fun applyMiddleware(vararg middleware: () -> (dynamic) -> dynamic) 25 | : ((dynamic) -> Unit, () -> ReduxState) -> Unit 26 | 27 | @JsName("compose") 28 | fun compose(vararg funcs: dynamic): (dynamic) -> dynamic 29 | } 30 | 31 | val ReduxThunk: dynamic = require("redux-thunk").default 32 | val composeWithDevTools: dynamic = require("redux-devtools-extension").composeWithDevTools 33 | 34 | 35 | fun Store.dispatch(action: ReduxAction) { 36 | this.doDispatch(action()) 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/redux/util.kt: -------------------------------------------------------------------------------- 1 | package redux 2 | 3 | import com.packtpub.util.createInstance 4 | import react.* 5 | 6 | 7 | inline fun , reified P : RProps, S : RState> 8 | ReactComponentSpec.asConnectedElement( 9 | connectFunction: (Any) -> ReactElement, props: P = P::class.createInstance()): Any { 10 | val wrap = ReactComponent.wrap(T::class) 11 | return React.createElement( 12 | connectFunction(ReactBuilder.Node(wrap, props).type), null) 13 | } 14 | 15 | 16 | inline fun , reified P : RProps, S : RState> 17 | ReactComponentSpec.asConnectedComponent( 18 | connectFunction: (Any) -> ReactElement, props: P = P::class.createInstance()): ReactExternalComponentSpec

{ 19 | val wrap = ReactComponent.wrap(T::class) 20 | return ReactExternalComponentSpec( 21 | connectFunction(ReactBuilder.Node(wrap, props).type).asDynamic() 22 | ) 23 | } -------------------------------------------------------------------------------- /frontend/src/main/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello Kotlin World 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/webpack.config.d/dce.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | config.resolve.modules.unshift(path.resolve("./js/min")); -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | production=true 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xantier/fullstack-kotlin/2541883cc998102c689ddd576c092aaa9927d2a1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jul 31 21:20:12 IST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'fullstack-kotlin' 2 | include 'backend' 3 | include 'backend:api' 4 | include 'backend:project' 5 | include 'backend:user' 6 | include 'frontend' 7 | rootProject.buildFileName = 'build.gradle.kts' 8 | 9 | --------------------------------------------------------------------------------