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
14 changes: 12 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack

val kotlinVersion = "1.7.20-Beta"
val serializationVersion = "1.3.3"
val ktorVersion = "2.0.3"
val ktorVersion = "2.2.1"
val kotlin_css_version = "1.0.0-pre.473"
val logbackVersion = "1.2.11"
val kotlinWrappersVersion = "1.0.0-pre.354"
val kmongoVersion = "4.5.0"
Expand Down Expand Up @@ -54,7 +55,15 @@ kotlin {
implementation("io.ktor:ktor-server-core-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-netty:$ktorVersion")
implementation("ch.qos.logback:logback-classic:$logbackVersion")
implementation("org.litote.kmongo:kmongo-coroutine-serialization:$kmongoVersion")
// implementation("org.litote.kmongo:kmongo-coroutine-serialization:$kmongoVersion")

implementation("org.jsoup:jsoup:1.15.3")
// implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.8.0")
// implementation("io.ktor:ktor-server-html-builder-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-html-builder:$ktorVersion")
implementation("org.jetbrains.kotlin-wrappers:kotlin-css:$kotlin_css_version")


}
}

Expand All @@ -66,6 +75,7 @@ kotlin {
implementation(project.dependencies.enforcedPlatform("org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom:$kotlinWrappersVersion"))
implementation("org.jetbrains.kotlin-wrappers:kotlin-react")
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom")

}
}
}
Expand Down
28 changes: 28 additions & 0 deletions src/commonMain/kotlin/News.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import kotlinx.serialization.Serializable

@Serializable
data class News(
val title: String = "",
val url: String = "",
val provider: String = "",
val overline: String = "",
val teaser: String = "",
val text: String = "",
val breadcrumbs: List<String> = emptyList(),
val author: String = "",
val date: String = ""
) {
fun contains(searchText: String): Boolean {
return this.toString().contains(searchText, true)
}

override fun toString(): String {
return "$title$url$provider$overline$teaser$text$breadcrumbs$author$date$id"
}

val id: Int = hashCode()

companion object {
const val path = "/news"
}
}
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"
}
}
1 change: 1 addition & 0 deletions src/commonMain/resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<title>Full Stack Shopping List</title>
<link rel="stylesheet" href="/styles.css" type="text/css"/>
</head>
<body>
<div id="root"></div>
Expand Down
35 changes: 35 additions & 0 deletions src/jsMain/kotlin/Api.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*

val jsonClient = HttpClient {
install(ContentNegotiation) {
json()
}
}

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

suspend fun addShoppingListItem(shoppingListItem: ShoppingListItem) {
jsonClient.post(ShoppingListItem.path) {
contentType(ContentType.Application.Json)
setBody(shoppingListItem)
}
}

suspend fun deleteShoppingListItem(shoppingListItem: ShoppingListItem) {
jsonClient.delete(ShoppingListItem.path + "/${shoppingListItem.id}")
}

suspend fun getNews(): List<News> {
return jsonClient.get(News.path).body()
}

suspend fun filterResults(filterText: String): List<News> {
return jsonClient.get(News.path + "/${filterText}").body()
}
71 changes: 71 additions & 0 deletions src/jsMain/kotlin/App.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import csstype.ClassName
import kotlinx.coroutines.*
import react.*
import react.dom.html.AnchorTarget
import react.dom.html.ReactHTML.a
import react.dom.html.ReactHTML.br
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.em
import react.dom.html.ReactHTML.h1
import react.dom.html.ReactHTML.li
import react.dom.html.ReactHTML.ol
import react.dom.html.ReactHTML.strong

private val scope = MainScope()

val App = FC<Props> { props ->
var news by useState(emptyList<News>())

useEffectOnce {
scope.launch {
news = getNews()
}
}
div {
className = ClassName("page_title")
h1 {
+"Sören's fast Scraper"
}
InputComponent {
onSubmit = { input ->
scope.launch {
news = if (input.isEmpty()) getNews() else filterResults(input)
}
}
}
}
div {
id = "headLineListContainer"
ol {
id = "headLineList"
news.forEach { item ->
li {
a {
href = item.url
target = AnchorTarget._blank
div {
+item.date
}
div {
+item.provider
}
div {
strong { +item.overline }
}
div {
em {
+item.title
}
}
div {
+item.teaser
}
div {
+item.text
}
}
}
}
}
}
}
34 changes: 34 additions & 0 deletions src/jsMain/kotlin/InputComponent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import org.w3c.dom.HTMLFormElement
import react.*
import org.w3c.dom.HTMLInputElement
import react.dom.events.ChangeEventHandler
import react.dom.events.FormEventHandler
import react.dom.html.InputType
import react.dom.html.ReactHTML.form
import react.dom.html.ReactHTML.input

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

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

val submitHandler: FormEventHandler<HTMLFormElement> = {
it.preventDefault()
props.onSubmit(text)
}

val changeHandler: ChangeEventHandler<HTMLInputElement> = {
setText(it.target.value)
}

form {
onSubmit = submitHandler
input {
type = InputType.text
onChange = changeHandler
value = text
}
}
}
7 changes: 5 additions & 2 deletions src/jsMain/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import kotlinx.browser.document
import react.create
import react.dom.client.createRoot

fun main() {
document.getElementById("root")?.innerHTML = "Hello, Kotlin/JS!"
}
val container = document.getElementById("root") ?: error("Couldn't find container!")
createRoot(container).render(App.create())
}
106 changes: 105 additions & 1 deletion src/jvmMain/kotlin/Server.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,107 @@
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.http.content.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.css.*
import kotlinx.css.Color.Companion.transparent
import kotlinx.css.properties.TextDecoration
import scraper.Scraper

val collection = mutableListOf(
ShoppingListItem("Cucumbers 🥒", 1),
ShoppingListItem("Tomatoes 🍅", 2),
ShoppingListItem("Orange Juice 🍊", 3)
)

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

routing {
get("/") {
call.respondText(
this::class.java.classLoader.getResource("index.html")!!.readText(),
ContentType.Text.Html
)
}
static("/") {
resources("")
}
get("/styles.css") {
call.respondCss {
body {
backgroundColor = Color.lightGray
margin(10.px)
}
rule(".page_title") {
color = Color.gray
margin(all = LinearDimension.auto)
width = LinearDimension("50%")
}
rule("a") {
color = Color.black
backgroundColor = transparent
textDecoration = TextDecoration.none
}
rule("li") {
padding(5.px)
}
}
}
route(ShoppingListItem.path) {
get {
call.respond(collection.toList())
}
post {
collection.add(call.receive<ShoppingListItem>())
call.respond(HttpStatusCode.OK)
}
delete("/{id}") {
val id = call.parameters["id"]?.toInt() ?: error("Invalid delete request")
collection.removeIf { it.id == id }
call.respond(HttpStatusCode.OK)
}
}
route(News.path) {
get { call.respond(scraper.newsList) }
post {
val filterString = call.receive<String>()
scraper.newsList.add(News(filterString, filterString, filterString))
call.respond(HttpStatusCode.OK)
}
get("/{filterstring}") {
call.respond(scraper.filterBy(call.parameters["filterstring"]))
}
route("/{filterstring}") {

}
}
}
}.start(wait = true)
}

suspend inline fun ApplicationCall.respondCss(builder: CssBuilder.() -> Unit) {
this.respondText(CssBuilder().apply(builder).toString(), ContentType.Text.CSS)
}
Loading