Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
orchestr7 committed Jun 3, 2024
1 parent 401112a commit c06e425
Show file tree
Hide file tree
Showing 45 changed files with 5,399 additions and 1 deletion.
31 changes: 31 additions & 0 deletions .github/workflows/custom-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Build and Publish
on: [ push, pull_request ]
jobs:
build:
name: Test and Build
runs-on: ubuntu-latest
steps:

# Setup Java 1.8 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8

# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2

# Build application
- name: Test and Build
run: ./gradlew build

# If main branch update, deploy to gh-pages
- name: Deploy
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main'
uses: JamesIves/[email protected]
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: build/js/packages/thetax-frontend # The folder the action should deploy.
CLEAN: true # Automatically remove deleted files from the deploy
46 changes: 46 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,49 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*

.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/

### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/

### Mac OS ###
.DS_Store

.kotlin
.idea
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# thetax.ru
# TheTax.ru

Tax calculator with a huge reference to https://github.com/stevermeister/dutch-tax-income-calculator. Small playground for @akuleshov7 to play with KotlinJS/React.
7 changes: 7 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugins {
java
}

group = "ru.thetax"
version = "0.0.1"

17 changes: 17 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
plugins {
kotlin("multiplatform") version("2.0.0")
}

kotlin {
jvm()
js(IR) {
browser()
useCommonJs()
}

sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
47 changes: 47 additions & 0 deletions common/src/commonMain/kotlin/ru.thetax/calculator/TaxCalculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package ru.thetax.calculator

const val selfEmployedTax = 0.06
const val nonResidentTax = 0.30

data class TaxDetail(
val taxRate: TaxRates, val amount: Double
)

class TaxCalculator(private val income: Double, isResident: Boolean = true, isSelfEmployed: Boolean = false) {
lateinit var taxDetails: List<TaxDetail>
var totalTax: Double = 0.0

init {
when {
isSelfEmployed -> {
totalTax = income * selfEmployedTax
taxDetails = listOf(TaxDetail(TaxRates.SELF_EMPLOYED, income * selfEmployedTax))
}

!isResident -> {
totalTax = income * nonResidentTax
taxDetails = listOf(TaxDetail(TaxRates.NON_RESIDENT, income * nonResidentTax))
}

else -> calculateTaxForRegularGuy()
}
}

private fun calculateTaxForRegularGuy() {
var totalTax = 0.0
var previousLimit = 0.0
val taxDetails = mutableListOf<TaxDetail>()

for (rate in TaxRates.entries.toTypedArray().filterNot { it.limit.isNaN() }.sortedBy { it.limit }) {
if (income <= previousLimit) break
val taxableIncome = minOf(income, rate.limit) - previousLimit
val taxAmount = taxableIncome * rate.rate
taxDetails.add(TaxDetail(rate, taxAmount))
totalTax += taxAmount
previousLimit = rate.limit
}

this.totalTax = totalTax
this.taxDetails = taxDetails
}
}
11 changes: 11 additions & 0 deletions common/src/commonMain/kotlin/ru.thetax/calculator/TaxRates.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ru.thetax.calculator

enum class TaxRates(val rate: Double, val limit: Double) {
SELF_EMPLOYED(0.06, Double.NaN),
NON_RESIDENT(0.30, Double.NaN),
RATE_13(0.13, 2400000.0),
RATE_15(0.15,5000000.0),
RATE_18(0.18,20000000.0),
RATE_20(0.20,50000000.0),
RATE_22(0.22, Double.POSITIVE_INFINITY),
}
22 changes: 22 additions & 0 deletions common/src/commonTest/kotlin/ru.thetax/TaxCalculatorTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ru.thetax

import ru.thetax.calculator.TaxCalculator
import ru.thetax.calculator.TaxDetail
import ru.thetax.calculator.TaxRates
import kotlin.test.Test
import kotlin.test.assertEquals

class TaxCalculatorTest {
@Test
fun basicScenarios() {
assertEquals(13000.0, TaxCalculator(100000.0).totalTax)

assertEquals(
listOf(
TaxDetail(TaxRates.RATE_13, 13000.0),
),

TaxCalculator(100000.0).taxDetails
)
}
}
93 changes: 93 additions & 0 deletions frontend/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import org.jetbrains.kotlin.js.inline.clean.removeDuplicateImports

plugins {
kotlin("multiplatform") version ("2.0.0")
}

kotlin {
// https://kotlinlang.org/docs/js-project-setup.html#execution-environments
js(IR) {
browser {
// https://kotlinlang.org/docs/js-project-setup.html#css
commonWebpackConfig {
cssSupport {
enabled.set(true)
}
}
}
// kotlin-wrapper migrates to commonjs and have no @JsNonModule annotations
// https://github.com/JetBrains/kotlin-wrappers/issues/1935
useCommonJs()
// already default for LEGACY, but explicitly needed for IR:
binaries.executable()
}

sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}

jsMain.dependencies {
api(project(":common"))
implementation(project.dependencies.enforcedPlatform(libs.kotlin.wrappers.bom))
implementation("org.jetbrains.kotlin-wrappers:kotlin-react")
implementation("org.jetbrains.kotlin-wrappers:kotlin-extensions")
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom")
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-router-dom")
implementation("org.jetbrains.kotlin-wrappers:kotlin-tanstack-react-table")


// ====== unknown technical dependencies
compileOnly(devNpm("sass", "^1.43.0"))
compileOnly(devNpm("sass-loader", "^12.0.0"))
compileOnly(devNpm("style-loader", "^3.3.1"))
compileOnly(devNpm("css-loader", "^6.5.0"))
compileOnly(devNpm("file-loader", "^6.2.0"))
// https://getbootstrap.com/docs/4.0/getting-started/webpack/#importing-precompiled-sass
compileOnly(devNpm("postcss-loader", "^6.2.1"))
compileOnly(devNpm("postcss", "^8.2.13"))
// See https://stackoverflow.com/a/72828500; newer versions are supported only for Bootstrap 5.2+
compileOnly(devNpm("autoprefixer", "10.4.5"))
compileOnly(devNpm("webpack-bundle-analyzer", "^4.5.0"))
compileOnly(devNpm("mini-css-extract-plugin", "^2.6.0"))
compileOnly(devNpm("html-webpack-plugin", "^5.5.0"))

implementation(npm("os-browserify", "^0.3.0"))
implementation(npm("path-browserify", "^1.0.1"))

// ====== used modules ======
implementation(npm("bootstrap", "5.3.3"))
implementation(npm("react", "^18.0.0"))
implementation(npm("react-dom", "^18.0.0"))
implementation(npm("react-modal", "^3.0.0"))
implementation(npm("@popperjs/core", "2.11.8"))
implementation(npm("animate.css", "^4.1.1"))
// ====== font awesome ======
implementation(npm("@fortawesome/fontawesome-svg-core", "^1.2.36"))
implementation(npm("@fortawesome/free-solid-svg-icons", "5.15.3"))
implementation(npm("@fortawesome/free-brands-svg-icons", "5.15.3"))
implementation(npm("@fortawesome/react-fontawesome", "^0.1.16"))
}
}
}

