Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
37df0c9
Add Tailwind CSS and DaisyUI setup with build and watch scripts
yamcodes Mar 18, 2026
ea43237
Merge branch 'main' into 10-add-tailwind-css
yamcodes Mar 18, 2026
47f2bdb
Add run configurations for "Contacts (Full)" and "Watch CSS"
yamcodes Mar 18, 2026
3a98aee
Refactor and redesign contact management templates for improved user …
yamcodes Mar 18, 2026
ef5cbb1
Refactor contact management templates to use DaisyUI fieldsets and ta…
yamcodes Mar 18, 2026
2b0c3f6
Improve search form SVG usability and update color theme configuration
yamcodes Mar 18, 2026
d73e9c7
Configure static resources in `application.yaml` with caching disabled
yamcodes Mar 18, 2026
9508906
Update template prefix, add dictionary word, and improve branding text
yamcodes Mar 18, 2026
45e1af8
Configure DevTools for additional restart paths and exclude HTML file…
yamcodes Mar 18, 2026
46dc83a
Resolve merge conflicts with main
yamcodes Mar 18, 2026
c0cd77f
Configure Dev environment, refactor templates, and update run configu…
yamcodes Mar 18, 2026
1eac456
Fix conditional rendering for contact initials in template
yamcodes Mar 18, 2026
2f13ab9
Simplify contact initials rendering logic in view template
yamcodes Mar 18, 2026
5e0a945
Redesign contact list rows with enhanced UI and interactivity
yamcodes Mar 18, 2026
730eee3
Refactor delete button to use `th:attr` for dynamic `hx-delete` attri…
yamcodes Mar 18, 2026
4ac0936
Update delete button to use `hx-push-url` and adjust `hx-target` for …
yamcodes Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .idea/dictionaries/project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .run/Contacts.run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<configuration default="false" name="Contacts" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
<module name="contacts" />
<option name="SPRING_BOOT_MAIN_CLASS" value="codes.yam.contacts.ContactsApplication" />
<option name="ACTIVE_PROFILES" value="dev" />
<method v="2">
<option name="Make" enabled="true" />
</method>
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
<goals><goal>bun</goal></goals>
<phase>initialize</phase>
<configuration>
<arguments>install</arguments>
<arguments>install --frozen-lockfile</arguments>
</configuration>
</execution>
<execution>
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/codes/yam/contacts/ContactController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.validation.Valid;
import java.net.URI;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
Expand All @@ -27,6 +28,9 @@ public String contacts(
Model model) {
model.addAttribute("contactPage", contactService.list(q, pageable));
model.addAttribute("search", q);
model.addAttribute("sort", pageable.getSort().stream()
.map(o -> o.getProperty() + "," + o.getDirection().name().toLowerCase())
.collect(Collectors.joining(",")));
return "contacts/index";
}

