API fixes & projects page
This commit is contained in:
parent
0809efbf81
commit
c69dadc4bf
|
@ -112,5 +112,5 @@ func UpdateCard(c *fiber.Ctx) error {
|
||||||
return c.SendStatus(fiber.StatusNotFound)
|
return c.SendStatus(fiber.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"@types/eslint": "8.56.0",
|
"@types/eslint": "8.56.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"@zerodevx/svelte-toast": "^0.9.5",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.35.1",
|
"eslint-plugin-svelte": "^2.35.1",
|
||||||
|
@ -1144,6 +1145,15 @@
|
||||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
|
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@zerodevx/svelte-toast": {
|
||||||
|
"version": "0.9.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zerodevx/svelte-toast/-/svelte-toast-0.9.5.tgz",
|
||||||
|
"integrity": "sha512-JLeB/oRdJfT+dz9A5bgd3Z7TuQnBQbeUtXrGIrNWMGqWbabpepBF2KxtWVhL2qtxpRqhae2f6NAOzH7xs4jUSw==",
|
||||||
|
"dev": true,
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^3.57.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.11.2",
|
"version": "8.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
"@types/eslint": "8.56.0",
|
"@types/eslint": "8.56.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"@zerodevx/svelte-toast": "^0.9.5",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.35.1",
|
"eslint-plugin-svelte": "^2.35.1",
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from 'axios';
|
|
||||||
import type { Card } from '../stores/interfaces';
|
import type { Card } from '../stores/interfaces';
|
||||||
import ModalCard from './modal_card.svelte';
|
import ModalCard from './modal_card.svelte';
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from 'axios';
|
|
||||||
import ModalTags from './modal_tags.svelte';
|
import ModalTags from './modal_tags.svelte';
|
||||||
import type { Card } from '../stores/interfaces';
|
import type { Card } from '../stores/interfaces';
|
||||||
import { backend } from '../stores/config';
|
import api, { processError } from '../utils/api';
|
||||||
|
import status from '../utils/status';
|
||||||
|
|
||||||
export let show: boolean;
|
export let show: boolean;
|
||||||
export let card: Card;
|
export let card: Card;
|
||||||
|
@ -11,18 +11,23 @@
|
||||||
|
|
||||||
let tempCard: Card = { ...card };
|
let tempCard: Card = { ...card };
|
||||||
|
|
||||||
function save(closeModal: boolean = true) {
|
async function save(closeModal: boolean = true) {
|
||||||
if (
|
if (
|
||||||
card.project_id != tempCard.project_id ||
|
card.project_id != tempCard.project_id ||
|
||||||
card.title !== tempCard.title ||
|
card.title !== tempCard.title ||
|
||||||
card.content !== tempCard.content
|
card.content !== tempCard.content
|
||||||
) {
|
) {
|
||||||
axios.put(`${backend}/api/v1/cards/${card.id}`, {
|
const response = await api.put(`/v1/cards/${card.id}`, {
|
||||||
project_id: tempCard.project_id,
|
project_id: tempCard.project_id,
|
||||||
title: tempCard.title,
|
title: tempCard.title,
|
||||||
content: tempCard.content
|
content: tempCard.content
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.status !== status.NoContent) {
|
||||||
|
processError(response, 'Failed to update card');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
card = { ...tempCard };
|
card = { ...tempCard };
|
||||||
}
|
}
|
||||||
if (closeModal) show = false;
|
if (closeModal) show = false;
|
||||||
|
|
|
@ -1,31 +1,44 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from 'axios';
|
|
||||||
import { backend } from '../stores/config';
|
|
||||||
import type { Tag } from '../stores/interfaces';
|
import type { Tag } from '../stores/interfaces';
|
||||||
|
import api, { processError } from '../utils/api';
|
||||||
|
import status from '../utils/status';
|
||||||
|
|
||||||
export let tag: Tag;
|
export let tag: Tag;
|
||||||
let newValue: string = tag.value;
|
let newValue: string = tag.value;
|
||||||
|
|
||||||
export let removeTag: (id: number) => void;
|
export let removeTag: (id: number) => void;
|
||||||
|
|
||||||
function saveTag() {
|
async function saveTag() {
|
||||||
if (tag.value === newValue) return;
|
if (tag.value === newValue) return;
|
||||||
// DELETE
|
// DELETE
|
||||||
if (tag.value !== '' && newValue === '') {
|
if (tag.value !== '' && newValue === '') {
|
||||||
axios.delete(`${backend}/api/v1/cards/${tag.card_id}/tags/${tag.tag_id}`);
|
const response = await api.delete(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`);
|
||||||
return;
|
|
||||||
|
if (response.status !== status.NoContent) {
|
||||||
|
processError(response, 'Failed to delete tag');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// CREATE
|
// CREATE
|
||||||
if (tag.value === '' && newValue !== '') {
|
else if (tag.value === '' && newValue !== '') {
|
||||||
axios.post(`${backend}/api/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
|
const response = await api.post(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
|
||||||
value: newValue
|
value: newValue
|
||||||
});
|
});
|
||||||
return;
|
if (response.status !== status.Created) {
|
||||||
|
processError(response, 'Failed to create tag');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// UPDATE
|
// UPDATE
|
||||||
axios.put(`${backend}/api/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
|
else {
|
||||||
value: newValue
|
const response = await api.put(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
|
||||||
});
|
value: newValue
|
||||||
|
});
|
||||||
|
if (response.status !== status.NoContent) {
|
||||||
|
processError(response, 'Failed to update tag');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tag.value = newValue;
|
tag.value = newValue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from 'axios';
|
|
||||||
import ModalTag from './modal_tag.svelte';
|
import ModalTag from './modal_tag.svelte';
|
||||||
import { backend } from '../stores/config';
|
|
||||||
import status from '../utils/status';
|
import status from '../utils/status';
|
||||||
import type { Card } from '../stores/interfaces';
|
import type { Card } from '../stores/interfaces';
|
||||||
|
import api, { processError } from '../utils/api';
|
||||||
|
|
||||||
export let card: Card;
|
export let card: Card;
|
||||||
|
|
||||||
|
@ -12,13 +11,16 @@
|
||||||
async function addTag() {
|
async function addTag() {
|
||||||
if (newTagName === '') return;
|
if (newTagName === '') return;
|
||||||
|
|
||||||
const response = await axios.post(`${backend}/api/v1/tags`, {
|
const response = await api.post(`/v1/tags`, {
|
||||||
project_id: card.project_id,
|
project_id: card.project_id,
|
||||||
title: newTagName,
|
title: newTagName,
|
||||||
type: 0
|
type: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status !== status.Created) return console.error(response);
|
if (response.status !== status.Created) {
|
||||||
|
processError(response, 'Failed to create tag');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const id = response.data.id;
|
const id = response.data.id;
|
||||||
|
|
||||||
card.tags = [...card.tags, { card_id: card.id, tag_id: id, tag_title: newTagName, value: '' }];
|
card.tags = [...card.tags, { card_id: card.id, tag_id: id, tag_title: newTagName, value: '' }];
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from 'axios';
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import CardC from './card.svelte';
|
import CardC from './card.svelte';
|
||||||
import { backend } from '../stores/config';
|
|
||||||
import { type Project, type Card, parseCards } from '../stores/interfaces';
|
import { type Project, type Card, parseCards } from '../stores/interfaces';
|
||||||
import status from '../utils/status';
|
import status from '../utils/status';
|
||||||
|
import api, { processError } from '../utils/api';
|
||||||
|
|
||||||
export let projectId: number;
|
export let projectId: number;
|
||||||
|
|
||||||
|
@ -12,30 +11,38 @@
|
||||||
let cards: Card[];
|
let cards: Card[];
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
let response = await axios.get(`${backend}/api/v1/projects/${projectId}`);
|
let response = await api.get(`/v1/projects/${projectId}`);
|
||||||
|
|
||||||
|
if (response.status !== status.OK) {
|
||||||
|
processError(response, 'Failed to fetch project');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
project = response.data;
|
project = response.data;
|
||||||
|
|
||||||
response = await axios.get(`${backend}/api/v1/projects/${projectId}/cards`);
|
response = await api.get(`/v1/projects/${projectId}/cards`, {
|
||||||
|
validateStatus: () => true
|
||||||
|
});
|
||||||
|
|
||||||
if (response.status === status.OK) {
|
if (response.status === status.OK) {
|
||||||
cards = parseCards(response.data);
|
cards = parseCards(response.data);
|
||||||
} else {
|
} else {
|
||||||
console.error(response.data);
|
cards = [];
|
||||||
|
processError(response, 'Failed to fetch cards');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let modalID = -1;
|
let modalID = -1;
|
||||||
|
|
||||||
async function newCard() {
|
async function newCard() {
|
||||||
const response = await axios.post(`${backend}/api/v1/cards`, {
|
const response = await api.post(`/v1/cards`, {
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
title: 'Untitled',
|
title: 'Untitled',
|
||||||
content: ''
|
content: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status !== status.Created) {
|
if (response.status !== status.Created) {
|
||||||
console.error(response.data);
|
processError(response, 'Failed to create card');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,10 +61,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCard(cardID: number) {
|
async function deleteCard(cardID: number) {
|
||||||
const response = await axios.delete(`${backend}/api/v1/cards/${cardID}`);
|
const response = await api.delete(`/v2/cards/${cardID}`);
|
||||||
|
|
||||||
if (response.status !== status.NoContent) {
|
if (response.status !== status.NoContent) {
|
||||||
console.error(response.data);
|
processError(response, 'Failed to delete card');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Project } from '../stores/interfaces';
|
||||||
|
import api, { processError } from '../utils/api';
|
||||||
|
import status from '../utils/status';
|
||||||
|
|
||||||
|
export let project: Project;
|
||||||
|
export let deleteProject: (project: Project) => void;
|
||||||
|
|
||||||
|
let edit = false;
|
||||||
|
let newTitle = project.title;
|
||||||
|
|
||||||
|
function focus(el: HTMLElement) {
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateProject() {
|
||||||
|
if (newTitle === project.title) {
|
||||||
|
edit = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.put(`/v1/projects/${project.id}`, { title: newTitle });
|
||||||
|
|
||||||
|
if (response.status !== status.NoContent) {
|
||||||
|
processError(response, 'Failed to update project');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(newTitle);
|
||||||
|
|
||||||
|
project.title = newTitle;
|
||||||
|
|
||||||
|
console.log(project.title);
|
||||||
|
|
||||||
|
edit = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
|
||||||
|
<li>
|
||||||
|
{#if edit}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={newTitle}
|
||||||
|
on:blur={updateProject}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
updateProject();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
use:focus
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="title"
|
||||||
|
on:click={() => (location.href = `/${project.id}`)}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
location.href = `/${project.id}`;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
{project.title}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<div class="buttons" on:keydown|stopPropagation>
|
||||||
|
{#if !edit}
|
||||||
|
<img
|
||||||
|
src="/img/edit-icon.svg"
|
||||||
|
alt="Edit"
|
||||||
|
class="button"
|
||||||
|
on:click={() => (edit = !edit)}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
on:keydown|stopPropagation={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
edit = !edit;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<img
|
||||||
|
src="/img/delete-icon.svg"
|
||||||
|
alt="Delete"
|
||||||
|
class="button"
|
||||||
|
on:click={() => deleteProject(project)}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
deleteProject(project);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
|
@ -1,13 +1,56 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { projects } from '../stores/projects';
|
import { onMount } from 'svelte';
|
||||||
|
import api, { processError } from '../utils/api';
|
||||||
|
import type { Project } from '../stores/interfaces';
|
||||||
|
|
||||||
let newProject = false;
|
let newProject = false;
|
||||||
let editProject: number | undefined = undefined;
|
let editProject: number | undefined = undefined;
|
||||||
|
let projects: Project[];
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const response = await api.get(`/v1/projects`);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
processError(response, 'Failed to fetch projects');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projects = response.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createProject(project: Project) {
|
||||||
|
const response = await api.post(`/v1/projects`, project);
|
||||||
|
|
||||||
|
if (response.status !== 201) {
|
||||||
|
processError(response, 'Failed to create project');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
project.id = response.data.id;
|
||||||
|
|
||||||
|
projects = [...projects, project];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateProject(project: Project) {
|
||||||
|
const response = await api.put(`/v1/projects/${project.id}`, project);
|
||||||
|
|
||||||
|
if (response.status !== 204) {
|
||||||
|
processError(response, 'Failed to update project');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projects = projects.map((p) => {
|
||||||
|
if (p.id === project.id) {
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function handleKeydown(event: KeyboardEvent, id: number | undefined = undefined) {
|
function handleKeydown(event: KeyboardEvent, id: number | undefined = undefined) {
|
||||||
if (event.key === 'Enter' && event.target) {
|
if (event.key === 'Enter' && event.target) {
|
||||||
if (id !== undefined) {
|
if (id !== undefined) {
|
||||||
projects.edit({
|
updateProject({
|
||||||
id: id,
|
id: id,
|
||||||
title: (event.target as HTMLInputElement).value
|
title: (event.target as HTMLInputElement).value
|
||||||
});
|
});
|
||||||
|
@ -20,7 +63,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNewProject(value: string) {
|
function createNewProject(value: string) {
|
||||||
projects.add({
|
createProject({
|
||||||
title: value,
|
title: value,
|
||||||
id: undefined
|
id: undefined
|
||||||
});
|
});
|
||||||
|
@ -40,42 +83,36 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="boards">
|
<div class="boards">
|
||||||
{#await projects.init()}
|
<h2>Projects</h2>
|
||||||
<p>Loading ...</p>
|
{#if projects}
|
||||||
{:then}
|
<ul>
|
||||||
<h2>Projects</h2>
|
{#each projects as project}
|
||||||
{#if $projects}
|
<li>
|
||||||
<ul>
|
{#if editProject === project.id && editProject !== undefined}
|
||||||
{#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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter project title"
|
on:keydown={(e) => handleKeydown(e, project.id)}
|
||||||
on:keydown={handleKeydown}
|
value={project.title}
|
||||||
style="padding: 8px; border: 1px solid #ccc; border-radius: 4px; width: 100%;"
|
class="edit-input"
|
||||||
/>
|
/>
|
||||||
</li>
|
{:else}
|
||||||
{/if}
|
<a href="/{project.id}">{project.title}</a>
|
||||||
</ul>
|
<img src="img/edit-icon.svg" alt="" on:click={() => (editProject = project.id)} />
|
||||||
{/if}
|
{/if}
|
||||||
{:catch error}
|
</li>
|
||||||
<p>Something went wrong: {error.message}</p>
|
{/each}
|
||||||
{/await}
|
{#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}
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom-links">
|
<div class="bottom-links">
|
||||||
<span on:click={() => (newProject = true)}>New project</span>
|
<span on:click={() => (newProject = true)}>New project</span>
|
||||||
|
|
|
@ -1,6 +1,77 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import Sidebar from "../components/sidebar.svelte";
|
import { SvelteToast } from '@zerodevx/svelte-toast';
|
||||||
|
import type { Project } from '../stores/interfaces';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import api, { processError } from '../utils/api';
|
||||||
|
import SelectProject from '../components/selectProject.svelte';
|
||||||
|
|
||||||
|
let projects: Project[];
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const response = await api.get(`/v1/projects`);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
processError(response, 'Failed to fetch projects');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projects = response.data || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
async function deleteProject(project: Project) {
|
||||||
|
if (!confirm(`Are you sure you want to delete ${project.title}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await api.delete(`/v1/projects/${project.id}`);
|
||||||
|
|
||||||
|
if (response.status !== 204) {
|
||||||
|
processError(response, 'Failed to delete project');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projects = projects.filter((p) => p.id !== project.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createProject() {
|
||||||
|
const response = await api.post(`/v1/projects`, {
|
||||||
|
title: 'New Project'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 201) {
|
||||||
|
processError(response, 'Failed to create project');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projects = [...projects, response.data];
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Sidebar />
|
<svelte:head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/projects.css" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section id="projects">
|
||||||
|
<h2>Projects</h2>
|
||||||
|
<ul>
|
||||||
|
{#if projects}
|
||||||
|
{#each projects as project}
|
||||||
|
<SelectProject {project} {deleteProject} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
<div
|
||||||
|
id="add"
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
on:click={createProject}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
createProject();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src="/img/add-icon.svg" alt="Add" width="30" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<SvelteToast />
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import Project from '../../components/project.svelte';
|
import Project from '../../components/project.svelte';
|
||||||
import Sidebar from '../../components/sidebar.svelte';
|
import Sidebar from '../../components/sidebar.svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import { SvelteToast } from '@zerodevx/svelte-toast';
|
||||||
|
|
||||||
let projectId: number = +$page.params.project;
|
let projectId: number = +$page.params.project;
|
||||||
</script>
|
</script>
|
||||||
|
@ -10,6 +11,7 @@
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<Project {projectId} />
|
<Project {projectId} />
|
||||||
</div>
|
</div>
|
||||||
|
<SvelteToast />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#projectPage {
|
#projectPage {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { toastAlert } from "../utils/toasts";
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
id: number | undefined;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,5 +28,16 @@ export function parseCard (c: any) {
|
||||||
|
|
||||||
export function parseCards (cards: any) {
|
export function parseCards (cards: any) {
|
||||||
if (cards == null) return [];
|
if (cards == null) return [];
|
||||||
return cards.map(parseCard);
|
|
||||||
|
let cardsArray;
|
||||||
|
try {
|
||||||
|
cardsArray = JSON.parse(cards);
|
||||||
|
} catch (e) {
|
||||||
|
toastAlert('Error', 'Could not parse cards');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cardsArray == null) return [];
|
||||||
|
|
||||||
|
return cardsArray.map(parseCard);
|
||||||
}
|
}
|
|
@ -1,55 +0,0 @@
|
||||||
import axios from "axios";
|
|
||||||
import { writable } from "svelte/store";
|
|
||||||
import { backend } from "./config";
|
|
||||||
|
|
||||||
export const projects = getProjects();
|
|
||||||
|
|
||||||
interface Project {
|
|
||||||
id: number | undefined,
|
|
||||||
title: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProjects() {
|
|
||||||
const { subscribe, set, update } = writable([] as Project[]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
init: async () => {
|
|
||||||
const response = await axios.get(`${backend}/api/v1/projects`);
|
|
||||||
|
|
||||||
if(response.status < 303) {
|
|
||||||
const data: Project[] = response.data;
|
|
||||||
|
|
||||||
set(data);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
add: async (project: Project) => {
|
|
||||||
const response = await axios.post(`${backend}/api/v1/projects`, project);
|
|
||||||
|
|
||||||
if(response.status < 303) {
|
|
||||||
project.id = response.data["id"];
|
|
||||||
update((oldProjects) => {
|
|
||||||
oldProjects.push(project)
|
|
||||||
return oldProjects;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
edit: async (project: 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){
|
|
||||||
if(p.id === project.id){
|
|
||||||
p.title = project.title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return oldProjects;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import axios, { Axios, type AxiosResponse } from "axios";
|
||||||
|
import { backend } from "../stores/config";
|
||||||
|
import { toastAlert } from "./toasts";
|
||||||
|
|
||||||
|
export default new Axios({
|
||||||
|
...axios.defaults,
|
||||||
|
baseURL: backend + '/api',
|
||||||
|
validateStatus: () => true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function processError (response: AxiosResponse<any, any>, message: string = '') {
|
||||||
|
let title = `${response.status} ${response.statusText}`;
|
||||||
|
let subtitle = message;
|
||||||
|
|
||||||
|
console.log(response.headers)
|
||||||
|
|
||||||
|
if(response.headers["content-type"] === "application/json") {
|
||||||
|
const parsed = response.data;
|
||||||
|
subtitle = parsed.error;
|
||||||
|
if(response.data.trace) {
|
||||||
|
subtitle += '<br><br>' + parsed.trace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.get(backend + '/api/trace');
|
||||||
|
|
||||||
|
toastAlert(title, subtitle);
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { toast } from '@zerodevx/svelte-toast';
|
||||||
|
|
||||||
|
export function toastAlert(title: string, subtitle: string = '', persistant: boolean = false) {
|
||||||
|
toast.push(
|
||||||
|
`<strong>${title}</strong><br>${subtitle}`,
|
||||||
|
{
|
||||||
|
theme: {
|
||||||
|
'--toastBackground': '#ff4d4f',
|
||||||
|
'--toastBarBackground': '#d32f2f',
|
||||||
|
'--toastColor': '#fff',
|
||||||
|
},
|
||||||
|
initial: persistant ? 0 : 1,
|
||||||
|
next: 0,
|
||||||
|
duration: 10000,
|
||||||
|
pausable: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
#projects {
|
||||||
|
margin: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects li {
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 10px 0;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects .title {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects .title:hover {
|
||||||
|
background-color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects .buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects li img {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects li img:hover {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects input {
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #333;
|
||||||
|
color: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects #add {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projects #add:hover {
|
||||||
|
background-color: #303030;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 271 B |
|
@ -0,0 +1,7 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 6h18"></path>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m2-2h10a2 2 0 0 1 2 2v2H5V6a2 2 0 0 1 2-2z"></path>
|
||||||
|
<line x1="10" y1="11" x2="10" y2="17"></line>
|
||||||
|
<line x1="14" y1="11" x2="14" y2="17"></line>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 420 B |
|
@ -1,5 +1,5 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="18" viewBox="0 0 576 512">
|
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 576 512">
|
||||||
<path
|
<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"
|
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" />
|
fill="#fff" />
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 645 B After Width: | Height: | Size: 645 B |
Loading…
Reference in New Issue