KobWeb integration
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user