Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 10 additions & 7 deletions src/main/java/codes/yam/contacts/ContactController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.net.URI;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
Expand All @@ -19,12 +21,13 @@ public class ContactController {
private final jakarta.validation.Validator validator;

@GetMapping
public String contacts(@RequestParam(required = false) String q, Model model) {
var contacts =
(q != null && !q.isBlank()) ? contactService.search(q) : contactService.findAll();
model.addAttribute("contacts", contacts);
public String contacts(
@RequestParam(required = false) String q,
@PageableDefault(sort = "last") Pageable pageable,
Model model) {
model.addAttribute("contactPage", contactService.list(q, pageable));
model.addAttribute("search", q);
return "contacts/list";
return "contacts/index";
}

@GetMapping("/{slug}")
Expand Down Expand Up @@ -86,10 +89,10 @@ public ResponseEntity<Void> deleteManyContacts(
public String validateEmail(@PathVariable String slug, @RequestParam String email) {
var violations = validator.validateValue(Contact.class, "email", email);
if (!violations.isEmpty()) {
return violations.iterator().next().getMessage();
return "<span class=\"error\">" + violations.iterator().next().getMessage() + "</span>";
}
if (contactService.isEmailTaken(email, slug)) {
return "Email already taken";
return "<span class=\"error\">Email already taken</span>";
}
return "";
}
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/codes/yam/contacts/ContactRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand All @@ -20,5 +22,5 @@ public interface ContactRepository extends JpaRepository<Contact, UUID> {
+ "LOWER(c.first) LIKE LOWER(CONCAT('%', :q, '%')) OR "
+ "LOWER(c.last) LIKE LOWER(CONCAT('%', :q, '%')) OR "
+ "LOWER(c.email) LIKE LOWER(CONCAT('%', :q, '%'))")
List<Contact> search(@Param("q") String q);
Page<Contact> search(@Param("q") String q, Pageable pageable);
}
17 changes: 9 additions & 8 deletions src/main/java/codes/yam/contacts/ContactService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ContactService {
private final ContactRepository contactRepository;

public List<Contact> findAll() {
return contactRepository.findAll();
}

public List<Contact> search(String q) {
return contactRepository.search(q);
public Page<Contact> list(String q, Pageable pageable) {
if (q != null && !q.isBlank()) {
return contactRepository.search(q, pageable);
}
return contactRepository.findAll(pageable);
}

public Contact findBySlug(String slug) {
Expand Down Expand Up @@ -46,9 +47,9 @@ public Contact update(String slug, Contact updated) {
return contactRepository.save(contact);
}

// TODO: This is fragile - throws NullPointException if first or last is null.
private String generateSlug(Contact contact) {
return contact.getFirst().toLowerCase() + "-" + contact.getLast().toLowerCase();
return (contact.getFirst().toLowerCase() + "-" + contact.getLast().toLowerCase())
.replaceAll("[^a-z0-9-]", "");
}

/**
Expand Down
52 changes: 31 additions & 21 deletions src/main/resources/data.sql
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
INSERT INTO contact (id, slug, first, last, email, phone)
VALUES ('550e8400-e29b-41d4-a716-446655440000', 'john-doe', 'John', 'Doe', 'john@example.com', '555-1234'),
('550e8400-e29b-41d4-a716-446655440001', 'jane-doe', 'Jane', 'Doe', 'jane@example.com', '555-5678'),
('550e8400-e29b-41d4-a716-446655440002', 'alice-smith', 'Alice', 'Smith', 'alice@example.com', '555-2001'),
('550e8400-e29b-41d4-a716-446655440003', 'bob-johnson', 'Bob', 'Johnson', 'bob@example.com', '555-2002'),
('550e8400-e29b-41d4-a716-446655440004', 'carol-williams', 'Carol', 'Williams', 'carol@example.com', '555-2003'),
('550e8400-e29b-41d4-a716-446655440005', 'dave-brown', 'Dave', 'Brown', 'dave@example.com', '555-2004'),
('550e8400-e29b-41d4-a716-446655440006', 'eve-jones', 'Eve', 'Jones', 'eve@example.com', '555-2005'),
('550e8400-e29b-41d4-a716-446655440007', 'frank-garcia', 'Frank', 'Garcia', 'frank@example.com', '555-2006'),
('550e8400-e29b-41d4-a716-446655440008', 'grace-martinez', 'Grace', 'Martinez', 'grace@example.com', '555-2007'),
('550e8400-e29b-41d4-a716-446655440009', 'henry-davis', 'Henry', 'Davis', 'henry@example.com', '555-2008'),
('550e8400-e29b-41d4-a716-44665544000a', 'iris-miller', 'Iris', 'Miller', 'iris@example.com', '555-2009'),
('550e8400-e29b-41d4-a716-44665544000b', 'jack-wilson', 'Jack', 'Wilson', 'jack@example.com', '555-2010'),
('550e8400-e29b-41d4-a716-44665544000c', 'karen-moore', 'Karen', 'Moore', 'karen@example.com', '555-2011'),
('550e8400-e29b-41d4-a716-44665544000d', 'leo-taylor', 'Leo', 'Taylor', 'leo@example.com', '555-2012'),
('550e8400-e29b-41d4-a716-44665544000e', 'mia-anderson', 'Mia', 'Anderson', 'mia@example.com', '555-2013'),
('550e8400-e29b-41d4-a716-44665544000f', 'nick-thomas', 'Nick', 'Thomas', 'nick@example.com', '555-2014'),
('550e8400-e29b-41d4-a716-446655440010', 'olivia-jackson', 'Olivia', 'Jackson', 'olivia@example.com', '555-2015'),
('550e8400-e29b-41d4-a716-446655440011', 'paul-white', 'Paul', 'White', 'paul@example.com', '555-2016'),
('550e8400-e29b-41d4-a716-446655440012', 'quinn-harris', 'Quinn', 'Harris', 'quinn@example.com', '555-2017'),
('550e8400-e29b-41d4-a716-446655440013', 'rachel-clark', 'Rachel', 'Clark', 'rachel@example.com', '555-2018');
INSERT INTO contact (id, slug, first, last, email, phone) VALUES
('550e8400-e29b-41d4-a716-446655440000', 'john-doe', 'John', 'Doe', 'john@example.com', '555-1234'),
('550e8400-e29b-41d4-a716-446655440001', 'jane-doe', 'Jane', 'Doe', 'jane@example.com', '555-5678'),
('550e8400-e29b-41d4-a716-446655440002', 'alice-smith', 'Alice', 'Smith', 'alice.smith@example.com', '555-1001'),
('550e8400-e29b-41d4-a716-446655440003', 'bob-johnson', 'Bob', 'Johnson', 'bob.johnson@example.com', '555-1002'),
('550e8400-e29b-41d4-a716-446655440004', 'carol-williams', 'Carol', 'Williams', 'carol.williams@example.com', '555-1003'),
('550e8400-e29b-41d4-a716-446655440005', 'david-brown', 'David', 'Brown', 'david.brown@example.com', '555-1004'),
('550e8400-e29b-41d4-a716-446655440006', 'eve-jones', 'Eve', 'Jones', 'eve.jones@example.com', '555-1005'),
('550e8400-e29b-41d4-a716-446655440007', 'frank-garcia', 'Frank', 'Garcia', 'frank.garcia@example.com', '555-1006'),
('550e8400-e29b-41d4-a716-446655440008', 'grace-miller', 'Grace', 'Miller', 'grace.miller@example.com', '555-1007'),
('550e8400-e29b-41d4-a716-446655440009', 'henry-davis', 'Henry', 'Davis', 'henry.davis@example.com', '555-1008'),
('550e8400-e29b-41d4-a716-44665544000a', 'iris-martinez', 'Iris', 'Martinez', 'iris.martinez@example.com', '555-1009'),
('550e8400-e29b-41d4-a716-44665544000b', 'jack-wilson', 'Jack', 'Wilson', 'jack.wilson@example.com', '555-1010'),
('550e8400-e29b-41d4-a716-44665544000c', 'karen-anderson', 'Karen', 'Anderson', 'karen.anderson@example.com', '555-1011'),
('550e8400-e29b-41d4-a716-44665544000d', 'leo-thomas', 'Leo', 'Thomas', 'leo.thomas@example.com', '555-1012'),
('550e8400-e29b-41d4-a716-44665544000e', 'mia-taylor', 'Mia', 'Taylor', 'mia.taylor@example.com', '555-1013'),
('550e8400-e29b-41d4-a716-44665544000f', 'noah-hernandez', 'Noah', 'Hernandez', 'noah.hernandez@example.com', '555-1014'),
('550e8400-e29b-41d4-a716-446655440010', 'olivia-moore', 'Olivia', 'Moore', 'olivia.moore@example.com', '555-1015'),
('550e8400-e29b-41d4-a716-446655440011', 'paul-jackson', 'Paul', 'Jackson', 'paul.jackson@example.com', '555-1016'),
('550e8400-e29b-41d4-a716-446655440012', 'quinn-martin', 'Quinn', 'Martin', 'quinn.martin@example.com', '555-1017'),
('550e8400-e29b-41d4-a716-446655440013', 'rachel-lee', 'Rachel', 'Lee', 'rachel.lee@example.com', '555-1018'),
('550e8400-e29b-41d4-a716-446655440014', 'sam-perez', 'Sam', 'Perez', 'sam.perez@example.com', '555-1019'),
('550e8400-e29b-41d4-a716-446655440015', 'tina-white', 'Tina', 'White', 'tina.white@example.com', '555-1020'),
('550e8400-e29b-41d4-a716-446655440016', 'ursula-harris', 'Ursula', 'Harris', 'ursula.harris@example.com', '555-1021'),
('550e8400-e29b-41d4-a716-446655440017', 'victor-clark', 'Victor', 'Clark', 'victor.clark@example.com', '555-1022'),
('550e8400-e29b-41d4-a716-446655440018', 'wendy-lewis', 'Wendy', 'Lewis', 'wendy.lewis@example.com', '555-1023'),
('550e8400-e29b-41d4-a716-446655440019', 'xavier-robinson', 'Xavier', 'Robinson', 'xavier.robinson@example.com', '555-1024'),
('550e8400-e29b-41d4-a716-44665544001a', 'yara-walker', 'Yara', 'Walker', 'yara.walker@example.com', '555-1025'),
('550e8400-e29b-41d4-a716-44665544001b', 'zane-hall', 'Zane', 'Hall', 'zane.hall@example.com', '555-1026'),
('550e8400-e29b-41d4-a716-44665544001c', 'amber-allen', 'Amber', 'Allen', 'amber.allen@example.com', '555-1027'),
('550e8400-e29b-41d4-a716-44665544001d', 'brian-young', 'Brian', 'Young', 'brian.young@example.com', '555-1028');
55 changes: 55 additions & 0 deletions src/main/resources/templates/contacts/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en" layout:decorate="~{layout}"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org">
<body>
<!--/*@thymesVar id="search" 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>
</form>

<p th:unless="${#strings.isEmpty(search)}">Results for "<span th:text="${search}"></span>":</p>

<p th:if="${contactPage.content.isEmpty()}">No contacts found.</p>
<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>
</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>
52 changes: 0 additions & 52 deletions src/main/resources/templates/contacts/list.html

This file was deleted.

3 changes: 2 additions & 1 deletion src/main/resources/templates/fragments/contact-fields.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
<div>
<label for="email">Email</label>
<input hx-target="next div" hx-trigger="change, keyup delay:200ms changed" placeholder="Email"
th:attr="${contact.slug != null} ? 'hx-get='+@{/contacts/{slug}/email(slug=${contact.slug})}"
th:with="validateUrl=@{/contacts/{slug}/email(slug=${contact.slug})}"
th:attr="hx-get=${contact.slug != null} ? ${validateUrl} : null"
th:field="*{email}"
type="email"/>
<div>
Expand Down
5 changes: 3 additions & 2 deletions src/main/resources/templates/fragments/contact-list-rows.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<th:block th:fragment="rows">
<th:block th:fragment="rows(contacts)">
<!--/*@thymesVar id="contacts" type="java.util.List<codes.yam.contacts.Contact>"*/-->
<!--/*@thymesVar id="contact" type="codes.yam.contacts.Contact"*/-->
<tr th:each="contact : ${contacts}" th:object="${contact}">
<td>
<input
name="selected_contact_slugs"
th:value="*{slug}"
th:attr="aria-label=*{first} + ' ' + *{last}"
th:value="*{slug}"
type="checkbox"
/>
</td>
Expand Down