From e0939c816161b41523255ae09499741a6a9b9988 Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Mon, 27 Apr 2026 11:51:43 +0200 Subject: [PATCH 1/3] feat: polish landing page with sticky nav, animations, and enhanced metrics layout --- .../service/PublicStatsService.java | 29 ++++++ .../presentation/web/UiController.java | 8 +- src/main/resources/static/app.css | 94 +++++++++++++++++++ src/main/resources/static/app.js | 12 +++ src/main/resources/templates/landing.html | 26 +++++ 5 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/example/projektarendehantering/application/service/PublicStatsService.java diff --git a/src/main/java/org/example/projektarendehantering/application/service/PublicStatsService.java b/src/main/java/org/example/projektarendehantering/application/service/PublicStatsService.java new file mode 100644 index 0000000..21d3895 --- /dev/null +++ b/src/main/java/org/example/projektarendehantering/application/service/PublicStatsService.java @@ -0,0 +1,29 @@ +package org.example.projektarendehantering.application.service; + +import lombok.RequiredArgsConstructor; +import org.example.projektarendehantering.infrastructure.persistence.CaseRepository; +import org.example.projektarendehantering.infrastructure.persistence.EmployeeRepository; +import org.example.projektarendehantering.infrastructure.persistence.PatientRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class PublicStatsService { + + private final CaseRepository caseRepository; + private final PatientRepository patientRepository; + private final EmployeeRepository employeeRepository; + + @Transactional(readOnly = true) + public Map getPublicStats() { + Map stats = new HashMap<>(); + stats.put("totalCases", caseRepository.count()); + stats.put("totalPatients", patientRepository.count()); + stats.put("totalEmployees", employeeRepository.count()); + return stats; + } +} diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java b/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java index 57efd1d..5614167 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java +++ b/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java @@ -3,6 +3,7 @@ import org.example.projektarendehantering.application.service.CaseService; import org.example.projektarendehantering.application.service.EmployeeService; import org.example.projektarendehantering.application.service.PatientService; +import org.example.projektarendehantering.application.service.PublicStatsService; import org.example.projektarendehantering.infrastructure.security.SecurityActorAdapter; import org.example.projektarendehantering.presentation.dto.CaseAssignmentDTO; import org.example.projektarendehantering.presentation.dto.CaseDTO; @@ -31,12 +32,14 @@ public class UiController { private final CaseService caseService; private final PatientService patientService; private final EmployeeService employeeService; + private final PublicStatsService publicStatsService; private final SecurityActorAdapter securityActorAdapter; - public UiController(CaseService caseService, PatientService patientService, EmployeeService employeeService, SecurityActorAdapter securityActorAdapter) { + public UiController(CaseService caseService, PatientService patientService, EmployeeService employeeService, PublicStatsService publicStatsService, SecurityActorAdapter securityActorAdapter) { this.caseService = caseService; this.patientService = patientService; this.employeeService = employeeService; + this.publicStatsService = publicStatsService; this.securityActorAdapter = securityActorAdapter; } @@ -47,7 +50,8 @@ public String addNote(@PathVariable UUID caseId, @RequestParam("content") String } @GetMapping("/") - public String landing() { + public String landing(Model model) { + model.addAttribute("stats", publicStatsService.getPublicStats()); return "landing"; } diff --git a/src/main/resources/static/app.css b/src/main/resources/static/app.css index 3fc9670..56e5ada 100644 --- a/src/main/resources/static/app.css +++ b/src/main/resources/static/app.css @@ -1188,3 +1188,97 @@ h1, h2 { filter: saturate(0.88); } } + +/* Landing Page Polish */ +.landing-nav { + position: sticky; + top: 14px; + z-index: 100; + background: rgba(16, 26, 51, 0.75); + backdrop-filter: blur(12px); + padding: 10px; + border: 1px solid rgba(110, 168, 255, 0.2); + border-radius: 20px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + transition: all 0.3s ease; +} + +.landing-nav-link.active { + background: rgba(110, 168, 255, 0.3); + border-color: var(--accent-strong); + color: #fff; +} + +.tab-panel, .flow-detail, .role-panel { + display: none; + opacity: 0; + transform: translateY(10px); + transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); +} + +.tab-panel.is-active, .flow-detail.is-active, .role-panel.is-active { + display: block; + opacity: 1; + transform: translateY(0); +} + +.landing-hero--presentation { + background: radial-gradient(circle at top right, rgba(110, 168, 255, 0.12), transparent 50%), + radial-gradient(circle at bottom left, rgba(110, 168, 255, 0.08), transparent 50%); + border-color: rgba(110, 168, 255, 0.3); + padding: 60px 30px; +} + +.metric-chip { + background: rgba(255, 255, 255, 0.05); + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 12px; + padding: 10px 16px; +} + +.metric-chip:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateY(-2px); + border-color: var(--accent); +} + +.metric-icon { + font-size: 20px; +} + +.metric-content { + display: flex; + flex-direction: column; + align-items: flex-start; + line-height: 1.2; +} + +.metric-content strong { + font-size: 18px; + color: #fff; +} + +.metric-content span { + font-size: 11px; + opacity: 0.7; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.hero-bg-glow { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 150%; + height: 150%; + background: radial-gradient(circle, rgba(110, 168, 255, 0.05) 0%, transparent 60%); + pointer-events: none; + z-index: -1; +} + +.landing-section { + scroll-margin-top: 100px; +} diff --git a/src/main/resources/static/app.js b/src/main/resources/static/app.js index b4e5087..c912563 100644 --- a/src/main/resources/static/app.js +++ b/src/main/resources/static/app.js @@ -221,12 +221,18 @@ const renderFlow = (key) => { const item = flowData[key]; if (!flowDetail || !item) return; + + flowDetail.classList.remove("is-active"); + // Force reflow + void flowDetail.offsetWidth; + flowDetail.innerHTML = `

${item.title}

${item.chips.map((chip) => `${chip}`).join("")}
`; + flowDetail.classList.add("is-active"); }; const flowSteps = Array.from(document.querySelectorAll(".flow-step")); @@ -262,12 +268,18 @@ const renderRole = (roleKey) => { const role = roleData[roleKey]; if (!role || !rolePanel) return; + + rolePanel.classList.remove("is-active"); + // Force reflow + void rolePanel.offsetWidth; + rolePanel.innerHTML = `
${role.allow.map((item) => `${item}`).join("")} ${role.deny.map((item) => `${item}`).join("")}
`; + rolePanel.classList.add("is-active"); }; const roleButtons = Array.from(document.querySelectorAll(".role-tabs__btn")); diff --git a/src/main/resources/templates/landing.html b/src/main/resources/templates/landing.html index 8e8f09c..5efa036 100644 --- a/src/main/resources/templates/landing.html +++ b/src/main/resources/templates/landing.html @@ -12,12 +12,38 @@
+
Projektpresentation ✦ 5-7 minuter

Vårdens ärenden i ett tryggt flöde

Ett samlat system för registrering, ansvar och uppföljning utan onödiga överlämningar.

+ +
+
+ 📋 +
+ 0 + Aktiva ärenden +
+
+
+ 👥 +
+ 0 + Patienter +
+
+
+ 🩺 +
+ 0 + Vårdpersonal +
+
+
+
Se lösningen Öppna applikationen From de2ffebf90763927ce0a524b19e9b31bb69b48ed Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Mon, 27 Apr 2026 11:57:08 +0200 Subject: [PATCH 2/3] feat: unify landing page design system and improve visual consistency --- src/main/resources/static/app.css | 378 ++++++++-------------- src/main/resources/templates/landing.html | 2 +- 2 files changed, 140 insertions(+), 240 deletions(-) diff --git a/src/main/resources/static/app.css b/src/main/resources/static/app.css index 56e5ada..b1d435e 100644 --- a/src/main/resources/static/app.css +++ b/src/main/resources/static/app.css @@ -985,300 +985,200 @@ textarea.input { padding: 12px; resize: vertical; } } } -/* Cohesive layout fix: avoid split panel feeling */ -.landing-page { - border: 0; - border-radius: 0; - background: none; - padding: 0; -} +/* --- Unified Landing Design System --- */ -.landing-progress { - position: relative; - top: auto; +.landing-page { + gap: 0; /* Panels merge for a cohesive strip */ } .landing-page > .panel { - background: rgba(255,255,255,0.03); - border: 1px solid var(--border); + padding: 32px; + background: rgba(16, 26, 51, 0.4); + border-color: rgba(110, 168, 255, 0.2); border-radius: var(--radius); - box-shadow: var(--shadow); - padding: 18px; -} - -.landing-page > .panel + .panel { + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.25); margin-top: 0; border-top: 0; + position: relative; + overflow: hidden; } -.landing-close-cta { - background: rgba(255,255,255,0.03); - border-color: var(--border); +.landing-page > .panel:first-of-type { + border-top: 1px solid rgba(110, 168, 255, 0.2); + border-top-left-radius: var(--radius); + border-top-right-radius: var(--radius); } -/* Presentation pop polish: higher contrast + playful depth */ -.landing-page > .panel { - border-color: rgba(143, 190, 255, 0.26); - box-shadow: 0 12px 30px rgba(2, 8, 28, 0.42); +.landing-page > .panel:last-of-type { + border-bottom-left-radius: var(--radius); + border-bottom-right-radius: var(--radius); } .landing-page > .panel:hover { - border-color: rgba(143, 190, 255, 0.48); -} - -.landing-slide::before { - content: ""; - position: absolute; - inset: auto -18% -80px auto; - width: 180px; - height: 180px; - border-radius: 999px; - background: radial-gradient(circle, rgba(143, 190, 255, 0.35), rgba(143, 190, 255, 0)); - pointer-events: none; - animation: panel-glow 7s ease-in-out infinite; -} - -.landing-slide::after { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 36%; - height: 2px; - background: linear-gradient(90deg, rgba(143, 190, 255, 0), rgba(180, 214, 255, 0.8), rgba(143, 190, 255, 0)); - pointer-events: none; - animation: shimmer-sweep 8s ease-in-out infinite; -} - -h1, h2 { - color: #f4f7ff; -} - -.landing-lead, -.module-intro, -.landing-section-kicker, -.landing-close-lead { - color: rgba(236, 242, 255, 0.88); -} - -.notice-badge, -.landing-hero-time { - border-color: rgba(143, 190, 255, 0.66); - background: rgba(110, 168, 255, 0.26); - color: #f3f7ff; -} - -.landing-nav-link { - color: rgba(239, 244, 255, 0.9); - border-color: rgba(143, 190, 255, 0.25); - background: rgba(110, 168, 255, 0.07); -} - -.landing-nav-link:hover, -.landing-nav-link.active { - border-color: rgba(143, 190, 255, 0.8); - background: rgba(110, 168, 255, 0.26); - box-shadow: 0 0 0 1px rgba(143, 190, 255, 0.2) inset; -} - -.button { - border-color: rgba(143, 190, 255, 0.78); - background: rgba(110, 168, 255, 0.33); -} - -.button:hover { - background: rgba(110, 168, 255, 0.46); - box-shadow: 0 8px 18px rgba(67, 131, 255, 0.28); -} - -.button-secondary { - border-color: rgba(231, 236, 255, 0.32); - background: rgba(255, 255, 255, 0.08); -} - -.button-secondary:hover { - background: rgba(255, 255, 255, 0.16); -} - -.button-strong { - border-color: rgba(180, 214, 255, 0.95); - background: linear-gradient(180deg, rgba(110, 168, 255, 0.6), rgba(93, 154, 251, 0.42)); - box-shadow: 0 0 0 1px rgba(180, 214, 255, 0.24), 0 10px 24px rgba(72, 142, 255, 0.34); -} - -.icon-card, -.flow-detail, -.role-panel { - border-color: rgba(143, 190, 255, 0.22); - background: rgba(255, 255, 255, 0.045); -} - -.icon-card:hover { - transform: translateY(-2px); - border-color: rgba(143, 190, 255, 0.56); - background: rgba(255, 255, 255, 0.07); - box-shadow: 0 12px 22px rgba(5, 14, 40, 0.35); -} - -.icon-dot { - color: #f2f6ff; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28); -} - -.metric-chip, -.landing-chip { - border-color: rgba(143, 190, 255, 0.45); - background: rgba(110, 168, 255, 0.18); - color: #f2f6ff; -} - -.permission-chip--allow { - border-color: rgba(123, 228, 149, 0.7); - background: rgba(123, 228, 149, 0.24); -} - -.permission-chip--deny { - border-color: rgba(255, 110, 138, 0.62); - background: rgba(255, 110, 138, 0.2); + border-color: rgba(110, 168, 255, 0.4); + background: rgba(16, 26, 51, 0.5); } -.flow-step, -.role-tabs__btn, -.segmented-tabs__btn, -.accordion__trigger { - border-color: rgba(143, 190, 255, 0.34); - color: #ebf1ff; +/* Hero Section Refinement */ +.landing-hero--presentation { + padding: 80px 32px !important; + text-align: center; + background: radial-gradient(circle at top right, rgba(110, 168, 255, 0.15), transparent 60%), + radial-gradient(circle at bottom left, rgba(110, 168, 255, 0.1), transparent 60%), + rgba(16, 26, 51, 0.6) !important; } -.flow-step.is-active, -.role-tabs__btn.is-active, -.segmented-tabs__btn.is-active { - border-color: rgba(180, 214, 255, 0.95); - background: rgba(110, 168, 255, 0.34); - box-shadow: 0 0 0 1px rgba(180, 214, 255, 0.24) inset; +.landing-hero h1 { + font-size: clamp(34px, 6vw, 52px); + font-weight: 800; + letter-spacing: -0.02em; + line-height: 1.1; + margin-bottom: 20px; + background: linear-gradient(180deg, #fff, rgba(255,255,255,0.8)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } -.accordion__trigger:hover { - background: rgba(110, 168, 255, 0.14); -} - -@media (min-width: 981px) { - .js-enhanced .landing-page { - padding-top: 10px; - padding-bottom: 18px; - } - - .js-enhanced .landing-slide { - min-height: clamp(480px, 72vh, 760px); - opacity: 0.86; - transform: translateY(8px) scale(0.995); - filter: saturate(0.88); - } - - .js-enhanced .landing-slide.is-active-slide { - opacity: 1; - transform: translateY(0) scale(1); - filter: none; - box-shadow: 0 18px 44px rgba(2, 8, 28, 0.52); - } - - .js-enhanced .landing-slide.is-inactive-slide { - opacity: 0.84; - transform: translateY(8px) scale(0.995); - filter: saturate(0.88); - } +.landing-lead { + font-size: 19px; + color: rgba(231, 236, 255, 0.8); + max-width: 60ch; + margin: 0 auto 40px; } -/* Landing Page Polish */ +/* Sticky Nav Refinement */ .landing-nav { position: sticky; top: 14px; z-index: 100; - background: rgba(16, 26, 51, 0.75); - backdrop-filter: blur(12px); - padding: 10px; - border: 1px solid rgba(110, 168, 255, 0.2); - border-radius: 20px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); - transition: all 0.3s ease; + display: flex; + justify-content: center; + gap: 10px; + background: rgba(16, 26, 51, 0.8); + backdrop-filter: blur(16px); + padding: 12px; + margin: 20px 0 !important; + border: 1px solid rgba(110, 168, 255, 0.25) !important; + border-radius: 999px !important; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); } -.landing-nav-link.active { - background: rgba(110, 168, 255, 0.3); - border-color: var(--accent-strong); - color: #fff; +.landing-nav-link { + border: 1px solid transparent; + padding: 8px 16px; + border-radius: 999px; + font-weight: 600; + font-size: 14px; + color: rgba(231, 236, 255, 0.7); + transition: all 0.2s ease; } -.tab-panel, .flow-detail, .role-panel { - display: none; - opacity: 0; - transform: translateY(10px); - transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); +.landing-nav-link:hover { + background: rgba(110, 168, 255, 0.1); + color: #fff; } -.tab-panel.is-active, .flow-detail.is-active, .role-panel.is-active { - display: block; - opacity: 1; - transform: translateY(0); +.landing-nav-link.active { + background: rgba(110, 168, 255, 0.25); + border-color: rgba(110, 168, 255, 0.5); + color: #fff; + box-shadow: 0 0 15px rgba(110, 168, 255, 0.2); } -.landing-hero--presentation { - background: radial-gradient(circle at top right, rgba(110, 168, 255, 0.12), transparent 50%), - radial-gradient(circle at bottom left, rgba(110, 168, 255, 0.08), transparent 50%); - border-color: rgba(110, 168, 255, 0.3); - padding: 60px 30px; +/* Metric Chips */ +.metrics-row--hero { + justify-content: center; + gap: 20px; + margin-bottom: 48px; } .metric-chip { - background: rgba(255, 255, 255, 0.05); - transition: all 0.2s ease; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(110, 168, 255, 0.15); + padding: 12px 20px; + border-radius: 14px; display: flex; align-items: center; - gap: 12px; - padding: 10px 16px; + gap: 14px; + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); } .metric-chip:hover { - background: rgba(255, 255, 255, 0.1); + background: rgba(110, 168, 255, 0.08); + border-color: rgba(110, 168, 255, 0.4); + transform: translateY(-4px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); +} + +.metric-icon { font-size: 24px; } +.metric-content strong { font-size: 20px; color: #fff; display: block; } +.metric-content span { font-size: 12px; color: rgba(231, 236, 255, 0.6); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700; } + +/* Action Buttons */ +.landing-cta-primary { + height: 48px; + padding: 0 32px; + font-size: 16px; + border-radius: 14px; + background: linear-gradient(180deg, var(--accent), #4a8eff); + border: 0; + color: #fff; + box-shadow: 0 10px 25px rgba(74, 142, 255, 0.3); +} + +.landing-cta-primary:hover { transform: translateY(-2px); - border-color: var(--accent); + box-shadow: 0 15px 30px rgba(74, 142, 255, 0.4); } -.metric-icon { - font-size: 20px; +.landing-cta-secondary { + height: 48px; + padding: 0 28px; + font-size: 16px; + border-radius: 14px; + border: 1px solid rgba(231, 236, 255, 0.2); } -.metric-content { - display: flex; - flex-direction: column; - align-items: flex-start; - line-height: 1.2; +/* Tab Panels & Details */ +.tab-panel, .flow-detail, .role-panel { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(110, 168, 255, 0.15); + padding: 24px; + border-radius: 14px; } -.metric-content strong { - font-size: 18px; - color: #fff; +.icon-card { + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(110, 168, 255, 0.1); + padding: 20px; + transition: all 0.3s ease; } -.metric-content span { - font-size: 11px; - opacity: 0.7; - text-transform: uppercase; - letter-spacing: 0.05em; +.icon-card:hover { + background: rgba(110, 168, 255, 0.05); + border-color: rgba(110, 168, 255, 0.3); + transform: scale(1.02); } -.hero-bg-glow { +/* Animations */ +.landing-slide::before { + content: ""; position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 150%; - height: 150%; - background: radial-gradient(circle, rgba(110, 168, 255, 0.05) 0%, transparent 60%); + inset: auto -10% -20% auto; + width: 300px; + height: 300px; + background: radial-gradient(circle, rgba(110, 168, 255, 0.15), transparent 70%); pointer-events: none; - z-index: -1; + animation: panel-glow 10s ease-in-out infinite; } -.landing-section { - scroll-margin-top: 100px; +@keyframes panel-glow { + 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.4; } + 50% { transform: scale(1.2) translate(-20px, -20px); opacity: 0.6; } +} + +@media (max-width: 900px) { + .landing-page > .panel { padding: 24px; } + .landing-hero--presentation { padding: 60px 24px !important; } + .landing-nav { top: 10px; padding: 8px; font-size: 12px; } } diff --git a/src/main/resources/templates/landing.html b/src/main/resources/templates/landing.html index 5efa036..25bb60d 100644 --- a/src/main/resources/templates/landing.html +++ b/src/main/resources/templates/landing.html @@ -20,7 +20,7 @@

Vårdens ärenden i ett tryggt flöde

Ett samlat system för registrering, ansvar och uppföljning utan onödiga överlämningar.

-
+
📋
From 069c4c88df808b1cbb971d858555a9b151ab5a37 Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Mon, 27 Apr 2026 11:59:58 +0200 Subject: [PATCH 3/3] style: remove presentation metadata badge from landing page hero --- src/main/resources/templates/landing.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/resources/templates/landing.html b/src/main/resources/templates/landing.html index 25bb60d..bc07e58 100644 --- a/src/main/resources/templates/landing.html +++ b/src/main/resources/templates/landing.html @@ -13,10 +13,6 @@
-
- Projektpresentation ✦ - 5-7 minuter -

Vårdens ärenden i ett tryggt flöde

Ett samlat system för registrering, ansvar och uppföljning utan onödiga överlämningar.