Expand Down Expand Up @@ -70,7 +74,6 @@ public String createContact(@Valid @ModelAttribute Contact contact, BindingResul
@DeleteMapping("/{slug}")
public ResponseEntity<Void> deleteContact(@PathVariable String slug) {
contactService.delete(slug);
// TODO: extract seeOther helper if this pattern repeats
return ResponseEntity.status(HttpStatus.SEE_OTHER).location(URI.create("/contacts")).build();
}

Expand Down
19 changes: 19 additions & 0 deletions src/main/resources/application-dev.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
spring:
thymeleaf:
cache: false
prefix: file:src/main/resources/templates/
devtools:
livereload:
enabled: true
restart:
additional-paths: src/main/resources/templates/
additional-exclude: "**/*.html"
web:
resources:
static-locations:
- file:src/main/resources/static/
- classpath:/static/
cache:
period: 0
chain:
cache: false
7 changes: 2 additions & 5 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ spring:
application:
name: Contacts
thymeleaf:
cache: false
devtools:
livereload:
enabled: true
cache: true
h2:
console:
enabled: true
jpa:
defer-datasource-initialization: true
datasource:
url: jdbc:h2:mem:contacts;DB_CLOSE_DELAY=-1
url: jdbc:h2:mem:contacts;DB_CLOSE_DELAY=-1
66 changes: 66 additions & 0 deletions src/main/resources/static/css/input.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,69 @@
@import "tailwindcss";
@source "../../templates";

@plugin "daisyui";

/* noinspection CssUnknownProperty */
@plugin "daisyui/theme" {
Comment on lines 2 to +7

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.

name: "shadcn";
default: true;
color-scheme: light;

/* Surfaces */
--color-base-100: oklch(100% 0 0);
--color-base-200: oklch(97.8% 0 0);
--color-base-300: oklch(94.5% 0 0);
--color-base-content: oklch(14.1% 0 0);

/* Primary — zinc-900 */
--color-primary: oklch(14.1% 0 0);
--color-primary-content: oklch(100% 0 0);

/* Secondary — zinc-100 */
--color-secondary: oklch(94.5% 0 0);
--color-secondary-content: oklch(20.5% 0 0);

/* Accent — same as primary */
--color-accent: oklch(14.1% 0 0);
--color-accent-content: oklch(100% 0 0);

/* Neutral */
--color-neutral: oklch(27.4% 0 0);
--color-neutral-content: oklch(100% 0 0);

/* States */
--color-error: oklch(57.7% 0.245 27.3);
--color-error-content: oklch(100% 0 0);
--color-warning: oklch(76.9% 0.188 70.1);
--color-warning-content: oklch(14.1% 0 0);
--color-success: oklch(64.8% 0.15 160);
--color-success-content: oklch(100% 0 0);
--color-info: oklch(60% 0.12 230);
--color-info-content: oklch(100% 0 0);

/* Shape — matches shadcn's rounded-md */
--radius-selector: 0.375rem;
--radius-field: 0.375rem;
--radius-box: 0.5rem;

/* Flat — no depth/emboss effects */
--depth: 0;
--noise: 0;

--border: 1px;

/* Button — match shadcn sizing and weight */
--size-field: 0.225rem; /* × 10 = 2.25rem = h-9, shadcn default */
--fontsize: 0.875rem;
}

@layer base {
body { font-family: Geist, sans-serif; }
}

@layer components {
.btn {
font-weight: 500;
letter-spacing: 0;
}
}
52 changes: 37 additions & 15 deletions src/main/resources/templates/contacts/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,43 @@
<body>
<!--/*@thymesVar id="contact" type="codes.yam.contacts.Contact"*/-->
<div layout:fragment="content">
<h2>Edit Contact</h2>
<form method="post" th:action="@{/contacts/{slug}/edit(slug=${contact.slug})}">
<th:block th:replace="~{fragments/contact-fields::contact-fields}"></th:block>
<button type="submit">Save</button>
</form>
<button
hx-confirm="Are you sure you want to delete this contact?"
hx-push-url="true"
hx-target="body"
th:attr="hx-delete=@{/contacts/{slug}(slug=${contact.slug})}">
Delete Contact
</button>
<p>
<a href="/contacts">Back</a>
</p>
<div class="max-w-xl">

<div class="mb-6">
<a th:href="@{/contacts}" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-900 transition-colors mb-4">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M9 11L5 7l4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
Back to contacts
</a>
<h1 class="text-xl font-semibold text-zinc-900"
th:text="${contact.first} + ' ' + ${contact.last}">Edit Contact</h1>
<p class="text-sm text-zinc-500 mt-0.5">Update contact information.</p>
</div>

<div class="bg-white border border-zinc-200 rounded-lg p-6 mb-4">
<form method="post" th:action="@{/contacts/{slug}/edit(slug=${contact.slug})}">
<th:block th:replace="~{fragments/contact-fields::contact-fields}"></th:block>
<div class="mt-6 flex items-center gap-3">
<button type="submit" class="btn btn-primary btn-sm">Save Changes</button>
<a th:href="@{/contacts}" class="btn btn-ghost btn-sm">Cancel</a>
</div>
</form>
</div>

<div class="bg-white border border-red-200 rounded-lg p-4">
<h2 class="text-sm font-medium text-zinc-900 mb-1">Danger Zone</h2>
<p class="text-xs text-zinc-500 mb-3">Permanently delete this contact. This action cannot be undone.</p>
<button
hx-confirm="Are you sure you want to delete this contact?"
hx-push-url="true"
hx-target="body"
th:attr="hx-delete=@{/contacts/{slug}(slug=${contact.slug})}"
class="btn btn-error btn-outline btn-sm gap-1.5">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M1.5 3h9M4.5 3V1.5h3V3M10.5 3l-.75 7.5h-7.5L1.5 3" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/></svg>
Delete Contact
</button>
</div>

</div>
</div>
</body>
</html>
139 changes: 101 additions & 38 deletions src/main/resources/templates/contacts/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,115 @@
xmlns:th="http://www.thymeleaf.org">
<body>
<!--/*@thymesVar id="search" type="java.lang.String"*/-->
<!--/*@thymesVar id="sort" type="java.lang.String"*/-->
<!--/*@thymesVar id="contactPage" type="org.springframework.data.domain.Page<codes.yam.contacts.Contact>"*/-->
<div layout:fragment="content">
<form action="/contacts" method="get">
<label for="search">Search contacts:</label>
<input
id="search" name="q"
placeholder="Search contacts..."
th:value="${search}"
type="search"
/>
<button type="submit">Search</button>

<!-- Page header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-xl font-semibold text-zinc-900">Contacts</h1>
<p class="text-sm text-zinc-500 mt-0.5"
th:text="${contactPage.totalElements} + ' total'">0 total</p>
</div>
<a th:href="@{/contacts/new}"
class="btn btn-primary btn-sm gap-1.5">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1v12M1 7h12" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/>
</svg>
New Contact
</a>
</div>

<!-- Search -->
<form th:action="@{/contacts}" method="get" class="mb-4">
<div class="flex gap-2">
<div class="relative flex-1 max-w-sm">
<svg class="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none" width="14" height="14" viewBox="0 0 14 14" fill="none">
<circle cx="6" cy="6" r="4.25" stroke="currentColor" stroke-width="1.5"/>
<path d="M9.5 9.5L13 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<label for="search" class="sr-only">Search contacts</label>
<input
id="search" name="q"
placeholder="Search contacts..."
th:value="${search}"
type="search"
class="w-full h-9 pl-9 pr-3 text-sm bg-white border border-zinc-200 rounded-md focus:outline-none focus:ring-2 focus:ring-zinc-900 focus:border-transparent placeholder:text-zinc-400"
/>
</div>
<a th:if="${!#strings.isEmpty(search)}" th:href="@{/contacts}" class="btn btn-ghost btn-sm">Clear</a>
</div>
<p th:unless="${#strings.isEmpty(search)}"
class="mt-2 text-sm text-zinc-500">
Results for "<span class="text-zinc-900 font-medium" th:text="${search}"></span>"
</p>
</form>

<p th:unless="${#strings.isEmpty(search)}">Results for "<span th:text="${search}"></span>":</p>
<!-- Empty state -->
<div th:if="${contactPage.content.isEmpty()}"
class="bg-white border border-zinc-200 rounded-lg py-16 text-center">
<div class="w-10 h-10 bg-zinc-100 rounded-full flex items-center justify-center mx-auto mb-3">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="6" r="3.25" stroke="#71717a" stroke-width="1.5"/>
<path d="M3 15c0-3.314 2.686-6 6-6s6 2.686 6 6" stroke="#71717a" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<p class="text-sm font-medium text-zinc-900">No contacts found</p>
<p class="text-sm text-zinc-500 mt-1">
<span th:if="${!#strings.isEmpty(search)}">Try a different search term or </span>
<a th:href="@{/contacts/new}" class="text-zinc-900 underline underline-offset-2">add your first contact</a>
</p>
</div>

<p th:if="${contactPage.content.isEmpty()}">No contacts found.</p>
<!-- Table -->
<form th:unless="${contactPage.content.isEmpty()}"
th:attr="hx-delete=@{/contacts}"
hx-target="body"
hx-push-url="true">
<table>
<thead>
<tr>
<th></th>
<th>First</th>
<th>Last</th>
<th>Phone</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody th:replace="~{fragments/contact-list-rows::rows(contacts=${contactPage.content})}">
</tbody>
</table>
<button type="submit">Delete Selected</button>
hx-push-url="true"
hx-confirm="Delete selected contacts? This cannot be undone.">
<div class="bg-white border border-zinc-200 rounded-lg overflow-hidden">
<table class="table">
<thead>
<tr>
<th class="w-10"></th>
<th>Name</th>
<th>Phone</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody th:replace="~{fragments/contact-list-rows::rows(contacts=${contactPage.content})}">
</tbody>
</table>
</div>

<!-- Bulk actions + pagination row -->
<div class="mt-3 flex items-center justify-between">
<button type="submit" class="btn btn-error btn-outline btn-sm gap-1.5">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 3h9M4.5 3V1.5h3V3M10.5 3l-.75 7.5h-7.5L1.5 3" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Delete Selected
</button>

<div th:if="${contactPage.totalPages > 1}" class="flex items-center gap-1">
<a th:href="@{/contacts(page=${contactPage.number - 1}, q=${search}, sort=${sort}, size=${contactPage.size})}"
th:if="${!contactPage.isFirst()}"
class="btn btn-ghost btn-xs gap-1">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M7.5 9L4.5 6l3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
Prev
</a>
<span class="px-2 text-xs text-zinc-500"
th:text="${contactPage.number + 1} + ' / ' + ${contactPage.totalPages}"></span>
<a th:href="@{/contacts(page=${contactPage.number + 1}, q=${search}, sort=${sort}, size=${contactPage.size})}"
th:if="${!contactPage.isLast()}"
class="btn btn-ghost btn-xs gap-1">
Next
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 9L7.5 6l-3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</a>
</div>
</div>
</form>
<div th:if="${contactPage.totalPages > 1}">
<a th:href="@{/contacts(page=${contactPage.number - 1}, q=${search}, sort='last,asc', size=${contactPage.size})}"
th:if="${!contactPage.isFirst()}">Previous</a>
<span th:text="${contactPage.number + 1} + ' / ' + ${contactPage.totalPages}"></span>
<a th:href="@{/contacts(page=${contactPage.number + 1}, q=${search}, sort='last,asc', size=${contactPage.size})}"
th:if="${!contactPage.isLast()}">Next</a>
</div>
<p>
<a th:href="@{/contacts/new}">Add New Contact</a>
</p>
</div>
</body>
</html>
</html>
Loading