Skip to content

Shadcn UI - like UI - #35

Merged
yamcodes merged 16 commits into
mainfrom
feat/shadcn-ui
Mar 18, 2026
Merged

Shadcn UI - like UI#35
yamcodes merged 16 commits into
mainfrom
feat/shadcn-ui

Conversation

@yamcodes

@yamcodes yamcodes commented Mar 18, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Enhanced list header: integrated search with results counter, New Contact action, and bulk delete with confirmation
    • Row-level navigation and avatar summaries for contacts
  • UI/Style Improvements

    • Complete redesign to card-based layouts, sticky header, and grid form layouts
    • New "shadcn" visual theme and updated typography
    • Improved error and 404 pages; hover-reveal action buttons on rows
  • Performance

    • Thymeleaf template caching enabled for production environments

- Configure `package.json` with Tailwind CSS build and watch scripts.
- Integrate Frontend Maven Plugin for CSS build automation.
- Update `.gitignore` to exclude frontend-related files and directories.
- Link compiled CSS in `layout.html`.
- Add compound run configuration: "Contacts (Full)" for Spring Boot and npm tasks.
- Add NPM run configuration for CSS watch script (`watch:css`).
…experience

- Revamp templates (`index.html`, `edit.html`, `new.html`, `view.html`, error pages) with modern UI designs using Tailwind CSS and DaisyUI.
- Enhance table and form layouts, adding consistent styling and responsiveness.
- Integrate new actions UI for edit, delete, and view navigation.
- Implement improved error and empty state handling.
- Update layout template to include custom font and color theme.
- Add `pointer-events-none` to search form SVG for better usability.
- Refactor and expand DaisyUI color theme configuration with `shadcn` defaults.
@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@yamcodes has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 17 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 533f5dfe-8771-4233-9e37-ae165512d65f

📥 Commits

Reviewing files that changed from the base of the PR and between 5e0a945 and 4ac0936.

📒 Files selected for processing (1)
  • src/main/resources/templates/fragments/contact-list-rows.html
📝 Walkthrough

Walkthrough

Adds a "shadcn" theme and UI redesign (templates, CSS, layout) for the contacts app, introduces a dev Spring profile and app-dev config, enables deterministic frontend install via bun, and updates minor IDE and run configurations. No API or backend signature changes.

Changes

Cohort / File(s) Summary
IDE & Run Config
.idea/dictionaries/project.xml, .run/Contacts.run.xml
Adds shadcn to the IDE dictionary and sets ACTIVE_PROFILES to dev in the run configuration.
Build & Frontend Install
pom.xml
Changes frontend-maven-plugin bun-install args to include --frozen-lockfile for deterministic installs.
Application Profiles / Config
src/main/resources/application.yaml, src/main/resources/application-dev.yaml
Enables Thymeleaf caching in default config, removes livereload from production YAML, and adds a new application-dev.yaml with disabled template cache, livereload, file-based template/static locations and dev restart settings.
Theme / Styles
src/main/resources/static/css/input.css
Adds a full DaisyUI/shadcn theme block with color tokens, radii, depth/noise, border vars, and typography/button base styles.
Global Layout
src/main/resources/templates/layout.html
Applies data-theme="shadcn", loads Geist font, rewrites header into a sticky branded nav, and constrains main content container and body typography.
Contacts UI — Pages
src/main/resources/templates/contacts/index.html, .../new.html, .../edit.html, .../view.html
Reworks all contact pages to card-based layouts, richer headers, search/empty states, bulk actions, styled forms, cancel/save buttons, and pagination adjustments; endpoints and form actions unchanged.
Contacts UI — Fragments & Rows
src/main/resources/templates/fragments/contact-fields.html, .../contact-list-rows.html
Converts contact-fields to a grid of fieldsets with standardized inputs and validation placement; contact-list-rows now include row navigation, avatar/name layout, checkboxes, hover actions, and HTMX-based delete (hx-delete, hx-confirm, hx-swap) with stop-propagation on controls.
Error Pages
src/main/resources/templates/error.html, src/main/resources/templates/error/404.html
Replaces simple error pages with centered, styled containers and Thymeleaf links/buttons.
Controller Adjustment
src/main/java/codes/yam/contacts/ContactController.java
Exposes a sort model attribute (comma-separated sort spec) for the contacts listing; removed an inline TODO in delete handler.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • 20 add bulk delete contacts view #24 — Modifies contact list UI and fragments (bulk-select, bulk-delete, HTMX delete) and appears tightly related to the contact-list and fragment changes here.

Poem

🐰 I nibble CSS and hum a tune,

shadcn leaves beneath the moon,
Cards and grids hop into place,
Buttons gleam with gentle grace,
A rabbit's cheer — the UI's in bloom.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ 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 'Shadcn UI - like UI' accurately reflects the main objective of the pull request, which involves implementing a comprehensive UI redesign using shadcn-themed styling across all templates and configuration files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/shadcn-ui
📝 Coding Plan
  • Generate coding plan for human review comments

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.

