Skip to content

Commit

Permalink
add expiration date
Browse files Browse the repository at this point in the history
  • Loading branch information
avan1235 committed Mar 12, 2024
1 parent c91d252 commit 2a8fd5d
Show file tree
Hide file tree
Showing 14 changed files with 241 additions and 57 deletions.
1 change: 1 addition & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ kotlin {
implementation(libs.ktor.client.resources)

implementation(libs.procyk.compose.qrcode)
implementation(libs.procyk.compose.calendar)

implementation(libs.decompose)
implementation(libs.decompose.extensionsComposeJetbrains)
Expand Down
9 changes: 6 additions & 3 deletions composeApp/src/commonMain/kotlin/in/procyk/shin/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,17 @@ fun ShinApp(component: ShinComponent) {
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.size(32.dp))
Spacer(Modifier.size(16.dp))

val shortenedUrl by component.shortenedUrl.subscribeAsState()
ShortenResponse(shortenedUrl.toNullable())

Spacer(Modifier.size(16.dp))
ShortenRequest(
component = component,
maxWidth = maxWidth,
isVertical = isVertical,
)
val shortenedUrl by component.shortenedUrl.subscribeAsState()
ShortenResponse(shortenedUrl.toNullable())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package `in`.procyk.shin.component

import Option
import Option.None
import Option.Some
import Shorten
import ShortenExpiring
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.value.Value
import `in`.procyk.shin.createHttpClient
Expand All @@ -14,19 +16,26 @@ import io.ktor.http.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.Instant
import kotlinx.datetime.*
import kotlinx.datetime.Clock.System.now
import toLocalDate


interface ShinComponent : Component {

val expirationDateTime: Value<Option<Instant>>
val extraElementsVisible: Value<Boolean>

val expirationDate: Value<LocalDate>

val url: Value<String>

val shortenedUrl: Value<Option<String>>

val protocol: Value<ShortenedProtocol>

fun onExpirationDateTimeChange(expirationDateTime: Instant?)
fun onExtraElementsVisibleChange()

fun onExpirationDateTimeChange(expirationDate: LocalDate?): Boolean

fun onUrlChange(url: String)

Expand All @@ -44,21 +53,38 @@ class ShinComponentImpl(

private val httpClient: HttpClient = createHttpClient()

private val _expirationDateTime = MutableStateFlow<Option<Instant>>(Option.None)
override val expirationDateTime: Value<Option<Instant>> = _expirationDateTime.asValue()
private val _extraElementsVisible = MutableStateFlow(false)
override val extraElementsVisible: Value<Boolean> = _extraElementsVisible.asValue()

private val _expirationDate = MutableStateFlow(tomorrow)
override val expirationDate: Value<LocalDate> = _expirationDate.asValue()

private val _url = MutableStateFlow("")
override val url: Value<String> = _url.asValue()

private val _shortenedUrl = MutableStateFlow<Option<String>>(Option.None)
private val _shortenedUrl = MutableStateFlow<Option<String>>(None)
override val shortenedUrl: Value<Option<String>> = _shortenedUrl.asValue()

private val _protocol = MutableStateFlow(ShortenedProtocol.HTTPS)
override val protocol: Value<ShortenedProtocol> = _protocol.asValue()

override fun onExpirationDateTimeChange(expirationDateTime: Instant?) {
val updatedValue = Option.fromNullable(expirationDateTime)
_expirationDateTime.update { updatedValue }
override fun onExtraElementsVisibleChange() {
_extraElementsVisible.update { !it }
}

override fun onExpirationDateTimeChange(expirationDate: LocalDate?): Boolean = when {
expirationDate == null -> {
val updatedDate = tomorrow
_expirationDate.update { updatedDate }
true
}

expirationDate < now().toLocalDate() -> false

else -> {
_expirationDate.update { expirationDate }
true
}
}

override fun onUrlChange(url: String) {
Expand All @@ -72,14 +98,16 @@ class ShinComponentImpl(
}

override fun onShortenedUrlReset() {
_shortenedUrl.update { Option.None }
_shortenedUrl.update { None }
_extraElementsVisible.update { false }
}

override fun onShorten() {
scope.launch {
httpClient.requestShortenedUrl(
url = _url.value,
shortenedProtocol = _protocol.value,
expirationDate = _expirationDate.value.takeIfVisible(),
onResponse = { response ->
val some = Some(response)
_shortenedUrl.update { some }
Expand All @@ -88,20 +116,32 @@ class ShinComponentImpl(
)
}
}

private inline fun <T> T.takeIfVisible(): T? =
takeIf { _extraElementsVisible.value }
}

private suspend fun HttpClient.requestShortenedUrl(
url: String,
shortenedProtocol: ShortenedProtocol,
expirationDate: LocalDate?,
onResponse: (String) -> Unit,
onError: (String) -> Unit,
) {
try {
post<Shorten>(Shorten(shortenedProtocol.buildUrl(url)))
val expirationAt = expirationDate?.plus(1, DateTimeUnit.DAY)?.atStartOfDayIn(TimeZone.currentSystemDefault())
val response = when (expirationAt) {
null -> post<_>(Shorten(shortenedProtocol.buildUrl(url)))
else -> post<_>(ShortenExpiring(shortenedProtocol.buildUrl(url), expirationAt))
}
response
.takeIf { it.status == HttpStatusCode.OK }
?.bodyAsText()
?.let(onResponse)
} catch (_: Exception) {
onError("Cannot connect to Shin. Try again later…")
}
}

private inline val tomorrow: LocalDate
get() = now().toLocalDate().plus(1, DateTimeUnit.DAY)
102 changes: 75 additions & 27 deletions composeApp/src/commonMain/kotlin/in/procyk/shin/ui/ShortenRequest.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package `in`.procyk.shin.ui

import androidx.compose.animation.*
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
Expand All @@ -17,6 +22,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import applyIf
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import `in`.procyk.compose.calendar.SelectableCalendar
import `in`.procyk.compose.calendar.rememberSelectableCalendarState
import `in`.procyk.compose.calendar.year.YearMonth
import `in`.procyk.shin.component.ShinComponent
import `in`.procyk.shin.model.ShortenedProtocol

Expand All @@ -27,38 +35,78 @@ internal fun ShortenRequest(
isVertical: Boolean,
space: Dp = 8.dp,
) {
if (isVertical) {
Column(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(
space = space,
alignment = Alignment.CenterVertically,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
ShortenRequestElements(
component = component,
fillMaxWidth = true,
maxTextFieldWidth = maxWidth / 2
)
Column(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
when {
isVertical -> Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(
space = space,
alignment = Alignment.CenterVertically,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
ShortenRequestElements(
component = component,
fillMaxWidth = true,
maxTextFieldWidth = maxWidth / 2
)
}

else -> Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(
space = space,
alignment = Alignment.CenterHorizontally,
),
verticalAlignment = Alignment.Bottom,
) {
ShortenRequestElements(
component = component,
fillMaxWidth = false,
maxTextFieldWidth = maxWidth / 2
)
}
}
} else {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(
space = space,
alignment = Alignment.CenterHorizontally,
),
verticalAlignment = Alignment.Bottom,
Spacer(Modifier.height(12.dp))
ShortenRequestExtraElements(component)
}
}

@Composable
private fun ShortenRequestExtraElements(
component: ShinComponent,
) {
val extraElementsVisible by component.extraElementsVisible.subscribeAsState()
val expirationDate by component.expirationDate.subscribeAsState()
val rotation by animateFloatAsState(if (extraElementsVisible) 180f else 0f)
OutlinedButton(onClick = component::onExtraElementsVisibleChange) {
Text("Extra Options")
Icon(
imageVector = Icons.Filled.ArrowDropDown,
modifier = Modifier.rotate(rotation),
contentDescription = "Extra Options"
)
}
AnimatedVisibility(
visible = extraElementsVisible,
enter = fadeIn() + expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut()
) {
Box(
modifier = Modifier.sizeIn(maxWidth = 280.dp),
) {
ShortenRequestElements(
component = component,
fillMaxWidth = false,
maxTextFieldWidth = maxWidth / 2
val calendarState = rememberSelectableCalendarState(
initialMonth = YearMonth.now(),
minMonth = YearMonth.now(),
initialSelection = listOf(expirationDate),
confirmSelectionChange = { component.onExpirationDateTimeChange(it.singleOrNull()) },
)
SelectableCalendar(calendarState = calendarState)
}
}

}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -23,6 +24,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import applyIf

@Composable
internal fun ShortenResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import kotlinx.datetime.*

fun Instant.toLocalDate(): LocalDate =
toLocalDateTime(TimeZone.currentSystemDefault()).date
6 changes: 4 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ androidx-activityCompose = "1.8.2"
jetbrains-compose = "1.6.0-rc02"
exposed = "0.47.0"
kotlin = "1.9.22"
ktor-server = "2.3.8"
ktor-server = "2.3.9"
ktor-client = "3.0.0-wasm2"
koin = "3.6.0-wasm-alpha2"
kotlinxDatetime = "0.6.0-RC.2"
kotlinxCoroutines = "1.8.0"
kotlinxSerialization = "1.6.3"
procyk-compose = "1.6.0-rc02.0"
procyk-compose = "1.6.0-rc02.2"
logback = "1.4.14"
postgres = "42.7.1"
dotenv = "6.4.1"
Expand All @@ -28,6 +28,7 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
ktor-shared-resources = { module = "io.ktor:ktor-resources", version.ref = "ktor-client" }
ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor-server" }
Expand All @@ -48,6 +49,7 @@ kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerialization" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
procyk-compose-qrcode = { module = "in.procyk.compose:qr-code", version.ref = "procyk-compose" }
procyk-compose-calendar = { module = "in.procyk.compose:calendar", version.ref = "procyk-compose" }
postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" }
dotenv = { module = "io.github.cdimascio:dotenv-kotlin", version.ref = "dotenv" }
decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" }
Expand Down
2 changes: 2 additions & 0 deletions server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ dependencies {
implementation(libs.exposed.core)
implementation(libs.exposed.dao)
implementation(libs.exposed.jdbc)
implementation(libs.exposed.kotlin.datetime)
implementation(libs.postgres)
implementation(libs.dotenv)
implementation(libs.kotlinx.datetime)

testImplementation(libs.ktor.server.tests)
testImplementation(libs.kotlin.test.junit)
Expand Down
Loading

0 comments on commit 2a8fd5d

Please sign in to comment.