├── settings.gradle
├── frontend
├── package.json.d
│ └── project.info
├── webpack.config.d
│ ├── minify.js
│ ├── babel.js
│ └── css.js
├── src
│ └── org
│ │ └── jetbrains
│ │ ├── react
│ │ ├── ReactExternalComponent.kt
│ │ ├── Helpers.kt
│ │ ├── Imports.kt
│ │ ├── dom
│ │ │ ├── ReactDOM.kt
│ │ │ ├── ReactDOMComponent.kt
│ │ │ ├── ReactDOMBuilder.kt
│ │ │ └── ReactDOMAttributes.kt
│ │ ├── ReactWrapper.kt
│ │ ├── ReactExtensions.kt
│ │ ├── ReactBuilder.kt
│ │ └── ReactComponent.kt
│ │ ├── common
│ │ ├── jsdom.kt
│ │ ├── JsReflection.kt
│ │ ├── commonjs.kt
│ │ └── coroutines.kt
│ │ └── demo
│ │ └── thinkter
│ │ ├── model
│ │ ├── atoms.kt
│ │ └── responses.kt
│ │ ├── ReactMarkdown.kt
│ │ ├── Polling.kt
│ │ ├── ThoughtsListComponent.kt
│ │ ├── HomeView.kt
│ │ ├── NewThoughtComponent.kt
│ │ ├── ViewThoughtComponent.kt
│ │ ├── Login.kt
│ │ ├── NavBarComponent.kt
│ │ ├── Rpc.kt
│ │ ├── Register.kt
│ │ └── Application.kt
├── build.gradle
└── resources
│ └── pure-blog.css
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── vcs.xml
└── runConfigurations
│ └── Backend____Jetty.xml
├── backend
├── src
│ └── org
│ │ └── jetbrains
│ │ └── demo
│ │ └── thinkter
│ │ ├── model
│ │ ├── atoms.kt
│ │ └── responses.kt
│ │ ├── dao
│ │ ├── Users.kt
│ │ ├── Thoughts.kt
│ │ ├── ThinkterStorage.kt
│ │ └── ThinkterDatabase.kt
│ │ ├── ViewThought.kt
│ │ ├── UserPage.kt
│ │ ├── Locations.kt
│ │ ├── PostThought.kt
│ │ ├── Delete.kt
│ │ ├── Login.kt
│ │ ├── Application.kt
│ │ ├── ApplicationPage.kt
│ │ ├── Utilities.kt
│ │ ├── Index.kt
│ │ └── Register.kt
├── resources
│ ├── application.conf
│ └── logback.xml
└── build.gradle
├── .gitignore
├── README.md
├── gradlew.bat
├── gradlew
└── LICENSE
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'thinkter'
2 |
3 | include "backend", "frontend"
4 |
5 |
--------------------------------------------------------------------------------
/frontend/package.json.d/project.info:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Thinkter – full-stack Kotlin application demo"
3 | }
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angebagui/kotlin-fullstack-sample/master/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/frontend/webpack.config.d/minify.js:
--------------------------------------------------------------------------------
1 | // config.plugins.push(new webpack.optimize.UglifyJsPlugin({
2 | // minimize: true
3 | // }));
4 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/react/ReactExternalComponent.kt:
--------------------------------------------------------------------------------
1 | package react
2 |
3 | open class ReactExternalComponentSpec
(val ref: dynamic)
4 |
--------------------------------------------------------------------------------
/frontend/webpack.config.d/babel.js:
--------------------------------------------------------------------------------
1 | config.module.rules.push({
2 | test: /\.jsx?$/,
3 | exclude: /node_modules/,
4 | loader: 'babel-loader'
5 | });
6 |
--------------------------------------------------------------------------------
/frontend/webpack.config.d/css.js:
--------------------------------------------------------------------------------
1 | config.module.rules.push(
2 | {
3 | test: /\.css$/,
4 | loader: 'style-loader!css-loader'
5 | }
6 | );
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/react/Helpers.kt:
--------------------------------------------------------------------------------
1 | package react
2 |
3 | class ReactComponentNoState : RState
4 |
5 | class ReactComponentNoProps : RProps()
6 |
7 | class ReactComponentEmptyProps : RProps()
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/common/jsdom.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.common
2 |
3 | import org.w3c.dom.*
4 | import org.w3c.dom.events.*
5 |
6 | internal val Event.inputValue: String
7 | get() = (target as? HTMLInputElement)?.value ?: (target as? HTMLTextAreaElement)?.value ?: ""
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/common/JsReflection.kt:
--------------------------------------------------------------------------------
1 | package runtime.reflect
2 |
3 | import kotlin.reflect.KClass
4 |
5 | fun KClass.createInstance(): T {
6 | @Suppress("UNUSED_VARIABLE")
7 | val ctor = this.js
8 |
9 | return js("new ctor()")
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Dec 14 10:54:20 MSK 2016
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-3.1-all.zip
7 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/model/atoms.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter.model
2 |
3 | data class Thought(val id: Int, val userId: String, val text: String, val date: String, val replyTo: Int?)
4 | data class User(val userId: String, val email: String, val displayName: String, val passwordHash: String)
5 |
6 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/demo/thinkter/model/atoms.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter.model
2 |
3 | data class Thought(val id: Int, val userId: String, val text: String, val date: String, val replyTo: Int?)
4 | data class User(val userId: String, val email: String, val displayName: String, val passwordHash: String)
5 |
6 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/demo/thinkter/ReactMarkdown.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import react.*
4 |
5 | private val ReactMarkdown: dynamic = runtime.wrappers.require("react-markdown")
6 |
7 | class ReactMarkdownProps(var source: String = "") : RProps()
8 |
9 | object ReactMarkdownComponent : ReactExternalComponentSpec(ReactMarkdown)
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | backend/.database/*
2 | !backend/.database/person-data.json
3 | !backend/.database/team-data.json
4 | !backend/.database/places-data.json
5 | .idea/*
6 | !.idea/runConfigurations
7 | !.idea/runConfigurations/*
8 | !.idea/vcs.xml
9 | build
10 | *.iml
11 |
12 | *.versionsBackup
13 | *.releaseBackup
14 | release.properties
15 | local.properties
16 |
17 | node_modules
18 | npm-debug.log
19 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/dao/Users.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter.dao
2 |
3 | import org.jetbrains.squash.definition.*
4 |
5 | object Users : TableDefinition() {
6 | val id = varchar("id", 20).primaryKey()
7 | val email = varchar("email", 128).uniqueIndex()
8 | val displayName = varchar("display_name", 256)
9 | val passwordHash = varchar("password_hash", 64)
10 | }
11 |
--------------------------------------------------------------------------------
/backend/resources/application.conf:
--------------------------------------------------------------------------------
1 | ktor {
2 | deployment {
3 | port = 9090
4 | environment = development
5 | autoreload = true
6 | watch = [ thinkter ]
7 | }
8 |
9 | application {
10 | id = Thinkter
11 | modules = [org.jetbrains.demo.thinkter.ApplicationKt.main]
12 | }
13 | }
14 |
15 | database {
16 | storage = ".database"
17 | logsql = true
18 | }
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/react/Imports.kt:
--------------------------------------------------------------------------------
1 | package react
2 |
3 | external interface ReactUpdater {
4 | fun enqueueSetState(dest: Any, state: Any?)
5 | fun enqueueReplaceState(dest: Any, state: Any?)
6 | fun enqueueCallback(dest: Any, callback: Any, method: String)
7 | }
8 |
9 | @JsModule("react")
10 | @JsNonModule
11 | external object React {
12 | fun createElement(type: Any, props: dynamic, vararg child: Any): ReactElement
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/dao/Thoughts.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter.dao
2 |
3 | import org.jetbrains.squash.definition.*
4 |
5 | object Thoughts : TableDefinition() {
6 | val id = integer("id").autoIncrement().primaryKey()
7 | val user = varchar("user_id", 20).index()
8 | val date = datetime("date")
9 | val replyTo = integer("reply_to").nullable().index()
10 | val directReplyTo = integer("direct_reply_to").nullable().index()
11 | val text = varchar("text", 1024)
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/react/dom/ReactDOM.kt:
--------------------------------------------------------------------------------
1 | package react.dom
2 |
3 | import org.w3c.dom.*
4 | import react.*
5 |
6 | @JsModule("react-dom")
7 | external object ReactDOM {
8 | fun render(element: ReactElement?, container: Element?)
9 | fun findDOMNode(component: ReactComponent): Element
10 | fun unmountComponentAtNode(domContainerNode: Element?)
11 | }
12 |
13 | fun ReactDOM.render(container: Element?, handler: ReactDOMBuilder.() -> Unit) =
14 | render(buildElement(handler), container)
15 |
--------------------------------------------------------------------------------
/backend/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/react/ReactWrapper.kt:
--------------------------------------------------------------------------------
1 | package react
2 |
3 | import runtime.wrappers.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/org/jetbrains/demo/thinkter/model/responses.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter.model
2 |
3 | interface RpcData
4 |
5 | data class IndexResponse(val top: List, val latest: List) : RpcData
6 | data class PostThoughtToken(val user: String, val date: Long, val code: String) : RpcData
7 | data class PostThoughtResult(val thought: Thought) : RpcData
8 | data class UserThoughtsResponse(val user: User, val thoughts: List) : RpcData
9 | data class ViewThoughtResponse(val thought: Thought, val date: Long, val code: String?) : RpcData
10 | data class LoginResponse(val user: User? = null, val error: String? = null) : RpcData
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/model/responses.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter.model
2 |
3 | interface RpcData
4 |
5 | data class IndexResponse(val top: List, val latest: List) : RpcData
6 | data class PostThoughtToken(val user: String, val date: Long, val code: String) : RpcData
7 | data class PostThoughtResult(val thought: Thought) : RpcData
8 | data class UserThoughtsResponse(val user: User, val thoughts: List) : RpcData
9 | data class ViewThoughtResponse(val thought: Thought, val date: Long, val code: String?) : RpcData
10 | data class LoginResponse(val user: User? = null, val error: String? = null) : RpcData
11 | data class PollResponse(val time: Long, val count: String) : RpcData
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/dao/ThinkterStorage.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter.dao
2 |
3 | import org.jetbrains.demo.thinkter.model.*
4 | import java.io.*
5 | import java.time.*
6 |
7 | interface ThinkterStorage : Closeable {
8 | fun countReplies(id: Int): Int
9 | fun createThought(user: String, text: String, replyTo: Int? = null, date: LocalDateTime = LocalDateTime.now()): Int
10 | fun deleteThought(id: Int)
11 | fun getThought(id: Int): Thought
12 | fun userThoughts(userId: String): List
13 | fun user(userId: String, hash: String? = null): User?
14 | fun userByEmail(email: String): User?
15 | fun createUser(user: User)
16 | fun top(count: Long = 10): List
17 | fun latest(count: Long = 10): List
18 | }
19 |
20 |
21 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/ViewThought.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import org.jetbrains.demo.thinkter.dao.*
4 | import org.jetbrains.demo.thinkter.model.*
5 | import org.jetbrains.ktor.application.*
6 | import org.jetbrains.ktor.locations.*
7 | import org.jetbrains.ktor.routing.*
8 | import org.jetbrains.ktor.sessions.*
9 |
10 | fun Route.viewThought(dao: ThinkterStorage, hashFunction: (String) -> String) {
11 | get {
12 | val user = call.sessionOrNull()?.let { dao.user(it.userId) }
13 | val date = System.currentTimeMillis()
14 | val code = if (user != null) call.securityCode(date, user, hashFunction) else null
15 |
16 | call.respond(ViewThoughtResponse(dao.getThought(it.id), date, code))
17 | }
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/UserPage.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import org.jetbrains.demo.thinkter.dao.*
4 | import org.jetbrains.demo.thinkter.model.*
5 | import org.jetbrains.ktor.application.*
6 | import org.jetbrains.ktor.http.*
7 | import org.jetbrains.ktor.locations.*
8 | import org.jetbrains.ktor.routing.*
9 |
10 | fun Route.userPage(dao: ThinkterStorage) {
11 | get {
12 | val viewUser = dao.user(it.user)
13 |
14 | if (viewUser == null) {
15 | call.respond(HttpStatusCode.NotFound.description("User ${it.user} doesn't exist"))
16 | } else {
17 | val thoughts = dao.userThoughts(it.user).map { dao.getThought(it) }
18 | call.respond(UserThoughtsResponse(viewUser, thoughts))
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/common/commonjs.kt:
--------------------------------------------------------------------------------
1 | package runtime.wrappers
2 |
3 | external fun require(module: String): dynamic
4 |
5 | inline fun jsObject(builder: T.() -> Unit): T {
6 | val obj: T = js("({})")
7 | return obj.apply {
8 | builder()
9 | }
10 | }
11 |
12 | inline fun js(builder: dynamic.() -> Unit): dynamic = jsObject(builder)
13 |
14 | fun Any.getOwnPropertyNames(): Array {
15 | val me = this
16 | return js("Object.getOwnPropertyNames(me)")
17 | }
18 |
19 | fun toPlainObjectStripNull(me: Any): dynamic {
20 | val obj = js("({})")
21 | for (p in me.getOwnPropertyNames().filterNot { it == "__proto__" || it == "constructor" }) {
22 | js("if (me[p] != null) { obj[p]=me[p] }")
23 | }
24 | return obj
25 | }
26 |
27 | fun jsstyle(builder: dynamic.() -> Unit): String = js(builder)
28 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/common/coroutines.kt:
--------------------------------------------------------------------------------
1 | package kotlinx.coroutines.experimental
2 |
3 | import kotlin.coroutines.experimental.*
4 | import kotlin.js.Promise
5 |
6 | suspend fun Promise.await() = suspendCoroutine { cont ->
7 | then({ value -> cont.resume(value) },
8 | { exception -> cont.resumeWithException(exception) })
9 | }
10 |
11 | fun async(block: suspend () -> T): Promise = Promise { resolve, reject ->
12 | block.startCoroutine(object : Continuation {
13 | override val context: CoroutineContext get() = EmptyCoroutineContext
14 | override fun resume(value: T) { resolve(value) }
15 | override fun resumeWithException(exception: Throwable) { reject(exception) }
16 | })
17 | }
18 |
19 | fun launch(block: suspend () -> Unit) {
20 | async(block).catch { exception -> console.log("Failed with $exception") }
21 | }
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/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 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/Locations.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import org.jetbrains.ktor.locations.*
4 |
5 |
6 | @location("/")
7 | class Index()
8 |
9 | @location("/poll")
10 | class Poll(val lastTime: String = "")
11 |
12 | @location("/post-new")
13 | data class PostThought(val text: String = "", val date: Long = 0L, val code: String = "", val replyTo: Int? = null)
14 |
15 | @location("/thought/{id}/delete")
16 | data class ThoughtDelete(val id: Int, val date: Long, val code: String)
17 |
18 | @location("/thought/{id}")
19 | data class ViewThought(val id: Int)
20 |
21 | @location("/user/{user}")
22 | @Deprecated("")
23 | data class UserPage(val user: String)
24 |
25 | @location("/user/{user}/thoughts")
26 | data class UserThoughts(val user: String)
27 |
28 | @location("/register")
29 | data class Register(val userId: String = "", val displayName: String = "", val email: String = "", val password: String = "", val error: String = "")
30 |
31 | @location("/login")
32 | data class Login(val userId: String = "", val password: String = "", val error: String = "")
33 |
34 | @location("/logout")
35 | class Logout()
--------------------------------------------------------------------------------
/.idea/runConfigurations/Backend____Jetty.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/PostThought.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import org.jetbrains.demo.thinkter.dao.*
4 | import org.jetbrains.demo.thinkter.model.*
5 | import org.jetbrains.ktor.application.*
6 | import org.jetbrains.ktor.http.*
7 | import org.jetbrains.ktor.locations.*
8 | import org.jetbrains.ktor.routing.*
9 | import org.jetbrains.ktor.sessions.*
10 |
11 | fun Route.postThought(dao: ThinkterStorage, hashFunction: (String) -> String) {
12 | get {
13 | val user = call.sessionOrNull()?.let { dao.user(it.userId) }
14 |
15 | if (user == null) {
16 | call.respond(HttpStatusCode.Forbidden)
17 | } else {
18 | val date = System.currentTimeMillis()
19 | val code = call.securityCode(date, user, hashFunction)
20 | call.respond(PostThoughtToken(user.userId, date, code))
21 | }
22 | }
23 | post {
24 | val user = call.sessionOrNull()?.let { dao.user(it.userId) }
25 | if (user == null || !call.verifyCode(it.date, user, it.code, hashFunction)) {
26 | call.respond(HttpStatusCode.Forbidden)
27 | } else {
28 | val id = dao.createThought(user.userId, it.text, it.replyTo)
29 | call.respond(PostThoughtResult(dao.getThought(id)))
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/Delete.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import org.jetbrains.demo.thinkter.dao.*
4 | import org.jetbrains.demo.thinkter.model.*
5 | import org.jetbrains.ktor.application.*
6 | import org.jetbrains.ktor.http.*
7 | import org.jetbrains.ktor.locations.*
8 | import org.jetbrains.ktor.routing.*
9 | import org.jetbrains.ktor.sessions.*
10 |
11 | fun Route.delete(dao: ThinkterStorage, hashFunction: (String) -> String) {
12 | get {
13 | val user = call.sessionOrNull()?.let { dao.user(it.userId) }
14 | val date = System.currentTimeMillis()
15 |
16 | if (user == null) {
17 | call.respond(HttpStatusCode.Forbidden)
18 | } else {
19 | val code = call.securityCode(date, user, hashFunction)
20 | call.respond(PostThoughtToken(user.userId, date, code))
21 | }
22 | }
23 |
24 | post {
25 | val user = call.sessionOrNull()?.let { dao.user(it.userId) }
26 | val thought = dao.getThought(it.id)
27 |
28 | if (user == null || thought.userId != user.userId || !call.verifyCode(it.date, user, it.code, hashFunction)) {
29 | call.respond(HttpStatusCode.Forbidden)
30 | } else {
31 | dao.deleteThought(it.id)
32 | call.respond(object : RpcData {})
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/Login.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import org.jetbrains.demo.thinkter.dao.*
4 | import org.jetbrains.demo.thinkter.model.*
5 | import org.jetbrains.ktor.application.*
6 | import org.jetbrains.ktor.http.*
7 | import org.jetbrains.ktor.locations.*
8 | import org.jetbrains.ktor.routing.*
9 | import org.jetbrains.ktor.sessions.*
10 |
11 | fun Route.login(dao: ThinkterStorage, hash: (String) -> String) {
12 | get {
13 | val user = call.sessionOrNull()?.let { dao.user(it.userId) }
14 | if (user == null) {
15 | call.respond(HttpStatusCode.Forbidden)
16 | } else {
17 | call.respond(LoginResponse(user))
18 | }
19 | }
20 | post {
21 | val login = when {
22 | it.userId.length < 4 -> null
23 | it.password.length < 6 -> null
24 | !userNameValid(it.userId) -> null
25 | else -> dao.user(it.userId, hash(it.password))
26 | }
27 |
28 | if (login == null) {
29 | call.respond(LoginResponse(error = "Invalid username or password"))
30 | } else {
31 | call.session(Session(login.userId))
32 | call.respond(LoginResponse(login))
33 | }
34 | }
35 | post {
36 | call.clearSession()
37 | call.respond(HttpStatusCode.OK)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/backend/build.gradle:
--------------------------------------------------------------------------------
1 | group = 'org.jetbrains.demo.thinkter'
2 | version = '0.0.1-SNAPSHOT'
3 |
4 | apply plugin: 'kotlin'
5 | apply plugin: 'application'
6 |
7 | dependencies {
8 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
9 | compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
10 |
11 | compile "org.jetbrains.ktor:ktor-locations:$ktor_version"
12 | compile "org.jetbrains.ktor:ktor-html-builder:$ktor_version"
13 | compile "org.ehcache:ehcache:3.0.0.m4"
14 |
15 | compile "org.jetbrains.squash:squash-h2:$squash_version"
16 |
17 | testCompile("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
18 | testCompile "org.jetbrains.ktor:ktor-test-host:$ktor_version"
19 | testCompile "org.jsoup:jsoup:1.9.1"
20 |
21 | compile "org.jetbrains.ktor:ktor-jetty:$ktor_version"
22 | compile group: 'com.google.code.gson', name: 'gson', version: '2.8.0'
23 | }
24 |
25 | sourceSets {
26 | main.java.srcDirs += "src"
27 | main.resources.srcDirs += "resources"
28 | main.kotlin.srcDirs += "src"
29 | test.java.srcDirs += "test"
30 | test.kotlin.srcDirs += "test"
31 | test.resources.srcDirs += "testResources"
32 | }
33 |
34 | compileKotlin {
35 | kotlinOptions.jvmTarget = "1.8"
36 | }
37 |
38 | kotlin {
39 | experimental {
40 | coroutines "enable"
41 | }
42 | }
43 |
44 | mainClassName = 'org.jetbrains.ktor.jetty.DevelopmentHost'
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Thinkter: A Kotlin Full-stack Application Example
2 |
3 | Thinkter is an example of a full-stack Web application built with Kotlin. The backend runs on the JVM, and the browser
4 | front-end uses React. The example implements a very simple microblogging application.
5 |
6 | ## Backend
7 |
8 | The backend of Thinkter is built using [Ktor](https://github.com/kotlin/ktor), a Web framework built by the Kotlin team.
9 | For data persistence, it uses [H2](http://www.h2database.com), allowing you to run the example without the need to configure
10 | an external SQL server. The HTTP server implementation is provided by [Jetty](http://www.eclipse.org/jetty/).
11 |
12 | To run the backend, use `./gradlew backend:run`, or open Thinkter as a project in IntelliJ IDEA and execute the shared
13 | run configuration `Backend :: Jetty`. This will start serving the REST API of the backend on port 9090.
14 |
15 | ## Frontend
16 |
17 | The frontend of Thinkter is built using [React](https://facebook.github.io/react/). To adapt the React APIs to Kotlin,
18 | it incorporates a set of [wrappers](https://github.com/orangy/thinkter/tree/master/frontend/src/org/jetbrains/react), which
19 | you can also use in your projects and adapt to your needs.
20 |
21 | The project is built using webpack and the [Kotlin frontend plugin](https://github.com/kotlin/kotlin-frontend-plugin).
22 |
23 | To run the frontend, use `./gradlew frontend:run`. This will start a webpack server on port 8080. Navigate to http://localhost:8080
24 | to start using the application.
25 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/demo/thinkter/Polling.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import kotlin.browser.*
4 | import kotlin.js.*
5 | import kotlinx.coroutines.experimental.launch
6 |
7 | class Polling(val period: Int = 20000) {
8 | private var timerId = 0
9 | var lastTime: Long = Date().getTime().toLong()
10 | var listeners: MutableList<(NewMessages) -> Unit> = ArrayList()
11 |
12 | fun start() {
13 | lastTime = Date().getTime().toLong()
14 | listeners.forEach { it(NewMessages.None) }
15 |
16 | if (timerId == 0) {
17 | stop()
18 | timerId = window.setInterval({ tick() }, period)
19 | }
20 | }
21 |
22 | fun stop() {
23 | if (timerId > 0) {
24 | window.clearInterval(timerId)
25 | timerId = 0
26 | }
27 | }
28 |
29 | fun tick() {
30 | launch {
31 | val newMessagesText = pollFromLastTime(lastTime.toString())
32 | val newMessages = when {
33 | newMessagesText == "0" || newMessagesText.isBlank() -> NewMessages.None
34 | newMessagesText.endsWith("+") -> NewMessages.MoreThan(newMessagesText.removeSuffix("+").toInt())
35 | else -> NewMessages.Few(newMessagesText.toInt())
36 | }
37 | listeners.forEach { it(newMessages) }
38 | }
39 | }
40 |
41 | sealed class NewMessages {
42 | object None : NewMessages()
43 | data class Few(val n: Int) : NewMessages()
44 | data class MoreThan(val n: Int) : NewMessages()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/Application.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import com.google.gson.*
4 | import org.jetbrains.demo.thinkter.dao.*
5 | import org.jetbrains.demo.thinkter.model.*
6 | import org.jetbrains.ktor.application.*
7 | import org.jetbrains.ktor.content.*
8 | import org.jetbrains.ktor.features.*
9 | import org.jetbrains.ktor.http.*
10 | import org.jetbrains.ktor.locations.*
11 | import org.jetbrains.ktor.logging.*
12 | import org.jetbrains.ktor.routing.*
13 | import org.jetbrains.ktor.sessions.*
14 | import org.jetbrains.ktor.transform.*
15 |
16 | data class Session(val userId: String)
17 |
18 | fun Application.main() {
19 | val storage = ThinkterDatabase(/*JDBCConnection.Companion.create(H2Dialect, pool)*/)
20 |
21 | install(DefaultHeaders)
22 | install(CallLogging)
23 | install(ConditionalHeaders)
24 | install(PartialContentSupport)
25 | install(Compression)
26 | install(Locations)
27 | install(StatusPages) {
28 | exception { call.respond(HttpStatusCode.NotImplemented) }
29 | }
30 |
31 | withSessions {
32 | withCookieByValue {
33 | settings = SessionCookiesSettings(transformers = listOf(SessionCookieTransformerMessageAuthentication(hashKey)))
34 | }
35 | }
36 |
37 | transform.register {
38 | TextContent(Gson().toJson(it), ContentType.Application.Json)
39 | }
40 |
41 | routing {
42 | index(storage)
43 | postThought(storage, ::hash)
44 | delete(storage, ::hash)
45 | userPage(storage)
46 | viewThought(storage, ::hash)
47 |
48 | login(storage, ::hash)
49 | register(storage, ::hash)
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/react/ReactExtensions.kt:
--------------------------------------------------------------------------------
1 | package react
2 |
3 | import kotlin.properties.*
4 | import kotlin.reflect.*
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 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/ApplicationPage.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import kotlinx.html.*
4 | import org.jetbrains.ktor.html.*
5 |
6 | class ApplicationPage : Template {
7 | val caption = Placeholder()
8 | val head = Placeholder()
9 |
10 | override fun HTML.apply() {
11 | classes += "mdc-typography"
12 | head {
13 | meta { charset = "utf-8" }
14 | meta {
15 | name = "viewport"
16 | content = "width=device-width, initial-scale=1.0"
17 | }
18 | title {
19 | insert(caption)
20 | }
21 | insert(head)
22 | link("https://fonts.googleapis.com/icon?family=Material+Icons", rel = "stylesheet")
23 |
24 | link(rel = LinkRel.stylesheet, type=LinkType.textCss, href = "http://yui.yahooapis.com/pure/0.6.0/pure-min.css")
25 | link(rel = LinkRel.stylesheet, type=LinkType.textCss, href = "http://yui.yahooapis.com/pure/0.6.0/grids-responsive-min.css")
26 |
27 | }
28 | body {
29 | script {
30 | unsafe {
31 | +"""
32 | var WebFontConfig = {
33 | google: { families: [ 'Roboto:400,300,500:latin' ] }
34 | };
35 | (function() {
36 | var wf = document.createElement('script');
37 | wf.src = ('https:' == document.location.protocol ? 'https' : 'http') +
38 | '://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js';
39 | wf.type = 'text/javascript';
40 | wf.async = 'true';
41 | var s = document.getElementsByTagName('script')[0];
42 | s.parentNode.insertBefore(wf, s);
43 | })();
44 | """
45 | }
46 | }
47 | div { id = "content" }
48 | script(src = "frontend/frontend.bundle.js")
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/Utilities.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import org.jetbrains.demo.thinkter.dao.*
4 | import org.jetbrains.demo.thinkter.model.*
5 | import org.jetbrains.ktor.application.*
6 | import org.jetbrains.ktor.http.*
7 | import org.jetbrains.ktor.locations.*
8 | import org.jetbrains.ktor.request.*
9 | import org.jetbrains.ktor.response.*
10 | import org.jetbrains.ktor.util.*
11 | import java.net.*
12 | import java.util.concurrent.*
13 | import javax.crypto.*
14 | import javax.crypto.spec.*
15 |
16 | val hashKey = hex("6819b57a326945c1968f45236589")
17 | val hmacKey = SecretKeySpec(hashKey, "HmacSHA1")
18 | fun hash(password: String): String {
19 | val hmac = Mac.getInstance("HmacSHA1")
20 | hmac.init(hmacKey)
21 | return hex(hmac.doFinal(password.toByteArray(Charsets.UTF_8)))
22 | }
23 |
24 | fun ApplicationCall.refererHost() = request.header(HttpHeaders.Referrer)?.let { URI.create(it).host }
25 |
26 | suspend fun ApplicationCall.redirect(location: Any) {
27 | val host = request.host() ?: "localhost"
28 | val portSpec = request.port().let { if (it == 80) "" else ":$it" }
29 | val address = host + portSpec
30 |
31 | respondRedirect("http://$address${application.feature(Locations).href(location)}")
32 | }
33 |
34 | fun ApplicationCall.securityCode(date: Long, user: User, hashFunction: (String) -> String) =
35 | hashFunction("$date:${user.userId}:${request.host()}:${refererHost()}")
36 |
37 | fun ApplicationCall.verifyCode(date: Long, user: User, code: String, hashFunction: (String) -> String) =
38 | securityCode(date, user, hashFunction) == code
39 | && (System.currentTimeMillis() - date).let { it > 0 && it < TimeUnit.MILLISECONDS.convert(2, TimeUnit.HOURS) }
40 |
41 |
42 | private val userIdPattern = "[a-zA-Z0-9_.]+".toRegex()
43 | internal fun userNameValid(userId: String) = userId.matches(userIdPattern)
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/demo/thinkter/ThoughtsListComponent.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import kotlinx.html.*
4 | import kotlinx.html.js.*
5 | import org.jetbrains.demo.thinkter.model.*
6 | import react.*
7 | import react.dom.*
8 |
9 | class ThoughtsListComponent : ReactDOMComponent() {
10 | companion object : ReactComponentSpec
11 |
12 | init {
13 | state = ReactComponentNoState()
14 | }
15 |
16 | override fun ReactDOMBuilder.render() {
17 | fun UL.thoughtLi(t: Thought) {
18 | li {
19 | section(classes = "post") {
20 | header(classes = "post-header") {
21 | p(classes = "post-meta") {
22 | a(href = "javascript:void(0)") {
23 | +t.date
24 | +" by "
25 | +t.userId
26 |
27 | onClickFunction = {
28 | props.show(t)
29 | }
30 | }
31 | }
32 | }
33 | div(classes = "post-description") {
34 | ReactMarkdownComponent {
35 | source = t.text
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
42 | div {
43 | ul {
44 | if (props.thoughts.isEmpty()) {
45 | li { +"There are no thoughts yet" }
46 | } else {
47 | for (t in props.thoughts) {
48 | thoughtLi(t)
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
55 | class Props(var thoughts: List = emptyList(), var show: (Thought) -> Unit = {}) : RProps()
56 | }
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/Index.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import org.jetbrains.demo.thinkter.dao.*
4 | import org.jetbrains.demo.thinkter.model.*
5 | import org.jetbrains.ktor.application.*
6 | import org.jetbrains.ktor.html.*
7 | import org.jetbrains.ktor.http.*
8 | import org.jetbrains.ktor.locations.*
9 | import org.jetbrains.ktor.response.*
10 | import org.jetbrains.ktor.routing.*
11 | import org.jetbrains.ktor.sessions.*
12 | import java.time.*
13 |
14 | fun Route.index(storage: ThinkterStorage) {
15 | contentType(ContentType.Text.Html) {
16 | get {
17 | call.respondHtmlTemplate(ApplicationPage()) {
18 | caption { +"Thinkter" }
19 | }
20 | }
21 | }
22 | contentType(ContentType.Application.Json) {
23 | get {
24 | val user = call.sessionOrNull()?.let { storage.user(it.userId) }
25 | val top = storage.top(10).map(storage::getThought)
26 | val latest = storage.latest(10).map(storage::getThought)
27 |
28 | call.response.pipeline.intercept(ApplicationResponsePipeline.After) {
29 | val etagString = user?.userId + "," + top.joinToString { it.id.toString() } + latest.joinToString { it.id.toString() }
30 | call.response.etag(etagString)
31 | }
32 |
33 | call.respond(IndexResponse(top, latest))
34 | }
35 | get { poll ->
36 | if (poll.lastTime.isBlank()) {
37 | call.respond(PollResponse(System.currentTimeMillis(), "0"))
38 | } else {
39 | val time = System.currentTimeMillis()
40 | val lastTime = poll.lastTime.toLong()
41 |
42 | val count = storage.latest(10).reversed().takeWhile { storage.getThought(it).toEpochMilli() > lastTime }.size
43 |
44 | call.respond(PollResponse(time, if (count == 10) "10+" else count.toString()))
45 | }
46 | }
47 | }
48 | }
49 |
50 | private fun Thought.toEpochMilli() = LocalDateTime.parse(date).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
51 |
--------------------------------------------------------------------------------
/frontend/build.gradle:
--------------------------------------------------------------------------------
1 | group = 'org.jetbrains.elemental'
2 | version = '0.0.1-SNAPSHOT'
3 |
4 | apply plugin: 'kotlin2js'
5 | apply plugin: 'org.jetbrains.kotlin.frontend'
6 |
7 | repositories {
8 | jcenter()
9 | }
10 |
11 | dependencies {
12 | compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version"
13 | compile "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version"
14 | compile("org.jetbrains.kotlinx:kotlinx-html-js:$html_version")
15 | }
16 |
17 | kotlinFrontend {
18 | sourceMaps = false
19 |
20 | npm {
21 | replaceVersion("kotlinx-html-js", "1.1.0")
22 | replaceVersion("kotlinx-html-shared", "1.1.0")
23 | replaceVersion("kotlin-js-library", "1.1.0")
24 |
25 | dependency("react")
26 | dependency("react-dom")
27 | dependency("react-router")
28 | dependency("jquery")
29 | dependency("react-markdown")
30 |
31 | devDependency("css-loader")
32 | devDependency("style-loader")
33 | devDependency("babel-loader")
34 | devDependency("babel-core")
35 | devDependency("karma")
36 | }
37 |
38 | webpackBundle {
39 | publicPath = "/frontend/"
40 | port = 8080
41 | proxyUrl = "http://localhost:9090"
42 | }
43 | }
44 |
45 | compileKotlin2Js {
46 | kotlinOptions.metaInfo = true
47 | kotlinOptions.outputFile = "$project.buildDir.path/js/${project.name}.js"
48 | kotlinOptions.sourceMap = true
49 | kotlinOptions.moduleKind = 'commonjs'
50 | kotlinOptions.main = "call"
51 | }
52 |
53 | sourceSets {
54 | main.kotlin.srcDirs += "src"
55 | main.resources.srcDirs += "resources"
56 | main.output.resourcesDir = "build/js/resources"
57 | test.kotlin.srcDirs += "test"
58 | test.resources.srcDirs += "testResources"
59 | }
60 |
61 | task copyResources(type: Copy) {
62 | from sourceSets.main.resources.srcDirs
63 | // into sourceSets.main.output.resourcesDir
64 | into file(buildDir.path + "/js")
65 | }
66 |
67 | afterEvaluate {
68 | tasks.getByName("webpack-bundle") { dependsOn(copyResources) }
69 | tasks.getByName("webpack-run") { dependsOn(copyResources) }
70 | }
71 |
72 | kotlin {
73 | experimental {
74 | coroutines "enable"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/demo/thinkter/HomeView.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import kotlinx.html.*
4 | import org.jetbrains.demo.thinkter.model.*
5 | import react.*
6 | import react.dom.*
7 | import kotlinx.coroutines.experimental.launch
8 |
9 | class HomeView : ReactDOMComponent() {
10 | companion object : ReactComponentSpec
11 |
12 | init {
13 | state = State(emptyList(), emptyList(), true, Polling.NewMessages.None)
14 | }
15 |
16 | override fun componentWillMount() {
17 | super.componentWillMount()
18 |
19 | props.polling.listeners.add(pollerHandler)
20 | loadHome()
21 | }
22 |
23 | override fun componentWillUnmount() {
24 | super.componentWillUnmount()
25 | props.polling.listeners.remove(pollerHandler)
26 | }
27 |
28 | override fun ReactDOMBuilder.render() {
29 | div {
30 | h2 { +"Thoughts" }
31 |
32 | if (state.loading) {
33 | p { +"Loading..." }
34 | } else {
35 | h3 { +"Top" }
36 | ThoughtsListComponent {
37 | thoughts = state.top
38 | show = props.showThought
39 | }
40 |
41 | h3 { +"Recent" }
42 | ThoughtsListComponent {
43 | thoughts = state.latest
44 | show = props.showThought
45 | }
46 | }
47 | }
48 | }
49 |
50 | private fun loadHome() {
51 | launch {
52 | val r = index()
53 | props.polling.start()
54 | setState {
55 | loading = false
56 | top = r.top
57 | latest = r.latest
58 | }
59 | }
60 | }
61 |
62 | private val pollerHandler = { m : Polling.NewMessages ->
63 | val oldMessages = state.newMessages
64 | setState {
65 | newMessages = m
66 | }
67 | if (oldMessages != m && m == Polling.NewMessages.None) {
68 | loadHome()
69 | }
70 | }
71 |
72 | class State(var top: List, var latest: List, var loading: Boolean, var newMessages: Polling.NewMessages) : RState
73 | class Props(var polling: Polling, var showThought: (Thought) -> Unit = {}) : RProps()
74 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/Register.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import org.jetbrains.demo.thinkter.dao.*
4 | import org.jetbrains.demo.thinkter.model.*
5 | import org.jetbrains.ktor.application.*
6 | import org.jetbrains.ktor.http.*
7 | import org.jetbrains.ktor.locations.*
8 | import org.jetbrains.ktor.routing.*
9 | import org.jetbrains.ktor.sessions.*
10 | import org.jetbrains.ktor.util.*
11 |
12 | fun Route.register(dao: ThinkterStorage, hashFunction: (String) -> String) {
13 | post { form ->
14 | val vm = call.request.content.get()
15 |
16 | val user = call.sessionOrNull()?.let { dao.user(it.userId) }
17 | if (user != null) {
18 | call.redirect(LoginResponse(user))
19 | } else {
20 | if (form.password.length < 6) {
21 | call.respond(LoginResponse(error = "Password should be at least 6 characters long"))
22 | } else if (form.userId.length < 4) {
23 | call.respond(LoginResponse(error = "Login should be at least 4 characters long"))
24 | } else if (!userNameValid(form.userId)) {
25 | call.respond(LoginResponse(error = "Login should be consists of digits, letters, dots or underscores"))
26 | } else if (dao.user(form.userId) != null) {
27 | call.respond(LoginResponse(error = "User with the following login is already registered"))
28 | } else {
29 | val hash = hashFunction(form.password)
30 | val newUser = User(form.userId, form.email, form.displayName, hash)
31 |
32 | try {
33 | dao.createUser(newUser)
34 | } catch (e: Throwable) {
35 | if (dao.user(form.userId) != null) {
36 | call.respond(LoginResponse(error = "User with the following login is already registered"))
37 | } else if (dao.userByEmail(form.email) != null) {
38 | call.respond(LoginResponse(error = "User with the following email ${form.email} is already registered"))
39 | } else {
40 | application.environment.log.error("Failed to register user", e)
41 | call.respond(LoginResponse(error = "Failed to register"))
42 | }
43 | }
44 |
45 | call.session(Session(newUser.userId))
46 | call.respond(LoginResponse(newUser))
47 | }
48 | }
49 | }
50 | get {
51 | call.respond(HttpStatusCode.MethodNotAllowed)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/demo/thinkter/NewThoughtComponent.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import kotlinx.html.*
4 | import kotlinx.html.js.*
5 | import org.jetbrains.common.*
6 | import org.jetbrains.demo.thinkter.model.*
7 | import react.*
8 | import react.dom.*
9 | import kotlinx.coroutines.experimental.async
10 |
11 | class NewThoughtComponent : ReactDOMComponent() {
12 | companion object : ReactComponentSpec
13 |
14 | init {
15 | state = State()
16 | }
17 |
18 | override fun ReactDOMBuilder.render() {
19 | form(classes = "pure-form pure-form-stacked") {
20 | legEnd {
21 | if (props.replyTo == null) {
22 | +"New thought"
23 | } else {
24 | +"Reply"
25 | }
26 | }
27 |
28 | props.replyTo?.let { replyTo ->
29 | div {
30 | + "reply to ${replyTo.userId}"
31 | }
32 | }
33 |
34 | textArea(classes = "pure-input-1-2") {
35 | placeholder = "Your thought..."
36 |
37 | onChangeFunction = {
38 | setState {
39 | text = it.inputValue
40 | }
41 | }
42 | }
43 |
44 | state.errorMessage?.let { message ->
45 | p { +message }
46 | }
47 |
48 | button(classes = "pure-button pure-button-primary") {
49 | + "Post"
50 |
51 | onClickFunction = {
52 | it.preventDefault()
53 | doPostThought()
54 | }
55 | }
56 | }
57 | }
58 |
59 | private fun doPostThought() {
60 | async {
61 | val token = postThoughtPrepare()
62 | val thought = postThought(props.replyTo?.id, state.text, token)
63 | onSubmitted(thought)
64 | }.catch { err -> onFailed(err) }
65 | }
66 |
67 | private fun onSubmitted(thought: Thought) {
68 | setState {
69 | errorMessage = null
70 | }
71 |
72 | props.showThought(thought)
73 | }
74 |
75 | private fun onFailed(err: Throwable) {
76 | setState {
77 | errorMessage = err.message
78 | }
79 | }
80 |
81 | class State(var text: String = "", var errorMessage: String? = null) : RState
82 | class Props(var replyTo: Thought? = null, var replyToUser: User? = null, var showThought: (Thought) -> Unit = {}) : RProps()
83 | }
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/demo/thinkter/ViewThoughtComponent.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import kotlinx.html.*
4 | import kotlinx.html.js.*
5 | import org.jetbrains.demo.thinkter.model.*
6 | import react.*
7 | import react.dom.*
8 | import runtime.wrappers.*
9 | import kotlin.browser.*
10 | import kotlinx.coroutines.experimental.launch
11 |
12 | class ViewThoughtComponent : ReactDOMComponent() {
13 |
14 | companion object : ReactComponentSpec
15 |
16 | init {
17 | state = ReactComponentNoState()
18 | }
19 |
20 | override fun ReactDOMBuilder.render() {
21 | val userId = props.thought.userId
22 | val text = props.thought.text
23 | val date = props.thought.date
24 |
25 | div(classes = "pure-g") {
26 | div(classes = "pure-u-1 pure-u-md-1-3") {
27 | +userId
28 | props.thought.replyTo?.let { id ->
29 | +" replies to $id"
30 | }
31 | }
32 | div(classes = "pure-u-1 pure-u-md-2-3") {
33 | +date
34 | }
35 | div(classes = "pure-u-2 pure-u-md-1-1") {
36 | ReactMarkdownComponent {
37 | source = text
38 | }
39 | }
40 |
41 | if (props.currentUser != null) {
42 | div(classes = "pure-u-3 pure-u-md-2-3") {
43 | +""
44 | }
45 | div(classes = "pure-u-3 pure-u-md-1-3") {
46 | a(href = "javascript:void(0)") {
47 | +"Delete"
48 |
49 | onClickFunction = {
50 | delete()
51 | }
52 | }
53 |
54 | span {
55 | style = jsstyle { margin = "0 5px 0 5px" }
56 | +" "
57 | }
58 |
59 | a(href = "javascript:void(0)") {
60 | +"Reply"
61 |
62 | onClickFunction = {
63 | props.reply(props.thought)
64 | }
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
71 | private fun delete() {
72 | if (window.confirm("Do you want to delete the thought?")) {
73 | launch {
74 | val token = postThoughtPrepare()
75 | deleteThought(props.thought.id, token.date, token.code)
76 | props.leave
77 | }
78 | }
79 | }
80 |
81 | class Props(var thought: Thought, var currentUser: User? = null, var reply: (Thought) -> Unit = {}, var leave: () -> Unit = {}) : RProps()
82 | }
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/demo/thinkter/Login.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import kotlinx.html.*
4 | import kotlinx.html.js.*
5 | import org.jetbrains.common.*
6 | import org.jetbrains.demo.thinkter.model.*
7 | import react.*
8 | import react.dom.*
9 | import kotlin.browser.*
10 | import kotlinx.coroutines.experimental.async
11 |
12 | class LoginComponent : ReactDOMComponent() {
13 | companion object : ReactComponentSpec
14 |
15 | init {
16 | state = LoginFormState("", "", false, "")
17 | }
18 |
19 | override fun ReactDOMBuilder.render() {
20 | div {
21 | form(classes = "pure-form pure-form-stacked") {
22 | legEnd { +"Login" }
23 |
24 | fieldSet(classes = "pure-group") {
25 | input(type = InputType.text, name = "login") {
26 | value = state.login
27 | placeholder = "Login"
28 | disabled = state.disabled
29 |
30 | onChangeFunction = {
31 | setState {
32 | login = it.inputValue
33 | }
34 | }
35 | }
36 | input(type = InputType.password, name = "password") {
37 | value = state.password
38 | placeholder = "Password"
39 | disabled = state.disabled
40 |
41 | onChangeFunction = {
42 | setState {
43 | password = it.inputValue
44 | }
45 | }
46 | }
47 | }
48 |
49 | state.errorMessage?.takeIf(String::isNotEmpty)?.let { message ->
50 | label {
51 | +message
52 | }
53 | }
54 |
55 | button(classes = "pure-button pure-button-primary") {
56 | +"Login"
57 | disabled = state.disabled
58 |
59 | onClickFunction = {
60 | it.preventDefault()
61 | doLogin()
62 | }
63 | }
64 | }
65 | }
66 | }
67 |
68 | private fun doLogin() {
69 | setState {
70 | disabled = true
71 | }
72 | async {
73 | val user = login(state.login, state.password)
74 | loggedIn(user)
75 | }.catch { err -> loginFailed(err) }
76 | }
77 |
78 | private fun loggedIn(user: User) {
79 | props.userAssigned(user)
80 | }
81 |
82 | private fun loginFailed(err: Throwable) {
83 | if (err is LoginOrRegisterFailedException) {
84 | setState {
85 | disabled = false
86 | errorMessage = err.message
87 | }
88 | } else {
89 | console.error("Login failed", err)
90 | setState {
91 | disabled = false
92 | errorMessage = "Login failed: please reload page and try again"
93 | }
94 | }
95 | }
96 | }
97 |
98 | class LoginFormState(var login: String, var password: String, var disabled: Boolean, var errorMessage: String?) : RState
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/react/ReactBuilder.kt:
--------------------------------------------------------------------------------
1 | package react
2 |
3 | import runtime.reflect.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/org/jetbrains/react/dom/ReactDOMBuilder.kt:
--------------------------------------------------------------------------------
1 | package react.dom
2 |
3 | import kotlinx.html.*
4 | import org.w3c.dom.events.*
5 | import react.*
6 |
7 | class InnerHTML (
8 | val __html: String
9 | )
10 |
11 | class DOMProps: RProps() {
12 | var dangerouslySetInnerHTML: InnerHTML? = null
13 | }
14 |
15 | class ReactDOMBuilder : ReactBuilder(), TagConsumer {
16 | /*
17 | inline operator fun , reified P : RProps, S: RState> ReactComponentSpec.invoke(
18 | noinline handler: P.() -> Unit = {}
19 | ) : ReactElement {
20 | val props = instantiateProps()
21 | return node(props) { props.handler() }
22 | }
23 |
24 | inline operator fun ReactExternalComponentSpec.invoke(
25 | noinline handler: P.() -> Unit = {}
26 | ) : ReactElement {
27 | val props = instantiateProps
()
28 | return node(props) { props.handler() }
29 | }
30 | */
31 |
32 | override fun createReactNode(type: Any, props: P) = Node(type, props)
33 |
34 | class DOMNode(val tagName: String) : Node(tagName, DOMProps())
35 |
36 | private fun currentDOMNode() = currentNodeOfType()
37 |
38 | var HTMLTag.key: String
39 | get() {
40 | return currentDOMNode().props.key ?: ""
41 | }
42 | set(value) {
43 | currentDOMNode().props.key = value
44 | }
45 |
46 | fun setProp(attribute: String, value: dynamic) {
47 | val node = currentNode()
48 | val key = fixAttributeName(attribute)
49 | if (value == null) {
50 | js("delete node.props[key]")
51 | } else {
52 | node.props.asDynamic()[key] = value
53 | }
54 | }
55 |
56 | override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) {
57 | setProp(attribute, value)
58 | }
59 |
60 | operator fun String.unaryPlus() {
61 | onTagContent(this)
62 | }
63 |
64 | override fun onTagContent(content: CharSequence): Unit {
65 | children.add(content)
66 | }
67 |
68 | override fun onTagContentEntity(entity: Entities): Unit {
69 | children.add(entity.text)
70 | }
71 |
72 | override fun onTagContentUnsafe(block: Unsafe.() -> Unit) {
73 | val sb = StringBuilder()
74 | object : Unsafe {
75 | override fun String.unaryPlus() {
76 | sb.append(this)
77 | }
78 | }.block()
79 | val node = currentDOMNode()
80 | node.props.dangerouslySetInnerHTML = InnerHTML(sb.toString())
81 | }
82 |
83 | override fun onTagStart(tag: Tag) {
84 | enterNode(DOMNode(tag.tagName))
85 | tag.attributesEntries.forEach { setProp(it.key, it.value) }
86 | }
87 |
88 | override fun onTagEnd(tag: Tag) {
89 | if (path.isEmpty() || currentDOMNode().tagName.toLowerCase() != tag.tagName.toLowerCase())
90 | throw IllegalStateException("We haven't entered tag ${tag.tagName} but trying to leave")
91 | exitCurrentNode()
92 | }
93 |
94 | override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) {
95 | setProp(event, value)
96 | }
97 |
98 | override fun finalize(): ReactElement? {
99 | return result()
100 | }
101 | }
102 |
103 | fun buildElement(handler: ReactDOMBuilder.() -> Unit) = with(ReactDOMBuilder()) {
104 | handler()
105 | finalize()
106 | }
107 |
--------------------------------------------------------------------------------
/frontend/resources/pure-blog.css:
--------------------------------------------------------------------------------
1 | * {
2 | -webkit-box-sizing: border-box;
3 | -moz-box-sizing: border-box;
4 | box-sizing: border-box;
5 | }
6 |
7 | a {
8 | text-decoration: none;
9 | color: rgb(61, 146, 201);
10 | }
11 | a:hover,
12 | a:focus {
13 | text-decoration: underline;
14 | }
15 |
16 | h3 {
17 | font-weight: 100;
18 | }
19 |
20 | /* LAYOUT CSS */
21 | .pure-img-responsive {
22 | max-width: 100%;
23 | height: auto;
24 | }
25 |
26 | #layout {
27 | padding: 0;
28 | }
29 |
30 | .header {
31 | text-align: center;
32 | top: auto;
33 | margin: 3em auto;
34 | }
35 |
36 | .sidebar {
37 | background: rgb(61, 79, 93);
38 | color: #fff;
39 | }
40 |
41 | .brand-title,
42 | .brand-tagline {
43 | margin: 0;
44 | }
45 | .brand-title {
46 | text-transform: uppercase;
47 | }
48 | .brand-tagline {
49 | font-weight: 300;
50 | color: rgb(176, 202, 219);
51 | }
52 |
53 | .nav-list {
54 | margin: 0;
55 | padding: 0;
56 | list-style: none;
57 | }
58 | .nav-item {
59 | display: inline-block;
60 | *display: inline;
61 | zoom: 1;
62 | }
63 | .nav-item a {
64 | background: transparent;
65 | border: 2px solid rgb(176, 202, 219);
66 | color: #fff;
67 | margin-top: 1em;
68 | letter-spacing: 0.05em;
69 | text-transform: uppercase;
70 | font-size: 85%;
71 | }
72 | .nav-item a:hover,
73 | .nav-item a:focus {
74 | border: 2px solid rgb(61, 146, 201);
75 | text-decoration: none;
76 | }
77 |
78 | .content-subhead {
79 | text-transform: uppercase;
80 | color: #aaa;
81 | border-bottom: 1px solid #eee;
82 | padding: 0.4em 0;
83 | font-size: 80%;
84 | font-weight: 500;
85 | letter-spacing: 0.1em;
86 | }
87 |
88 | .content {
89 | padding: 2em 1em 0;
90 | }
91 |
92 | .post {
93 | padding-bottom: 2em;
94 | }
95 | .post-title {
96 | font-size: 2em;
97 | color: #222;
98 | margin-bottom: 0.2em;
99 | }
100 | .post-avatar {
101 | border-radius: 50px;
102 | float: right;
103 | margin-left: 1em;
104 | }
105 | .post-description {
106 | font-family: Georgia, "Cambria", serif;
107 | color: #444;
108 | line-height: 1.8em;
109 | }
110 | .post-meta {
111 | color: #999;
112 | font-size: 90%;
113 | margin: 0;
114 | }
115 |
116 | .post-category {
117 | margin: 0 0.1em;
118 | padding: 0.3em 1em;
119 | color: #fff;
120 | background: #999;
121 | font-size: 80%;
122 | }
123 | .post-category-design {
124 | background: #5aba59;
125 | }
126 | .post-category-pure {
127 | background: #4d85d1;
128 | }
129 | .post-category-yui {
130 | background: #8156a7;
131 | }
132 | .post-category-js {
133 | background: #df2d4f;
134 | }
135 |
136 | .post-images {
137 | margin: 1em 0;
138 | }
139 | .post-image-meta {
140 | margin-top: -3.5em;
141 | margin-left: 1em;
142 | color: #fff;
143 | text-shadow: 0 1px 1px #333;
144 | }
145 |
146 | .footer {
147 | text-align: center;
148 | padding: 1em 0;
149 | }
150 | .footer a {
151 | color: #ccc;
152 | font-size: 80%;
153 | }
154 | .footer .pure-menu a:hover,
155 | .footer .pure-menu a:focus {
156 | background: none;
157 | }
158 |
159 | @media (min-width: 48em) {
160 | .content {
161 | padding: 2em 3em 0;
162 | margin-left: 25%;
163 | }
164 |
165 | .header {
166 | margin: 80% 2em 0;
167 | text-align: right;
168 | }
169 |
170 | .sidebar {
171 | position: fixed;
172 | top: 0;
173 | bottom: 0;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/demo/thinkter/NavBarComponent.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import kotlinx.html.*
4 | import kotlinx.html.js.*
5 | import org.jetbrains.demo.thinkter.model.*
6 | import react.*
7 | import react.dom.*
8 | import kotlinx.coroutines.experimental.launch
9 |
10 | class NavBarComponent : ReactDOMComponent() {
11 |
12 | companion object : ReactComponentSpec
13 |
14 | init {
15 | state = NavBarState()
16 | }
17 |
18 | override fun componentDidMount() {
19 | props.poller?.let { p ->
20 | p.listeners.add(pollerHandler)
21 | p.start()
22 | }
23 |
24 | super.componentDidMount()
25 | }
26 |
27 | override fun componentWillUnmount() {
28 | super.componentWillUnmount()
29 | props.poller?.listeners?.remove(pollerHandler)
30 | }
31 |
32 | override fun ReactDOMBuilder.render() {
33 | val user = props.user
34 | val newMessages = state.newMessages
35 |
36 | ul(classes = "nav-list") {
37 | val timelineText = "Timeline" + when (newMessages) {
38 | Polling.NewMessages.None -> ""
39 | is Polling.NewMessages.Few -> "(${newMessages.n})"
40 | is Polling.NewMessages.MoreThan -> "(${newMessages.n}+"
41 | }
42 |
43 | navItem(timelineText) {
44 | props.poller?.start()
45 | timeline()
46 | setState {
47 | this.newMessages = Polling.NewMessages.None
48 | }
49 | }
50 |
51 | if (user != null) {
52 | navItem("New thought") {
53 | postNew()
54 | }
55 | navItem("Sign out, ${user.displayName.takeIf(String::isNotBlank) ?: user.userId}") {
56 | logout()
57 | }
58 | } else {
59 | navItem("Sign up") {
60 | register()
61 | }
62 | navItem("Sign in") {
63 | login()
64 | }
65 | }
66 | }
67 | }
68 |
69 | private val pollerHandler = { count: Polling.NewMessages ->
70 | setState {
71 | newMessages = count
72 | }
73 | }
74 |
75 | private fun timeline() {
76 | props.handler(MainView.Home)
77 | }
78 |
79 | private fun register() {
80 | props.handler(MainView.Register)
81 | }
82 |
83 | private fun login() {
84 | props.handler(MainView.Login)
85 | }
86 |
87 | private fun logout() {
88 | launch {
89 | logoutUser()
90 | props.logoutHandler()
91 | }
92 | }
93 |
94 | private fun postNew() {
95 | props.handler(MainView.PostThought)
96 | }
97 |
98 | private fun UL.navItem(title: String, function: () -> Unit = {}) {
99 | li(classes = "nav-item") {
100 | a(classes = "pure-button", href = "javascript:void(0)") {
101 | +title
102 | onClickFunction = { function() }
103 | }
104 | }
105 | }
106 |
107 | class NavBarState(var newMessages: Polling.NewMessages = Polling.NewMessages.None) : RState
108 |
109 | class NavBarHandlerProps : RProps() {
110 | var user: User? = null
111 | var logoutHandler: () -> Unit = {}
112 | var handler: (MainView) -> Unit = { }
113 | var poller: Polling? = null
114 | }
115 | }
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/demo/thinkter/Rpc.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import kotlinx.coroutines.experimental.await
4 | import org.jetbrains.demo.thinkter.model.*
5 | import org.w3c.dom.url.*
6 | import org.w3c.fetch.*
7 | import kotlin.browser.*
8 | import kotlin.js.*
9 |
10 | suspend fun index(): IndexResponse =
11 | getAndParseResult("/", null, ::parseIndexResponse)
12 |
13 | suspend fun register(userId: String, password: String, displayName: String, email: String): User =
14 | postAndParseResult("/register", URLSearchParams().apply {
15 | append("userId", userId)
16 | append("password", password)
17 | append("displayName", displayName)
18 | append("email", email)
19 | }, ::parseLoginOrRegisterResponse)
20 |
21 | suspend fun pollFromLastTime(lastTime: String = ""): String =
22 | getAndParseResult("/poll?lastTime=$lastTime", null, { json ->
23 | json.count
24 | })
25 |
26 | suspend fun checkSession(): User =
27 | getAndParseResult("/login", null, ::parseLoginOrRegisterResponse)
28 |
29 | suspend fun login(userId: String, password: String): User =
30 | postAndParseResult("/login", URLSearchParams().apply {
31 | append("userId", userId)
32 | append("password", password)
33 | }, ::parseLoginOrRegisterResponse)
34 |
35 | suspend fun postThoughtPrepare(): PostThoughtToken =
36 | getAndParseResult("/post-new", null, ::parseNewPostTokenResponse)
37 |
38 | suspend fun postThought(replyTo: Int?, text: String, token: PostThoughtToken): Thought =
39 | postAndParseResult("/post-new", URLSearchParams().apply {
40 | append("text", text)
41 | append("date", token.date.toString())
42 | append("code", token.code)
43 | if (replyTo != null) {
44 | append("replyTo", replyTo.toString())
45 | }
46 | }, ::parsePostThoughtResponse)
47 |
48 | suspend fun logoutUser() {
49 | window.fetch("/logout", object : RequestInit {
50 | override var method: String? = "POST"
51 | override var credentials: RequestCredentials? = "same-origin".asDynamic()
52 | }).await()
53 | }
54 |
55 | suspend fun deleteThought(id: Int, date: Long, code: String) =
56 | postAndParseResult("/thought/$id/delete", URLSearchParams().apply {
57 | append("date", date.toString())
58 | append("code", code)
59 | }, { Unit })
60 |
61 | private fun parseIndexResponse(json: dynamic): IndexResponse {
62 | val top = json.top as Array
63 | val latest = json.latest as Array
64 |
65 | return IndexResponse(top.map(::parseThought), latest.map(::parseThought))
66 | }
67 |
68 | private fun parsePostThoughtResponse(json: dynamic): Thought {
69 | return parseThought(json.thought)
70 | }
71 |
72 | private fun parseThought(json: dynamic): Thought {
73 | return Thought(json.id, json.userId, json.text, json.date, json.replyTo)
74 | }
75 |
76 | private fun parseNewPostTokenResponse(json: dynamic): PostThoughtToken {
77 | return PostThoughtToken(json.user, json.date, json.code)
78 | }
79 |
80 | private fun parseLoginOrRegisterResponse(json: dynamic): User {
81 | if (json.error != null) {
82 | throw LoginOrRegisterFailedException(json.error.toString())
83 | }
84 |
85 | return User(json.user.userId, json.user.email, json.user.displayName, json.user.passwordHash)
86 | }
87 |
88 | class LoginOrRegisterFailedException(message: String) : Throwable(message)
89 |
90 | suspend fun postAndParseResult(url: String, body: dynamic, parse: (dynamic) -> T): T =
91 | requestAndParseResult("POST", url, body, parse)
92 |
93 | suspend fun getAndParseResult(url: String, body: dynamic, parse: (dynamic) -> T): T =
94 | requestAndParseResult("GET", url, body, parse)
95 |
96 | suspend fun requestAndParseResult(method: String, url: String, body: dynamic, parse: (dynamic) -> T): T {
97 | val response = window.fetch(url, object: RequestInit {
98 | override var method: String? = method
99 | override var body: dynamic = body
100 | override var credentials: RequestCredentials? = "same-origin".asDynamic()
101 | override var headers: dynamic = json("Accept" to "application/json")
102 | }).await()
103 | return parse(response.json().await())
104 | }
105 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/demo/thinkter/Register.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import kotlinx.html.*
4 | import kotlinx.html.js.*
5 | import org.jetbrains.common.*
6 | import org.jetbrains.demo.thinkter.model.*
7 | import react.*
8 | import react.dom.*
9 | import kotlin.browser.*
10 | import kotlinx.coroutines.experimental.async
11 |
12 |
13 | class RegisterComponent : ReactDOMComponent() {
14 | companion object : ReactComponentSpec
15 |
16 | init {
17 | state = RegisterFormState("", "", "", "", null, false)
18 | }
19 |
20 | override fun ReactDOMBuilder.render() {
21 | div {
22 | form(classes = "pure-form pure-form-stacked") {
23 | legEnd { +"Register" }
24 |
25 | fieldSet(classes = "pure-group") {
26 | input(type = InputType.text, name = "login") {
27 | value = state.login
28 | placeholder = "Login"
29 | disabled = state.disabled
30 |
31 | onChangeFunction = { e ->
32 | setState {
33 | login = e.inputValue
34 | }
35 | }
36 | }
37 | input(type = InputType.text, name = "email") {
38 | value = state.email
39 | placeholder = "Email"
40 | disabled = state.disabled
41 |
42 | onChangeFunction = { e ->
43 | setState {
44 | email = e.inputValue
45 | }
46 | }
47 | }
48 | input(type = InputType.password, name = "password") {
49 | value = state.password
50 | placeholder = "Password"
51 | disabled = state.disabled
52 |
53 | onChangeFunction = { e ->
54 | setState {
55 | password = e.inputValue
56 | }
57 | }
58 | }
59 | }
60 | fieldSet(classes = "pure-group") {
61 | input(type = InputType.text, name = "displayName") {
62 | value = state.displayName
63 | placeholder = "Display name"
64 | disabled = state.disabled
65 |
66 | onChangeFunction = { e ->
67 | setState {
68 | displayName = e.inputValue
69 | }
70 | }
71 | }
72 | }
73 |
74 | state.errorMessage?.takeIf(String::isNotEmpty)?.let { message ->
75 | label {
76 | + message
77 | }
78 | }
79 |
80 | button(classes = "pure-button pure-button-primary") {
81 | +"Register"
82 | disabled = state.disabled
83 |
84 | onClickFunction = {
85 | doRegister()
86 | it.preventDefault()
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
93 | private fun doRegister() {
94 | setState {
95 | disabled = true
96 | }
97 | async {
98 | with(state) {
99 | val user = register(login, password, displayName, email)
100 | registered(user)
101 | }
102 | }.catch { err -> registrationFailed(err) }
103 | }
104 |
105 | private fun registered(user: User) {
106 | props.userAssigned(user)
107 | }
108 |
109 | private fun registrationFailed(err: Throwable) {
110 | if (err is LoginOrRegisterFailedException) {
111 | setState {
112 | errorMessage = err.message
113 | disabled = false
114 | }
115 | } else {
116 | console.log("Registration failed", err)
117 | setState {
118 | errorMessage = "Registration failed"
119 | }
120 | }
121 | }
122 | }
123 |
124 | class RegisterFormState(var login: String, var displayName: String, var password: String, var email: String, var errorMessage: String?, var disabled: Boolean) : RState
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/react/dom/ReactDOMAttributes.kt:
--------------------------------------------------------------------------------
1 | package react.dom
2 |
3 | import kotlinx.html.*
4 | import kotlinx.html.attributes.*
5 |
6 | private val events = listOf(
7 | "onCopy",
8 | "onCut",
9 | "onPaste",
10 | "onCompositionEnd",
11 | "onCompositionStart",
12 | "onCompositionUpdate",
13 | "onKeyDown",
14 | "onKeyPress",
15 | "onKeyUp",
16 | "onFocus",
17 | "onBlur",
18 | "onChange",
19 | "onInput",
20 | "onSubmit",
21 | "onClick",
22 | "onContextMenu",
23 | "onDoubleClick",
24 | "onDrag",
25 | "onDragEnd",
26 | "onDragEnter",
27 | "onDragExit",
28 | "onDragLeave",
29 | "onDragOver",
30 | "onDragStart",
31 | "onDrop",
32 | "onMouseDown",
33 | "onMouseEnter",
34 | "onMouseLeave",
35 | "onMouseMove",
36 | "onMouseOut",
37 | "onMouseOver",
38 | "onMouseUp",
39 | "onSelect",
40 | "onTouchCancel",
41 | "onTouchEnd",
42 | "onTouchMove",
43 | "onTouchStart",
44 | "onScroll",
45 | "onWheel",
46 | "onAbort",
47 | "onCanPlay",
48 | "onCanPlayThrough",
49 | "onDurationChange",
50 | "onEmptied",
51 | "onEncrypted",
52 | "onEnded",
53 | "onError",
54 | "onLoadedData",
55 | "onLoadedMetadata",
56 | "onLoadStart",
57 | "onPause",
58 | "onPlay",
59 | "onPlaying",
60 | "onProgress",
61 | "onRateChange",
62 | "onSeeked",
63 | "onSeeking",
64 | "onStalled",
65 | "onSuspend",
66 | "onTimeUpdate",
67 | "onVolumeChange",
68 | "onWaiting",
69 | "onLoad",
70 | "onError",
71 | "onAnimationStart",
72 | "onAnimationEnd",
73 | "onAnimationIteration",
74 | "onTransitionEnd",
75 |
76 |
77 | // HTML attributes
78 | "accept",
79 | "acceptCharset",
80 | "accessKey",
81 | "action",
82 | "allowFullScreen",
83 | "allowTransparency",
84 | "alt",
85 | "async",
86 | "autoComplete",
87 | "autoFocus",
88 | "autoPlay",
89 | "capture",
90 | "cellPadding",
91 | "cellSpacing",
92 | "challenge",
93 | "charSet",
94 | "checked",
95 | "cite",
96 | "classID",
97 | "className",
98 | "colSpan",
99 | "cols",
100 | "content",
101 | "contentEditable",
102 | "contextMenu",
103 | "controls",
104 | "coords",
105 | "crossOrigin",
106 | "data",
107 | "dateTime",
108 | "default",
109 | "defer",
110 | "dir",
111 | "disabled",
112 | "download",
113 | "draggable",
114 | "encType",
115 | "form",
116 | "formAction",
117 | "formEncType",
118 | "formMethod",
119 | "formNoValidate",
120 | "formTarget",
121 | "frameBorder",
122 | "headers",
123 | "height",
124 | "hidden",
125 | "high",
126 | "href",
127 | "hrefLang",
128 | "htmlFor",
129 | "httpEquiv",
130 | "icon",
131 | "id",
132 | "inputMode",
133 | "integrity",
134 | "is",
135 | "keyParams",
136 | "keyType",
137 | "kind",
138 | "label",
139 | "lang",
140 | "list",
141 | "loop",
142 | "low",
143 | "manifest",
144 | "marginHeight",
145 | "marginWidth",
146 | "max",
147 | "maxLength",
148 | "media",
149 | "mediaGroup",
150 | "method",
151 | "min",
152 | "minLength",
153 | "multiple",
154 | "muted",
155 | "name",
156 | "noValidate",
157 | "nonce",
158 | "open",
159 | "optimum",
160 | "pattern",
161 | "placeholder",
162 | "poster",
163 | "preload",
164 | "profile",
165 | "radioGroup",
166 | "readOnly",
167 | "rel",
168 | "required",
169 | "reversed",
170 | "role",
171 | "rowSpan",
172 | "rows",
173 | "sandbox",
174 | "scope",
175 | "scoped",
176 | "scrolling",
177 | "seamless",
178 | "selected",
179 | "shape",
180 | "size",
181 | "sizes",
182 | "span",
183 | "spellCheck",
184 | "src",
185 | "srcDoc",
186 | "srcLang",
187 | "srcSet",
188 | "start",
189 | "step",
190 | "style",
191 | "summary",
192 | "tabIndex",
193 | "target",
194 | "title",
195 | "type",
196 | "useMap",
197 | "value",
198 | "width",
199 | "wmode",
200 | "wrap")
201 |
202 | private val eventMap = events.map {it.toLowerCase() to it}.toMap()
203 |
204 | fun fixAttributeName(event: String): String = eventMap[event] ?: if (event == "class") "className" else event
205 |
206 | private val attributeStringString : Attribute = StringAttribute()
207 |
208 | // See https://facebook.github.io/react/docs/forms.html
209 | var INPUT.defaultValue : String
210 | get() = attributeStringString.get(this, "defaultValue")
211 | set(newValue) {attributeStringString.set(this, "defaultValue", newValue)}
212 |
213 | var TEXTAREA.defaultValue : String
214 | get() = attributeStringString.get(this, "defaultValue")
215 | set(newValue) {attributeStringString.set(this, "defaultValue", newValue)}
216 |
217 | var TEXTAREA.value : String
218 | get() = attributeStringString.get(this, "value")
219 | set(newValue) {attributeStringString.set(this, "value", newValue)}
220 |
221 |
--------------------------------------------------------------------------------
/backend/src/org/jetbrains/demo/thinkter/dao/ThinkterDatabase.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter.dao
2 |
3 | import org.jetbrains.demo.thinkter.model.*
4 | import org.jetbrains.squash.connection.*
5 | import org.jetbrains.squash.dialects.h2.*
6 | import org.jetbrains.squash.expressions.*
7 | import org.jetbrains.squash.query.*
8 | import org.jetbrains.squash.results.*
9 | import org.jetbrains.squash.schema.*
10 | import org.jetbrains.squash.statements.*
11 | import java.io.*
12 | import java.time.*
13 |
14 | class ThinkterDatabase(val db: DatabaseConnection = H2Connection.createMemoryConnection()) : ThinkterStorage {
15 | constructor(dir: File) : this(H2Connection.create("jdbc:h2:file:${dir.canonicalFile.absolutePath}"))
16 |
17 | init {
18 | db.transaction {
19 | databaseSchema().create(Users, Thoughts)
20 | }
21 | }
22 |
23 | override fun countReplies(id: Int): Int = db.transaction {
24 | from(Thoughts).select(Thoughts.id.count()).select {
25 | Thoughts.replyTo eq id
26 | }.execute().single().get(0)
27 | }
28 |
29 | override fun createThought(user: String, text: String, replyTo: Int?, date: LocalDateTime): Int = db.transaction {
30 | insertInto(Thoughts).values {
31 | it[Thoughts.user] = user
32 | it[Thoughts.date] = date
33 | it[Thoughts.replyTo] = replyTo
34 | it[Thoughts.text] = text
35 | }.fetch(Thoughts.id).execute()
36 | }
37 |
38 | override fun deleteThought(id: Int) = db.transaction {
39 | deleteFrom(Thoughts).where { Thoughts.id eq id }.execute()
40 | }
41 |
42 | override fun getThought(id: Int) = db.transaction {
43 | val row = from(Thoughts).where { Thoughts.id eq id }.execute().single()
44 | Thought(id, row[Thoughts.user], row[Thoughts.text], row[Thoughts.date].toString(), row[Thoughts.replyTo])
45 | }
46 |
47 | override fun userThoughts(userId: String) = db.transaction {
48 | from(Thoughts)
49 | .select(Thoughts.id)
50 | .where { Thoughts.user eq userId }
51 | .orderBy(Thoughts.date, false)
52 | .limit(100)
53 | .execute()
54 | .map { it[Thoughts.id] }
55 | .toList()
56 | }
57 |
58 | override fun user(userId: String, hash: String?) = db.transaction {
59 | from(Users).where { Users.id eq userId }.execute()
60 | .mapNotNull {
61 | if (hash == null || it[Users.passwordHash] == hash) {
62 | User(userId, it[Users.email], it[Users.displayName], it[Users.passwordHash])
63 | } else {
64 | null
65 | }
66 | }
67 | .singleOrNull()
68 | }
69 |
70 | override fun userByEmail(email: String) = db.transaction {
71 | from(Users).where { Users.email eq email }.execute()
72 | .map { User(it[Users.id], email, it[Users.displayName], it[Users.passwordHash]) }.singleOrNull()
73 | }
74 |
75 | override fun createUser(user: User) = db.transaction {
76 | insertInto(Users).values {
77 | it[id] = user.userId
78 | it[displayName] = user.displayName
79 | it[email] = user.email
80 | it[passwordHash] = user.passwordHash
81 | }.execute()
82 | }
83 |
84 | override fun top(count: Long): List = db.transaction {
85 | // note: in a real application you shouldn't do it like this
86 | // as it may cause database outages on big data
87 | // so this implementation is just for demo purposes
88 |
89 | val k2 = Thoughts.alias("k2")
90 | from(Thoughts).leftJoin(k2) { Thoughts.id eq Thoughts.replyTo(k2) }
91 | .select(Thoughts.id, Thoughts.id(k2).count())
92 | .groupBy(Thoughts.id)
93 | .orderBy(Thoughts.id(k2).count(), ascending = false)
94 | .limit(count)
95 | .execute()
96 | .map { it[Thoughts.id(k2)] }
97 | .toList()
98 | }
99 |
100 | override fun latest(count: Long): List = db.transaction {
101 | var attempt = 0
102 | var allCount: Int? = null
103 |
104 | for (minutes in generateSequence(2L) { it * it }) {
105 | attempt++
106 |
107 | val dt = LocalDateTime.now().minusMinutes(minutes)
108 |
109 | val all = from(Thoughts)
110 | .select(Thoughts.id)
111 | .where { Thoughts.date gt dt }
112 | .orderBy(Thoughts.date, false)
113 | .limit(count)
114 | .execute()
115 | .map { it[Thoughts.id] }
116 | .toList()
117 |
118 | if (all.size >= count) {
119 | return@transaction all
120 | }
121 | if (attempt > 10 && allCount == null) {
122 | allCount = from(Thoughts).select(Thoughts.id.count()).execute().single().get(0)
123 | if (allCount <= count) {
124 | return@transaction from(Thoughts).select(Thoughts.id).execute().map { it[Thoughts.id] }.toList()
125 | }
126 | }
127 | }
128 |
129 | emptyList()
130 | }
131 |
132 | override fun close() {
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/demo/thinkter/Application.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.demo.thinkter
2 |
3 | import kotlinx.html.*
4 | import kotlinx.html.js.*
5 | import org.jetbrains.demo.thinkter.model.*
6 | import react.*
7 | import react.dom.*
8 | import kotlin.browser.*
9 | import kotlinx.coroutines.experimental.async
10 |
11 | fun main(args: Array) {
12 | runtime.wrappers.require("pure-blog.css")
13 |
14 | ReactDOM.render(document.getElementById("content")) {
15 | div {
16 | Application {}
17 | }
18 | }
19 | }
20 |
21 | class Application : ReactDOMComponent() {
22 | companion object : ReactComponentSpec
23 | val polling = Polling()
24 |
25 | init {
26 | state = ApplicationPageState(MainView.Home)
27 | checkUserSession()
28 | }
29 |
30 | override fun componentWillUnmount() {
31 | polling.stop()
32 | super.componentWillUnmount()
33 | }
34 |
35 | override fun ReactDOMBuilder.render() {
36 | div("pure-g") {
37 | div("sidebar pure-u-1 pure-u-md-1-4") {
38 | div("header") {
39 | div("brand-title") {
40 | +"Thinkter"
41 |
42 | if (state.selected != MainView.Loading) {
43 | onClickFunction = { mainViewSelected() }
44 | }
45 | }
46 | nav("nav") {
47 | if (state.selected != MainView.Loading) {
48 | NavBarComponent {
49 | user = state.currentUser
50 | handler = { navBarSelected(it) }
51 | logoutHandler = { onLoggedOut() }
52 | poller = this@Application.polling
53 | }
54 | }
55 | }
56 | }
57 | }
58 |
59 | div("content pure-u-1 pure-u-md-3-4") {
60 | when (state.selected) {
61 | MainView.Loading -> h1 { +"Loading..." }
62 | MainView.Home -> HomeView {
63 | showThought = { t -> onShowThought(t) }
64 | polling = this@Application.polling
65 | }
66 | MainView.Login -> LoginComponent {
67 | userAssigned = { onUserAssigned(it) }
68 | }
69 | MainView.Register -> RegisterComponent {
70 | userAssigned = { onUserAssigned(it) }
71 | }
72 | MainView.PostThought -> NewThoughtComponent {
73 | showThought = { t -> onShowThought(t) }
74 | replyTo = state.replyTo
75 | }
76 | MainView.User -> {}
77 | MainView.Thought -> ViewThoughtComponent {
78 | thought = state.currentThought ?: Thought(0, "?", "?", "?", null)
79 | currentUser = state.currentUser
80 | reply = { onReplyTo(it) }
81 | leave = { mainViewSelected() }
82 | }
83 | }
84 | }
85 |
86 | div("footer") {
87 | +"Thinkter kotlin frontend + react + ktor example"
88 | }
89 | }
90 | }
91 |
92 | private fun onReplyTo(t: Thought) {
93 | setState {
94 | replyTo = t
95 | selected = MainView.PostThought
96 | }
97 | }
98 |
99 | private fun onLoggedOut() {
100 | val oldSelected = state.selected
101 |
102 | setState {
103 | currentUser = null
104 | selected = when (oldSelected) {
105 | MainView.Home, MainView.Thought, MainView.Login, MainView.Register -> oldSelected
106 | else -> MainView.Home
107 | }
108 | }
109 | }
110 |
111 | private fun onShowThought(t: Thought) {
112 | setState {
113 | currentThought = t
114 | selected = MainView.Thought
115 | }
116 | }
117 |
118 | private fun navBarSelected(newSelected: MainView) {
119 | setState {
120 | selected = newSelected
121 | }
122 | }
123 |
124 | private fun onUserAssigned(user: User) {
125 | setState {
126 | currentUser = user
127 | selected = MainView.Home
128 | }
129 | }
130 |
131 | private fun mainViewSelected() {
132 | setState {
133 | selected = MainView.Home
134 | }
135 | }
136 |
137 | private fun checkUserSession() {
138 | async {
139 | val user = checkSession()
140 | onUserAssigned(user)
141 | }.catch {
142 | setState {
143 | selected = MainView.Home
144 | }
145 | }
146 | }
147 | }
148 |
149 | enum class MainView {
150 | Loading,
151 | Register,
152 | Login,
153 | User,
154 | PostThought,
155 | Thought,
156 | Home
157 | }
158 |
159 | class ApplicationPageState(var selected: MainView, var currentUser: User? = null, var currentThought: Thought? = null, var replyTo: Thought? = null) : RState
160 | class UserProps : RProps() {
161 | var userAssigned: (User) -> Unit = {}
162 | }
163 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
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 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
165 | if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then
166 | cd "$(dirname "$0")"
167 | fi
168 |
169 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
170 |
--------------------------------------------------------------------------------
/frontend/src/org/jetbrains/react/ReactComponent.kt:
--------------------------------------------------------------------------------
1 | package react
2 |
3 | import runtime.reflect.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 |
17 |
18 | private var initWrapper: ReactComponentWrapper<*, *, *>? = null
19 |
20 | abstract class ReactComponent : ReactExtensionProvider {
21 |
22 | internal val wrapper = initWrapper as ReactComponentWrapper<*, *, S>
23 | internal lateinit var stateField: S
24 | internal var isSealed = false
25 | internal var hasState = false
26 | val props: P
27 | get() = wrapper.props.asDynamic()
28 |
29 | var state: S
30 | get() = stateField
31 | set(value) {
32 | if (!isSealed) {
33 | stateField = value
34 | hasState = true
35 | } else {
36 | throw RuntimeException("You can't set initial state not in constructor")
37 | }
38 | }
39 |
40 | fun setState(builder: S.() -> Unit) {
41 | if (!isSealed) {
42 | state.builder()
43 | } else {
44 | wrapper.setState(builder)
45 | }
46 | }
47 |
48 | fun replaceState(state: S) {
49 | if (!isSealed) {
50 | this.state = state
51 | } else {
52 | wrapper.replaceState(state)
53 | }
54 | }
55 |
56 | internal fun seal() {
57 | isSealed = true
58 | }
59 |
60 | internal fun setStateFromWrapper(state: S) {
61 | stateField = state
62 | hasState = true
63 | }
64 |
65 | companion object {
66 |
67 | private val wrappers = HashMap()
68 |
69 | inline fun wrap(): (P, Any, ReactUpdater) -> ReactComponentWrapper where K : ReactComponent {
70 | return wrap(K::class)
71 | }
72 |
73 | fun wrap(clazz: KClass): (P, Any, ReactUpdater) -> ReactComponentWrapper where K : ReactComponent {
74 | if (wrappers[clazz] == null) {
75 | wrappers[clazz] = { p: P, context: Any, updater: ReactUpdater -> ReactComponentWrapper(p, updater, clazz) }
76 | wrappers[clazz].asDynamic().displayName = clazz.js.name
77 | }
78 | return wrappers[clazz] as (P, Any, ReactUpdater) -> ReactComponentWrapper
79 | }
80 | }
81 |
82 | abstract fun render(): ReactElement?
83 |
84 | open fun componentWillMount() {
85 |
86 | }
87 |
88 | open fun componentDidMount() {
89 |
90 | }
91 |
92 | open fun componentWillUnmount() {
93 |
94 | }
95 |
96 | open fun componentDidUpdate(prevProps: P, prevState: S) {
97 |
98 | }
99 |
100 | open fun shouldComponentUpdate(nextProps: P, nextState: S): Boolean {
101 | return true
102 | }
103 |
104 | open fun componentWillUpdate() {
105 |
106 | }
107 |
108 | open fun componentWillReceiveProps(nextProps: P) {
109 |
110 | }
111 |
112 | override fun subscribe(listener: ReactComponentLifecycleListener) {
113 | wrapper.subscribers.add(listener)
114 | }
115 |
116 | override fun unsubscribe(listener: ReactComponentLifecycleListener) {
117 | wrapper.subscribers.remove(listener)
118 | }
119 | }
120 |
121 | //
122 | // Wrapper Class
123 | // Passed directly to React and proxifies all method calls to a real one
124 | // Created for not mixing react and kotlin (overridable) functions and for having ability
125 | // to alter our component's behaviour with powerful kotlin black magic
126 | //
127 |
128 | class ReactComponentWrapper(var props: P, val updater: ReactUpdater, val klazz: KClass) where K : ReactComponent {
129 |
130 | private val delegate: K
131 | private var stateField: S
132 | var state: S
133 | get() = stateField
134 | set(value) {
135 | stateField = value
136 | delegate.setStateFromWrapper(value)
137 | }
138 | var subscribers = ArrayList()
139 |
140 | init {
141 | val oldGlobal = initWrapper
142 | initWrapper = this
143 | delegate = klazz.createInstance()
144 | delegate.seal()
145 | initWrapper = oldGlobal
146 |
147 | if (!delegate.hasState) {
148 | throw RuntimeException("You haven't set initial state in your constructor of ${klazz.simpleName}!")
149 | }
150 | this.stateField = delegate.state
151 | }
152 |
153 | fun setState(stateBuilder: S.() -> Unit) {
154 | val partialState: S = js("({})")
155 | partialState.stateBuilder()
156 |
157 | updater.enqueueSetState(this, partialState)
158 | }
159 |
160 | fun replaceState(state: S) {
161 | updater.enqueueReplaceState(this, state)
162 | }
163 |
164 | @JsName("render")
165 | fun render(): ReactElement? {
166 | return delegate.render()
167 | }
168 |
169 | @JsName("shouldComponentUpdate")
170 | fun shouldComponentUpdate(nextProps: P, nextState: S): Boolean {
171 | return delegate.shouldComponentUpdate(nextProps, nextState)
172 | }
173 |
174 | @JsName("componentWillReceiveProps")
175 | fun componentWillReceiveProps(nextProps: P) {
176 | delegate.componentWillReceiveProps(nextProps)
177 | }
178 |
179 | @JsName("componentWillUpdate")
180 | fun componentWillUpdate() {
181 | subscribers.forEach {
182 | it.reactComponentWillUpdate()
183 | }
184 | delegate.componentWillUpdate()
185 | }
186 |
187 | @JsName("componentDidUpdate")
188 | fun componentDidUpdate(prevProps: P, prevState: S) {
189 | delegate.componentDidUpdate(prevProps, prevState)
190 | }
191 |
192 | @JsName("componentWillUnmount")
193 | fun componentWillUnmount() {
194 | subscribers.forEach {
195 | it.reactComponentWillUnmount()
196 | }
197 | delegate.componentWillUnmount()
198 | }
199 |
200 | @JsName("componentWillMount")
201 | fun componentWillMount() {
202 | subscribers.forEach {
203 | it.reactComponentWillMount()
204 | }
205 | delegate.componentWillMount()
206 | }
207 |
208 | @JsName("componentDidMount")
209 | fun componentDidMount() {
210 | subscribers.forEach {
211 | it.reactComponentDidMount()
212 | }
213 | delegate.componentDidMount()
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------