- Configure Thymeleaf `prefix` in `application.yaml` for file-based templates.
- Add `shadcn` to project-specific dictionary.
- Update branding text in layout template from `contact.app` to `Contacts`.
Base automatically changed from 10-add-tailwind-css to main March 18, 2026 18:44
@yamcodes
yamcodes marked this pull request as ready for review March 18, 2026 18:46
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@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: 12

🧹 Nitpick comments (4)
.idea/compiler.xml (1)

11-12: Remove the duplicated Lombok processor path entry.

Line 12 repeats the exact same processor path as Line 11. Keeping a single entry avoids config drift and review noise.

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

In @.idea/compiler.xml around lines 11 - 12, Remove the duplicated processor
path entry for Lombok by deleting one of the two identical <entry
name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
lines so only a single entry remains, ensuring the Lombok processor path appears
once in the configuration.
src/main/resources/templates/layout.html (1)

14-16: Move inline font styling into the static CSS pipeline.

Line 14-Line 16 can be moved to src/main/resources/static/css/input.css so style concerns stay in static assets and templates remain purely structural.

As per coding guidelines src/main/resources/static/**/*.{css,js}: "Organize static assets (CSS, JavaScript) in the static/ directory".

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

In `@src/main/resources/templates/layout.html` around lines 14 - 16, Remove the
inline <style> block in layout.html that sets body { font-family: 'Geist',
sans-serif; } and add that rule to the global static stylesheet named input.css,
then ensure the layout template includes the shared CSS file (so templates
remain structural and fonts live in the static pipeline). Use the existing style
block in layout.html to locate the rule and update input.css accordingly, then
delete the inline block from layout.html.
src/main/resources/templates/fragments/contact-fields.html (1)

23-30: Avoid positional hx-target selectors for validation output.

hx-target="next p" is brittle. Prefer an explicit target ID so future markup edits don’t break live validation rendering.

💡 Proposed fix
-      <input hx-target="next p" hx-trigger="change, keyup delay:200ms changed"
+      <input hx-target="#email-validation" hx-trigger="change, keyup delay:200ms changed"
              placeholder="Email"
              th:with="validateUrl=@{/contacts/{slug}/email(slug=${contact.slug})}"
              th:attr="hx-get=${contact.slug != null} ? ${validateUrl} : null"
              th:field="*{email}"
              type="email"
              class="input input-bordered w-full"/>
-      <p class="fieldset-label text-error" th:errors="*{email}"></p>
+      <p id="email-validation" class="fieldset-label text-error" th:errors="*{email}"></p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/fragments/contact-fields.html` around lines 23 -
30, Replace the brittle positional hx-target="next p" on the email input with an
explicit ID selector: add a unique id attribute to the validation paragraph (the
<p class="fieldset-label text-error" th:errors="*{email}"> element, e.g.
id="email-error") and change the input's hx-target to that ID (e.g.
hx-target="#email-error"); keep existing th:field="*{email}" and
th:attr="hx-get=..." behavior unchanged so live validation still requests the
same endpoint but now renders reliably into the explicit target.
pom.xml (1)

126-131: Prefer deterministic Bun installs in Maven builds.

Using bun install without lockfile enforcement can produce non-reproducible builds.

💡 Proposed fix
                     <execution>
                         <id>bun-install</id>
                         <goals><goal>bun</goal></goals>
                         <phase>initialize</phase>
                         <configuration>
-                            <arguments>install</arguments>
+                            <arguments>install --frozen-lockfile</arguments>
                         </configuration>
                     </execution>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pom.xml` around lines 126 - 131, The Bun install step (maven plugin entry
with id "bun-install" and goal "bun" in phase "initialize" that currently uses
arguments "install") should enforce the lockfile for deterministic installs:
change the <arguments> from "install" to "install --frozen-lockfile" (or an
equivalent lock-enforcing flag supported by your Bun version) and ensure the
repo includes the bun.lockb lockfile so CI fails on drift rather than producing
non-reproducible installs.
🤖 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/application.yaml`:
- Line 6: Update the Thymeleaf prefix to use the classpath default by replacing
the current file-based value for spring.thymeleaf.prefix so templates are
resolved from the packaged classpath (use the standard classpath:/templates/
pattern) rather than file:src/main/resources/templates/, ensuring portability in
packaged deployments and matching the existing static resource approach.
- Around line 18-21: The current application.yaml holds development-focused
settings (cache.period and chain.cache) without profile separation; extract the
environment-specific keys (cache.period and chain.cache) into profile-specific
files (e.g., application-dev.yaml for development defaults and
application-prod.yaml for production overrides) and keep only common settings in
application.yaml, then set or document spring.profiles.active (or use
spring.profiles.include) to select the appropriate profile at runtime so
production can enable different cache values without changing the main
application.yaml.

In `@src/main/resources/static/css/input.css`:
- Around line 2-7: stylelint is flagging valid Tailwind v4 at-rules (`@source`,
`@plugin`, `@plugin` "daisyui/theme") because the config extends
stylelint-config-standard-scss; update the stylelint configuration to modify the
at-rule-no-unknown rule so it ignores Tailwind at-rules (add "tailwind",
"layer", "source", "plugin", "import" to the ignoreAtRules list) while keeping
the existing extends, ensuring the at-rule-no-unknown rule is enabled with the
new ignoreAtRules entry.

In `@src/main/resources/templates/contacts/index.html`:
- Line 27: The search form currently hardcodes action="/contacts" which will
break when the app runs under a non-root context; replace the literal action
attribute on the <form> (the search form element) with Thymeleaf URL binding by
using th:action with a context-aware expression (e.g. th:action="@{/contacts}")
while keeping method="get" and class="mb-4", and remove or replace the existing
static action attribute so Thymeleaf resolves the correct context path.
- Around line 91-96: The "Delete Selected" submit button (the button element
with text "Delete Selected" and classes including btn-error btn-outline)
performs a destructive bulk delete without confirmation; add a confirmation
guard by intercepting the form submission or adding an on-click handler that
shows a confirmation dialog (e.g., window.confirm) and only allows the submit to
proceed if the user confirms, otherwise preventDefault/stop submission; ensure
the handler targets the specific form/button so only bulk-delete is gated.

In `@src/main/resources/templates/contacts/new.html`:
- Line 22: Replace the hardcoded form action attribute with a Thymeleaf
context-path-safe attribute: change the <form action="/contacts" method="post">
to use th:action="@{/contacts}" (keeping method="post") so the form submission
uses the application's context path the same way as the edit.html pattern;
locate the form tag in new.html and swap action for th:action accordingly.

In `@src/main/resources/templates/contacts/view.html`:
- Around line 23-24: Replace the current th:text expression that unconditionally
calls `#strings.substring`(contact.first, 0, 1) and
`#strings.substring`(contact.last, 0, 1) with a defensive expression that checks
for null/empty before taking substrings; use Thymeleaf's `#strings.isEmpty` (or
length checks) to produce each initial only if contact.first/contact.last is
non-empty and fall back to an empty string otherwise, and set the combined
result into the existing th:text attribute so the template doesn't throw
StringIndexOutOfBoundsException when contact.first or contact.last are
null/blank.