// somehow index.html started to duplicate after I added a common module
// "Entry index.html is a duplicate but no duplicate handling strategy has been set."
tasks.named<Copy>("jsBrowserDistribution") {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

tasks.withType<org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack> {
// Since we inject timestamp into HTML file, we would like this task to always be re-run.
inputs.property("Build timestamp", System.currentTimeMillis())
doFirst {
val additionalWebpackResources = fileTree("$buildDir/processedResources/js/main/") {
include("scss/**")
include("index.html")
}
copy {
from(additionalWebpackResources)
into("${rootProject.buildDir}/js/packages/${rootProject.name}-${project.name}")
}
}
}
54 changes: 54 additions & 0 deletions frontend/src/jsMain/kotlin/ru/thetax/App.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Main entrypoint
*/

package ru.thetax


import js.objects.jso
import react.*
import react.dom.client.createRoot

import web.dom.document
import web.html.HTMLElement

import kotlinx.browser.window
import react.router.dom.RouterProvider
import react.router.dom.createBrowserRouter
import ru.thetax.views.taxCalculatorView


/**
* Main component for the whole App
*/
@JsExport
@OptIn(ExperimentalJsExport::class)
val App: FC<Props> = FC {
RouterProvider {
router = createBrowserRouter(
routes = arrayOf(
jso {
path = "/"
element = taxCalculatorView.create()
}
)
)
}
}

fun main() {
/* Workaround for issue: https://youtrack.jetbrains.com/issue/KT-31888 */
@Suppress("UnsafeCastFromDynamic")
if (window.asDynamic().__karma__) {
return
}
// this is needed for webpack to include resources
kotlinext.js.require<dynamic>("../scss/tax-app.scss")
// this is needed for webpack to include bootstrap
kotlinext.js.require<dynamic>("bootstrap")
/* ReactModal.setAppElement(document.getElementById("wrapper") as HTMLElement) // required for accessibility in react-modal
initI18n()*/
val mainDiv = document.getElementById("wrapper") as HTMLElement
createRoot(mainDiv).render(App.create())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* External declarations of icons from fontawesome-solid
*/

package ru.thetax.common.externals.fontawesome

@JsModule("@fortawesome/free-brands-svg-icons/faGithub")
@JsNonModule
external val faGithub: FontAwesomeIconModule

@JsModule("@fortawesome/free-brands-svg-icons/faTwitter")
@JsNonModule
external val faTwitter: FontAwesomeIconModule

@JsModule("@fortawesome/free-brands-svg-icons/faLinkedinIn")
@JsNonModule
external val faLinkedIn: FontAwesomeIconModule
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* External declarations from fontawesome-svg-core
*/

@file:JsModule("@fortawesome/fontawesome-svg-core")
@file:JsNonModule

package ru.thetax.common.externals.fontawesome

external val library: dynamic
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@file:JsModule("@fortawesome/react-fontawesome")
@file:JsNonModule

package ru.thetax.common.externals.fontawesome

import react.Component
import react.ReactElement
import react.State

/**
* External declaration of [FontAwesomeIcon] react component
*/
external class FontAwesomeIcon : Component<FontAwesomeIconProps, State> {
override fun render(): ReactElement<FontAwesomeIconProps>?
}
Loading

0 comments on commit c06e425

Please sign in to comment.