Update Design

This commit is contained in:
Brieuc Dubois 2024-01-01 23:39:59 +01:00
parent cc9ddc21a0
commit e8ec7919d9
29 changed files with 986 additions and 623 deletions

View File

@ -242,7 +242,7 @@ func UpdateTagOption(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNotFound)
}
return c.SendStatus(fiber.StatusOK)
return c.SendStatus(fiber.StatusNoContent)
}
func DeleteTagOptions(c *fiber.Ctx) error {

View File

@ -9,6 +9,7 @@
"version": "0.0.1",
"dependencies": {
"axios": "^1.6.3",
"less": "^4.2.0",
"svelte-multiselect": "^10.2.0"
},
"devDependencies": {
@ -1432,6 +1433,17 @@
"node": ">= 0.6"
}
},
"node_modules/copy-anything": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
"dependencies": {
"is-what": "^3.14.1"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -1557,6 +1569,18 @@
"node": ">=6.0.0"
}
},
"node_modules/errno": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
"optional": true,
"dependencies": {
"prr": "~1.0.1"
},
"bin": {
"errno": "cli.js"
}
},
"node_modules/es6-promise": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
@ -2122,7 +2146,7 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
"devOptional": true
},
"node_modules/graphemer": {
"version": "1.4.0",
@ -2139,6 +2163,18 @@
"node": ">=8"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"optional": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ignore": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
@ -2148,6 +2184,18 @@
"node": ">= 4"
}
},
"node_modules/image-size": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
"optional": true,
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -2258,6 +2306,11 @@
"@types/estree": "*"
}
},
"node_modules/is-what": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA=="
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -2318,6 +2371,31 @@
"integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==",
"dev": true
},
"node_modules/less": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz",
"integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
"dependencies": {
"copy-anything": "^2.0.1",
"parse-node-version": "^1.0.1",
"tslib": "^2.3.0"
},
"bin": {
"lessc": "bin/lessc"
},
"engines": {
"node": ">=6"
},
"optionalDependencies": {
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"mime": "^1.4.1",
"needle": "^3.1.0",
"source-map": "~0.6.0"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@ -2389,6 +2467,28 @@
"node": ">=12"
}
},
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
"optional": true,
"dependencies": {
"pify": "^4.0.1",
"semver": "^5.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"optional": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
@ -2416,6 +2516,18 @@
"node": ">=8.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"optional": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -2528,6 +2640,22 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/needle": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.3",
"sax": "^1.2.4"
},
"bin": {
"needle": "bin/needle"
},
"engines": {
"node": ">= 4.4.x"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -2605,6 +2733,14 @@
"node": ">=6"
}
},
"node_modules/parse-node-version": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -2669,6 +2805,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/playwright": {
"version": "1.40.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.1.tgz",
@ -2850,6 +2995,12 @@
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
"optional": true
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -2989,6 +3140,12 @@
"node": ">=6"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"optional": true
},
"node_modules/sander": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz",
@ -3013,6 +3170,12 @@
"rimraf": "bin.js"
}
},
"node_modules/sax": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==",
"optional": true
},
"node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
@ -3093,6 +3256,15 @@
"sorcery": "bin/sorcery"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@ -3356,8 +3528,7 @@
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/type-check": {
"version": "0.4.0",

View File

@ -35,6 +35,7 @@
"type": "module",
"dependencies": {
"axios": "^1.6.3",
"less": "^4.2.0",
"svelte-multiselect": "^10.2.0"
}
}

View File

