├── .gitignore
├── Dockerfile
├── README.md
├── androidapp
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── stevdza
│ │ └── san
│ │ └── androidapp
│ │ ├── MainActivity.kt
│ │ ├── components
│ │ ├── EmptyUI.kt
│ │ ├── NavigationDrawer.kt
│ │ └── PostCard.kt
│ │ ├── data
│ │ ├── MongoSync.kt
│ │ └── MongoSyncRepository.kt
│ │ ├── models
│ │ └── Post.kt
│ │ ├── navigation
│ │ ├── NavGraph.kt
│ │ ├── Screen.kt
│ │ └── destinations
│ │ │ ├── Category.kt
│ │ │ ├── Details.kt
│ │ │ └── Home.kt
│ │ ├── screens
│ │ ├── category
│ │ │ ├── CategoryScreen.kt
│ │ │ └── CategoryViewModel.kt
│ │ ├── details
│ │ │ └── DetailsScreen.kt
│ │ └── home
│ │ │ ├── HomeScreen.kt
│ │ │ └── HomeViewModel.kt
│ │ ├── ui
│ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ └── util
│ │ ├── Constants.kt
│ │ ├── Functions.kt
│ │ └── RequestState.kt
│ └── res
│ ├── drawable
│ ├── ic_launcher_background.xml
│ ├── ic_launcher_foreground.xml
│ └── logo.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
├── shared
├── .gitignore
├── build.gradle.kts
└── src
│ ├── androidMain
│ └── kotlin
│ │ └── com
│ │ └── example
│ │ └── shared
│ │ └── Category.kt
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── example
│ │ └── shared
│ │ ├── Category.kt
│ │ └── Constants.kt
│ ├── jsMain
│ └── kotlin
│ │ └── com
│ │ └── example
│ │ └── shared
│ │ └── Category.kt
│ └── jvmMain
│ └── kotlin
│ └── com
│ └── example
│ └── shared
│ └── Category.kt
└── site
├── .gitignore
├── .kobweb
└── conf.yaml
├── build.gradle.kts
└── src
├── commonMain
└── kotlin
│ └── com
│ └── example
│ └── blogmultiplatform
│ └── models
│ ├── ApiListResponse.kt
│ ├── Constants.kt
│ ├── Newsletter.kt
│ ├── Post.kt
│ └── User.kt
├── jsMain
├── kotlin
│ └── com
│ │ └── example
│ │ └── blogmultiplatform
│ │ ├── MyApp.kt
│ │ ├── components
│ │ ├── AdminPageLayout.kt
│ │ ├── CategoryChip.kt
│ │ ├── CategoryNavigationItems.kt
│ │ ├── ErrorView.kt
│ │ ├── LoadingIndicator.kt
│ │ ├── Popup.kt
│ │ ├── PostPreview.kt
│ │ ├── PostsView.kt
│ │ ├── SearchBar.kt
│ │ └── SidePanel.kt
│ │ ├── models
│ │ ├── ControlStyle.kt
│ │ ├── EditorControl.kt
│ │ ├── RandomJoke.kt
│ │ └── User.kt
│ │ ├── navigation
│ │ └── Screen.kt
│ │ ├── pages
│ │ ├── Index.kt
│ │ ├── admin
│ │ │ ├── Create.kt
│ │ │ ├── Index.kt
│ │ │ ├── Login.kt
│ │ │ ├── MyPosts.kt
│ │ │ └── Success.kt
│ │ ├── posts
│ │ │ └── Index.kt
│ │ └── search
│ │ │ └── Index.kt
│ │ ├── sections
│ │ ├── FooterSection.kt
│ │ ├── HeaderSection.kt
│ │ ├── MainSection.kt
│ │ ├── NewsletterSection.kt
│ │ ├── PostsSection.kt
│ │ └── SponsoredPostsSection.kt
│ │ ├── styles
│ │ ├── CreateStyle.kt
│ │ ├── HeaderStyle.kt
│ │ ├── LoginStyle.kt
│ │ ├── NewsletterStyle.kt
│ │ ├── PostPreviewStyle.kt
│ │ └── SidePanelStyle.kt
│ │ └── util
│ │ ├── ApiFunctions.kt
│ │ ├── Constants.kt
│ │ └── Functions.kt
└── resources
│ └── public
│ ├── bold.svg
│ ├── checkmark.svg
│ ├── code.svg
│ ├── favicon.ico
│ ├── github-dark.css
│ ├── highlight.min.js
│ ├── image.svg
│ ├── italic.svg
│ ├── laugh.png
│ ├── link.svg
│ ├── logo.svg
│ ├── quote.svg
│ ├── subtitle.svg
│ └── title.svg
└── jvmMain
└── kotlin
└── com
└── example
└── blogmultiplatform
├── api
├── Newsletter.kt
├── Posts.kt
└── UserCheck.kt
├── data
├── MongoDB.kt
└── MongoRepository.kt
├── models
└── User.kt
└── util
└── Constants.kt
/.gitignore:
--------------------------------------------------------------------------------
1 | # General ignores
2 | .DS_Store
3 | build
4 | out
5 | kotlin-js-store
6 |
7 | # IntelliJ ignores
8 | *.iml
9 | /*.ipr
10 |
11 | /.idea/caches
12 | /.idea/libraries
13 | /.idea/modules.xml
14 | /.idea/workspace.xml
15 | /.idea/gradle.xml
16 | /.idea/navEditor.xml
17 | /.idea/assetWizardSettings.xml
18 | /.idea/artifacts
19 | /.idea/compiler.xml
20 | /.idea/jarRepositories.xml
21 | /.idea/*.iml
22 | /.idea/modules
23 | /.idea/libraries-with-intellij-classes.xml
24 |
25 | # Gradle ignores
26 | .gradle
27 |
28 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | #-----------------------------------------------------------------------------
2 | # Variables shared across multiple stages (they need to be explicitly opted
3 | # into each stage by being declaring there too, but their values need only be
4 | # specified once).
5 | ARG KOBWEB_APP_ROOT="site"
6 |
7 | FROM eclipse-temurin:17 as java
8 |
9 | FROM java as export
10 |
11 | #-----------------------------------------------------------------------------
12 | # Create an intermediate stage which builds and exports our site. In the
13 | # final stage, we'll only extract what we need from this stage, saving a lot
14 | # of space.
15 |
16 | ENV KOBWEB_CLI_VERSION=0.9.13
17 | ARG KOBWEB_APP_ROOT
18 |
19 | ENV NODE_MAJOR=20
20 |
21 | # Copy the project code to an arbitrary subdir so we can install stuff in the
22 | # Docker container root without worrying about clobbering project files.
23 | COPY . /project
24 |
25 | # Update and install required OS packages to continue
26 | # Note: Node install instructions from: https://github.com/nodesource/distributions#installation-instructions
27 | # Note: Playwright is a system for running browsers, and here we use it to
28 | # install Chromium.
29 | RUN apt-get update \
30 | && apt-get install -y ca-certificates curl gnupg unzip wget \
31 | && mkdir -p /etc/apt/keyrings \
32 | && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
33 | && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
34 | && apt-get update \
35 | && apt-get install -y nodejs \
36 | && npm init -y \
37 | && npx playwright install --with-deps chromium
38 |
39 | # Fetch the latest version of the Kobweb CLI
40 | RUN wget https://github.com/varabyte/kobweb-cli/releases/download/v${KOBWEB_CLI_VERSION}/kobweb-${KOBWEB_CLI_VERSION}.zip \
41 | && unzip kobweb-${KOBWEB_CLI_VERSION}.zip \
42 | && rm kobweb-${KOBWEB_CLI_VERSION}.zip
43 |
44 | ENV PATH="/kobweb-${KOBWEB_CLI_VERSION}/bin:${PATH}"
45 |
46 | WORKDIR /project/${KOBWEB_APP_ROOT}
47 |
48 | # Decrease Gradle memory usage to avoid OOM situations in tight environments
49 | # (many free Cloud tiers only give you 512M of RAM). The following amount
50 | # should be more than enough to build and export our site.
51 | RUN mkdir ~/.gradle && \
52 | echo "org.gradle.jvmargs=-Xmx256m" >> ~/.gradle/gradle.properties
53 |
54 | RUN kobweb export --notty
55 |
56 | #-----------------------------------------------------------------------------
57 | # Create the final stage, which contains just enough bits to run the Kobweb
58 | # server.
59 | FROM java as run
60 |
61 | ARG KOBWEB_APP_ROOT
62 |
63 | COPY --from=export /project/${KOBWEB_APP_ROOT}/.kobweb .kobweb
64 |
65 | ENTRYPOINT .kobweb/server/start.sh
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Kotlin Multiplatform KMP Development | Web Mobile
2 |
3 | Online Course
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/androidapp/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/androidapp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
2 | plugins {
3 | alias(libs.plugins.com.android.application)
4 | alias(libs.plugins.org.jetbrains.kotlin.android)
5 | alias(libs.plugins.mongodb.realm)
6 | alias(libs.plugins.serialization.plugin)
7 | }
8 |
9 | android {
10 | namespace = "com.stevdza.san.androidapp"
11 | compileSdk = 34
12 |
13 | defaultConfig {
14 | applicationId = "com.stevdza.san.androidapp"
15 | minSdk = 24
16 | targetSdk = 34
17 | versionCode = 1
18 | versionName = "1.0"
19 |
20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
21 | vectorDrawables {
22 | useSupportLibrary = true
23 | }
24 | }
25 |
26 | buildTypes {
27 | release {
28 | isMinifyEnabled = false
29 | proguardFiles(
30 | getDefaultProguardFile("proguard-android-optimize.txt"),
31 | "proguard-rules.pro"
32 | )
33 | }
34 | }
35 | compileOptions {
36 | sourceCompatibility = JavaVersion.VERSION_17
37 | targetCompatibility = JavaVersion.VERSION_17
38 | }
39 | tasks.withType().configureEach {
40 | kotlinOptions {
41 | jvmTarget = "17"
42 | }
43 | }
44 | kotlinOptions {
45 | jvmTarget = "17"
46 | }
47 | buildFeatures {
48 | compose = true
49 | }
50 | composeOptions {
51 | kotlinCompilerExtensionVersion = "1.5.7"
52 | }
53 | packaging {
54 | resources {
55 | excludes += "/META-INF/**"
56 | }
57 | }
58 | }
59 |
60 | dependencies {
61 | implementation(libs.core.ktx)
62 | implementation(libs.lifecycle.runtime.ktx)
63 | implementation(libs.activity.compose)
64 | implementation(platform(libs.compose.bom))
65 | implementation(libs.ui)
66 | implementation(libs.ui.graphics)
67 | implementation(libs.ui.tooling.preview)
68 | implementation(libs.material3)
69 |
70 | implementation(libs.navigation.compose)
71 | implementation(libs.kotlinx.coroutines)
72 | implementation(libs.mongodb.sync)
73 | implementation(libs.coil.compose)
74 | implementation(libs.kotlinx.serialization)
75 | implementation(project(":shared"))
76 | }
--------------------------------------------------------------------------------
/androidapp/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/androidapp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.navigation.compose.rememberNavController
7 | import com.stevdza.san.androidapp.navigation.SetupNavGraph
8 | import com.stevdza.san.androidapp.ui.theme.BlogMultiplatformTheme
9 |
10 | class MainActivity : ComponentActivity() {
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | setContent {
14 | BlogMultiplatformTheme {
15 | val navController = rememberNavController()
16 | SetupNavGraph(navController = navController)
17 | }
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/components/EmptyUI.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.material3.CircularProgressIndicator
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 |
11 | @Composable
12 | fun EmptyUI(
13 | loading: Boolean = false,
14 | hideMessage: Boolean = false,
15 | message: String = "No posts to show."
16 | ) {
17 | Box(
18 | modifier = Modifier.fillMaxSize(),
19 | contentAlignment = Alignment.Center
20 | ) {
21 | if (loading) {
22 | CircularProgressIndicator()
23 | } else {
24 | if (!hideMessage) {
25 | Text(text = message)
26 | }
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/components/NavigationDrawer.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.components
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.DrawerState
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.ModalDrawerSheet
10 | import androidx.compose.material3.ModalNavigationDrawer
11 | import androidx.compose.material3.NavigationDrawerItem
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.alpha
17 | import com.stevdza.san.androidapp.R
18 | import androidx.compose.ui.res.painterResource
19 | import androidx.compose.ui.unit.dp
20 | import com.example.shared.Category
21 |
22 | @Composable
23 | fun NavigationDrawer(
24 | drawerState: DrawerState,
25 | onCategorySelect: (Category) -> Unit,
26 | content: @Composable () -> Unit
27 | ) {
28 | ModalNavigationDrawer(
29 | drawerState = drawerState,
30 | drawerContent = {
31 | ModalDrawerSheet(
32 | content = {
33 | Box(
34 | modifier = Modifier
35 | .fillMaxWidth()
36 | .padding(vertical = 100.dp),
37 | contentAlignment = Alignment.Center
38 | ) {
39 | Image(
40 | painter = painterResource(id = R.drawable.logo),
41 | contentDescription = "Logo Image"
42 | )
43 | }
44 | Text(
45 | modifier = Modifier
46 | .fillMaxWidth()
47 | .alpha(0.5f)
48 | .padding(start = 20.dp, bottom = 12.dp),
49 | text = "Categories"
50 | )
51 | Category.entries.forEach { category ->
52 | NavigationDrawerItem(
53 | label = {
54 | Text(
55 | modifier = Modifier.padding(horizontal = 12.dp),
56 | text = category.name,
57 | color = MaterialTheme.colorScheme.onSurface
58 | )
59 | },
60 | selected = false,
61 | onClick = { onCategorySelect(category) }
62 | )
63 | }
64 | }
65 | )
66 | },
67 | content = { content() }
68 | )
69 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/components/PostCard.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.lazy.LazyColumn
11 | import androidx.compose.foundation.lazy.items
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.SuggestionChip
15 | import androidx.compose.material3.Surface
16 | import androidx.compose.material3.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.draw.alpha
20 | import androidx.compose.ui.draw.clip
21 | import androidx.compose.ui.layout.ContentScale
22 | import androidx.compose.ui.platform.LocalContext
23 | import androidx.compose.ui.text.font.FontWeight
24 | import androidx.compose.ui.text.style.TextOverflow
25 | import androidx.compose.ui.unit.Dp
26 | import androidx.compose.ui.unit.dp
27 | import coil.compose.AsyncImage
28 | import coil.request.ImageRequest
29 | import com.example.shared.Category
30 | import com.stevdza.san.androidapp.models.Post
31 | import com.stevdza.san.androidapp.util.RequestState
32 | import com.stevdza.san.androidapp.util.convertLongToDate
33 | import com.stevdza.san.androidapp.util.decodeThumbnailImage
34 |
35 | @Composable
36 | fun PostCard(
37 | post: Post,
38 | onPostClick: (String) -> Unit
39 | ) {
40 | val context = LocalContext.current
41 | Surface(
42 | modifier = Modifier
43 | .fillMaxWidth()
44 | .clip(RoundedCornerShape(size = 12.dp))
45 | .clickable { onPostClick(post._id) },
46 | tonalElevation = 1.dp
47 | ) {
48 | Column(modifier = Modifier.fillMaxWidth()) {
49 | AsyncImage(
50 | modifier = Modifier.height(260.dp),
51 | model = ImageRequest
52 | .Builder(context)
53 | .data(
54 | if (post.thumbnail.contains("http")) post.thumbnail
55 | else post.thumbnail.decodeThumbnailImage()
56 | )
57 | .build(),
58 | contentDescription = "Post Thumbnail",
59 | contentScale = ContentScale.Crop
60 | )
61 | Column(
62 | modifier = Modifier
63 | .fillMaxWidth()
64 | .padding(all = 16.dp)
65 | ) {
66 | Text(
67 | modifier = Modifier
68 | .padding(bottom = 6.dp)
69 | .alpha(0.5f),
70 | text = post.date.convertLongToDate(),
71 | fontSize = MaterialTheme.typography.bodySmall.fontSize,
72 | fontWeight = FontWeight.Normal,
73 | overflow = TextOverflow.Ellipsis,
74 | maxLines = 1
75 | )
76 | Text(
77 | modifier = Modifier.padding(bottom = 6.dp),
78 | text = post.title,
79 | fontSize = MaterialTheme.typography.titleLarge.fontSize,
80 | fontWeight = FontWeight.Bold,
81 | overflow = TextOverflow.Ellipsis,
82 | maxLines = 2
83 | )
84 | Text(
85 | modifier = Modifier.padding(bottom = 6.dp),
86 | text = post.subtitle,
87 | fontSize = MaterialTheme.typography.bodyMedium.fontSize,
88 | fontWeight = FontWeight.Normal,
89 | overflow = TextOverflow.Ellipsis,
90 | maxLines = 3
91 | )
92 | SuggestionChip(
93 | onClick = { },
94 | label = { Text(text = Category.valueOf(post.category).name) }
95 | )
96 | }
97 | }
98 | }
99 | }
100 |
101 | @Composable
102 | fun PostCardsView(
103 | posts: RequestState>,
104 | topMargin: Dp,
105 | hideMessage: Boolean = false,
106 | onPostClick: (String) -> Unit
107 | ) {
108 | when (posts) {
109 | is RequestState.Success -> {
110 | if(posts.data.isNotEmpty()) {
111 | LazyColumn(
112 | modifier = Modifier
113 | .fillMaxSize()
114 | .padding(top = topMargin)
115 | .padding(horizontal = 24.dp),
116 | verticalArrangement = Arrangement.spacedBy(12.dp)
117 | ) {
118 | items(
119 | items = posts.data,
120 | key = { post -> post._id }
121 | ) { post ->
122 | PostCard(post = post, onPostClick = onPostClick)
123 | }
124 | }
125 | } else {
126 | EmptyUI()
127 | }
128 | }
129 |
130 | is RequestState.Error -> {
131 | EmptyUI(message = posts.error.message.toString())
132 | }
133 |
134 | is RequestState.Idle -> {
135 | EmptyUI(hideMessage = hideMessage)
136 | }
137 |
138 | is RequestState.Loading -> {
139 | EmptyUI(loading = true)
140 | }
141 | }
142 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/data/MongoSync.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.data
2 |
3 | import com.example.shared.Category
4 | import com.stevdza.san.androidapp.models.Post
5 | import com.stevdza.san.androidapp.util.Constants.APP_ID
6 | import com.stevdza.san.androidapp.util.RequestState
7 | import io.realm.kotlin.Realm
8 | import io.realm.kotlin.ext.query
9 | import io.realm.kotlin.log.LogLevel
10 | import io.realm.kotlin.mongodb.App
11 | import io.realm.kotlin.mongodb.sync.SyncConfiguration
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.flow
14 | import kotlinx.coroutines.flow.map
15 |
16 | object MongoSync : MongoSyncRepository {
17 | private val app = App.create(APP_ID)
18 | private val user = app.currentUser
19 | private lateinit var realm: Realm
20 |
21 | init {
22 | configureTheRealm()
23 | }
24 |
25 | override fun configureTheRealm() {
26 | if (user != null) {
27 | val config = SyncConfiguration.Builder(user, setOf(Post::class))
28 | .initialSubscriptions {
29 | add(
30 | query = it.query(Post::class),
31 | name = "Blog Posts"
32 | )
33 | }
34 | .log(LogLevel.ALL)
35 | .build()
36 | realm = Realm.open(config)
37 | }
38 | }
39 |
40 | override fun readAllPosts(): Flow>> {
41 | return if (user != null) {
42 | try {
43 | realm.query(Post::class)
44 | .asFlow()
45 | .map { result ->
46 | RequestState.Success(data = result.list)
47 | }
48 | } catch (e: Exception) {
49 | flow { emit(RequestState.Error(Exception(e.message))) }
50 | }
51 | } else {
52 | flow { emit(RequestState.Error(Exception("User not authenticated."))) }
53 | }
54 | }
55 |
56 | override fun searchPostsByTitle(query: String): Flow>> {
57 | return if (user != null) {
58 | try {
59 | realm.query(query = "title CONTAINS[c] $0", query)
60 | .asFlow()
61 | .map { result ->
62 | RequestState.Success(data = result.list)
63 | }
64 | } catch (e: Exception) {
65 | flow { emit(RequestState.Error(Exception(e.message))) }
66 | }
67 | } else {
68 | flow { emit(RequestState.Error(Exception("User not authenticated."))) }
69 | }
70 | }
71 |
72 | override fun searchPostsByCategory(category: Category): Flow>> {
73 | return if (user != null) {
74 | try {
75 | realm.query(query = "category == $0", category.name)
76 | .asFlow()
77 | .map { result ->
78 | RequestState.Success(data = result.list)
79 | }
80 | } catch (e: Exception) {
81 | flow { emit(RequestState.Error(Exception(e.message))) }
82 | }
83 | } else {
84 | flow { emit(RequestState.Error(Exception("User not authenticated."))) }
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/data/MongoSyncRepository.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.data
2 |
3 | import com.example.shared.Category
4 | import com.stevdza.san.androidapp.models.Post
5 | import com.stevdza.san.androidapp.util.RequestState
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | interface MongoSyncRepository {
9 | fun configureTheRealm()
10 | fun readAllPosts(): Flow>>
11 | fun searchPostsByTitle(query: String): Flow>>
12 | fun searchPostsByCategory(category: Category): Flow>>
13 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/models/Post.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.models
2 |
3 | import com.example.shared.Category
4 | import io.realm.kotlin.types.RealmObject
5 | import io.realm.kotlin.types.annotations.PrimaryKey
6 |
7 | open class Post: RealmObject {
8 | @PrimaryKey
9 | var _id: String = ""
10 | var author: String = ""
11 | var date: Long = 0L
12 | var title: String = ""
13 | var subtitle: String = ""
14 | var thumbnail: String = ""
15 | var category: String = Category.Programming.name
16 | }
17 |
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/navigation/NavGraph.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.navigation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.navigation.NavHostController
5 | import androidx.navigation.compose.NavHost
6 | import com.stevdza.san.androidapp.navigation.destinations.categoryRoute
7 | import com.stevdza.san.androidapp.navigation.destinations.detailsRoute
8 | import com.stevdza.san.androidapp.navigation.destinations.homeRoute
9 |
10 | @Composable
11 | fun SetupNavGraph(navController: NavHostController) {
12 | NavHost(
13 | navController = navController,
14 | startDestination = Screen.Home.route
15 | ) {
16 | homeRoute(
17 | onCategorySelect = { category ->
18 | navController.navigate(Screen.Category.passCategory(category))
19 | },
20 | onPostClick = { postId ->
21 | navController.navigate(Screen.Details.passPostId(postId))
22 | }
23 | )
24 | categoryRoute(
25 | onBackPress = { navController.popBackStack() },
26 | onPostClick = { postId ->
27 | navController.navigate(Screen.Details.passPostId(postId))
28 | }
29 | )
30 | detailsRoute(onBackPress = { navController.popBackStack() })
31 | }
32 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/navigation/Screen.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.navigation
2 |
3 | import com.stevdza.san.androidapp.util.Constants.CATEGORY_ARGUMENT
4 | import com.stevdza.san.androidapp.util.Constants.POST_ID_ARGUMENT
5 | import com.example.shared.Category as PostCategory
6 |
7 | sealed class Screen(val route: String) {
8 | data object Home : Screen(route = "home_screen")
9 | data object Category : Screen(route = "category_screen/{${CATEGORY_ARGUMENT}}") {
10 | fun passCategory(category: PostCategory) = "category_screen/${category.name}"
11 | }
12 |
13 | data object Details : Screen(route = "details_screen/{${POST_ID_ARGUMENT}}") {
14 | fun passPostId(id: String) = "details_screen/${id}"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/navigation/destinations/Category.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.navigation.destinations
2 |
3 | import androidx.lifecycle.viewmodel.compose.viewModel
4 | import androidx.navigation.NavGraphBuilder
5 | import androidx.navigation.NavType
6 | import androidx.navigation.compose.composable
7 | import androidx.navigation.navArgument
8 | import com.example.shared.Category
9 | import com.stevdza.san.androidapp.navigation.Screen
10 | import com.stevdza.san.androidapp.screens.category.CategoryScreen
11 | import com.stevdza.san.androidapp.screens.category.CategoryViewModel
12 | import com.stevdza.san.androidapp.util.Constants.CATEGORY_ARGUMENT
13 |
14 | fun NavGraphBuilder.categoryRoute(
15 | onBackPress: () -> Unit,
16 | onPostClick: (String) -> Unit
17 | ) {
18 | composable(
19 | route = Screen.Category.route,
20 | arguments = listOf(navArgument(name = CATEGORY_ARGUMENT) {
21 | type = NavType.StringType
22 | })
23 | ) {
24 | val viewModel: CategoryViewModel = viewModel()
25 | val selectedCategory = it.arguments?.getString(CATEGORY_ARGUMENT) ?: Category.Programming.name
26 | CategoryScreen(
27 | posts = viewModel.categoryPosts.value,
28 | category = Category.valueOf(selectedCategory),
29 | onBackPress = onBackPress,
30 | onPostClick = onPostClick
31 | )
32 | }
33 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/navigation/destinations/Details.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.navigation.destinations
2 |
3 | import androidx.navigation.NavGraphBuilder
4 | import androidx.navigation.NavType
5 | import androidx.navigation.compose.composable
6 | import androidx.navigation.navArgument
7 | import com.example.shared.Constants
8 | import com.stevdza.san.androidapp.navigation.Screen
9 | import com.stevdza.san.androidapp.screens.details.DetailsScreen
10 | import com.stevdza.san.androidapp.util.Constants.POST_ID_ARGUMENT
11 |
12 | fun NavGraphBuilder.detailsRoute(
13 | onBackPress: () -> Unit
14 | ) {
15 | composable(
16 | route = Screen.Details.route,
17 | arguments = listOf(navArgument(name = POST_ID_ARGUMENT) {
18 | type = NavType.StringType
19 | })
20 | ) {
21 | val postId = it.arguments?.getString(POST_ID_ARGUMENT)
22 | DetailsScreen(
23 | url = "http://10.0.2.2:8080/posts/post?${POST_ID_ARGUMENT}=$postId&${Constants.SHOW_SECTIONS_PARAM}=false",
24 | onBackPress = onBackPress
25 | )
26 | }
27 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/navigation/destinations/Home.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.navigation.destinations
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.viewmodel.compose.viewModel
8 | import androidx.navigation.NavGraphBuilder
9 | import androidx.navigation.compose.composable
10 | import com.example.shared.Category
11 | import com.stevdza.san.androidapp.navigation.Screen
12 | import com.stevdza.san.androidapp.screens.home.HomeScreen
13 | import com.stevdza.san.androidapp.screens.home.HomeViewModel
14 |
15 | fun NavGraphBuilder.homeRoute(
16 | onCategorySelect: (Category) -> Unit,
17 | onPostClick: (String) -> Unit
18 | ) {
19 | composable(route = Screen.Home.route) {
20 | val viewModel: HomeViewModel = viewModel()
21 | var query by remember { mutableStateOf("") }
22 | var searchBarOpened by remember { mutableStateOf(false) }
23 | var active by remember { mutableStateOf(false) }
24 |
25 | HomeScreen(
26 | posts = viewModel.allPosts.value,
27 | searchedPosts = viewModel.searchedPosts.value,
28 | query = query,
29 | searchBarOpened = searchBarOpened,
30 | active = active,
31 | onActiveChange = { active = it },
32 | onQueryChange = { query = it },
33 | onCategorySelect = onCategorySelect,
34 | onSearchBarChange = { opened ->
35 | searchBarOpened = opened
36 | if (!opened) {
37 | query = ""
38 | active = false
39 | viewModel.resetSearchedPosts()
40 | }
41 | },
42 | onSearch = viewModel::searchPostsByTitle,
43 | onPostClick = onPostClick
44 | )
45 | }
46 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/screens/category/CategoryScreen.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.screens.category
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.ArrowBack
6 | import androidx.compose.material3.ExperimentalMaterial3Api
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.IconButton
9 | import androidx.compose.material3.Scaffold
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.TopAppBar
12 | import androidx.compose.runtime.Composable
13 | import com.stevdza.san.androidapp.components.PostCardsView
14 | import com.example.shared.Category
15 | import com.stevdza.san.androidapp.models.Post
16 | import com.stevdza.san.androidapp.util.RequestState
17 |
18 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
19 | @OptIn(ExperimentalMaterial3Api::class)
20 | @Composable
21 | fun CategoryScreen(
22 | posts: RequestState>,
23 | category: Category,
24 | onBackPress: () -> Unit,
25 | onPostClick: (String) -> Unit
26 | ) {
27 | Scaffold(
28 | topBar = {
29 | TopAppBar(
30 | title = { Text(text = category.name) },
31 | navigationIcon = {
32 | IconButton(onClick = { onBackPress() }) {
33 | Icon(
34 | imageVector = Icons.Default.ArrowBack,
35 | contentDescription = "Back Arrow Icon"
36 | )
37 | }
38 | }
39 | )
40 | }
41 | ) {
42 | PostCardsView(
43 | posts = posts,
44 | topMargin = it.calculateTopPadding(),
45 | onPostClick = onPostClick
46 | )
47 | }
48 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/screens/category/CategoryViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.screens.category
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.State
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.lifecycle.SavedStateHandle
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import com.stevdza.san.androidapp.data.MongoSync
10 | import com.example.shared.Category
11 | import com.stevdza.san.androidapp.models.Post
12 | import com.stevdza.san.androidapp.util.RequestState
13 | import kotlinx.coroutines.flow.collectLatest
14 | import kotlinx.coroutines.launch
15 |
16 | class CategoryViewModel(
17 | savedStateHandle: SavedStateHandle
18 | ) : ViewModel() {
19 | private val _categoryPosts: MutableState>> =
20 | mutableStateOf(RequestState.Idle)
21 | val categoryPosts: State>> = _categoryPosts
22 |
23 | init {
24 | _categoryPosts.value = RequestState.Loading
25 | val selectedCategory = savedStateHandle.get("category")
26 | if (selectedCategory != null) {
27 | viewModelScope.launch {
28 | MongoSync.searchPostsByCategory(
29 | category = Category.valueOf(selectedCategory)
30 | ).collectLatest { _categoryPosts.value = it }
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/screens/details/DetailsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.screens.details
2 |
3 | import android.annotation.SuppressLint
4 | import android.view.ViewGroup
5 | import android.webkit.WebView
6 | import android.webkit.WebViewClient
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.IconButton
13 | import androidx.compose.material3.Scaffold
14 | import androidx.compose.material3.Text
15 | import androidx.compose.material3.TopAppBar
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.viewinterop.AndroidView
19 |
20 | @SuppressLint("SetJavaScriptEnabled")
21 | @OptIn(ExperimentalMaterial3Api::class)
22 | @Composable
23 | fun DetailsScreen(
24 | url: String,
25 | onBackPress: () -> Unit
26 | ) {
27 | Scaffold(
28 | topBar = {
29 | TopAppBar(
30 | title = { Text(text = "Details") },
31 | navigationIcon = {
32 | IconButton(onClick = { onBackPress() }) {
33 | Icon(
34 | imageVector = Icons.AutoMirrored.Default.ArrowBack,
35 | contentDescription = "Back Arrow Icon"
36 | )
37 | }
38 | }
39 | )
40 | }
41 | ) {
42 | AndroidView(
43 | modifier = Modifier.padding(top = it.calculateTopPadding()),
44 | factory = { context ->
45 | WebView(context).apply {
46 | layoutParams = ViewGroup.LayoutParams(
47 | ViewGroup.LayoutParams.MATCH_PARENT,
48 | ViewGroup.LayoutParams.MATCH_PARENT
49 | )
50 | settings.javaScriptEnabled = true
51 | webViewClient = WebViewClient()
52 | loadUrl(url)
53 | }
54 | }
55 | )
56 | }
57 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/screens/home/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.screens.home
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.ArrowBack
6 | import androidx.compose.material.icons.filled.Close
7 | import androidx.compose.material.icons.filled.Menu
8 | import androidx.compose.material.icons.filled.Search
9 | import androidx.compose.material3.CenterAlignedTopAppBar
10 | import androidx.compose.material3.DrawerValue
11 | import androidx.compose.material3.ExperimentalMaterial3Api
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.IconButton
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Scaffold
16 | import androidx.compose.material3.SearchBar
17 | import androidx.compose.material3.Text
18 | import androidx.compose.material3.rememberDrawerState
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.rememberCoroutineScope
21 | import androidx.compose.ui.unit.dp
22 | import com.stevdza.san.androidapp.components.NavigationDrawer
23 | import com.stevdza.san.androidapp.components.PostCardsView
24 | import com.example.shared.Category
25 | import com.stevdza.san.androidapp.models.Post
26 | import com.stevdza.san.androidapp.util.RequestState
27 | import kotlinx.coroutines.launch
28 |
29 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
30 | @OptIn(ExperimentalMaterial3Api::class)
31 | @Composable
32 | fun HomeScreen(
33 | posts: RequestState>,
34 | searchedPosts: RequestState>,
35 | query: String,
36 | searchBarOpened: Boolean,
37 | active: Boolean,
38 | onActiveChange: (Boolean) -> Unit,
39 | onQueryChange: (String) -> Unit,
40 | onCategorySelect: (Category) -> Unit,
41 | onSearchBarChange: (Boolean) -> Unit,
42 | onSearch: (String) -> Unit,
43 | onPostClick: (String) -> Unit
44 | ) {
45 | val scope = rememberCoroutineScope()
46 | val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
47 | NavigationDrawer(
48 | drawerState = drawerState,
49 | onCategorySelect = {
50 | scope.launch {
51 | drawerState.close()
52 | }
53 | onCategorySelect(it)
54 | }
55 | ) {
56 | Scaffold(
57 | topBar = {
58 | CenterAlignedTopAppBar(
59 | title = {
60 | Text(text = "Blog")
61 | },
62 | navigationIcon = {
63 | IconButton(
64 | onClick = {
65 | scope.launch {
66 | drawerState.open()
67 | }
68 | }
69 | ) {
70 | Icon(
71 | imageVector = Icons.Default.Menu,
72 | contentDescription = "Drawer Icon"
73 | )
74 | }
75 | },
76 | actions = {
77 | IconButton(
78 | onClick = {
79 | onSearchBarChange(true)
80 | onActiveChange(true)
81 | }
82 | ) {
83 | Icon(
84 | imageVector = Icons.Default.Search,
85 | contentDescription = "Search Icon",
86 | tint = MaterialTheme.colorScheme.onSurface
87 | )
88 | }
89 | }
90 | )
91 | if (searchBarOpened) {
92 | SearchBar(
93 | query = query,
94 | onQueryChange = onQueryChange,
95 | onSearch = onSearch,
96 | active = active,
97 | onActiveChange = onActiveChange,
98 | placeholder = { Text(text = "Search here...") },
99 | leadingIcon = {
100 | IconButton(onClick = { onSearchBarChange(false) }) {
101 | Icon(
102 | imageVector = Icons.Default.ArrowBack,
103 | contentDescription = "Back Arrow Icon",
104 | tint = MaterialTheme.colorScheme.onSurface
105 | )
106 | }
107 | },
108 | trailingIcon = {
109 | IconButton(onClick = { onQueryChange("") }) {
110 | Icon(
111 | imageVector = Icons.Default.Close,
112 | contentDescription = "Close Icon",
113 | tint = MaterialTheme.colorScheme.onSurface
114 | )
115 | }
116 | }
117 | ) {
118 | PostCardsView(
119 | posts = searchedPosts,
120 | topMargin = 12.dp,
121 | onPostClick = onPostClick
122 | )
123 | }
124 | }
125 | }
126 | ) {
127 | PostCardsView(
128 | posts = posts,
129 | topMargin = it.calculateTopPadding(),
130 | hideMessage = true,
131 | onPostClick = onPostClick
132 | )
133 | }
134 | }
135 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/screens/home/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.screens.home
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.State
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.stevdza.san.androidapp.data.MongoSync
9 | import com.stevdza.san.androidapp.models.Post
10 | import com.stevdza.san.androidapp.util.Constants.APP_ID
11 | import com.stevdza.san.androidapp.util.RequestState
12 | import io.realm.kotlin.mongodb.App
13 | import io.realm.kotlin.mongodb.Credentials
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.flow.collectLatest
16 | import kotlinx.coroutines.launch
17 | import kotlinx.coroutines.withContext
18 |
19 | class HomeViewModel : ViewModel() {
20 | private val _allPosts: MutableState>> =
21 | mutableStateOf(RequestState.Idle)
22 | val allPosts: State>> = _allPosts
23 | private val _searchedPosts: MutableState>> =
24 | mutableStateOf(RequestState.Idle)
25 | val searchedPosts: State>> = _searchedPosts
26 |
27 | init {
28 | viewModelScope.launch(Dispatchers.IO) {
29 | App.create(APP_ID).login(Credentials.anonymous())
30 | fetchAllPosts()
31 | }
32 | }
33 |
34 | private suspend fun fetchAllPosts() {
35 | withContext(Dispatchers.Main) {
36 | _allPosts.value = RequestState.Loading
37 | }
38 | MongoSync.readAllPosts().collectLatest {
39 | _allPosts.value = it
40 | }
41 | }
42 |
43 | fun searchPostsByTitle(query: String) {
44 | viewModelScope.launch {
45 | withContext(Dispatchers.Main) {
46 | _searchedPosts.value = RequestState.Loading
47 | }
48 | MongoSync.searchPostsByTitle(query = query).collectLatest {
49 | _searchedPosts.value = it
50 | }
51 | }
52 | }
53 |
54 | fun resetSearchedPosts() {
55 | _searchedPosts.value = RequestState.Idle
56 | }
57 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val md_theme_light_primary = Color(0xFF00629D)
6 | val md_theme_light_onPrimary = Color(0xFFFFFFFF)
7 | val md_theme_light_primaryContainer = Color(0xFFCFE5FF)
8 | val md_theme_light_onPrimaryContainer = Color(0xFF001D34)
9 | val md_theme_light_secondary = Color(0xFF006689)
10 | val md_theme_light_onSecondary = Color(0xFFFFFFFF)
11 | val md_theme_light_secondaryContainer = Color(0xFFC2E8FF)
12 | val md_theme_light_onSecondaryContainer = Color(0xFF001E2C)
13 | val md_theme_light_tertiary = Color(0xFF00658E)
14 | val md_theme_light_onTertiary = Color(0xFFFFFFFF)
15 | val md_theme_light_tertiaryContainer = Color(0xFFC7E7FF)
16 | val md_theme_light_onTertiaryContainer = Color(0xFF001E2E)
17 | val md_theme_light_error = Color(0xFFBA1A1A)
18 | val md_theme_light_errorContainer = Color(0xFFFFDAD6)
19 | val md_theme_light_onError = Color(0xFFFFFFFF)
20 | val md_theme_light_onErrorContainer = Color(0xFF410002)
21 | val md_theme_light_background = Color(0xFFF8FDFF)
22 | val md_theme_light_onBackground = Color(0xFF001F25)
23 | val md_theme_light_surface = Color(0xFFF8FDFF)
24 | val md_theme_light_onSurface = Color(0xFF001F25)
25 | val md_theme_light_surfaceVariant = Color(0xFFDEE3EB)
26 | val md_theme_light_onSurfaceVariant = Color(0xFF42474E)
27 | val md_theme_light_outline = Color(0xFF72777F)
28 | val md_theme_light_inverseOnSurface = Color(0xFFD6F6FF)
29 | val md_theme_light_inverseSurface = Color(0xFF00363F)
30 | val md_theme_light_inversePrimary = Color(0xFF99CBFF)
31 | val md_theme_light_shadow = Color(0xFF000000)
32 | val md_theme_light_surfaceTint = Color(0xFF00629D)
33 | val md_theme_light_outlineVariant = Color(0xFFC2C7CF)
34 | val md_theme_light_scrim = Color(0xFF000000)
35 |
36 | val md_theme_dark_primary = Color(0xFF99CBFF)
37 | val md_theme_dark_onPrimary = Color(0xFF003355)
38 | val md_theme_dark_primaryContainer = Color(0xFF004A78)
39 | val md_theme_dark_onPrimaryContainer = Color(0xFFCFE5FF)
40 | val md_theme_dark_secondary = Color(0xFF77D1FF)
41 | val md_theme_dark_onSecondary = Color(0xFF003549)
42 | val md_theme_dark_secondaryContainer = Color(0xFF004D68)
43 | val md_theme_dark_onSecondaryContainer = Color(0xFFC2E8FF)
44 | val md_theme_dark_tertiary = Color(0xFF85CFFF)
45 | val md_theme_dark_onTertiary = Color(0xFF00344C)
46 | val md_theme_dark_tertiaryContainer = Color(0xFF004C6C)
47 | val md_theme_dark_onTertiaryContainer = Color(0xFFC7E7FF)
48 | val md_theme_dark_error = Color(0xFFFFB4AB)
49 | val md_theme_dark_errorContainer = Color(0xFF93000A)
50 | val md_theme_dark_onError = Color(0xFF690005)
51 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
52 | val md_theme_dark_background = Color(0xFF001F25)
53 | val md_theme_dark_onBackground = Color(0xFFA6EEFF)
54 | val md_theme_dark_surface = Color(0xFF001F25)
55 | val md_theme_dark_onSurface = Color(0xFFA6EEFF)
56 | val md_theme_dark_surfaceVariant = Color(0xFF42474E)
57 | val md_theme_dark_onSurfaceVariant = Color(0xFFC2C7CF)
58 | val md_theme_dark_outline = Color(0xFF8C9199)
59 | val md_theme_dark_inverseOnSurface = Color(0xFF001F25)
60 | val md_theme_dark_inverseSurface = Color(0xFFA6EEFF)
61 | val md_theme_dark_inversePrimary = Color(0xFF00629D)
62 | val md_theme_dark_shadow = Color(0xFF000000)
63 | val md_theme_dark_surfaceTint = Color(0xFF99CBFF)
64 | val md_theme_dark_outlineVariant = Color(0xFF42474E)
65 | val md_theme_dark_scrim = Color(0xFF000000)
66 |
67 |
68 | val seed = Color(0xFF00A2FF)
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 |
18 | private val lightColors = lightColorScheme(
19 | primary = md_theme_light_primary,
20 | onPrimary = md_theme_light_onPrimary,
21 | primaryContainer = md_theme_light_primaryContainer,
22 | onPrimaryContainer = md_theme_light_onPrimaryContainer,
23 | secondary = md_theme_light_secondary,
24 | onSecondary = md_theme_light_onSecondary,
25 | secondaryContainer = md_theme_light_secondaryContainer,
26 | onSecondaryContainer = md_theme_light_onSecondaryContainer,
27 | tertiary = md_theme_light_tertiary,
28 | onTertiary = md_theme_light_onTertiary,
29 | tertiaryContainer = md_theme_light_tertiaryContainer,
30 | onTertiaryContainer = md_theme_light_onTertiaryContainer,
31 | error = md_theme_light_error,
32 | errorContainer = md_theme_light_errorContainer,
33 | onError = md_theme_light_onError,
34 | onErrorContainer = md_theme_light_onErrorContainer,
35 | background = md_theme_light_background,
36 | onBackground = md_theme_light_onBackground,
37 | surface = md_theme_light_surface,
38 | onSurface = md_theme_light_onSurface,
39 | surfaceVariant = md_theme_light_surfaceVariant,
40 | onSurfaceVariant = md_theme_light_onSurfaceVariant,
41 | outline = md_theme_light_outline,
42 | inverseOnSurface = md_theme_light_inverseOnSurface,
43 | inverseSurface = md_theme_light_inverseSurface,
44 | inversePrimary = md_theme_light_inversePrimary,
45 | surfaceTint = md_theme_light_surfaceTint,
46 | outlineVariant = md_theme_light_outlineVariant,
47 | scrim = md_theme_light_scrim,
48 | )
49 |
50 | private val darkColors = darkColorScheme(
51 | primary = md_theme_dark_primary,
52 | onPrimary = md_theme_dark_onPrimary,
53 | primaryContainer = md_theme_dark_primaryContainer,
54 | onPrimaryContainer = md_theme_dark_onPrimaryContainer,
55 | secondary = md_theme_dark_secondary,
56 | onSecondary = md_theme_dark_onSecondary,
57 | secondaryContainer = md_theme_dark_secondaryContainer,
58 | onSecondaryContainer = md_theme_dark_onSecondaryContainer,
59 | tertiary = md_theme_dark_tertiary,
60 | onTertiary = md_theme_dark_onTertiary,
61 | tertiaryContainer = md_theme_dark_tertiaryContainer,
62 | onTertiaryContainer = md_theme_dark_onTertiaryContainer,
63 | error = md_theme_dark_error,
64 | errorContainer = md_theme_dark_errorContainer,
65 | onError = md_theme_dark_onError,
66 | onErrorContainer = md_theme_dark_onErrorContainer,
67 | background = md_theme_dark_background,
68 | onBackground = md_theme_dark_onBackground,
69 | surface = md_theme_dark_surface,
70 | onSurface = md_theme_dark_onSurface,
71 | surfaceVariant = md_theme_dark_surfaceVariant,
72 | onSurfaceVariant = md_theme_dark_onSurfaceVariant,
73 | outline = md_theme_dark_outline,
74 | inverseOnSurface = md_theme_dark_inverseOnSurface,
75 | inverseSurface = md_theme_dark_inverseSurface,
76 | inversePrimary = md_theme_dark_inversePrimary,
77 | surfaceTint = md_theme_dark_surfaceTint,
78 | outlineVariant = md_theme_dark_outlineVariant,
79 | scrim = md_theme_dark_scrim,
80 | )
81 |
82 | @Composable
83 | fun BlogMultiplatformTheme(
84 | darkTheme: Boolean = isSystemInDarkTheme(),
85 | // Dynamic color is available on Android 12+
86 | dynamicColor: Boolean = false,
87 | content: @Composable () -> Unit
88 | ) {
89 | val colorScheme = when {
90 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
91 | val context = LocalContext.current
92 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
93 | }
94 |
95 | darkTheme -> darkColors
96 | else -> lightColors
97 | }
98 | val view = LocalView.current
99 | if (!view.isInEditMode) {
100 | SideEffect {
101 | val window = (view.context as Activity).window
102 | window.statusBarColor = colorScheme.primary.toArgb()
103 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
104 | }
105 | }
106 |
107 | MaterialTheme(
108 | colorScheme = colorScheme,
109 | typography = Typography,
110 | content = content
111 | )
112 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/util/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.util
2 |
3 | object Constants {
4 | const val APP_ID = "myblogapp-bwscb"
5 | const val CATEGORY_ARGUMENT = "category"
6 | const val POST_ID_ARGUMENT = "postId"
7 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/util/Functions.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.util
2 |
3 | import android.graphics.Bitmap
4 | import android.graphics.BitmapFactory
5 | import android.util.Base64
6 | import java.lang.Exception
7 | import java.text.DateFormat
8 | import java.util.Date
9 |
10 | fun String.decodeThumbnailImage(): Bitmap? {
11 | return try {
12 | val byteArray = Base64.decode(this.cleanupImageString(), Base64.NO_WRAP)
13 | return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
14 | } catch (e: Exception) {
15 | null
16 | }
17 | }
18 |
19 | fun String.cleanupImageString(): String {
20 | return this.replace("data:image/png;base64,", "")
21 | .replace("data:image/jpeg;base64,", "")
22 | }
23 |
24 | fun Long.convertLongToDate(): String {
25 | return DateFormat.getDateInstance().format(Date(this))
26 | }
--------------------------------------------------------------------------------
/androidapp/src/main/java/com/stevdza/san/androidapp/util/RequestState.kt:
--------------------------------------------------------------------------------
1 | package com.stevdza.san.androidapp.util
2 |
3 | sealed class RequestState {
4 | data object Idle : RequestState()
5 | data object Loading : RequestState()
6 | data class Success(val data: T) : RequestState()
7 | data class Error(val error: Throwable) : RequestState()
8 | }
9 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/drawable/logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androidapp/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androidapp/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androidapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androidapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androidapp/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AndroidApp
3 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlin.multiplatform) apply false
3 | alias(libs.plugins.jetbrains.compose) apply false
4 | alias(libs.plugins.kobweb.library) apply false
5 | alias(libs.plugins.serialization.plugin) apply false
6 | alias(libs.plugins.com.android.application) apply false
7 | alias(libs.plugins.org.jetbrains.kotlin.android) apply false
8 | alias(libs.plugins.mongodb.realm) apply false
9 | }
10 |
11 | subprojects {
12 | repositories {
13 | mavenCentral()
14 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
15 | google()
16 | maven("https://us-central1-maven.pkg.dev/varabyte-repos/public")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 | android.useAndroidX=true
3 | org.gradle.jvmargs=-Xmx4096m
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | jetbrains-compose = "1.5.11"
3 | kobweb = "0.16.0"
4 | kotlin = "1.9.21"
5 | #kmongo = "4.9.0"
6 | serialization = "1.6.2"
7 | agp = "8.2.2"
8 | core-ktx = "1.12.0"
9 | lifecycle-runtime-ktx = "2.7.0"
10 | activity-compose = "1.8.2"
11 | compose-bom = "2024.02.00"
12 | navigation = "2.7.7"
13 | coroutines = "1.7.3"
14 | mongodb = "1.13.0"
15 | mongodb-kotlin = "4.11.0"
16 | coil = "2.5.0"
17 |
18 | [libraries]
19 | kobweb-api = { module = "com.varabyte.kobweb:kobweb-api", version.ref = "kobweb" }
20 | kobweb-core = { module = "com.varabyte.kobweb:kobweb-core ", version.ref = "kobweb" }
21 | kobweb-silk-core = { module = "com.varabyte.kobweb:kobweb-silk", version.ref = "kobweb" }
22 | kobweb-silk-icons-fa = { module = "com.varabyte.kobwebx:silk-icons-fa", version.ref = "kobweb" }
23 | kobweb-silk-icons-mdi = { module = "com.varabyte.kobweb:kobweb-silk-icons-mdi", version.ref = "kobweb" }
24 | kobwebx-markdown = { module = "com.varabyte.kobwebx:kobwebx-markdown", version.ref = "kobweb" }
25 | #kmongo-database = { module = "org.litote.kmongo:kmongo-coroutine-serialization", version.ref = "kmongo" }
26 | kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
27 | core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" }
28 | lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
29 | activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
30 | compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
31 | ui = { module = "androidx.compose.ui:ui" }
32 | ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
33 | ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
34 | ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
35 | material3 = { module = "androidx.compose.material3:material3" }
36 | navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
37 | kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
38 | mongodb-sync = { module = "io.realm.kotlin:library-sync", version.ref = "mongodb" }
39 | mongodb-kotlin-driver = { module = "org.mongodb:mongodb-driver-kotlin-coroutine", version.ref = "mongodb-kotlin" }
40 | coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
41 |
42 | [plugins]
43 | jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "jetbrains-compose" }
44 | kobweb-application = { id = "com.varabyte.kobweb.application", version.ref = "kobweb" }
45 | kobweb-library = { id = "com.varabyte.kobweb.library", version.ref = "kobweb" }
46 | kobwebx-markdown = { id = "com.varabyte.kobwebx.markdown", version.ref = "kobweb" }
47 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
48 | serialization-plugin = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
49 | com-android-application = { id = "com.android.application", version.ref = "agp" }
50 | org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
51 | mongodb-realm = { id = "io.realm.kotlin", version.ref = "mongodb" }
52 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MSYS* | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
7 | maven("https://us-central1-maven.pkg.dev/varabyte-repos/public")
8 | }
9 | }
10 |
11 | rootProject.name = "BlogMultiplatform"
12 |
13 | include(":site")
14 | include(":androidapp")
15 | include(":shared")
16 |
--------------------------------------------------------------------------------
/shared/.gitignore:
--------------------------------------------------------------------------------
1 | # Kobweb ignores
2 | .kobweb/*
3 | !.kobweb/conf.yaml
4 |
--------------------------------------------------------------------------------
/shared/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.varabyte.kobweb.gradle.library.util.configAsKobwebLibrary
2 |
3 | plugins {
4 | alias(libs.plugins.kotlin.multiplatform)
5 | alias(libs.plugins.kobweb.library)
6 | id("com.android.library")
7 | }
8 |
9 | group = "com.example.shared"
10 | version = "1.0-SNAPSHOT"
11 |
12 | kotlin {
13 | configAsKobwebLibrary(includeServer = true)
14 |
15 | js(IR) { browser() }
16 | jvm()
17 | androidTarget {
18 | compilations.all {
19 | kotlinOptions {
20 | jvmTarget = "17"
21 | }
22 | }
23 | }
24 |
25 | sourceSets {
26 | val commonMain by getting {
27 | dependencies {
28 | implementation(libs.kotlinx.serialization)
29 | }
30 | }
31 |
32 | val jsMain by getting {
33 | dependencies {
34 | implementation(libs.kobweb.core)
35 | implementation(libs.kobweb.silk.core)
36 | implementation(libs.kobweb.silk.icons.fa)
37 | }
38 | }
39 |
40 | val jvmMain by getting {
41 | dependencies {}
42 | }
43 |
44 | androidMain.dependencies {}
45 | }
46 | }
47 |
48 | android {
49 | namespace = "com.example.shared"
50 | compileSdk = 34
51 | defaultConfig {
52 | minSdk = 24
53 | }
54 |
55 | compileOptions {
56 | sourceCompatibility = JavaVersion.VERSION_17
57 | targetCompatibility = JavaVersion.VERSION_17
58 | }
59 | }
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/example/shared/Category.kt:
--------------------------------------------------------------------------------
1 | package com.example.shared
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | actual enum class Category(override val color: String): CategoryColor {
7 | Technology(color = ""),
8 | Programming(color = ""),
9 | Design(color = "")
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/example/shared/Category.kt:
--------------------------------------------------------------------------------
1 | package com.example.shared
2 |
3 | expect enum class Category: CategoryColor {
4 | Technology,
5 | Programming,
6 | Design
7 | }
8 |
9 | interface CategoryColor {
10 | val color: String
11 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/example/shared/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.example.shared
2 |
3 | object Constants {
4 | const val SHOW_SECTIONS_PARAM = "showSections"
5 | }
--------------------------------------------------------------------------------
/shared/src/jsMain/kotlin/com/example/shared/Category.kt:
--------------------------------------------------------------------------------
1 | package com.example.shared
2 |
3 | import com.varabyte.kobweb.compose.ui.graphics.Color
4 | import com.varabyte.kobweb.compose.ui.graphics.Color.Companion.rgb
5 | import com.varabyte.kobweb.compose.ui.graphics.Color.Companion.rgba
6 | import kotlinx.serialization.Serializable
7 |
8 | @Serializable
9 | actual enum class Category(override val color: String): CategoryColor {
10 | Technology(color = JsTheme.Green.hex),
11 | Programming(color = JsTheme.Yellow.hex),
12 | Design(color = JsTheme.Purple.hex)
13 | }
14 |
15 | enum class JsTheme(
16 | val hex: String,
17 | val rgb: Color.Rgb
18 | ) {
19 | Primary(
20 | hex = "#00A2FF",
21 | rgb = rgb(r = 0, g = 162, b = 255)
22 | ),
23 | Secondary(
24 | hex = "#001019",
25 | rgb = rgb(r = 0, g = 16, b = 25)
26 | ),
27 | Tertiary(
28 | hex = "#001925",
29 | rgb = rgb(r = 0, g = 25, b = 37)
30 | ),
31 | LightGray(
32 | hex = "#FAFAFA",
33 | rgb = rgb(r = 250, g = 250, b = 250)
34 | ),
35 | Gray(
36 | hex = "#E9E9E9",
37 | rgb = rgb(r = 233, g = 233, b = 233)
38 | ),
39 | DarkGray(
40 | hex = "#646464",
41 | rgb = rgb(r = 100, g = 100, b = 100)
42 | ),
43 | HalfWhite(
44 | hex = "#FFFFFF",
45 | rgb = rgba(r = 255, g = 255, b = 255, a = 0.5f)
46 | ),
47 | HalfBlack(
48 | hex = "#000000",
49 | rgb = rgba(r = 0, g = 0, b = 0, a = 0.5f)
50 | ),
51 | White(
52 | hex = "#FFFFFF",
53 | rgb = rgb(r = 255, g = 255, b = 255)
54 | ),
55 | Green(
56 | hex = "#00FF94",
57 | rgb = rgb(r = 0, g = 255, b = 148)
58 | ),
59 | Yellow(
60 | hex = "#FFEC45",
61 | rgb = rgb(r = 255, g = 236, b = 69)
62 | ),
63 | Red(
64 | hex = "#FF6359",
65 | rgb = rgb(r = 255, g = 99, b = 89)
66 | ),
67 | Purple(
68 | hex = "#8B6DFF",
69 | rgb = rgb(r = 139, g = 109, b = 255)
70 | ),
71 | Sponsored(
72 | hex = "#3300FF",
73 | rgb = rgb(r = 51, g = 0, b = 255)
74 | )
75 | }
--------------------------------------------------------------------------------
/shared/src/jvmMain/kotlin/com/example/shared/Category.kt:
--------------------------------------------------------------------------------
1 | package com.example.shared
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | actual enum class Category(override val color: String): CategoryColor {
7 | Technology(color = JvmTheme.Green.hex),
8 | Programming(color = JvmTheme.Yellow.hex),
9 | Design(color = JvmTheme.Purple.hex)
10 | }
11 |
12 | enum class JvmTheme(val hex: String) {
13 | Purple(hex = "#8B6DFF"),
14 | Green(hex = "#00FF94"),
15 | Yellow(hex = "#FFEC45")
16 | }
--------------------------------------------------------------------------------
/site/.gitignore:
--------------------------------------------------------------------------------
1 | # Kobweb ignores
2 | .kobweb/*
3 | !.kobweb/conf.yaml
4 |
--------------------------------------------------------------------------------
/site/.kobweb/conf.yaml:
--------------------------------------------------------------------------------
1 | site:
2 | title: "BlogMultiplatform"
3 |
4 | server:
5 | files:
6 | dev:
7 | contentRoot: "build/processedResources/js/main/public"
8 | script: "build/dist/js/developmentExecutable/blogmultiplatform.js"
9 | api: "build/libs/blogmultiplatform.jar"
10 | prod:
11 | script: "build/dist/js/productionExecutable/blogmultiplatform.js"
12 | siteRoot: ".kobweb/site"
13 | cors:
14 | hosts:
15 | - name: "blogmultiplatform.onrender.com"
16 | schemes:
17 | - "https"
18 |
19 | port: 8080
--------------------------------------------------------------------------------
/site/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.varabyte.kobweb.gradle.application.util.configAsKobwebApplication
2 | import kotlinx.html.link
3 | import kotlinx.html.script
4 |
5 | plugins {
6 | alias(libs.plugins.kotlin.multiplatform)
7 | alias(libs.plugins.jetbrains.compose)
8 | alias(libs.plugins.kobweb.application)
9 | alias(libs.plugins.serialization.plugin)
10 | // alias(libs.plugins.kobwebx.markdown)
11 | }
12 |
13 | group = "com.example.blogmultiplatform"
14 | version = "1.0-SNAPSHOT"
15 |
16 | kobweb {
17 | app {
18 | index {
19 | description.set("Powered by Kobweb")
20 |
21 | head.add {
22 | script {
23 | src = "/highlight.min.js"
24 | }
25 | link {
26 | rel = "stylesheet"
27 | href = "/github-dark.css"
28 | }
29 | script {
30 | src = "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
31 | }
32 | link {
33 | rel = "stylesheet"
34 | href = "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
35 | }
36 | }
37 | }
38 | }
39 | }
40 |
41 | kotlin {
42 | configAsKobwebApplication("blogmultiplatform", includeServer = true)
43 |
44 | @Suppress("UNUSED_VARIABLE") // Suppress spurious warnings about sourceset variables not being used
45 | sourceSets {
46 | val commonMain by getting {
47 | dependencies {
48 | implementation(compose.runtime)
49 | implementation(libs.kotlinx.serialization)
50 | implementation(project(":shared"))
51 | }
52 | }
53 |
54 | val jsMain by getting {
55 | dependencies {
56 | implementation(compose.html.core)
57 | implementation(libs.kobweb.core)
58 | implementation(libs.kobweb.silk.core)
59 | implementation(libs.kobweb.silk.icons.fa)
60 | implementation(libs.kotlinx.serialization)
61 | // implementation(libs.kobwebx.markdown)
62 | }
63 | }
64 | val jvmMain by getting {
65 | dependencies {
66 | implementation(libs.kobweb.api)
67 | implementation(libs.mongodb.kotlin.driver)
68 | implementation(libs.kotlinx.serialization)
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/site/src/commonMain/kotlin/com/example/blogmultiplatform/models/ApiListResponse.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.models
2 |
3 | import kotlinx.serialization.Serializable
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.json.JsonContentPolymorphicSerializer
6 | import kotlinx.serialization.json.JsonElement
7 | import kotlinx.serialization.json.jsonObject
8 |
9 | @Serializable(ApiListResponseSerializer::class)
10 | sealed class ApiListResponse {
11 | @Serializable
12 | @SerialName("idle")
13 | object Idle : ApiListResponse()
14 |
15 | @Serializable
16 | @SerialName("success")
17 | data class Success(val data: List) : ApiListResponse()
18 |
19 | @Serializable
20 | @SerialName("error")
21 | data class Error(val message: String) : ApiListResponse()
22 | }
23 |
24 | @Serializable(ApiResponseSerializer::class)
25 | sealed class ApiResponse {
26 | @Serializable
27 | @SerialName("idle")
28 | object Idle : ApiResponse()
29 |
30 | @Serializable
31 | @SerialName("success")
32 | data class Success(val data: Post) : ApiResponse()
33 |
34 | @Serializable
35 | @SerialName("error")
36 | data class Error(val message: String) : ApiResponse()
37 | }
38 |
39 | object ApiListResponseSerializer :
40 | JsonContentPolymorphicSerializer(ApiListResponse::class) {
41 | override fun selectDeserializer(element: JsonElement) = when {
42 | "data" in element.jsonObject -> ApiListResponse.Success.serializer()
43 | "message" in element.jsonObject -> ApiListResponse.Error.serializer()
44 | else -> ApiListResponse.Idle.serializer()
45 | }
46 | }
47 |
48 | object ApiResponseSerializer :
49 | JsonContentPolymorphicSerializer(ApiResponse::class) {
50 | override fun selectDeserializer(element: JsonElement) = when {
51 | "data" in element.jsonObject -> ApiResponse.Success.serializer()
52 | "message" in element.jsonObject -> ApiResponse.Error.serializer()
53 | else -> ApiResponse.Idle.serializer()
54 | }
55 | }
--------------------------------------------------------------------------------
/site/src/commonMain/kotlin/com/example/blogmultiplatform/models/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.models
2 |
3 | object Constants {
4 | const val POSTS_PER_PAGE = 8
5 | const val QUERY_PARAM = "query"
6 | const val CATEGORY_PARAM = "category"
7 | const val POST_ID_PARAM = "postId"
8 | const val SKIP_PARAM = "skip"
9 | const val AUTHOR_PARAM = "author"
10 | const val UPDATED_PARAM = "updated"
11 | }
--------------------------------------------------------------------------------
/site/src/commonMain/kotlin/com/example/blogmultiplatform/models/Newsletter.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.models
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class Newsletter(
7 | val email: String
8 | )
9 |
--------------------------------------------------------------------------------
/site/src/commonMain/kotlin/com/example/blogmultiplatform/models/Post.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.models
2 |
3 | import com.example.shared.Category
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class Post(
8 | val _id: String = "",
9 | val author: String = "",
10 | val date: Double = 0.0,
11 | // val date: Long = 0L,
12 | val title: String,
13 | val subtitle: String,
14 | val thumbnail: String,
15 | val content: String,
16 | val category: Category,
17 | val popular: Boolean = false,
18 | val main: Boolean = false,
19 | val sponsored: Boolean = false
20 | )
21 |
22 | @Serializable
23 | data class PostWithoutDetails(
24 | val _id: String = "",
25 | val author: String,
26 | val date: Double,
27 | // val date: Long,
28 | val title: String,
29 | val subtitle: String,
30 | val thumbnail: String,
31 | val category: Category,
32 | val popular: Boolean = false,
33 | val main: Boolean = false,
34 | val sponsored: Boolean = false
35 | )
--------------------------------------------------------------------------------
/site/src/commonMain/kotlin/com/example/blogmultiplatform/models/User.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.models
2 |
3 | expect class User {
4 | val _id: String
5 | val username: String
6 | val password: String
7 | }
8 |
9 | expect class UserWithoutPassword {
10 | val _id: String
11 | val username: String
12 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/MyApp.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform
2 |
3 | import androidx.compose.runtime.*
4 | import com.varabyte.kobweb.compose.ui.modifiers.*
5 | import com.varabyte.kobweb.core.App
6 | import com.varabyte.kobweb.silk.init.InitSilk
7 | import com.varabyte.kobweb.silk.init.InitSilkContext
8 | import com.varabyte.kobweb.silk.SilkApp
9 | import com.varabyte.kobweb.silk.components.layout.Surface
10 | import com.varabyte.kobweb.silk.components.style.common.SmoothColorStyle
11 | import com.varabyte.kobweb.silk.components.style.toModifier
12 |
13 | import org.jetbrains.compose.web.css.*
14 |
15 | @InitSilk
16 | fun updateTheme(ctx: InitSilkContext) {
17 | // Configure silk here
18 | }
19 |
20 | @App
21 | @Composable
22 | fun MyApp(content: @Composable () -> Unit) {
23 | SilkApp {
24 | Surface(SmoothColorStyle.toModifier().minHeight(100.vh)) {
25 | content()
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/components/AdminPageLayout.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.runtime.setValue
8 | import com.example.blogmultiplatform.util.Constants.PAGE_WIDTH
9 | import com.varabyte.kobweb.compose.foundation.layout.Box
10 | import com.varabyte.kobweb.compose.foundation.layout.Column
11 | import com.varabyte.kobweb.compose.ui.Alignment
12 | import com.varabyte.kobweb.compose.ui.Modifier
13 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize
14 | import com.varabyte.kobweb.compose.ui.modifiers.maxWidth
15 | import org.jetbrains.compose.web.css.px
16 |
17 | @Composable
18 | fun AdminPageLayout(content: @Composable () -> Unit) {
19 | var overflowOpened by remember { mutableStateOf(false) }
20 | Box(
21 | modifier = Modifier.fillMaxSize(),
22 | contentAlignment = Alignment.Center
23 | ) {
24 | Column(
25 | modifier = Modifier
26 | .fillMaxSize()
27 | .maxWidth(PAGE_WIDTH.px)
28 | ) {
29 | SidePanel(onMenuClick = {
30 | overflowOpened = true
31 | })
32 | if (overflowOpened) {
33 | OverflowSidePanel(
34 | onMenuClose = {
35 | overflowOpened = false
36 | },
37 | content = {
38 | NavigationItems()
39 | }
40 | )
41 | }
42 | content()
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/components/CategoryChip.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.example.shared.JsTheme
5 | import com.example.blogmultiplatform.util.Constants.FONT_FAMILY
6 | import com.varabyte.kobweb.compose.foundation.layout.Box
7 | import com.varabyte.kobweb.compose.ui.Alignment
8 | import com.varabyte.kobweb.compose.ui.Modifier
9 | import com.varabyte.kobweb.compose.ui.modifiers.border
10 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius
11 | import com.varabyte.kobweb.compose.ui.modifiers.color
12 | import com.varabyte.kobweb.compose.ui.modifiers.fontFamily
13 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize
14 | import com.varabyte.kobweb.compose.ui.modifiers.height
15 | import com.varabyte.kobweb.compose.ui.modifiers.padding
16 | import com.varabyte.kobweb.silk.components.text.SpanText
17 | import org.jetbrains.compose.web.css.LineStyle
18 | import org.jetbrains.compose.web.css.px
19 |
20 | @Composable
21 | fun CategoryChip(
22 | category: com.example.shared.Category,
23 | darkTheme: Boolean = false
24 | ) {
25 | Box(
26 | modifier = Modifier
27 | .height(32.px)
28 | .padding(leftRight = 14.px)
29 | .borderRadius(r = 100.px)
30 | .border(
31 | width = 1.px,
32 | style = LineStyle.Solid,
33 | color = if(darkTheme) JsTheme.entries.find { it.hex == category.color }?.rgb else JsTheme.HalfBlack.rgb
34 | ),
35 | contentAlignment = Alignment.Center
36 | ) {
37 | SpanText(
38 | modifier = Modifier
39 | .fontFamily(FONT_FAMILY)
40 | .fontSize(12.px)
41 | .color(
42 | if (darkTheme) JsTheme.entries.find { it.hex == category.color }?.rgb ?: JsTheme.HalfBlack.rgb
43 | else JsTheme.HalfBlack.rgb
44 | ),
45 | text = category.name
46 | )
47 | }
48 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/components/CategoryNavigationItems.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.example.shared.Category
5 | import com.example.shared.JsTheme
6 | import com.example.blogmultiplatform.navigation.Screen
7 | import com.example.blogmultiplatform.styles.CategoryItemStyle
8 | import com.example.blogmultiplatform.util.Constants
9 | import com.varabyte.kobweb.compose.css.FontWeight
10 | import com.varabyte.kobweb.compose.css.TextDecorationLine
11 | import com.varabyte.kobweb.compose.ui.Modifier
12 | import com.varabyte.kobweb.compose.ui.modifiers.color
13 | import com.varabyte.kobweb.compose.ui.modifiers.fontFamily
14 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize
15 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight
16 | import com.varabyte.kobweb.compose.ui.modifiers.margin
17 | import com.varabyte.kobweb.compose.ui.modifiers.onClick
18 | import com.varabyte.kobweb.compose.ui.modifiers.textDecorationLine
19 | import com.varabyte.kobweb.compose.ui.thenIf
20 | import com.varabyte.kobweb.core.rememberPageContext
21 | import com.varabyte.kobweb.silk.components.navigation.Link
22 | import com.varabyte.kobweb.silk.components.style.toModifier
23 | import org.jetbrains.compose.web.css.px
24 |
25 | @Composable
26 | fun CategoryNavigationItems(
27 | selectedCategory: Category? = null,
28 | vertical: Boolean = false
29 | ) {
30 | val context = rememberPageContext()
31 | Category.entries.forEach { category ->
32 | Link(
33 | modifier = CategoryItemStyle.toModifier()
34 | .thenIf(
35 | condition = vertical,
36 | other = Modifier.margin(bottom = 24.px)
37 | )
38 | .thenIf(
39 | condition = !vertical,
40 | other = Modifier.margin(right = 24.px)
41 | )
42 | .thenIf(
43 | condition = selectedCategory == category,
44 | other = Modifier.color(JsTheme.Primary.rgb)
45 | )
46 | .fontFamily(Constants.FONT_FAMILY)
47 | .fontSize(16.px)
48 | .fontWeight(FontWeight.Medium)
49 | .textDecorationLine(TextDecorationLine.None)
50 | .onClick { context.router.navigateTo(Screen.SearchPage.searchByCategory(category)) },
51 | path = "",
52 | text = category.name
53 | )
54 | }
55 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/components/ErrorView.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.example.blogmultiplatform.util.Constants.FONT_FAMILY
5 | import com.varabyte.kobweb.compose.css.FontWeight
6 | import com.varabyte.kobweb.compose.foundation.layout.Box
7 | import com.varabyte.kobweb.compose.ui.Alignment
8 | import com.varabyte.kobweb.compose.ui.Modifier
9 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize
10 | import com.varabyte.kobweb.compose.ui.modifiers.fontFamily
11 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize
12 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight
13 | import com.varabyte.kobweb.silk.components.text.SpanText
14 | import org.jetbrains.compose.web.css.px
15 |
16 | @Composable
17 | fun ErrorView(message: String) {
18 | Box(
19 | modifier = Modifier
20 | .fillMaxSize(),
21 | contentAlignment = Alignment.Center
22 | ) {
23 | SpanText(
24 | modifier = Modifier
25 | .fontFamily(FONT_FAMILY)
26 | .fontSize(18.px)
27 | .fontWeight(FontWeight.Medium),
28 | text = message
29 | )
30 | }
31 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/components/LoadingIndicator.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.varabyte.kobweb.compose.foundation.layout.Box
5 | import com.varabyte.kobweb.compose.ui.Alignment
6 | import com.varabyte.kobweb.compose.ui.Modifier
7 | import com.varabyte.kobweb.compose.ui.modifiers.classNames
8 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize
9 | import com.varabyte.kobweb.compose.ui.modifiers.height
10 | import com.varabyte.kobweb.compose.ui.modifiers.padding
11 | import com.varabyte.kobweb.compose.ui.toAttrs
12 | import org.jetbrains.compose.web.css.px
13 | import org.jetbrains.compose.web.css.vh
14 | import org.jetbrains.compose.web.dom.Div
15 | import org.jetbrains.compose.web.dom.Span
16 | import org.jetbrains.compose.web.dom.Text
17 |
18 | @Composable
19 | fun LoadingIndicator(modifier: Modifier = Modifier) {
20 | Box(
21 | modifier = modifier
22 | .fillMaxSize()
23 | .height(100.vh)
24 | .padding(topBottom = 50.px),
25 | contentAlignment = Alignment.Center
26 | ) {
27 | Div(
28 | attrs = Modifier
29 | .classNames("spinner-border", "text-primary")
30 | .toAttrs()
31 | ) {
32 | Span(
33 | attrs = Modifier
34 | .classNames("visually-hidden")
35 | .toAttrs()
36 | ) { Text("Loading...") }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/components/Popup.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.example.blogmultiplatform.models.EditorControl
5 | import com.example.shared.JsTheme
6 | import com.example.blogmultiplatform.util.Constants.FONT_FAMILY
7 | import com.example.blogmultiplatform.util.Id
8 | import com.example.blogmultiplatform.util.noBorder
9 | import com.varabyte.kobweb.compose.css.TextAlign
10 | import com.varabyte.kobweb.compose.foundation.layout.Box
11 | import com.varabyte.kobweb.compose.foundation.layout.Column
12 | import com.varabyte.kobweb.compose.ui.Alignment
13 | import com.varabyte.kobweb.compose.ui.Modifier
14 | import com.varabyte.kobweb.compose.ui.graphics.Colors
15 | import com.varabyte.kobweb.compose.ui.modifiers.backgroundColor
16 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius
17 | import com.varabyte.kobweb.compose.ui.modifiers.color
18 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize
19 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth
20 | import com.varabyte.kobweb.compose.ui.modifiers.fontFamily
21 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize
22 | import com.varabyte.kobweb.compose.ui.modifiers.height
23 | import com.varabyte.kobweb.compose.ui.modifiers.id
24 | import com.varabyte.kobweb.compose.ui.modifiers.margin
25 | import com.varabyte.kobweb.compose.ui.modifiers.onClick
26 | import com.varabyte.kobweb.compose.ui.modifiers.padding
27 | import com.varabyte.kobweb.compose.ui.modifiers.position
28 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign
29 | import com.varabyte.kobweb.compose.ui.modifiers.width
30 | import com.varabyte.kobweb.compose.ui.modifiers.zIndex
31 | import com.varabyte.kobweb.compose.ui.toAttrs
32 | import com.varabyte.kobweb.silk.components.text.SpanText
33 | import kotlinx.browser.document
34 | import org.jetbrains.compose.web.attributes.InputType
35 | import org.jetbrains.compose.web.css.Position
36 | import org.jetbrains.compose.web.css.px
37 | import org.jetbrains.compose.web.css.vh
38 | import org.jetbrains.compose.web.css.vw
39 | import org.jetbrains.compose.web.dom.Button
40 | import org.jetbrains.compose.web.dom.Input
41 | import org.w3c.dom.HTMLInputElement
42 |
43 | @Composable
44 | fun MessagePopup(
45 | message: String,
46 | onDialogDismiss: () -> Unit
47 | ) {
48 | Box(
49 | modifier = Modifier
50 | .width(100.vw)
51 | .height(100.vh)
52 | .position(Position.Fixed)
53 | .zIndex(19),
54 | contentAlignment = Alignment.Center
55 | ) {
56 | Box(
57 | modifier = Modifier
58 | .fillMaxSize()
59 | .backgroundColor(JsTheme.HalfBlack.rgb)
60 | .onClick { onDialogDismiss() }
61 | )
62 | Box(
63 | modifier = Modifier
64 | .padding(all = 24.px)
65 | .backgroundColor(Colors.White)
66 | .borderRadius(r = 4.px)
67 | ) {
68 | SpanText(
69 | modifier = Modifier
70 | .fillMaxWidth()
71 | .textAlign(TextAlign.Center)
72 | .fontFamily(FONT_FAMILY)
73 | .fontSize(16.px),
74 | text = message
75 | )
76 | }
77 | }
78 | }
79 |
80 | @Composable
81 | fun ControlPopup(
82 | editorControl: EditorControl,
83 | onDialogDismiss: () -> Unit,
84 | onAddClick: (String, String) -> Unit
85 | ) {
86 | Box(
87 | modifier = Modifier
88 | .width(100.vw)
89 | .height(100.vh)
90 | .position(Position.Fixed)
91 | .zIndex(19),
92 | contentAlignment = Alignment.Center
93 | ) {
94 | Box(
95 | modifier = Modifier
96 | .fillMaxSize()
97 | .backgroundColor(JsTheme.HalfBlack.rgb)
98 | .onClick { onDialogDismiss() }
99 | )
100 | Column(
101 | modifier = Modifier
102 | .width(500.px)
103 | .padding(all = 24.px)
104 | .backgroundColor(Colors.White)
105 | .borderRadius(r = 4.px)
106 | ) {
107 | Input(
108 | type = InputType.Text,
109 | attrs = Modifier
110 | .id(Id.linkHrefInput)
111 | .fillMaxWidth()
112 | .height(54.px)
113 | .padding(left = 20.px)
114 | .margin(bottom = 12.px)
115 | .fontFamily(FONT_FAMILY)
116 | .fontSize(14.px)
117 | .noBorder()
118 | .borderRadius(r = 4.px)
119 | .backgroundColor(JsTheme.LightGray.rgb)
120 | .toAttrs {
121 | attr(
122 | "placeholder",
123 | if (editorControl == EditorControl.Link) "Href" else "Image URL"
124 | )
125 | }
126 | )
127 | Input(
128 | type = InputType.Text,
129 | attrs = Modifier
130 | .id(Id.linkTitleInput)
131 | .fillMaxWidth()
132 | .height(54.px)
133 | .padding(left = 20.px)
134 | .margin(bottom = 20.px)
135 | .fontFamily(FONT_FAMILY)
136 | .fontSize(14.px)
137 | .noBorder()
138 | .borderRadius(r = 4.px)
139 | .backgroundColor(JsTheme.LightGray.rgb)
140 | .toAttrs {
141 | attr(
142 | "placeholder",
143 | if (editorControl == EditorControl.Link) "Title" else "Description"
144 | )
145 | }
146 | )
147 | Button(
148 | attrs = Modifier
149 | .onClick {
150 | val href =
151 | (document.getElementById(Id.linkHrefInput) as HTMLInputElement).value
152 | val title =
153 | (document.getElementById(Id.linkTitleInput) as HTMLInputElement).value
154 | onAddClick(href, title)
155 | onDialogDismiss()
156 | }
157 | .fillMaxWidth()
158 | .height(54.px)
159 | .borderRadius(r = 4.px)
160 | .backgroundColor(JsTheme.Primary.rgb)
161 | .color(Colors.White)
162 | .noBorder()
163 | .fontFamily(FONT_FAMILY)
164 | .fontSize(14.px)
165 | .toAttrs()
166 | ) {
167 | SpanText(text = "Add")
168 | }
169 | }
170 | }
171 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/components/PostsView.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.example.blogmultiplatform.models.PostWithoutDetails
5 | import com.example.blogmultiplatform.util.Constants.FONT_FAMILY
6 | import com.varabyte.kobweb.compose.css.Cursor
7 | import com.varabyte.kobweb.compose.css.FontWeight
8 | import com.varabyte.kobweb.compose.css.TextAlign
9 | import com.varabyte.kobweb.compose.css.Visibility
10 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement
11 | import com.varabyte.kobweb.compose.foundation.layout.Column
12 | import com.varabyte.kobweb.compose.ui.Modifier
13 | import com.varabyte.kobweb.compose.ui.modifiers.cursor
14 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth
15 | import com.varabyte.kobweb.compose.ui.modifiers.fontFamily
16 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize
17 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight
18 | import com.varabyte.kobweb.compose.ui.modifiers.margin
19 | import com.varabyte.kobweb.compose.ui.modifiers.onClick
20 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign
21 | import com.varabyte.kobweb.compose.ui.modifiers.visibility
22 | import com.varabyte.kobweb.silk.components.layout.SimpleGrid
23 | import com.varabyte.kobweb.silk.components.layout.numColumns
24 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint
25 | import com.varabyte.kobweb.silk.components.text.SpanText
26 | import org.jetbrains.compose.web.css.percent
27 | import org.jetbrains.compose.web.css.px
28 |
29 | @Composable
30 | fun PostsView(
31 | breakpoint: Breakpoint,
32 | posts: List,
33 | title: String? = null,
34 | selectableMode: Boolean = false,
35 | onSelect: (String) -> Unit = {},
36 | onDeselect: (String) -> Unit = {},
37 | showMoreVisibility: Boolean,
38 | onShowMore: () -> Unit,
39 | onClick: (String) -> Unit
40 | ) {
41 | Column(
42 | modifier = Modifier.fillMaxWidth(
43 | if (breakpoint > Breakpoint.MD) 80.percent
44 | else 90.percent
45 | ),
46 | verticalArrangement = Arrangement.Center
47 | ) {
48 | if (title != null) {
49 | SpanText(
50 | modifier = Modifier
51 | .margin(bottom = 24.px)
52 | .fontFamily(FONT_FAMILY)
53 | .fontSize(18.px)
54 | .fontWeight(FontWeight.Medium),
55 | text = title
56 | )
57 | }
58 | SimpleGrid(
59 | modifier = Modifier.fillMaxWidth(),
60 | numColumns = numColumns(base = 1, sm = 2, md = 3, lg = 4)
61 | ) {
62 | posts.forEach {
63 | PostPreview(
64 | post = it,
65 | selectableMode = selectableMode,
66 | onSelect = onSelect,
67 | onDeselect = onDeselect,
68 | onClick = onClick
69 | )
70 | }
71 | }
72 | SpanText(
73 | modifier = Modifier
74 | .fillMaxWidth()
75 | .margin(topBottom = 50.px)
76 | .textAlign(TextAlign.Center)
77 | .fontFamily(FONT_FAMILY)
78 | .fontSize(16.px)
79 | .fontWeight(FontWeight.Medium)
80 | .cursor(Cursor.Pointer)
81 | .visibility(if (showMoreVisibility) Visibility.Visible else Visibility.Hidden)
82 | .onClick { onShowMore() },
83 | text = "Show more"
84 | )
85 | }
86 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/components/SearchBar.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.runtime.setValue
9 | import com.example.shared.JsTheme
10 | import com.example.blogmultiplatform.util.Id
11 | import com.example.blogmultiplatform.util.noBorder
12 | import com.varabyte.kobweb.compose.css.CSSTransition
13 | import com.varabyte.kobweb.compose.css.Cursor
14 | import com.varabyte.kobweb.compose.foundation.layout.Row
15 | import com.varabyte.kobweb.compose.ui.Alignment
16 | import com.varabyte.kobweb.compose.ui.Modifier
17 | import com.varabyte.kobweb.compose.ui.graphics.Colors
18 | import com.varabyte.kobweb.compose.ui.modifiers.backgroundColor
19 | import com.varabyte.kobweb.compose.ui.modifiers.border
20 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius
21 | import com.varabyte.kobweb.compose.ui.modifiers.color
22 | import com.varabyte.kobweb.compose.ui.modifiers.cursor
23 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize
24 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth
25 | import com.varabyte.kobweb.compose.ui.modifiers.height
26 | import com.varabyte.kobweb.compose.ui.modifiers.id
27 | import com.varabyte.kobweb.compose.ui.modifiers.margin
28 | import com.varabyte.kobweb.compose.ui.modifiers.onClick
29 | import com.varabyte.kobweb.compose.ui.modifiers.onFocusIn
30 | import com.varabyte.kobweb.compose.ui.modifiers.onFocusOut
31 | import com.varabyte.kobweb.compose.ui.modifiers.onKeyDown
32 | import com.varabyte.kobweb.compose.ui.modifiers.padding
33 | import com.varabyte.kobweb.compose.ui.modifiers.transition
34 | import com.varabyte.kobweb.compose.ui.thenIf
35 | import com.varabyte.kobweb.compose.ui.toAttrs
36 | import com.varabyte.kobweb.silk.components.icons.fa.FaMagnifyingGlass
37 | import com.varabyte.kobweb.silk.components.icons.fa.IconSize
38 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint
39 | import org.jetbrains.compose.web.attributes.InputType
40 | import org.jetbrains.compose.web.css.LineStyle
41 | import org.jetbrains.compose.web.css.ms
42 | import org.jetbrains.compose.web.css.px
43 | import org.jetbrains.compose.web.dom.Input
44 |
45 | @Composable
46 | fun SearchBar(
47 | breakpoint: Breakpoint,
48 | modifier: Modifier = Modifier,
49 | fullWidth: Boolean = true,
50 | darkTheme: Boolean = false,
51 | onEnterClick: () -> Unit,
52 | onSearchIconClick: (Boolean) -> Unit
53 | ) {
54 | var focused by remember { mutableStateOf(false) }
55 |
56 | LaunchedEffect(breakpoint) {
57 | if (breakpoint >= Breakpoint.SM) onSearchIconClick(false)
58 | }
59 |
60 | if (breakpoint >= Breakpoint.SM || fullWidth) {
61 | Row(
62 | modifier = modifier
63 | .thenIf(
64 | condition = fullWidth,
65 | other = Modifier.fillMaxWidth()
66 | )
67 | .padding(left = 20.px)
68 | .height(54.px)
69 | .backgroundColor(if (darkTheme) JsTheme.Tertiary.rgb else JsTheme.LightGray.rgb)
70 | .borderRadius(r = 100.px)
71 | .border(
72 | width = 2.px,
73 | style = LineStyle.Solid,
74 | color = if (focused && !darkTheme) JsTheme.Primary.rgb
75 | else if (focused && darkTheme) JsTheme.Primary.rgb
76 | else if (!focused && !darkTheme) JsTheme.LightGray.rgb
77 | else if (!focused && darkTheme) JsTheme.Secondary.rgb
78 | else JsTheme.LightGray.rgb
79 | )
80 | .transition(CSSTransition(property = "border", duration = 200.ms)),
81 | verticalAlignment = Alignment.CenterVertically
82 | ) {
83 | FaMagnifyingGlass(
84 | modifier = Modifier
85 | .margin(right = 14.px)
86 | .color(if (focused) JsTheme.Primary.rgb else JsTheme.DarkGray.rgb)
87 | .transition(CSSTransition(property = "color", duration = 200.ms)),
88 | size = IconSize.SM
89 | )
90 | Input(
91 | type = InputType.Text,
92 | attrs = Modifier
93 | .id(Id.adminSearchBar)
94 | .fillMaxSize()
95 | .color(if (darkTheme) Colors.White else Colors.Black)
96 | .backgroundColor(Colors.Transparent)
97 | .noBorder()
98 | .onFocusIn { focused = true }
99 | .onFocusOut { focused = false }
100 | .onKeyDown {
101 | if (it.key == "Enter") {
102 | onEnterClick()
103 | }
104 | }
105 | .toAttrs {
106 | attr("placeholder", "Search...")
107 | }
108 | )
109 | }
110 | } else {
111 | FaMagnifyingGlass(
112 | modifier = Modifier
113 | .margin(right = 14.px)
114 | .color(JsTheme.Primary.rgb)
115 | .cursor(Cursor.Pointer)
116 | .onClick { onSearchIconClick(true) },
117 | size = IconSize.SM
118 | )
119 | }
120 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/models/ControlStyle.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.models
2 |
3 | sealed class ControlStyle(val style: String) {
4 | data class Bold(val selectedText: String?) : ControlStyle(
5 | style = "$selectedText"
6 | )
7 |
8 | data class Italic(val selectedText: String?) : ControlStyle(
9 | style = "$selectedText"
10 | )
11 |
12 | data class Link(
13 | val selectedText: String?,
14 | val href: String,
15 | val title: String
16 | ) : ControlStyle(
17 | style = "$selectedText"
18 | )
19 |
20 | data class Title(val selectedText: String?) : ControlStyle(
21 | style = "$selectedText
"
22 | )
23 |
24 | data class Subtitle(val selectedText: String?) : ControlStyle(
25 | style = "$selectedText
"
26 | )
27 |
28 | data class Quote(val selectedText: String?) : ControlStyle(
29 | style = "❞ $selectedText
"
30 | )
31 |
32 | data class Code(val selectedText: String?) : ControlStyle(
33 | style = ""
34 | )
35 |
36 | data class Image(
37 | val selectedText: String?,
38 | val imageUrl: String,
39 | val alt: String
40 | ) : ControlStyle(
41 | style = "
$selectedText"
42 | )
43 |
44 | data class Break(val selectedText: String?) : ControlStyle(
45 | style = "$selectedText
"
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/models/EditorControl.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.models
2 |
3 | import com.example.blogmultiplatform.util.Res
4 |
5 | enum class EditorControl(
6 | val icon: String,
7 | ) {
8 | Bold(icon = Res.Icon.bold),
9 | Italic(icon = Res.Icon.italic),
10 | Link(icon = Res.Icon.link),
11 | Title(icon = Res.Icon.title),
12 | Subtitle(icon = Res.Icon.subtitle),
13 | Quote(icon = Res.Icon.quote),
14 | Code(icon = Res.Icon.code),
15 | Image(icon = Res.Icon.image)
16 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/models/RandomJoke.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.models
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class RandomJoke(
7 | val id: Int,
8 | val joke: String
9 | )
10 |
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/models/User.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.models
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | actual data class User(
8 | @SerialName(value = "_id")
9 | actual val _id: String = "",
10 | actual val username: String = "",
11 | actual val password: String = ""
12 | )
13 |
14 | @Serializable
15 | actual data class UserWithoutPassword(
16 | @SerialName(value = "_id")
17 | actual val _id: String = "",
18 | actual val username: String = ""
19 | )
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/navigation/Screen.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.navigation
2 |
3 | import com.example.shared.Category
4 | import com.example.blogmultiplatform.models.Constants.CATEGORY_PARAM
5 | import com.example.blogmultiplatform.models.Constants.POST_ID_PARAM
6 | import com.example.blogmultiplatform.models.Constants.QUERY_PARAM
7 | import com.example.blogmultiplatform.models.Constants.UPDATED_PARAM
8 |
9 | sealed class Screen(val route: String) {
10 | object AdminHome : Screen(route = "/admin/")
11 | object AdminLogin : Screen(route = "/admin/login")
12 | object AdminCreate : Screen(route = "/admin/create") {
13 | fun passPostId(id: String) = "/admin/create?${POST_ID_PARAM}=$id"
14 | }
15 |
16 | object AdminMyPosts : Screen(route = "/admin/myposts") {
17 | fun searchByTitle(query: String) = "/admin/myposts?${QUERY_PARAM}=$query"
18 | }
19 |
20 | object AdminSuccess : Screen(route = "/admin/success") {
21 | fun postUpdated() = "/admin/success?${UPDATED_PARAM}=true"
22 | }
23 |
24 | object HomePage : Screen(route = "/")
25 | object SearchPage : Screen(route = "/search/query") {
26 | fun searchByCategory(category: Category) =
27 | "/search/query?${CATEGORY_PARAM}=${category.name}"
28 |
29 | fun searchByTitle(query: String) = "/search/query?${QUERY_PARAM}=$query"
30 | }
31 |
32 | object PostPage : Screen(route = "/posts/post") {
33 | fun getPost(id: String) = "/posts/post?${POST_ID_PARAM}=$id"
34 | }
35 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/pages/Index.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.pages
2 |
3 | import androidx.compose.runtime.*
4 | import com.example.blogmultiplatform.components.CategoryNavigationItems
5 | import com.example.blogmultiplatform.components.OverflowSidePanel
6 | import com.example.blogmultiplatform.models.ApiListResponse
7 | import com.example.blogmultiplatform.models.Constants.POSTS_PER_PAGE
8 | import com.example.blogmultiplatform.models.PostWithoutDetails
9 | import com.example.blogmultiplatform.navigation.Screen
10 | import com.example.blogmultiplatform.sections.FooterSection
11 | import com.example.blogmultiplatform.sections.HeaderSection
12 | import com.example.blogmultiplatform.sections.MainSection
13 | import com.example.blogmultiplatform.sections.NewsletterSection
14 | import com.example.blogmultiplatform.sections.PostsSection
15 | import com.example.blogmultiplatform.sections.SponsoredPostsSection
16 | import com.example.blogmultiplatform.util.fetchLatestPosts
17 | import com.example.blogmultiplatform.util.fetchMainPosts
18 | import com.example.blogmultiplatform.util.fetchPopularPosts
19 | import com.example.blogmultiplatform.util.fetchSponsoredPosts
20 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement
21 | import com.varabyte.kobweb.compose.foundation.layout.Column
22 | import com.varabyte.kobweb.compose.ui.Alignment
23 | import com.varabyte.kobweb.compose.ui.Modifier
24 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize
25 | import com.varabyte.kobweb.core.Page
26 | import com.varabyte.kobweb.core.rememberPageContext
27 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint
28 | import kotlinx.coroutines.launch
29 |
30 | @Page
31 | @Composable
32 | fun HomePage() {
33 | val context = rememberPageContext()
34 | val scope = rememberCoroutineScope()
35 | val breakpoint = rememberBreakpoint()
36 | var overflowOpened by remember { mutableStateOf(false) }
37 | var mainPosts by remember { mutableStateOf(ApiListResponse.Idle) }
38 | val latestPosts = remember { mutableStateListOf() }
39 | val sponsoredPosts = remember { mutableStateListOf() }
40 | val popularPosts = remember { mutableStateListOf() }
41 | var latestPostsToSkip by remember { mutableStateOf(0) }
42 | var popularPostsToSkip by remember { mutableStateOf(0) }
43 | var showMoreLatest by remember { mutableStateOf(false) }
44 | var showMorePopular by remember { mutableStateOf(false) }
45 |
46 | LaunchedEffect(Unit) {
47 | fetchMainPosts(
48 | onSuccess = { mainPosts = it },
49 | onError = {}
50 | )
51 | fetchLatestPosts(
52 | skip = latestPostsToSkip,
53 | onSuccess = { response ->
54 | if (response is ApiListResponse.Success) {
55 | latestPosts.addAll(response.data)
56 | latestPostsToSkip += POSTS_PER_PAGE
57 | if (response.data.size >= POSTS_PER_PAGE) showMoreLatest = true
58 | }
59 | },
60 | onError = {}
61 | )
62 | fetchSponsoredPosts(
63 | onSuccess = { response ->
64 | if (response is ApiListResponse.Success) {
65 | sponsoredPosts.addAll(response.data)
66 | }
67 | },
68 | onError = {}
69 | )
70 | fetchPopularPosts(
71 | skip = popularPostsToSkip,
72 | onSuccess = { response ->
73 | if (response is ApiListResponse.Success) {
74 | popularPosts.addAll(response.data)
75 | popularPostsToSkip += POSTS_PER_PAGE
76 | if (response.data.size >= POSTS_PER_PAGE) showMorePopular = true
77 | }
78 | },
79 | onError = {}
80 | )
81 | }
82 |
83 | Column(
84 | modifier = Modifier.fillMaxSize(),
85 | verticalArrangement = Arrangement.Top,
86 | horizontalAlignment = Alignment.CenterHorizontally
87 | ) {
88 | if (overflowOpened) {
89 | OverflowSidePanel(
90 | onMenuClose = { overflowOpened = false },
91 | content = { CategoryNavigationItems(vertical = true) }
92 | )
93 | }
94 | HeaderSection(
95 | breakpoint = breakpoint,
96 | onMenuOpen = { overflowOpened = true }
97 | )
98 | MainSection(
99 | breakpoint = breakpoint,
100 | posts = mainPosts,
101 | onClick = { context.router.navigateTo(Screen.PostPage.getPost(id = it)) }
102 | )
103 | PostsSection(
104 | breakpoint = breakpoint,
105 | posts = latestPosts,
106 | title = "Latest Posts",
107 | showMoreVisibility = showMoreLatest,
108 | onShowMore = {
109 | scope.launch {
110 | fetchLatestPosts(
111 | skip = latestPostsToSkip,
112 | onSuccess = { response ->
113 | if (response is ApiListResponse.Success) {
114 | if (response.data.isNotEmpty()) {
115 | if (response.data.size < POSTS_PER_PAGE) {
116 | showMoreLatest = false
117 | }
118 | latestPosts.addAll(response.data)
119 | latestPostsToSkip += POSTS_PER_PAGE
120 | } else {
121 | showMoreLatest = false
122 | }
123 | }
124 | },
125 | onError = {}
126 | )
127 | }
128 | },
129 | onClick = { context.router.navigateTo(Screen.PostPage.getPost(id = it)) }
130 | )
131 | SponsoredPostsSection(
132 | breakpoint = breakpoint,
133 | posts = sponsoredPosts,
134 | onClick = { context.router.navigateTo(Screen.PostPage.getPost(id = it)) }
135 | )
136 | PostsSection(
137 | breakpoint = breakpoint,
138 | posts = popularPosts,
139 | title = "Popular Posts",
140 | showMoreVisibility = showMorePopular,
141 | onShowMore = {
142 | scope.launch {
143 | fetchPopularPosts(
144 | skip = popularPostsToSkip,
145 | onSuccess = { response ->
146 | if (response is ApiListResponse.Success) {
147 | if (response.data.isNotEmpty()) {
148 | if (response.data.size < POSTS_PER_PAGE) {
149 | showMorePopular = false
150 | }
151 | popularPosts.addAll(response.data)
152 | popularPostsToSkip += POSTS_PER_PAGE
153 | } else {
154 | showMorePopular = false
155 | }
156 | }
157 | },
158 | onError = {}
159 | )
160 | }
161 | },
162 | onClick = { context.router.navigateTo(Screen.PostPage.getPost(id = it)) }
163 | )
164 | NewsletterSection(breakpoint = breakpoint)
165 | FooterSection()
166 | }
167 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/pages/admin/Index.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.pages.admin
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.runtime.setValue
9 | import com.example.blogmultiplatform.components.AdminPageLayout
10 | import com.example.blogmultiplatform.components.LoadingIndicator
11 | import com.example.blogmultiplatform.models.RandomJoke
12 | import com.example.shared.JsTheme
13 | import com.example.blogmultiplatform.navigation.Screen
14 | import com.example.blogmultiplatform.util.Constants.FONT_FAMILY
15 | import com.example.blogmultiplatform.util.Constants.PAGE_WIDTH
16 | import com.example.blogmultiplatform.util.Constants.SIDE_PANEL_WIDTH
17 | import com.example.blogmultiplatform.util.Res
18 | import com.example.blogmultiplatform.util.fetchRandomJoke
19 | import com.example.blogmultiplatform.util.isUserLoggedIn
20 | import com.varabyte.kobweb.compose.css.Cursor
21 | import com.varabyte.kobweb.compose.css.FontWeight
22 | import com.varabyte.kobweb.compose.css.TextAlign
23 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement
24 | import com.varabyte.kobweb.compose.foundation.layout.Box
25 | import com.varabyte.kobweb.compose.foundation.layout.Column
26 | import com.varabyte.kobweb.compose.ui.Alignment
27 | import com.varabyte.kobweb.compose.ui.Modifier
28 | import com.varabyte.kobweb.compose.ui.graphics.Colors
29 | import com.varabyte.kobweb.compose.ui.modifiers.backgroundColor
30 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius
31 | import com.varabyte.kobweb.compose.ui.modifiers.color
32 | import com.varabyte.kobweb.compose.ui.modifiers.cursor
33 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize
34 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth
35 | import com.varabyte.kobweb.compose.ui.modifiers.fontFamily
36 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize
37 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight
38 | import com.varabyte.kobweb.compose.ui.modifiers.height
39 | import com.varabyte.kobweb.compose.ui.modifiers.margin
40 | import com.varabyte.kobweb.compose.ui.modifiers.maxWidth
41 | import com.varabyte.kobweb.compose.ui.modifiers.onClick
42 | import com.varabyte.kobweb.compose.ui.modifiers.padding
43 | import com.varabyte.kobweb.compose.ui.modifiers.position
44 | import com.varabyte.kobweb.compose.ui.modifiers.size
45 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign
46 | import com.varabyte.kobweb.compose.ui.styleModifier
47 | import com.varabyte.kobweb.core.Page
48 | import com.varabyte.kobweb.core.rememberPageContext
49 | import com.varabyte.kobweb.silk.components.graphics.Image
50 | import com.varabyte.kobweb.silk.components.icons.fa.FaPlus
51 | import com.varabyte.kobweb.silk.components.icons.fa.IconSize
52 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint
53 | import com.varabyte.kobweb.silk.components.text.SpanText
54 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint
55 | import org.jetbrains.compose.web.css.Position
56 | import org.jetbrains.compose.web.css.percent
57 | import org.jetbrains.compose.web.css.px
58 | import org.jetbrains.compose.web.css.vh
59 |
60 | @Page
61 | @Composable
62 | fun HomePage() {
63 | isUserLoggedIn {
64 | HomeScreen()
65 | }
66 | }
67 |
68 | @Composable
69 | fun HomeScreen() {
70 | var randomJoke: RandomJoke? by remember { mutableStateOf(null) }
71 |
72 | LaunchedEffect(Unit) {
73 | fetchRandomJoke { randomJoke = it }
74 | }
75 |
76 | AdminPageLayout {
77 | HomeContent(randomJoke = randomJoke)
78 | AddButton()
79 | }
80 | }
81 |
82 | @Composable
83 | fun HomeContent(randomJoke: RandomJoke?) {
84 | val breakpoint = rememberBreakpoint()
85 | Box(
86 | modifier = Modifier
87 | .fillMaxSize()
88 | .padding(left = if (breakpoint > Breakpoint.MD) SIDE_PANEL_WIDTH.px else 0.px),
89 | contentAlignment = Alignment.Center
90 | ) {
91 | if (randomJoke != null) {
92 | Column(
93 | modifier = Modifier
94 | .fillMaxSize()
95 | .padding(topBottom = 50.px),
96 | verticalArrangement = Arrangement.Center,
97 | horizontalAlignment = Alignment.CenterHorizontally
98 | ) {
99 | if (randomJoke.id != -1) {
100 | Image(
101 | modifier = Modifier
102 | .size(150.px)
103 | .margin(bottom = 50.px),
104 | src = Res.Image.laugh,
105 | alt = "Laugh Image"
106 | )
107 | }
108 | if (randomJoke.joke.contains("Q:")) {
109 | SpanText(
110 | modifier = Modifier
111 | .margin(bottom = 14.px)
112 | .fillMaxWidth(40.percent)
113 | .textAlign(TextAlign.Center)
114 | .color(JsTheme.Secondary.rgb)
115 | .fontSize(28.px)
116 | .fontFamily(FONT_FAMILY)
117 | .fontWeight(FontWeight.Bold),
118 | text = randomJoke.joke.split(":")[1].dropLast(1)
119 | )
120 | SpanText(
121 | modifier = Modifier
122 | .fillMaxWidth(40.percent)
123 | .textAlign(TextAlign.Center)
124 | .color(JsTheme.HalfBlack.rgb)
125 | .fontSize(20.px)
126 | .fontFamily(FONT_FAMILY)
127 | .fontWeight(FontWeight.Normal),
128 | text = randomJoke.joke.split(":").last()
129 | )
130 | } else {
131 | SpanText(
132 | modifier = Modifier
133 | .margin(bottom = 14.px)
134 | .fillMaxWidth(40.percent)
135 | .textAlign(TextAlign.Center)
136 | .color(JsTheme.Secondary.rgb)
137 | .fontFamily(FONT_FAMILY)
138 | .fontSize(28.px)
139 | .fontWeight(FontWeight.Bold),
140 | text = randomJoke.joke
141 | )
142 | }
143 | }
144 | } else {
145 | LoadingIndicator()
146 | }
147 | }
148 | }
149 |
150 | @Composable
151 | fun AddButton() {
152 | val breakpoint = rememberBreakpoint()
153 | val context = rememberPageContext()
154 | Box(
155 | modifier = Modifier
156 | .height(100.vh)
157 | .fillMaxWidth()
158 | .maxWidth(PAGE_WIDTH.px)
159 | .position(Position.Fixed)
160 | .styleModifier {
161 | property("pointer-events", "none")
162 | },
163 | contentAlignment = Alignment.BottomEnd
164 | ) {
165 | Box(
166 | modifier = Modifier
167 | .margin(
168 | right = if (breakpoint > Breakpoint.MD) 40.px else 20.px,
169 | bottom = if (breakpoint > Breakpoint.MD) 40.px else 20.px
170 | )
171 | .backgroundColor(JsTheme.Primary.rgb)
172 | .size(if (breakpoint > Breakpoint.MD) 80.px else 50.px)
173 | .borderRadius(r = 14.px)
174 | .cursor(Cursor.Pointer)
175 | .onClick {
176 | context.router.navigateTo(Screen.AdminCreate.route)
177 | }
178 | .styleModifier {
179 | property("pointer-events", "auto")
180 | },
181 | contentAlignment = Alignment.Center
182 | ) {
183 | FaPlus(
184 | modifier = Modifier.color(Colors.White),
185 | size = IconSize.LG
186 | )
187 | }
188 | }
189 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/pages/admin/Success.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.pages.admin
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import com.example.blogmultiplatform.models.Constants.UPDATED_PARAM
6 | import com.example.shared.JsTheme
7 | import com.example.blogmultiplatform.navigation.Screen
8 | import com.example.blogmultiplatform.util.Constants.FONT_FAMILY
9 | import com.example.blogmultiplatform.util.Res
10 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement
11 | import com.varabyte.kobweb.compose.foundation.layout.Column
12 | import com.varabyte.kobweb.compose.ui.Alignment
13 | import com.varabyte.kobweb.compose.ui.Modifier
14 | import com.varabyte.kobweb.compose.ui.modifiers.color
15 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize
16 | import com.varabyte.kobweb.compose.ui.modifiers.fontFamily
17 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize
18 | import com.varabyte.kobweb.compose.ui.modifiers.margin
19 | import com.varabyte.kobweb.core.Page
20 | import com.varabyte.kobweb.core.rememberPageContext
21 | import com.varabyte.kobweb.silk.components.graphics.Image
22 | import com.varabyte.kobweb.silk.components.text.SpanText
23 | import kotlinx.coroutines.delay
24 | import org.jetbrains.compose.web.css.px
25 |
26 | @Page
27 | @Composable
28 | fun SuccessPage() {
29 | val context = rememberPageContext()
30 | val postUpdated = context.route.params.containsKey(UPDATED_PARAM)
31 | LaunchedEffect(Unit) {
32 | delay(5000)
33 | context.router.navigateTo(Screen.AdminCreate.route)
34 | }
35 |
36 | Column(
37 | modifier = Modifier.fillMaxSize(),
38 | verticalArrangement = Arrangement.Center,
39 | horizontalAlignment = Alignment.CenterHorizontally
40 | ) {
41 | Image(
42 | modifier = Modifier.margin(bottom = 24.px),
43 | src = Res.Icon.checkmark,
44 | alt = "Checkmark Icon"
45 | )
46 | SpanText(
47 | modifier = Modifier
48 | .fontFamily(FONT_FAMILY)
49 | .fontSize(24.px),
50 | text = if(postUpdated) "Post Successfully Updated!" else "Post Successfully Created!"
51 | )
52 | SpanText(
53 | modifier = Modifier
54 | .color(JsTheme.HalfBlack.rgb)
55 | .fontFamily(FONT_FAMILY)
56 | .fontSize(18.px),
57 | text = "Redirecting you back..."
58 | )
59 | }
60 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/sections/FooterSection.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.sections
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.example.shared.JsTheme
5 | import com.example.blogmultiplatform.util.Constants.FONT_FAMILY
6 | import com.varabyte.kobweb.compose.foundation.layout.Box
7 | import com.varabyte.kobweb.compose.foundation.layout.Row
8 | import com.varabyte.kobweb.compose.ui.Alignment
9 | import com.varabyte.kobweb.compose.ui.Modifier
10 | import com.varabyte.kobweb.compose.ui.graphics.Colors
11 | import com.varabyte.kobweb.compose.ui.modifiers.backgroundColor
12 | import com.varabyte.kobweb.compose.ui.modifiers.color
13 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth
14 | import com.varabyte.kobweb.compose.ui.modifiers.fontFamily
15 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize
16 | import com.varabyte.kobweb.compose.ui.modifiers.padding
17 | import com.varabyte.kobweb.silk.components.text.SpanText
18 | import org.jetbrains.compose.web.css.px
19 |
20 | @Composable
21 | fun FooterSection() {
22 | Box(
23 | modifier = Modifier
24 | .fillMaxWidth()
25 | .padding(topBottom = 50.px)
26 | .backgroundColor(JsTheme.Secondary.rgb),
27 | contentAlignment = Alignment.Center
28 | ) {
29 | Row {
30 | SpanText(
31 | modifier = Modifier
32 | .fontFamily(FONT_FAMILY)
33 | .fontSize(14.px)
34 | .color(Colors.White),
35 | text = "Copyright © 2023 • "
36 | )
37 | SpanText(
38 | modifier = Modifier
39 | .fontFamily(FONT_FAMILY)
40 | .fontSize(14.px)
41 | .color(JsTheme.Primary.rgb),
42 | text = "Stevdza-San"
43 | )
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/sections/HeaderSection.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.sections
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.runtime.setValue
8 | import com.example.blogmultiplatform.components.CategoryNavigationItems
9 | import com.example.blogmultiplatform.components.SearchBar
10 | import com.example.shared.Category
11 | import com.example.shared.JsTheme
12 | import com.example.blogmultiplatform.navigation.Screen
13 | import com.example.blogmultiplatform.util.Constants.HEADER_HEIGHT
14 | import com.example.blogmultiplatform.util.Constants.PAGE_WIDTH
15 | import com.example.blogmultiplatform.util.Id
16 | import com.example.blogmultiplatform.util.Res
17 | import com.varabyte.kobweb.compose.css.Cursor
18 | import com.varabyte.kobweb.compose.foundation.layout.Box
19 | import com.varabyte.kobweb.compose.foundation.layout.Row
20 | import com.varabyte.kobweb.compose.foundation.layout.Spacer
21 | import com.varabyte.kobweb.compose.ui.Alignment
22 | import com.varabyte.kobweb.compose.ui.Modifier
23 | import com.varabyte.kobweb.compose.ui.graphics.Colors
24 | import com.varabyte.kobweb.compose.ui.modifiers.backgroundColor
25 | import com.varabyte.kobweb.compose.ui.modifiers.color
26 | import com.varabyte.kobweb.compose.ui.modifiers.cursor
27 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth
28 | import com.varabyte.kobweb.compose.ui.modifiers.height
29 | import com.varabyte.kobweb.compose.ui.modifiers.margin
30 | import com.varabyte.kobweb.compose.ui.modifiers.maxWidth
31 | import com.varabyte.kobweb.compose.ui.modifiers.onClick
32 | import com.varabyte.kobweb.compose.ui.modifiers.width
33 | import com.varabyte.kobweb.core.rememberPageContext
34 | import com.varabyte.kobweb.silk.components.graphics.Image
35 | import com.varabyte.kobweb.silk.components.icons.fa.FaBars
36 | import com.varabyte.kobweb.silk.components.icons.fa.FaXmark
37 | import com.varabyte.kobweb.silk.components.icons.fa.IconSize
38 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint
39 | import kotlinx.browser.document
40 | import org.jetbrains.compose.web.css.percent
41 | import org.jetbrains.compose.web.css.px
42 | import org.w3c.dom.HTMLInputElement
43 |
44 | @Composable
45 | fun HeaderSection(
46 | breakpoint: Breakpoint,
47 | selectedCategory: Category? = null,
48 | logo: String = Res.Image.logoHome,
49 | onMenuOpen: () -> Unit
50 | ) {
51 | Box(
52 | modifier = Modifier
53 | .fillMaxWidth()
54 | .backgroundColor(JsTheme.Secondary.rgb),
55 | contentAlignment = Alignment.Center
56 | ) {
57 | Box(
58 | modifier = Modifier
59 | .fillMaxWidth()
60 | .backgroundColor(JsTheme.Secondary.rgb)
61 | .maxWidth(PAGE_WIDTH.px),
62 | contentAlignment = Alignment.TopCenter
63 | ) {
64 | Header(
65 | breakpoint = breakpoint,
66 | logo = logo,
67 | selectedCategory = selectedCategory,
68 | onMenuOpen = onMenuOpen
69 | )
70 | }
71 | }
72 | }
73 |
74 | @Composable
75 | fun Header(
76 | breakpoint: Breakpoint,
77 | logo: String,
78 | selectedCategory: Category?,
79 | onMenuOpen: () -> Unit
80 | ) {
81 | val context = rememberPageContext()
82 | var fullSearchBarOpened by remember { mutableStateOf(false) }
83 | Row(
84 | modifier = Modifier
85 | .fillMaxWidth(if (breakpoint > Breakpoint.MD) 80.percent else 90.percent)
86 | .height(HEADER_HEIGHT.px),
87 | verticalAlignment = Alignment.CenterVertically
88 | ) {
89 | if (breakpoint <= Breakpoint.MD) {
90 | if (fullSearchBarOpened) {
91 | FaXmark(
92 | modifier = Modifier
93 | .margin(right = 24.px)
94 | .color(Colors.White)
95 | .cursor(Cursor.Pointer)
96 | .onClick { fullSearchBarOpened = false },
97 | size = IconSize.XL
98 | )
99 | }
100 | if (!fullSearchBarOpened) {
101 | FaBars(
102 | modifier = Modifier
103 | .margin(right = 24.px)
104 | .color(Colors.White)
105 | .cursor(Cursor.Pointer)
106 | .onClick { onMenuOpen() },
107 | size = IconSize.XL
108 | )
109 | }
110 | }
111 | if (!fullSearchBarOpened) {
112 | Image(
113 | modifier = Modifier
114 | .margin(right = 50.px)
115 | .width(if (breakpoint >= Breakpoint.SM) 100.px else 70.px)
116 | .cursor(Cursor.Pointer)
117 | .onClick { context.router.navigateTo(Screen.HomePage.route) },
118 | src = logo,
119 | alt = "Logo Image"
120 | )
121 | }
122 | if (breakpoint >= Breakpoint.LG) {
123 | CategoryNavigationItems(selectedCategory = selectedCategory)
124 | }
125 | Spacer()
126 | SearchBar(
127 | breakpoint = breakpoint,
128 | fullWidth = fullSearchBarOpened,
129 | darkTheme = true,
130 | onEnterClick = {
131 | val query = (document.getElementById(Id.adminSearchBar) as HTMLInputElement).value
132 | context.router.navigateTo(Screen.SearchPage.searchByTitle(query = query))
133 | },
134 | onSearchIconClick = { fullSearchBarOpened = it }
135 | )
136 | }
137 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/sections/MainSection.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.sections
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.example.blogmultiplatform.components.PostPreview
5 | import com.example.blogmultiplatform.models.ApiListResponse
6 | import com.example.blogmultiplatform.models.PostWithoutDetails
7 | import com.example.shared.JsTheme
8 | import com.example.blogmultiplatform.util.Constants.PAGE_WIDTH
9 | import com.varabyte.kobweb.compose.foundation.layout.Box
10 | import com.varabyte.kobweb.compose.foundation.layout.Column
11 | import com.varabyte.kobweb.compose.foundation.layout.Row
12 | import com.varabyte.kobweb.compose.ui.Alignment
13 | import com.varabyte.kobweb.compose.ui.Modifier
14 | import com.varabyte.kobweb.compose.ui.modifiers.backgroundColor
15 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth
16 | import com.varabyte.kobweb.compose.ui.modifiers.margin
17 | import com.varabyte.kobweb.compose.ui.modifiers.maxWidth
18 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint
19 | import org.jetbrains.compose.web.css.percent
20 | import org.jetbrains.compose.web.css.px
21 |
22 | @Composable
23 | fun MainSection(
24 | breakpoint: Breakpoint,
25 | posts: ApiListResponse,
26 | onClick: (String) -> Unit
27 | ) {
28 | Box(
29 | modifier = Modifier
30 | .fillMaxWidth()
31 | .backgroundColor(JsTheme.Secondary.rgb),
32 | contentAlignment = Alignment.Center
33 | ) {
34 | Box(
35 | modifier = Modifier
36 | .fillMaxWidth()
37 | .maxWidth(PAGE_WIDTH.px),
38 | contentAlignment = Alignment.Center
39 | ) {
40 | when (posts) {
41 | is ApiListResponse.Idle -> {}
42 | is ApiListResponse.Success -> {
43 | MainPosts(
44 | breakpoint = breakpoint,
45 | posts = posts.data,
46 | onClick = onClick
47 | )
48 | }
49 |
50 | is ApiListResponse.Error -> {}
51 | }
52 | }
53 | }
54 | }
55 |
56 | @Composable
57 | fun MainPosts(
58 | breakpoint: Breakpoint,
59 | posts: List,
60 | onClick: (String) -> Unit
61 | ) {
62 | Row(
63 | modifier = Modifier
64 | .fillMaxWidth(
65 | if (breakpoint > Breakpoint.MD) 80.percent
66 | else 90.percent
67 | )
68 | .margin(topBottom = 50.px)
69 | ) {
70 | if (breakpoint == Breakpoint.XL) {
71 | PostPreview(
72 | post = posts.first(),
73 | darkTheme = true,
74 | thumbnailHeight = 640.px,
75 | onClick = { onClick(posts.first()._id) }
76 | )
77 | Column(
78 | modifier = Modifier
79 | .fillMaxWidth(80.percent)
80 | .margin(left = 20.px)
81 | ) {
82 | posts.drop(1).forEach { postWithoutDetails ->
83 | PostPreview(
84 | modifier = Modifier.margin(bottom = 20.px),
85 | post = postWithoutDetails,
86 | darkTheme = true,
87 | vertical = false,
88 | thumbnailHeight = 200.px,
89 | titleMaxLines = 1,
90 | onClick = { onClick(postWithoutDetails._id) }
91 | )
92 | }
93 | }
94 | } else if (breakpoint >= Breakpoint.LG) {
95 | Box(modifier = Modifier.margin(right = 10.px)) {
96 | PostPreview(
97 | post = posts.first(),
98 | darkTheme = true,
99 | onClick = { onClick(posts.first()._id) }
100 | )
101 | }
102 | Box(modifier = Modifier.margin(left = 10.px)) {
103 | PostPreview(
104 | post = posts[1],
105 | darkTheme = true,
106 | onClick = { onClick(posts[1]._id) }
107 | )
108 | }
109 | } else {
110 | PostPreview(
111 | post = posts.first(),
112 | darkTheme = true,
113 | thumbnailHeight = 640.px,
114 | onClick = { onClick(posts.first()._id) }
115 | )
116 | }
117 | }
118 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/sections/PostsSection.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.sections
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.example.blogmultiplatform.components.PostsView
5 | import com.example.blogmultiplatform.models.PostWithoutDetails
6 | import com.example.blogmultiplatform.util.Constants.PAGE_WIDTH
7 | import com.varabyte.kobweb.compose.foundation.layout.Box
8 | import com.varabyte.kobweb.compose.ui.Alignment
9 | import com.varabyte.kobweb.compose.ui.Modifier
10 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize
11 | import com.varabyte.kobweb.compose.ui.modifiers.margin
12 | import com.varabyte.kobweb.compose.ui.modifiers.maxWidth
13 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint
14 | import org.jetbrains.compose.web.css.px
15 |
16 | @Composable
17 | fun PostsSection(
18 | breakpoint: Breakpoint,
19 | posts: List,
20 | title: String? = null,
21 | showMoreVisibility: Boolean,
22 | onShowMore: () -> Unit,
23 | onClick: (String) -> Unit
24 | ) {
25 | Box(
26 | modifier = Modifier
27 | .fillMaxSize()
28 | .margin(topBottom = 50.px)
29 | .maxWidth(PAGE_WIDTH.px),
30 | contentAlignment = Alignment.TopCenter
31 | ) {
32 | PostsView(
33 | breakpoint = breakpoint,
34 | posts = posts,
35 | title = title,
36 | showMoreVisibility = showMoreVisibility,
37 | onShowMore = onShowMore,
38 | onClick = onClick
39 | )
40 | }
41 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/sections/SponsoredPostsSection.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.sections
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.example.blogmultiplatform.components.PostPreview
5 | import com.example.blogmultiplatform.models.PostWithoutDetails
6 | import com.example.shared.JsTheme
7 | import com.example.blogmultiplatform.util.Constants.FONT_FAMILY
8 | import com.example.blogmultiplatform.util.Constants.PAGE_WIDTH
9 | import com.varabyte.kobweb.compose.css.FontWeight
10 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement
11 | import com.varabyte.kobweb.compose.foundation.layout.Box
12 | import com.varabyte.kobweb.compose.foundation.layout.Column
13 | import com.varabyte.kobweb.compose.foundation.layout.Row
14 | import com.varabyte.kobweb.compose.ui.Alignment
15 | import com.varabyte.kobweb.compose.ui.Modifier
16 | import com.varabyte.kobweb.compose.ui.modifiers.backgroundColor
17 | import com.varabyte.kobweb.compose.ui.modifiers.color
18 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth
19 | import com.varabyte.kobweb.compose.ui.modifiers.fontFamily
20 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize
21 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight
22 | import com.varabyte.kobweb.compose.ui.modifiers.margin
23 | import com.varabyte.kobweb.compose.ui.modifiers.maxWidth
24 | import com.varabyte.kobweb.silk.components.icons.fa.FaTag
25 | import com.varabyte.kobweb.silk.components.icons.fa.IconSize
26 | import com.varabyte.kobweb.silk.components.layout.SimpleGrid
27 | import com.varabyte.kobweb.silk.components.layout.numColumns
28 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint
29 | import com.varabyte.kobweb.silk.components.text.SpanText
30 | import org.jetbrains.compose.web.css.percent
31 | import org.jetbrains.compose.web.css.px
32 |
33 | @Composable
34 | fun SponsoredPostsSection(
35 | breakpoint: Breakpoint,
36 | posts: List,
37 | onClick: (String) -> Unit
38 | ) {
39 | Box(
40 | modifier = Modifier
41 | .fillMaxWidth()
42 | .margin(bottom = 100.px)
43 | .backgroundColor(JsTheme.LightGray.rgb),
44 | contentAlignment = Alignment.Center
45 | ) {
46 | Box(
47 | modifier = Modifier
48 | .fillMaxWidth()
49 | .maxWidth(PAGE_WIDTH.px)
50 | .margin(topBottom = 50.px),
51 | contentAlignment = Alignment.TopCenter
52 | ) {
53 | SponsoredPosts(
54 | breakpoint = breakpoint,
55 | posts = posts,
56 | onClick = onClick
57 | )
58 | }
59 | }
60 | }
61 |
62 | @Composable
63 | fun SponsoredPosts(
64 | breakpoint: Breakpoint,
65 | posts: List,
66 | onClick: (String) -> Unit
67 | ) {
68 | Column(
69 | modifier = Modifier.fillMaxWidth(
70 | if (breakpoint > Breakpoint.MD) 80.percent
71 | else 90.percent
72 | ),
73 | verticalArrangement = Arrangement.Center
74 | ) {
75 | Row(
76 | modifier = Modifier.margin(bottom = 30.px),
77 | verticalAlignment = Alignment.CenterVertically
78 | ) {
79 | FaTag(
80 | modifier = Modifier
81 | .margin(right = 10.px)
82 | .color(JsTheme.Sponsored.rgb),
83 | size = IconSize.XL
84 | )
85 | SpanText(
86 | modifier = Modifier
87 | .fontFamily(FONT_FAMILY)
88 | .fontSize(18.px)
89 | .fontWeight(FontWeight.Medium)
90 | .color(JsTheme.Sponsored.rgb),
91 | text = "Sponsored Posts"
92 | )
93 | }
94 | SimpleGrid(
95 | modifier = Modifier.fillMaxWidth(),
96 | numColumns = numColumns(base = 1, xl = 2)
97 | ) {
98 | posts.forEach { post ->
99 | PostPreview(
100 | modifier = Modifier.margin(right = 50.px),
101 | post = post,
102 | vertical = breakpoint < Breakpoint.MD,
103 | titleMaxLines = 1,
104 | titleColor = JsTheme.Sponsored.rgb,
105 | thumbnailHeight = if (breakpoint >= Breakpoint.MD) 200.px else 300.px,
106 | onClick = onClick
107 | )
108 | }
109 | }
110 | }
111 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/styles/CreateStyle.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.styles
2 |
3 | import com.example.shared.JsTheme
4 | import com.varabyte.kobweb.compose.css.CSSTransition
5 | import com.varabyte.kobweb.compose.ui.Modifier
6 | import com.varabyte.kobweb.compose.ui.graphics.Colors
7 | import com.varabyte.kobweb.compose.ui.modifiers.backgroundColor
8 | import com.varabyte.kobweb.compose.ui.modifiers.transition
9 | import com.varabyte.kobweb.silk.components.style.ComponentStyle
10 | import com.varabyte.kobweb.silk.components.style.hover
11 | import org.jetbrains.compose.web.css.ms
12 |
13 | val EditorKeyStyle by ComponentStyle {
14 | base {
15 | Modifier
16 | .backgroundColor(Colors.Transparent)
17 | .transition(CSSTransition(property = "background", duration = 300.ms))
18 | }
19 | hover {
20 | Modifier.backgroundColor(JsTheme.Primary.rgb)
21 | }
22 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/styles/HeaderStyle.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.styles
2 |
3 | import com.example.shared.JsTheme
4 | import com.varabyte.kobweb.compose.css.CSSTransition
5 | import com.varabyte.kobweb.compose.ui.Modifier
6 | import com.varabyte.kobweb.compose.ui.graphics.Colors
7 | import com.varabyte.kobweb.compose.ui.modifiers.color
8 | import com.varabyte.kobweb.compose.ui.modifiers.transition
9 | import com.varabyte.kobweb.silk.components.style.ComponentStyle
10 | import com.varabyte.kobweb.silk.components.style.anyLink
11 | import com.varabyte.kobweb.silk.components.style.hover
12 | import org.jetbrains.compose.web.css.ms
13 |
14 | val CategoryItemStyle by ComponentStyle {
15 | base {
16 | Modifier
17 | .color(Colors.White)
18 | .transition(CSSTransition(property = "color", duration = 200.ms))
19 | }
20 | anyLink {
21 | Modifier.color(Colors.White)
22 | }
23 | hover {
24 | Modifier.color(JsTheme.Primary.rgb)
25 | }
26 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/styles/LoginStyle.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.styles
2 |
3 | import com.example.shared.JsTheme
4 | import com.varabyte.kobweb.compose.css.CSSTransition
5 | import com.varabyte.kobweb.compose.ui.Modifier
6 | import com.varabyte.kobweb.compose.ui.graphics.Colors
7 | import com.varabyte.kobweb.compose.ui.modifiers.border
8 | import com.varabyte.kobweb.compose.ui.modifiers.transition
9 | import com.varabyte.kobweb.silk.components.style.ComponentStyle
10 | import com.varabyte.kobweb.silk.components.style.focus
11 | import org.jetbrains.compose.web.css.LineStyle
12 | import org.jetbrains.compose.web.css.ms
13 | import org.jetbrains.compose.web.css.px
14 |
15 | val LoginInputStyle by ComponentStyle {
16 | base {
17 | Modifier
18 | .border(
19 | width = 2.px,
20 | style = LineStyle.Solid,
21 | color = Colors.Transparent
22 | )
23 | .transition(CSSTransition(property = "border", duration = 300.ms))
24 | }
25 | focus {
26 | Modifier.border(
27 | width = 2.px,
28 | style = LineStyle.Solid,
29 | color = JsTheme.Primary.rgb
30 | )
31 | }
32 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/styles/NewsletterStyle.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.styles
2 |
3 | import com.example.shared.JsTheme
4 | import com.varabyte.kobweb.compose.css.CSSTransition
5 | import com.varabyte.kobweb.compose.css.TransitionProperty
6 | import com.varabyte.kobweb.compose.ui.Modifier
7 | import com.varabyte.kobweb.compose.ui.graphics.Colors
8 | import com.varabyte.kobweb.compose.ui.modifiers.border
9 | import com.varabyte.kobweb.compose.ui.modifiers.outline
10 | import com.varabyte.kobweb.compose.ui.modifiers.transition
11 | import com.varabyte.kobweb.silk.components.style.ComponentStyle
12 | import com.varabyte.kobweb.silk.components.style.focus
13 | import org.jetbrains.compose.web.css.LineStyle
14 | import org.jetbrains.compose.web.css.ms
15 | import org.jetbrains.compose.web.css.px
16 |
17 | val NewsletterInputStyle by ComponentStyle {
18 | base {
19 | Modifier
20 | .outline(
21 | width = 1.px,
22 | style = LineStyle.Solid,
23 | color = Colors.Transparent
24 | )
25 | .border(
26 | width = 1.px,
27 | style = LineStyle.Solid,
28 | color = Colors.Transparent
29 | )
30 | .transition(CSSTransition(property = TransitionProperty.All, duration = 300.ms))
31 | }
32 | focus {
33 | Modifier
34 | .outline(
35 | width = 1.px,
36 | style = LineStyle.Solid,
37 | color = JsTheme.Primary.rgb
38 | )
39 | .border(
40 | width = 1.px,
41 | style = LineStyle.Solid,
42 | color = JsTheme.Primary.rgb
43 | )
44 | }
45 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/styles/PostPreviewStyle.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.styles
2 |
3 | import com.varabyte.kobweb.compose.css.CSSTransition
4 | import com.varabyte.kobweb.compose.css.TransitionProperty
5 | import com.varabyte.kobweb.compose.ui.Modifier
6 | import com.varabyte.kobweb.compose.ui.modifiers.boxShadow
7 | import com.varabyte.kobweb.compose.ui.modifiers.scale
8 | import com.varabyte.kobweb.compose.ui.modifiers.transition
9 | import com.varabyte.kobweb.silk.components.style.ComponentStyle
10 | import com.varabyte.kobweb.silk.components.style.hover
11 | import org.jetbrains.compose.web.css.ms
12 | import org.jetbrains.compose.web.css.percent
13 | import org.jetbrains.compose.web.css.px
14 | import org.jetbrains.compose.web.css.rgba
15 |
16 | val PostPreviewStyle by ComponentStyle {
17 | base {
18 | Modifier
19 | .scale(100.percent)
20 | .transition(CSSTransition(property = TransitionProperty.All, duration = 100.ms))
21 | }
22 | hover {
23 | Modifier
24 | .boxShadow(
25 | offsetY = 0.px,
26 | offsetX = 0.px,
27 | blurRadius = 8.px,
28 | spreadRadius = 6.px,
29 | color = rgba(0, 0, 0, 0.06)
30 | )
31 | .scale(102.percent)
32 | }
33 | }
34 |
35 | val MainPostPreviewStyle by ComponentStyle {
36 | base {
37 | Modifier
38 | .scale(100.percent)
39 | .transition(CSSTransition(property = TransitionProperty.All, duration = 100.ms))
40 | }
41 | hover {
42 | Modifier
43 | .boxShadow(
44 | offsetY = 0.px,
45 | offsetX = 0.px,
46 | blurRadius = 8.px,
47 | spreadRadius = 6.px,
48 | color = rgba(0, 162, 255, 0.06)
49 | )
50 | .scale(102.percent)
51 | }
52 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/styles/SidePanelStyle.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.styles
2 |
3 | import com.example.shared.JsTheme
4 | import com.example.blogmultiplatform.util.Id
5 | import com.varabyte.kobweb.compose.css.CSSTransition
6 | import com.varabyte.kobweb.compose.css.TransitionProperty
7 | import com.varabyte.kobweb.compose.ui.Modifier
8 | import com.varabyte.kobweb.compose.ui.modifiers.color
9 | import com.varabyte.kobweb.compose.ui.modifiers.transition
10 | import com.varabyte.kobweb.compose.ui.styleModifier
11 | import com.varabyte.kobweb.silk.components.style.ComponentStyle
12 | import org.jetbrains.compose.web.css.ms
13 |
14 | val NavigationItemStyle by ComponentStyle {
15 | cssRule(" > #${Id.svgParent} > #${Id.vectorIcon}") {
16 | Modifier
17 | .transition(CSSTransition(property = TransitionProperty.All, duration = 300.ms))
18 | .styleModifier {
19 | property("stroke", JsTheme.White.hex)
20 | }
21 | }
22 | cssRule(":hover > #${Id.svgParent} > #${Id.vectorIcon}") {
23 | Modifier
24 | .styleModifier {
25 | property("stroke", JsTheme.Primary.hex)
26 | }
27 | }
28 | cssRule(" > #${Id.navigationText}") {
29 | Modifier
30 | .transition(CSSTransition(property = TransitionProperty.All, duration = 300.ms))
31 | .color(JsTheme.White.rgb)
32 | }
33 | cssRule(":hover > #${Id.navigationText}") {
34 | Modifier.color(JsTheme.Primary.rgb)
35 | }
36 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/util/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.util
2 |
3 | object Constants {
4 | const val FONT_FAMILY = "Roboto"
5 | const val SIDE_PANEL_WIDTH = 250
6 | const val PAGE_WIDTH = 1920
7 | const val HEADER_HEIGHT = 100
8 | const val COLLAPSED_PANEL_HEIGHT = 100
9 | const val HUMOR_API_URL = "https://api.humorapi.com/jokes/random?api-key=f0cac1365ead42e58b2ee94684b45b56&max-length=180"
10 | }
11 |
12 | object Res {
13 | object Image {
14 | const val logo = "/logo.svg"
15 | const val logoHome = "logo.svg"
16 | const val laugh = "/laugh.png"
17 | }
18 | object Icon {
19 | const val bold = "/bold.svg"
20 | const val italic = "/italic.svg"
21 | const val link = "/link.svg"
22 | const val title = "/title.svg"
23 | const val subtitle = "/subtitle.svg"
24 | const val quote = "/quote.svg"
25 | const val code = "/code.svg"
26 | const val image = "/image.svg"
27 | const val checkmark = "/checkmark.svg"
28 | }
29 | object PathIcon {
30 | const val home = "M3 12L5 10M5 10L12 3L19 10M5 10V20C5 20.2652 5.10536 20.5196 5.29289 20.7071C5.48043 20.8946 5.73478 21 6 21H9M19 10L21 12M19 10V20C19 20.2652 18.8946 20.5196 18.7071 20.7071C18.5196 20.8946 18.2652 21 18 21H15M9 21C9.26522 21 9.51957 20.8946 9.70711 20.7071C9.89464 20.5196 10 20.2652 10 20V16C10 15.7348 10.1054 15.4804 10.2929 15.2929C10.4804 15.1054 10.7348 15 11 15H13C13.2652 15 13.5196 15.1054 13.7071 15.2929C13.8946 15.4804 14 15.7348 14 16V20C14 20.2652 14.1054 20.5196 14.2929 20.7071C14.4804 20.8946 14.7348 21 15 21M9 21H15"
31 | const val create = "M12 9.52148V12.5215M12 12.5215V15.5215M12 12.5215H15M12 12.5215H9M21 12.5215C21 13.7034 20.7672 14.8737 20.3149 15.9656C19.8626 17.0576 19.1997 18.0497 18.364 18.8854C17.5282 19.7212 16.5361 20.3841 15.4442 20.8364C14.3522 21.2887 13.1819 21.5215 12 21.5215C10.8181 21.5215 9.64778 21.2887 8.55585 20.8364C7.46392 20.3841 6.47177 19.7212 5.63604 18.8854C4.80031 18.0497 4.13738 17.0576 3.68508 15.9656C3.23279 14.8737 3 13.7034 3 12.5215C3 10.1345 3.94821 7.84535 5.63604 6.15752C7.32387 4.4697 9.61305 3.52148 12 3.52148C14.3869 3.52148 16.6761 4.4697 18.364 6.15752C20.0518 7.84535 21 10.1345 21 12.5215Z"
32 | const val posts = "M9 5H7C6.46957 5 5.96086 5.21071 5.58579 5.58579C5.21071 5.96086 5 6.46957 5 7V19C5 19.5304 5.21071 20.0391 5.58579 20.4142C5.96086 20.7893 6.46957 21 7 21H17C17.5304 21 18.0391 20.7893 18.4142 20.4142C18.7893 20.0391 19 19.5304 19 19V7C19 6.46957 18.7893 5.96086 18.4142 5.58579C18.0391 5.21071 17.5304 5 17 5H15M9 5C9 5.53043 9.21071 6.03914 9.58579 6.41421C9.96086 6.78929 10.4696 7 11 7H13C13.5304 7 14.0391 6.78929 14.4142 6.41421C14.7893 6.03914 15 5.53043 15 5M9 5C9 4.46957 9.21071 3.96086 9.58579 3.58579C9.96086 3.21071 10.4696 3 11 3H13C13.5304 3 14.0391 3.21071 14.4142 3.58579C14.7893 3.96086 15 4.46957 15 5M12 12H15M12 16H15M9 12H9.01M9 16H9.01"
33 | const val logout = "M11 16.5215L7 12.5215M7 12.5215L11 8.52148M7 12.5215H21M16 16.5215V17.5215C16 18.3171 15.6839 19.0802 15.1213 19.6428C14.5587 20.2054 13.7956 20.5215 13 20.5215H6C5.20435 20.5215 4.44129 20.2054 3.87868 19.6428C3.31607 19.0802 3 18.3171 3 17.5215V7.52148C3 6.72583 3.31607 5.96277 3.87868 5.40016C4.44129 4.83755 5.20435 4.52148 6 4.52148H13C13.7956 4.52148 14.5587 4.83755 15.1213 5.40016C15.6839 5.96277 16 6.72583 16 7.52148V8.52148"
34 | }
35 | }
36 |
37 | object Id {
38 | const val usernameInput = "usernameInput"
39 | const val passwordInput = "passwordInput"
40 | const val svgParent = "svgParent"
41 | const val vectorIcon = "vectorIcon"
42 | const val navigationText = "navigationText"
43 | const val editor = "editor"
44 | const val editorPreview = "editorPreview"
45 | const val titleInput = "titleInput"
46 | const val subtitleInput = "subtitleInput"
47 | const val thumbnailInput = "thumbnailInput"
48 | const val linkHrefInput = "linkHrefInput"
49 | const val linkTitleInput = "linkTitleInput"
50 | const val adminSearchBar = "adminSearchBar"
51 | const val emailInput = "emailInput"
52 | const val postContent = "postContent"
53 | }
--------------------------------------------------------------------------------
/site/src/jsMain/kotlin/com/example/blogmultiplatform/util/Functions.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.util
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.setValue
9 | import com.example.blogmultiplatform.models.ControlStyle
10 | import com.example.blogmultiplatform.models.EditorControl
11 | import com.example.blogmultiplatform.navigation.Screen
12 | import com.varabyte.kobweb.compose.ui.Modifier
13 | import com.varabyte.kobweb.compose.ui.graphics.Colors
14 | import com.varabyte.kobweb.compose.ui.modifiers.border
15 | import com.varabyte.kobweb.compose.ui.modifiers.outline
16 | import com.varabyte.kobweb.core.rememberPageContext
17 | import kotlinx.browser.document
18 | import kotlinx.browser.localStorage
19 | import org.jetbrains.compose.web.css.LineStyle
20 | import org.jetbrains.compose.web.css.px
21 | import org.w3c.dom.HTMLTextAreaElement
22 | import org.w3c.dom.get
23 | import org.w3c.dom.set
24 | import kotlin.js.Date
25 |
26 | @Composable
27 | fun isUserLoggedIn(content: @Composable () -> Unit) {
28 | val context = rememberPageContext()
29 | val remembered = remember { localStorage["remember"].toBoolean() }
30 | val userId = remember { localStorage["userId"] }
31 | var userIdExists by remember { mutableStateOf(false) }
32 |
33 | LaunchedEffect(key1 = Unit) {
34 | userIdExists = if (!userId.isNullOrEmpty()) checkUserId(id = userId) else false
35 | if (!remembered || !userIdExists) {
36 | context.router.navigateTo(Screen.AdminLogin.route)
37 | }
38 | }
39 |
40 | if (remembered && userIdExists) {
41 | content()
42 | } else {
43 | println("Loading...")
44 | }
45 | }
46 |
47 | fun logout() {
48 | localStorage["remember"] = "false"
49 | localStorage["userId"] = ""
50 | localStorage["username"] = ""
51 | }
52 |
53 | fun Modifier.noBorder(): Modifier {
54 | return this.border(
55 | width = 0.px,
56 | style = LineStyle.None,
57 | color = Colors.Transparent
58 | ).outline(
59 | width = 0.px,
60 | style = LineStyle.None,
61 | color = Colors.Transparent
62 | )
63 | }
64 |
65 | fun getEditor() = document.getElementById(Id.editor) as HTMLTextAreaElement
66 |
67 | fun getSelectedIntRange(): IntRange? {
68 | val editor = getEditor()
69 | val start = editor.selectionStart
70 | val end = editor.selectionEnd
71 | return if (start != null && end != null) {
72 | IntRange(start, (end - 1))
73 | } else null
74 | }
75 |
76 | fun getSelectedText(): String? {
77 | val range = getSelectedIntRange()
78 | return if (range != null) {
79 | getEditor().value.substring(range)
80 | } else null
81 | }
82 |
83 | fun applyStyle(controlStyle: ControlStyle) {
84 | val selectedText = getSelectedText()
85 | val selectedIntRange = getSelectedIntRange()
86 | if (selectedIntRange != null && selectedText != null) {
87 | getEditor().value = getEditor().value.replaceRange(
88 | range = selectedIntRange,
89 | replacement = controlStyle.style
90 | )
91 | document.getElementById(Id.editorPreview)?.innerHTML = getEditor().value
92 | }
93 | }
94 |
95 | fun applyControlStyle(
96 | editorControl: EditorControl,
97 | onLinkClick: () -> Unit,
98 | onImageClick: () -> Unit
99 | ) {
100 | when (editorControl) {
101 | EditorControl.Bold -> {
102 | applyStyle(
103 | ControlStyle.Bold(
104 | selectedText = getSelectedText()
105 | )
106 | )
107 | }
108 |
109 | EditorControl.Italic -> {
110 | applyStyle(
111 | ControlStyle.Italic(
112 | selectedText = getSelectedText()
113 | )
114 | )
115 | }
116 |
117 | EditorControl.Link -> {
118 | onLinkClick()
119 | }
120 |
121 | EditorControl.Title -> {
122 | applyStyle(
123 | ControlStyle.Title(
124 | selectedText = getSelectedText()
125 | )
126 | )
127 | }
128 |
129 | EditorControl.Subtitle -> {
130 | applyStyle(
131 | ControlStyle.Subtitle(
132 | selectedText = getSelectedText()
133 | )
134 | )
135 | }
136 |
137 | EditorControl.Quote -> {
138 | applyStyle(
139 | ControlStyle.Quote(
140 | selectedText = getSelectedText()
141 | )
142 | )
143 | }
144 |
145 | EditorControl.Code -> {
146 | applyStyle(
147 | ControlStyle.Code(
148 | selectedText = getSelectedText()
149 | )
150 | )
151 | }
152 | EditorControl.Image -> {
153 | onImageClick()
154 | }
155 | }
156 | }
157 |
158 | fun Long.parseDateString() = Date(this).toLocaleDateString()
159 |
160 | fun parseSwitchText(posts: List): String {
161 | return if (posts.size == 1) "1 Post Selected" else "${posts.size} Posts Selected"
162 | }
163 |
164 | fun validateEmail(email: String): Boolean {
165 | val regex = "^[A-Za-z](.*)(@)(.+)(\\.)(.+)"
166 | return regex.toRegex().matches(email)
167 | }
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/bold.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/checkmark.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/code.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/site/src/jsMain/resources/public/favicon.ico
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/github-dark.css:
--------------------------------------------------------------------------------
1 | /*!
2 | Theme: GitHub Dark
3 | Description: Dark theme as seen on github.com
4 | Author: github.com
5 | Maintainer: @Hirse
6 | Updated: 2021-05-15
7 |
8 | Outdated base version: https://github.com/primer/github-syntax-dark
9 | Current colors taken from GitHub's CSS
10 | */
11 |
12 | .hljs {
13 | color: #c9d1d9;
14 | background: #0d1117;
15 | }
16 |
17 | .hljs-doctag,
18 | .hljs-keyword,
19 | .hljs-meta .hljs-keyword,
20 | .hljs-template-tag,
21 | .hljs-template-variable,
22 | .hljs-type,
23 | .hljs-variable.language_ {
24 | /* prettylights-syntax-keyword */
25 | color: #ff7b72;
26 | }
27 |
28 | .hljs-title,
29 | .hljs-title.class_,
30 | .hljs-title.class_.inherited__,
31 | .hljs-title.function_ {
32 | /* prettylights-syntax-entity */
33 | color: #d2a8ff;
34 | }
35 |
36 | .hljs-attr,
37 | .hljs-attribute,
38 | .hljs-literal,
39 | .hljs-meta,
40 | .hljs-number,
41 | .hljs-operator,
42 | .hljs-variable,
43 | .hljs-selector-attr,
44 | .hljs-selector-class,
45 | .hljs-selector-id {
46 | /* prettylights-syntax-constant */
47 | color: #79c0ff;
48 | }
49 |
50 | .hljs-regexp,
51 | .hljs-string,
52 | .hljs-meta .hljs-string {
53 | /* prettylights-syntax-string */
54 | color: #a5d6ff;
55 | }
56 |
57 | .hljs-built_in,
58 | .hljs-symbol {
59 | /* prettylights-syntax-variable */
60 | color: #ffa657;
61 | }
62 |
63 | .hljs-comment,
64 | .hljs-code,
65 | .hljs-formula {
66 | /* prettylights-syntax-comment */
67 | color: #8b949e;
68 | }
69 |
70 | .hljs-name,
71 | .hljs-quote,
72 | .hljs-selector-tag,
73 | .hljs-selector-pseudo {
74 | /* prettylights-syntax-entity-tag */
75 | color: #7ee787;
76 | }
77 |
78 | .hljs-subst {
79 | /* prettylights-syntax-storage-modifier-import */
80 | color: #c9d1d9;
81 | }
82 |
83 | .hljs-section {
84 | /* prettylights-syntax-markup-heading */
85 | color: #1f6feb;
86 | font-weight: bold;
87 | }
88 |
89 | .hljs-bullet {
90 | /* prettylights-syntax-markup-list */
91 | color: #f2cc60;
92 | }
93 |
94 | .hljs-emphasis {
95 | /* prettylights-syntax-markup-italic */
96 | color: #c9d1d9;
97 | font-style: italic;
98 | }
99 |
100 | .hljs-strong {
101 | /* prettylights-syntax-markup-bold */
102 | color: #c9d1d9;
103 | font-weight: bold;
104 | }
105 |
106 | .hljs-addition {
107 | /* prettylights-syntax-markup-inserted */
108 | color: #aff5b4;
109 | background-color: #033a16;
110 | }
111 |
112 | .hljs-deletion {
113 | /* prettylights-syntax-markup-deleted */
114 | color: #ffdcd7;
115 | background-color: #67060c;
116 | }
117 |
118 | .hljs-char.escape_,
119 | .hljs-link,
120 | .hljs-params,
121 | .hljs-property,
122 | .hljs-punctuation,
123 | .hljs-tag {
124 | /* purposely ignored */
125 | }
126 |
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/image.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/italic.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/laugh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/site/src/jsMain/resources/public/laugh.png
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/link.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/logo.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/quote.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/subtitle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/site/src/jsMain/resources/public/title.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/site/src/jvmMain/kotlin/com/example/blogmultiplatform/api/Newsletter.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.api
2 |
3 | import com.example.blogmultiplatform.data.MongoDB
4 | import com.example.blogmultiplatform.models.Newsletter
5 | import com.varabyte.kobweb.api.Api
6 | import com.varabyte.kobweb.api.ApiContext
7 | import com.varabyte.kobweb.api.data.getValue
8 |
9 | @Api(routeOverride = "subscribe")
10 | suspend fun subscribeNewsletter(context: ApiContext) {
11 | try {
12 | val newsletter = context.req.getBody()
13 | context.res.setBody(newsletter?.let {
14 | context.data.getValue().subscribe(newsletter = it)
15 | })
16 | } catch (e: Exception) {
17 | context.res.setBody(e.message)
18 | }
19 | }
--------------------------------------------------------------------------------
/site/src/jvmMain/kotlin/com/example/blogmultiplatform/api/Posts.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.api
2 |
3 | import com.example.blogmultiplatform.data.MongoDB
4 | import com.example.blogmultiplatform.models.ApiListResponse
5 | import com.example.blogmultiplatform.models.ApiResponse
6 | import com.example.blogmultiplatform.models.Constants.AUTHOR_PARAM
7 | import com.example.blogmultiplatform.models.Constants.CATEGORY_PARAM
8 | import com.example.blogmultiplatform.models.Constants.POST_ID_PARAM
9 | import com.example.blogmultiplatform.models.Constants.QUERY_PARAM
10 | import com.example.blogmultiplatform.models.Constants.SKIP_PARAM
11 | import com.example.blogmultiplatform.models.Post
12 | import com.varabyte.kobweb.api.Api
13 | import com.varabyte.kobweb.api.ApiContext
14 | import com.varabyte.kobweb.api.data.getValue
15 | import com.varabyte.kobweb.api.http.Request
16 | import com.varabyte.kobweb.api.http.Response
17 | import com.varabyte.kobweb.api.http.setBodyText
18 | import kotlinx.serialization.json.Json
19 | import kotlinx.serialization.encodeToString
20 | import org.bson.codecs.ObjectIdGenerator
21 | import com.example.shared.Category
22 |
23 | @Api(routeOverride = "addpost")
24 | suspend fun addPost(context: ApiContext) {
25 | try {
26 | val post = context.req.getBody()
27 | val newPost = post?.copy(_id = ObjectIdGenerator().generate().toString())
28 | context.res.setBody(
29 | newPost?.let {
30 | context.data.getValue().addPost(it)
31 | }
32 | )
33 | } catch (e: Exception) {
34 | context.res.setBody(e.message)
35 | }
36 | }
37 |
38 | @Api(routeOverride = "updatepost")
39 | suspend fun updatePost(context: ApiContext) {
40 | try {
41 | val updatedPost = context.req.getBody()
42 | context.res.setBody(
43 | updatedPost?.let {
44 | context.data.getValue().updatePost(it)
45 | }
46 | )
47 | } catch (e: Exception) {
48 | context.res.setBody(e.message)
49 | }
50 | }
51 |
52 | @Api(routeOverride = "readmyposts")
53 | suspend fun readMyPosts(context: ApiContext) {
54 | try {
55 | val skip = context.req.params[SKIP_PARAM]?.toInt() ?: 0
56 | val author = context.req.params[AUTHOR_PARAM] ?: ""
57 | val myPosts = context.data.getValue().readMyPosts(
58 | skip = skip,
59 | author = author
60 | )
61 | context.res.setBody(ApiListResponse.Success(data = myPosts))
62 | } catch (e: Exception) {
63 | context.res.setBody(ApiListResponse.Error(message = e.message.toString()))
64 | }
65 | }
66 |
67 | @Api(routeOverride = "readmainposts")
68 | suspend fun readMainPosts(context: ApiContext) {
69 | try {
70 | val mainPosts = context.data.getValue().readMainPosts()
71 | context.res.setBody(ApiListResponse.Success(data = mainPosts))
72 | } catch (e: Exception) {
73 | context.res.setBody(ApiListResponse.Error(message = e.message.toString()))
74 | }
75 | }
76 |
77 | @Api(routeOverride = "readlatestposts")
78 | suspend fun readLatestPosts(context: ApiContext) {
79 | try {
80 | val skip = context.req.params[SKIP_PARAM]?.toInt() ?: 0
81 | val latestPosts = context.data.getValue().readLatestPosts(skip = skip)
82 | context.res.setBody(ApiListResponse.Success(data = latestPosts))
83 | } catch (e: Exception) {
84 | context.res.setBody(ApiListResponse.Error(message = e.message.toString()))
85 | }
86 | }
87 |
88 | @Api(routeOverride = "readsponsoredposts")
89 | suspend fun readSponsoredPosts(context: ApiContext) {
90 | try {
91 | val sponsoredPosts = context.data.getValue().readSponsoredPosts()
92 | context.res.setBody(ApiListResponse.Success(data = sponsoredPosts))
93 | } catch (e: Exception) {
94 | context.res.setBody(ApiListResponse.Error(message = e.message.toString()))
95 | }
96 | }
97 |
98 | @Api(routeOverride = "readpopularposts")
99 | suspend fun readPopularPosts(context: ApiContext) {
100 | try {
101 | val skip = context.req.params[SKIP_PARAM]?.toInt() ?: 0
102 | val popularPosts = context.data.getValue().readPopularPosts(skip = skip)
103 | context.res.setBody(ApiListResponse.Success(data = popularPosts))
104 | } catch (e: Exception) {
105 | context.res.setBody(ApiListResponse.Error(message = e.message.toString()))
106 | }
107 | }
108 |
109 | @Api(routeOverride = "deleteselectedposts")
110 | suspend fun deleteSelectedPosts(context: ApiContext) {
111 | try {
112 | val request = context.req.getBody>()
113 | context.res.setBody(request?.let {
114 | context.data.getValue().deleteSelectedPosts(ids = it)
115 | })
116 | } catch (e: Exception) {
117 | context.res.setBody(e.message)
118 | }
119 | }
120 |
121 | @Api(routeOverride = "searchposts")
122 | suspend fun searchPostsByTitle(context: ApiContext) {
123 | try {
124 | val query = context.req.params[QUERY_PARAM] ?: ""
125 | val skip = context.req.params[SKIP_PARAM]?.toInt() ?: 0
126 | val posts = context.data.getValue().searchPostsByTittle(
127 | query = query,
128 | skip = skip
129 | )
130 | context.res.setBody(ApiListResponse.Success(data = posts))
131 | } catch (e: Exception) {
132 | context.res.setBody(ApiListResponse.Error(message = e.message.toString()))
133 | }
134 | }
135 |
136 | @Api(routeOverride = "searchpostsbycategory")
137 | suspend fun searchPostsByCategory(context: ApiContext) {
138 | try {
139 | val category =
140 | Category.valueOf(context.req.params[CATEGORY_PARAM] ?: Category.Programming.name)
141 | val skip = context.req.params[SKIP_PARAM]?.toInt() ?: 0
142 | val posts = context.data.getValue().searchPostsByCategory(
143 | category = category,
144 | skip = skip
145 | )
146 | context.res.setBody(ApiListResponse.Success(data = posts))
147 | } catch (e: Exception) {
148 | context.res.setBody(ApiListResponse.Error(message = e.message.toString()))
149 | }
150 | }
151 |
152 | @Api(routeOverride = "readselectedpost")
153 | suspend fun readSelectedPost(context: ApiContext) {
154 | val postId = context.req.params[POST_ID_PARAM]
155 | if (!postId.isNullOrEmpty()) {
156 | try {
157 | val selectedPost = context.data.getValue().readSelectedPost(id = postId)
158 | context.res.setBody(ApiResponse.Success(data = selectedPost))
159 | } catch (e: Exception) {
160 | context.res.setBody(ApiResponse.Error(message = e.message.toString()))
161 | }
162 | } else {
163 | context.res.setBody(ApiResponse.Error(message = "Selected Post does not exist."))
164 | }
165 | }
166 |
167 | inline fun Response.setBody(data: T) {
168 | setBodyText(Json.encodeToString(data))
169 | }
170 |
171 | inline fun Request.getBody(): T? {
172 | return body?.decodeToString()?.let { return Json.decodeFromString(it) }
173 | }
--------------------------------------------------------------------------------
/site/src/jvmMain/kotlin/com/example/blogmultiplatform/api/UserCheck.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.api
2 |
3 | import com.example.blogmultiplatform.data.MongoDB
4 | import com.example.blogmultiplatform.models.User
5 | import com.example.blogmultiplatform.models.UserWithoutPassword
6 | import com.varabyte.kobweb.api.Api
7 | import com.varabyte.kobweb.api.ApiContext
8 | import com.varabyte.kobweb.api.data.getValue
9 | import com.varabyte.kobweb.api.http.setBodyText
10 | import kotlinx.serialization.json.Json
11 | import kotlinx.serialization.encodeToString
12 | import java.nio.charset.StandardCharsets
13 | import java.security.MessageDigest
14 |
15 | @Api(routeOverride = "usercheck")
16 | suspend fun userCheck(context: ApiContext) {
17 | try {
18 | val userRequest =
19 | context.req.body?.decodeToString()?.let { Json.decodeFromString(it) }
20 | val user = userRequest?.let {
21 | context.data.getValue().checkUserExistence(
22 | User(username = it.username, password = hashPassword(it.password))
23 | )
24 | }
25 | if (user != null) {
26 | context.res.setBodyText(
27 | Json.encodeToString(
28 | UserWithoutPassword(_id = user._id, username = user.username)
29 | )
30 | )
31 | } else {
32 | context.res.setBodyText(Json.encodeToString("User doesn't exist."))
33 | }
34 | } catch (e: Exception) {
35 | context.res.setBodyText(Json.encodeToString(e.message))
36 | }
37 | }
38 |
39 | @Api(routeOverride = "checkuserid")
40 | suspend fun checkUserId(context: ApiContext) {
41 | try {
42 | val idRequest =
43 | context.req.body?.decodeToString()?.let { Json.decodeFromString(it) }
44 | val result = idRequest?.let {
45 | context.data.getValue().checkUserId(it)
46 | }
47 | if (result != null) {
48 | context.res.setBodyText(Json.encodeToString(result))
49 | } else {
50 | context.res.setBodyText(Json.encodeToString(false))
51 | }
52 | } catch (e: Exception) {
53 | context.res.setBodyText(Json.encodeToString(false))
54 | }
55 | }
56 |
57 | private fun hashPassword(password: String): String {
58 | val messageDigest = MessageDigest.getInstance("SHA-256")
59 | val hashBytes = messageDigest.digest(password.toByteArray(StandardCharsets.UTF_8))
60 | val hexString = StringBuffer()
61 |
62 | for (byte in hashBytes) {
63 | hexString.append(String.format("%02x", byte))
64 | }
65 |
66 | return hexString.toString()
67 | }
--------------------------------------------------------------------------------
/site/src/jvmMain/kotlin/com/example/blogmultiplatform/data/MongoDB.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.data
2 |
3 | import com.example.shared.Category
4 | import com.example.blogmultiplatform.models.Constants.POSTS_PER_PAGE
5 | import com.example.blogmultiplatform.models.Newsletter
6 | import com.example.blogmultiplatform.models.Post
7 | import com.example.blogmultiplatform.models.PostWithoutDetails
8 | import com.example.blogmultiplatform.models.User
9 | import com.example.blogmultiplatform.util.Constants.DATABASE_NAME
10 | import com.example.blogmultiplatform.util.Constants.MAIN_POSTS_LIMIT
11 | import com.mongodb.client.model.Filters
12 | import com.mongodb.client.model.Indexes.descending
13 | import com.mongodb.client.model.Updates
14 | import com.mongodb.kotlin.client.coroutine.MongoClient
15 | import com.varabyte.kobweb.api.data.add
16 | import com.varabyte.kobweb.api.init.InitApi
17 | import com.varabyte.kobweb.api.init.InitApiContext
18 | import kotlinx.coroutines.flow.firstOrNull
19 | import kotlinx.coroutines.flow.toList
20 |
21 | @InitApi
22 | fun initMongoDB(ctx: InitApiContext) {
23 | System.setProperty(
24 | "org.litote.mongo.test.mapping.service",
25 | "org.litote.kmongo.serialization.SerializationClassMappingTypeService"
26 | )
27 | ctx.data.add(MongoDB(ctx))
28 | }
29 |
30 | class MongoDB(private val context: InitApiContext) : MongoRepository {
31 | // For testing with a localhost.
32 | // private val client = MongoClient.create()
33 | // For a remote mongo database.
34 | private val client = MongoClient.create(System.getenv("MONGODB_URI"))
35 |
36 | private val database = client.getDatabase(DATABASE_NAME)
37 | private val userCollection = database.getCollection("user")
38 | private val postCollection = database.getCollection("post")
39 | private val newsletterCollection = database.getCollection("newsletter")
40 |
41 | override suspend fun addPost(post: Post): Boolean {
42 | return postCollection.insertOne(post).wasAcknowledged()
43 | }
44 |
45 | override suspend fun updatePost(post: Post): Boolean {
46 | return postCollection
47 | .updateOne(
48 | Filters.eq(Post::_id.name, post._id),
49 | mutableListOf(
50 | Updates.set(Post::title.name, post.title),
51 | Updates.set(Post::subtitle.name, post.subtitle),
52 | Updates.set(Post::category.name, post.category),
53 | Updates.set(Post::thumbnail.name, post.thumbnail),
54 | Updates.set(Post::content.name, post.content),
55 | Updates.set(Post::main.name, post.main),
56 | Updates.set(Post::popular.name, post.popular),
57 | Updates.set(Post::sponsored.name, post.sponsored)
58 | )
59 | )
60 | .wasAcknowledged()
61 | }
62 |
63 | override suspend fun readMyPosts(skip: Int, author: String): List {
64 | return postCollection
65 | .withDocumentClass(PostWithoutDetails::class.java)
66 | .find(Filters.eq(PostWithoutDetails::author.name, author))
67 | .sort(descending(PostWithoutDetails::date.name))
68 | .skip(skip)
69 | .limit(POSTS_PER_PAGE)
70 | .toList()
71 | }
72 |
73 | override suspend fun readMainPosts(): List {
74 | return postCollection
75 | .withDocumentClass(PostWithoutDetails::class.java)
76 | .find(Filters.eq(PostWithoutDetails::main.name, true))
77 | .sort(descending(PostWithoutDetails::date.name))
78 | .limit(MAIN_POSTS_LIMIT)
79 | .toList()
80 | }
81 |
82 | override suspend fun readLatestPosts(skip: Int): List {
83 | return postCollection
84 | .withDocumentClass(PostWithoutDetails::class.java)
85 | .find(
86 | Filters.and(
87 | Filters.eq(PostWithoutDetails::popular.name, false),
88 | Filters.eq(PostWithoutDetails::main.name, false),
89 | Filters.eq(PostWithoutDetails::sponsored.name, false)
90 | )
91 | )
92 | .sort(descending(PostWithoutDetails::date.name))
93 | .skip(skip)
94 | .limit(POSTS_PER_PAGE)
95 | .toList()
96 | }
97 |
98 | override suspend fun readSponsoredPosts(): List {
99 | return postCollection
100 | .withDocumentClass(PostWithoutDetails::class.java)
101 | .find(Filters.eq(PostWithoutDetails::sponsored.name, true))
102 | .sort(descending(PostWithoutDetails::date.name))
103 | .limit(2)
104 | .toList()
105 | }
106 |
107 | override suspend fun readPopularPosts(skip: Int): List {
108 | return postCollection
109 | .withDocumentClass(PostWithoutDetails::class.java)
110 | .find(Filters.eq(PostWithoutDetails::popular.name, true))
111 | .sort(descending(PostWithoutDetails::date.name))
112 | .skip(skip)
113 | .limit(POSTS_PER_PAGE)
114 | .toList()
115 | }
116 |
117 | override suspend fun deleteSelectedPosts(ids: List): Boolean {
118 | return postCollection
119 | .deleteMany(Filters.`in`(Post::_id.name, ids))
120 | .wasAcknowledged()
121 | }
122 |
123 | override suspend fun searchPostsByTittle(query: String, skip: Int): List {
124 | val regexQuery = query.toRegex(RegexOption.IGNORE_CASE)
125 | return postCollection
126 | .withDocumentClass(PostWithoutDetails::class.java)
127 | .find(Filters.regex(PostWithoutDetails::title.name, regexQuery.pattern))
128 | .sort(descending(PostWithoutDetails::date.name))
129 | .skip(skip)
130 | .limit(POSTS_PER_PAGE)
131 | .toList()
132 | }
133 |
134 | override suspend fun searchPostsByCategory(
135 | category: Category,
136 | skip: Int
137 | ): List {
138 | return postCollection
139 | .withDocumentClass(PostWithoutDetails::class.java)
140 | .find(Filters.eq(PostWithoutDetails::category.name, category))
141 | .sort(descending(PostWithoutDetails::date.name))
142 | .skip(skip)
143 | .limit(POSTS_PER_PAGE)
144 | .toList()
145 | }
146 |
147 | override suspend fun readSelectedPost(id: String): Post {
148 | return postCollection.find(Filters.eq(Post::_id.name, id)).toList().first()
149 | }
150 |
151 | override suspend fun checkUserExistence(user: User): User? {
152 | return try {
153 | userCollection
154 | .find(
155 | Filters.and(
156 | Filters.eq(User::username.name, user.username),
157 | Filters.eq(User::password.name, user.password)
158 | )
159 | ).firstOrNull()
160 | } catch (e: Exception) {
161 | context.logger.error(e.message.toString())
162 | null
163 | }
164 | }
165 |
166 | override suspend fun checkUserId(id: String): Boolean {
167 | return try {
168 | val documentCount = userCollection.countDocuments(Filters.eq(User::_id.name, id))
169 | documentCount > 0
170 | } catch (e: Exception) {
171 | context.logger.error(e.message.toString())
172 | false
173 | }
174 | }
175 |
176 | override suspend fun subscribe(newsletter: Newsletter): String {
177 | val result = newsletterCollection
178 | .find(Filters.eq(Newsletter::email.name, newsletter.email))
179 | .toList()
180 | return if (result.isNotEmpty()) {
181 | "You're already subscribed."
182 | } else {
183 | val newEmail = newsletterCollection
184 | .insertOne(newsletter)
185 | .wasAcknowledged()
186 | if (newEmail) "Successfully Subscribed!"
187 | else "Something went wrong. Please try again later."
188 | }
189 | }
190 | }
--------------------------------------------------------------------------------
/site/src/jvmMain/kotlin/com/example/blogmultiplatform/data/MongoRepository.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.data
2 |
3 | import com.example.shared.Category
4 | import com.example.blogmultiplatform.models.Newsletter
5 | import com.example.blogmultiplatform.models.Post
6 | import com.example.blogmultiplatform.models.PostWithoutDetails
7 | import com.example.blogmultiplatform.models.User
8 |
9 | interface MongoRepository {
10 | suspend fun addPost(post: Post): Boolean
11 | suspend fun updatePost(post: Post): Boolean
12 | suspend fun readMyPosts(skip: Int, author: String): List
13 | suspend fun readMainPosts(): List
14 | suspend fun readLatestPosts(skip: Int): List
15 | suspend fun readSponsoredPosts(): List
16 | suspend fun readPopularPosts(skip: Int): List
17 | suspend fun deleteSelectedPosts(ids: List): Boolean
18 | suspend fun searchPostsByTittle(query: String, skip: Int): List
19 | suspend fun searchPostsByCategory(category: Category, skip: Int): List
20 | suspend fun readSelectedPost(id: String): Post
21 | suspend fun checkUserExistence(user: User): User?
22 | suspend fun checkUserId(id: String): Boolean
23 | suspend fun subscribe(newsletter: Newsletter): String
24 | }
--------------------------------------------------------------------------------
/site/src/jvmMain/kotlin/com/example/blogmultiplatform/models/User.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.models
2 |
3 | import kotlinx.serialization.Serializable
4 | import org.bson.codecs.ObjectIdGenerator
5 |
6 | @Serializable
7 | actual data class User(
8 | actual val _id: String = ObjectIdGenerator().generate().toString(),
9 | actual val username: String = "",
10 | actual val password: String = ""
11 | )
12 |
13 | @Serializable
14 | actual data class UserWithoutPassword(
15 | actual val _id: String = ObjectIdGenerator().generate().toString(),
16 | actual val username: String = ""
17 | )
--------------------------------------------------------------------------------
/site/src/jvmMain/kotlin/com/example/blogmultiplatform/util/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.example.blogmultiplatform.util
2 |
3 | object Constants {
4 | // For a remote mongo database.
5 | const val DATABASE_NAME = "MyBlog"
6 | // For testing with a localhost.
7 | // const val DATABASE_NAME = "my_blog"
8 | const val MAIN_POSTS_LIMIT = 4
9 | }
--------------------------------------------------------------------------------