KobWeb integration

This commit is contained in:
stefan
2025-09-09 17:43:31 +02:00
parent 599c1e8bcb
commit 0ba27e7e87
29 changed files with 990 additions and 2011 deletions
@@ -1,125 +0,0 @@
@file:OptIn(org.jetbrains.compose.web.ExperimentalComposeWebApi::class)
package at.mocode.client.web
import org.jetbrains.compose.web.css.*
object AppStylesheet : StyleSheet() {
val container by style {
display(DisplayStyle.Flex)
flexDirection(FlexDirection.Column)
minHeight(100.vh)
fontFamily("'Segoe UI', system-ui, sans-serif")
margin(0.px)
padding(0.px)
backgroundColor(Color("#f5f5f5"))
}
val header by style {
backgroundColor(Color("#1976d2"))
color(Color.white)
padding(20.px)
textAlign("center")
property("box-shadow", "0 2px 4px rgba(0,0,0,0.1)")
}
val main by style {
flex(1)
display(DisplayStyle.Flex)
justifyContent(JustifyContent.Center)
alignItems(AlignItems.Center)
padding(40.px, 20.px)
}
val footer by style {
backgroundColor(Color("#333"))
color(Color.white)
textAlign("center")
padding(20.px)
fontSize(14.px)
}
val card by style {
backgroundColor(Color.white)
borderRadius(12.px)
property("box-shadow", "0 4px 6px rgba(0, 0, 0, 0.1)")
padding(32.px)
maxWidth(500.px)
width(100.percent)
textAlign("center")
}
val button by style {
border(0.px)
borderRadius(8.px)
padding(12.px, 24.px)
fontSize(16.px)
fontWeight("bold")
cursor("pointer")
property("transition", "all 0.2s ease")
width(100.percent)
marginBottom(20.px)
// Improved focus management using property
property("&:focus", "outline: 2px solid #1976d2; outline-offset: 2px; box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.2);")
// Enhanced active state
property("&:active", "transform: scale(0.98);")
}
val buttonHover by style {
transform { scale(1.02) }
property("box-shadow", "0 2px 8px rgba(0, 0, 0, 0.15)")
}
val buttonDisabled by style {
opacity(0.6)
cursor("not-allowed")
property("transform", "none")
property("box-shadow", "none")
}
val primaryButton by style {
backgroundColor(Color("#1976d2"))
color(Color.white)
hover(self) style {
backgroundColor(Color("#1565c0"))
property("box-shadow", "0 4px 12px rgba(25, 118, 210, 0.3)")
}
// Using property for disabled state
property("&:disabled", "background-color: #bbbbbb; cursor: not-allowed;")
}
val successMessage by style {
backgroundColor(Color("#e8f5e8"))
color(Color("#2e7d32"))
padding(16.px)
borderRadius(8.px)
marginTop(16.px)
border(1.px, LineStyle.Solid, Color("#c8e6c9"))
}
val errorMessage by style {
backgroundColor(Color("#ffebee"))
color(Color("#c62828"))
padding(16.px)
borderRadius(8.px)
marginTop(16.px)
border(1.px, LineStyle.Solid, Color("#ffcdd2"))
}
val spinner by style {
display(DisplayStyle.InlineBlock)
width(16.px)
height(16.px)
border(2.px, LineStyle.Solid, Color("#f3f3f3"))
property("border-top", "2px solid #1976d2")
borderRadius(50.percent)
property("animation", "spin 1s linear infinite")
marginRight(8.px)
property("vertical-align", "middle")
}
}
@@ -1,201 +0,0 @@
package at.mocode.client.web
import androidx.compose.runtime.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable
import at.mocode.client.data.service.PingService
import at.mocode.client.ui.viewmodel.PingViewModel
import at.mocode.client.ui.viewmodel.PingUiState
fun main() {
// Catch any initialization errors and display user-friendly error
try {
renderComposable(rootElementId = "root") {
Style(AppStylesheet)
MeldestelleWebApp()
}
} catch (e: Exception) {
console.error("Failed to initialize Meldestelle Web App", e)
// Fallback error display
val rootElement = js("document.getElementById('root')")
if (rootElement != null) {
val errorHtml = """
<div style="display: flex; justify-content: center; align-items: center; height: 100vh; flex-direction: column; font-family: system-ui;">
<h1 style="color: #c62828; margin-bottom: 16px;">⚠️ Fehler beim Laden</h1>
<p style="color: #666; text-align: center;">Die Anwendung konnte nicht geladen werden.<br>Bitte laden Sie die Seite neu oder kontaktieren Sie den Support.</p>
<button onclick="window.location.reload()" style="margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer;">Seite neu laden</button>
</div>
""".trimIndent()
js("rootElement.innerHTML = errorHtml")
}
}
}
@Composable
fun MeldestelleWebApp() {
// Get baseUrl from window location with error handling
val baseUrl = remember {
try {
js("window.location.origin").toString().ifEmpty { "http://localhost:8080" }
} catch (e: Exception) {
console.warn("Could not get window location, using default", e)
"http://localhost:8080"
}
}
// Create services with proper error handling
val pingService = remember(baseUrl) {
try {
PingService(baseUrl)
} catch (e: Exception) {
console.error("Failed to create PingService", e)
throw e
}
}
val viewModel = remember(pingService) {
try {
PingViewModel(pingService)
} catch (e: Exception) {
console.error("Failed to create PingViewModel", e)
throw e
}
}
// Ensure proper cleanup on component disposal
DisposableEffect(viewModel) {
onDispose {
try {
viewModel.dispose()
} catch (e: Exception) {
console.warn("Error during ViewModel disposal", e)
}
}
}
Div(attrs = {
classes(AppStylesheet.container)
attr("role", "application")
attr("aria-label", "Meldestelle Web Application")
}) {
Header(attrs = {
classes(AppStylesheet.header)
attr("role", "banner")
}) {
H1(attrs = {
attr("id", "app-title")
}) {
Text("Meldestelle Web App")
}
}
Main(attrs = {
classes(AppStylesheet.main)
attr("role", "main")
attr("aria-labelledby", "app-title")
}) {
PingTestWebView(
state = viewModel.uiState,
onTestConnection = { viewModel.pingBackend() }
)
}
Footer(attrs = {
classes(AppStylesheet.footer)
attr("role", "contentinfo")
}) {
P { Text("© 2025 Meldestelle - Powered by Kotlin Multiplatform") }
}
}
}
@Composable
fun PingTestWebView(
state: PingUiState,
onTestConnection: () -> Unit
) {
Div(attrs = {
classes(AppStylesheet.card)
attr("role", "region")
attr("aria-labelledby", "ping-test-title")
}) {
H2(attrs = {
attr("id", "ping-test-title")
}) {
Text("Backend Verbindungstest")
}
Button(
attrs = {
classes(AppStylesheet.button, AppStylesheet.primaryButton)
if (state is PingUiState.Loading) {
attr("disabled", "")
attr("aria-disabled", "true")
}
attr("aria-describedby", "ping-status")
attr("type", "button")
onClick { onTestConnection() }
}
) {
if (state is PingUiState.Loading) {
Span(attrs = {
classes(AppStylesheet.spinner)
attr("aria-hidden", "true")
}) {}
Text(" Pinge Backend...")
} else {
Text("Ping Backend")
}
}
// Status display with four distinct states and proper announcements
Div(attrs = {
attr("id", "ping-status")
attr("role", "status")
attr("aria-live", "polite")
attr("aria-atomic", "true")
}) {
when (state) {
is PingUiState.Initial -> {
Div(attrs = {
attr("aria-label", "Bereit für Backend-Test")
}) {
Text("Klicke auf den Button, um das Backend zu testen")
}
}
is PingUiState.Loading -> {
Div(attrs = {
attr("aria-label", "Backend wird getestet")
}) {
Span(attrs = {
classes(AppStylesheet.spinner)
attr("aria-hidden", "true")
}) {}
Text(" Pinge Backend ...")
}
}
is PingUiState.Success -> {
Div(attrs = {
classes(AppStylesheet.successMessage)
attr("role", "alert")
attr("aria-label", "Backend-Test erfolgreich")
}) {
Span(attrs = { attr("aria-hidden", "true") }) { Text("") }
Text("Antwort vom Backend: ${state.response.status}")
}
}
is PingUiState.Error -> {
Div(attrs = {
classes(AppStylesheet.errorMessage)
attr("role", "alert")
attr("aria-label", "Backend-Test fehlgeschlagen")
}) {
Span(attrs = { attr("aria-hidden", "true") }) { Text("") }
Text("Fehler: ${state.message}")
}
}
}
}
}
}
@@ -1,89 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- App Identity -->
<title>Meldestelle Web App</title>
<meta name="description" content="Meldestelle - Vereinsverwaltung für Pferdesport">
<meta name="keywords" content="Meldestelle, Pferdesport, Vereinsverwaltung, Turnier">
<meta name="author" content="Meldestelle Team">
<!-- PWA Support -->
<meta name="theme-color" content="#1976d2">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Meldestelle">
<link rel="manifest" href="/manifest.json">
<!-- Icons -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<!-- Security -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:* ws://localhost:*">
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<meta http-equiv="X-Frame-Options" content="DENY">
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
<!-- Performance -->
<link rel="preconnect" href="http://localhost:8080">
<link rel="dns-prefetch" href="http://localhost:8080">
<!-- Reset & Base Styles -->
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
background-color: #f5f5f5;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
flex-direction: column;
gap: 20px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #e0e0e0;
border-top: 4px solid #1976d2;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
color: #666;
font-size: 16px;
}
</style>
</head>
<body>
<div id="root">
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">Meldestelle wird geladen...</div>
</div>
</div>
<script src="web-app.js"></script>
</body>
</html>
@@ -1,69 +0,0 @@
{
"name": "Meldestelle - Vereinsverwaltung",
"short_name": "Meldestelle",
"description": "Meldestelle - Vereinsverwaltung für Pferdesport",
"start_url": "/",
"display": "standalone",
"orientation": "portrait-primary",
"theme_color": "#1976d2",
"background_color": "#f5f5f5",
"categories": ["sports", "productivity", "utilities"],
"lang": "de",
"icons": [
{
"src": "/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/screenshot-desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
},
{
"src": "/screenshot-mobile.png",
"sizes": "390x844",
"type": "image/png",
"form_factor": "narrow"
}
],
"prefer_related_applications": false,
"shortcuts": [
{
"name": "Backend Test",
"description": "Backend Verbindung testen",
"url": "/?action=ping",
"icons": [
{
"src": "/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
}
]
}
]
}