In `@src/main/resources/templates/error.html`:
- Around line 18-19: Replace the hardcoded anchor href in the error.html
template with a Thymeleaf link expression so the link becomes context-path
aware: update the <a ... href="/contacts" ...> anchor to use th:href with the
@{} syntax (e.g., th:href="@{/contacts}") on the same element (you can keep or
remove the static href fallback per project conventions) so the Contacts link
resolves correctly when the app is deployed under a context path.

In `@src/main/resources/templates/error/404.html`:
- Around line 13-14: The anchor in the 404 template uses a hardcoded href (the
<a> element linking to "/contacts"); change it to use Thymeleaf's
context-path-safe URL syntax by replacing the literal href attribute with
th:href="@{/contacts}" on that same anchor (and apply the same change to the
same anchors in layout.html and error.html) so links are rewritten to include
the application's context path at runtime.

In `@src/main/resources/templates/fragments/contact-list-rows.html`:
- Around line 23-34: The action buttons are hidden by the Tailwind class
"opacity-0 group-hover:opacity-100", which makes them inaccessible to keyboard
and touch users; update the wrapper div (the element with class "flex
items-center justify-end gap-1 opacity-0 group-hover:opacity-100
transition-opacity") to also reveal controls on keyboard focus and when a row
has focus by adding "group-focus-within:opacity-100" and ensure each control
(the "View", "Edit", and "Delete" buttons) supports keyboard focus by adding a
visible focus style class such as "focus-visible:opacity-100 focus:outline-none
focus:ring" or similar and a tabindex if necessary so these elements become
visible and operable for keyboard and touch users.
- Line 30: Replace the generic Thymeleaf attribute binding
th:attr="hx-delete=@{/contacts/{slug}(slug=*{slug})}" with the htmx-dialect
style attribute so the dialect handles it directly; i.e. remove th:attr and add
hx-delete="@{/contacts/{slug}(slug=*{slug})}" on the element so the
htmx-spring-boot-thymeleaf dialect processes the hx-delete attribute natively.

In `@src/main/resources/templates/layout.html`:
- Line 24: Replace hardcoded href attributes on the navigation anchor elements
with Thymeleaf URL expressions so links respect the application's context path;
specifically update the <a> elements that currently use href="/contacts" (the
navigation link in layout.html referenced by the review) to use
th:href="@{/contacts}" instead (preserve existing class and attributes) and do
the same for the second occurrence mentioned.

---

Nitpick comments:
In @.idea/compiler.xml:
- Around line 11-12: Remove the duplicated processor path entry for Lombok by
deleting one of the two identical <entry
name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
lines so only a single entry remains, ensuring the Lombok processor path appears
once in the configuration.

In `@pom.xml`:
- Around line 126-131: The Bun install step (maven plugin entry with id
"bun-install" and goal "bun" in phase "initialize" that currently uses arguments
"install") should enforce the lockfile for deterministic installs: change the
<arguments> from "install" to "install --frozen-lockfile" (or an equivalent
lock-enforcing flag supported by your Bun version) and ensure the repo includes
the bun.lockb lockfile so CI fails on drift rather than producing
non-reproducible installs.

In `@src/main/resources/templates/fragments/contact-fields.html`:
- Around line 23-30: Replace the brittle positional hx-target="next p" on the
email input with an explicit ID selector: add a unique id attribute to the
validation paragraph (the <p class="fieldset-label text-error"
th:errors="*{email}"> element, e.g. id="email-error") and change the input's
hx-target to that ID (e.g. hx-target="#email-error"); keep existing
th:field="*{email}" and th:attr="hx-get=..." behavior unchanged so live
validation still requests the same endpoint but now renders reliably into the
explicit target.

In `@src/main/resources/templates/layout.html`:
- Around line 14-16: Remove the inline <style> block in layout.html that sets
body { font-family: 'Geist', sans-serif; } and add that rule to the global
static stylesheet named input.css, then ensure the layout template includes the
shared CSS file (so templates remain structural and fonts live in the static
pipeline). Use the existing style block in layout.html to locate the rule and
update input.css accordingly, then delete the inline block from layout.html.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9874a5fe-e32e-48a4-9948-802d6be45dfc

📥 Commits

Reviewing files that changed from the base of the PR and between 1e6c613 and 45e1af8.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .gitignore
  • .idea/compiler.xml
  • .idea/dictionaries/project.xml
  • .run/Contacts (Full).run.xml
  • .run/Watch CSS.run.xml
  • package.json
  • pom.xml
  • src/main/resources/application.yaml
  • src/main/resources/static/css/input.css
  • src/main/resources/templates/contacts/edit.html
  • src/main/resources/templates/contacts/index.html
  • src/main/resources/templates/contacts/new.html
  • src/main/resources/templates/contacts/view.html
  • src/main/resources/templates/error.html
  • src/main/resources/templates/error/404.html
  • src/main/resources/templates/fragments/contact-fields.html
  • src/main/resources/templates/fragments/contact-list-rows.html
  • src/main/resources/templates/layout.html

Comment thread src/main/resources/application.yaml Outdated
Comment thread src/main/resources/application.yaml Outdated
Comment on lines +18 to +21
cache:
period: 0
chain:
cache: false

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

🧩 Analysis chain

🏁 Script executed:

cat -n src/main/resources/application.yaml

Repository: yamcodes/contacts

Length of output: 849


🏁 Script executed:

find src/main/resources -name "application*.yaml" -o -name "application*.yml"

Repository: yamcodes/contacts

Length of output: 96


🏁 Script executed:

find src -name "application*" -type f | head -20

Repository: yamcodes/contacts

Length of output: 96


🏁 Script executed:

head -30 README.md 2>/dev/null || echo "No README found"

Repository: yamcodes/contacts

Length of output: 2370


🏁 Script executed:

head -50 pom.xml

Repository: yamcodes/contacts

Length of output: 1836


Profile-based configuration would be a best practice if deploying to production, but isn't necessary for this educational project.

This is a learning application (0.0.1-SNAPSHOT), and the entire application.yaml is intentionally development-focused (including h2 console, devtools, and thymeleaf caching). Static resource caching being disabled is appropriate for the intended use case. If production deployment becomes planned, consider separating profiles then.

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

In `@src/main/resources/application.yaml` around lines 18 - 21, The current
application.yaml holds development-focused settings (cache.period and
chain.cache) without profile separation; extract the environment-specific keys
(cache.period and chain.cache) into profile-specific files (e.g.,
application-dev.yaml for development defaults and application-prod.yaml for
production overrides) and keep only common settings in application.yaml, then
set or document spring.profiles.active (or use spring.profiles.include) to
select the appropriate profile at runtime so production can enable different
cache values without changing the main application.yaml.

Comment on lines 2 to +7
@source "../../templates";

@plugin "daisyui";

/* noinspection CssUnknownProperty */
@plugin "daisyui/theme" {

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify stylelint config and at-rule handling for Tailwind directives.

set -euo pipefail

echo "== Locate stylelint config files =="
fd -HI 'stylelint|\.stylelintrc'

echo
echo "== Show at-rule related stylelint settings =="
rg -n -C3 'at-rule-no-unknown|scss/at-rule-no-unknown|ignoreAtRules|tailwind|daisyui'

echo
echo "== Confirm Tailwind directives present in CSS =="
rg -n '^\s*@source|^\s*@plugin' src/main/resources/static/css/input.css

Repository: yamcodes/contacts

Length of output: 9695


🏁 Script executed:

cat .stylelintrc.json

Repository: yamcodes/contacts

Length of output: 1067


Configure stylelint to recognize Tailwind v4 at-rules.

The @source, @plugin, and @plugin "daisyui/theme" directives on lines 2, 4, and 7 are valid in Tailwind v4 but your current stylelint configuration extends stylelint-config-standard-scss, which flags these as unknown at-rules and will fail CI lint checks. Add these directives to the at-rule-no-unknown rule's ignoreAtRules list in .stylelintrc.json:

Example fix
{
  "extends": ["stylelint-config-standard-scss"],
  "rules": {
    "at-rule-no-unknown": [true, { "ignoreAtRules": ["tailwind", "layer", "source", "plugin", "import"] }],
    ...
  }
}
🧰 Tools
🪛 Stylelint (17.4.0)

[error] 2-2: Unexpected unknown at-rule "@source" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)


[error] 4-4: Unexpected unknown at-rule "@plugin" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)


