Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AddShoppingListElement.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
POST http://localhost:9090/shoppingList
Content-Type: application/json

{
"desc": "Peppers 🌶",
"priority": 5
}
1 change: 1 addition & 0 deletions DeleteShoppingListElement.http
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE http://localhost:9090/shoppingList/AN_ID_GOES_HERE
10 changes: 10 additions & 0 deletions src/commonMain/kotlin/ShoppingListItem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import kotlinx.serialization.Serializable

@Serializable
data class ShoppingListItem(val desc: String, val priority: Int) {
val id: Int = desc.hashCode()

companion object {
const val path = "/shoppingList"
}
}
28 changes: 28 additions & 0 deletions src/jsMain/kotlin/Api.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import io.ktor.http.*
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer

import kotlinx.browser.window

val endpoint = window.location.origin // only needed until https://github.com/ktorio/ktor/issues/1695 is resolved

val jsonClient = HttpClient {
install(JsonFeature) { serializer = KotlinxSerializer() }
}

suspend fun getShoppingList(): List<ShoppingListItem> {
return jsonClient.get(endpoint + ShoppingListItem.path)
}

suspend fun addShoppingListItem(shoppingListItem: ShoppingListItem) {
jsonClient.post<Unit>(endpoint + ShoppingListItem.path) {
contentType(ContentType.Application.Json)
body = shoppingListItem
}
}

suspend fun deleteShoppingListItem(shoppingListItem: ShoppingListItem) {
jsonClient.delete<Unit>(endpoint + ShoppingListItem.path + "/${shoppingListItem.id}")
}
47 changes: 47 additions & 0 deletions src/jsMain/kotlin/App.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import react.*
import react.dom.*
import kotlinext.js.*
import kotlinx.html.js.*
import kotlinx.coroutines.*

private val scope = MainScope()

val App = functionalComponent<RProps> { _ ->
val (shoppingList, setShoppingList) = useState(emptyList<ShoppingListItem>())

useEffect(arrayOf(emptyList<ShoppingListItem>().asDynamic())) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useEffectOnce fixes this too, a little simpler.

scope.launch {
setShoppingList(getShoppingList())
}
}

h1 {
+"Full-Stack Shopping List"
}
ul {
shoppingList.sortedByDescending(ShoppingListItem::priority).forEach { item ->
li {
key = item.toString()
attrs.onClickFunction = {
scope.launch {
deleteShoppingListItem(item)
setShoppingList(getShoppingList())
}
}
+"[${item.priority}] ${item.desc} "
}
}
}
child(
InputComponent,
props = jsObject {
onSubmit = { input ->
val cartItem = ShoppingListItem(input.replace("!", ""), input.count { it == '!' })
scope.launch {
addShoppingListItem(cartItem)
setShoppingList(getShoppingList())
}
}
}
)
}
33 changes: 33 additions & 0 deletions src/jsMain/kotlin/InputComponent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import react.*
import react.dom.*
import kotlinx.html.js.*
import kotlinx.html.InputType
import org.w3c.dom.events.Event
import org.w3c.dom.HTMLInputElement

external interface InputProps : RProps {
var onSubmit: (String) -> Unit
}

val InputComponent = functionalComponent<InputProps> { props ->
val (text, setText) = useState("")

val submitHandler: (Event) -> Unit = {
it.preventDefault()
setText("")
props.onSubmit(text)
}

val changeHandler: (Event) -> Unit = {
val value = (it.target as HTMLInputElement).value
setText(value)
}

form {
attrs.onSubmitFunction = submitHandler
input(InputType.text) {
attrs.onChangeFunction = changeHandler
attrs.value = text
}
}
}
8 changes: 6 additions & 2 deletions src/jsMain/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import react.child
import react.dom.render
import kotlinx.browser.document

fun main() {
document.getElementById("root")?.innerHTML = "Hello, Kotlin/JS!"
}
render(document.getElementById("root")) {
child(App)
}
}
65 changes: 64 additions & 1 deletion src/jvmMain/kotlin/Server.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,66 @@
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.serialization.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import org.litote.kmongo.*
import org.litote.kmongo.coroutine.*
import com.mongodb.ConnectionString
import org.litote.kmongo.reactivestreams.KMongo

val connectionString: ConnectionString? = System.getenv("MONGODB_URI")?.let {
ConnectionString("$it?retryWrites=false")
}

val client = if (connectionString != null) KMongo.createClient(connectionString).coroutine else KMongo.createClient().coroutine
val database = client.getDatabase(connectionString?.database ?: "test")
val collection = database.getCollection<ShoppingListItem>()

fun main() {
println("Hello, JVM!")
val port = System.getenv("PORT")?.toInt() ?: 9090
embeddedServer(Netty, port) {
install(ContentNegotiation) {
json()
}
install(CORS) {
method(HttpMethod.Get)
method(HttpMethod.Post)
method(HttpMethod.Delete)
anyHost()
}
install(Compression) {
gzip()
}

routing {
get("/") {
call.respondText(
this::class.java.classLoader.getResource("index.html")!!.readText(),
ContentType.Text.Html
)
}
static("/") {
resources("")
}
route(ShoppingListItem.path) {
get {
call.respond(collection.find().toList())
}
post {
collection.insertOne(call.receive<ShoppingListItem>())
call.respond(HttpStatusCode.OK)
}
delete("/{id}") {
val id = call.parameters["id"]?.toInt() ?: error("Invalid delete request")
collection.deleteOne(ShoppingListItem::id eq id)
call.respond(HttpStatusCode.OK)
}
}
}
}.start(wait = true)
}