API consistence in frontend

This commit is contained in:
Brieuc Dubois 2023-12-30 18:15:17 +01:00
parent 47c8184967
commit 236bea14dc
15 changed files with 183 additions and 149 deletions

View File

@ -74,7 +74,7 @@ func UpdateTag(t types.Tag) (int64, error) {
func ExistTag(id int) (bool, error) {
var count int
err := db.QueryRow("SELECT COUNT(*) FROM tas WHERE id = ?", id).Scan(&count)
err := db.QueryRow("SELECT COUNT(*) FROM tags WHERE id = ?", id).Scan(&count)
if err != nil {
return false, err
}

View File

@ -11,6 +11,8 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.50.0 // indirect
@ -18,4 +20,7 @@ require (
golang.org/x/sys v0.14.0 // indirect
)
require github.com/mattn/go-sqlite3 v1.14.19
require (
github.com/json-iterator/go v1.1.12
github.com/mattn/go-sqlite3 v1.14.19
)

View File

@ -1,9 +1,14 @@
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -15,8 +20,15 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=

View File

@ -52,7 +52,7 @@ func GetProjectTags(c *fiber.Ctx) error {
})
}
return c.JSON(tags)
return c.Status(fiber.StatusOK).JSON(tags)
}
func GetTag(c *fiber.Ctx) error {

View File

@ -30,29 +30,29 @@ func main() {
AllowHeaders: "Origin, Content-Type, Accept",
}))
app.Post("/api/projects", handlers.CreateProject)
app.Get("/api/projects", handlers.GetAllProjects)
app.Get("/api/projects/:id", handlers.GetProject)
app.Put("/api/projects/:id", handlers.UpdateProject)
app.Delete("/api/projects/:id", handlers.DeleteProject)
app.Get("/api/projects/:project_id/cards", handlers.GetProjectCards)
app.Get("/api/projects/:project_id/tags", handlers.GetProjectTags)
app.Post("/api/v1/projects", handlers.CreateProject)
app.Get("/api/v1/projects", handlers.GetAllProjects)
app.Get("/api/v1/projects/:id", handlers.GetProject)
app.Put("/api/v1/projects/:id", handlers.UpdateProject)
app.Delete("/api/v1/projects/:id", handlers.DeleteProject)
app.Get("/api/v1/projects/:project_id/cards", handlers.GetProjectCards)
app.Get("/api/v1/projects/:project_id/tags", handlers.GetProjectTags)
app.Post("/api/cards", handlers.CreateCard)
app.Get("/api/cards/:id", handlers.GetCard)
app.Put("/api/cards/:id", handlers.UpdateCard)
app.Delete("/api/cards/:id", handlers.DeleteCard)
app.Post("/api/v1/cards", handlers.CreateCard)
app.Get("/api/v1/cards/:id", handlers.GetCard)
app.Put("/api/v1/cards/:id", handlers.UpdateCard)
app.Delete("/api/v1/cards/:id", handlers.DeleteCard)
app.Post("/api/tags", handlers.CreateTag)
app.Get("/api/tags/:id", handlers.GetTag)
app.Delete("/api/tags/:id", handlers.DeleteTag)
app.Put("/api/tags/:id", handlers.UpdateTag)
app.Post("/api/v1/tags", handlers.CreateTag)
app.Get("/api/v1/tags/:id", handlers.GetTag)
app.Delete("/api/v1/tags/:id", handlers.DeleteTag)
app.Put("/api/v1/tags/:id", handlers.UpdateTag)
app.Post("/api/cards/:card_id/tags/:tag_id", handlers.CreateCardTag)
app.Get("/api/cards/:card_id/tags", handlers.GetCardTags)
app.Put("/api/cards/:card_id/tags/:tag_id", handlers.UpdateCardTag)
app.Delete("/api/cards/:card_id/tags/:tag_id", handlers.DeleteCardTag)
app.Delete("/api/cards/:card_id/tags", handlers.DeleteCardTags)
app.Post("/api/v1/cards/:card_id/tags/:tag_id", handlers.CreateCardTag)
app.Get("/api/v1/cards/:card_id/tags", handlers.GetCardTags)
app.Put("/api/v1/cards/:card_id/tags/:tag_id", handlers.UpdateCardTag)
app.Delete("/api/v1/cards/:card_id/tags/:tag_id", handlers.DeleteCardTag)
app.Delete("/api/v1/cards/:card_id/tags", handlers.DeleteCardTags)
log.Fatal(app.Listen(fmt.Sprintf(":%v", port)))
}