[error] 7-7: Unexpected unknown at-rule "@plugin" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)

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

In `@src/main/resources/static/css/input.css` around lines 2 - 7, stylelint is
flagging valid Tailwind v4 at-rules (`@source`, `@plugin`, `@plugin` "daisyui/theme")
because the config extends stylelint-config-standard-scss; update the stylelint
configuration to modify the at-rule-no-unknown rule so it ignores Tailwind
at-rules (add "tailwind", "layer", "source", "plugin", "import" to the
ignoreAtRules list) while keeping the existing extends, ensuring the
at-rule-no-unknown rule is enabled with the new ignoreAtRules entry.

Comment thread src/main/resources/templates/contacts/index.html Outdated
Comment thread src/main/resources/templates/contacts/index.html
Comment thread src/main/resources/templates/error.html Outdated
Comment on lines +13 to +14
<a href="/contacts"
class="inline-flex items-center gap-1.5 h-9 px-4 text-sm font-medium bg-zinc-900 text-white rounded-md hover:bg-zinc-800 transition-colors">

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

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and examine its content
cat -n src/main/resources/templates/error/404.html

Repository: yamcodes/contacts

Length of output: 933


🏁 Script executed:

# Search for similar hardcoded href patterns in other HTML templates
rg 'href="/' src/main/resources/templates --type html -n