@ -1,8 +1,8 @@
import type { Card } from '../stores/interfaces';
import type { Card, TagValue } from '../stores/interfaces';
import api, { processError } from '../utils/api';
import status from '../utils/status';
export async function newCardApi(projectId: number): Promise<Card> {
export async function newCardApi(projectId: number, tags: TagValue[]): Promise<Card> {
const response = await api.post(`/v1/cards`, {
project_id: projectId,
title: 'Untitled',
@ -16,12 +16,14 @@ export async function newCardApi(projectId: number): Promise<Card> {
const id: number = response.data.id;
tags.forEach((tag) => (tag.card_id = id));
return {
id: id,
projectId: projectId,
title: 'Untitled',
content: '',
tags: []
tags: tags
};
}

17
frontend/src/api/tags.ts Normal file
View File

@ -0,0 +1,17 @@
import type { MeTag, TagOption } from '../stores/interfaces';
import api, { processError } from '../utils/api';
import status from '../utils/status';
export async function updateTagAPI(option: TagOption): Promise<boolean> {
const response =
option.value === ''
? await api.delete(`/v1/tags/${option.tag_id}/options/${option.id}`)
: await api.put(`/v1/tags/${option.tag_id}/options/${option.id}`, option);
if (response.status !== status.NoContent) {
processError(response, 'Failed to update tag option');
return false;
}
return true;
}

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<link rel="icon" href="/img/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="/css/index.css" />
<link rel="stylesheet" type="text/css" href="/css/global.css" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

View File

@ -1,89 +0,0 @@
<script lang="ts">
import { updateCardTagApi } from '../api/cards';
import type { Card, TagOption } from '../stores/interfaces';
import { cards, currentDraggedCard } from '../stores/smallStore';
import api, { processError } from '../utils/api';
import status from '../utils/status';
import CardC from './card.svelte';
export let option: TagOption;
export let columnCards: Card[] = [];
async function onDrop(e: DragEvent) {
e.preventDefault();
if ($currentDraggedCard && $currentDraggedCard.tags) {
for (let tag of $currentDraggedCard.tags) {
if (tag.tag_id == option.tag_id) {
try {
if (tag.option_id == option.id) return;
// DELETE
if (tag.option_id !== -1 && option.id === -1) {
const response = await api.delete(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`);
if (response.status !== status.NoContent) {
processError(response, 'Failed to delete tag');
return;
}
}
// CREATE
else if (tag.option_id == -1 && option.id !== -1) {
const response = await api.post(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
value: tag.value,
option_id: option.id
});
if (response.status !== status.Created) {
processError(response, 'Failed to create tag');
return;
}
}
// UPDATE
else {
const response = await api.put(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
value: tag.value,
option_id: option.id
});
if (response.status !== status.NoContent) {
processError(response, 'Failed to update tag');
return;
}
}
tag.option_id = option.id;
cards.reload();
} catch (e) {}
break;
}
}
currentDraggedCard.set(null);
}
}
</script>
<div
class="column"
role="listbox"
tabindex="-1"
on:drop={onDrop}
on:dragover={(e) => {
e.preventDefault();
}}
>
<h3>{option.value}</h3>
<ul>
{#each columnCards as card}
<CardC {card} />
{/each}
</ul>
</div>
<style>
h3 {
text-align: center;
}
.column {
margin: 0 10px;
width: 200px;
height: 100%;
}
</style>

View File

@ -0,0 +1,13 @@
<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="7" x2="12" y2="21"></line>
<line x1="5" y1="14" x2="19" y2="14"></line>
</svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"
><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path
opacity="1"
fill="#FFFFFF"
d="M128 32h32c17.7 0 32 14.3 32 32V96H96V64c0-17.7 14.3-32 32-32zm64 96V448c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32V388.9c0-34.6 9.4-68.6 27.2-98.3C40.9 267.8 49.7 242.4 53 216L60.5 156c2-16 15.6-28 31.8-28H192zm227.8 0c16.1 0 29.8 12 31.8 28L459 216c3.3 26.4 12.1 51.8 25.8 74.6c17.8 29.7 27.2 63.7 27.2 98.3V448c0 17.7-14.3 32-32 32H352c-17.7 0-32-14.3-32-32V128h99.8zM320 64c0-17.7 14.3-32 32-32h32c17.7 0 32 14.3 32 32V96H320V64zm-32 64V288H224V128h64z"
/></svg
>

After

Width:  |  Height:  |  Size: 752 B

View File

@ -1,65 +0,0 @@
<script lang="ts">
import ModalTags from './modal_tags.svelte';
import type { Card } from '../stores/interfaces';
import api, { processError } from '../utils/api';
import status from '../utils/status';
import { cards, currentModalCard } from '../stores/smallStore';
import TrashIcon from './icons/trashIcon.svelte';
import CloseIcon from './icons/closeIcon.svelte';
export let card: Card;
let tempCard: Card = { ...card };
async function save(closeModal: boolean = true) {
if (
card.projectId != tempCard.projectId ||
card.title !== tempCard.title ||
card.content !== tempCard.content
) {
const response = await api.put(`/v1/cards/${card.id}`, {
project_id: tempCard.projectId,
title: tempCard.title,
content: tempCard.content
});
if (response.status !== status.NoContent) {
processError(response, 'Failed to update card');
return;
}
card = { ...tempCard };
}
if (closeModal) currentModalCard.set(-1);
}
</script>
{#if $currentModalCard == card.id}
<!-- 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={() => cards.remove(card)}>
<TrashIcon />
</button>
<button on:click={() => currentModalCard.set(-1)}>
<CloseIcon />
</button>
</div>
</div>
<div class="tags">
<ModalTags bind:card />
</div>
<div class="body">
<textarea
bind:value={tempCard.content}
placeholder="Add a description"
on:blur={() => save(false)}
/>
</div>
</div>
</div>
{/if}

View File

@ -1,108 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { type Project, type Card, type TagOption, type View } from '../stores/interfaces';
import projectTags from '../stores/projectTags';
import { getProjectAPI } from '../api/projects';
import Column from './column.svelte';
import { cards, currentView } from '../stores/smallStore';
export let projectId: number;
let project: Project;
// let cards: Card[] = [];
let view: View | null = null;
let columns: { id: number; title: string; cards: Card[] }[] = [];
onMount(async () => {
getProjectAPI(projectId).then((p) => {
project = p;
});
cards.init(projectId);
cards.subscribe((c) => {
loadColumns();
});
if (!(await projectTags.init(projectId))) {
return;
}
currentView.subscribe((v) => {
view = v;
loadColumns();
});
});
function loadColumns() {
if (!view) return;
let primary_tag_id = view.primary_tag_id;
columns =
$projectTags[primary_tag_id]?.options.map((o) => {
return {
id: o.id,
title: o.value,
cards: $cards?.filter((c) => c.tags.map((t) => t.option_id).includes(o.id)) || []
};
}) || [];
columns.push({
id: -1,
title: 'No tag',
cards:
$cards?.filter((c) => {
const tag = c.tags.find((t) => t.tag_id === primary_tag_id);
return tag?.option_id == -1;
}) || []
});
}
async function newCard() {
await cards.add(projectId);
}
</script>
<svelte:head>
<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>
{#if project}
<section>
<header>
<h2>{project.title}</h2>
<button on:click={newCard}>New card</button>
</header>
{#if view && $projectTags[view.primary_tag_id] && $cards}
<div class="grid">
{#each $projectTags[view.primary_tag_id].options as option}
<Column
{option}
columnCards={$cards.filter((c) => c.tags.map((t) => t.option_id).includes(option.id))}
/>
{/each}
<Column
option={{
id: -1,
tag_id: view.primary_tag_id,
value: `No ${$projectTags[view.primary_tag_id].title}`
}}
columnCards={$cards.filter((c) => c.tags.find((t) => t.tag_id)?.option_id == -1 || false)}
/>
</div>
{/if}
</section>
{/if}
<style>
section {
display: flex;
flex-direction: column;
}
.grid {
display: flex;
flex-direction: row;
flex: 1;
}
</style>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { Card } from '../stores/interfaces';
import projectTags from '../stores/projectTags';
import { currentDraggedCard, currentModalCard } from '../stores/smallStore';
import type { Card } from '../../../stores/interfaces';
import projectTags from '../../../stores/projectTags';
import { currentModalCard, currentDraggedCard } from '../../../stores/smallStore';
import ModalCard from './modal_card.svelte';
export let card: Card;
@ -40,3 +40,36 @@
</div>
<ModalCard bind:card />
<style lang="less">
.card {
padding: 10px;
border-radius: 6px;
border: 1px solid #555;
font-family: 'Open Sans', sans-serif;
font-size: 14px;
margin-bottom: 10px;
}
.card:hover {
background-color: #303030;
cursor: pointer;
}
.card .title {
font-weight: normal;
}
.card .tags {
padding-top: 10px;
font-weight: lighter;
}
.card .tag {
padding: 2px 8px;
margin: 4px 4px 0 0;
text-transform: uppercase;
border-radius: 3px;
font-size: 90%;
}
</style>

View File

@ -0,0 +1,171 @@
<script lang="ts">
import type { Card } from '../../../stores/interfaces';
import { currentModalCard, cards } from '../../../stores/smallStore';
import api, { processError } from '../../../utils/api';
import status from '../../../utils/status';
import CloseIcon from '../../icons/closeIcon.svelte';
import TrashIcon from '../../icons/trashIcon.svelte';
import ModalTags from './modal_tags.svelte';
export let card: Card;
let tempCard: Card = { ...card };
async function save(closeModal: boolean = true) {
if (
card.projectId != tempCard.projectId ||
card.title !== tempCard.title ||
card.content !== tempCard.content
) {
const response = await api.put(`/v1/cards/${card.id}`, {
project_id: tempCard.projectId,
title: tempCard.title,
content: tempCard.content
});
if (response.status !== status.NoContent) {
processError(response, 'Failed to update card');
return;
}
card = { ...tempCard };
}
if (closeModal) currentModalCard.set(-1);
}
</script>
{#if $currentModalCard == card.id}
<!-- 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={() => cards.remove(card)}>
<TrashIcon />
</button>
<button on:click={() => currentModalCard.set(-1)}>
<CloseIcon />
</button>
</div>
</div>
<div class="tags">
<ModalTags bind:card />
</div>
<div class="body">
<textarea
bind:value={tempCard.content}
placeholder="Add a description"
on:blur={() => save(false)}
/>
</div>
</div>
</div>
{/if}
<style lang="less">
.content {
}
.modal {
background-color: rgba(0, 0, 0, 0.8);
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
.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;
}
</style>

View File

@ -1,11 +1,12 @@
<script lang="ts">
import type { TagValue } from '../stores/interfaces';
import projectTags from '../stores/projectTags';
import api, { processError } from '../utils/api';
import status from '../utils/status';
import type { TagValue } from '../../../stores/interfaces';
import projectTags from '../../../stores/projectTags';
import { cards } from '../../../stores/smallStore';
import api, { processError } from '../../../utils/api';
import status from '../../../utils/status';
export let tag: TagValue;
export let removeTag: (id: number) => void;
// export let removeTag: (id: number) => void;
let newValue: string = tag.value;
let newOption: number = tag.option_id;
@ -47,6 +48,7 @@
tag.value = newValue;
tag.option_id = newOption;
cards.reload();
}
async function newTagOption() {

View File

@ -1,8 +1,8 @@
<script lang="ts">
import type { Card } from '../../../stores/interfaces';
import api, { processError } from '../../../utils/api';
import status from '../../../utils/status';
import ModalTag from './modal_tag.svelte';
import status from '../utils/status';
import type { Card } from '../stores/interfaces';
import api, { processError } from '../utils/api';
export let card: Card;
@ -41,7 +41,7 @@
<table>
{#if card.tags}
{#each card.tags as tag}
<ModalTag bind:tag {removeTag} />
<ModalTag bind:tag />
{/each}
{/if}
<tr class="tag">

View File

@ -0,0 +1,199 @@
<script lang="ts">
import type { TagOption, Card, MeTag, TagValue } from '../../stores/interfaces';
import { cards, currentDraggedCard } from '../../stores/smallStore';
import api, { processError } from '../../utils/api';
import status from '../../utils/status';
import CardC from './card/card.svelte';
import AddIcon from '../icons/addIcon.svelte';
import projectTags from '../../stores/projectTags';
import { updateTagAPI as updateTagOptionAPI } from '../../api/tags';
import { get } from 'svelte/store';
export let projectId: number;
export let editable: boolean = true;
export let option: TagOption;
export let columnCards: Card[] = [];
let lastOptionValue = option.value;
async function onDrop(e: DragEvent) {
e.preventDefault();
if ($currentDraggedCard && $currentDraggedCard.tags) {
for (let tag of $currentDraggedCard.tags) {
if (tag.tag_id == option.tag_id) {
try {
if (tag.option_id == option.id) return;
// DELETE
if (tag.option_id !== -1 && option.id === -1) {
const response = await api.delete(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`);
if (response.status !== status.NoContent) {
processError(response, 'Failed to delete tag');
return;
}
}
// CREATE
else if (tag.option_id == -1 && option.id !== -1) {
const response = await api.post(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
value: tag.value,
option_id: option.id
});
if (response.status !== status.Created) {
processError(response, 'Failed to create tag');
return;
}
}
// UPDATE
else {
const response = await api.put(`/v1/cards/${tag.card_id}/tags/${tag.tag_id}`, {
value: tag.value,
option_id: option.id
});
if (response.status !== status.NoContent) {
processError(response, 'Failed to update tag');
return;
}
}
tag.option_id = option.id;
cards.reload();
} catch (e) {}
break;
}
}
currentDraggedCard.set(null);
}
}
async function addCard() {
const tags: TagValue[] = [];
for (let tag of Object.values(get(projectTags))) {
tags.push({
card_id: -1,
tag_id: tag.id,
option_id: tag.id === option.tag_id ? option.id : -1,
value: ''
});
}
await cards.add(projectId, tags);
}
</script>
<div
class="column"
role="listbox"
tabindex="-1"
on:drop={onDrop}
on:dragover={(e) => {
e.preventDefault();
}}
>
<header>
<input
bind:value={option.value}
type="text"
on:blur={async () => {
if (lastOptionValue === option.value) return;
await updateTagOptionAPI(option);
lastOptionValue = option.value;
cards.reload();
}}
disabled={!editable}
/>
<span>
<span>{columnCards.length}</span>
<span
class="add"
on:click={addCard}
role="button"
tabindex="0"
on:keypress={(e) => {
if (e.key === 'Enter') {
addCard();
}
}}
>
<AddIcon />
</span>
</span>
</header>
<ul>
{#each columnCards as card}
<CardC {card} />
{/each}
</ul>
<div
class="addEnd"
on:click={addCard}
role="button"
tabindex="0"
on:keypress={(e) => {
if (e.key === 'Enter') {
addCard();
}
}}
>
+
</div>
</div>
<style lang="less">
.column {
margin: 20px 10px;
width: 250px;
}
header {
margin-bottom: 20px;
display: flex;
flex-direction: row;
justify-content: space-between;
input {
background-color: #444;
flex: 1;
margin-right: 10px;
padding: 3px 10px;
border-radius: 3px;
overflow: hidden;
color: inherit;
border: none;
}
:last-child {
display: flex;
}
.add {
cursor: pointer;
border-radius: 4px;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
margin-left: 10px;
&:hover {
background-color: #fff1;
}
}
}
.addEnd {
cursor: pointer;
border-radius: 4px;
height: 40px;
font-size: 150%;
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px;
margin-bottom: 10px;
&:hover {
background-color: #fff1;
}
}
</style>

View File

@ -0,0 +1,76 @@
<script lang="ts">
import { get } from 'svelte/store';
import type { Project, TagValue } from '../../stores/interfaces';
import { cards } from '../../stores/smallStore';
import projectTags from '../../stores/projectTags';
export let project: Project;
function getEmptyTags(): TagValue[] {
const tags: TagValue[] = [];
for (let tag of Object.values(get(projectTags))) {
tags.push({
card_id: -1,
tag_id: tag.id,
option_id: -1,
value: ''
});
}
return tags;
}
</script>
<header>
<h2>{project.title}</h2>
<nav>
<span>Group</span>
<span>Sub-group</span>
<span>Filter</span>
<span>Sort</span>
<button on:click={async () => cards.add(project.id, getEmptyTags())}>New</button>
</nav>
</header>
<style lang="less">
header {
padding: 20px 0;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 2px solid #444;
}
h2 {
font-size: 40px;
}
nav {
* {
cursor: pointer;
}
span {
margin-right: 10px;
color: #aaa;
padding: 5px 10px;
border-radius: 7px;
&:hover {
// background-color: #fff2;
}
}
button {
background: #324067;
color: inherit;
border: none;
border-radius: 10px;
padding: 10px 20px;
font-size: inherit;
&:hover {
// background-color: #3a4a77;
}
}
}
</style>

View File

@ -0,0 +1,66 @@
<script lang="ts">
import { onMount } from 'svelte';
import Column from './column.svelte';
import type { Project, View } from '../../stores/interfaces';
import projectTags from '../../stores/projectTags';
import { cards, currentView } from '../../stores/smallStore';
import Header from './header.svelte';
export let project: Project;
let view: View | null = null;
onMount(async () => {
await cards.init(project.id);
if (!(await projectTags.init(project.id))) {
return;
}
currentView.subscribe((v) => {
view = v;
});
});
</script>
{#if project}
<section>
<Header {project} />
{#if view && $projectTags[view.primary_tag_id] && $cards}
<div class="grid">
{#each $projectTags[view.primary_tag_id].options as option}
<Column
{option}
columnCards={$cards.filter((c) => c.tags.map((t) => t.option_id).includes(option.id))}
projectId={project.id}
/>
{/each}
<Column
option={{
id: -1,
tag_id: view.primary_tag_id,
value: `No ${$projectTags[view.primary_tag_id].title}`
}}
columnCards={$cards.filter((c) => c.tags.find((t) => t.tag_id)?.option_id == -1 || false)}
projectId={project.id}
editable={false}
/>
</div>
{/if}
</section>
{/if}
<style>
section {
display: flex;
flex-direction: column;
width: 100%;
margin: 0 40px;
}
.grid {
display: flex;
flex-direction: row;
flex: 1;
}
</style>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { Project } from '../stores/interfaces';
import api, { processError } from '../utils/api';
import status from '../utils/status';
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;
@ -94,3 +94,50 @@
/>
</div>
</li>
<style lang="less">
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;
}
.title {
font-weight: bold;
padding: 20px;
width: 100%;
}
.title:hover {
background-color: #303030;
}
.buttons {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
li img {
padding: 20px;
}
li img:hover {
background-color: #333;
}
input {
padding: 20px;
width: 100%;
background-color: #333;
color: inherit;
font-weight: bold;
font-size: inherit;
}
</style>

View File

@ -1,14 +1,15 @@
<script lang="ts">
import { onMount } from 'svelte';
import api, { processError } from '../utils/api';
import type { View } from '../stores/interfaces';
import type { Project, View } from '../stores/interfaces';
import { currentView } from '../stores/smallStore';
import ViewIcon from './icons/viewIcon.svelte';
export let projectID: number;
export let project: Project;
let views: View[];
onMount(async () => {
const response = await api.get(`/v1/projects/${projectID}/views`);
const response = await api.get(`/v1/projects/${project.id}/views`);
if (response.status !== 200) {
processError(response, 'Failed to fetch views');
@ -21,30 +22,95 @@
});
</script>
<link rel="stylesheet" type="text/css" href="/css/sidebar.css" />
<nav>
<div>
<div id="branding">
<span id="title">Focus.</span>
<span id="version">v0.0.1</span>
</div>
<div id="views">
{#if views}
<h2>{project.title}</h2>
<ul>
{#each views as view}
<li>
<ViewIcon />
<!-- on:click={() => {
currentView.set(view);
}} -->
<span>
{view.title}
</span>
</li>
{/each}
</ul>
{/if}
</div>
</div>
<div>
<div class="separator"></div>
<div id="newView" on:click={() => {}}>+ New view</div>
</div>
</nav>
<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">
<h2>Projects</h2>
{#if views}
<ul>
{#each views as view}
<li>
<span
on:click={() => {
currentView.set(view);
}}>{view.title}</span
>
</li>
{/each}
</ul>
{/if}
</div>
</div>
<style lang="less">
nav {
min-width: 300px;
background-color: #273049;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 22px;
}
#branding {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 20px;
#title {
font-size: 40px;
}
#version {
font-size: 30px;
color: #aaa;
}
}
#views {
h2 {
font-weight: normal;
font-size: 25px;
padding: 20px 10px;
}
ul {
font-size: 22px;
padding: 10px;
}
span {
padding-left: 10px;
}
}
.separator {
height: 2px;
widows: 100%;
background-color: #444;
}
#newView {
text-align: center;
padding: 20px 0;
}
#newView:hover {
cursor: pointer;
background-color: #fff1;
}
</style>

View File

@ -3,7 +3,7 @@
import type { Project } from '../stores/interfaces';
import { onMount } from 'svelte';
import api, { processError } from '../utils/api';
import SelectProject from '../components/selectProject.svelte';
import SelectProject from '../components/projects/selectProject.svelte';
let projects: Project[];
@ -46,11 +46,7 @@
}
</script>
<svelte:head>
<link rel="stylesheet" type="text/css" href="/css/projects.css" />
</svelte:head>
<section id="projects">
<section>
<h2>Projects</h2>
<ul>
{#if projects}
@ -75,3 +71,30 @@
</section>
<SvelteToast />
<style lang="less">
section {
margin: 40px;
}
h2 {
text-align: center;
margin-bottom: 40px;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
#add {
width: 100%;
display: flex;
justify-content: center;
padding: 20px 0;
cursor: pointer;
}
#add:hover {
background-color: #303030;
}
</style>

View File

@ -1,23 +1,33 @@
<script lang="ts">
import Project from '../../components/project.svelte';
import Sidebar from '../../components/sidebar.svelte';
import { page } from '$app/stores';
import { SvelteToast } from '@zerodevx/svelte-toast';
import type { View } from '../../stores/interfaces';
import { onMount } from 'svelte';
import { getProjectAPI } from '../../api/projects';
import { type Project as P } from '../../stores/interfaces';
import Sidebar from '../../components/sidebar.svelte';
import Project from '../../components/project/project.svelte';
let projectID: number = +$page.params.project;
let projectId: number = +$page.params.project;
let currentView: View;
let project: P;
onMount(() => {
getProjectAPI(projectId).then((p) => {
project = p;
});
});
</script>
<div id="projectPage">
<Sidebar {projectID} />
<Project projectId={projectID} />
</div>
<SvelteToast />
{#if project}
<div>
<Sidebar {project} />
<Project {project} />
</div>
<SvelteToast />
{/if}
<style>
#projectPage {
div {
display: flex;
}
</style>

View File

@ -1,5 +1,5 @@
import { writable } from 'svelte/store';
import { parseCards, type Card, type View } from './interfaces';
import { parseCards, type Card, type View, type TagValue } from './interfaces';
import { deleteCardApi, newCardApi } from '../api/cards';
import { getProjectCardsAPI } from '../api/projects';
@ -19,10 +19,10 @@ export const cards = (() => {
set(parseCards(c));
});
},
add: async (projectId: number) => {
await newCardApi(projectId).then((card) => {
currentModalCard.set(card.id);
add: async (projectId: number, tags: TagValue[]) => {
await newCardApi(projectId, tags).then((card) => {
update((cards) => [...cards, card]);
currentModalCard.set(card.id);
});
},
remove: async (card: Card) => {

View File

@ -1,31 +0,0 @@
.card {
padding: 10px;
border-radius: 6px;
border: 1px solid #555;
margin: 10px;
/* width: 200px; */
font-family: "Open Sans", sans-serif;
font-size: 14px;
}
.card:hover{
background-color: #303030;
cursor: pointer;
}
.card .title {
font-weight: normal;
}
.card .tags {
padding-top: 10px;
font-weight: lighter;
}
.card .tag {
padding: 2px 8px;
margin: 4px 4px 0 0;
text-transform: uppercase;
border-radius: 3px;
font-size: 90%;
}

View File

@ -8,3 +8,12 @@ body {
color: white;
background-color: #252525;
}
a {
text-decoration: none;
color: inherit;
}
ul {
list-style: none;
}

View File

@ -1,100 +0,0 @@
.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;
}

View File

@ -1,3 +0,0 @@
#project {
padding: 10px 20px;
}

View File

@ -1,72 +0,0 @@
#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;
}

View File

@ -1,82 +0,0 @@
#sidebar {
width: 240px;
background-color: #262a2b;
color: white;
height: 100vh;
display: flex;
flex-direction: column;
float: left;
}
#sidebar .logo {
display: flex;
align-items: center;
padding: 10px;
margin-top: 20px;
}
#sidebar .logo img {
max-height: 30px;
margin-right: 10px;
}
#sidebar .logo .title {
padding-right: 10px;
}
#sidebar .logo .version {
font-size: 0.8em;
opacity: 0.7;
}
#sidebar .boards h2 {
padding-left: 10px;
font-size: 0.9em;
margin-top: 20px;
margin-bottom: 10px;
}
#sidebar .boards ul {
list-style: none;
padding: 0;
margin: 0;
}
#sidebar a {
text-decoration: none;
color: white;
}
#sidebar .boards ul li:hover {
background-color: #444;
}
#sidebar .bottom-links {
margin-top: auto;
}
#sidebar .bottom-links span {
text-decoration: none;
color: white;
display: block;
padding: 10px;
font-size: 0.9em;
border-top: 1px solid #444;
cursor: pointer;
}
#sidebar li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
}
#sidebar .edit-icon {
visibility: hidden;
margin-right: 10px;
cursor: pointer;
}
#sidebar li:hover .edit-icon {
visibility: visible;
}