├── .fleet └── settings.json ├── .gitignore ├── .idea ├── .name ├── migrations.xml ├── misc.xml ├── other.xml └── vcs.xml ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties ├── settings.gradle.kts ├── site-android ├── .gitignore ├── .kobweb │ ├── conf.yaml │ ├── server │ │ ├── logs │ │ │ └── kobweb-server.log │ │ ├── server.jar │ │ ├── start.bat │ │ └── start.sh │ └── site │ │ ├── admin │ │ ├── all-posts.html │ │ ├── create-post.html │ │ └── my-posts.html │ │ ├── android-figure.webp │ │ ├── androidhub-logo-dark.webp │ │ ├── androidhub-logo.webp │ │ ├── androidhub.js │ │ ├── androidhub.js.map │ │ ├── author.html │ │ ├── bold.png │ │ ├── category.html │ │ ├── code.png │ │ ├── favicon.ico │ │ ├── image.png │ │ ├── index.html │ │ ├── italic.png │ │ ├── jetpack.webp │ │ ├── link.png │ │ ├── login.html │ │ ├── posts.html │ │ ├── quote.png │ │ ├── search.html │ │ └── title-subtitle.png ├── build.gradle.kts └── src │ └── jsMain │ ├── kotlin │ └── com │ │ └── canerture │ │ └── androidhub │ │ ├── AppEntry.kt │ │ ├── AppStyles.kt │ │ ├── SiteTheme.kt │ │ ├── common │ │ ├── Constants.kt │ │ └── Extensions.kt │ │ ├── components │ │ ├── layouts │ │ │ ├── AdminPageLayout.kt │ │ │ └── PageLayout.kt │ │ ├── sections │ │ │ ├── Footer.kt │ │ │ ├── NavHeader.kt │ │ │ ├── OverflowSidePanel.kt │ │ │ └── SidePanel.kt │ │ └── widgets │ │ │ ├── CategoryChip.kt │ │ │ ├── ErrorView.kt │ │ │ ├── LoadingIndicator.kt │ │ │ ├── Popup.kt │ │ │ ├── PostPreview.kt │ │ │ ├── PostsView.kt │ │ │ ├── SearchArea.kt │ │ │ └── SearchBar.kt │ │ ├── data │ │ ├── CategoryApis.kt │ │ ├── PostApis.kt │ │ ├── UserApis.kt │ │ └── model │ │ │ ├── AuthorDetail.kt │ │ │ ├── BaseResponse.kt │ │ │ ├── Category.kt │ │ │ ├── GetPostsResponse.kt │ │ │ ├── LoginRequest.kt │ │ │ ├── Post.kt │ │ │ ├── RegisterRequest.kt │ │ │ └── User.kt │ │ ├── models │ │ ├── ControlStyle.kt │ │ └── EditorControl.kt │ │ ├── navigation │ │ └── Screen.kt │ │ ├── pages │ │ ├── adminallposts │ │ │ └── AdminAllPosts.kt │ │ ├── admincreatepost │ │ │ ├── AdminCreatePost.kt │ │ │ └── Functions.kt │ │ ├── adminmyposts │ │ │ └── AdminMyPosts.kt │ │ ├── author │ │ │ └── Author.kt │ │ ├── category │ │ │ └── Category.kt │ │ ├── index │ │ │ ├── AndroidHeroContent.kt │ │ │ ├── Index.kt │ │ │ ├── LatestArticleItem.kt │ │ │ └── PopularArticleItem.kt │ │ ├── login │ │ │ └── Login.kt │ │ ├── post │ │ │ └── PostDetail.kt │ │ └── search │ │ │ └── Search.kt │ │ └── utils │ │ └── ApiUtils.kt │ └── resources │ ├── markdown │ └── About.md │ └── public │ ├── android-figure.webp │ ├── androidhub-logo-dark.webp │ ├── androidhub-logo.webp │ ├── bold.png │ ├── code.png │ ├── favicon.ico │ ├── image.png │ ├── italic.png │ ├── jetpack.webp │ ├── link.png │ ├── quote.png │ └── title-subtitle.png └── site-ios ├── .gitignore ├── .kobweb ├── conf.yaml ├── server │ ├── logs │ │ └── kobweb-server.log │ ├── server.jar │ ├── start.bat │ └── start.sh └── site │ ├── admin │ ├── all-posts.html │ ├── create-post.html │ └── my-posts.html │ ├── androidhub.js │ ├── androidhub.js.map │ ├── author.html │ ├── bold.png │ ├── category.html │ ├── code.png │ ├── favicon.ico │ ├── image.png │ ├── index.html │ ├── ios-figure.webp │ ├── ioshub-logo-dark.webp │ ├── ioshub-logo.webp │ ├── italic.png │ ├── jetpack.webp │ ├── link.png │ ├── login.html │ ├── posts.html │ ├── quote.png │ ├── search.html │ └── title-subtitle.png ├── build.gradle.kts └── src └── jsMain ├── kotlin └── com │ └── canerture │ └── androidhub │ ├── AppEntry.kt │ ├── AppStyles.kt │ ├── SiteTheme.kt │ ├── common │ ├── Constants.kt │ └── Extensions.kt │ ├── components │ ├── layouts │ │ ├── AdminPageLayout.kt │ │ └── PageLayout.kt │ ├── sections │ │ ├── Footer.kt │ │ ├── NavHeader.kt │ │ ├── OverflowSidePanel.kt │ │ └── SidePanel.kt │ └── widgets │ │ ├── CategoryChip.kt │ │ ├── ErrorView.kt │ │ ├── LoadingIndicator.kt │ │ ├── Popup.kt │ │ ├── PostPreview.kt │ │ ├── PostsView.kt │ │ ├── SearchArea.kt │ │ └── SearchBar.kt │ ├── data │ ├── CategoryApis.kt │ ├── PostApis.kt │ ├── UserApis.kt │ └── model │ │ ├── AuthorDetail.kt │ │ ├── BaseResponse.kt │ │ ├── Category.kt │ │ ├── GetPostsResponse.kt │ │ ├── LoginRequest.kt │ │ ├── Post.kt │ │ ├── RegisterRequest.kt │ │ └── User.kt │ ├── models │ ├── ControlStyle.kt │ └── EditorControl.kt │ ├── navigation │ └── Screen.kt │ ├── pages │ ├── adminallposts │ │ └── AdminAllPosts.kt │ ├── admincreatepost │ │ ├── AdminCreatePost.kt │ │ └── Functions.kt │ ├── adminmyposts │ │ └── AdminMyPosts.kt │ ├── author │ │ └── Author.kt │ ├── category │ │ └── Category.kt │ ├── index │ │ ├── IOSHeroContent.kt │ │ ├── Index.kt │ │ ├── LatestArticleItem.kt │ │ └── PopularArticleItem.kt │ ├── login │ │ └── Login.kt │ ├── post │ │ └── PostDetail.kt │ └── search │ │ └── Search.kt │ └── utils │ └── ApiUtils.kt └── resources ├── markdown └── About.md └── public ├── bold.png ├── code.png ├── favicon.ico ├── image.png ├── ios-figure.webp ├── ioshub-logo-dark.webp ├── ioshub-logo.webp ├── italic.png ├── jetpack.webp ├── link.png ├── quote.png └── title-subtitle.png /.fleet/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "toolchains": [] 3 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | androidhub -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Kobweb](https://github.com/varabyte/kobweb) project bootstrapped with the `app` template. 2 | 3 | ## Screenshots 4 | 5 | ![Artboard 12312](https://github.com/cnrture/AndroidHub/assets/29903779/35b30b09-35f8-4eda-85b3-363261b7180f) 6 | 7 | Live -> [https://androidhub.dev/](https://androidhub.dev/) 8 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | subprojects { 2 | repositories { 3 | mavenCentral() 4 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 5 | google() 6 | maven("https://us-central1-maven.pkg.dev/varabyte-repos/public") 7 | maven(url = "https://jitpack.io") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.jvmargs=-Xmx4096m -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | jetbrains-compose = "1.6.2" 3 | kobweb = "0.17.3" 4 | kotlin = "1.9.23" 5 | kotlinx-serialization = "1.4.0" 6 | colorMath = "3.5.0" 7 | 8 | [libraries] 9 | kobweb-api = { module = "com.varabyte.kobweb:kobweb-api", version.ref = "kobweb" } 10 | kobweb-core = { module = "com.varabyte.kobweb:kobweb-core ", version.ref = "kobweb" } 11 | kobweb-silk = { module = "com.varabyte.kobweb:kobweb-silk", version.ref = "kobweb" } 12 | silk-icons-fa = { module = "com.varabyte.kobwebx:silk-icons-fa", version.ref = "kobweb" } 13 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 14 | colorMath = { module = "com.github.ajalt.colormath:colormath", version.ref = "colorMath" } 15 | 16 | [plugins] 17 | jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "jetbrains-compose" } 18 | kobweb-application = { id = "com.varabyte.kobweb.application", version.ref = "kobweb" } 19 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 20 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/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.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Wed Apr 17 01:45:18 TRT 2024 8 | sdk.dir=/Users/canerture/Library/Android/sdk 9 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 5 | maven("https://us-central1-maven.pkg.dev/varabyte-repos/public") 6 | } 7 | } 8 | 9 | rootProject.name = "androidhub" 10 | 11 | include(":site-android") 12 | include(":site-ios") 13 | -------------------------------------------------------------------------------- /site-android/.gitignore: -------------------------------------------------------------------------------- 1 | # Kobweb ignores 2 | !.kobweb/conf.yaml 3 | -------------------------------------------------------------------------------- /site-android/.kobweb/conf.yaml: -------------------------------------------------------------------------------- 1 | site: 2 | title: "." 3 | 4 | server: 5 | files: 6 | dev: 7 | contentRoot: "build/processedResources/js/main/public" 8 | script: "build/dist/js/developmentExecutable/androidhub.js" 9 | api: "build/libs/androidhub.jar" 10 | prod: 11 | script: "build/dist/js/productionExecutable/androidhub.js" 12 | siteRoot: ".kobweb/site" 13 | 14 | port: 8080 15 | -------------------------------------------------------------------------------- /site-android/.kobweb/server/logs/kobweb-server.log: -------------------------------------------------------------------------------- 1 | 2024-06-15 04:01:37.322 [main] DEBUG ktor.application - Java Home: /Users/canerture/Library/Java/JavaVirtualMachines/jbr-17.0.9/Contents 2 | 2024-06-15 04:01:37.326 [main] DEBUG ktor.application - Class Loader: jdk.internal.loader.ClassLoaders$AppClassLoader@7aec35a: [file:/Users/canerture/Documents/GitHub/AndroidHub/site-android/.kobweb/server/server.jar!/ch] 3 | 2024-06-15 04:01:37.334 [main] INFO ktor.application - API jar found and will be loaded: "build/libs/androidhub.jar" 4 | 2024-06-15 04:01:37.355 [main] INFO ktor.application - Loaded and initialized server API jar in 1ms. 5 | 2024-06-15 04:01:37.365 [main] INFO ktor.application - Initializing Kobweb streams. 6 | 2024-06-15 04:01:37.382 [main] INFO ktor.application - Application started in 0.089 seconds. 7 | 2024-06-15 04:01:37.439 [main] INFO ktor.application - Responding at http://0.0.0.0:8080 8 | 2024-06-15 04:02:06.316 [kotlinx.coroutines.DefaultExecutor] INFO ktor.application - Kobweb server shutting down... 9 | 2024-06-15 04:02:07.375 [kotlinx.coroutines.DefaultExecutor] INFO ktor.application - Server finished shutting down. 10 | -------------------------------------------------------------------------------- /site-android/.kobweb/server/server.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/server/server.jar -------------------------------------------------------------------------------- /site-android/.kobweb/server/start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: Find the kobweb project folder, using this file's location to start form 4 | cd /d "%~dp0" 5 | cd ..\.. 6 | 7 | if defined KOBWEB_JAVA_HOME ( 8 | set "java_cmd=%KOBWEB_JAVA_HOME%\bin\java" 9 | ) else if defined JAVA_HOME ( 10 | set "java_cmd=%JAVA_HOME%\bin\java" 11 | ) else ( 12 | set "java_cmd=java" 13 | ) 14 | 15 | :: Run the java command with the common parameters 16 | %java_cmd% -Dkobweb.server.environment=PROD -Dkobweb.site.layout=FULLSTACK -Dio.ktor.development=false -jar .kobweb/server/server.jar -------------------------------------------------------------------------------- /site-android/.kobweb/server/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Find the kobweb project folder, using this file's location to start form 4 | cd "$(dirname "$0")" 5 | cd ../.. 6 | 7 | args="-Dkobweb.server.environment=PROD -Dkobweb.site.layout=FULLSTACK -Dio.ktor.development=false -jar .kobweb/server/server.jar" 8 | 9 | if [ -n "$KOBWEB_JAVA_HOME" ]; then 10 | "$KOBWEB_JAVA_HOME/bin/java" $args 11 | elif [ -n "$JAVA_HOME" ]; then 12 | "$JAVA_HOME/bin/java" $args 13 | else 14 | java $args 15 | fi -------------------------------------------------------------------------------- /site-android/.kobweb/site/android-figure.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/site/android-figure.webp -------------------------------------------------------------------------------- /site-android/.kobweb/site/androidhub-logo-dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/site/androidhub-logo-dark.webp -------------------------------------------------------------------------------- /site-android/.kobweb/site/androidhub-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/site/androidhub-logo.webp -------------------------------------------------------------------------------- /site-android/.kobweb/site/bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/site/bold.png -------------------------------------------------------------------------------- /site-android/.kobweb/site/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/site/code.png -------------------------------------------------------------------------------- /site-android/.kobweb/site/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/site/favicon.ico -------------------------------------------------------------------------------- /site-android/.kobweb/site/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/site/image.png -------------------------------------------------------------------------------- /site-android/.kobweb/site/italic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/site/italic.png -------------------------------------------------------------------------------- /site-android/.kobweb/site/jetpack.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/site/jetpack.webp -------------------------------------------------------------------------------- /site-android/.kobweb/site/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/site/link.png -------------------------------------------------------------------------------- /site-android/.kobweb/site/quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/site/quote.png -------------------------------------------------------------------------------- /site-android/.kobweb/site/title-subtitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/.kobweb/site/title-subtitle.png -------------------------------------------------------------------------------- /site-android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.varabyte.kobweb.gradle.application.extensions.AppBlock.LegacyRouteRedirectStrategy 2 | import com.varabyte.kobweb.gradle.application.util.configAsKobwebApplication 3 | 4 | plugins { 5 | alias(libs.plugins.kotlin.multiplatform) 6 | alias(libs.plugins.jetbrains.compose) 7 | alias(libs.plugins.kobweb.application) 8 | alias(libs.plugins.kotlin.serialization) 9 | } 10 | 11 | group = "com.canerture.androidhub" 12 | version = "1.0-SNAPSHOT" 13 | 14 | kobweb { 15 | app { 16 | index { 17 | description.set("Powered by Kobweb") 18 | } 19 | legacyRouteRedirectStrategy.set(LegacyRouteRedirectStrategy.DISALLOW) 20 | } 21 | } 22 | 23 | kotlin { 24 | configAsKobwebApplication("androidhub", includeServer = true) 25 | 26 | sourceSets { 27 | commonMain.dependencies { 28 | implementation(compose.runtime) 29 | } 30 | 31 | jsMain.dependencies { 32 | implementation(compose.html.core) 33 | implementation(libs.kobweb.core) 34 | implementation(libs.kobweb.silk) 35 | implementation(libs.silk.icons.fa) 36 | implementation(libs.kotlinx.serialization.json) 37 | implementation(libs.colorMath) 38 | } 39 | 40 | jvmMain.dependencies { 41 | implementation(libs.kobweb.api) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/AppEntry.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub 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.canerture.androidhub.common.Res 9 | import com.varabyte.kobweb.compose.css.Cursor 10 | import com.varabyte.kobweb.compose.css.ScrollBehavior 11 | import com.varabyte.kobweb.compose.foundation.layout.Box 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.border 17 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius 18 | import com.varabyte.kobweb.compose.ui.modifiers.bottom 19 | import com.varabyte.kobweb.compose.ui.modifiers.color 20 | import com.varabyte.kobweb.compose.ui.modifiers.cursor 21 | import com.varabyte.kobweb.compose.ui.modifiers.display 22 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize 23 | import com.varabyte.kobweb.compose.ui.modifiers.minHeight 24 | import com.varabyte.kobweb.compose.ui.modifiers.minWidth 25 | import com.varabyte.kobweb.compose.ui.modifiers.onClick 26 | import com.varabyte.kobweb.compose.ui.modifiers.outline 27 | import com.varabyte.kobweb.compose.ui.modifiers.padding 28 | import com.varabyte.kobweb.compose.ui.modifiers.position 29 | import com.varabyte.kobweb.compose.ui.modifiers.right 30 | import com.varabyte.kobweb.compose.ui.modifiers.scrollBehavior 31 | import com.varabyte.kobweb.compose.ui.modifiers.size 32 | import com.varabyte.kobweb.compose.ui.modifiers.zIndex 33 | import com.varabyte.kobweb.compose.ui.toAttrs 34 | import com.varabyte.kobweb.core.App 35 | import com.varabyte.kobweb.silk.SilkApp 36 | import com.varabyte.kobweb.silk.components.graphics.Image 37 | import com.varabyte.kobweb.silk.components.layout.Surface 38 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 39 | import com.varabyte.kobweb.silk.components.style.toModifier 40 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint 41 | import kotlinx.browser.document 42 | import kotlinx.browser.window 43 | import org.jetbrains.compose.web.css.Color 44 | import org.jetbrains.compose.web.css.DisplayStyle 45 | import org.jetbrains.compose.web.css.Position 46 | import org.jetbrains.compose.web.css.percent 47 | import org.jetbrains.compose.web.css.px 48 | import org.jetbrains.compose.web.css.vh 49 | import org.jetbrains.compose.web.css.vw 50 | import org.jetbrains.compose.web.dom.Div 51 | import org.w3c.dom.SMOOTH 52 | import org.w3c.dom.ScrollToOptions 53 | 54 | @App 55 | @Composable 56 | fun AppEntry(content: @Composable () -> Unit) { 57 | SilkApp { 58 | var isButtonVisible by remember { mutableStateOf(false) } 59 | val breakpoint = rememberBreakpoint() 60 | 61 | window.onscroll = { 62 | if (document.body!!.scrollTop > 300 || document.documentElement!!.scrollTop > 300) { 63 | isButtonVisible = true 64 | } else { 65 | isButtonVisible = false 66 | } 67 | } 68 | 69 | Surface( 70 | Modifier 71 | .minHeight(100.vh) 72 | .minWidth(100.vw) 73 | .scrollBehavior(ScrollBehavior.Smooth) 74 | ) { 75 | Box( 76 | contentAlignment = Alignment.Center, 77 | modifier = Modifier.fillMaxSize() 78 | ) { 79 | content() 80 | 81 | Div( 82 | attrs = Modifier 83 | .position(Position.Fixed) 84 | .bottom(20.px) 85 | .right(if (breakpoint < Breakpoint.MD) 0.px else 30.px) 86 | .zIndex(99) 87 | .border(0.px) 88 | .outline(0.px) 89 | .backgroundColor(Color.transparent) 90 | .color(Colors.White) 91 | .cursor(Cursor.Pointer) 92 | .borderRadius(100.percent) 93 | .padding( 94 | bottom = if (breakpoint < Breakpoint.MD) 10.px else 15.px, 95 | right = if (breakpoint < Breakpoint.MD) 4.px else 22.px 96 | ) 97 | .display( 98 | if (isButtonVisible) DisplayStyle.Block else DisplayStyle.None 99 | ) 100 | .onClick { 101 | document.documentElement?.scrollTo( 102 | ScrollToOptions( 103 | behavior = org.w3c.dom.ScrollBehavior.SMOOTH, 104 | top = 0.0 105 | ) 106 | ) 107 | }.toAttrs() 108 | ) { 109 | val size = if (breakpoint < Breakpoint.MD) 40.px else 60.px 110 | Image( 111 | src = Res.Image.JETPACK, 112 | modifier = Modifier.size(size, size) 113 | ) 114 | } 115 | } 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/SiteTheme.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub 2 | 3 | import com.varabyte.kobweb.compose.ui.graphics.Color 4 | import com.varabyte.kobweb.compose.ui.graphics.Colors 5 | import com.varabyte.kobweb.silk.init.InitSilk 6 | import com.varabyte.kobweb.silk.init.InitSilkContext 7 | import com.varabyte.kobweb.silk.theme.colors.palette.background 8 | import com.varabyte.kobweb.silk.theme.colors.palette.color 9 | 10 | /** 11 | * @property nearBackground A useful color to apply to a container that should differentiate itself from the background 12 | * but just a little. 13 | */ 14 | class SitePalette( 15 | val green: Color, 16 | val white: Color, 17 | val blue: Color, 18 | val gray: Color, 19 | val lightGray: Color, 20 | val grayTransparent: Color, 21 | ) 22 | 23 | object SitePalettes { 24 | val palette = SitePalette( 25 | green = Color.rgb(0x3DDB86), 26 | white = Colors.White, 27 | blue = Color.rgb(0x092F42), 28 | gray = Color.rgb(0xC8C8C8), 29 | lightGray = Color.rgb(0xF0F0F0), 30 | grayTransparent = Color.rgba(255f, 255f, 255f, 0.15f), 31 | ) 32 | } 33 | 34 | fun getSitePalette(): SitePalette { 35 | return SitePalettes.palette 36 | } 37 | 38 | @InitSilk 39 | fun initTheme(ctx: InitSilkContext) { 40 | ctx.theme.palettes.light.background = Color.rgb(0xFAF8FF) 41 | ctx.theme.palettes.light.color = Colors.White 42 | } 43 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/common/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.common 2 | 3 | object Constants { 4 | const val BASE_URL = "https://api.canerture.com/androidhub/" 5 | 6 | const val QUERY_PARAM = "query" 7 | const val CATEGORY_PARAM = "category" 8 | const val POST_SHORT_PARAM = "short" 9 | const val POST_AUTHOR_ID_PARAM = "authorId" 10 | 11 | const val SIDE_PANEL_WIDTH = 250 12 | const val COLLAPSED_PANEL_HEIGHT = 100 13 | } 14 | 15 | object Res { 16 | object Image { 17 | const val LOGO = "/androidhub-logo.webp" 18 | const val LOGO_DARK = "/androidhub-logo-dark.webp" 19 | const val ANDROID_FIGURE = "/android-figure.webp" 20 | const val JETPACK = "/jetpack.webp" 21 | } 22 | 23 | object Icon { 24 | const val BOLD = "/bold.png" 25 | const val ITALIC = "/italic.png" 26 | const val LINK = "/link.png" 27 | const val TITLE = "/title-subtitle.png" 28 | const val SUBTITLE = "/title-subtitle.png" 29 | const val QUOTE = "/quote.png" 30 | const val CODE = "/code.png" 31 | const val IMAGE = "/image.png" 32 | } 33 | 34 | object PathIcon { 35 | const val CREATE = 36 | "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" 37 | const val POSTS = 38 | "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" 39 | const val LOGOUT = 40 | "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" 41 | } 42 | } 43 | 44 | object Id { 45 | const val NAME_INPUT = "nameInput" 46 | const val EMAIL_INPUT = "emailInput" 47 | const val PASSWORD_INPUT = "passwordInput" 48 | const val SVG_PARENT = "svgParent" 49 | const val VECTOR_ICON = "vectorIcon" 50 | const val NAVIGATION_TEXT = "navigationText" 51 | const val EDITOR = "editor" 52 | const val EDITOR_PREVIEW = "editorPreview" 53 | const val TITLE_INPUT = "titleInput" 54 | const val THUMBNAIL_INPUT = "thumbnailInput" 55 | const val LINK_HREF_INPUT = "linkHrefInput" 56 | const val LINK_TITLE_INPUT = "linkTitleInput" 57 | const val ADMIN_SEARCH_BAR = "adminSearchBar" 58 | const val POST_CONTENT = "postContent" 59 | const val NAVBAR_SEARCH_INPUT = "navbarSearchInput" 60 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/common/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.common 2 | 3 | import com.varabyte.kobweb.compose.ui.Modifier 4 | import com.varabyte.kobweb.compose.ui.graphics.Colors 5 | import com.varabyte.kobweb.compose.ui.modifiers.border 6 | import com.varabyte.kobweb.compose.ui.modifiers.outline 7 | import org.jetbrains.compose.web.css.LineStyle 8 | import org.jetbrains.compose.web.css.px 9 | import kotlin.js.Date 10 | 11 | fun Modifier.noBorder(): Modifier { 12 | return this.border( 13 | width = 0.px, 14 | style = LineStyle.None, 15 | color = Colors.Transparent 16 | ).outline( 17 | width = 0.px, 18 | style = LineStyle.None, 19 | color = Colors.Transparent 20 | ) 21 | } 22 | 23 | fun Long.parseDateString() = Date(this).toLocaleDateString() 24 | 25 | fun String.formatShort() = replace(" ", "-").lowercase() 26 | .replace("ğ", "g") 27 | .replace("ü", "u") 28 | .replace("ş", "s") 29 | .replace("ı", "i") 30 | .replace("ö", "o") 31 | .replace("ç", "c") 32 | .replace("'", "") -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/components/layouts/AdminPageLayout.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.layouts 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.canerture.androidhub.components.sections.AdminNavigationItems 9 | import com.canerture.androidhub.components.sections.OverflowSidePanel 10 | import com.canerture.androidhub.components.sections.SidePanel 11 | import com.varabyte.kobweb.compose.foundation.layout.Box 12 | import com.varabyte.kobweb.compose.foundation.layout.Column 13 | import com.varabyte.kobweb.compose.ui.Alignment 14 | import com.varabyte.kobweb.compose.ui.Modifier 15 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize 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 | ) { 28 | SidePanel( 29 | onMenuClick = { overflowOpened = true } 30 | ) 31 | if (overflowOpened) { 32 | OverflowSidePanel( 33 | onMenuClose = { 34 | overflowOpened = false 35 | }, 36 | content = { 37 | AdminNavigationItems() 38 | } 39 | ) 40 | } 41 | content() 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/components/sections/Footer.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.sections 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.ShadowedGreenVariant 5 | import com.canerture.androidhub.common.noBorder 6 | import com.canerture.androidhub.data.isUserLoggedIn 7 | import com.canerture.androidhub.getSitePalette 8 | import com.canerture.androidhub.navigation.Screen 9 | import com.varabyte.kobweb.compose.css.Cursor 10 | import com.varabyte.kobweb.compose.css.FontWeight 11 | import com.varabyte.kobweb.compose.css.TextAlign 12 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement 13 | import com.varabyte.kobweb.compose.foundation.layout.Box 14 | import com.varabyte.kobweb.compose.foundation.layout.Column 15 | import com.varabyte.kobweb.compose.ui.Alignment 16 | import com.varabyte.kobweb.compose.ui.Modifier 17 | import com.varabyte.kobweb.compose.ui.modifiers.backgroundColor 18 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius 19 | import com.varabyte.kobweb.compose.ui.modifiers.color 20 | import com.varabyte.kobweb.compose.ui.modifiers.cursor 21 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 22 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight 23 | import com.varabyte.kobweb.compose.ui.modifiers.margin 24 | import com.varabyte.kobweb.compose.ui.modifiers.onClick 25 | import com.varabyte.kobweb.compose.ui.modifiers.padding 26 | import com.varabyte.kobweb.compose.ui.modifiers.setVariable 27 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign 28 | import com.varabyte.kobweb.compose.ui.modifiers.width 29 | import com.varabyte.kobweb.compose.ui.toAttrs 30 | import com.varabyte.kobweb.core.rememberPageContext 31 | import com.varabyte.kobweb.silk.components.forms.ButtonVars 32 | import com.varabyte.kobweb.silk.components.navigation.Link 33 | import com.varabyte.kobweb.silk.components.style.ComponentStyle 34 | import com.varabyte.kobweb.silk.components.style.base 35 | import com.varabyte.kobweb.silk.components.style.toModifier 36 | import com.varabyte.kobweb.silk.components.text.SpanText 37 | import org.jetbrains.compose.web.css.cssRem 38 | import org.jetbrains.compose.web.css.percent 39 | import org.jetbrains.compose.web.css.px 40 | import org.jetbrains.compose.web.dom.Button 41 | import org.jetbrains.compose.web.dom.Span 42 | 43 | val FooterStyle by ComponentStyle.base { 44 | Modifier.padding(topBottom = 5.cssRem, leftRight = 10.percent) 45 | } 46 | 47 | @Composable 48 | fun Footer(modifier: Modifier = Modifier) { 49 | val context = rememberPageContext() 50 | val isUserLoggedIn = isUserLoggedIn() 51 | Column( 52 | modifier = FooterStyle.toModifier().then(modifier), 53 | verticalArrangement = Arrangement.Center, 54 | horizontalAlignment = Alignment.CenterHorizontally 55 | ) { 56 | Button( 57 | attrs = ShadowedGreenVariant.toModifier() 58 | .backgroundColor(getSitePalette().green) 59 | .setVariable(ButtonVars.BackgroundDefaultColor, getSitePalette().green) 60 | .setVariable(ButtonVars.BackgroundHoverColor, getSitePalette().green) 61 | .setVariable(ButtonVars.BackgroundPressedColor, getSitePalette().green) 62 | .width(120.px) 63 | .borderRadius(3.cssRem) 64 | .padding(leftRight = 1.cssRem, topBottom = 0.5.cssRem) 65 | .margin(bottom = 1.cssRem) 66 | .noBorder() 67 | .cursor(Cursor.Pointer) 68 | .onClick { 69 | if (isUserLoggedIn) { 70 | context.router.navigateTo(Screen.AdminMyPosts.route) 71 | } else { 72 | context.router.navigateTo(Screen.Login.route) 73 | } 74 | } 75 | .toAttrs(), 76 | ) { 77 | SpanText( 78 | text = "Join us", 79 | modifier = Modifier 80 | .fontSize(16.px) 81 | .color(getSitePalette().white) 82 | ) 83 | } 84 | 85 | Span(Modifier.textAlign(TextAlign.Center).toAttrs()) { 86 | Link("https://bento.me/canerture") { 87 | SpanText( 88 | text = "2024 - Caner Ture", 89 | modifier = Modifier 90 | .color(getSitePalette().green) 91 | .fontWeight(FontWeight.Bold) 92 | ) 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/CategoryChip.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.data.model.Category 5 | import com.canerture.androidhub.data.model.colorParse 6 | import com.canerture.androidhub.getSitePalette 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.backgroundColor 11 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius 12 | import com.varabyte.kobweb.compose.ui.modifiers.color 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.px 18 | 19 | @Composable 20 | fun CategoryChip(category: Category) { 21 | Box( 22 | modifier = Modifier 23 | .height(32.px) 24 | .padding(leftRight = 14.px) 25 | .borderRadius(r = 100.px) 26 | .backgroundColor(colorParse(category.color)), 27 | contentAlignment = Alignment.Center 28 | ) { 29 | SpanText( 30 | modifier = Modifier 31 | .fontSize(12.px) 32 | .color(getSitePalette().white), 33 | text = category.name 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/ErrorView.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.getSitePalette 5 | import com.varabyte.kobweb.compose.css.FontWeight 6 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement 7 | import com.varabyte.kobweb.compose.foundation.layout.Column 8 | import com.varabyte.kobweb.compose.ui.Alignment 9 | import com.varabyte.kobweb.compose.ui.Modifier 10 | import com.varabyte.kobweb.compose.ui.modifiers.color 11 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize 12 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 13 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight 14 | import com.varabyte.kobweb.compose.ui.modifiers.padding 15 | import com.varabyte.kobweb.silk.components.icons.fa.FaX 16 | import com.varabyte.kobweb.silk.components.icons.fa.IconSize 17 | import com.varabyte.kobweb.silk.components.text.SpanText 18 | import org.jetbrains.compose.web.css.px 19 | 20 | @Composable 21 | fun ErrorView( 22 | message: String = "" 23 | ) { 24 | Column( 25 | modifier = Modifier 26 | .fillMaxSize(), 27 | horizontalAlignment = Alignment.CenterHorizontally, 28 | verticalArrangement = Arrangement.Center 29 | ) { 30 | FaX( 31 | size = IconSize.X6, 32 | modifier = Modifier 33 | .padding(bottom = 48.px) 34 | .color(getSitePalette().blue) 35 | ) 36 | 37 | SpanText( 38 | text = message.ifEmpty { "An error occurred while loading the page." }, 39 | modifier = Modifier 40 | .color(getSitePalette().blue) 41 | .fontSize(24.px) 42 | .fontWeight(FontWeight.Bold) 43 | ) 44 | } 45 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/LoadingIndicator.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.LoaderStyle 5 | import com.varabyte.kobweb.compose.foundation.layout.Box 6 | import com.varabyte.kobweb.compose.ui.Alignment 7 | import com.varabyte.kobweb.compose.ui.Modifier 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.silk.components.style.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 | 16 | @Composable 17 | fun LoadingIndicator() { 18 | Box( 19 | modifier = Modifier 20 | .fillMaxSize() 21 | .height(100.vh) 22 | .padding(topBottom = 50.px), 23 | contentAlignment = Alignment.Center 24 | ) { 25 | Div( 26 | attrs = LoaderStyle.toAttrs() 27 | ) 28 | } 29 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/Popup.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.common.Id 5 | import com.canerture.androidhub.common.noBorder 6 | import com.canerture.androidhub.getSitePalette 7 | import com.canerture.androidhub.models.EditorControl 8 | import com.varabyte.kobweb.compose.foundation.layout.Box 9 | import com.varabyte.kobweb.compose.foundation.layout.Column 10 | import com.varabyte.kobweb.compose.ui.Alignment 11 | import com.varabyte.kobweb.compose.ui.Modifier 12 | import com.varabyte.kobweb.compose.ui.graphics.Colors 13 | import com.varabyte.kobweb.compose.ui.modifiers.backgroundColor 14 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius 15 | import com.varabyte.kobweb.compose.ui.modifiers.color 16 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize 17 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 18 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 19 | import com.varabyte.kobweb.compose.ui.modifiers.height 20 | import com.varabyte.kobweb.compose.ui.modifiers.id 21 | import com.varabyte.kobweb.compose.ui.modifiers.margin 22 | import com.varabyte.kobweb.compose.ui.modifiers.onClick 23 | import com.varabyte.kobweb.compose.ui.modifiers.padding 24 | import com.varabyte.kobweb.compose.ui.modifiers.position 25 | import com.varabyte.kobweb.compose.ui.modifiers.width 26 | import com.varabyte.kobweb.compose.ui.modifiers.zIndex 27 | import com.varabyte.kobweb.compose.ui.toAttrs 28 | import com.varabyte.kobweb.silk.components.text.SpanText 29 | import kotlinx.browser.document 30 | import org.jetbrains.compose.web.attributes.InputType 31 | import org.jetbrains.compose.web.css.Position 32 | import org.jetbrains.compose.web.css.px 33 | import org.jetbrains.compose.web.css.vh 34 | import org.jetbrains.compose.web.css.vw 35 | import org.jetbrains.compose.web.dom.Button 36 | import org.jetbrains.compose.web.dom.Input 37 | import org.w3c.dom.HTMLInputElement 38 | 39 | @Composable 40 | fun ControlPopup( 41 | editorControl: EditorControl, 42 | onDialogDismiss: () -> Unit, 43 | onAddClick: (String, String) -> Unit 44 | ) { 45 | Box( 46 | modifier = Modifier 47 | .width(100.vw) 48 | .height(100.vh) 49 | .position(Position.Fixed) 50 | .zIndex(19), 51 | contentAlignment = Alignment.Center 52 | ) { 53 | Box( 54 | modifier = Modifier 55 | .fillMaxSize() 56 | .backgroundColor(getSitePalette().blue) 57 | .onClick { onDialogDismiss() } 58 | ) 59 | Column( 60 | modifier = Modifier 61 | .width(500.px) 62 | .padding(all = 24.px) 63 | .backgroundColor(Colors.White) 64 | .borderRadius(r = 4.px) 65 | ) { 66 | Input( 67 | type = InputType.Text, 68 | attrs = Modifier 69 | .id(Id.LINK_HREF_INPUT) 70 | .fillMaxWidth() 71 | .height(54.px) 72 | .padding(left = 20.px) 73 | .margin(bottom = 12.px) 74 | .fontSize(14.px) 75 | .noBorder() 76 | .borderRadius(r = 4.px) 77 | .backgroundColor(getSitePalette().white) 78 | .toAttrs { 79 | attr( 80 | "placeholder", 81 | if (editorControl == EditorControl.Link) "Href" else "Image URL" 82 | ) 83 | } 84 | ) 85 | Input( 86 | type = InputType.Text, 87 | attrs = Modifier 88 | .id(Id.LINK_TITLE_INPUT) 89 | .fillMaxWidth() 90 | .height(54.px) 91 | .padding(left = 20.px) 92 | .margin(bottom = 20.px) 93 | .fontSize(14.px) 94 | .noBorder() 95 | .borderRadius(r = 4.px) 96 | .backgroundColor(getSitePalette().white) 97 | .toAttrs { 98 | attr( 99 | "placeholder", 100 | if (editorControl == EditorControl.Link) "Title" else "Description" 101 | ) 102 | } 103 | ) 104 | Button( 105 | attrs = Modifier 106 | .onClick { 107 | val href = 108 | (document.getElementById(Id.LINK_HREF_INPUT) as HTMLInputElement).value 109 | val title = 110 | (document.getElementById(Id.LINK_TITLE_INPUT) as HTMLInputElement).value 111 | onAddClick(href, title) 112 | onDialogDismiss() 113 | } 114 | .fillMaxWidth() 115 | .height(54.px) 116 | .borderRadius(r = 4.px) 117 | .backgroundColor(getSitePalette().blue) 118 | .color(Colors.White) 119 | .noBorder() 120 | .fontSize(14.px) 121 | .toAttrs() 122 | ) { 123 | SpanText(text = "Add") 124 | } 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/PostPreview.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.ShadowedGrayVariant 5 | import com.canerture.androidhub.common.parseDateString 6 | import com.canerture.androidhub.data.model.Post 7 | import com.canerture.androidhub.getSitePalette 8 | import com.varabyte.kobweb.compose.css.CSSTransition 9 | import com.varabyte.kobweb.compose.css.Cursor 10 | import com.varabyte.kobweb.compose.css.FontWeight 11 | import com.varabyte.kobweb.compose.css.Overflow 12 | import com.varabyte.kobweb.compose.css.TextOverflow 13 | import com.varabyte.kobweb.compose.css.TransitionProperty 14 | import com.varabyte.kobweb.compose.foundation.layout.Column 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.borderRadius 18 | import com.varabyte.kobweb.compose.ui.modifiers.color 19 | import com.varabyte.kobweb.compose.ui.modifiers.cursor 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.marginBlock 24 | import com.varabyte.kobweb.compose.ui.modifiers.marginInline 25 | import com.varabyte.kobweb.compose.ui.modifiers.onClick 26 | import com.varabyte.kobweb.compose.ui.modifiers.overflow 27 | import com.varabyte.kobweb.compose.ui.modifiers.padding 28 | import com.varabyte.kobweb.compose.ui.modifiers.textOverflow 29 | import com.varabyte.kobweb.compose.ui.modifiers.transition 30 | import com.varabyte.kobweb.silk.components.style.toModifier 31 | import com.varabyte.kobweb.silk.components.text.SpanText 32 | import org.jetbrains.compose.web.css.cssRem 33 | import org.jetbrains.compose.web.css.ms 34 | import org.jetbrains.compose.web.css.px 35 | 36 | @Composable 37 | fun PostPreview( 38 | post: Post, 39 | onClick: (String) -> Unit 40 | ) { 41 | Column( 42 | modifier = ShadowedGrayVariant.toModifier() 43 | .marginInline(end = 1.5.cssRem) 44 | .marginBlock(end = 1.5.cssRem) 45 | .backgroundColor(getSitePalette().white) 46 | .borderRadius(r = 10.px) 47 | .padding(all = 18.px) 48 | .transition(CSSTransition(property = TransitionProperty.All, duration = 400.ms)) 49 | .cursor(Cursor.Pointer) 50 | .onClick { onClick(post.short) } 51 | ) { 52 | SpanText( 53 | modifier = Modifier 54 | .fontSize(12.px) 55 | .color(getSitePalette().blue), 56 | text = post.date.toLong().parseDateString() 57 | ) 58 | SpanText( 59 | modifier = Modifier 60 | .weight(1f) 61 | .margin(topBottom = 12.px) 62 | .fontSize(20.px) 63 | .fontWeight(FontWeight.Bold) 64 | .color(getSitePalette().blue) 65 | .textOverflow(TextOverflow.Ellipsis) 66 | .overflow(Overflow.Hidden), 67 | text = post.title 68 | ) 69 | CategoryChip(category = post.category) 70 | } 71 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/PostsView.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.data.model.Post 5 | import com.varabyte.kobweb.compose.ui.Modifier 6 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 7 | import com.varabyte.kobweb.compose.ui.modifiers.margin 8 | import com.varabyte.kobweb.silk.components.layout.SimpleGrid 9 | import com.varabyte.kobweb.silk.components.layout.numColumns 10 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 11 | import com.varabyte.kobweb.silk.components.style.breakpoint.ResponsiveValues 12 | import org.jetbrains.compose.web.css.percent 13 | import org.jetbrains.compose.web.css.px 14 | 15 | @Composable 16 | fun PostsView( 17 | breakpoint: Breakpoint, 18 | posts: List, 19 | numColumns: ResponsiveValues = numColumns(base = 1, sm = 2, md = 3, lg = 3), 20 | onClick: (String) -> Unit 21 | ) { 22 | SimpleGrid( 23 | modifier = Modifier 24 | .fillMaxWidth( 25 | if (breakpoint > Breakpoint.MD) 80.percent 26 | else 90.percent 27 | ) 28 | .margin(top = 40.px), 29 | numColumns = numColumns 30 | ) { 31 | posts.forEach { 32 | PostPreview( 33 | post = it, 34 | onClick = onClick 35 | ) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/SearchBar.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 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.canerture.androidhub.common.Id 10 | import com.canerture.androidhub.common.noBorder 11 | import com.canerture.androidhub.getSitePalette 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.toAttrs 35 | import com.varabyte.kobweb.silk.components.icons.fa.FaMagnifyingGlass 36 | import com.varabyte.kobweb.silk.components.icons.fa.IconSize 37 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 38 | import org.jetbrains.compose.web.attributes.InputType 39 | import org.jetbrains.compose.web.css.LineStyle 40 | import org.jetbrains.compose.web.css.ms 41 | import org.jetbrains.compose.web.css.px 42 | import org.jetbrains.compose.web.dom.Input 43 | 44 | @Composable 45 | fun SearchBar( 46 | breakpoint: Breakpoint, 47 | modifier: Modifier = Modifier, 48 | onEnterClick: () -> Unit, 49 | onSearchIconClick: (Boolean) -> Unit 50 | ) { 51 | var focused by remember { mutableStateOf(false) } 52 | 53 | LaunchedEffect(breakpoint) { 54 | if (breakpoint >= Breakpoint.SM) onSearchIconClick(false) 55 | } 56 | 57 | if (breakpoint >= Breakpoint.SM) { 58 | Row( 59 | modifier = modifier 60 | .fillMaxWidth() 61 | .padding(left = 20.px) 62 | .height(54.px) 63 | .backgroundColor(getSitePalette().white) 64 | .borderRadius(r = 100.px) 65 | .border( 66 | width = 2.px, 67 | style = LineStyle.Solid, 68 | color = if (focused) getSitePalette().green else getSitePalette().blue 69 | ) 70 | .transition(CSSTransition(property = "border", duration = 200.ms)), 71 | verticalAlignment = Alignment.CenterVertically 72 | ) { 73 | FaMagnifyingGlass( 74 | modifier = Modifier 75 | .margin(right = 14.px) 76 | .color(if (focused) getSitePalette().green else getSitePalette().blue) 77 | .transition(CSSTransition(property = "color", duration = 200.ms)), 78 | size = IconSize.SM 79 | ) 80 | Input( 81 | type = InputType.Text, 82 | attrs = Modifier 83 | .id(Id.ADMIN_SEARCH_BAR) 84 | .fillMaxSize() 85 | .color(getSitePalette().blue) 86 | .backgroundColor(Colors.Transparent) 87 | .noBorder() 88 | .onFocusIn { focused = true } 89 | .onFocusOut { focused = false } 90 | .onKeyDown { 91 | if (it.key == "Enter") { 92 | onEnterClick() 93 | } 94 | } 95 | .toAttrs { 96 | attr("placeholder", "Search...") 97 | } 98 | ) 99 | } 100 | } else { 101 | FaMagnifyingGlass( 102 | modifier = Modifier 103 | .margin(right = 14.px) 104 | .color(getSitePalette().green) 105 | .cursor(Cursor.Pointer) 106 | .onClick { onSearchIconClick(true) }, 107 | size = IconSize.SM 108 | ) 109 | } 110 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/data/CategoryApis.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data 2 | 3 | import com.canerture.androidhub.data.model.Category 4 | import com.canerture.androidhub.utils.ApiUtils.get 5 | import com.canerture.androidhub.utils.ApiUtils.safeApiCall 6 | 7 | suspend fun getCategories( 8 | onSuccess: (List) -> Unit = {}, 9 | onError: (String) -> Unit = {} 10 | ) = safeApiCall>( 11 | call = { get("categories") }, 12 | onSuccess = onSuccess, 13 | onError = onError 14 | ) 15 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/data/PostApis.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data 2 | 3 | import com.canerture.androidhub.common.formatShort 4 | import com.canerture.androidhub.data.model.AddPostRequest 5 | import com.canerture.androidhub.data.model.AuthorDetail 6 | import com.canerture.androidhub.data.model.GetPostsResponse 7 | import com.canerture.androidhub.data.model.Post 8 | import com.canerture.androidhub.data.model.UpdatePostRequest 9 | import com.canerture.androidhub.utils.ApiUtils.delete 10 | import com.canerture.androidhub.utils.ApiUtils.get 11 | import com.canerture.androidhub.utils.ApiUtils.post 12 | import com.canerture.androidhub.utils.ApiUtils.safeApiCall 13 | import kotlinx.browser.localStorage 14 | import kotlinx.serialization.encodeToString 15 | import kotlinx.serialization.json.Json 16 | import org.w3c.dom.get 17 | import kotlin.js.Date 18 | 19 | suspend fun getAdminAllPosts( 20 | onSuccess: (List) -> Unit = {}, 21 | onError: (String) -> Unit = {} 22 | ) = safeApiCall>( 23 | call = { get("admin_all_posts") }, 24 | onSuccess = onSuccess, 25 | onError = onError 26 | ) 27 | 28 | suspend fun getPosts( 29 | page: Int = 0, 30 | onSuccess: (GetPostsResponse) -> Unit = {}, 31 | onError: (String) -> Unit = {} 32 | ) = safeApiCall( 33 | call = { get("posts", "page" to page) }, 34 | onSuccess = onSuccess, 35 | onError = onError 36 | ) 37 | 38 | suspend fun getPopularPosts( 39 | onSuccess: (List) -> Unit = {}, 40 | onError: (String) -> Unit = {} 41 | ) = safeApiCall>( 42 | call = { get("posts", "popular" to "") }, 43 | onSuccess = onSuccess, 44 | onError = onError 45 | ) 46 | 47 | suspend fun getPostDetail( 48 | short: String, 49 | onSuccess: (Post) -> Unit = {}, 50 | onError: (String) -> Unit = {} 51 | ) = safeApiCall( 52 | call = { get("posts", "short" to short) }, 53 | onSuccess = onSuccess, 54 | onError = onError 55 | ) 56 | 57 | suspend fun getPendingPosts( 58 | onSuccess: (List) -> Unit = {}, 59 | onError: (String) -> Unit = {} 60 | ) = safeApiCall>( 61 | call = { get("pending_posts") }, 62 | onSuccess = onSuccess, 63 | onError = onError 64 | ) 65 | 66 | suspend fun getDraftPosts( 67 | onSuccess: (List) -> Unit = {}, 68 | onError: (String) -> Unit = {} 69 | ) = safeApiCall>( 70 | call = { get("draft_posts") }, 71 | onSuccess = onSuccess, 72 | onError = onError 73 | ) 74 | 75 | suspend fun addPost( 76 | content: String, 77 | title: String, 78 | category: String, 79 | thumbnail: String, 80 | status: String, 81 | onSuccess: (String) -> Unit = {}, 82 | onError: (String) -> Unit = {} 83 | ) = safeApiCall( 84 | call = { 85 | val id = localStorage["userId"] ?: "" 86 | val name = localStorage["name"] ?: "" 87 | val short = title.formatShort() 88 | post( 89 | path = "posts", 90 | body = Json.encodeToString( 91 | AddPostRequest(id, name, Date.now(), content, title, short, category, thumbnail, status) 92 | ).encodeToByteArray() 93 | ) 94 | }, 95 | onSuccess = onSuccess, 96 | onError = onError 97 | ) 98 | 99 | suspend fun updatePost( 100 | id: Int, 101 | content: String, 102 | title: String, 103 | category: String, 104 | thumbnail: String, 105 | status: String, 106 | onSuccess: (String) -> Unit = {}, 107 | onError: (String) -> Unit = {} 108 | ) = safeApiCall( 109 | call = { 110 | val short = title.formatShort() 111 | post( 112 | path = "posts", 113 | body = Json.encodeToString( 114 | UpdatePostRequest(id, Date.now(), content, title, short, category, thumbnail, status) 115 | ).encodeToByteArray() 116 | ) 117 | }, 118 | onSuccess = onSuccess, 119 | onError = onError 120 | ) 121 | 122 | suspend fun searchPostsByTitle( 123 | title: String, 124 | onSuccess: (List) -> Unit = {}, 125 | onError: (String) -> Unit = {} 126 | ) = safeApiCall>( 127 | call = { get("posts", "title" to title) }, 128 | onSuccess = onSuccess, 129 | onError = onError 130 | ) 131 | 132 | suspend fun getMyPosts( 133 | onSuccess: (AuthorDetail) -> Unit = {}, 134 | onError: (String) -> Unit = {} 135 | ) = safeApiCall( 136 | call = { get("posts", "authorId" to localStorage["userId"].orEmpty()) }, 137 | onSuccess = onSuccess, 138 | onError = onError 139 | ) 140 | 141 | suspend fun deletePost( 142 | id: Int, 143 | onSuccess: (String) -> Unit = {}, 144 | onError: (String) -> Unit = {} 145 | ) = safeApiCall( 146 | call = { delete("posts", "id" to id) }, 147 | onSuccess = onSuccess, 148 | onError = onError 149 | ) 150 | 151 | suspend fun getPostsByCategory( 152 | category: String, 153 | onSuccess: (List) -> Unit = {}, 154 | onError: (String) -> Unit = {} 155 | ) = safeApiCall>( 156 | call = { get("posts", "category" to category) }, 157 | onSuccess = onSuccess, 158 | onError = onError 159 | ) 160 | 161 | suspend fun getPostsByAuthorId( 162 | authorId: String, 163 | onSuccess: (AuthorDetail) -> Unit = {}, 164 | onError: (String) -> Unit = {} 165 | ) = safeApiCall( 166 | call = { get("posts", "authorId" to authorId) }, 167 | onSuccess = onSuccess, 168 | onError = onError 169 | ) -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/data/UserApis.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data 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.canerture.androidhub.data.model.LoginRequest 10 | import com.canerture.androidhub.data.model.RegisterRequest 11 | import com.canerture.androidhub.data.model.User 12 | import com.canerture.androidhub.utils.ApiUtils.get 13 | import com.canerture.androidhub.utils.ApiUtils.post 14 | import com.canerture.androidhub.utils.ApiUtils.safeApiCall 15 | import kotlinx.browser.localStorage 16 | import kotlinx.serialization.encodeToString 17 | import kotlinx.serialization.json.Json 18 | import org.w3c.dom.get 19 | import org.w3c.dom.set 20 | 21 | suspend fun login( 22 | email: String, 23 | password: String, 24 | onSuccess: (User) -> Unit = {}, 25 | onError: (String) -> Unit = {} 26 | ) = safeApiCall( 27 | call = { 28 | post( 29 | path = "login", 30 | body = Json.encodeToString(LoginRequest(email, password)).encodeToByteArray() 31 | ) 32 | }, 33 | onSuccess = onSuccess, 34 | onError = onError 35 | ) 36 | 37 | suspend fun register( 38 | name: String, 39 | email: String, 40 | password: String, 41 | onSuccess: (User) -> Unit = {}, 42 | onError: (String) -> Unit = {} 43 | ) = safeApiCall( 44 | call = { 45 | post( 46 | path = "register", 47 | body = Json.encodeToString(RegisterRequest(name, email, password)).encodeToByteArray() 48 | ) 49 | }, 50 | onSuccess = onSuccess, 51 | onError = onError 52 | ) 53 | 54 | suspend fun checkUserId( 55 | userId: String, 56 | onSuccess: (Boolean) -> Unit = {}, 57 | onError: (String) -> Unit = {} 58 | ) = safeApiCall( 59 | call = { get("check_user_id", "userId" to userId) }, 60 | onSuccess = onSuccess, 61 | onError = onError 62 | ) 63 | 64 | @Composable 65 | fun isUserLoggedIn(): Boolean { 66 | val remembered = remember { localStorage["remember"].toBoolean() } 67 | val userId = remember { localStorage["userId"] } 68 | var userIdExists by remember { mutableStateOf(null) } 69 | LaunchedEffect(key1 = Unit) { 70 | if (!userId.isNullOrEmpty()) { 71 | checkUserId( 72 | userId = userId, 73 | onSuccess = { 74 | userIdExists = it 75 | }, 76 | onError = { 77 | userIdExists = null 78 | } 79 | ) 80 | } 81 | } 82 | 83 | return remembered && userIdExists ?: false 84 | } 85 | 86 | fun logout() { 87 | localStorage["remember"] = "false" 88 | localStorage["userId"] = "" 89 | localStorage["username"] = "" 90 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/data/model/AuthorDetail.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class AuthorDetail( 7 | val authorName: String, 8 | val posts: List, 9 | ) 10 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/data/model/BaseResponse.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class BaseResponse( 7 | val status: Int? = null, 8 | val message: String? = null, 9 | val data: T? = null 10 | ) { 11 | fun isSuccess() = status == 200 12 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/data/model/Category.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import com.github.ajalt.colormath.model.RGBInt 4 | import com.github.ajalt.colormath.parse 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Category( 9 | val id: Int = 0, 10 | val name: String = "", 11 | val short: String = "", 12 | val color: String = "", 13 | val parentCategory: String = "", 14 | ) 15 | 16 | data class ParentCategory( 17 | val id: Int = 0, 18 | val name: String = "", 19 | val short: String = "", 20 | val color: String = "", 21 | val subCategories: MutableList = mutableListOf(), 22 | ) 23 | 24 | data class SubCategory( 25 | val id: Int = 0, 26 | val name: String = "", 27 | val short: String = "", 28 | val color: String = "", 29 | val parentCategory: String = "", 30 | ) 31 | 32 | fun colorParse(color: String): com.varabyte.kobweb.compose.ui.graphics.Color { 33 | return com.github.ajalt.colormath.Color.parse("#$color").toComposeColor() 34 | } 35 | 36 | fun com.github.ajalt.colormath.Color.toComposeColor(): com.varabyte.kobweb.compose.ui.graphics.Color { 37 | return if (this is RGBInt) { 38 | com.varabyte.kobweb.compose.ui.graphics.Color.rgb(argb.toInt()) 39 | } else { 40 | val (r, g, b, a) = toSRGB() 41 | com.varabyte.kobweb.compose.ui.graphics.Color.rgba(r, g, b, a) 42 | } 43 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/data/model/GetPostsResponse.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class GetPostsResponse( 7 | val totalPage: Int, 8 | val posts: List 9 | ) 10 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/data/model/LoginRequest.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class LoginRequest( 7 | val email: String, 8 | val password: String, 9 | ) 10 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/data/model/Post.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class AddPostRequest( 7 | val authorId: String = "", 8 | val authorName: String = "", 9 | val date: Double = 0.0, 10 | val content: String = "", 11 | val title: String = "", 12 | val short: String = "", 13 | val category: String = "", 14 | val thumbnail: String = "", 15 | val status: String = "", 16 | ) 17 | 18 | @Serializable 19 | data class UpdatePostRequest( 20 | val id: Int = 0, 21 | val date: Double = 0.0, 22 | val content: String = "", 23 | val title: String = "", 24 | val short: String = "", 25 | val category: String = "", 26 | val thumbnail: String = "", 27 | val status: String = "", 28 | ) 29 | 30 | @Serializable 31 | data class Post( 32 | val id: Int = 0, 33 | val authorId: String = "", 34 | val authorName: String = "", 35 | val date: Double = 0.0, 36 | val content: String = "", 37 | val title: String = "", 38 | val short: String = "", 39 | val category: Category = Category(), 40 | val thumbnail: String = "", 41 | val commentCount: Int = 0, 42 | val status: String = "", 43 | ) -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/data/model/RegisterRequest.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class RegisterRequest( 7 | val name: String, 8 | val email: String, 9 | val password: String, 10 | ) 11 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/data/model/User.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class User( 7 | val userId: String, 8 | val name: String, 9 | val email: String, 10 | val isAdmin: Boolean 11 | ) 12 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/models/ControlStyle.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.models 2 | 3 | import com.canerture.androidhub.getSitePalette 4 | 5 | sealed class ControlStyle(val style: String) { 6 | data class Bold(val selectedText: String?) : ControlStyle( 7 | style = "$selectedText" 8 | ) 9 | 10 | data class Italic(val selectedText: String?) : ControlStyle( 11 | style = "$selectedText" 12 | ) 13 | 14 | data class Link( 15 | val selectedText: String?, 16 | val href: String, 17 | val title: String 18 | ) : ControlStyle( 19 | style = "$selectedText" 20 | ) 21 | 22 | data class Title(val selectedText: String?) : ControlStyle( 23 | style = "

$selectedText

" 24 | ) 25 | 26 | data class Subtitle(val selectedText: String?) : ControlStyle( 27 | style = "

$selectedText

" 28 | ) 29 | 30 | data class Quote(val selectedText: String?) : ControlStyle( 31 | style = "
❞ $selectedText
" 32 | ) 33 | 34 | data class Code(val selectedText: String?) : ControlStyle( 35 | style = "
$selectedText
" 36 | ) 37 | 38 | data class Image( 39 | val selectedText: String?, 40 | val imageUrl: String, 41 | val alt: String 42 | ) : ControlStyle( 43 | style = "\"$alt\"$selectedText" 44 | ) 45 | 46 | data class Break(val selectedText: String?) : ControlStyle( 47 | style = "$selectedText
" 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/models/EditorControl.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.models 2 | 3 | import com.canerture.androidhub.common.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-android/src/jsMain/kotlin/com/canerture/androidhub/navigation/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.navigation 2 | 3 | import com.canerture.androidhub.common.Constants.CATEGORY_PARAM 4 | import com.canerture.androidhub.common.Constants.POST_AUTHOR_ID_PARAM 5 | import com.canerture.androidhub.common.Constants.POST_SHORT_PARAM 6 | import com.canerture.androidhub.common.Constants.QUERY_PARAM 7 | 8 | sealed class Screen(val route: String) { 9 | data object HomePage : Screen(route = "/") 10 | 11 | data object PostPage : Screen(route = "/posts") { 12 | fun getPost(short: String) = "/posts?${POST_SHORT_PARAM}=$short" 13 | } 14 | 15 | data object AuthorPage : Screen(route = "/author") { 16 | fun getAuthor(authorId: String) = "/author?${POST_AUTHOR_ID_PARAM}=$authorId" 17 | } 18 | 19 | data object CategoryPage : Screen(route = "/category") { 20 | fun getCategoryPosts(category: String) = "/category?${CATEGORY_PARAM}=${category}" 21 | } 22 | 23 | data object Login : Screen(route = "/login") 24 | 25 | data object AdminCreate : Screen(route = "/admin/create-post") { 26 | fun passPostId(short: String) = "/admin/create-post?${POST_SHORT_PARAM}=$short" 27 | } 28 | 29 | data object AdminMyPosts : Screen(route = "/admin/my-posts") 30 | 31 | data object AdminAllPosts : Screen(route = "/admin/all-posts") 32 | 33 | data object SearchPage : Screen(route = "/search") { 34 | fun searchByTitle(query: String) = "/search?${QUERY_PARAM}=$query" 35 | } 36 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/pages/adminallposts/AdminAllPosts.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.adminallposts 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.canerture.androidhub.common.Constants.SIDE_PANEL_WIDTH 10 | import com.canerture.androidhub.components.layouts.AdminPageLayout 11 | import com.canerture.androidhub.components.widgets.PostsView 12 | import com.canerture.androidhub.data.getAdminAllPosts 13 | import com.canerture.androidhub.data.getDraftPosts 14 | import com.canerture.androidhub.data.getPendingPosts 15 | import com.canerture.androidhub.data.model.Post 16 | import com.canerture.androidhub.getSitePalette 17 | import com.canerture.androidhub.navigation.Screen 18 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement 19 | import com.varabyte.kobweb.compose.foundation.layout.Column 20 | import com.varabyte.kobweb.compose.foundation.layout.Row 21 | import com.varabyte.kobweb.compose.ui.Alignment 22 | import com.varabyte.kobweb.compose.ui.Modifier 23 | import com.varabyte.kobweb.compose.ui.modifiers.color 24 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize 25 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 26 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 27 | import com.varabyte.kobweb.compose.ui.modifiers.margin 28 | import com.varabyte.kobweb.compose.ui.modifiers.padding 29 | import com.varabyte.kobweb.core.Page 30 | import com.varabyte.kobweb.core.rememberPageContext 31 | import com.varabyte.kobweb.silk.components.forms.Switch 32 | import com.varabyte.kobweb.silk.components.forms.SwitchSize 33 | import com.varabyte.kobweb.silk.components.layout.numColumns 34 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 35 | import com.varabyte.kobweb.silk.components.text.SpanText 36 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint 37 | import org.jetbrains.compose.web.css.percent 38 | import org.jetbrains.compose.web.css.px 39 | 40 | data class AllPostsUIState( 41 | var isLoading: Boolean = true, 42 | var posts: List = emptyList(), 43 | var pendingPostEnabled: Boolean = false, 44 | var draftPostEnabled: Boolean = false, 45 | ) 46 | 47 | @Page("/admin/all-posts") 48 | @Composable 49 | fun AdminAllPosts() { 50 | val context = rememberPageContext() 51 | val breakpoint = rememberBreakpoint() 52 | 53 | var state by remember { mutableStateOf(AllPostsUIState()) } 54 | 55 | LaunchedEffect(state.pendingPostEnabled || state.draftPostEnabled) { 56 | when { 57 | state.pendingPostEnabled -> getPendingPosts( 58 | onSuccess = { 59 | state = state.copy(isLoading = false, posts = it) 60 | } 61 | ) 62 | 63 | state.draftPostEnabled -> getDraftPosts( 64 | onSuccess = { 65 | state = state.copy(isLoading = false, posts = it) 66 | } 67 | ) 68 | 69 | else -> getAdminAllPosts( 70 | onSuccess = { 71 | state = state.copy(isLoading = false, posts = it) 72 | } 73 | ) 74 | } 75 | } 76 | 77 | AdminPageLayout { 78 | Column( 79 | modifier = Modifier 80 | .margin(topBottom = 60.px) 81 | .fillMaxSize() 82 | .padding(left = if (breakpoint > Breakpoint.MD) SIDE_PANEL_WIDTH.px else 0.px), 83 | verticalArrangement = Arrangement.Top, 84 | horizontalAlignment = Alignment.CenterHorizontally 85 | ) { 86 | Row( 87 | modifier = Modifier 88 | .fillMaxWidth( 89 | if (breakpoint > Breakpoint.MD) 80.percent 90 | else 90.percent 91 | ) 92 | .margin(top = 80.px), 93 | horizontalArrangement = Arrangement.Start, 94 | verticalAlignment = Alignment.CenterVertically 95 | ) { 96 | Switch( 97 | modifier = Modifier.margin(right = 8.px), 98 | checked = state.pendingPostEnabled, 99 | onCheckedChange = { 100 | state = state.copy(pendingPostEnabled = it) 101 | if (it) { 102 | state = state.copy(draftPostEnabled = false) 103 | } 104 | }, 105 | size = SwitchSize.MD 106 | ) 107 | SpanText( 108 | modifier = Modifier 109 | .fontSize(14.px) 110 | .color(getSitePalette().blue), 111 | text = "Show Pending Posts" 112 | ) 113 | 114 | Switch( 115 | modifier = Modifier.margin(leftRight = 8.px), 116 | checked = state.draftPostEnabled, 117 | onCheckedChange = { 118 | state = state.copy(draftPostEnabled = it) 119 | if (it) { 120 | state = state.copy(pendingPostEnabled = false) 121 | } 122 | }, 123 | size = SwitchSize.MD 124 | ) 125 | SpanText( 126 | modifier = Modifier 127 | .fontSize(14.px) 128 | .color(getSitePalette().blue), 129 | text = "Show Draft Posts" 130 | ) 131 | } 132 | PostsView( 133 | breakpoint = breakpoint, 134 | posts = state.posts, 135 | numColumns = numColumns(base = 1, sm = 1, md = 2, lg = 2), 136 | onClick = { context.router.navigateTo(Screen.AdminCreate.passPostId(short = it)) } 137 | ) 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/pages/admincreatepost/Functions.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.admincreatepost 2 | 3 | import com.canerture.androidhub.common.Id 4 | import com.canerture.androidhub.models.ControlStyle 5 | import com.canerture.androidhub.models.EditorControl 6 | import kotlinx.browser.document 7 | import org.w3c.dom.HTMLTextAreaElement 8 | 9 | fun getEditor() = document.getElementById(Id.EDITOR) as HTMLTextAreaElement 10 | 11 | fun getSelectedIntRange(): IntRange? { 12 | val editor = getEditor() 13 | val start = editor.selectionStart 14 | val end = editor.selectionEnd 15 | return if (start != null && end != null) { 16 | IntRange(start, (end - 1)) 17 | } else null 18 | } 19 | 20 | fun getSelectedText(): String? { 21 | val range = getSelectedIntRange() 22 | return if (range != null) { 23 | getEditor().value.substring(range) 24 | } else null 25 | } 26 | 27 | fun applyStyle(controlStyle: ControlStyle) { 28 | val selectedText = getSelectedText() 29 | val selectedIntRange = getSelectedIntRange() 30 | if (selectedIntRange != null && selectedText != null) { 31 | getEditor().value = getEditor().value.replaceRange( 32 | range = selectedIntRange, 33 | replacement = controlStyle.style 34 | ) 35 | document.getElementById(Id.EDITOR_PREVIEW)?.innerHTML = getEditor().value 36 | } 37 | } 38 | 39 | fun applyControlStyle( 40 | editorControl: EditorControl, 41 | onLinkClick: () -> Unit, 42 | onImageClick: () -> Unit 43 | ) { 44 | when (editorControl) { 45 | EditorControl.Bold -> { 46 | applyStyle( 47 | ControlStyle.Bold( 48 | selectedText = getSelectedText() 49 | ) 50 | ) 51 | } 52 | 53 | EditorControl.Italic -> { 54 | applyStyle( 55 | ControlStyle.Italic( 56 | selectedText = getSelectedText() 57 | ) 58 | ) 59 | } 60 | 61 | EditorControl.Link -> { 62 | onLinkClick() 63 | } 64 | 65 | EditorControl.Title -> { 66 | applyStyle( 67 | ControlStyle.Title( 68 | selectedText = getSelectedText() 69 | ) 70 | ) 71 | } 72 | 73 | EditorControl.Subtitle -> { 74 | applyStyle( 75 | ControlStyle.Subtitle( 76 | selectedText = getSelectedText(), 77 | ) 78 | ) 79 | } 80 | 81 | EditorControl.Quote -> { 82 | applyStyle( 83 | ControlStyle.Quote( 84 | selectedText = getSelectedText() 85 | ) 86 | ) 87 | } 88 | 89 | EditorControl.Code -> { 90 | applyStyle( 91 | ControlStyle.Code( 92 | selectedText = getSelectedText() 93 | ) 94 | ) 95 | } 96 | 97 | EditorControl.Image -> { 98 | onImageClick() 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/pages/author/Author.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.author 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.canerture.androidhub.common.Constants.POST_AUTHOR_ID_PARAM 10 | import com.canerture.androidhub.components.layouts.PageLayout 11 | import com.canerture.androidhub.components.widgets.ErrorView 12 | import com.canerture.androidhub.components.widgets.LoadingIndicator 13 | import com.canerture.androidhub.data.getPostsByAuthorId 14 | import com.canerture.androidhub.data.model.Post 15 | import com.canerture.androidhub.getSitePalette 16 | import com.canerture.androidhub.pages.index.LatestArticleItem 17 | import com.varabyte.kobweb.compose.css.FontWeight 18 | import com.varabyte.kobweb.compose.css.TextAlign 19 | import com.varabyte.kobweb.compose.foundation.layout.Row 20 | import com.varabyte.kobweb.compose.ui.Alignment 21 | import com.varabyte.kobweb.compose.ui.Modifier 22 | import com.varabyte.kobweb.compose.ui.modifiers.alignContent 23 | import com.varabyte.kobweb.compose.ui.modifiers.color 24 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 25 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 26 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight 27 | import com.varabyte.kobweb.compose.ui.modifiers.height 28 | import com.varabyte.kobweb.compose.ui.modifiers.margin 29 | import com.varabyte.kobweb.compose.ui.modifiers.padding 30 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign 31 | import com.varabyte.kobweb.core.Page 32 | import com.varabyte.kobweb.core.rememberPageContext 33 | import com.varabyte.kobweb.silk.components.icons.fa.FaBoltLightning 34 | import com.varabyte.kobweb.silk.components.layout.SimpleGrid 35 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 36 | import com.varabyte.kobweb.silk.components.style.breakpoint.ResponsiveValues 37 | import com.varabyte.kobweb.silk.components.text.SpanText 38 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint 39 | import kotlinx.browser.document 40 | import org.jetbrains.compose.web.css.AlignContent 41 | import org.jetbrains.compose.web.css.cssRem 42 | import org.jetbrains.compose.web.css.px 43 | 44 | data class AuthorUIState( 45 | val isLoading: Boolean = true, 46 | val posts: List = emptyList(), 47 | val authorId: String = "", 48 | val authorName: String = "", 49 | val isError: Boolean = false 50 | ) 51 | 52 | @Page("/author") 53 | @Composable 54 | fun Author() { 55 | val context = rememberPageContext() 56 | 57 | var state by remember { mutableStateOf(AuthorUIState()) } 58 | 59 | state = state.copy(authorId = context.route.queryParams[POST_AUTHOR_ID_PARAM].orEmpty()) 60 | 61 | LaunchedEffect(state.authorId) { 62 | state = state.copy(isLoading = true) 63 | document.title = state.authorName 64 | getPostsByAuthorId( 65 | authorId = state.authorId, 66 | onSuccess = { 67 | state = state.copy(isLoading = false, authorName = it.authorName, posts = it.posts) 68 | }, 69 | onError = { 70 | state = state.copy(isLoading = false, isError = true) 71 | } 72 | ) 73 | } 74 | 75 | if (state.isLoading) { 76 | LoadingIndicator() 77 | } else { 78 | PageLayout("Home") { 79 | if (state.isError) { 80 | ErrorView() 81 | return@PageLayout 82 | } 83 | 84 | if (state.posts.isNotEmpty()) { 85 | AuthorPosts(state.authorName, state.posts) 86 | } 87 | } 88 | } 89 | } 90 | 91 | @Composable 92 | private fun AuthorPosts(authorName: String, list: List) { 93 | val breakpoint = rememberBreakpoint() 94 | Row( 95 | modifier = Modifier 96 | .fillMaxWidth() 97 | .padding(bottom = 2.cssRem, top = if (breakpoint < Breakpoint.MD) 1.cssRem else 6.cssRem, leftRight = 6.cssRem), 98 | verticalAlignment = Alignment.CenterVertically 99 | ) { 100 | FaBoltLightning( 101 | modifier = Modifier 102 | .height(20.px) 103 | .color(getSitePalette().green) 104 | .margin(right = 0.5.cssRem) 105 | .alignContent(AlignContent.Start), 106 | ) 107 | SpanText( 108 | text = authorName, 109 | modifier = Modifier 110 | .textAlign(TextAlign.Center) 111 | .fontWeight(FontWeight.Bold) 112 | .fontSize(1.25.cssRem) 113 | .color(getSitePalette().blue.toRgb()) 114 | ) 115 | } 116 | 117 | SimpleGrid( 118 | modifier = Modifier.fillMaxWidth().padding(leftRight = if (breakpoint < Breakpoint.MD) 1.cssRem else 6.cssRem), 119 | numColumns = ResponsiveValues(1, 1, 3, 3, 3), 120 | content = { 121 | list.forEach { article -> 122 | LatestArticleItem( 123 | article = article 124 | ) 125 | } 126 | } 127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/pages/category/Category.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.category 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.canerture.androidhub.components.layouts.PageLayout 10 | import com.canerture.androidhub.components.widgets.ErrorView 11 | import com.canerture.androidhub.components.widgets.LoadingIndicator 12 | import com.canerture.androidhub.data.getPostsByCategory 13 | import com.canerture.androidhub.data.model.Post 14 | import com.canerture.androidhub.getSitePalette 15 | import com.canerture.androidhub.pages.index.LatestArticleItem 16 | import com.varabyte.kobweb.compose.css.FontWeight 17 | import com.varabyte.kobweb.compose.css.TextAlign 18 | import com.varabyte.kobweb.compose.foundation.layout.Row 19 | import com.varabyte.kobweb.compose.ui.Alignment 20 | import com.varabyte.kobweb.compose.ui.Modifier 21 | import com.varabyte.kobweb.compose.ui.modifiers.alignContent 22 | import com.varabyte.kobweb.compose.ui.modifiers.color 23 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 24 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 25 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight 26 | import com.varabyte.kobweb.compose.ui.modifiers.height 27 | import com.varabyte.kobweb.compose.ui.modifiers.margin 28 | import com.varabyte.kobweb.compose.ui.modifiers.padding 29 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign 30 | import com.varabyte.kobweb.core.Page 31 | import com.varabyte.kobweb.core.rememberPageContext 32 | import com.varabyte.kobweb.silk.components.icons.fa.FaBoltLightning 33 | import com.varabyte.kobweb.silk.components.layout.SimpleGrid 34 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 35 | import com.varabyte.kobweb.silk.components.style.breakpoint.ResponsiveValues 36 | import com.varabyte.kobweb.silk.components.text.SpanText 37 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint 38 | import kotlinx.browser.document 39 | import org.jetbrains.compose.web.css.AlignContent 40 | import org.jetbrains.compose.web.css.cssRem 41 | import org.jetbrains.compose.web.css.px 42 | 43 | data class CategoryUIState( 44 | val isLoading: Boolean = true, 45 | val posts: List = emptyList(), 46 | val category: String = "", 47 | val isError: Boolean = false 48 | ) 49 | 50 | @Page("/category") 51 | @Composable 52 | fun Category() { 53 | val context = rememberPageContext() 54 | 55 | var state by remember { mutableStateOf(CategoryUIState()) } 56 | 57 | state = state.copy(category = context.route.queryParams["category"].orEmpty()) 58 | 59 | LaunchedEffect(state.category) { 60 | state = state.copy(isLoading = true) 61 | document.title = state.category 62 | getPostsByCategory( 63 | category = state.category, 64 | onSuccess = { posts -> 65 | state = CategoryUIState( 66 | isLoading = false, 67 | posts = posts 68 | ) 69 | }, 70 | onError = { 71 | state = state.copy(isLoading = false, isError = true) 72 | } 73 | ) 74 | } 75 | 76 | if (state.isLoading) { 77 | LoadingIndicator() 78 | } else { 79 | PageLayout("Home") { 80 | if (state.isError) { 81 | ErrorView() 82 | return@PageLayout 83 | } 84 | 85 | if (state.posts.isNotEmpty()) { 86 | CategoryPosts(state.category, state.posts) 87 | } 88 | } 89 | } 90 | } 91 | 92 | @Composable 93 | private fun CategoryPosts(category: String, list: List) { 94 | val breakpoint = rememberBreakpoint() 95 | Row( 96 | modifier = Modifier 97 | .fillMaxWidth() 98 | .padding(bottom = 2.cssRem, top = if (breakpoint < Breakpoint.MD) 1.cssRem else 6.cssRem, leftRight = 6.cssRem), 99 | verticalAlignment = Alignment.CenterVertically 100 | ) { 101 | FaBoltLightning( 102 | modifier = Modifier 103 | .height(20.px) 104 | .color(getSitePalette().green) 105 | .margin(right = 0.5.cssRem) 106 | .alignContent(AlignContent.Start), 107 | ) 108 | SpanText( 109 | text = category, 110 | modifier = Modifier 111 | .textAlign(TextAlign.Center) 112 | .fontWeight(FontWeight.Bold) 113 | .fontSize(1.25.cssRem) 114 | .color(getSitePalette().blue.toRgb()) 115 | ) 116 | } 117 | 118 | SimpleGrid( 119 | modifier = Modifier.fillMaxWidth().padding(leftRight = if (breakpoint < Breakpoint.MD) 1.cssRem else 6.cssRem), 120 | numColumns = ResponsiveValues(1, 2, 3, 3, 3), 121 | content = { 122 | list.forEach { article -> 123 | LatestArticleItem( 124 | article = article 125 | ) 126 | } 127 | } 128 | ) 129 | } 130 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/pages/index/AndroidHeroContent.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.index 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.common.Res 5 | import com.canerture.androidhub.getSitePalette 6 | import com.varabyte.kobweb.compose.css.AlignContent 7 | import com.varabyte.kobweb.compose.css.FontWeight 8 | import com.varabyte.kobweb.compose.css.TextAlign 9 | import com.varabyte.kobweb.compose.foundation.layout.Column 10 | import com.varabyte.kobweb.compose.foundation.layout.Row 11 | import com.varabyte.kobweb.compose.foundation.layout.Spacer 12 | import com.varabyte.kobweb.compose.ui.Alignment 13 | import com.varabyte.kobweb.compose.ui.Modifier 14 | import com.varabyte.kobweb.compose.ui.modifiers.alignContent 15 | import com.varabyte.kobweb.compose.ui.modifiers.color 16 | import com.varabyte.kobweb.compose.ui.modifiers.display 17 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 18 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight 19 | import com.varabyte.kobweb.compose.ui.modifiers.lineHeight 20 | import com.varabyte.kobweb.compose.ui.modifiers.padding 21 | import com.varabyte.kobweb.compose.ui.modifiers.size 22 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign 23 | import com.varabyte.kobweb.silk.components.graphics.Image 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.Color 27 | import org.jetbrains.compose.web.css.DisplayStyle 28 | import org.jetbrains.compose.web.css.cssRem 29 | import org.jetbrains.compose.web.css.px 30 | 31 | @Composable 32 | fun AndroidHeroContent( 33 | breakpoint: Breakpoint 34 | ) { 35 | if (breakpoint < Breakpoint.SM) { 36 | Column( 37 | horizontalAlignment = Alignment.CenterHorizontally 38 | ) { 39 | Image( 40 | src = Res.Image.ANDROID_FIGURE, 41 | description = "Android Figure", 42 | modifier = Modifier 43 | .size(240.px) 44 | .display(DisplayStyle.Block) 45 | ) 46 | TextArea( 47 | breakpoint = breakpoint, 48 | modifier = Modifier.padding(leftRight = 2.cssRem).alignContent(AlignContent.Center) 49 | ) 50 | } 51 | } else { 52 | Row( 53 | modifier = Modifier.padding(leftRight = 10.cssRem), 54 | verticalAlignment = Alignment.CenterVertically 55 | ) { 56 | TextArea(breakpoint) 57 | Image( 58 | src = Res.Image.ANDROID_FIGURE, 59 | description = "Android Figure", 60 | modifier = Modifier 61 | .size(400.px) 62 | .display(DisplayStyle.Block) 63 | ) 64 | } 65 | } 66 | } 67 | 68 | @Composable 69 | private fun TextArea(breakpoint: Breakpoint, modifier: Modifier = Modifier) { 70 | Column( 71 | modifier = modifier 72 | ) { 73 | SpanText( 74 | text = "Hi \uD83D\uDC4B", 75 | modifier = Modifier 76 | .fontWeight(FontWeight.Bold) 77 | .textAlign(TextAlign.Start) 78 | .color(getSitePalette().green) 79 | .fontSize(if (breakpoint < Breakpoint.MD) 18.px else 28.px) 80 | ) 81 | Spacer() 82 | SpanText( 83 | text = "Let’s explore everything about the exciting world of Android together. On this platform, you can delve into the vast opportunities offered by the Android operating system and examine the latest developments in the field. Come on, let’s discover the limitless world of Android together!", 84 | modifier = Modifier 85 | .textAlign(TextAlign.Start) 86 | .color(Color.gray) 87 | .fontSize(if (breakpoint < Breakpoint.MD) 14.px else 20.px) 88 | .lineHeight(2) 89 | .padding(right = 2.cssRem) 90 | ) 91 | } 92 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/pages/search/Search.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.search 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.canerture.androidhub.components.layouts.PageLayout 10 | import com.canerture.androidhub.components.widgets.ErrorView 11 | import com.canerture.androidhub.components.widgets.LoadingIndicator 12 | import com.canerture.androidhub.data.model.Post 13 | import com.canerture.androidhub.data.searchPostsByTitle 14 | import com.canerture.androidhub.getSitePalette 15 | import com.canerture.androidhub.pages.index.LatestArticleItem 16 | import com.varabyte.kobweb.compose.css.FontWeight 17 | import com.varabyte.kobweb.compose.css.TextAlign 18 | import com.varabyte.kobweb.compose.foundation.layout.Row 19 | import com.varabyte.kobweb.compose.ui.Alignment 20 | import com.varabyte.kobweb.compose.ui.Modifier 21 | import com.varabyte.kobweb.compose.ui.modifiers.alignContent 22 | import com.varabyte.kobweb.compose.ui.modifiers.color 23 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 24 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 25 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight 26 | import com.varabyte.kobweb.compose.ui.modifiers.height 27 | import com.varabyte.kobweb.compose.ui.modifiers.margin 28 | import com.varabyte.kobweb.compose.ui.modifiers.padding 29 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign 30 | import com.varabyte.kobweb.core.Page 31 | import com.varabyte.kobweb.core.rememberPageContext 32 | import com.varabyte.kobweb.silk.components.icons.fa.FaBoltLightning 33 | import com.varabyte.kobweb.silk.components.layout.SimpleGrid 34 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 35 | import com.varabyte.kobweb.silk.components.style.breakpoint.ResponsiveValues 36 | import com.varabyte.kobweb.silk.components.text.SpanText 37 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint 38 | import kotlinx.browser.document 39 | import org.jetbrains.compose.web.css.AlignContent 40 | import org.jetbrains.compose.web.css.cssRem 41 | import org.jetbrains.compose.web.css.px 42 | 43 | data class SearchUIState( 44 | val isLoading: Boolean = true, 45 | val posts: List = emptyList(), 46 | val query: String = "", 47 | val isError: Boolean = false, 48 | ) 49 | 50 | @Page("/search") 51 | @Composable 52 | fun Search() { 53 | val context = rememberPageContext() 54 | 55 | var state by remember { mutableStateOf(SearchUIState()) } 56 | 57 | state = state.copy(query = context.route.queryParams["query"].orEmpty()) 58 | 59 | LaunchedEffect(state.query) { 60 | state = state.copy(isLoading = true) 61 | document.title = state.query 62 | searchPostsByTitle( 63 | title = state.query, 64 | onSuccess = { posts -> 65 | state = SearchUIState( 66 | isLoading = false, 67 | posts = posts 68 | ) 69 | }, 70 | onError = { 71 | state = state.copy( 72 | isLoading = false, 73 | isError = true 74 | ) 75 | } 76 | ) 77 | } 78 | 79 | if (state.isLoading) { 80 | LoadingIndicator() 81 | } else { 82 | PageLayout("Search") { 83 | if (state.isError) { 84 | ErrorView() 85 | return@PageLayout 86 | } 87 | 88 | if (state.posts.isNotEmpty()) { 89 | SearchPosts(state.query, state.posts) 90 | } 91 | } 92 | } 93 | } 94 | 95 | @Composable 96 | private fun SearchPosts(query: String, list: List) { 97 | val breakpoint = rememberBreakpoint() 98 | Row( 99 | modifier = Modifier 100 | .fillMaxWidth() 101 | .padding(bottom = 2.cssRem, top = if (breakpoint < Breakpoint.MD) 1.cssRem else 6.cssRem, leftRight = 6.cssRem), 102 | verticalAlignment = Alignment.CenterVertically 103 | ) { 104 | FaBoltLightning( 105 | modifier = Modifier 106 | .height(if (breakpoint < Breakpoint.MD) 10.px else 20.px) 107 | .color(getSitePalette().green) 108 | .margin(right = 0.5.cssRem) 109 | .alignContent(AlignContent.Center), 110 | ) 111 | SpanText( 112 | text = "${list.size} posts found for \"$query\"", 113 | modifier = Modifier 114 | .textAlign(TextAlign.Center) 115 | .fontWeight(FontWeight.Bold) 116 | .fontSize(if (breakpoint < Breakpoint.MD) 14.px else 18.px) 117 | .color(getSitePalette().blue.toRgb()) 118 | ) 119 | } 120 | 121 | SimpleGrid( 122 | modifier = Modifier.fillMaxWidth().padding(leftRight = if (breakpoint < Breakpoint.MD) 1.cssRem else 6.cssRem), 123 | numColumns = ResponsiveValues(1, 1, 3, 3, 3), 124 | content = { 125 | list.forEach { article -> 126 | LatestArticleItem( 127 | article = article 128 | ) 129 | } 130 | } 131 | ) 132 | } 133 | -------------------------------------------------------------------------------- /site-android/src/jsMain/kotlin/com/canerture/androidhub/utils/ApiUtils.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.utils 2 | 3 | import com.canerture.androidhub.common.Constants 4 | import com.canerture.androidhub.data.model.BaseResponse 5 | import com.varabyte.kobweb.browser.http.http 6 | import kotlinx.browser.window 7 | import kotlinx.serialization.decodeFromString 8 | import kotlinx.serialization.json.Json 9 | 10 | object ApiUtils { 11 | inline fun safeApiCall( 12 | call: () -> ByteArray, 13 | onSuccess: (T) -> Unit, 14 | onError: (String) -> Unit, 15 | ) { 16 | try { 17 | val response = call().parseData>() 18 | if (response.status == 200) { 19 | onSuccess(response.data!!) 20 | } else { 21 | onError(response.message.orEmpty()) 22 | } 23 | } catch (e: Exception) { 24 | onError(e.message.orEmpty()) 25 | } 26 | } 27 | 28 | suspend fun get(path: String, param: Pair? = null): ByteArray { 29 | val url = if (param == null) { 30 | Constants.BASE_URL.plus(path) 31 | } else { 32 | Constants.BASE_URL.plus(path).plus("?${param.first}=${param.second}") 33 | } 34 | return window.http.get(url) 35 | } 36 | 37 | suspend fun post(path: String, body: ByteArray): ByteArray { 38 | return window.http.post( 39 | resource = Constants.BASE_URL.plus(path), 40 | body = body 41 | ) 42 | } 43 | 44 | suspend fun put(path: String, body: ByteArray): ByteArray { 45 | return window.http.put( 46 | resource = Constants.BASE_URL.plus(path), 47 | body = body 48 | ) 49 | } 50 | 51 | suspend fun delete(path: String, param: Pair? = null): ByteArray { 52 | val url = if (param == null) { 53 | Constants.BASE_URL.plus(path) 54 | } else { 55 | Constants.BASE_URL.plus(path).plus("?${param.first}=${param.second}") 56 | } 57 | return window.http.delete(url) 58 | } 59 | 60 | inline fun ByteArray?.parseData(): T { 61 | val json = this?.decodeToString().orEmpty() 62 | return Json.decodeFromString(json) 63 | } 64 | } -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/markdown/About.md: -------------------------------------------------------------------------------- 1 | --- 2 | root: .components.layouts.MarkdownLayout("About") 3 | --- 4 | 5 | # About this template 6 | 7 | This template is intended to both demonstrate some fundamentals of the Kobweb framework and act a 8 | starting point you can 9 | build your own site from. 10 | 11 | --- 12 | 13 | ## Learn 14 | 15 | If this is your first time using Kobweb, please open this site's project in an IDE and take a few 16 | minutes to look around 17 | the code. 18 | 19 | ### Files 20 | 21 | #### AppEntry.kt 22 | 23 | This file declares a method that is an entry point for all pages on your site. You can rename the 24 | file and the method if 25 | you like. Kobweb searches for a single method at compile time annotated with `@App`. 26 | 27 | #### AppStyles.kt 28 | 29 | An example of declaring generally useful styles that can be used anywhere across the whole site. 30 | Otherwise, you normally 31 | declare styles close to the widget that uses them. 32 | 33 | #### SiteTheme.kt 34 | 35 | An example of how to define some site-specific colors, effectively extending the palette provided by 36 | Silk. 37 | 38 | #### components/ 39 | 40 | By convention, Kobweb codebases organize reusable site components under this folder. Within it, you 41 | have: 42 | 43 | * `layout/`
44 | Represents top-level organization for pages 45 | * `sections/`
46 | Areas of content that appear across multiple pages (such as nav bars and footers) 47 | * `widgets/`
48 | Home for low-level UI pieces that you can use around your site 49 | 50 | #### pages/ 51 | 52 | Any `@Composable` under this folder additionally tagged with `@Page` will have a route generated for 53 | it automatically. 54 | Defining a page outside of this folder will be flagged as an error by the Kobweb Gradle plugin at 55 | compile time. Note 56 | that additional pages (like this one!) might live under the `resources/markdown` folder. 57 | 58 | #### resources/ 59 | 60 | * `public`
61 | If you want to host any media on your site (such as an icon, an image, text configuration files, 62 | movies, fonts, etc.), 63 | you should put it under this folder. 64 | * `markdown`
65 | Any markdown discovered in here by Kobweb at compile time will be converted into pages on your 66 | site. 67 | 68 | ### Classes 69 | 70 | The Kobweb and Silk APIs introduce a lot of powerful concepts. Here are some of the most important 71 | ones to know about 72 | which you can find used throughout this template. 73 | 74 | #### Modifier 75 | 76 | Kobweb introduces the `Modifier` keyword that Android developers will recognize from the Jetpack 77 | Compose API. In a 78 | webdev context, this is used for setting CSS styles and html attributes on elements in the page. 79 | 80 | #### ComponentStyle 81 | 82 | Traditional HTML pages use CSS to style their UI. In Kobweb, these styles can be declared using 83 | the `ComponentStyle` 84 | class in a Kotlin-idiomatic way. You can find examples of component styles used throughout the 85 | template. 86 | 87 | #### ComponentVariant 88 | 89 | You can generate variants from component styles, which are ways to take base component styles and 90 | tweak them further. 91 | 92 | #### Keyframes 93 | 94 | You can create animations by declaring keyframes for them. 95 | 96 | ## Starting Point 97 | 98 | This template aims to create some generally useful pieces that most sites will want to use. Making 99 | your own site could 100 | be as easy as deleting this *About* page and working from there. However, you are welcome to modify 101 | or delete anything 102 | you find in the template that you don't plan to use in your final site. 103 | 104 | If instead you'd like to start from scratch, you can run 105 | 106 | ``` 107 | $ kobweb create app/empty 108 | ``` 109 | 110 | which will create a new project with nothing inside of it except for a minimal, skeletal structure. 111 | 112 | ## Export and Deploy 113 | 114 | When you are ready to share your site with the world, you'll want to export it first. This will 115 | create a production 116 | snapshot of your site. 117 | 118 | There are two flavors of Kobweb sites: *static layout* and *full stack*. You 119 | can [read more about these choices here](https://github.com/varabyte/kobweb#static-layout-vs-full-stack-sites). 120 | 121 | For most sites, a static layout site is what you want, so to do that, return to the command line and 122 | run: 123 | 124 | ``` 125 | $ kobweb export --layout static 126 | ``` 127 | 128 | After that runs for a little while, your production site should be generated! You can find the files 129 | under the 130 | `.kobweb/site` folder. 131 | 132 | Test it locally by running: 133 | 134 | ``` 135 | $ kobweb run --layout static --env prod 136 | ``` 137 | 138 | If you're satisfied, you can upload your site files to the static website host provider of your 139 | choice. Each provider 140 | has its own instructions for how it discovers your files, so please refer to their documentation. 141 | 142 | You can [read this blog post](https://bitspittle.dev/blog/2022/staticdeploy) for some concrete 143 | examples of exporting a 144 | Kobweb site to two popular static website hosting providers. 145 | -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/public/android-figure.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/src/jsMain/resources/public/android-figure.webp -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/public/androidhub-logo-dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/src/jsMain/resources/public/androidhub-logo-dark.webp -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/public/androidhub-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/src/jsMain/resources/public/androidhub-logo.webp -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/public/bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/src/jsMain/resources/public/bold.png -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/public/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/src/jsMain/resources/public/code.png -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/src/jsMain/resources/public/favicon.ico -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/src/jsMain/resources/public/image.png -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/public/italic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/src/jsMain/resources/public/italic.png -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/public/jetpack.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/src/jsMain/resources/public/jetpack.webp -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/public/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/src/jsMain/resources/public/link.png -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/public/quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/src/jsMain/resources/public/quote.png -------------------------------------------------------------------------------- /site-android/src/jsMain/resources/public/title-subtitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-android/src/jsMain/resources/public/title-subtitle.png -------------------------------------------------------------------------------- /site-ios/.gitignore: -------------------------------------------------------------------------------- 1 | # Kobweb ignores 2 | !.kobweb/conf.yaml 3 | -------------------------------------------------------------------------------- /site-ios/.kobweb/conf.yaml: -------------------------------------------------------------------------------- 1 | site: 2 | title: "." 3 | 4 | server: 5 | files: 6 | dev: 7 | contentRoot: "build/processedResources/js/main/public" 8 | script: "build/dist/js/developmentExecutable/androidhub.js" 9 | api: "build/libs/androidhub.jar" 10 | prod: 11 | script: "build/dist/js/productionExecutable/androidhub.js" 12 | siteRoot: ".kobweb/site" 13 | 14 | port: 8080 15 | -------------------------------------------------------------------------------- /site-ios/.kobweb/server/logs/kobweb-server.log: -------------------------------------------------------------------------------- 1 | 2024-06-15 04:08:56.446 [main] DEBUG ktor.application - Java Home: /Users/canerture/Library/Java/JavaVirtualMachines/jbr-17.0.9/Contents 2 | 2024-06-15 04:08:56.447 [main] DEBUG ktor.application - Class Loader: jdk.internal.loader.ClassLoaders$AppClassLoader@7aec35a: [file:/Users/canerture/Documents/GitHub/AndroidHub/site-ios/.kobweb/server/server.jar!/ch] 3 | 2024-06-15 04:08:56.456 [main] INFO ktor.application - API jar found and will be loaded: "build/libs/androidhub.jar" 4 | 2024-06-15 04:08:56.475 [main] INFO ktor.application - Loaded and initialized server API jar in 1ms. 5 | 2024-06-15 04:08:56.486 [main] INFO ktor.application - Initializing Kobweb streams. 6 | 2024-06-15 04:08:56.504 [main] INFO ktor.application - Application started in 0.086 seconds. 7 | 2024-06-15 04:08:56.561 [main] INFO ktor.application - Responding at http://0.0.0.0:8080 8 | 2024-06-15 04:09:24.194 [kotlinx.coroutines.DefaultExecutor] INFO ktor.application - Kobweb server shutting down... 9 | 2024-06-15 04:09:25.244 [kotlinx.coroutines.DefaultExecutor] INFO ktor.application - Server finished shutting down. 10 | -------------------------------------------------------------------------------- /site-ios/.kobweb/server/server.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/server/server.jar -------------------------------------------------------------------------------- /site-ios/.kobweb/server/start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: Find the kobweb project folder, using this file's location to start form 4 | cd /d "%~dp0" 5 | cd ..\.. 6 | 7 | if defined KOBWEB_JAVA_HOME ( 8 | set "java_cmd=%KOBWEB_JAVA_HOME%\bin\java" 9 | ) else if defined JAVA_HOME ( 10 | set "java_cmd=%JAVA_HOME%\bin\java" 11 | ) else ( 12 | set "java_cmd=java" 13 | ) 14 | 15 | :: Run the java command with the common parameters 16 | %java_cmd% -Dkobweb.server.environment=PROD -Dkobweb.site.layout=FULLSTACK -Dio.ktor.development=false -jar .kobweb/server/server.jar -------------------------------------------------------------------------------- /site-ios/.kobweb/server/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Find the kobweb project folder, using this file's location to start form 4 | cd "$(dirname "$0")" 5 | cd ../.. 6 | 7 | args="-Dkobweb.server.environment=PROD -Dkobweb.site.layout=FULLSTACK -Dio.ktor.development=false -jar .kobweb/server/server.jar" 8 | 9 | if [ -n "$KOBWEB_JAVA_HOME" ]; then 10 | "$KOBWEB_JAVA_HOME/bin/java" $args 11 | elif [ -n "$JAVA_HOME" ]; then 12 | "$JAVA_HOME/bin/java" $args 13 | else 14 | java $args 15 | fi -------------------------------------------------------------------------------- /site-ios/.kobweb/site/bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/site/bold.png -------------------------------------------------------------------------------- /site-ios/.kobweb/site/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/site/code.png -------------------------------------------------------------------------------- /site-ios/.kobweb/site/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/site/favicon.ico -------------------------------------------------------------------------------- /site-ios/.kobweb/site/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/site/image.png -------------------------------------------------------------------------------- /site-ios/.kobweb/site/ios-figure.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/site/ios-figure.webp -------------------------------------------------------------------------------- /site-ios/.kobweb/site/ioshub-logo-dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/site/ioshub-logo-dark.webp -------------------------------------------------------------------------------- /site-ios/.kobweb/site/ioshub-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/site/ioshub-logo.webp -------------------------------------------------------------------------------- /site-ios/.kobweb/site/italic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/site/italic.png -------------------------------------------------------------------------------- /site-ios/.kobweb/site/jetpack.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/site/jetpack.webp -------------------------------------------------------------------------------- /site-ios/.kobweb/site/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/site/link.png -------------------------------------------------------------------------------- /site-ios/.kobweb/site/quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/site/quote.png -------------------------------------------------------------------------------- /site-ios/.kobweb/site/title-subtitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/.kobweb/site/title-subtitle.png -------------------------------------------------------------------------------- /site-ios/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.varabyte.kobweb.gradle.application.extensions.AppBlock.LegacyRouteRedirectStrategy 2 | import com.varabyte.kobweb.gradle.application.util.configAsKobwebApplication 3 | 4 | plugins { 5 | alias(libs.plugins.kotlin.multiplatform) 6 | alias(libs.plugins.jetbrains.compose) 7 | alias(libs.plugins.kobweb.application) 8 | alias(libs.plugins.kotlin.serialization) 9 | } 10 | 11 | group = "com.canerture.androidhub" 12 | version = "1.0-SNAPSHOT" 13 | 14 | kobweb { 15 | app { 16 | index { 17 | description.set("Powered by Kobweb") 18 | } 19 | legacyRouteRedirectStrategy.set(LegacyRouteRedirectStrategy.DISALLOW) 20 | } 21 | } 22 | 23 | kotlin { 24 | configAsKobwebApplication("androidhub", includeServer = true) 25 | 26 | sourceSets { 27 | commonMain.dependencies { 28 | implementation(compose.runtime) 29 | } 30 | 31 | jsMain.dependencies { 32 | implementation(compose.html.core) 33 | implementation(libs.kobweb.core) 34 | implementation(libs.kobweb.silk) 35 | implementation(libs.silk.icons.fa) 36 | implementation(libs.kotlinx.serialization.json) 37 | implementation(libs.colorMath) 38 | } 39 | 40 | jvmMain.dependencies { 41 | implementation(libs.kobweb.api) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/AppEntry.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub 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.canerture.androidhub.common.Res 9 | import com.varabyte.kobweb.compose.css.Cursor 10 | import com.varabyte.kobweb.compose.css.ScrollBehavior 11 | import com.varabyte.kobweb.compose.foundation.layout.Box 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.border 17 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius 18 | import com.varabyte.kobweb.compose.ui.modifiers.bottom 19 | import com.varabyte.kobweb.compose.ui.modifiers.color 20 | import com.varabyte.kobweb.compose.ui.modifiers.cursor 21 | import com.varabyte.kobweb.compose.ui.modifiers.display 22 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize 23 | import com.varabyte.kobweb.compose.ui.modifiers.minHeight 24 | import com.varabyte.kobweb.compose.ui.modifiers.minWidth 25 | import com.varabyte.kobweb.compose.ui.modifiers.onClick 26 | import com.varabyte.kobweb.compose.ui.modifiers.outline 27 | import com.varabyte.kobweb.compose.ui.modifiers.padding 28 | import com.varabyte.kobweb.compose.ui.modifiers.position 29 | import com.varabyte.kobweb.compose.ui.modifiers.right 30 | import com.varabyte.kobweb.compose.ui.modifiers.scrollBehavior 31 | import com.varabyte.kobweb.compose.ui.modifiers.size 32 | import com.varabyte.kobweb.compose.ui.modifiers.zIndex 33 | import com.varabyte.kobweb.compose.ui.toAttrs 34 | import com.varabyte.kobweb.core.App 35 | import com.varabyte.kobweb.silk.SilkApp 36 | import com.varabyte.kobweb.silk.components.graphics.Image 37 | import com.varabyte.kobweb.silk.components.layout.Surface 38 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 39 | import com.varabyte.kobweb.silk.components.style.toModifier 40 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint 41 | import kotlinx.browser.document 42 | import kotlinx.browser.window 43 | import org.jetbrains.compose.web.css.Color 44 | import org.jetbrains.compose.web.css.DisplayStyle 45 | import org.jetbrains.compose.web.css.Position 46 | import org.jetbrains.compose.web.css.percent 47 | import org.jetbrains.compose.web.css.px 48 | import org.jetbrains.compose.web.css.vh 49 | import org.jetbrains.compose.web.css.vw 50 | import org.jetbrains.compose.web.dom.Div 51 | import org.w3c.dom.SMOOTH 52 | import org.w3c.dom.ScrollToOptions 53 | 54 | @App 55 | @Composable 56 | fun AppEntry(content: @Composable () -> Unit) { 57 | SilkApp { 58 | var isButtonVisible by remember { mutableStateOf(false) } 59 | val breakpoint = rememberBreakpoint() 60 | 61 | window.onscroll = { 62 | if (document.body!!.scrollTop > 300 || document.documentElement!!.scrollTop > 300) { 63 | isButtonVisible = true 64 | } else { 65 | isButtonVisible = false 66 | } 67 | } 68 | 69 | Surface( 70 | Modifier 71 | .minHeight(100.vh) 72 | .minWidth(100.vw) 73 | .scrollBehavior(ScrollBehavior.Smooth) 74 | ) { 75 | Box( 76 | contentAlignment = Alignment.Center, 77 | modifier = Modifier.fillMaxSize() 78 | ) { 79 | content() 80 | 81 | Div( 82 | attrs = Modifier 83 | .position(Position.Fixed) 84 | .bottom(20.px) 85 | .right(if (breakpoint < Breakpoint.MD) 0.px else 30.px) 86 | .zIndex(99) 87 | .border(0.px) 88 | .outline(0.px) 89 | .backgroundColor(Color.transparent) 90 | .color(Colors.White) 91 | .cursor(Cursor.Pointer) 92 | .borderRadius(100.percent) 93 | .padding( 94 | bottom = if (breakpoint < Breakpoint.MD) 10.px else 15.px, 95 | right = if (breakpoint < Breakpoint.MD) 4.px else 22.px 96 | ) 97 | .display( 98 | if (isButtonVisible) DisplayStyle.Block else DisplayStyle.None 99 | ) 100 | .onClick { 101 | document.documentElement?.scrollTo( 102 | ScrollToOptions( 103 | behavior = org.w3c.dom.ScrollBehavior.SMOOTH, 104 | top = 0.0 105 | ) 106 | ) 107 | }.toAttrs() 108 | ) { 109 | val size = if (breakpoint < Breakpoint.MD) 40.px else 60.px 110 | Image( 111 | src = Res.Image.JETPACK, 112 | modifier = Modifier.size(size, size) 113 | ) 114 | } 115 | } 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/SiteTheme.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub 2 | 3 | import com.varabyte.kobweb.compose.ui.graphics.Color 4 | import com.varabyte.kobweb.compose.ui.graphics.Colors 5 | import com.varabyte.kobweb.silk.init.InitSilk 6 | import com.varabyte.kobweb.silk.init.InitSilkContext 7 | import com.varabyte.kobweb.silk.theme.colors.palette.background 8 | import com.varabyte.kobweb.silk.theme.colors.palette.color 9 | 10 | /** 11 | * @property nearBackground A useful color to apply to a container that should differentiate itself from the background 12 | * but just a little. 13 | */ 14 | class SitePalette( 15 | val primary: Color, 16 | val white: Color, 17 | val secondary: Color, 18 | val gray: Color, 19 | val lightGray: Color, 20 | val grayTransparent: Color, 21 | ) 22 | 23 | object SitePalettes { 24 | val palette = SitePalette( 25 | primary = Color.rgb(0x2b2b2b), 26 | white = Colors.White, 27 | secondary = Color.rgb(0x2b2b2b), 28 | gray = Color.rgb(0xC8C8C8), 29 | lightGray = Color.rgb(0xF0F0F0), 30 | grayTransparent = Color.rgba(255f, 255f, 255f, 0.15f), 31 | ) 32 | } 33 | 34 | fun getSitePalette(): SitePalette { 35 | return SitePalettes.palette 36 | } 37 | 38 | @InitSilk 39 | fun initTheme(ctx: InitSilkContext) { 40 | ctx.theme.palettes.light.background = Color.rgb(0xFAF8FF) 41 | ctx.theme.palettes.light.color = Colors.White 42 | } 43 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/common/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.common 2 | 3 | object Constants { 4 | const val BASE_URL = "https://api.canerture.com/ioshub/" 5 | 6 | const val QUERY_PARAM = "query" 7 | const val CATEGORY_PARAM = "category" 8 | const val POST_SHORT_PARAM = "short" 9 | const val POST_AUTHOR_ID_PARAM = "authorId" 10 | 11 | const val SIDE_PANEL_WIDTH = 250 12 | const val COLLAPSED_PANEL_HEIGHT = 100 13 | } 14 | 15 | object Res { 16 | object Image { 17 | const val LOGO = "/ioshub-logo.webp" 18 | const val LOGO_DARK = "/ioshub-logo-dark.webp" 19 | const val IOS_FIGURE = "/ios-figure.webp" 20 | const val JETPACK = "/jetpack.webp" 21 | } 22 | 23 | object Icon { 24 | const val BOLD = "/bold.png" 25 | const val ITALIC = "/italic.png" 26 | const val LINK = "/link.png" 27 | const val TITLE = "/title-subtitle.png" 28 | const val SUBTITLE = "/title-subtitle.png" 29 | const val QUOTE = "/quote.png" 30 | const val CODE = "/code.png" 31 | const val IMAGE = "/image.png" 32 | } 33 | 34 | object PathIcon { 35 | const val CREATE = 36 | "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" 37 | const val POSTS = 38 | "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" 39 | const val LOGOUT = 40 | "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" 41 | } 42 | } 43 | 44 | object Id { 45 | const val NAME_INPUT = "nameInput" 46 | const val EMAIL_INPUT = "emailInput" 47 | const val PASSWORD_INPUT = "passwordInput" 48 | const val SVG_PARENT = "svgParent" 49 | const val VECTOR_ICON = "vectorIcon" 50 | const val NAVIGATION_TEXT = "navigationText" 51 | const val EDITOR = "editor" 52 | const val EDITOR_PREVIEW = "editorPreview" 53 | const val TITLE_INPUT = "titleInput" 54 | const val THUMBNAIL_INPUT = "thumbnailInput" 55 | const val LINK_HREF_INPUT = "linkHrefInput" 56 | const val LINK_TITLE_INPUT = "linkTitleInput" 57 | const val ADMIN_SEARCH_BAR = "adminSearchBar" 58 | const val POST_CONTENT = "postContent" 59 | const val NAVBAR_SEARCH_INPUT = "navbarSearchInput" 60 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/common/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.common 2 | 3 | import com.varabyte.kobweb.compose.ui.Modifier 4 | import com.varabyte.kobweb.compose.ui.graphics.Colors 5 | import com.varabyte.kobweb.compose.ui.modifiers.border 6 | import com.varabyte.kobweb.compose.ui.modifiers.outline 7 | import org.jetbrains.compose.web.css.LineStyle 8 | import org.jetbrains.compose.web.css.px 9 | import kotlin.js.Date 10 | 11 | fun Modifier.noBorder(): Modifier { 12 | return this.border( 13 | width = 0.px, 14 | style = LineStyle.None, 15 | color = Colors.Transparent 16 | ).outline( 17 | width = 0.px, 18 | style = LineStyle.None, 19 | color = Colors.Transparent 20 | ) 21 | } 22 | 23 | fun Long.parseDateString() = Date(this).toLocaleDateString() 24 | 25 | fun String.formatShort() = replace(" ", "-").lowercase() 26 | .replace("ğ", "g") 27 | .replace("ü", "u") 28 | .replace("ş", "s") 29 | .replace("ı", "i") 30 | .replace("ö", "o") 31 | .replace("ç", "c") 32 | .replace("'", "") -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/components/layouts/AdminPageLayout.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.layouts 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.canerture.androidhub.components.sections.AdminNavigationItems 9 | import com.canerture.androidhub.components.sections.OverflowSidePanel 10 | import com.canerture.androidhub.components.sections.SidePanel 11 | import com.varabyte.kobweb.compose.foundation.layout.Box 12 | import com.varabyte.kobweb.compose.foundation.layout.Column 13 | import com.varabyte.kobweb.compose.ui.Alignment 14 | import com.varabyte.kobweb.compose.ui.Modifier 15 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize 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 | ) { 28 | SidePanel( 29 | onMenuClick = { overflowOpened = true } 30 | ) 31 | if (overflowOpened) { 32 | OverflowSidePanel( 33 | onMenuClose = { 34 | overflowOpened = false 35 | }, 36 | content = { 37 | AdminNavigationItems() 38 | } 39 | ) 40 | } 41 | content() 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/components/sections/Footer.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.sections 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.ShadowedGreenVariant 5 | import com.canerture.androidhub.common.noBorder 6 | import com.canerture.androidhub.data.isUserLoggedIn 7 | import com.canerture.androidhub.getSitePalette 8 | import com.canerture.androidhub.navigation.Screen 9 | import com.varabyte.kobweb.compose.css.Cursor 10 | import com.varabyte.kobweb.compose.css.FontWeight 11 | import com.varabyte.kobweb.compose.css.TextAlign 12 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement 13 | import com.varabyte.kobweb.compose.foundation.layout.Column 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.borderRadius 18 | import com.varabyte.kobweb.compose.ui.modifiers.color 19 | import com.varabyte.kobweb.compose.ui.modifiers.cursor 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.onClick 24 | import com.varabyte.kobweb.compose.ui.modifiers.padding 25 | import com.varabyte.kobweb.compose.ui.modifiers.setVariable 26 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign 27 | import com.varabyte.kobweb.compose.ui.modifiers.width 28 | import com.varabyte.kobweb.compose.ui.toAttrs 29 | import com.varabyte.kobweb.core.rememberPageContext 30 | import com.varabyte.kobweb.silk.components.forms.ButtonVars 31 | import com.varabyte.kobweb.silk.components.navigation.Link 32 | import com.varabyte.kobweb.silk.components.style.ComponentStyle 33 | import com.varabyte.kobweb.silk.components.style.base 34 | import com.varabyte.kobweb.silk.components.style.toModifier 35 | import com.varabyte.kobweb.silk.components.text.SpanText 36 | import org.jetbrains.compose.web.css.cssRem 37 | import org.jetbrains.compose.web.css.percent 38 | import org.jetbrains.compose.web.css.px 39 | import org.jetbrains.compose.web.dom.Button 40 | import org.jetbrains.compose.web.dom.Span 41 | 42 | val FooterStyle by ComponentStyle.base { 43 | Modifier.padding(topBottom = 5.cssRem, leftRight = 10.percent) 44 | } 45 | 46 | @Composable 47 | fun Footer(modifier: Modifier = Modifier) { 48 | val context = rememberPageContext() 49 | val isUserLoggedIn = isUserLoggedIn() 50 | Column( 51 | modifier = FooterStyle.toModifier().then(modifier), 52 | verticalArrangement = Arrangement.Center, 53 | horizontalAlignment = Alignment.CenterHorizontally 54 | ) { 55 | Button( 56 | attrs = ShadowedGreenVariant.toModifier() 57 | .backgroundColor(getSitePalette().primary) 58 | .setVariable(ButtonVars.BackgroundDefaultColor, getSitePalette().primary) 59 | .setVariable(ButtonVars.BackgroundHoverColor, getSitePalette().primary) 60 | .setVariable(ButtonVars.BackgroundPressedColor, getSitePalette().primary) 61 | .width(120.px) 62 | .borderRadius(3.cssRem) 63 | .padding(leftRight = 1.cssRem, topBottom = 0.5.cssRem) 64 | .margin(bottom = 1.cssRem) 65 | .noBorder() 66 | .cursor(Cursor.Pointer) 67 | .onClick { 68 | if (isUserLoggedIn) { 69 | context.router.navigateTo(Screen.AdminMyPosts.route) 70 | } else { 71 | context.router.navigateTo(Screen.Login.route) 72 | } 73 | } 74 | .toAttrs(), 75 | ) { 76 | SpanText( 77 | text = "Join us", 78 | modifier = Modifier 79 | .fontSize(16.px) 80 | .color(getSitePalette().white) 81 | ) 82 | } 83 | 84 | Span(Modifier.textAlign(TextAlign.Center).toAttrs()) { 85 | Link("https://bento.me/canerture") { 86 | SpanText( 87 | text = "2024 - Caner Ture", 88 | modifier = Modifier 89 | .color(getSitePalette().primary) 90 | .fontWeight(FontWeight.Bold) 91 | ) 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/CategoryChip.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.data.model.Category 5 | import com.canerture.androidhub.data.model.colorParse 6 | import com.canerture.androidhub.getSitePalette 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.backgroundColor 11 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius 12 | import com.varabyte.kobweb.compose.ui.modifiers.color 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.px 18 | 19 | @Composable 20 | fun CategoryChip(category: Category) { 21 | Box( 22 | modifier = Modifier 23 | .height(32.px) 24 | .padding(leftRight = 14.px) 25 | .borderRadius(r = 100.px) 26 | .backgroundColor(colorParse(category.color)), 27 | contentAlignment = Alignment.Center 28 | ) { 29 | SpanText( 30 | modifier = Modifier 31 | .fontSize(12.px) 32 | .color(getSitePalette().white), 33 | text = category.name 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/ErrorView.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.getSitePalette 5 | import com.varabyte.kobweb.compose.css.FontWeight 6 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement 7 | import com.varabyte.kobweb.compose.foundation.layout.Column 8 | import com.varabyte.kobweb.compose.ui.Alignment 9 | import com.varabyte.kobweb.compose.ui.Modifier 10 | import com.varabyte.kobweb.compose.ui.modifiers.color 11 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize 12 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 13 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight 14 | import com.varabyte.kobweb.compose.ui.modifiers.padding 15 | import com.varabyte.kobweb.silk.components.icons.fa.FaX 16 | import com.varabyte.kobweb.silk.components.icons.fa.IconSize 17 | import com.varabyte.kobweb.silk.components.text.SpanText 18 | import org.jetbrains.compose.web.css.px 19 | 20 | @Composable 21 | fun ErrorView( 22 | message: String = "" 23 | ) { 24 | Column( 25 | modifier = Modifier 26 | .fillMaxSize(), 27 | horizontalAlignment = Alignment.CenterHorizontally, 28 | verticalArrangement = Arrangement.Center 29 | ) { 30 | FaX( 31 | size = IconSize.X6, 32 | modifier = Modifier 33 | .padding(bottom = 48.px) 34 | .color(getSitePalette().secondary) 35 | ) 36 | 37 | SpanText( 38 | text = message.ifEmpty { "An error occurred while loading the page." }, 39 | modifier = Modifier 40 | .color(getSitePalette().secondary) 41 | .fontSize(24.px) 42 | .fontWeight(FontWeight.Bold) 43 | ) 44 | } 45 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/LoadingIndicator.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.LoaderStyle 5 | import com.varabyte.kobweb.compose.foundation.layout.Box 6 | import com.varabyte.kobweb.compose.ui.Alignment 7 | import com.varabyte.kobweb.compose.ui.Modifier 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.silk.components.style.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 | 16 | @Composable 17 | fun LoadingIndicator() { 18 | Box( 19 | modifier = Modifier 20 | .fillMaxSize() 21 | .height(100.vh) 22 | .padding(topBottom = 50.px), 23 | contentAlignment = Alignment.Center 24 | ) { 25 | Div( 26 | attrs = LoaderStyle.toAttrs() 27 | ) 28 | } 29 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/Popup.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.common.Id 5 | import com.canerture.androidhub.common.noBorder 6 | import com.canerture.androidhub.getSitePalette 7 | import com.canerture.androidhub.models.EditorControl 8 | import com.varabyte.kobweb.compose.foundation.layout.Box 9 | import com.varabyte.kobweb.compose.foundation.layout.Column 10 | import com.varabyte.kobweb.compose.ui.Alignment 11 | import com.varabyte.kobweb.compose.ui.Modifier 12 | import com.varabyte.kobweb.compose.ui.graphics.Colors 13 | import com.varabyte.kobweb.compose.ui.modifiers.backgroundColor 14 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius 15 | import com.varabyte.kobweb.compose.ui.modifiers.color 16 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxSize 17 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 18 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 19 | import com.varabyte.kobweb.compose.ui.modifiers.height 20 | import com.varabyte.kobweb.compose.ui.modifiers.id 21 | import com.varabyte.kobweb.compose.ui.modifiers.margin 22 | import com.varabyte.kobweb.compose.ui.modifiers.onClick 23 | import com.varabyte.kobweb.compose.ui.modifiers.padding 24 | import com.varabyte.kobweb.compose.ui.modifiers.position 25 | import com.varabyte.kobweb.compose.ui.modifiers.width 26 | import com.varabyte.kobweb.compose.ui.modifiers.zIndex 27 | import com.varabyte.kobweb.compose.ui.toAttrs 28 | import com.varabyte.kobweb.silk.components.text.SpanText 29 | import kotlinx.browser.document 30 | import org.jetbrains.compose.web.attributes.InputType 31 | import org.jetbrains.compose.web.css.Position 32 | import org.jetbrains.compose.web.css.px 33 | import org.jetbrains.compose.web.css.vh 34 | import org.jetbrains.compose.web.css.vw 35 | import org.jetbrains.compose.web.dom.Button 36 | import org.jetbrains.compose.web.dom.Input 37 | import org.w3c.dom.HTMLInputElement 38 | 39 | @Composable 40 | fun ControlPopup( 41 | editorControl: EditorControl, 42 | onDialogDismiss: () -> Unit, 43 | onAddClick: (String, String) -> Unit 44 | ) { 45 | Box( 46 | modifier = Modifier 47 | .width(100.vw) 48 | .height(100.vh) 49 | .position(Position.Fixed) 50 | .zIndex(19), 51 | contentAlignment = Alignment.Center 52 | ) { 53 | Box( 54 | modifier = Modifier 55 | .fillMaxSize() 56 | .backgroundColor(getSitePalette().secondary) 57 | .onClick { onDialogDismiss() } 58 | ) 59 | Column( 60 | modifier = Modifier 61 | .width(500.px) 62 | .padding(all = 24.px) 63 | .backgroundColor(Colors.White) 64 | .borderRadius(r = 4.px) 65 | ) { 66 | Input( 67 | type = InputType.Text, 68 | attrs = Modifier 69 | .id(Id.LINK_HREF_INPUT) 70 | .fillMaxWidth() 71 | .height(54.px) 72 | .padding(left = 20.px) 73 | .margin(bottom = 12.px) 74 | .fontSize(14.px) 75 | .noBorder() 76 | .borderRadius(r = 4.px) 77 | .backgroundColor(getSitePalette().white) 78 | .toAttrs { 79 | attr( 80 | "placeholder", 81 | if (editorControl == EditorControl.Link) "Href" else "Image URL" 82 | ) 83 | } 84 | ) 85 | Input( 86 | type = InputType.Text, 87 | attrs = Modifier 88 | .id(Id.LINK_TITLE_INPUT) 89 | .fillMaxWidth() 90 | .height(54.px) 91 | .padding(left = 20.px) 92 | .margin(bottom = 20.px) 93 | .fontSize(14.px) 94 | .noBorder() 95 | .borderRadius(r = 4.px) 96 | .backgroundColor(getSitePalette().white) 97 | .toAttrs { 98 | attr( 99 | "placeholder", 100 | if (editorControl == EditorControl.Link) "Title" else "Description" 101 | ) 102 | } 103 | ) 104 | Button( 105 | attrs = Modifier 106 | .onClick { 107 | val href = 108 | (document.getElementById(Id.LINK_HREF_INPUT) as HTMLInputElement).value 109 | val title = 110 | (document.getElementById(Id.LINK_TITLE_INPUT) as HTMLInputElement).value 111 | onAddClick(href, title) 112 | onDialogDismiss() 113 | } 114 | .fillMaxWidth() 115 | .height(54.px) 116 | .borderRadius(r = 4.px) 117 | .backgroundColor(getSitePalette().secondary) 118 | .color(Colors.White) 119 | .noBorder() 120 | .fontSize(14.px) 121 | .toAttrs() 122 | ) { 123 | SpanText(text = "Add") 124 | } 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/PostPreview.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.ShadowedGrayVariant 5 | import com.canerture.androidhub.common.parseDateString 6 | import com.canerture.androidhub.data.model.Post 7 | import com.canerture.androidhub.getSitePalette 8 | import com.varabyte.kobweb.compose.css.CSSTransition 9 | import com.varabyte.kobweb.compose.css.Cursor 10 | import com.varabyte.kobweb.compose.css.FontWeight 11 | import com.varabyte.kobweb.compose.css.Overflow 12 | import com.varabyte.kobweb.compose.css.TextOverflow 13 | import com.varabyte.kobweb.compose.css.TransitionProperty 14 | import com.varabyte.kobweb.compose.foundation.layout.Column 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.borderRadius 18 | import com.varabyte.kobweb.compose.ui.modifiers.color 19 | import com.varabyte.kobweb.compose.ui.modifiers.cursor 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.marginBlock 24 | import com.varabyte.kobweb.compose.ui.modifiers.marginInline 25 | import com.varabyte.kobweb.compose.ui.modifiers.onClick 26 | import com.varabyte.kobweb.compose.ui.modifiers.overflow 27 | import com.varabyte.kobweb.compose.ui.modifiers.padding 28 | import com.varabyte.kobweb.compose.ui.modifiers.textOverflow 29 | import com.varabyte.kobweb.compose.ui.modifiers.transition 30 | import com.varabyte.kobweb.silk.components.style.toModifier 31 | import com.varabyte.kobweb.silk.components.text.SpanText 32 | import org.jetbrains.compose.web.css.cssRem 33 | import org.jetbrains.compose.web.css.ms 34 | import org.jetbrains.compose.web.css.px 35 | 36 | @Composable 37 | fun PostPreview( 38 | post: Post, 39 | onClick: (String) -> Unit 40 | ) { 41 | Column( 42 | modifier = ShadowedGrayVariant.toModifier() 43 | .marginInline(end = 1.5.cssRem) 44 | .marginBlock(end = 1.5.cssRem) 45 | .backgroundColor(getSitePalette().white) 46 | .borderRadius(r = 10.px) 47 | .padding(all = 18.px) 48 | .transition(CSSTransition(property = TransitionProperty.All, duration = 400.ms)) 49 | .cursor(Cursor.Pointer) 50 | .onClick { onClick(post.short) } 51 | ) { 52 | SpanText( 53 | modifier = Modifier 54 | .fontSize(12.px) 55 | .color(getSitePalette().secondary), 56 | text = post.date.toLong().parseDateString() 57 | ) 58 | SpanText( 59 | modifier = Modifier 60 | .weight(1f) 61 | .margin(topBottom = 12.px) 62 | .fontSize(20.px) 63 | .fontWeight(FontWeight.Bold) 64 | .color(getSitePalette().secondary) 65 | .textOverflow(TextOverflow.Ellipsis) 66 | .overflow(Overflow.Hidden), 67 | text = post.title 68 | ) 69 | CategoryChip(category = post.category) 70 | } 71 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/PostsView.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.data.model.Post 5 | import com.varabyte.kobweb.compose.ui.Modifier 6 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 7 | import com.varabyte.kobweb.compose.ui.modifiers.margin 8 | import com.varabyte.kobweb.silk.components.layout.SimpleGrid 9 | import com.varabyte.kobweb.silk.components.layout.numColumns 10 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 11 | import com.varabyte.kobweb.silk.components.style.breakpoint.ResponsiveValues 12 | import org.jetbrains.compose.web.css.percent 13 | import org.jetbrains.compose.web.css.px 14 | 15 | @Composable 16 | fun PostsView( 17 | breakpoint: Breakpoint, 18 | posts: List, 19 | numColumns: ResponsiveValues = numColumns(base = 1, sm = 2, md = 3, lg = 3), 20 | onClick: (String) -> Unit 21 | ) { 22 | SimpleGrid( 23 | modifier = Modifier 24 | .fillMaxWidth( 25 | if (breakpoint > Breakpoint.MD) 80.percent 26 | else 90.percent 27 | ) 28 | .margin(top = 40.px), 29 | numColumns = numColumns 30 | ) { 31 | posts.forEach { 32 | PostPreview( 33 | post = it, 34 | onClick = onClick 35 | ) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/components/widgets/SearchBar.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.components.widgets 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.canerture.androidhub.common.Id 10 | import com.canerture.androidhub.common.noBorder 11 | import com.canerture.androidhub.getSitePalette 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.toAttrs 35 | import com.varabyte.kobweb.silk.components.icons.fa.FaMagnifyingGlass 36 | import com.varabyte.kobweb.silk.components.icons.fa.IconSize 37 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 38 | import org.jetbrains.compose.web.attributes.InputType 39 | import org.jetbrains.compose.web.css.LineStyle 40 | import org.jetbrains.compose.web.css.ms 41 | import org.jetbrains.compose.web.css.px 42 | import org.jetbrains.compose.web.dom.Input 43 | 44 | @Composable 45 | fun SearchBar( 46 | breakpoint: Breakpoint, 47 | modifier: Modifier = Modifier, 48 | onEnterClick: () -> Unit, 49 | onSearchIconClick: (Boolean) -> Unit 50 | ) { 51 | var focused by remember { mutableStateOf(false) } 52 | 53 | LaunchedEffect(breakpoint) { 54 | if (breakpoint >= Breakpoint.SM) onSearchIconClick(false) 55 | } 56 | 57 | if (breakpoint >= Breakpoint.SM) { 58 | Row( 59 | modifier = modifier 60 | .fillMaxWidth() 61 | .padding(left = 20.px) 62 | .height(54.px) 63 | .backgroundColor(getSitePalette().white) 64 | .borderRadius(r = 100.px) 65 | .border( 66 | width = 2.px, 67 | style = LineStyle.Solid, 68 | color = if (focused) getSitePalette().primary else getSitePalette().secondary 69 | ) 70 | .transition(CSSTransition(property = "border", duration = 200.ms)), 71 | verticalAlignment = Alignment.CenterVertically 72 | ) { 73 | FaMagnifyingGlass( 74 | modifier = Modifier 75 | .margin(right = 14.px) 76 | .color(if (focused) getSitePalette().primary else getSitePalette().secondary) 77 | .transition(CSSTransition(property = "color", duration = 200.ms)), 78 | size = IconSize.SM 79 | ) 80 | Input( 81 | type = InputType.Text, 82 | attrs = Modifier 83 | .id(Id.ADMIN_SEARCH_BAR) 84 | .fillMaxSize() 85 | .color(getSitePalette().secondary) 86 | .backgroundColor(Colors.Transparent) 87 | .noBorder() 88 | .onFocusIn { focused = true } 89 | .onFocusOut { focused = false } 90 | .onKeyDown { 91 | if (it.key == "Enter") { 92 | onEnterClick() 93 | } 94 | } 95 | .toAttrs { 96 | attr("placeholder", "Search...") 97 | } 98 | ) 99 | } 100 | } else { 101 | FaMagnifyingGlass( 102 | modifier = Modifier 103 | .margin(right = 14.px) 104 | .color(getSitePalette().primary) 105 | .cursor(Cursor.Pointer) 106 | .onClick { onSearchIconClick(true) }, 107 | size = IconSize.SM 108 | ) 109 | } 110 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/data/CategoryApis.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data 2 | 3 | import com.canerture.androidhub.data.model.Category 4 | import com.canerture.androidhub.utils.ApiUtils.get 5 | import com.canerture.androidhub.utils.ApiUtils.safeApiCall 6 | 7 | suspend fun getCategories( 8 | onSuccess: (List) -> Unit = {}, 9 | onError: (String) -> Unit = {} 10 | ) = safeApiCall>( 11 | call = { get("categories") }, 12 | onSuccess = onSuccess, 13 | onError = onError 14 | ) 15 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/data/PostApis.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data 2 | 3 | import com.canerture.androidhub.common.formatShort 4 | import com.canerture.androidhub.data.model.AddPostRequest 5 | import com.canerture.androidhub.data.model.AuthorDetail 6 | import com.canerture.androidhub.data.model.GetPostsResponse 7 | import com.canerture.androidhub.data.model.Post 8 | import com.canerture.androidhub.data.model.UpdatePostRequest 9 | import com.canerture.androidhub.utils.ApiUtils.delete 10 | import com.canerture.androidhub.utils.ApiUtils.get 11 | import com.canerture.androidhub.utils.ApiUtils.post 12 | import com.canerture.androidhub.utils.ApiUtils.put 13 | import com.canerture.androidhub.utils.ApiUtils.safeApiCall 14 | import kotlinx.browser.localStorage 15 | import kotlinx.serialization.encodeToString 16 | import kotlinx.serialization.json.Json 17 | import org.w3c.dom.get 18 | import kotlin.js.Date 19 | 20 | suspend fun getPosts( 21 | page: Int, 22 | onSuccess: (GetPostsResponse) -> Unit = {}, 23 | onError: (String) -> Unit = {} 24 | ) = safeApiCall( 25 | call = { get("posts", "page" to page) }, 26 | onSuccess = onSuccess, 27 | onError = onError 28 | ) 29 | 30 | suspend fun getPopularPosts( 31 | onSuccess: (List) -> Unit = {}, 32 | onError: (String) -> Unit = {} 33 | ) = safeApiCall>( 34 | call = { get("posts", "popular" to "") }, 35 | onSuccess = onSuccess, 36 | onError = onError 37 | ) 38 | 39 | suspend fun getPostDetail( 40 | short: String, 41 | onSuccess: (Post) -> Unit = {}, 42 | onError: (String) -> Unit = {} 43 | ) = safeApiCall( 44 | call = { get("posts", "short" to short) }, 45 | onSuccess = onSuccess, 46 | onError = onError 47 | ) 48 | 49 | suspend fun getPendingPosts( 50 | onSuccess: (List) -> Unit = {}, 51 | onError: (String) -> Unit = {} 52 | ) = safeApiCall>( 53 | call = { get("pending_posts") }, 54 | onSuccess = onSuccess, 55 | onError = onError 56 | ) 57 | 58 | suspend fun addPost( 59 | content: String, 60 | title: String, 61 | category: String, 62 | thumbnail: String, 63 | status: String, 64 | onSuccess: (String) -> Unit = {}, 65 | onError: (String) -> Unit = {} 66 | ) = safeApiCall( 67 | call = { 68 | val id = localStorage["userId"] ?: "" 69 | val name = localStorage["name"] ?: "" 70 | val short = title.formatShort() 71 | post( 72 | path = "posts", 73 | body = Json.encodeToString( 74 | AddPostRequest(id, name, Date.now(), content, title, short, category, thumbnail, status) 75 | ).encodeToByteArray() 76 | ) 77 | }, 78 | onSuccess = onSuccess, 79 | onError = onError 80 | ) 81 | 82 | suspend fun updatePost( 83 | id: Int, 84 | content: String, 85 | title: String, 86 | category: String, 87 | thumbnail: String, 88 | status: String, 89 | onSuccess: (String) -> Unit = {}, 90 | onError: (String) -> Unit = {} 91 | ) = safeApiCall( 92 | call = { 93 | val short = title.formatShort() 94 | post( 95 | path = "posts", 96 | body = Json.encodeToString( 97 | UpdatePostRequest(id, Date.now(), content, title, short, category, thumbnail, status) 98 | ).encodeToByteArray() 99 | ) 100 | }, 101 | onSuccess = onSuccess, 102 | onError = onError 103 | ) 104 | 105 | suspend fun searchPostsByTitle( 106 | title: String, 107 | onSuccess: (List) -> Unit = {}, 108 | onError: (String) -> Unit = {} 109 | ) = safeApiCall>( 110 | call = { get("posts", "title" to title) }, 111 | onSuccess = onSuccess, 112 | onError = onError 113 | ) 114 | 115 | suspend fun getMyPosts( 116 | onSuccess: (AuthorDetail) -> Unit = {}, 117 | onError: (String) -> Unit = {} 118 | ) = safeApiCall( 119 | call = { get("posts", "authorId" to localStorage["userId"].orEmpty()) }, 120 | onSuccess = onSuccess, 121 | onError = onError 122 | ) 123 | 124 | suspend fun deletePost( 125 | id: Int, 126 | onSuccess: (String) -> Unit = {}, 127 | onError: (String) -> Unit = {} 128 | ) = safeApiCall( 129 | call = { delete("posts", "id" to id) }, 130 | onSuccess = onSuccess, 131 | onError = onError 132 | ) 133 | 134 | suspend fun getPostsByCategory( 135 | category: String, 136 | onSuccess: (List) -> Unit = {}, 137 | onError: (String) -> Unit = {} 138 | ) = safeApiCall>( 139 | call = { get("posts", "category" to category) }, 140 | onSuccess = onSuccess, 141 | onError = onError 142 | ) 143 | 144 | suspend fun getPostsByAuthorId( 145 | authorId: String, 146 | onSuccess: (AuthorDetail) -> Unit = {}, 147 | onError: (String) -> Unit = {} 148 | ) = safeApiCall( 149 | call = { get("posts", "authorId" to authorId) }, 150 | onSuccess = onSuccess, 151 | onError = onError 152 | ) -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/data/UserApis.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data 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.canerture.androidhub.data.model.LoginRequest 10 | import com.canerture.androidhub.data.model.RegisterRequest 11 | import com.canerture.androidhub.data.model.User 12 | import com.canerture.androidhub.utils.ApiUtils.get 13 | import com.canerture.androidhub.utils.ApiUtils.post 14 | import com.canerture.androidhub.utils.ApiUtils.safeApiCall 15 | import kotlinx.browser.localStorage 16 | import kotlinx.serialization.encodeToString 17 | import kotlinx.serialization.json.Json 18 | import org.w3c.dom.get 19 | import org.w3c.dom.set 20 | 21 | suspend fun login( 22 | email: String, 23 | password: String, 24 | onSuccess: (User) -> Unit = {}, 25 | onError: (String) -> Unit = {} 26 | ) = safeApiCall( 27 | call = { 28 | post( 29 | path = "login", 30 | body = Json.encodeToString(LoginRequest(email, password)).encodeToByteArray() 31 | ) 32 | }, 33 | onSuccess = onSuccess, 34 | onError = onError 35 | ) 36 | 37 | suspend fun register( 38 | name: String, 39 | email: String, 40 | password: String, 41 | onSuccess: (User) -> Unit = {}, 42 | onError: (String) -> Unit = {} 43 | ) = safeApiCall( 44 | call = { 45 | post( 46 | path = "register", 47 | body = Json.encodeToString(RegisterRequest(name, email, password)).encodeToByteArray() 48 | ) 49 | }, 50 | onSuccess = onSuccess, 51 | onError = onError 52 | ) 53 | 54 | suspend fun checkUserId( 55 | userId: String, 56 | onSuccess: (Boolean) -> Unit = {}, 57 | onError: (String) -> Unit = {} 58 | ) = safeApiCall( 59 | call = { get("check_user_id", "userId" to userId) }, 60 | onSuccess = onSuccess, 61 | onError = onError 62 | ) 63 | 64 | @Composable 65 | fun isUserLoggedIn(): Boolean { 66 | val remembered = remember { localStorage["remember"].toBoolean() } 67 | val userId = remember { localStorage["userId"] } 68 | var userIdExists by remember { mutableStateOf(null) } 69 | LaunchedEffect(key1 = Unit) { 70 | if (!userId.isNullOrEmpty()) { 71 | checkUserId( 72 | userId = userId, 73 | onSuccess = { 74 | userIdExists = it 75 | }, 76 | onError = { 77 | userIdExists = null 78 | } 79 | ) 80 | } 81 | } 82 | 83 | return remembered && userIdExists ?: false 84 | } 85 | 86 | fun logout() { 87 | localStorage["remember"] = "false" 88 | localStorage["userId"] = "" 89 | localStorage["username"] = "" 90 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/data/model/AuthorDetail.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class AuthorDetail( 7 | val authorName: String, 8 | val posts: List, 9 | ) 10 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/data/model/BaseResponse.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class BaseResponse( 7 | val status: Int? = null, 8 | val message: String? = null, 9 | val data: T? = null 10 | ) { 11 | fun isSuccess() = status == 200 12 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/data/model/Category.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import com.github.ajalt.colormath.model.RGBInt 4 | import com.github.ajalt.colormath.parse 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Category( 9 | val id: Int = 0, 10 | val name: String = "", 11 | val short: String = "", 12 | val color: String = "", 13 | val parentCategory: String = "", 14 | ) 15 | 16 | data class ParentCategory( 17 | val id: Int = 0, 18 | val name: String = "", 19 | val short: String = "", 20 | val color: String = "", 21 | val subCategories: MutableList = mutableListOf(), 22 | ) 23 | 24 | data class SubCategory( 25 | val id: Int = 0, 26 | val name: String = "", 27 | val short: String = "", 28 | val color: String = "", 29 | val parentCategory: String = "", 30 | ) 31 | 32 | fun colorParse(color: String): com.varabyte.kobweb.compose.ui.graphics.Color { 33 | return com.github.ajalt.colormath.Color.parse("#$color").toComposeColor() 34 | } 35 | 36 | fun com.github.ajalt.colormath.Color.toComposeColor(): com.varabyte.kobweb.compose.ui.graphics.Color { 37 | return if (this is RGBInt) { 38 | com.varabyte.kobweb.compose.ui.graphics.Color.rgb(argb.toInt()) 39 | } else { 40 | val (r, g, b, a) = toSRGB() 41 | com.varabyte.kobweb.compose.ui.graphics.Color.rgba(r, g, b, a) 42 | } 43 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/data/model/GetPostsResponse.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class GetPostsResponse( 7 | val totalPage: Int, 8 | val posts: List 9 | ) 10 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/data/model/LoginRequest.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class LoginRequest( 7 | val email: String, 8 | val password: String, 9 | ) 10 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/data/model/Post.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class AddPostRequest( 7 | val authorId: String = "", 8 | val authorName: String = "", 9 | val date: Double = 0.0, 10 | val content: String = "", 11 | val title: String = "", 12 | val short: String = "", 13 | val category: String = "", 14 | val thumbnail: String = "", 15 | val status: String = "", 16 | ) 17 | 18 | @Serializable 19 | data class UpdatePostRequest( 20 | val id: Int = 0, 21 | val date: Double = 0.0, 22 | val content: String = "", 23 | val title: String = "", 24 | val short: String = "", 25 | val category: String = "", 26 | val thumbnail: String = "", 27 | val status: String = "", 28 | ) 29 | 30 | @Serializable 31 | data class Post( 32 | val id: Int = 0, 33 | val authorId: String = "", 34 | val authorName: String = "", 35 | val date: Double = 0.0, 36 | val content: String = "", 37 | val title: String = "", 38 | val short: String = "", 39 | val category: Category = Category(), 40 | val thumbnail: String = "", 41 | val commentCount: Int = 0, 42 | val status: String = "", 43 | ) -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/data/model/RegisterRequest.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class RegisterRequest( 7 | val name: String, 8 | val email: String, 9 | val password: String, 10 | ) 11 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/data/model/User.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class User( 7 | val userId: String, 8 | val name: String, 9 | val email: String, 10 | val isAdmin: Boolean 11 | ) 12 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/models/ControlStyle.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.models 2 | 3 | import com.canerture.androidhub.getSitePalette 4 | 5 | sealed class ControlStyle(val style: String) { 6 | data class Bold(val selectedText: String?) : ControlStyle( 7 | style = "$selectedText" 8 | ) 9 | 10 | data class Italic(val selectedText: String?) : ControlStyle( 11 | style = "$selectedText" 12 | ) 13 | 14 | data class Link( 15 | val selectedText: String?, 16 | val href: String, 17 | val title: String 18 | ) : ControlStyle( 19 | style = "$selectedText" 20 | ) 21 | 22 | data class Title(val selectedText: String?) : ControlStyle( 23 | style = "

