Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add collection feature #19

Merged
merged 2 commits into from
Dec 22, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.vmaier.marvel.snap.cards.db.dao.Card
import com.vmaier.marvel.snap.cards.dto.CreateCardDTO
import com.vmaier.marvel.snap.cards.service.CardsService
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.*
Expand All @@ -25,7 +26,7 @@ class CardsController constructor(private val cardsService: CardsService) {
if (pageSize > Constants.MAX_PAGE_SIZE) {
pageSize = Constants.MAX_PAGE_SIZE
}
val cardPage = cardsService.getAllCardsByKeyword(PageRequest.of(currentPage - 1, pageSize), keyword)
val cardPage = cardsService.getAllCardsByKeyword(PageRequest.of(currentPage - 1, pageSize, Sort.by("name").ascending()), keyword)
model.addAttribute("keyword", keyword)
model.addAttribute("cardPage", cardPage)
return "cards"
Expand All @@ -36,14 +37,16 @@ class CardsController constructor(private val cardsService: CardsService) {
model: Model,
@RequestParam(value = "keyword", required = false) keyword: String?,
@RequestParam(value = "cost", required = false) cost: Int?,
@RequestParam(value = "power", required = false) power: Int?
@RequestParam(value = "power", required = false) power: Int?,
@RequestParam(value = "isOwned", required = false) isOwned: Boolean?,
): String {
val cards = cardsService.getAllCardsByKeyword(keyword, cost, power)
val cards = cardsService.getAllCardsByKeyword(keyword, cost, power, isOwned).sortedBy { card -> card.name }
val costValues = cards.map { card: Card -> card.cost }.toSet().sorted()
val powerValues = cards.map { card: Card -> card.power }.toSet().sorted()
model.addAttribute("keyword", keyword)
model.addAttribute("cost", cost)
model.addAttribute("power", power)
model.addAttribute("isOwned", isOwned)
model.addAttribute("costValues", costValues)
model.addAttribute("powerValues", powerValues)
model.addAttribute("cards", cards)
Expand Down Expand Up @@ -72,6 +75,6 @@ class CardsController constructor(private val cardsService: CardsService) {
@DeleteMapping("cards")
fun removeCards(model: Model): String {
cardsService.removeAllCards()
return "redirect:/cards"
return "redirect:/card-grid"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,16 @@ class CardsApiController constructor(private val cardsService: CardsService) : C
cardsService.removeAllCards()
return ResponseEntity(HttpStatus.NO_CONTENT)
}

override fun addToCollection(cardId: Int): ResponseEntity<Void> {
Validator.checkIfCardIdIsValid(cardId)
cardsService.addToCollection(cardId)
return ResponseEntity(HttpStatus.NO_CONTENT)
}

override fun removeFromCollection(cardId: Int): ResponseEntity<Void> {
Validator.checkIfCardIdIsValid(cardId)
cardsService.removeFromCollection(cardId)
return ResponseEntity(HttpStatus.NO_CONTENT)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class Card(
val power: Int,
val ability: String? = null,
val url: String? = null,
val isOwned: Boolean = false,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Int? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ interface CardRepository : CrudRepository<Card, Int> {
@Modifying
@Query("UPDATE Card c SET c.url = :url WHERE c.id = :id")
fun updateImageUrl(@Param("id") id: Int, @Param("url") url: String)

@Modifying
@Query("UPDATE Card c SET c.isOwned = :isOwned WHERE c.id = :id")
fun updateIsOwned(@Param("id") id: Int, @Param("isOwned") isOwned: Boolean)
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,22 @@ interface CardsApi {
)
@DeleteMapping
fun removeCards(): ResponseEntity<Void>

@Operation(summary = "Add card to collection", description = "TODO ...")
@ApiResponses(
value = [
ApiResponse(responseCode = "204", description = "No Content")
]
)
@PostMapping("{cardId}/collection")
fun addToCollection(@PathVariable("cardId") cardId: Int): ResponseEntity<Void>

@Operation(summary = "Remove card from collection", description = "TODO ...")
@ApiResponses(
value = [
ApiResponse(responseCode = "204", description = "No Content")
]
)
@DeleteMapping("{cardId}/collection")
fun removeFromCollection(@PathVariable("cardId") cardId: Int): ResponseEntity<Void>
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class CardsService constructor(
return cards
}

fun getAllCardsByKeyword(keyword: String?, cost: Int?, power: Int?): Iterable<Card> {
fun getAllCardsByKeyword(keyword: String?, cost: Int?, power: Int?, isOwned: Boolean?): Iterable<Card> {
var cards = if (keyword.isNullOrEmpty()) {
cardRepository.findAll()
} else {
Expand All @@ -59,6 +59,11 @@ class CardsService constructor(
card.power == power
}
}
if (isOwned != null) {
cards = cards.filter { card ->
card.isOwned == isOwned
}
}
return cards
}

Expand All @@ -81,4 +86,12 @@ class CardsService constructor(
fun removeAllCards() {
cardRepository.deleteAll()
}

fun addToCollection(cardId: Int) {
cardRepository.updateIsOwned(getOneCard(cardId).id!!, true)
}

fun removeFromCollection(cardId: Int) {
cardRepository.updateIsOwned(getOneCard(cardId).id!!, false)
}
}
13 changes: 7 additions & 6 deletions src/main/resources/db/migration/V1_0_0__Initial.sql
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
CREATE TABLE cards
(
id SERIAL PRIMARY KEY,
ability VARCHAR(1024),
cost INTEGER NOT NULL,
name VARCHAR(255),
power INTEGER NOT NULL,
url VARCHAR(255)
id SERIAL PRIMARY KEY,
ability VARCHAR(1024),
cost INTEGER NOT NULL,
name VARCHAR(255),
power INTEGER NOT NULL,
url VARCHAR(255),
is_owned BOOLEAN
);
53 changes: 39 additions & 14 deletions src/main/resources/templates/card-grid.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,82 @@
<title>All cards</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0"
rel="stylesheet"/>
<script>
function imageClicked(value) {
location.href = "cards/" + value;
}
function toggleCollectionStatus(cardId, owned) {
fetch("/v1/cards/" + cardId + "/collection", {
method: owned === 'true' ? "DELETE" : "POST"
})
document.getElementById("card-" + cardId).classList.remove(owned === 'true' ? "bg-success" : "bg-secondary");
document.getElementById("card-" + cardId).classList.add(owned === 'true' ? "bg-secondary" : "bg-success");
}
</script>
</head>
<body>
<h1 class="mx-4 my-4">All cards</h1>
<div class="ms-4 d-flex">
<a class="btn btn-outline-primary" href="cards">
<div class="ms-4" style="display: inline-flex; flex-wrap: wrap;">
<a class="my-1 btn btn-outline-primary" href="cards">
<i class="material-icons" style="font-size: 20px; vertical-align: text-bottom;">table_rows</i>
</a>
<form class="d-flex" th:action="@{/card-grid?keyword=${keyword}}" th:method="GET">
<input name="cost" th:if="${cost != null}" th:value="${cost}" type="hidden"/>
<input name="power" th:if="${power != null}" th:value="${power}" type="hidden"/>
<input class="ms-1 form-control" placeholder="Search" th:name="keyword" th:value="${keyword}" type="text"/>
<button class="ms-1 btn btn-primary" type="submit">
<input name="isOwned" th:if="${isOwned != null}" th:value="${isOwned}" type="hidden"/>
<input class="ms-1 my-1 form-control" placeholder="Search" th:name="keyword" th:value="${keyword}" type="text"/>
<button class="ms-1 my-1 btn btn-primary" type="submit">
<i class="material-icons" style="font-size: 20px; vertical-align: text-bottom;">search</i>
</button>
</form>
<form class="ms-2 d-inline-flex">
<form class="ms-2 d-flex">
<input name="keyword" th:if="${keyword != null}" th:value="${keyword}" type="hidden"/>
<div class="d-flex text-center">
<div class="d-flex">
<label class="me-1 my-auto control-label" for="cost">Cost:</label>
<select class="form-select" id="cost" name="cost">
<select class="my-auto form-select" id="cost" name="cost">
<option value="">any</option>
<option th:each="i : ${costValues}" th:selected="${i} == ${cost}" th:text="${i}"
th:value="${i}"></option>
</select>
</div>
<div class="form-group d-flex">
<div class="d-flex">
<label class="ms-2 me-1 my-auto control-label" for="power">Power:</label>
<select class="ms-1 form-select" id="power" name="power">
<select class="ms-1 my-auto form-select" id="power" name="power">
<option value="">any</option>
<option th:each="i : ${powerValues}" th:selected="${i} == ${power}" th:text="${i}"
th:value="${i}"></option>
</select>
</div>
<button class="ms-2 btn btn-primary" type="submit">Apply</button>
<div class="d-flex">
<label class="ms-2 me-1 my-auto control-label" for="isOwned">Status:</label>
<select class="ms-1 my-auto form-select" id="isOwned" name="isOwned">
<option value="" th:selected="${isOwned eq null}">any</option>
<option value="true" th:selected="${isOwned eq true}">owned</option>
<option value="false" th:selected="${isOwned eq false}">not owned</option>
</select>
</div>
<button class="ms-1 my-1 btn btn-primary" type="submit">Apply</button>
</form>
<a class="ms-1 btn btn-primary" href="new-card">Add card</a>
<form class="d-flex" th:action="@{/cards}" th:method="DELETE">
<a class="ms-1 my-1 d-flex btn btn-primary" href="new-card">Add card</a>
<form class="my-1 d-flex" th:action="@{/cards}" th:method="DELETE">
<button class="ms-1 btn btn-danger" style="vertical-align: text-bottom;" type="submit">Delete all</button>
</form>
</div>
<div class="container-fluid text-center">
<div class="row my-4 g-0 px-4">
<div class="card mx-auto mt-4" style="width: 18rem; cursor: pointer" th:each="card: ${cards}">
<div class="card mx-auto mt-4" style="width: 18rem;" th:each="card: ${cards}">
<img class="card-img-top"
style="cursor: pointer;"
onclick="imageClicked(this.getAttribute('data-id'));"
th:data-id="${card.id}" th:src="@{${card.url}}"/>
th:data-id="${card.id}" th:src="@{${card.url}}" alt="card image"/>
<span class="position-absolute top-0 start-100 translate-middle p-1 badge material-symbols-outlined" onclick="toggleCollectionStatus(this.getAttribute('data-id'), this.getAttribute('data-owned'));"
style="cursor: pointer;"
th:classappend="${card.owned ? 'bg-success' : 'bg-secondary'}"
th:data-id="${card.id}"
th:data-owned="${card.owned}"
th:id="'card-' + ${card.id}">check_circle</span>
<div class="card-body">
<h5 class="card-title" th:text="${card.name}"></h5>
<p class="card-text" th:text="${card.ability eq '' ? 'No ability' : card.ability}"></p>
Expand Down
21 changes: 13 additions & 8 deletions src/main/resources/templates/cards.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<title>All cards</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0"
rel="stylesheet"/>
<script>
function imageClicked(value) {
location.href = "cards/" + value;
Expand Down Expand Up @@ -32,23 +34,26 @@ <h1 class="mx-4 my-4">All cards</h1>
<table class="mx-3 table table-hover align-middle">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">Name</th>
<th scope="col">Cost</th>
<th scope="col">Power</th>
<th scope="col">Ability</th>
<th class="col-md-1 text-center" scope="col">Owned</th>
<th class="col-md-1" scope="col"></th>
<th class="col-md-1" scope="col">Name</th>
<th class="col-md-1 text-center" scope="col">Cost</th>
<th class="col-md-1 text-center" scope="col">Power</th>
<th class="col-md-7" scope="col">Ability</th>
</tr>
</thead>
<tbody>
<tr onclick="imageClicked(this.getAttribute('data-id'));" style="cursor: pointer"
th:data-id="${card.id}"
th:each="card: ${cardPage.content}">
<td class="text-center"><span style="color: green;" class="material-symbols-outlined"
th:text="${card.owned} ? 'check_circle' : ''"></span></td>
<td>
<img class="img-fluid" style="width: 100px" th:src="@{${card.url}}"/>
<img class="img-fluid" style="width: 100px" th:src="@{${card.url}}" alt="card image"/>
</td>
<td th:text="${card.name}"/>
<td th:text="${card.cost}"/>
<td th:text="${card.power}"/>
<td class="text-center" th:text="${card.cost}"/>
<td class="text-center" th:text="${card.power}"/>
<td th:text="${card.ability}"/>
</tr>
</tbody>
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</head>
<body>
<h1 class="mx-4 my-4">Marvel SNAP cards</h1>
<a class="ms-4 btn btn-outline-primary" href="cards">List all</a>
<a class="ms-4 btn btn-outline-primary" href="card-grid">List all</a>
<a class="ms-1 btn btn-outline-primary" href="new-card">Add card</a>
</body>
</html>