Skip to content

Feature/landing page polish#110

Merged
LinusWestling merged 3 commits into
mainfrom
feature/landing-page-polish
Apr 27, 2026
Merged

Feature/landing page polish#110
LinusWestling merged 3 commits into
mainfrom
feature/landing-page-polish

Conversation

@Tyreviel

@Tyreviel Tyreviel commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

closes #109

Summary by CodeRabbit

  • New Features

    • Landing page now displays public statistics showing total active cases, patients, and healthcare employees.
  • Style

    • Landing page redesigned with updated visual hierarchy, navigation styling, spacing, and decorative elements.
  • Improvements

    • Enhanced animation and transition effects on dynamic content updates.

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

A new PublicStatsService is introduced to query repository counts for cases, patients, and employees. The UiController is extended to inject this service and populate the landing page model with aggregated statistics. The landing view and associated styles are updated to display these metrics dynamically.

Changes

Cohort / File(s) Summary
Backend Service Layer
src/main/java/.../service/PublicStatsService.java
New Spring-managed service with transactional read-only getPublicStats() method that aggregates counts from CaseRepository, PatientRepository, and EmployeeRepository into a Map.
Controller Integration
src/main/java/.../presentation/web/UiController.java
Updated constructor to accept PublicStatsService dependency; landing() handler now accepts Model parameter and populates it with stats attribute before view rendering.
Landing Page Template
src/main/resources/templates/landing.html
Replaces static hero metadata with dynamic metrics row displaying case, patient, and employee counts using Thymeleaf bindings with zero defaults.
Frontend Styling & Animation
src/main/resources/static/app.css, src/main/resources/static/app.js
Comprehensive CSS redesign of landing page layout (hero, navigation, metrics display); minor JS animation fix toggling is-active class to retrigger transitions during content updates.

Sequence Diagram

sequenceDiagram
    actor User
    participant UiController
    participant PublicStatsService
    participant Repositories as CaseRepository<br/>PatientRepository<br/>EmployeeRepository
    participant View as landing.html

    User->>UiController: GET /
    UiController->>PublicStatsService: getPublicStats()
    PublicStatsService->>Repositories: count() × 3
    Repositories-->>PublicStatsService: totalCases, totalPatients,<br/>totalEmployees
    PublicStatsService-->>UiController: Map with stats
    UiController->>UiController: model.addAttribute("stats", ...)
    UiController->>View: render landing view
    View-->>User: landing page with metrics
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 Stats bloom bright on pages of spring,
Where repositories sing and numbers take wing,
The landing grows metrics—a visual delight,
As CSS dances with animation so light,
Our cottoned counts hop through Thymeleaf's embrace! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The linked issue #109 lacks specific requirements, making it impossible to validate whether the code changes address the actual objectives. The PR description only states 'closes #109' without details. Review the full requirements in issue #109 to confirm all coding objectives are met. Currently, only the commit messages and file changes suggest landing page improvements without confirmation against stated requirements.
Out of Scope Changes check ❓ Inconclusive The PR introduces backend changes (PublicStatsService and UiController modifications) alongside frontend styling and layout improvements, which may extend beyond typical 'landing page polish' scope. Verify that backend service changes and controller modifications are necessary for the landing page improvements or if they represent separate feature work that should be in a different PR.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/landing page polish' directly describes the main objective of the PR, which focuses on enhancing the landing page UI with sticky navigation, animations, metrics improvements, and design consistency.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/landing-page-polish

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/main/java/org/example/projektarendehantering/application/service/PublicStatsService.java (1)

21-28: Cache the public stats; consider returning an immutable map.

getPublicStats() is called on every hit to the public GET / landing page (see UiController.landing) and issues three full table COUNT queries each time. For an unauthenticated, presumably high-traffic page this is unnecessary load on the DB and slows TTFB. A short TTL cache (Spring @Cacheable with Caffeine, or a manual AtomicReference + scheduled refresh) is a much better fit since the numbers don't need to be real-time.

Also, since callers (Thymeleaf) only read the result, returning an immutable Map.of(...) makes the intent clearer and avoids accidental mutation.