$selectedText

" 24 | ) 25 | 26 | data class Subtitle(val selectedText: String?) : ControlStyle( 27 | style = "

$selectedText

" 28 | ) 29 | 30 | data class Quote(val selectedText: String?) : ControlStyle( 31 | style = "
❞ $selectedText
" 32 | ) 33 | 34 | data class Code(val selectedText: String?) : ControlStyle( 35 | style = "
$selectedText
" 36 | ) 37 | 38 | data class Image( 39 | val selectedText: String?, 40 | val imageUrl: String, 41 | val alt: String 42 | ) : ControlStyle( 43 | style = "\"$alt\"$selectedText" 44 | ) 45 | 46 | data class Break(val selectedText: String?) : ControlStyle( 47 | style = "$selectedText
" 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/models/EditorControl.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.models 2 | 3 | import com.canerture.androidhub.common.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-ios/src/jsMain/kotlin/com/canerture/androidhub/navigation/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.navigation 2 | 3 | import com.canerture.androidhub.common.Constants.CATEGORY_PARAM 4 | import com.canerture.androidhub.common.Constants.POST_AUTHOR_ID_PARAM 5 | import com.canerture.androidhub.common.Constants.POST_SHORT_PARAM 6 | import com.canerture.androidhub.common.Constants.QUERY_PARAM 7 | 8 | sealed class Screen(val route: String) { 9 | data object HomePage : Screen(route = "/") 10 | 11 | data object PostPage : Screen(route = "/posts") { 12 | fun getPost(short: String) = "/posts?${POST_SHORT_PARAM}=$short" 13 | } 14 | 15 | data object AuthorPage : Screen(route = "/author") { 16 | fun getAuthor(authorId: String) = "/author?${POST_AUTHOR_ID_PARAM}=$authorId" 17 | } 18 | 19 | data object CategoryPage : Screen(route = "/category") { 20 | fun getCategoryPosts(category: String) = "/category?${CATEGORY_PARAM}=${category}" 21 | } 22 | 23 | data object Login : Screen(route = "/login") 24 | 25 | data object AdminCreate : Screen(route = "/admin/create-post") { 26 | fun passPostId(short: String) = "/admin/create-post?${POST_SHORT_PARAM}=$short" 27 | } 28 | 29 | data object AdminMyPosts : Screen(route = "/admin/my-posts") 30 | 31 | data object AdminAllPosts : Screen(route = "/admin/all-posts") 32 | 33 | data object SearchPage : Screen(route = "/search") { 34 | fun searchByTitle(query: String) = "/search?${QUERY_PARAM}=$query" 35 | } 36 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/pages/adminallposts/AdminAllPosts.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.adminallposts 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.canerture.androidhub.common.Constants.SIDE_PANEL_WIDTH 10 | import com.canerture.androidhub.components.layouts.AdminPageLayout 11 | import com.canerture.androidhub.components.widgets.PostsView 12 | import com.canerture.androidhub.data.getPendingPosts 13 | import com.canerture.androidhub.data.getPosts 14 | import com.canerture.androidhub.data.model.Post 15 | import com.canerture.androidhub.getSitePalette 16 | import com.canerture.androidhub.navigation.Screen 17 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement 18 | import com.varabyte.kobweb.compose.foundation.layout.Column 19 | import com.varabyte.kobweb.compose.foundation.layout.Row 20 | import com.varabyte.kobweb.compose.ui.Alignment 21 | import com.varabyte.kobweb.compose.ui.Modifier 22 | import com.varabyte.kobweb.compose.ui.modifiers.color 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.fontSize 26 | import com.varabyte.kobweb.compose.ui.modifiers.margin 27 | import com.varabyte.kobweb.compose.ui.modifiers.padding 28 | import com.varabyte.kobweb.core.Page 29 | import com.varabyte.kobweb.core.rememberPageContext 30 | import com.varabyte.kobweb.silk.components.forms.Switch 31 | import com.varabyte.kobweb.silk.components.forms.SwitchSize 32 | import com.varabyte.kobweb.silk.components.layout.numColumns 33 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 34 | import com.varabyte.kobweb.silk.components.text.SpanText 35 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint 36 | import org.jetbrains.compose.web.css.percent 37 | import org.jetbrains.compose.web.css.px 38 | 39 | data class AllPostsUIState( 40 | var isLoading: Boolean = true, 41 | var posts: List = emptyList(), 42 | var pendingPostEnabled: Boolean = false, 43 | ) 44 | 45 | @Page("/admin/all-posts") 46 | @Composable 47 | fun AdminAllPosts() { 48 | val context = rememberPageContext() 49 | val breakpoint = rememberBreakpoint() 50 | 51 | var state by remember { mutableStateOf(AllPostsUIState()) } 52 | 53 | LaunchedEffect(state.pendingPostEnabled) { 54 | if (state.pendingPostEnabled) { 55 | getPendingPosts( 56 | onSuccess = { 57 | state = state.copy(isLoading = false, posts = it) 58 | } 59 | ) 60 | } else { 61 | getPosts( 62 | page = 1, 63 | onSuccess = { 64 | state = state.copy(isLoading = false, posts = it.posts) 65 | } 66 | ) 67 | } 68 | } 69 | 70 | AdminPageLayout { 71 | Column( 72 | modifier = Modifier 73 | .margin(topBottom = 60.px) 74 | .fillMaxSize() 75 | .padding(left = if (breakpoint > Breakpoint.MD) SIDE_PANEL_WIDTH.px else 0.px), 76 | verticalArrangement = Arrangement.Top, 77 | horizontalAlignment = Alignment.CenterHorizontally 78 | ) { 79 | Row( 80 | modifier = Modifier 81 | .fillMaxWidth( 82 | if (breakpoint > Breakpoint.MD) 80.percent 83 | else 90.percent 84 | ) 85 | .margin(top = 80.px), 86 | horizontalArrangement = Arrangement.Start, 87 | verticalAlignment = Alignment.CenterVertically 88 | ) { 89 | Switch( 90 | modifier = Modifier.margin(right = 8.px), 91 | checked = state.pendingPostEnabled, 92 | onCheckedChange = { state = state.copy(pendingPostEnabled = it) }, 93 | size = SwitchSize.MD 94 | ) 95 | SpanText( 96 | modifier = Modifier 97 | .fontSize(14.px) 98 | .color(getSitePalette().secondary), 99 | text = "Show Pending Posts" 100 | ) 101 | } 102 | PostsView( 103 | breakpoint = breakpoint, 104 | posts = state.posts, 105 | numColumns = numColumns(base = 1, sm = 1, md = 2, lg = 2), 106 | onClick = { context.router.navigateTo(Screen.AdminCreate.passPostId(short = it)) } 107 | ) 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/pages/admincreatepost/Functions.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.admincreatepost 2 | 3 | import com.canerture.androidhub.common.Id 4 | import com.canerture.androidhub.models.ControlStyle 5 | import com.canerture.androidhub.models.EditorControl 6 | import kotlinx.browser.document 7 | import org.w3c.dom.HTMLTextAreaElement 8 | 9 | fun getEditor() = document.getElementById(Id.EDITOR) as HTMLTextAreaElement 10 | 11 | fun getSelectedIntRange(): IntRange? { 12 | val editor = getEditor() 13 | val start = editor.selectionStart 14 | val end = editor.selectionEnd 15 | return if (start != null && end != null) { 16 | IntRange(start, (end - 1)) 17 | } else null 18 | } 19 | 20 | fun getSelectedText(): String? { 21 | val range = getSelectedIntRange() 22 | return if (range != null) { 23 | getEditor().value.substring(range) 24 | } else null 25 | } 26 | 27 | fun applyStyle(controlStyle: ControlStyle) { 28 | val selectedText = getSelectedText() 29 | val selectedIntRange = getSelectedIntRange() 30 | if (selectedIntRange != null && selectedText != null) { 31 | getEditor().value = getEditor().value.replaceRange( 32 | range = selectedIntRange, 33 | replacement = controlStyle.style 34 | ) 35 | document.getElementById(Id.EDITOR_PREVIEW)?.innerHTML = getEditor().value 36 | } 37 | } 38 | 39 | fun applyControlStyle( 40 | editorControl: EditorControl, 41 | onLinkClick: () -> Unit, 42 | onImageClick: () -> Unit 43 | ) { 44 | when (editorControl) { 45 | EditorControl.Bold -> { 46 | applyStyle( 47 | ControlStyle.Bold( 48 | selectedText = getSelectedText() 49 | ) 50 | ) 51 | } 52 | 53 | EditorControl.Italic -> { 54 | applyStyle( 55 | ControlStyle.Italic( 56 | selectedText = getSelectedText() 57 | ) 58 | ) 59 | } 60 | 61 | EditorControl.Link -> { 62 | onLinkClick() 63 | } 64 | 65 | EditorControl.Title -> { 66 | applyStyle( 67 | ControlStyle.Title( 68 | selectedText = getSelectedText() 69 | ) 70 | ) 71 | } 72 | 73 | EditorControl.Subtitle -> { 74 | applyStyle( 75 | ControlStyle.Subtitle( 76 | selectedText = getSelectedText(), 77 | ) 78 | ) 79 | } 80 | 81 | EditorControl.Quote -> { 82 | applyStyle( 83 | ControlStyle.Quote( 84 | selectedText = getSelectedText() 85 | ) 86 | ) 87 | } 88 | 89 | EditorControl.Code -> { 90 | applyStyle( 91 | ControlStyle.Code( 92 | selectedText = getSelectedText() 93 | ) 94 | ) 95 | } 96 | 97 | EditorControl.Image -> { 98 | onImageClick() 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/pages/adminmyposts/AdminMyPosts.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.adminmyposts 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.rememberCoroutineScope 9 | import androidx.compose.runtime.setValue 10 | import com.canerture.androidhub.common.Constants.SIDE_PANEL_WIDTH 11 | import com.canerture.androidhub.common.Id 12 | import com.canerture.androidhub.components.layouts.AdminPageLayout 13 | import com.canerture.androidhub.components.widgets.ErrorView 14 | import com.canerture.androidhub.components.widgets.LoadingIndicator 15 | import com.canerture.androidhub.components.widgets.PostsView 16 | import com.canerture.androidhub.components.widgets.SearchBar 17 | import com.canerture.androidhub.data.getMyPosts 18 | import com.canerture.androidhub.data.model.Post 19 | import com.canerture.androidhub.data.searchPostsByTitle 20 | import com.canerture.androidhub.navigation.Screen 21 | import com.varabyte.kobweb.compose.css.CSSTransition 22 | import com.varabyte.kobweb.compose.css.TransitionProperty 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.modifiers.fillMaxSize 29 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 30 | import com.varabyte.kobweb.compose.ui.modifiers.margin 31 | import com.varabyte.kobweb.compose.ui.modifiers.padding 32 | import com.varabyte.kobweb.compose.ui.modifiers.transition 33 | import com.varabyte.kobweb.core.Page 34 | import com.varabyte.kobweb.core.rememberPageContext 35 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 36 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint 37 | import kotlinx.browser.document 38 | import kotlinx.coroutines.launch 39 | import org.jetbrains.compose.web.css.ms 40 | import org.jetbrains.compose.web.css.percent 41 | import org.jetbrains.compose.web.css.px 42 | import org.w3c.dom.HTMLInputElement 43 | 44 | data class AdminMyPostsUIState( 45 | var isLoading: Boolean = true, 46 | var posts: List = emptyList(), 47 | var isError: Boolean = false, 48 | ) 49 | 50 | @Page("/admin/my-posts") 51 | @Composable 52 | fun MyPostsScreen() { 53 | val context = rememberPageContext() 54 | val breakpoint = rememberBreakpoint() 55 | val scope = rememberCoroutineScope() 56 | 57 | var state by remember { mutableStateOf(AdminMyPostsUIState()) } 58 | 59 | LaunchedEffect(context.route) { 60 | getMyPosts( 61 | onSuccess = { 62 | state = state.copy(isLoading = false, posts = it.posts) 63 | }, 64 | onError = { 65 | state = state.copy(isLoading = false, isError = true) 66 | } 67 | ) 68 | } 69 | 70 | AdminPageLayout { 71 | Column( 72 | modifier = Modifier 73 | .margin(topBottom = 50.px) 74 | .fillMaxSize() 75 | .padding(left = if (breakpoint > Breakpoint.MD) SIDE_PANEL_WIDTH.px else 0.px), 76 | verticalArrangement = Arrangement.Top, 77 | horizontalAlignment = Alignment.CenterHorizontally 78 | ) { 79 | Box( 80 | modifier = Modifier 81 | .fillMaxWidth( 82 | if (breakpoint > Breakpoint.MD) 30.percent 83 | else 50.percent 84 | ) 85 | .margin(bottom = 24.px), 86 | contentAlignment = Alignment.Center 87 | ) { 88 | SearchBar( 89 | breakpoint = breakpoint, 90 | modifier = Modifier 91 | .transition(CSSTransition(property = TransitionProperty.All, duration = 200.ms)), 92 | onEnterClick = { 93 | val searchInput = 94 | (document.getElementById(Id.ADMIN_SEARCH_BAR) as HTMLInputElement).value 95 | scope.launch { 96 | state = state.copy(isLoading = true) 97 | if (searchInput.isNotEmpty()) { 98 | searchPostsByTitle( 99 | title = searchInput, 100 | onSuccess = { 101 | state = state.copy(isLoading = false, posts = it) 102 | }, 103 | onError = { 104 | state = state.copy(isLoading = false, isError = true) 105 | } 106 | ) 107 | } else { 108 | getMyPosts( 109 | onSuccess = { 110 | state = state.copy(isLoading = false, posts = it.posts) 111 | }, 112 | ) 113 | } 114 | } 115 | }, 116 | onSearchIconClick = {} 117 | ) 118 | } 119 | 120 | if (state.isLoading) { 121 | LoadingIndicator() 122 | } else { 123 | if (state.isError) { 124 | ErrorView("You don't have a post yet") 125 | return@Column 126 | } 127 | PostsView( 128 | breakpoint = breakpoint, 129 | posts = state.posts, 130 | onClick = { context.router.navigateTo(Screen.AdminCreate.passPostId(short = it)) } 131 | ) 132 | } 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/pages/author/Author.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.author 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.canerture.androidhub.common.Constants.POST_AUTHOR_ID_PARAM 10 | import com.canerture.androidhub.components.layouts.PageLayout 11 | import com.canerture.androidhub.components.widgets.ErrorView 12 | import com.canerture.androidhub.components.widgets.LoadingIndicator 13 | import com.canerture.androidhub.data.getPostsByAuthorId 14 | import com.canerture.androidhub.data.model.Post 15 | import com.canerture.androidhub.getSitePalette 16 | import com.canerture.androidhub.pages.index.LatestArticleItem 17 | import com.varabyte.kobweb.compose.css.FontWeight 18 | import com.varabyte.kobweb.compose.css.TextAlign 19 | import com.varabyte.kobweb.compose.foundation.layout.Row 20 | import com.varabyte.kobweb.compose.ui.Alignment 21 | import com.varabyte.kobweb.compose.ui.Modifier 22 | import com.varabyte.kobweb.compose.ui.modifiers.alignContent 23 | import com.varabyte.kobweb.compose.ui.modifiers.color 24 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 25 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 26 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight 27 | import com.varabyte.kobweb.compose.ui.modifiers.height 28 | import com.varabyte.kobweb.compose.ui.modifiers.margin 29 | import com.varabyte.kobweb.compose.ui.modifiers.padding 30 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign 31 | import com.varabyte.kobweb.core.Page 32 | import com.varabyte.kobweb.core.rememberPageContext 33 | import com.varabyte.kobweb.silk.components.icons.fa.FaBoltLightning 34 | import com.varabyte.kobweb.silk.components.layout.SimpleGrid 35 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 36 | import com.varabyte.kobweb.silk.components.style.breakpoint.ResponsiveValues 37 | import com.varabyte.kobweb.silk.components.text.SpanText 38 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint 39 | import kotlinx.browser.document 40 | import org.jetbrains.compose.web.css.AlignContent 41 | import org.jetbrains.compose.web.css.cssRem 42 | import org.jetbrains.compose.web.css.px 43 | 44 | data class AuthorUIState( 45 | val isLoading: Boolean = true, 46 | val posts: List = emptyList(), 47 | val authorId: String = "", 48 | val authorName: String = "", 49 | val isError: Boolean = false 50 | ) 51 | 52 | @Page("/author") 53 | @Composable 54 | fun Author() { 55 | val context = rememberPageContext() 56 | 57 | var state by remember { mutableStateOf(AuthorUIState()) } 58 | 59 | state = state.copy(authorId = context.route.queryParams[POST_AUTHOR_ID_PARAM].orEmpty()) 60 | 61 | LaunchedEffect(state.authorId) { 62 | state = state.copy(isLoading = true) 63 | document.title = state.authorName 64 | getPostsByAuthorId( 65 | authorId = state.authorId, 66 | onSuccess = { 67 | state = state.copy(isLoading = false, authorName = it.authorName, posts = it.posts) 68 | }, 69 | onError = { 70 | state = state.copy(isLoading = false, isError = true) 71 | } 72 | ) 73 | } 74 | 75 | if (state.isLoading) { 76 | LoadingIndicator() 77 | } else { 78 | PageLayout("Home") { 79 | if (state.isError) { 80 | ErrorView() 81 | return@PageLayout 82 | } 83 | 84 | if (state.posts.isNotEmpty()) { 85 | AuthorPosts(state.authorName, state.posts) 86 | } 87 | } 88 | } 89 | } 90 | 91 | @Composable 92 | private fun AuthorPosts(authorName: String, list: List) { 93 | val breakpoint = rememberBreakpoint() 94 | Row( 95 | modifier = Modifier 96 | .fillMaxWidth() 97 | .padding(bottom = 2.cssRem, top = if (breakpoint < Breakpoint.MD) 1.cssRem else 6.cssRem, leftRight = 6.cssRem), 98 | verticalAlignment = Alignment.CenterVertically 99 | ) { 100 | FaBoltLightning( 101 | modifier = Modifier 102 | .height(20.px) 103 | .color(getSitePalette().primary) 104 | .margin(right = 0.5.cssRem) 105 | .alignContent(AlignContent.Start), 106 | ) 107 | SpanText( 108 | text = authorName, 109 | modifier = Modifier 110 | .textAlign(TextAlign.Center) 111 | .fontWeight(FontWeight.Bold) 112 | .fontSize(1.25.cssRem) 113 | .color(getSitePalette().secondary.toRgb()) 114 | ) 115 | } 116 | 117 | SimpleGrid( 118 | modifier = Modifier.fillMaxWidth().padding(leftRight = if (breakpoint < Breakpoint.MD) 1.cssRem else 6.cssRem), 119 | numColumns = ResponsiveValues(1, 1, 3, 3, 3), 120 | content = { 121 | list.forEach { article -> 122 | LatestArticleItem( 123 | article = article 124 | ) 125 | } 126 | } 127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/pages/category/Category.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.category 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.canerture.androidhub.components.layouts.PageLayout 10 | import com.canerture.androidhub.components.widgets.ErrorView 11 | import com.canerture.androidhub.components.widgets.LoadingIndicator 12 | import com.canerture.androidhub.data.getPostsByCategory 13 | import com.canerture.androidhub.data.model.Post 14 | import com.canerture.androidhub.getSitePalette 15 | import com.canerture.androidhub.pages.index.LatestArticleItem 16 | import com.varabyte.kobweb.compose.css.FontWeight 17 | import com.varabyte.kobweb.compose.css.TextAlign 18 | import com.varabyte.kobweb.compose.foundation.layout.Row 19 | import com.varabyte.kobweb.compose.ui.Alignment 20 | import com.varabyte.kobweb.compose.ui.Modifier 21 | import com.varabyte.kobweb.compose.ui.modifiers.alignContent 22 | import com.varabyte.kobweb.compose.ui.modifiers.color 23 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 24 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 25 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight 26 | import com.varabyte.kobweb.compose.ui.modifiers.height 27 | import com.varabyte.kobweb.compose.ui.modifiers.margin 28 | import com.varabyte.kobweb.compose.ui.modifiers.padding 29 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign 30 | import com.varabyte.kobweb.core.Page 31 | import com.varabyte.kobweb.core.rememberPageContext 32 | import com.varabyte.kobweb.silk.components.icons.fa.FaBoltLightning 33 | import com.varabyte.kobweb.silk.components.layout.SimpleGrid 34 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 35 | import com.varabyte.kobweb.silk.components.style.breakpoint.ResponsiveValues 36 | import com.varabyte.kobweb.silk.components.text.SpanText 37 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint 38 | import kotlinx.browser.document 39 | import org.jetbrains.compose.web.css.AlignContent 40 | import org.jetbrains.compose.web.css.cssRem 41 | import org.jetbrains.compose.web.css.px 42 | 43 | data class CategoryUIState( 44 | val isLoading: Boolean = true, 45 | val posts: List = emptyList(), 46 | val category: String = "", 47 | val isError: Boolean = false 48 | ) 49 | 50 | @Page("/category") 51 | @Composable 52 | fun Category() { 53 | val context = rememberPageContext() 54 | 55 | var state by remember { mutableStateOf(CategoryUIState()) } 56 | 57 | state = state.copy(category = context.route.queryParams["category"].orEmpty()) 58 | 59 | LaunchedEffect(state.category) { 60 | state = state.copy(isLoading = true) 61 | document.title = state.category 62 | getPostsByCategory( 63 | category = state.category, 64 | onSuccess = { posts -> 65 | state = CategoryUIState( 66 | isLoading = false, 67 | posts = posts 68 | ) 69 | }, 70 | onError = { 71 | state = state.copy(isLoading = false, isError = true) 72 | } 73 | ) 74 | } 75 | 76 | if (state.isLoading) { 77 | LoadingIndicator() 78 | } else { 79 | PageLayout("Home") { 80 | if (state.isError) { 81 | ErrorView() 82 | return@PageLayout 83 | } 84 | 85 | if (state.posts.isNotEmpty()) { 86 | CategoryPosts(state.category, state.posts) 87 | } 88 | } 89 | } 90 | } 91 | 92 | @Composable 93 | private fun CategoryPosts(category: String, list: List) { 94 | val breakpoint = rememberBreakpoint() 95 | Row( 96 | modifier = Modifier 97 | .fillMaxWidth() 98 | .padding(bottom = 2.cssRem, top = if (breakpoint < Breakpoint.MD) 1.cssRem else 6.cssRem, leftRight = 6.cssRem), 99 | verticalAlignment = Alignment.CenterVertically 100 | ) { 101 | FaBoltLightning( 102 | modifier = Modifier 103 | .height(20.px) 104 | .color(getSitePalette().primary) 105 | .margin(right = 0.5.cssRem) 106 | .alignContent(AlignContent.Start), 107 | ) 108 | SpanText( 109 | text = category, 110 | modifier = Modifier 111 | .textAlign(TextAlign.Center) 112 | .fontWeight(FontWeight.Bold) 113 | .fontSize(1.25.cssRem) 114 | .color(getSitePalette().secondary.toRgb()) 115 | ) 116 | } 117 | 118 | SimpleGrid( 119 | modifier = Modifier.fillMaxWidth().padding(leftRight = if (breakpoint < Breakpoint.MD) 1.cssRem else 6.cssRem), 120 | numColumns = ResponsiveValues(1, 2, 3, 3, 3), 121 | content = { 122 | list.forEach { article -> 123 | LatestArticleItem( 124 | article = article 125 | ) 126 | } 127 | } 128 | ) 129 | } 130 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/pages/index/IOSHeroContent.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.index 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.canerture.androidhub.common.Res 5 | import com.canerture.androidhub.getSitePalette 6 | import com.varabyte.kobweb.compose.css.AlignContent 7 | import com.varabyte.kobweb.compose.css.FontWeight 8 | import com.varabyte.kobweb.compose.css.TextAlign 9 | import com.varabyte.kobweb.compose.foundation.layout.Column 10 | import com.varabyte.kobweb.compose.foundation.layout.Row 11 | import com.varabyte.kobweb.compose.foundation.layout.Spacer 12 | import com.varabyte.kobweb.compose.ui.Alignment 13 | import com.varabyte.kobweb.compose.ui.Modifier 14 | import com.varabyte.kobweb.compose.ui.modifiers.alignContent 15 | import com.varabyte.kobweb.compose.ui.modifiers.color 16 | import com.varabyte.kobweb.compose.ui.modifiers.display 17 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 18 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight 19 | import com.varabyte.kobweb.compose.ui.modifiers.lineHeight 20 | import com.varabyte.kobweb.compose.ui.modifiers.margin 21 | import com.varabyte.kobweb.compose.ui.modifiers.padding 22 | import com.varabyte.kobweb.compose.ui.modifiers.size 23 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign 24 | import com.varabyte.kobweb.silk.components.graphics.Image 25 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 26 | import com.varabyte.kobweb.silk.components.text.SpanText 27 | import org.jetbrains.compose.web.css.Color 28 | import org.jetbrains.compose.web.css.DisplayStyle 29 | import org.jetbrains.compose.web.css.cssRem 30 | import org.jetbrains.compose.web.css.px 31 | 32 | @Composable 33 | fun AndroidHeroContent( 34 | breakpoint: Breakpoint 35 | ) { 36 | if (breakpoint < Breakpoint.SM) { 37 | Column( 38 | horizontalAlignment = Alignment.CenterHorizontally 39 | ) { 40 | Image( 41 | src = Res.Image.IOS_FIGURE, 42 | description = "iOS Figure", 43 | modifier = Modifier 44 | .size(140.px, 160.px) 45 | .display(DisplayStyle.Block) 46 | ) 47 | TextArea( 48 | breakpoint = breakpoint, 49 | modifier = Modifier 50 | .padding(leftRight = 2.cssRem) 51 | .alignContent(AlignContent.Center) 52 | .margin(top = 64.px) 53 | ) 54 | } 55 | } else { 56 | Row( 57 | modifier = Modifier.padding(leftRight = 13.cssRem), 58 | verticalAlignment = Alignment.CenterVertically 59 | ) { 60 | TextArea(breakpoint) 61 | Image( 62 | src = Res.Image.IOS_FIGURE, 63 | description = "iOS Figure", 64 | modifier = Modifier 65 | .size(260.px) 66 | .margin(left = 2.cssRem) 67 | .display(DisplayStyle.Block) 68 | ) 69 | } 70 | } 71 | } 72 | 73 | @Composable 74 | private fun TextArea(breakpoint: Breakpoint, modifier: Modifier = Modifier) { 75 | Column( 76 | modifier = modifier 77 | ) { 78 | SpanText( 79 | text = "Hi \uD83D\uDC4B", 80 | modifier = Modifier 81 | .fontWeight(FontWeight.Bold) 82 | .textAlign(TextAlign.Start) 83 | .color(getSitePalette().primary) 84 | .fontSize(if (breakpoint < Breakpoint.MD) 18.px else 28.px) 85 | ) 86 | Spacer() 87 | SpanText( 88 | text = "Let’s explore everything about the exciting world of iOS together. On this platform, you can delve into the vast opportunities offered by the iOS operating system and examine the latest developments in the field. Come on, let’s discover the limitless world of iOS together!", 89 | modifier = Modifier 90 | .textAlign(TextAlign.Start) 91 | .color(Color.gray) 92 | .fontSize(if (breakpoint < Breakpoint.MD) 14.px else 20.px) 93 | .lineHeight(2) 94 | .padding(right = 2.cssRem) 95 | ) 96 | } 97 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/pages/search/Search.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.pages.search 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.canerture.androidhub.components.layouts.PageLayout 10 | import com.canerture.androidhub.components.widgets.ErrorView 11 | import com.canerture.androidhub.components.widgets.LoadingIndicator 12 | import com.canerture.androidhub.data.model.Post 13 | import com.canerture.androidhub.data.searchPostsByTitle 14 | import com.canerture.androidhub.getSitePalette 15 | import com.canerture.androidhub.pages.index.LatestArticleItem 16 | import com.varabyte.kobweb.compose.css.FontWeight 17 | import com.varabyte.kobweb.compose.css.TextAlign 18 | import com.varabyte.kobweb.compose.foundation.layout.Row 19 | import com.varabyte.kobweb.compose.ui.Alignment 20 | import com.varabyte.kobweb.compose.ui.Modifier 21 | import com.varabyte.kobweb.compose.ui.modifiers.alignContent 22 | import com.varabyte.kobweb.compose.ui.modifiers.color 23 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 24 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 25 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight 26 | import com.varabyte.kobweb.compose.ui.modifiers.height 27 | import com.varabyte.kobweb.compose.ui.modifiers.margin 28 | import com.varabyte.kobweb.compose.ui.modifiers.padding 29 | import com.varabyte.kobweb.compose.ui.modifiers.textAlign 30 | import com.varabyte.kobweb.core.Page 31 | import com.varabyte.kobweb.core.rememberPageContext 32 | import com.varabyte.kobweb.silk.components.icons.fa.FaBoltLightning 33 | import com.varabyte.kobweb.silk.components.layout.SimpleGrid 34 | import com.varabyte.kobweb.silk.components.style.breakpoint.Breakpoint 35 | import com.varabyte.kobweb.silk.components.style.breakpoint.ResponsiveValues 36 | import com.varabyte.kobweb.silk.components.text.SpanText 37 | import com.varabyte.kobweb.silk.theme.breakpoint.rememberBreakpoint 38 | import kotlinx.browser.document 39 | import org.jetbrains.compose.web.css.AlignContent 40 | import org.jetbrains.compose.web.css.cssRem 41 | import org.jetbrains.compose.web.css.px 42 | 43 | data class SearchUIState( 44 | val isLoading: Boolean = true, 45 | val posts: List = emptyList(), 46 | val query: String = "", 47 | val isError: Boolean = false, 48 | ) 49 | 50 | @Page("/search") 51 | @Composable 52 | fun Search() { 53 | val context = rememberPageContext() 54 | 55 | var state by remember { mutableStateOf(SearchUIState()) } 56 | 57 | state = state.copy(query = context.route.queryParams["query"].orEmpty()) 58 | 59 | LaunchedEffect(state.query) { 60 | state = state.copy(isLoading = true) 61 | document.title = state.query 62 | searchPostsByTitle( 63 | title = state.query, 64 | onSuccess = { posts -> 65 | state = SearchUIState( 66 | isLoading = false, 67 | posts = posts 68 | ) 69 | }, 70 | onError = { 71 | state = state.copy( 72 | isLoading = false, 73 | isError = true 74 | ) 75 | } 76 | ) 77 | } 78 | 79 | if (state.isLoading) { 80 | LoadingIndicator() 81 | } else { 82 | PageLayout("Search") { 83 | if (state.isError) { 84 | ErrorView() 85 | return@PageLayout 86 | } 87 | 88 | if (state.posts.isNotEmpty()) { 89 | SearchPosts(state.query, state.posts) 90 | } 91 | } 92 | } 93 | } 94 | 95 | @Composable 96 | private fun SearchPosts(query: String, list: List) { 97 | val breakpoint = rememberBreakpoint() 98 | Row( 99 | modifier = Modifier 100 | .fillMaxWidth() 101 | .padding(bottom = 2.cssRem, top = if (breakpoint < Breakpoint.MD) 1.cssRem else 6.cssRem, leftRight = 6.cssRem), 102 | verticalAlignment = Alignment.CenterVertically 103 | ) { 104 | FaBoltLightning( 105 | modifier = Modifier 106 | .height(if (breakpoint < Breakpoint.MD) 10.px else 20.px) 107 | .color(getSitePalette().primary) 108 | .margin(right = 0.5.cssRem) 109 | .alignContent(AlignContent.Center), 110 | ) 111 | SpanText( 112 | text = "${list.size} posts found for \"$query\"", 113 | modifier = Modifier 114 | .textAlign(TextAlign.Center) 115 | .fontWeight(FontWeight.Bold) 116 | .fontSize(if (breakpoint < Breakpoint.MD) 14.px else 18.px) 117 | .color(getSitePalette().secondary.toRgb()) 118 | ) 119 | } 120 | 121 | SimpleGrid( 122 | modifier = Modifier.fillMaxWidth().padding(leftRight = if (breakpoint < Breakpoint.MD) 1.cssRem else 6.cssRem), 123 | numColumns = ResponsiveValues(1, 1, 3, 3, 3), 124 | content = { 125 | list.forEach { article -> 126 | LatestArticleItem( 127 | article = article 128 | ) 129 | } 130 | } 131 | ) 132 | } 133 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/kotlin/com/canerture/androidhub/utils/ApiUtils.kt: -------------------------------------------------------------------------------- 1 | package com.canerture.androidhub.utils 2 | 3 | import com.canerture.androidhub.common.Constants 4 | import com.canerture.androidhub.data.model.BaseResponse 5 | import com.varabyte.kobweb.browser.http.HttpMethod 6 | import com.varabyte.kobweb.browser.http.fetch 7 | import com.varabyte.kobweb.browser.http.http 8 | import kotlinx.browser.window 9 | import kotlinx.serialization.decodeFromString 10 | import kotlinx.serialization.json.Json 11 | 12 | object ApiUtils { 13 | inline fun safeApiCall( 14 | call: () -> ByteArray, 15 | onSuccess: (T) -> Unit, 16 | onError: (String) -> Unit, 17 | ) { 18 | try { 19 | val response = call().parseData>() 20 | if (response.status == 200) { 21 | onSuccess(response.data!!) 22 | } else { 23 | onError(response.message.orEmpty()) 24 | } 25 | } catch (e: Exception) { 26 | onError("Something went wrong!") 27 | } 28 | } 29 | 30 | suspend fun get(path: String, param: Pair? = null): ByteArray { 31 | val url = if (param == null) { 32 | Constants.BASE_URL.plus(path) 33 | } else { 34 | Constants.BASE_URL.plus(path).plus("?${param.first}=${param.second}") 35 | } 36 | return window.http.get(url) 37 | } 38 | 39 | suspend fun post(path: String, body: ByteArray): ByteArray { 40 | return window.http.post( 41 | resource = Constants.BASE_URL.plus(path), 42 | body = body 43 | ) 44 | } 45 | 46 | suspend fun put(path: String, body: ByteArray): ByteArray { 47 | return window.http.put( 48 | resource = Constants.BASE_URL.plus(path), 49 | body = body 50 | ) 51 | } 52 | 53 | suspend fun delete(path: String, param: Pair? = null): ByteArray { 54 | val url = if (param == null) { 55 | Constants.BASE_URL.plus(path) 56 | } else { 57 | Constants.BASE_URL.plus(path).plus("?${param.first}=${param.second}") 58 | } 59 | return window.http.delete(url) 60 | } 61 | 62 | inline fun ByteArray?.parseData(): T { 63 | val json = this?.decodeToString().orEmpty() 64 | return Json.decodeFromString(json) 65 | } 66 | } -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/markdown/About.md: -------------------------------------------------------------------------------- 1 | --- 2 | root: .components.layouts.MarkdownLayout("About") 3 | --- 4 | 5 | # About this template 6 | 7 | This template is intended to both demonstrate some fundamentals of the Kobweb framework and act a 8 | starting point you can 9 | build your own site from. 10 | 11 | --- 12 | 13 | ## Learn 14 | 15 | If this is your first time using Kobweb, please open this site's project in an IDE and take a few 16 | minutes to look around 17 | the code. 18 | 19 | ### Files 20 | 21 | #### AppEntry.kt 22 | 23 | This file declares a method that is an entry point for all pages on your site. You can rename the 24 | file and the method if 25 | you like. Kobweb searches for a single method at compile time annotated with `@App`. 26 | 27 | #### AppStyles.kt 28 | 29 | An example of declaring generally useful styles that can be used anywhere across the whole site. 30 | Otherwise, you normally 31 | declare styles close to the widget that uses them. 32 | 33 | #### SiteTheme.kt 34 | 35 | An example of how to define some site-specific colors, effectively extending the palette provided by 36 | Silk. 37 | 38 | #### components/ 39 | 40 | By convention, Kobweb codebases organize reusable site components under this folder. Within it, you 41 | have: 42 | 43 | * `layout/`
44 | Represents top-level organization for pages 45 | * `sections/`
46 | Areas of content that appear across multiple pages (such as nav bars and footers) 47 | * `widgets/`
48 | Home for low-level UI pieces that you can use around your site 49 | 50 | #### pages/ 51 | 52 | Any `@Composable` under this folder additionally tagged with `@Page` will have a route generated for 53 | it automatically. 54 | Defining a page outside of this folder will be flagged as an error by the Kobweb Gradle plugin at 55 | compile time. Note 56 | that additional pages (like this one!) might live under the `resources/markdown` folder. 57 | 58 | #### resources/ 59 | 60 | * `public`
61 | If you want to host any media on your site (such as an icon, an image, text configuration files, 62 | movies, fonts, etc.), 63 | you should put it under this folder. 64 | * `markdown`
65 | Any markdown discovered in here by Kobweb at compile time will be converted into pages on your 66 | site. 67 | 68 | ### Classes 69 | 70 | The Kobweb and Silk APIs introduce a lot of powerful concepts. Here are some of the most important 71 | ones to know about 72 | which you can find used throughout this template. 73 | 74 | #### Modifier 75 | 76 | Kobweb introduces the `Modifier` keyword that Android developers will recognize from the Jetpack 77 | Compose API. In a 78 | webdev context, this is used for setting CSS styles and html attributes on elements in the page. 79 | 80 | #### ComponentStyle 81 | 82 | Traditional HTML pages use CSS to style their UI. In Kobweb, these styles can be declared using 83 | the `ComponentStyle` 84 | class in a Kotlin-idiomatic way. You can find examples of component styles used throughout the 85 | template. 86 | 87 | #### ComponentVariant 88 | 89 | You can generate variants from component styles, which are ways to take base component styles and 90 | tweak them further. 91 | 92 | #### Keyframes 93 | 94 | You can create animations by declaring keyframes for them. 95 | 96 | ## Starting Point 97 | 98 | This template aims to create some generally useful pieces that most sites will want to use. Making 99 | your own site could 100 | be as easy as deleting this *About* page and working from there. However, you are welcome to modify 101 | or delete anything 102 | you find in the template that you don't plan to use in your final site. 103 | 104 | If instead you'd like to start from scratch, you can run 105 | 106 | ``` 107 | $ kobweb create app/empty 108 | ``` 109 | 110 | which will create a new project with nothing inside of it except for a minimal, skeletal structure. 111 | 112 | ## Export and Deploy 113 | 114 | When you are ready to share your site with the world, you'll want to export it first. This will 115 | create a production 116 | snapshot of your site. 117 | 118 | There are two flavors of Kobweb sites: *static layout* and *full stack*. You 119 | can [read more about these choices here](https://github.com/varabyte/kobweb#static-layout-vs-full-stack-sites). 120 | 121 | For most sites, a static layout site is what you want, so to do that, return to the command line and 122 | run: 123 | 124 | ``` 125 | $ kobweb export --layout static 126 | ``` 127 | 128 | After that runs for a little while, your production site should be generated! You can find the files 129 | under the 130 | `.kobweb/site` folder. 131 | 132 | Test it locally by running: 133 | 134 | ``` 135 | $ kobweb run --layout static --env prod 136 | ``` 137 | 138 | If you're satisfied, you can upload your site files to the static website host provider of your 139 | choice. Each provider 140 | has its own instructions for how it discovers your files, so please refer to their documentation. 141 | 142 | You can [read this blog post](https://bitspittle.dev/blog/2022/staticdeploy) for some concrete 143 | examples of exporting a 144 | Kobweb site to two popular static website hosting providers. 145 | -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/public/bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/src/jsMain/resources/public/bold.png -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/public/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/src/jsMain/resources/public/code.png -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/src/jsMain/resources/public/favicon.ico -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/src/jsMain/resources/public/image.png -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/public/ios-figure.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/src/jsMain/resources/public/ios-figure.webp -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/public/ioshub-logo-dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/src/jsMain/resources/public/ioshub-logo-dark.webp -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/public/ioshub-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/src/jsMain/resources/public/ioshub-logo.webp -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/public/italic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/src/jsMain/resources/public/italic.png -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/public/jetpack.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/src/jsMain/resources/public/jetpack.webp -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/public/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/src/jsMain/resources/public/link.png -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/public/quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/src/jsMain/resources/public/quote.png -------------------------------------------------------------------------------- /site-ios/src/jsMain/resources/public/title-subtitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnrture/AndroidHub/227823ff53b5344cb9784e7acf2baa482283c4df/site-ios/src/jsMain/resources/public/title-subtitle.png --------------------------------------------------------------------------------