Modal card

This commit is contained in:
Brieuc Dubois 2023-12-29 17:02:46 +01:00
parent e38385cab5
commit 82ba28adeb
13 changed files with 396 additions and 59 deletions

View File

@ -30,7 +30,7 @@ func GetAllCardsOf(projectID int) ([]types.FullCard, error) {
return nil, err return nil, err
} }
tags, err := GetAllTagsOfCard(c.ID) tags, err := GetAllTagsOfCard(c.ID, projectID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -54,7 +54,7 @@ func GetCard(id int) (*types.FullCard, error) {
return nil, err return nil, err
} }
tags, err := GetAllTagsOfCard(id) tags, err := GetAllTagsOfCard(id, c.ProjectID)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -9,12 +9,20 @@ func AddTagToCard(ct types.CardTag) error {
return err return err
} }
func GetAllTagsOfCard(cardID int) ([]types.FullCardTag, error) { func GetAllTagsOfCard(cardID int, projectID int) ([]types.FullCardTag, error) {
rows, err := db.Query(`SELECT ct.card_id, ct.tag_id, t.title, ct.value if projectID < 0 {
FROM cardtags ct card, err := GetCard(cardID)
JOIN tags t ON ct.tag_id = t.id if err != nil {
WHERE ct.card_id = ?; return nil, err
`, cardID) }
projectID = card.ProjectID
}
rows, err := db.Query(`SELECT t.id, t.title, COALESCE(ct.value, '')
FROM tags t
LEFT JOIN cardtags ct ON ct.tag_id = t.id AND ct.card_id = ?
WHERE t.project_id = ?
`, cardID, projectID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -22,8 +30,8 @@ func GetAllTagsOfCard(cardID int) ([]types.FullCardTag, error) {
var cardtags []types.FullCardTag var cardtags []types.FullCardTag
for rows.Next() { for rows.Next() {
var ct types.FullCardTag ct := types.FullCardTag{CardID: cardID}
if err := rows.Scan(&ct.CardID, &ct.TagID, &ct.TagTitle, &ct.Value); err != nil { if err := rows.Scan(&ct.TagID, &ct.TagTitle, &ct.Value); err != nil {
return nil, err return nil, err
} }
cardtags = append(cardtags, ct) cardtags = append(cardtags, ct)

View File

@ -26,12 +26,12 @@ func CreateCard(c *fiber.Ctx) error {
func GetAllCardsOf(c *fiber.Ctx) error { func GetAllCardsOf(c *fiber.Ctx) error {
projectID, err := strconv.Atoi(c.Params("project_id")) projectID, err := strconv.Atoi(c.Params("project_id"))
if err != nil { if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "error", "error": "Invalid project_id"}) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "error", "error": "Invalid project_id", "trace": fmt.Sprint(err)})
} }
projects, err := db.GetAllCardsOf(projectID) projects, err := db.GetAllCardsOf(projectID)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Cannot retrieve cards"}) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"status": "error", "error": "Cannot retrieve cards", "trace": fmt.Sprint(err)})
} }
return c.JSON(fiber.Map{"status": "ok", "data": projects}) return c.JSON(fiber.Map{"status": "ok", "data": projects})

View File

@ -29,7 +29,7 @@ func GetAllTagsOfCard(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "error", "error": "Invalid card_id"}) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "error", "error": "Invalid card_id"})
} }
cardtags, err := db.GetAllTagsOfCard(cardID) cardtags, err := db.GetAllTagsOfCard(cardID, -1)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"status": "error", "error": "Cannot retrieve tags of card", "stack": fmt.Sprint(err)}) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"status": "error", "error": "Cannot retrieve tags of card", "stack": fmt.Sprint(err)})
} }
@ -78,7 +78,7 @@ func UpdateTagOfCard(c *fiber.Ctx) error {
tag_id, err := strconv.Atoi(c.Params("tag_id")) tag_id, err := strconv.Atoi(c.Params("tag_id"))
if err != nil { if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid tag ID"}) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "error", "error": "Invalid tag ID", "stack": fmt.Sprint(err)})
} }
cardtag := types.CardTag{CardID: card_id, TagID: tag_id} cardtag := types.CardTag{CardID: card_id, TagID: tag_id}

View File