View File

@ -2,17 +2,9 @@
import axios from 'axios';
import type { Card } from '../stores/interfaces';
import ModalCard from './modal_card.svelte';
import { backend } from '../stores/config';
export let card: Card = {
id: 0,
project_id: 0,
title: 'No title',
content: 'Nocontent',
tags: []
};
export let showModal = false;
export let card: Card;
export let showModal: boolean;
export let onDelete: () => void;
function editCard() {

View File

@ -12,8 +12,12 @@
let tempCard: Card = { ...card };
function save(closeModal: boolean = true) {
if (card != tempCard) {
axios.put(`${backend}/api/card/${card.id}`, {
if (
card.project_id != tempCard.project_id ||
card.title !== tempCard.title ||
card.content !== tempCard.content
) {
axios.put(`${backend}/api/v1/cards/${card.id}`, {
project_id: tempCard.project_id,
title: tempCard.title,
content: tempCard.content
@ -70,7 +74,7 @@
</div>
</div>
<div class="tags">
<ModalTags {tempCard} />
<ModalTags bind:card />
</div>
<div class="body">
<textarea

View File

@ -3,13 +3,7 @@
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: ''
};
export let tag: Tag;
let newValue: string = tag.value;
export let removeTag: (id: number) => void;
@ -18,20 +12,18 @@
if (tag.value === newValue) return;
// DELETE
if (tag.value !== '' && newValue === '') {
axios.delete(`${backend}/api/cardtag/${tag.card_id}/${tag.tag_id}`);
axios.delete(`${backend}/api/v1/cards/${tag.card_id}/tags/${tag.tag_id}`);
return;
}
// CREATE
if (tag.value === '' && newValue !== '') {
axios.post(`${backend}/api/cardtag`, {
card_id: tag.card_id,
tag_id: tag.tag_id,
axios.post(`${backend}/api/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
value: newValue
});
return;
}
// UPDATE
axios.put(`${backend}/api/cardtag/${tag.card_id}/${tag.tag_id}`, {
axios.put(`${backend}/api/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
value: newValue
});

View File

@ -2,33 +2,26 @@
import axios from 'axios';
import ModalTag from './modal_tag.svelte';
import { backend } from '../stores/config';
import status from '../utils/status';
import type { Card } from '../stores/interfaces';
export let tempCard: Card = {
id: 0,
project_id: 0,
title: 'No title',
content: 'Nocontent',
tags: []
};
export let card: Card;
let newTagName = '';
async function addTag() {
if (newTagName === '') return;
const response = await axios.post(`${backend}/api/tag`, {
project_id: tempCard.project_id,
const response = await axios.post(`${backend}/api/v1/tags`, {
project_id: card.project_id,
title: newTagName,
type: 0
});
const { id } = response.data.id;
if (response.status !== status.Created) return console.error(response);
const id = response.data.id;
tempCard.tags = [
...tempCard.tags,
{ card_id: tempCard.id, tag_id: id, tag_title: newTagName, value: '' }
];
card.tags = [...card.tags, { card_id: card.id, tag_id: id, tag_title: newTagName, value: '' }];
newTagName = '';
}
@ -39,14 +32,14 @@
}
function removeTag(id: number) {
tempCard.tags = tempCard.tags.filter((tag) => tag.tag_id !== id);
card.tags = card.tags.filter((tag) => tag.tag_id !== id);
}
</script>
<table>
{#if tempCard.tags}
{#each tempCard.tags as tag}
<ModalTag {tag} {removeTag} />
{#if card.tags}
{#each card.tags as tag}
<ModalTag bind:tag {removeTag} />
{/each}
{/if}
<tr class="tag">

View File

@ -3,22 +3,23 @@
import { onMount } from 'svelte';
import CardC from './card.svelte';
import { backend } from '../stores/config';
import type { Card, Project } from '../stores/interfaces';
import { type Project, type Card, parseCards } from '../stores/interfaces';
import status from '../utils/status';
export let projectId: number = 0;
export let projectId: number;
let project: Project;
let cards: Card[];
onMount(async () => {
let response = await axios.get(`${backend}/api/project/${projectId}`);
let response = await axios.get(`${backend}/api/v1/projects/${projectId}`);
project = response.data;
response = await axios.get(`${backend}/api/cards/${projectId}`);
response = await axios.get(`${backend}/api/v1/projects/${projectId}/cards`);
if (response.data.status === 'ok') {
cards = response.data.data;
if (response.status === status.OK) {
cards = parseCards(response.data);
} else {
console.error(response.data);
}
@ -27,13 +28,13 @@
let modalID = -1;
async function newCard() {
const response = await axios.post(`${backend}/api/card`, {
const response = await axios.post(`${backend}/api/v1/cards`, {
project_id: projectId,
title: 'Untitled',
content: ''
});
if (response.data.status !== 'ok') {
if (response.status !== status.Created) {
console.error(response.data);
return;
}
@ -53,9 +54,9 @@
}
async function deleteCard(cardID: number) {
const response = await axios.delete(`${backend}/api/card/${cardID}`);
const response = await axios.delete(`${backend}/api/v1/cards/${cardID}`);
if (response.status !== 204) {
if (response.status !== status.NoContent) {
console.error(response.data);
return;
}

View File

@ -1,26 +1,26 @@
<script lang="ts">
import { projects } from "../stores/projects";
import { projects } from '../stores/projects';
let newProject = false;
let editProject: number | undefined = undefined;
let newProject = false;
let editProject: number | undefined = undefined;
function handleKeydown(event: KeyboardEvent, id: number | undefined = undefined) {
if (event.key === 'Enter' && event.target) {
if(id !== undefined){
projects.edit({
id: id,
title: (event.target as HTMLInputElement).value
})
editProject = undefined;
} else {
createNewProject((event.target as HTMLInputElement).value);
newProject = false;
}
}
}
function handleKeydown(event: KeyboardEvent, id: number | undefined = undefined) {
if (event.key === 'Enter' && event.target) {
if (id !== undefined) {
projects.edit({
id: id,
title: (event.target as HTMLInputElement).value
});
editProject = undefined;
} else {
createNewProject((event.target as HTMLInputElement).value);
newProject = false;
}
}
}
function createNewProject(value: string) {
projects.add({
projects.add({
title: value,
id: undefined
});
@ -28,55 +28,57 @@
</script>
<svelte:head>
<link rel="stylesheet" type="text/css" href="/css/sidebar.css" />
<link rel="stylesheet" type="text/css" href="/css/sidebar.css" />
</svelte:head>
<div id="sidebar" class="sidebar">
<div class="logo">
<a href="/">
<img src="img/icon.svg" alt="">
<span class="title">Focus</span>
<span class="version">v0.0.1</span>
</a>
</div>
<div class="boards">
{#await projects.init()}
<p>Loading ...</p>
{:then}
<h2>Projects</h2>
<ul>
{#each $projects as project}
<li>
{#if editProject === project.id}
<input
type="text"
on:keydown={(e) => handleKeydown(e, project.id)}
value={project.title}
class="edit-input"
/>
{:else}
<a href="/{project.id}">{project.title}</a><span class="edit-icon" on:click={() => editProject = project.id}>
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="18" viewBox="0 0 576 512"><path d="M402.6 83.2l90.2 90.2c3.8 3.8 3.8 10 0 13.8L274.4 405.6l-92.8 10.3c-12.4 1.4-22.9-9.1-21.5-21.5l10.3-92.8L388.8 83.2c3.8-3.8 10-3.8 13.8 0zm162-22.9l-48.8-48.8c-15.2-15.2-39.9-15.2-55.2 0l-35.4 35.4c-3.8 3.8-3.8 10 0 13.8l90.2 90.2c3.8 3.8 10 3.8 13.8 0l35.4-35.4c15.2-15.3 15.2-40 0-55.2zM384 346.2V448H64V128h229.8c3.2 0 6.2-1.3 8.5-3.5l40-40c7.6-7.6 2.2-20.5-8.5-20.5H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V306.2c0-10.7-12.9-16-20.5-8.5l-40 40c-2.2 2.3-3.5 5.3-3.5 8.5z" fill="#aaa"/></svg>
</span>
{/if}
</li>
{/each}
{#if newProject}
<li>
<input
type="text"
placeholder="Enter project title"
on:keydown={handleKeydown}
style="padding: 8px; border: 1px solid #ccc; border-radius: 4px; width: 100%;" />
</li>
{/if}
</ul>
{:catch error}
<p>Something went wrong: {error.message}</p>
{/await}
</div>
<div class="bottom-links">
<span on:click={() => newProject = true}>New project</span>
<span>Settings</span>
</div>
</div>
<div class="logo">
<a href="/">
<img src="img/icon.svg" alt="" />
<span class="title">Focus</span>
<span class="version">v0.0.1</span>
</a>
</div>
<div class="boards">
{#await projects.init()}
<p>Loading ...</p>
{:then}
<h2>Projects</h2>
{#if $projects}
<ul>
{#each $projects as project}
<li>
{#if editProject === project.id}
<input
type="text"
on:keydown={(e) => handleKeydown(e, project.id)}
value={project.title}
class="edit-input"
/>
{:else}
<a href="/{project.id}">{project.title}</a>
<img src="img/edit-icon.svg" alt="" on:click={() => (editProject = project.id)} />
{/if}
</li>
{/each}
{#if newProject}
<li>
<input
type="text"
placeholder="Enter project title"
on:keydown={handleKeydown}
style="padding: 8px; border: 1px solid #ccc; border-radius: 4px; width: 100%;"
/>
</li>
{/if}
</ul>
{/if}
{:catch error}
<p>Something went wrong: {error.message}</p>
{/await}
</div>
<div class="bottom-links">
<span on:click={() => (newProject = true)}>New project</span>
<span>Settings</span>
</div>
</div>

View File

@ -16,4 +16,15 @@ export interface Tag {
tag_id: number;
tag_title: string;
value: string;
}
export function parseCard (c: any) {
let card: Card = c;
if (card.tags == null) card.tags = [];
return card;
};
export function parseCards (cards: any) {
if (cards == null) return [];
return cards.map(parseCard);
}

View File

@ -1,10 +1,9 @@
import axios from "axios";
import { writable } from "svelte/store";
import { backend } from "./config";
export const projects = getProjects();
const backend = 'http://127.0.0.1:3000'
interface Project {
id: number | undefined,
title: string,
@ -16,7 +15,7 @@ function getProjects() {
return {
subscribe,
init: async () => {
const response = await axios.get(`${backend}/api/projects`);
const response = await axios.get(`${backend}/api/v1/projects`);
if(response.status < 303) {
const data: Project[] = response.data;
@ -27,7 +26,7 @@ function getProjects() {
}
},
add: async (project: Project) => {
const response = await axios.post(`${backend}/api/project`, project);
const response = await axios.post(`${backend}/api/v1/projects`, project);
if(response.status < 303) {
project.id = response.data["id"];
@ -38,9 +37,8 @@ function getProjects() {
}
},
edit: async (project: Project) => {
const response = await axios.put(`${backend}/api/project/${project.id}`, project)
const response = await axios.put(`${backend}/api/v1/projects/${project.id}`, project)
if(response.status < 303) {
update((oldProjects: Project[]) => {
for(let p of oldProjects){

View File

@ -0,0 +1,19 @@
export let statusOK = 200;
export let statusCreated = 201;
export let statusNoContent = 204;
export let statusConflict = 409;
export let statusBadRequest = 400;
export let statusUnauthorized = 401;
export let statusNotFound = 404;
export let statusInternalServerError = 500;
export default {
OK: statusOK,
Created: statusCreated,
NoContent: statusNoContent,
Conflict: statusConflict,
BadRequest: statusBadRequest,
Unauthorized: statusUnauthorized,
NotFound: statusNotFound,
InternalServerError: statusInternalServerError,
};

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="18" viewBox="0 0 576 512">
<path
d="M402.6 83.2l90.2 90.2c3.8 3.8 3.8 10 0 13.8L274.4 405.6l-92.8 10.3c-12.4 1.4-22.9-9.1-21.5-21.5l10.3-92.8L388.8 83.2c3.8-3.8 10-3.8 13.8 0zm162-22.9l-48.8-48.8c-15.2-15.2-39.9-15.2-55.2 0l-35.4 35.4c-3.8 3.8-3.8 10 0 13.8l90.2 90.2c3.8 3.8 10 3.8 13.8 0l35.4-35.4c15.2-15.3 15.2-40 0-55.2zM384 346.2V448H64V128h229.8c3.2 0 6.2-1.3 8.5-3.5l40-40c7.6-7.6 2.2-20.5-8.5-20.5H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V306.2c0-10.7-12.9-16-20.5-8.5l-40 40c-2.2 2.3-3.5 5.3-3.5 8.5z"
fill="#aaa" />
</svg>

After

Width:  |  Height:  |  Size: 645 B