Repository: yamcodes/contacts

Length of output: 499


🏁 Script executed:

# Check if there are other error or layout templates that might have similar issues
fd '\.html$' src/main/resources/templates

Repository: yamcodes/contacts

Length of output: 498


Use th:href for context-path-safe navigation.

The back link uses a hardcoded /contacts href, which will break if the application is deployed under a non-root context path. Thymeleaf's @{...} syntax automatically rewrites URLs with the correct context path.

Proposed fix
-    <a href="/contacts"
+    <a th:href="@{/contacts}"
        class="inline-flex items-center gap-1.5 h-9 px-4 text-sm font-medium bg-zinc-900 text-white rounded-md hover:bg-zinc-800 transition-colors">

This same pattern appears in layout.html and error.html and should be updated consistently across all templates.

📝 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
<a href="/contacts"
class="inline-flex items-center gap-1.5 h-9 px-4 text-sm font-medium bg-zinc-900 text-white rounded-md hover:bg-zinc-800 transition-colors">
<a th:href="@{/contacts}"
class="inline-flex items-center gap-1.5 h-9 px-4 text-sm font-medium bg-zinc-900 text-white rounded-md hover:bg-zinc-800 transition-colors">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/error/404.html` around lines 13 - 14, The anchor
in the 404 template uses a hardcoded href (the <a> element linking to
"/contacts"); change it to use Thymeleaf's context-path-safe URL syntax by
replacing the literal href attribute with th:href="@{/contacts}" on that same
anchor (and apply the same change to the same anchors in layout.html and
error.html) so links are rewritten to include the application's context path at
runtime.

Comment on lines +23 to +34
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<a th:href="@{/contacts/{slug}(slug=*{slug})}" class="btn btn-ghost btn-xs">View</a>
<a th:href="@{/contacts/{slug}/edit(slug=*{slug})}" class="btn btn-ghost btn-xs">Edit</a>
<button type="button"
hx-confirm="Are you sure you want to delete this contact?"
hx-swap="delete swap:300ms"
hx-target="closest tr"
th:attr="hx-delete=@{/contacts/{slug}(slug=*{slug})}"
class="btn btn-ghost btn-xs text-error hover:text-error">
Delete
</button>
</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 | 🟠 Major

Hover-only actions make row operations inaccessible on keyboard/touch.

At Line 23, actions are hidden unless group-hover is active. On touch devices (no hover) and keyboard navigation, controls can remain effectively invisible.

Suggested fix
-      <div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
+      <div class="flex items-center justify-end gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 transition-opacity">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/fragments/contact-list-rows.html` around lines
23 - 34, The action buttons are hidden by the Tailwind class "opacity-0
group-hover:opacity-100", which makes them inaccessible to keyboard and touch
users; update the wrapper div (the element with class "flex items-center
justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity") to also
reveal controls on keyboard focus and when a row has focus by adding
"group-focus-within:opacity-100" and ensure each control (the "View", "Edit",
and "Delete" buttons) supports keyboard focus by adding a visible focus style
class such as "focus-visible:opacity-100 focus:outline-none focus:ring" or
similar and a tabindex if necessary so these elements become visible and
operable for keyboard and touch users.

Comment thread src/main/resources/templates/fragments/contact-list-rows.html
Comment thread src/main/resources/templates/layout.html Outdated
…rations

- Add `application-dev.yaml` for development-specific configurations, including Thymeleaf caching, static resource setup, and DevTools paths.
- Update Maven `bun` plugin arguments with `--frozen-lockfile` for consistency.
- Refactor templates to use Thymeleaf expressions for better routing compatibility (`th:href`/`th:action`).
- Enhance accessibility with improved error messaging structure and focus states.
- Migrate custom font definition to `input.css` and remove inline styles.
- Add `dev` profile to Spring Boot run configuration.
- Fix conditional rendering in contact view template for initials handling.
- Correct logic in `th:text` expression to properly handle concatenation of first and last name initials.

@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: 1

♻️ Duplicate comments (1)
src/main/resources/templates/contacts/view.html (1)

23-24: ⚠️ Potential issue | 🟠 Major

Fix broken ternary expression for initials rendering.

The current expression has operator precedence issues - string concatenation + '' interferes with the ternary operators, producing incorrect results. The defensive check was attempted but implemented incorrectly.

Correct implementation using th:with
           <div class="w-10 h-10 bg-zinc-100 rounded-full flex items-center justify-center text-sm font-semibold text-zinc-600"
-               th:text="${!#strings.isEmpty(contact.first)} ? ${`#strings.substring`(contact.first, 0, 1)} : '' + ${!#strings.isEmpty(contact.last)} ? ${`#strings.substring`(contact.last, 0, 1)} : ''">AB</div>
+               th:with="firstInitial=${!#strings.isEmpty(contact.first) ? `#strings.substring`(contact.first, 0, 1) : ''},
+                        lastInitial=${!#strings.isEmpty(contact.last) ? `#strings.substring`(contact.last, 0, 1) : ''}"
+               th:text="${firstInitial + lastInitial}">AB</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/contacts/view.html` around lines 23 - 24, The
ternary concatenation in the th:text attribute is broken due to operator
precedence; replace the inline expression with a small th:with block that
computes firstInitial and lastInitial separately (e.g., evaluate contact.first
and contact.last with conditional expressions to return either the first char or
empty string) and then set th:text to the concatenation of those two variables;
update the DIV to use the computed locals (firstInitial, lastInitial) so
initials render correctly and defensively when names are empty or null.
🧹 Nitpick comments (3)
src/main/resources/application-dev.yaml (1)

8-10: Use additional-exclude instead of exclude to preserve DevTools defaults.

The spring.devtools.restart.exclude property replaces the default restart exclusion patterns entirely, which may disable important built-in exclusions. Use additional-exclude to append your HTML exclusion rule to the defaults instead.

Suggested diff
   devtools:
     livereload:
       enabled: true
     restart:
       additional-paths: src/main/resources/templates/
-      exclude: "**/*.html"
+      additional-exclude: "**/*.html"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/application-dev.yaml` around lines 8 - 10, Replace the
restart.exclude entry with restart.additional-exclude so you append (not
replace) DevTools' default exclusion patterns; specifically, under the existing
restart block (which already contains additional-paths) rename the "exclude:
\"**/*.html\"" key to "additional-exclude: \"**/*.html\"" (i.e., use
spring.devtools.restart.additional-exclude) to preserve defaults while adding
your HTML exclusion.
src/main/resources/templates/contacts/index.html (1)

100-113: Pagination hardcodes sort='last,asc', ignoring user's current sort preference.

If users can sort by different columns, this will reset their preference when paginating. Consider passing through the current sort parameter from the request.

Suggested approach

Add a sort model attribute from the controller that captures the current sort, then use it in pagination links:

-           th:href="@{/contacts(page=${contactPage.number - 1}, q=${search}, sort='last,asc', size=${contactPage.size})}"
+           th:href="@{/contacts(page=${contactPage.number - 1}, q=${search}, sort=${currentSort}, size=${contactPage.size})}"

Or use ${contactPage.sort} if Spring Data's Sort object is accessible in the template.

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

In `@src/main/resources/templates/contacts/index.html` around lines 100 - 113, The
pagination links currently hardcode sort='last,asc' which resets user sorting;
update the template to use the current sort value (e.g., replace the hardcoded
sort with the template variable ${sort} or ${contactPage.sort}) in both Prev and
Next th:href expressions and ensure the controller that prepares the model
populates a "sort" attribute from the incoming request or Spring Data Pageable
so the current sort preference is carried through; adjust any references to
contactPage, search and contactPage.size accordingly so pagination preserves the
user's sort parameter.
src/main/resources/static/css/input.css (1)

61-61: Remove quotes around single-word font name to satisfy stylelint.

Stylelint's font-family-name-quotes rule flags the quoted 'Geist'. Since it's a single-word identifier without special characters, quotes are unnecessary.

Suggested fix
 `@layer` base {
-  body { font-family: 'Geist', sans-serif; }
+  body { font-family: Geist, sans-serif; }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/css/input.css` at line 61, The CSS rule for the
body selector uses a quoted single-word font name; update the font-family
declaration in the body rule (the line containing "font-family: 'Geist',
sans-serif") to remove the quotes around Geist so it reads without quotes,
satisfying stylelint's font-family-name-quotes rule and leaving the fallback
sans-serif unchanged.
🤖 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/templates/fragments/contact-list-rows.html`:
- Around line 26-33: Update the Thymeleaf template so hx-delete uses a resolved
URL via th:attr (replace hx-delete="@{/contacts/{slug}(slug=*{slug})}" with
th:attr="hx-delete=@{/contacts/{slug}(slug=*{slug})}") and keep hx-swap="delete
swap:300ms" targeting the row; then change the delete controller endpoint that
currently returns 303 SEE_OTHER so it returns a 200 OK with either an empty body
(so htmx will remove the row) or return 200 OK with an HX-Redirect: /contacts
header if you want client-side navigation after deletion.

---

Duplicate comments:
In `@src/main/resources/templates/contacts/view.html`:
- Around line 23-24: The ternary concatenation in the th:text attribute is
broken due to operator precedence; replace the inline expression with a small
th:with block that computes firstInitial and lastInitial separately (e.g.,
evaluate contact.first and contact.last with conditional expressions to return
either the first char or empty string) and then set th:text to the concatenation
of those two variables; update the DIV to use the computed locals (firstInitial,
lastInitial) so initials render correctly and defensively when names are empty
or null.

---

Nitpick comments:
In `@src/main/resources/application-dev.yaml`:
- Around line 8-10: Replace the restart.exclude entry with
restart.additional-exclude so you append (not replace) DevTools' default
exclusion patterns; specifically, under the existing restart block (which
already contains additional-paths) rename the "exclude: \"**/*.html\"" key to
"additional-exclude: \"**/*.html\"" (i.e., use
spring.devtools.restart.additional-exclude) to preserve defaults while adding
your HTML exclusion.

In `@src/main/resources/static/css/input.css`:
- Line 61: The CSS rule for the body selector uses a quoted single-word font
name; update the font-family declaration in the body rule (the line containing
"font-family: 'Geist', sans-serif") to remove the quotes around Geist so it
reads without quotes, satisfying stylelint's font-family-name-quotes rule and
leaving the fallback sans-serif unchanged.

In `@src/main/resources/templates/contacts/index.html`:
- Around line 100-113: The pagination links currently hardcode sort='last,asc'
which resets user sorting; update the template to use the current sort value
(e.g., replace the hardcoded sort with the template variable ${sort} or
${contactPage.sort}) in both Prev and Next th:href expressions and ensure the
controller that prepares the model populates a "sort" attribute from the
incoming request or Spring Data Pageable so the current sort preference is
carried through; adjust any references to contactPage, search and
contactPage.size accordingly so pagination preserves the user's sort parameter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6fb81370-ee02-4551-a56c-dbf707b42fa6

📥 Commits

Reviewing files that changed from the base of the PR and between 45e1af8 and c0cd77f.

📒 Files selected for processing (13)
  • .run/Contacts.run.xml
  • pom.xml
  • src/main/resources/application-dev.yaml
  • src/main/resources/application.yaml
  • src/main/resources/static/css/input.css
  • src/main/resources/templates/contacts/index.html
  • src/main/resources/templates/contacts/new.html
  • src/main/resources/templates/contacts/view.html
  • src/main/resources/templates/error.html
  • src/main/resources/templates/error/404.html
  • src/main/resources/templates/fragments/contact-fields.html
  • src/main/resources/templates/fragments/contact-list-rows.html
  • src/main/resources/templates/layout.html
✅ Files skipped from review due to trivial changes (1)
  • src/main/resources/templates/error.html
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/resources/application.yaml
  • src/main/resources/templates/error/404.html
  • src/main/resources/templates/contacts/new.html
  • pom.xml

Comment on lines +26 to +33
<button type="button"
hx-confirm="Are you sure you want to delete this contact?"
hx-swap="delete swap:300ms"
hx-target="closest tr"
hx-delete="@{/contacts/{slug}(slug=*{slug})}"
class="btn btn-ghost btn-xs text-error hover:text-error">
Delete
</button>

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the delete endpoint response behavior
rg -A 10 '@DeleteMapping.*slug' src/main/java/codes/yam/contacts/ContactController.java

Repository: yamcodes/contacts

Length of output: 543


🏁 Script executed:

#!/bin/bash
# Check how other hx- attributes are used in templates
fd -e html -x grep -l "hx-" {} \;

Repository: yamcodes/contacts

Length of output: 286


🏁 Script executed:

#!/bin/bash
# Look for th:attr usage with hx- attributes in templates
rg "th:attr.*hx-" src/main/resources/templates/ -A 2 -B 2

Repository: yamcodes/contacts

Length of output: 1668


🏁 Script executed:

#!/bin/bash
# Check how hx- attributes are currently written in the codebase
rg "hx-(get|post|delete|put)" src/main/resources/templates/ | head -20

Repository: yamcodes/contacts

Length of output: 529


🌐 Web query:

htmx hx-swap delete response redirect 303

💡 Result:

In htmx, when using hx-delete with hx-swap="delete", the standard approach is to return a 200 OK status with an empty body from the server. This triggers the deletion of the target element, as hx-swap="delete" removes the target regardless of the response content (except for special cases like 204 No Content, which skips swapping). Official htmx docs and examples recommend this for delete operations, such as removing a table row. A 303 See Other response is not typically used or recommended for this pattern. htmx, via XMLHttpRequest/Fetch, follows 3xx redirects transparently as part of the AJAX request: the browser handles the redirect (potentially changing the method per HTTP rules, e.g., 303 changes non-GET to GET), fetches the final response, and swaps based on that final content/status. Response headers like HX-Redirect or HX-Location from the initial 303 are ignored because headers are not processed on 3xx responses. This means: - If the 303 redirects to a URL returning 200 empty, it may delete the target (if swapped). - If it redirects to a full page, that page swaps into the target (often undesired). - No full browser redirect occurs; it remains an AJAX swap. To achieve a full page redirect after delete (e.g., to a list page), return 200 OK with an HX-Redirect: /new-url header instead of 303. This triggers a browser navigation. For hx-swap="delete" specifically, combine with server logic to decide: empty 200 for local delete, or HX-Redirect for page change. Example for local delete: Delete Server: 200 OK, empty body. Example for redirect after delete: Server: 200 OK, HX-Redirect: /items. Note: 204 skips swap (including delete), so use 200 empty for deletion.

Citations:


🌐 Web query:

htmx delete swap strategy HTTP response code

💡 Result:

For htmx DELETE requests (hx-delete), the recommended HTTP response code to trigger a swap that removes or replaces an element (e.g., using hx-swap="delete" or "outerHTML") is 200 OK with an empty body. A 204 No Content response prevents any swap by default, as htmx interprets 204 as "do nothing" to the target element. Key points: - hx-swap="delete": Deletes the target regardless of response content, but requires a swappable response (200/201 OK, not 204). - Official htmx delete-row example uses 200 with empty content to remove a row. - To enable swap on 204, listen to htmx:beforeSwap and set detail.shouldSwap = true. - Configurable via htmx.config.responseHandling.

Citations:


Critical: hx-delete URL won't resolve – use th:attr for Thymeleaf expressions.

The hx-delete="@{/contacts/{slug}(slug=*{slug})}" passes the literal string to htmx instead of the resolved URL. Other templates in the codebase correctly bind Thymeleaf expressions via th:attr="hx-delete=@{...}".

Additionally, the controller returns 303 SEE_OTHER redirect, but hx-swap="delete swap:300ms" expects a 200 OK empty response to remove the row. A 303 redirect causes htmx to fetch the final response at /contacts and swap that page content into the target—the row will not be deleted.

Fix the template binding and update the endpoint to return 200 OK with an empty body, or return 200 OK with the HX-Redirect: /contacts header for post-delete navigation.

Fix URL binding
         <button type="button"
                 hx-confirm="Are you sure you want to delete this contact?"
                 hx-swap="delete swap:300ms"
                 hx-target="closest tr"
-                hx-delete="@{/contacts/{slug}(slug=*{slug})}"
+                th:attr="hx-delete=@{/contacts/{slug}(slug=*{slug})}"
                 class="btn btn-ghost btn-xs text-error hover:text-error">
           Delete
         </button>
📝 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
<button type="button"
hx-confirm="Are you sure you want to delete this contact?"
hx-swap="delete swap:300ms"
hx-target="closest tr"
hx-delete="@{/contacts/{slug}(slug=*{slug})}"
class="btn btn-ghost btn-xs text-error hover:text-error">
Delete
</button>
<button type="button"
hx-confirm="Are you sure you want to delete this contact?"
hx-swap="delete swap:300ms"
hx-target="closest tr"
th:attr="hx-delete=@{/contacts/{slug}(slug=*{slug})}"
class="btn btn-ghost btn-xs text-error hover:text-error">
Delete
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/fragments/contact-list-rows.html` around lines
26 - 33, Update the Thymeleaf template so hx-delete uses a resolved URL via
th:attr (replace hx-delete="@{/contacts/{slug}(slug=*{slug})}" with
th:attr="hx-delete=@{/contacts/{slug}(slug=*{slug})}") and keep hx-swap="delete
swap:300ms" targeting the row; then change the delete controller endpoint that
currently returns 303 SEE_OTHER so it returns a 200 OK with either an empty body
(so htmx will remove the row) or return 200 OK with an HX-Redirect: /contacts
header if you want client-side navigation after deletion.

- Add hover effects, clickable rows, and revamped layout for contact details.
- Combine "First" and "Last" columns into a single "Name" column.
- Improve bulk delete functionality with confirmation prompt.
- Simplify and modernize table structure, aligning with updated design practices.
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.

1 participant