@ -58,7 +58,7 @@ func main() {
app.Get("/api/cardtags/:card_id", handlers.GetAllTagsOfCard) app.Get("/api/cardtags/:card_id", handlers.GetAllTagsOfCard)
app.Delete("/api/cardtag/:card_id/:tag_id", handlers.DeleteTagOfCard) app.Delete("/api/cardtag/:card_id/:tag_id", handlers.DeleteTagOfCard)
app.Delete("/api/cardtags/:card_id", handlers.DeleteTagsOfCard) app.Delete("/api/cardtags/:card_id", handlers.DeleteTagsOfCard)
app.Put("/api/cardtag/:card_id/:tag_id", handlers.UpdateTag) app.Put("/api/cardtag/:card_id/:tag_id", handlers.UpdateTagOfCard)
log.Fatal(app.Listen(fmt.Sprintf(":%v", port))) log.Fatal(app.Listen(fmt.Sprintf(":%v", port)))
} }

View File

@ -1,41 +1,52 @@
<script lang="ts"> <script lang="ts">
import axios from 'axios'; import axios from 'axios';
import { onMount } from 'svelte'; import type { Card } from '../stores/interfaces';
import ModalCard from './modal_card.svelte';
const backend = 'http://127.0.0.1:3000'; import { backend } from '../stores/config';
interface Card {
id: number;
title: string;
content: string;
tags: Tag[];
}
interface Tag {
tag_id: number;
tag_title: string;
value: string;
}
export let card: Card = { export let card: Card = {
id: 0, id: 0,
project_id: 0,
title: 'No title', title: 'No title',
content: 'Nocontent', content: 'Nocontent',
tags: [] tags: []
}; };
let showModal = false;
function editCard() {
showModal = true;
}
function cancelEdit() {
showModal = false;
}
function editCardHandler(event: KeyboardEvent) {
if (event.key === 'Enter') {
editCard();
}
}
</script> </script>
<svelte:head> <div
<link rel="stylesheet" type="text/css" href="/css/card.css" /> class="card"
</svelte:head> tabindex="0"
draggable={true}
<div class="card" draggable={true}> on:click={editCard}
role="button"
on:keydown={editCardHandler}
>
<div class="title">{card.title}</div> <div class="title">{card.title}</div>
{#if card.tags} {#if card.tags}
<div class="tags"> <div class="tags">
{#each card.tags as tag} {#each card.tags as tag}
<span class="tag" style="border: 1px solid #333">{tag.value}</span> {#if tag.value}
<span class="tag" style="border: 1px solid #333">{tag.value}</span>
{/if}
{/each} {/each}
</div> </div>
{/if} {/if}
</div> </div>
<ModalCard bind:show={showModal} {card} onCancel={cancelEdit} />

View File

@ -0,0 +1,75 @@
<script lang="ts">
import axios from 'axios';
import ModalTags from './modal_tags.svelte';
import type { Card } from '../stores/interfaces';
import { backend } from '../stores/config';
export let show: boolean;
export let card: Card;
export let onCancel: () => void;
let tempCard: Card = { ...card };
function save(closeModal: boolean = true) {
if (card != tempCard) {
axios.put(`${backend}/api/card/${card.id}`, {
project_id: tempCard.project_id,
title: tempCard.title,
content: tempCard.content
});
card = tempCard;
}
if (closeModal) show = false;
}
</script>
{#if show}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal" on:click={() => save(true)}>
<div class="content" on:click|stopPropagation>
<div class="header">
<input class="title" bind:value={tempCard.title} on:blur={() => save(false)} />
<div class="buttons">
<button on:click={() => save(true)}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="white"
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
</button>
<button on:click={onCancel}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="white"
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 12 12z"
/>
</svg>
</button>
</div>
</div>
<div class="tags">
<ModalTags {tempCard} />
</div>
<div class="body">
<textarea
bind:value={tempCard.content}
placeholder="Add a description"
on:blur={() => save(false)}
/>
</div>
</div>
</div>
{/if}

View File

@ -0,0 +1,65 @@
<script lang="ts">
import axios from 'axios';
import { backend } from '../stores/config';
import type { Tag } from '../stores/interfaces';
export let tag: Tag = {
card_id: 0,
tag_id: 0,
tag_title: 'No title',
value: ''
};
let newValue: string = tag.value;
export let removeTag: (id: number) => void;
function saveTag() {
if (tag.value === newValue) return;
// DELETE
if (tag.value !== '' && newValue === '') {
axios.delete(`${backend}/api/cardtag/${tag.card_id}/${tag.tag_id}`);
return;
}
// CREATE
if (tag.value === '' && newValue !== '') {
axios.post(`${backend}/api/cardtag`, {
card_id: tag.card_id,
tag_id: tag.tag_id,
value: newValue
});
return;
}
// UPDATE
axios.put(`${backend}/api/cardtag/${tag.card_id}/${tag.tag_id}`, {
value: newValue
});
tag.value = newValue;
}
</script>
<tr class="tag">
<td class="tag-title">{tag.tag_title}</td>
<td>
<input bind:value={newValue} on:blur={saveTag} />
</td>
<td>
<button on:click={() => removeTag(tag.tag_id)} class="remove-tag-button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</td>
</tr>

View File

@ -0,0 +1,74 @@
<script lang="ts">
import axios from 'axios';
import ModalTag from './modal_tag.svelte';
import { backend } from '../stores/config';
import type { Card } from '../stores/interfaces';
export let tempCard: Card = {
id: 0,
project_id: 0,
title: 'No title',
content: 'Nocontent',
tags: []
};
let newTagName = '';
async function addTag() {
if (newTagName === '') return;
const response = await axios.post(`${backend}/api/tag`, {
project_id: tempCard.project_id,
title: newTagName,
type: 0
});
const { id } = response.data.id;
tempCard.tags = [
...tempCard.tags,
{ card_id: tempCard.id, tag_id: id, tag_title: newTagName, value: '' }
];
newTagName = '';
}
function addTagHandler(event: KeyboardEvent) {
if (event.key === 'Enter') {
addTag();
}
}
function removeTag(id: number) {
tempCard.tags = tempCard.tags.filter((tag) => tag.tag_id !== id);
}
</script>
<table>
{#if tempCard.tags}
{#each tempCard.tags as tag}
<ModalTag {tag} {removeTag} />
{/each}
{/if}
<tr class="tag">
<td class="tag-title"
><input placeholder="Add a new tag" on:keydown={addTagHandler} bind:value={newTagName} /></td
>
<td
><button on:click={addTag}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</td>
</tr>
</table>

View File

@ -1,30 +1,12 @@
<script lang="ts"> <script lang="ts">
import axios from 'axios'; import axios from 'axios';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Card from './card.svelte'; import CardC from './card.svelte';
import { backend } from '../stores/config';
import type { Card, Project } from '../stores/interfaces';
export let projectId: number = 0; export let projectId: number = 0;
const backend = 'http://127.0.0.1:3000';
interface Project {
id: number | undefined;
title: string;
}
interface Tag {
tag_id: number;
tag_title: string;
value: string;
}
interface Card {
id: number;
title: string;
content: string;
tags: Tag[];
}
let project: Project; let project: Project;
let cards: Card[]; let cards: Card[];
@ -45,6 +27,8 @@
<svelte:head> <svelte:head>
<link rel="stylesheet" type="text/css" href="/css/project.css" /> <link rel="stylesheet" type="text/css" href="/css/project.css" />
<link rel="stylesheet" type="text/css" href="/css/card.css" />
<link rel="stylesheet" type="text/css" href="/css/modalCard.css" />
</svelte:head> </svelte:head>
{#if project} {#if project}
@ -54,7 +38,7 @@
<ul> <ul>
{#if cards} {#if cards}
{#each cards as card} {#each cards as card}
<Card {card} /> <CardC {card} />
{/each} {/each}
{/if} {/if}
</ul> </ul>

View File

@ -0,0 +1 @@
export const backend = 'http://127.0.0.1:3000';

View File

@ -0,0 +1,19 @@
export interface Project {
id: number | undefined;
title: string;
}
export interface Card {
id: number;
project_id: number;
title: string;
content: string;
tags: Tag[];
}
export interface Tag {
card_id: number;
tag_id: number;
tag_title: string;
value: string;
}

View File

@ -0,0 +1,100 @@
.modal {
background-color: rgba(0, 0, 0, 0.8);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid green;
display: flex;
align-items: center;
}
.modal .content {
background: #1e1e1e;
padding: 20px;
border-radius: 8px;
max-width: 1000px;
width: 100%;
display: flex;
justify-content: center;
flex-direction: column;
gap: 30px;
}
.modal input,
.modal textarea {
background: none;
color: inherit;
border: 1px solid #333;
border-radius: 7px;
padding: 4px;
}
.modal .title {
font-size: 24px;
font-weight: bold;
width: 100%;
}
.modal .header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.modal .buttons {
display: flex;
flex-direction: row;
align-items: center;
}
.modal button {
margin-left: 5px;
height: 50px;
width: 50px;
background: none;
border: none;
border-radius: 10px;
}
.modal button:hover {
background-color: #333;
cursor: pointer;
}
.modal .buttons button:first-child:hover {
background-color: #343;
}
.modal .buttons button:last-child:hover {
background-color: #433;
}
.modal .body {
margin-bottom: 20px;
}
.modal textarea {
width: 100%;
min-height: 200px;
resize: vertical;
font-family: inherit;
}
.modal td {
margin-right: 40px;
}
.modal td:first-child {
font-weight: bold;
}
.modal td:last-child {
margin-right: 0;
}
.modal td button {
width: 30px;
height: 30px;
}