♻️ Suggested refactor
-    `@Transactional`(readOnly = true)
-    public Map<String, Long> getPublicStats() {
-        Map<String, Long> stats = new HashMap<>();
-        stats.put("totalCases", caseRepository.count());
-        stats.put("totalPatients", patientRepository.count());
-        stats.put("totalEmployees", employeeRepository.count());
-        return stats;
-    }
+    `@Transactional`(readOnly = true)
+    `@Cacheable`("publicStats")
+    public Map<String, Long> getPublicStats() {
+        return Map.of(
+                "totalCases", caseRepository.count(),
+                "totalPatients", patientRepository.count(),
+                "totalEmployees", employeeRepository.count()
+        );
+    }

(Requires @EnableCaching on a config class and a cache provider; also schedule eviction, e.g., every 1–5 minutes.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/projektarendehantering/application/service/PublicStatsService.java`
around lines 21 - 28, getPublicStats currently issues three COUNT queries on
every public hit (used by UiController.landing); wrap it with a short TTL cache
(e.g., annotate PublicStatsService.getPublicStats with Spring's `@Cacheable`
backed by Caffeine and ensure `@EnableCaching` is present) so the DB counts are
served from cache for 1–5 minutes, or alternatively use a cached AtomicReference
with a scheduled refresh; also return an immutable map (e.g., Map.of(...))
instead of a mutable HashMap to prevent accidental mutation by callers.
src/main/resources/static/app.js (1)

224-235: Reflow trick currently has no visible effect.

The remove-class → force-reflow → re-add-class pattern is meant to restart a CSS transition/animation tied to .is-active. Looking at .flow-detail (and analogously .role-panel at lines 271-282) in app.css, neither selector defines a transition or animation that keys off is-active, so this code path adds complexity without visible payoff. Either drop the toggle or add an is-active transition (e.g., fade/translate) to actually use it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/app.js` around lines 224 - 235, The remove-class →
void flowDetail.offsetWidth → add-class pattern around flowDetail (and the
analogous rolePanel code) currently has no effect because .flow-detail and
.role-panel lack any CSS transition/animation keyed to the .is-active class;
either remove the DOM class-toggle + forced reflow (delete the
flowDetail.classList.remove / void flowDetail.offsetWidth /
flowDetail.classList.add lines) to simplify the code, or add CSS animations for
.flow-detail.is-active and .role-panel.is-active (e.g., add transition +
transform/opacity rules for .flow-detail and .role-panel, and a different state
for .is-active) so the existing flowDetail and rolePanel class toggles actually
trigger a visible fade/translate animation. Ensure you update both the JS
references (flowDetail, rolePanel) and the CSS selectors
(.flow-detail.is-active, .role-panel.is-active) consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/resources/static/app.css`:
- Around line 1175-1178: There are two conflicting `@keyframes` declarations named
`panel-glow`; remove the duplicate by either deleting the earlier definition or
renaming this second one to a distinct name (e.g., `panel-glow-variant`) and
update any CSS rules that reference `animation-name`/`animation` using
`panel-glow` to the new name so the intended animation is applied; make the
change in the `@keyframes panel-glow` block and any selectors that consume it
(search for `panel-glow` usages) to keep behavior consistent.
- Around line 1088-1115: The new large-card styles unintentionally override the
base .metric-chip used across the site; scope the hero-only styling instead of
replacing .metric-chip by either (A) prefixing the rules with .metrics-row--hero
.metric-chip or (B) creating a modifier like .metric-chip--hero and moving the
large-card rules into that selector (and updating .metric-icon and
.metric-content selectors to .metric-chip--hero .metric-icon /
.metric-chip--hero .metric-content). Restore the original small-pill properties
on .metric-chip (min-height: 28px; border-radius: 999px; padding: 0 10px;
font-size: 12px) so non-hero chips keep their intended appearance, and update
any hero markup to use the new modifier if you choose option B.

In `@src/main/resources/templates/landing.html`:
- Around line 19-41: The template currently uses literal "0" inside the <strong>
tags as a static preview, but that does not serve as a runtime fallback; update
the three <strong> elements that use th:text (the ones bound to
stats.totalCases, stats.totalPatients, and stats.totalEmployees) to use the
Thymeleaf/OGNL Elvis operator in their th:text expressions so that they render 0
when the stats value is null or missing (replace the current th:text expressions
on those <strong> elements with an Elvis-style fallback producing 0).

---

Nitpick comments:
In
`@src/main/java/org/example/projektarendehantering/application/service/PublicStatsService.java`:
- Around line 21-28: getPublicStats currently issues three COUNT queries on
every public hit (used by UiController.landing); wrap it with a short TTL cache
(e.g., annotate PublicStatsService.getPublicStats with Spring's `@Cacheable`
backed by Caffeine and ensure `@EnableCaching` is present) so the DB counts are
served from cache for 1–5 minutes, or alternatively use a cached AtomicReference
with a scheduled refresh; also return an immutable map (e.g., Map.of(...))
instead of a mutable HashMap to prevent accidental mutation by callers.

In `@src/main/resources/static/app.js`:
- Around line 224-235: The remove-class → void flowDetail.offsetWidth →
add-class pattern around flowDetail (and the analogous rolePanel code) currently
has no effect because .flow-detail and .role-panel lack any CSS
transition/animation keyed to the .is-active class; either remove the DOM
class-toggle + forced reflow (delete the flowDetail.classList.remove / void
flowDetail.offsetWidth / flowDetail.classList.add lines) to simplify the code,
or add CSS animations for .flow-detail.is-active and .role-panel.is-active
(e.g., add transition + transform/opacity rules for .flow-detail and
.role-panel, and a different state for .is-active) so the existing flowDetail
and rolePanel class toggles actually trigger a visible fade/translate animation.
Ensure you update both the JS references (flowDetail, rolePanel) and the CSS
selectors (.flow-detail.is-active, .role-panel.is-active) consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: acaca7db-8400-475c-bbef-523a5886c30e

📥 Commits

Reviewing files that changed from the base of the PR and between f74ca19 and 069c4c8.

📒 Files selected for processing (5)
  • src/main/java/org/example/projektarendehantering/application/service/PublicStatsService.java
  • src/main/java/org/example/projektarendehantering/presentation/web/UiController.java
  • src/main/resources/static/app.css
  • src/main/resources/static/app.js
  • src/main/resources/templates/landing.html

Comment on lines +1088 to +1115
/* Metric Chips */
.metrics-row--hero {
justify-content: center;
gap: 20px;
margin-bottom: 48px;
}

.button-secondary {
border-color: rgba(231, 236, 255, 0.32);
background: rgba(255, 255, 255, 0.08);
.metric-chip {
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: 14px;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

.button-secondary:hover {
background: rgba(255, 255, 255, 0.16);
.metric-chip:hover {
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);
}

.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);
}
.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; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

.metric-chip is redefined here and unintentionally restyles non-hero usages.

The original .metric-chip (line 928) is a small pill (min-height: 28px, border-radius: 999px, padding: 0 10px, font-size: 12px). This new definition replaces it everywhere with a much larger card-like element (padding: 12px 20px, border-radius: 14px, display: flex with gap: 14px, hover lift). However .metric-chip is reused as a plain text chip in several places that don't have the new .metric-icon/.metric-content child structure:

  • landing.html lines 111–113 (Documents row: "MIME-kontroll", "Automatisk retry", "Säker uppföljning")
  • landing.html lines 129–131 (Audit row: "Aktör", "Händelse", "Tidpunkt")
  • app.js line 232 in renderFlow (chips inside flow detail)
  • app.js lines 278–279 in renderRole (permission chips, including .permission-chip--allow/--deny)

All of these now render as oversized rounded rectangles rather than small pills, which contradicts the surrounding visual design and bloats those sections. Consider scoping the new style to a hero-only modifier instead of overriding the base class.

♻️ Suggested fix — scope new chip style with a modifier
-/* Metric Chips */
-.metrics-row--hero {
-  justify-content: center;
-  gap: 20px;
-  margin-bottom: 48px;
-}
-
-.metric-chip {
-  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: 14px;
-  transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
-}
-
-.metric-chip:hover {
-  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 Chips — hero variant only */
+.metrics-row--hero {
+  justify-content: center;
+  gap: 20px;
+  margin-bottom: 48px;
+}
+
+.metrics-row--hero .metric-chip {
+  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: 14px;
+  transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.metrics-row--hero .metric-chip:hover {
+  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);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/* Metric Chips */
.metrics-row--hero {
justify-content: center;
gap: 20px;
margin-bottom: 48px;
}
.button-secondary {
border-color: rgba(231, 236, 255, 0.32);
background: rgba(255, 255, 255, 0.08);
.metric-chip {
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: 14px;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.button-secondary:hover {
background: rgba(255, 255, 255, 0.16);
.metric-chip:hover {
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);
}
.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);
}
.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; }
/* Metric Chips — hero variant only */
.metrics-row--hero {
justify-content: center;
gap: 20px;
margin-bottom: 48px;
}
.metrics-row--hero .metric-chip {
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: 14px;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.metrics-row--hero .metric-chip:hover {
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; }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/app.css` around lines 1088 - 1115, The new
large-card styles unintentionally override the base .metric-chip used across the
site; scope the hero-only styling instead of replacing .metric-chip by either
(A) prefixing the rules with .metrics-row--hero .metric-chip or (B) creating a
modifier like .metric-chip--hero and moving the large-card rules into that
selector (and updating .metric-icon and .metric-content selectors to
.metric-chip--hero .metric-icon / .metric-chip--hero .metric-content). Restore
the original small-pill properties on .metric-chip (min-height: 28px;
border-radius: 999px; padding: 0 10px; font-size: 12px) so non-hero chips keep
their intended appearance, and update any hero markup to use the new modifier if
you choose option B.

Comment thread src/main/resources/static/app.css
Comment on lines +19 to +41
<div class="metrics-row metrics-row--hero">
<div class="metric-chip">
<span class="metric-icon">📋</span>
<div class="metric-content">
<strong th:text="${stats.totalCases}">0</strong>
<span>Aktiva ärenden</span>
</div>
</div>
<div class="metric-chip">
<span class="metric-icon">👥</span>
<div class="metric-content">
<strong th:text="${stats.totalPatients}">0</strong>
<span>Patienter</span>
</div>
</div>
<div class="metric-chip">
<span class="metric-icon">🩺</span>
<div class="metric-content">
<strong th:text="${stats.totalEmployees}">0</strong>
<span>Vårdpersonal</span>
</div>
</div>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The 0 inside the tag isn't a runtime fallback.

<strong th:text="${stats.totalCases}">0</strong> — the literal 0 is only rendered when Thymeleaf doesn't process the template (static preview). At runtime, if stats is missing or a key is absent, you'll get an empty string or null, not 0. To make this a real fallback, use the Elvis operator.

🛡️ Suggested fix
-                    <strong th:text="${stats.totalCases}">0</strong>
+                    <strong th:text="${stats.totalCases ?: 0}">0</strong>
@@
-                    <strong th:text="${stats.totalPatients}">0</strong>
+                    <strong th:text="${stats.totalPatients ?: 0}">0</strong>
@@
-                    <strong th:text="${stats.totalEmployees}">0</strong>
+                    <strong th:text="${stats.totalEmployees ?: 0}">0</strong>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="metrics-row metrics-row--hero">
<div class="metric-chip">
<span class="metric-icon">📋</span>
<div class="metric-content">
<strong th:text="${stats.totalCases}">0</strong>
<span>Aktiva ärenden</span>
</div>
</div>
<div class="metric-chip">
<span class="metric-icon">👥</span>
<div class="metric-content">
<strong th:text="${stats.totalPatients}">0</strong>
<span>Patienter</span>
</div>
</div>
<div class="metric-chip">
<span class="metric-icon">🩺</span>
<div class="metric-content">
<strong th:text="${stats.totalEmployees}">0</strong>
<span>Vårdpersonal</span>
</div>
</div>
</div>
<div class="metrics-row metrics-row--hero">
<div class="metric-chip">
<span class="metric-icon">📋</span>
<div class="metric-content">
<strong th:text="${stats.totalCases ?: 0}">0</strong>
<span>Aktiva ärenden</span>
</div>
</div>
<div class="metric-chip">
<span class="metric-icon">👥</span>
<div class="metric-content">
<strong th:text="${stats.totalPatients ?: 0}">0</strong>
<span>Patienter</span>
</div>
</div>
<div class="metric-chip">
<span class="metric-icon">🩺</span>
<div class="metric-content">
<strong th:text="${stats.totalEmployees ?: 0}">0</strong>
<span>Vårdpersonal</span>
</div>
</div>
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/landing.html` around lines 19 - 41, The template
currently uses literal "0" inside the <strong> tags as a static preview, but
that does not serve as a runtime fallback; update the three <strong> elements
that use th:text (the ones bound to stats.totalCases, stats.totalPatients, and
stats.totalEmployees) to use the Thymeleaf/OGNL Elvis operator in their th:text
expressions so that they render 0 when the stats value is null or missing
(replace the current th:text expressions on those <strong> elements with an
Elvis-style fallback producing 0).

@LinusWestling
LinusWestling merged commit 9be2eab into main Apr 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

landing page improvements

2 participants